C函数的调用机制
- 函数调用操作包括从一块代码到另一块代码之间的双向数据操作和执行控制转移。CPU为控制传递提供指令,而数据的传递和局部变量存储空间的分配与回收则通过栈操作实现。
栈帧结构
- 用栈来传递过程参数、存储和返回信息、保存寄存器。
- 单个过程分配的栈叫做栈帧。%ebp是栈帧指针,%esp是栈指针。当程序运行时,栈指针移动,因此绝大部分信息是相对帧指针的
转移控制(P是调用者,Q是被调用者)
- CALL指令:将返回地址压人栈,并跳转到被调用过程的起始处。返回地址是在过程中紧跟call后面的那条指令地址,这样当过程返回的时候,执行处会从此处继续执行。
- RET指令:从栈中弹出地址,并跳转到该地址处。正确使用这条指令,可以使栈做好准备,栈指针要指向前面call指令存储的返回地址。
- LEAVE指令:为返回准备栈。等价于 movl %ebp.%esp ,popl %ebp
- 一些规定:调用者保存%eax,%edx,%ecx,当过程P调用Q的时候,Q可以覆盖这些寄存器,而不会破坏P所需要的数据。而被调用者需要保护 %ebx %esi %efi %ebp %edp
一个说明CALL RET例子
int accum = 0;
int sum(int x,int y)
{
int t = x + y;
accum += t;
return t;
}
它的反汇编:
//sum起点
08048394 <sum>:
8048394: 55 push %ebp
........
//返回
80483a4: c3 ret
.......
//从主函数中调用sum
80483dc: e8 b3 ff ff ff call 8058394 <sum>
80483e1:83 c4 14 add $0x14,%esp
堆栈状况如下:
过程描述:
- 进入main函数后
- 当指令到达80483dc的时候,执行call,此时跳转到8048394,并把call后面指令add $0x14..的地址80483e1压入调用的堆栈中
- 进入sum后当指令执行到80483a4处时,指令弹出80483e1,并跳转到这个地址
- 继续main函数的执行
过程示例
重要的地方在于:
- pushl %ebp,movl %esp,%ebp #保存原ebp的值,设置当前函数的帧指针
- %eax用来存储返回值
main()
main()也是一个函数,编译的时候会作为crt0.s一个桩程序。改程序的目标文件会被链接程序在每个程序执行的开始部分,主要用来设置初始化的全局变量
递归过程
由于调用很多堆栈,处理不当可能导致内存溢出。所以许多尾递归函数编译器会将其优化至迭代过程。
?迭代与递归
汇编程序中调用C函数
- 在汇编程序调用C时,要自己处理参数位置,即将函数最右边的参数首先压入栈,而左边的第1个参数在最后调用指令之前入栈
- 执行CALL指令执行被调用的函数
- 调用函数返回后,程序把先前压入栈中的函数参数清除掉
- 如果调用涉及到代码特权级的变化,CPU进行堆栈切换。Linux内核中只使用中断门和陷阱门处理特权级变换的情况,未用CALL指令处理特权级的变化。
如果我们没有设置调用函数fun()压入参数而直接调用的话,那么func()函数仍然会把EIP的上方内容当做参数使用。举个例子:
// kernal /sys_call.s汇编_sys_fork部分 push %gs push %esi push %edi push %ebp push %eax call _copy_process addl $20,%esp ret
其实_copy_process带有多达17个参数,这里只压入了五个- 用jmp指令同样可以完成调用函数,只不过需要人工将返回地址压入到栈中,然后用jmp跳转到函数执行处
从C中调用汇编函数
- 将汇编函数写入.s文件 a.s
- 调用汇编的函数写入.c文件 a.c
- 生成目标文件的步骤:
- #as -o a.o a.s
- #gcc -o a a.c a.o