系统调用与函数调用
我的意思是,我知道它们是系统调用,printf 和 scanf 也是 C 函数。
许多 C 库函数是系统调用的包装器。printf和scanf是就是这样的例子。但是,不应假设所有 C 库函数都执行系统调用,因为string.h包括 在内的库函数均不strcmp执行任何系统调用。
系统调用是进入内核的受控入口点,允许进程请求内核代表进程执行某些操作。内核通过系统调用应用程序编程接口 (API) 使程序可以访问一系列服务。1
进行系统调用的机制与进行函数调用的机制完全不同:
[C library] 包装函数执行陷阱机器指令 ( int 0x80),这会导致处理器从用户模式切换到内核模式并执行0x80系统陷阱向量的位置(十进制 128)所指向的代码。
较新的 x86-32 体系结构实现了该sysenter指令,与传统的 int 0x80trap 指令相比,它提供了一种更快的进入内核模式的方法。sysenter2.6 内核和 glibc 2.3.2 以后支持使用。1
这是execve正在执行的 C 库函数的可视化描述,其中execve进行系统调用:

x86 调用约定
当一个函数被调用时,控制流通过call指令分支到内存中的不同位置:
将过程链接信息保存在堆栈上并分支到目标(目标)操作数指定的过程(被调用的过程)。目标操作数指定被调用过程中第一条指令的地址。该操作数可以是立即数、通用寄存器或内存位置。2
下面是一些简单的示例代码:
0804841d <main>:
804841d: 55 push %ebp
804841e: 89 e5 mov %esp,%ebp
8048420: 83 e4 f0 and $0xfffffff0,%esp
8048423: 83 ec 20 sub $0x20,%esp
8048426: c7 44 24 18 f0 84 04 movl $0x80484f0,0x18(%esp)
804842d: 08
804842e: c7 44 24 1c 04 00 00 movl $0x4,0x1c(%esp)
8048435: 00
8048436: 8b 44 24 18 mov 0x18(%esp),%eax
804843a: 89 44 24 08 mov %eax,0x8(%esp) <--- argument 3
804843e: 8b 44 24 1c mov 0x1c(%esp),%eax
8048442: 89 44 24 04 mov %eax,0x4(%esp) <--- argument 2
8048446: c7 04 24 0a 85 04 08 movl $0x804850a,(%esp) <--- argument 1
804844d: e8 9e fe ff ff call 80482f0 <printf@plt> <--- function call
8048452: b8 00 00 00 00 mov $0x0,%eax
8048457: c9 leave
8048458: c3 ret
这里,执行分支到 whenprintf被调用的内存地址call是0x80482f0。
但我的问题是他们从哪里获得参数?
在函数调用之前,参数按照与函数定义中的相应参数相反的顺序压入堆栈。返回值保存在%eax. 这符合 x86 调用约定,称为cdecl:
来电规则
要进行子路由调用,调用者应该:
在调用子程序之前,调用者应该保存指定调用者保存的某些寄存器的内容。调用者保存的寄存器是 EAX、ECX、EDX。由于允许被调用的子程序修改这些寄存器,如果子程序返回后调用者依赖它们的值,则调用者必须将这些寄存器中的值压入堆栈(以便子程序返回后可以恢复它们)。
要将参数传递给子例程,请在调用之前将它们压入堆栈。参数应按倒序推送(即最后一个参数在前)。由于堆栈向下增长,第一个参数将存储在最低地址(这种参数反转在历史上用于允许函数传递可变数量的参数)。
要调用子程序,请使用 call 指令。该指令将返回地址放在堆栈上的参数顶部,并跳转到子程序代码。这将调用子程序,它应该遵循下面的被调用者规则。
在子程序返回后(紧跟在 call 指令之后),调用者可以期望在寄存器 EAX 中找到子程序的返回值。要恢复机器状态,调用者应该:
- 从堆栈中删除参数。这会将堆栈恢复到执行调用之前的状态。
- 通过从堆栈中弹出来恢复调用者保存的寄存器(EAX、ECX、EDX)的内容。调用者可以假设子程序没有修改其他寄存器。3
有关 x86 调用约定的更深入讨论,请参阅System V 应用程序二进制接口 Intel386 架构处理器增补,第四版中的 x86 ABI 文档。
__stack_chk_fail 和堆栈守卫
而且,调用 sym.imp.__stack_chk_fail 有什么作用?
__stack_chk_fail当由于缓冲区溢出而覆盖堆栈金丝雀时调用:
堆栈保护背后的基本思想是在函数返回指针被压入后立即在堆栈上压入一个“金丝雀”(一个随机选择的整数)。然后在函数返回之前检查金丝雀值;如果它改变了,程序将中止。通常,堆栈缓冲区溢出(又名“堆栈粉碎”)攻击必须更改金丝雀的值,因为它们在缓冲区末尾之外写入,然后才能到达返回指针。由于金丝雀的价值对攻击者来说是未知的,所以它不能被攻击所取代。因此,堆栈保护允许程序在发生这种情况时中止,而不是返回到攻击者想要它去的任何地方。4
下面是一些带注释的示例代码:
000000000040055d <test>:
40055d: 55 push %rbp
40055e: 48 89 e5 mov %rsp,%rbp
400561: 48 83 ec 20 sub $0x20,%rsp
400565: 89 7d ec mov %edi,-0x14(%rbp)
400568: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax <- get guard variable value
40056f: 00 00
400571: 48 89 45 f8 mov %rax,-0x8(%rbp) <- save guard variable on stack
400575: 31 c0 xor %eax,%eax
400577: 8b 45 ec mov -0x14(%rbp),%eax
40057a: 48 8b 55 f8 mov -0x8(%rbp),%rdx <- move it to register
40057e: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx <- check it against original
400585: 00 00
400587: 74 05 je 40058e <test+0x31>
400589: e8 b2 fe ff ff callq 400440 <__stack_chk_fail@plt>
40058e: c9 leaveq
40058f: c3 retq
1. Linux 编程接口,第 3 章“系统编程概念”
2. x86 指令集参考 - CALL - c9x.me
3. x86 汇编指南- 弗吉尼亚大学计算机科学
4. GCC 的“强”堆栈保护- LWN.net