我正在尝试了解Linux系统调用机制。我正在读一本书,书中说退出函数看起来像这样(使用 gdb):
mov $0x0,%ebx
mov $0x1,%eax
80 int $0x80
我知道这是一个退出的系统调用,但在我的 Debian 中它看起来像这样:
jmp *0x8049698
push $0x8
jmp 0x80482c0
也许有人可以解释我为什么不一样?当我尝试在 0x80482c0 上执行 disas 时,gdb 打印出:
没有函数包含指定的地址。
谢谢!
答案1
您的代码的exit()
调用最终会链接到 C 库 (libc) 函数exit()
,该函数实际上可能不会执行int $0x80
.
代码对exit()
函数的调用实际上被编译 call
为程序链接表(PLT)中的指令。运行时动态链接器负责将文件映射/usr/lib/libc.so
到内存中。那是C 库。运行时动态链接器还修复 PLT 中的条目,最终调用 from 映射的代码/usr/lib/libc.so
。
据我所知(我使用的是 Arch linux),你的第二个 3 条指令是 PLT 条目,gdb
当我单步进入它时,它会调用“exit@plt”。跳转jmp 0x80482c0
到另一个地址,最终跳转到libc.so
代码中。
您可以通过相当复杂的练习向自己证明这一点。首先,您已经获得了 PLT 表条目的地址,无论gdb
告诉您的是jmp *0x8049698
“exit@plt”的地址。在我的 x86 Arch linux 机器上:
(gdb) disassemble 0x8048310,+20
Dump of assembler code from 0x8048310 to 0x8048324:
0x08048310 <exit@plt+0>: jmp *0x80496e8
0x08048316 <exit@plt+6>: push $0x10
0x0804831b <exit@plt+11>: jmp 0x80482e0
然后做readelf -e _program_ > elf.headers
。查看文件elf.headers
。您会发现一行文本,上面写着“节标题:”在节标题中的某个位置,您会看到类似这样的内容:
[ 9] .rel.dyn REL 08048290 000290 000008 08 A 5 0 4
[10] .rel.plt REL 08048298 000298 000020 08 AI 5 12 4
[11] .init PROGBITS 080482b8 0002b8 000023 00 AX 0 0 4
[12] .plt PROGBITS 080482e0 0002e0 000050 04 AX 0 0 16
“exit@plt”位于地址 0x8048310。就在“.rel.plt”部分。 “.rel.plt”可能代表“重定位程序链接表”。
int $0x80
现在我们到达了可能不存在的部分。做ldd _program_
。同样,Arch linux x86 是这样说的:
linux-gate.so.1 (0xb77d9000)
libc.so.6 => /usr/lib/libc.so.6 (0xb7603000)
/lib/ld-linux.so.2 (0xb77da000)
看到那个“linux-gate.so.1”了吗?它包含执行系统调用的实际代码。它可能是int $0x80
,或者可能是sysenter
指令,或者可能是其他东西。 Linux 内核应该将一个“小型共享库”与实际代码一起放入进程的地址空间中,然后将该小型共享库的地址移交给 ELF“辅助向量”。做man vdso
一些细节。动态链接器/lib/ld-linux.so.2
知道 ELF 辅助向量的详细信息,并最终将地址linx-gate.so.1
放入 PLT 中的某个位置,因此实际的 C 函数调用最终可以进行高效的系统调用。
如果您多次调用ldd _program_
,您将看到 的地址linux-gate.so.1
在每次调用时都不相同。实际上,内核不会每次都将堆栈顶部放在同一地址,以试图迷惑需要知道堆栈位置以执行自己的代码的恶意软件。