初めに
x86のLinux32ビットのシステムコール呼び出しにはINT 0x80
が使われていました。
筆者は32ビットの端末は所有していないため、64ビットのLinuxから32ビットのプログラムを動かし、INT
命令がどのような仕組みになっているのかを調べます。
test@test-ThinkPad-X280:~/test/nasm$ nasm -f elf32 test.asm && ld -m elf_i386 test.o -o test
test@test-ThinkPad-X280:~/test/nasm$ ./test
Hello, int80!
section .data
msg db "Hello, int80!", 0xA
len equ $ - msg
section .text
global _start
_start:
mov eax, 4 ; sys_write
mov ebx, 1 ; stdout
mov ecx, msg
mov edx, len
int 0x80
mov eax, 1 ; sys_exit
xor ebx, ebx
int 0x80
IDTについて
INT0x80
の割り込みは割り込み記述子(IDT)の128(=0x80)番目に登録されている。
先ずはIDTが保存されている先頭アドレス及び、全体の大きさを調べます。
ユーザモード(Ring 3)では取得ができないため、カーネルモード(Ring 0)で調べます。
obj-m := idt_show.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
#include <linux/module.h>
#include <asm/desc.h>
static int __init idt_show(void)
{
struct desc_ptr idtr;
store_idt(&idtr);
pr_info("IDT base = 0x%lx, limit = 0x%x\n",
(unsigned long)idtr.address, idtr.size);
return 0;
}
static void __exit idt_exit(void)
{
pr_info("idt_show unloaded\n");
}
module_init(idt_show);
module_exit(idt_exit);
MODULE_LICENSE("GPL");
test@test-ThinkPad-X280:~/test/nasm2$ make
make -C /lib/modules/6.8.0-85-generic/build M=/home/test/test/nasm modules
make[1]: 进入目录“/usr/src/linux-headers-6.8.0-85-generic”
warning: the compiler differs from the one used to build the kernel
The kernel was built by: x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04.2) 12.3.0
You are using: gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04.2) 12.3.0
CC [M] /home/test/test/nasm/idt_show.o
MODPOST /home/test/test/nasm/Module.symvers
CC [M] /home/test/test/nasm/idt_show.mod.o
LD [M] /home/test/test/nasm/idt_show.ko
BTF [M] /home/test/test/nasm/idt_show.ko
Skipping BTF generation for /home/test/test/nasm/idt_show.ko due to unavailability of vmlinux
make[1]: 离开目录“/usr/src/linux-headers-6.8.0-85-generic”
test@test-ThinkPad-X280:~/test/nasm$ sudo insmod idt_show.ko
test@test-ThinkPad-X280:~/test/nasm$ sudo dmesg | tail -n 3
[ 9440.007193] idt_show: loading out-of-tree module taints kernel.
[ 9440.007199] idt_show: module verification failed: signature and/or required key missing - tainting kernel
[ 9440.009049] IDT base = 0xfffffe0000000000, limit = 0xfff
test@test-ThinkPad-X280:~/test$ sudo rmmod show_int80_handler
上で調べたところIDTの先頭アドレスは0xfffffe0000000000
大きさは0x0fff
= 4095
。4096バイト = 256エントリ × 16バイト(64bit環境では)
よってIDTは以下のように並んでいる。
IDT base = 0xfffffe0000000000
↓
0xfffffe0000000000 ─ [0x00] Divide Error
0xfffffe0000000010 ─ [0x01] Debug
...
0xfffffe0000000800 ─ [0x80] INT 0x80 ←★ここ
...
0xfffffe0000000ff0 ─ [0xFF] 最後のエントリ
IDT[0x80] のアドレス
= IDT base + (割り込み番号 × 1エントリの大きさ)
= 0xfffffe0000000000 + (0x80 × 0x10)
= 0xfffffe0000000000 + 0x800
= 0xfffffe0000000800
INT0x80のアドレス
Linuxのカーネルを見ると1エントリはこのように16バイトになっている。
https://github.com/torvalds/linux/blob/master/arch/x86/include/asm/desc_defs.h
//...(省略)...
struct idt_bits {
u16 ist : 3,
zero : 5,
type : 5,
dpl : 2,
p : 1;
} __attribute__((packed));
//...(省略)...
struct gate_struct {
u16 offset_low;
u16 segment;
struct idt_bits bits;
u16 offset_middle;
#ifdef CONFIG_X86_64
u32 offset_high;
u32 reserved;
#endif
} __attribute__((packed));
//...(省略)...
このエントリ内の情報を基に以下の式を用いて64ビットの仮想アドレスを組み立てられる。
handler_address = offset_low
| (offset_middle << 16)
| ((u64)offset_high << 32)
obj-m := show_int80_handler.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
#include <linux/module.h>
#include <asm/desc.h>
static int __init show_int80_handler(void)
{
struct desc_ptr idtr;
struct gate_struct *idt;
// IDT base を取得
store_idt(&idtr);
idt = (struct gate_struct *)idtr.address;
// INT 0x80 のハンドラアドレスを組み立てる
u64 handler = (u64)idt[0x80].offset_low
| ((u64)idt[0x80].offset_middle << 16)
| ((u64)idt[0x80].offset_high << 32);
pr_info("INT 0x80 handler: 0x%llx\n", handler);
return 0;
}
static void __exit exit_func(void)
{
pr_info("Module unloaded\n");
}
module_init(show_int80_handler);
module_exit(exit_func);
MODULE_LICENSE("GPL");
test@test-ThinkPad-X280:~/test/nasm2$ make
make -C /lib/modules/6.8.0-85-generic/build M=/home/test/test/nasm2 modules
make[1]: 进入目录“/usr/src/linux-headers-6.8.0-85-generic”
warning: the compiler differs from the one used to build the kernel
The kernel was built by: x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04.2) 12.3.0
You are using: gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04.2) 12.3.0
make[1]: 离开目录“/usr/src/linux-headers-6.8.0-85-generic”
test@test-ThinkPad-X280:~/test/nasm2$ sudo insmod show_int80_handler.ko
test@test-ThinkPad-X280:~/test/nasm2$ sudo dmesg | tail -n 1
[13446.441809] INT 0x80 handler: 0xffffffffb6e00bd0
test@test-ThinkPad-X280:~/test/nasm2$
test@test-ThinkPad-X280:~/test$ sudo rmmod show_int80_handler
よってint 0x80
の入り口のアドレスは以下だと分かる。(仮想アドレス)
INT 0x80 handler: 0xffffffffb6e00bd0
int 0x80冒頭をダンプしてみる
obj-m := show_int80_handler.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init dump_int80(void)
{
unsigned char *p = (unsigned char *)0xffffffffb6e00bd0;
int i;
pr_info("INT 0x80 dump:\n");
for (i = 0; i < 64; i++) {
pr_cont("%02x ", p[i]);
if ((i+1) % 16 == 0) pr_cont("\n");
}
return 0;
}
static void __exit dump_exit(void)
{
pr_info("Module unloaded\n");
}
module_init(dump_int80);
module_exit(dump_exit);
MODULE_LICENSE("GPL");
test@test-ThinkPad-X280:~/test$ make
make -C /lib/modules/6.8.0-85-generic/build M=/home/test/test modules
make[1]: 进入目录“/usr/src/linux-headers-6.8.0-85-generic”
warning: the compiler differs from the one used to build the kernel
The kernel was built by: x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04.2) 12.3.0
You are using: gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04.2) 12.3.0
CC [M] /home/test/test/dump_int80.o
MODPOST /home/test/test/Module.symvers
CC [M] /home/test/test/dump_int80.mod.o
LD [M] /home/test/test/dump_int80.ko
BTF [M] /home/test/test/dump_int80.ko
Skipping BTF generation for /home/test/test/dump_int80.ko due to unavailability of vmlinux
make[1]: 离开目录“/usr/src/linux-headers-6.8.0-85-generic”
test@test-ThinkPad-X280:~/test$ sudo insmod dump_int80.ko
test@test-ThinkPad-X280:~/test$ sudo dmesg | tail -n 5
[14456.175446] INT 0x80 dump:
[14456.175450] 0f 01 ca fc 6a ff e8 d5 09 00 00 48 89 c4 48 8d
[14456.175465] 6c 24 01 48 89 e7 e8 c5 0f 00 00 e9 10 0b 00 00
[14456.175479] 0f 01 ca fc 6a ff f6 44 24 10 03 75 12 e8 4e 08
[14456.175492] 00 00 48 89 e7 e8 26 15 e2 ff e9 31 09 00 00 e8
test@test-ThinkPad-X280:~/test$ sudo rmmod dump_int80
int0x80
を逆アセンブルすると以下の通り
オフセット | バイト列 | アセンブリ命令 | 注釈 |
---|---|---|---|
0x00 | 0f 01 ca | swapgs | 64bit専用、カーネルスタック切替用 |
0x03 | fc | cld | 方向フラグクリア |
0x04 | 6a ff | push 0xFF | |
0x06 | e8 d5 09 00 00 | call 0x9D5 | 相対アドレス |
0x0B | 48 89 c4 | mov rsp, rax | |
0x0E | 48 8d 6c 24 01 | lea rbp, [rsp+0x1] | |
0x13 | 48 89 e7 | mov rdi, rsp | |
0x16 | e8 c5 0f 00 00 | call 0xFC5 | |
0x1B | e9 10 0b 00 00 | jmp 0xB2B | 相対ジャンプ |
0x20 | 0f 01 ca | swapgs | カーネル用の再切替 |
0x23 | fc | cld | |
0x24 | 6a ff | push 0xFF | |
0x26 | f6 44 24 10 03 | test BYTE PTR [rsp+0x10], 0x3 | |
0x2B | 75 12 | jnz 0x2F | テスト結果がゼロでなければジャンプ |
0x2D | e8 4e 08 00 00 | call 0x84E | |
0x32 | 48 89 e7 | mov rdi, rsp | |
0x35 | e8 26 15 e2 ff | call 0xFFE21561 | 相対アドレス |
0x3A | e9 31 09 00 00 | jmp 0x970 | 相対ジャンプ |
0x3F | e8 | call (途中) | バイト列不足で未完 |
考察
INT 0x80は、**割り込み記述子(IDT)**の128番目のエントリ(0xfffffe0000000800)を参照します。
このエントリが指す実際のカーネル内の処理開始アドレスは、本環境では0xffffffffb6e00bd0
でした。