函数在OS层面的调用主要涉及4个对象:
对象 | 解释 | 操作 |
---|---|---|
函数调用栈 | 本质上一段连续的内存地址,只是其写内存的方式按照栈的方式,FILO | 地址按逆序增长,进栈栈顶指针指向地址减小,退栈栈顶指针指向地址变大 |
esp寄存器 | 用来存储函数调用栈的当前栈栈顶地址,指向栈顶 | 随进栈和出栈其寄存器存储地址值变化 |
ebp寄存器 | 用来存储当前调用函数的调用栈基地址,指向栈底 | 在当前函数调用完成前保持不变 |
eip寄存器 | 存储下一条待执行的指令地址 | OS自动更新 |
函数调用过程
示例代码如下:
#include<stdio.h>
unsigned int test(int a,int b)
{
int c,d;
c = a;
d = b;
return c;
}
int main()
{
unsigned int r;
r = test(1,2);
return 0;
}
反编译代码如下:
08048394 <test>:
#include<stdio.h>
unsigned int test(int a,int b)
{
8048394: 55 push %ebp // 将ebp寄存器存储地址压栈,被
// 调用函数基地址存储。
8048395: 89 e5 mov %esp,%ebp // 将esp存储地址赋值给ebp寄存器
// 当前调用的被调用函数基地址存储
8048397: 83 ec 10 sub $0x10,%esp // esp自减0x10
int c,d;
c = a;
804839a: 8b 45 08 mov 0x8(%ebp),%eax // 将[ebp+0x8]地址里的内容赋给eax
804839d: 89 45 fc mov %eax,-0x4(%ebp) // 把eax的值放到[ebp-4]这个地址里
d = b;
80483a0: 8b 45 0c mov 0xc(%ebp),%eax // 将[ebp+0xc]地址里的内容赋给eax
80483a3: 89 45 f8 mov %eax,-0x8(%ebp) // 把eax的值放到[ebp-8]这个地址里
return c;
80483a6: 8b 45 fc mov -0x4(%ebp),%eax // 通过eax寄存器保存函数的返回值
}
80483a9: c9 leave
80483aa: c3 ret
080483ab <main>:
int main()
{
80483ab: 55 push %ebp
80483ac: 89 e5 mov %esp,%ebp
80483ae: 83 ec 18 sub $0x18,%esp // esp的值自减0x18
unsigned int r;
r = test(1,2);
80483b1: c7 44 24 04 02 00 00 movl $0x2,0x4(%esp) // 将0x2(第2个参数)放到esp寄存器存储的
// 地址+0x4中,即第二个参数压栈
80483b8: 00
80483b9: c7 04 24 01 00 00 00 movl $0x1,(%esp) // 将0x1(第1个参数)放到esp寄存器存储的
// 地址中,即第一个参数压栈
80483c0: e8 cf ff ff ff call 8048394 <test> // 调用test函数,此时会将断点(返回地址)
// 压栈, 跳转到test函数,开始执行
80483c5: 89 45 fc mov %eax,-0x4(%ebp)
return 0;
80483c8: b8 00 00 00 00 mov $0x0,%eax
}
80483cd: c9 leave
80483ce: c3 ret
80483cf: 90 nop
- 被调用函数传入参数逆序压栈。 main
- 调用函数的下一条执行指令进栈,即返回地址进栈。 main
- 调用函数的基地址压栈(0x80483ab),即读当前ebp寄存器存储地址,进行压栈。main
- 更新ebp寄存器的值,为被调用函数,即当前执行函数,的基地址(0x8048394)。test
- 局部变量压栈。test
调用函数返回地址进栈–>返回后的eip,调用函数的ebp进栈–>返回后的ebp,被调用函数的执行完成后的栈顶–>返回后的esp
PUSH 等价于:
subl $4, %esp
movl %ebp (%esp)
POP 等价于:
movl (%esp), %ead
addl $4, %esp
CALL指令的步骤:首先是将返回地址(也就是call指令要执行时EIP的值)压入栈顶,然后是将程序跳转到当前调用的方法的起始地址。执行push和jump指令。
RET指令则是将栈顶的返回地址弹出到EIP,然后按照EIP此时指示的指令地址继续执行程序。
LEAVE指令是将栈指针指向帧指针,然后POP备份的原帧指针到%EBP。