失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > c语言函数调用与ebp esp的关系

c语言函数调用与ebp esp的关系

时间:2021-08-11 10:05:57

相关推荐

c语言函数调用与ebp esp的关系

简单的介绍一下intel汇编指令集和gnu 汇编指令有什么差别

下面的介绍很多引用来自于这一篇文档

AT&T 汇编语言与 GCC 内嵌汇编简介

在 INTEL 语法中,第一个表示目的操作数,第二个表示源操作数,赋值方向从右向左。

AT&T 语法第一个为源操作数,第二个为目的操作数,方向从左到右,合乎自然。例

INTEL AT&TMOV EAX,EBX movl %ebx,%eax

两者的含义都是把ebx寄存器的值赋值给 eax

在 INTEL 语法中寄存器和立即数不需要前缀; AT&T 中寄存器需要加前缀“%”;立即数需要加前缀“$”。例:

INTEL AT&TMOV EAX,1 movl $1,%eax

符号,或者标号常数直接引用,不需要加前缀,如:

movl value , %ebx //value 为一常数;一般是一个符号,一个标号

在符号前加前缀 $, 表示引用符号地址, 如:

movl $value, %ebx //将 value 的地址放到 ebx 中。

AT&T 语法中大部分指令操作码的最后一个字母表示操作数大小,“b”表示 byte(一个字节);“w”表示 word(2,个字节);“l”表示 long(4,个字节)。 INTEL 中处理内存操作数时也有类似的语法如: BYTE PTR、 WORD PTR、 DWORD PTR。例:

INTEL AT&Tmov al, bl movb %bl,%almov ax,bx movw %bx,%axmov eax, dword ptr [ebx] movl (%ebx), %eax

保留一份esp的值得原因是因为需要在栈上面存取数据,而esp是系统在管理,你也不知道他指向哪?不方便程序查找数据,尤其是参数传递的时候。

先来看普通的c语言程序执行函数调用是一个什么样的流程

test.c

#include <stdlib.h>int add(int a, int b){return (a + b);}int main(){int i = 2;i = add(1, 2);return 0;}

上面这个函数很简单,只是在main函数里面调用了一个add函数

执行编译的命令

gcc -ggdb3 -o test test.c

使用gdb调试他,我们在这一行加一个断点

Breakpoint 1, main () at test.c:1010i = add(1, 2);(gdb) display /4i $pc---每次显示 当前位置往后的 4个反汇编命令

执行display /4i $pc 这个命令,就能看出来附近的反汇编

马上要开始执行add函数的汇编指令

(gdb) display /4i $pc1: x/4i $pc=> 0x8048407 <main+13>:movl $0x2,0x4(%esp)0x804840f <main+21>:movl $0x1,(%esp)0x8048416 <main+28>:call 0x80483ed <add>0x804841b <main+33>:mov %eax,-0x4(%ebp)(gdb)

参数的传递是通过栈来进行的,我们先保存一下sp(栈指针)的值,sp是从高地址往低地址递减的。

(gdb) info reg spsp 0xbffff0c00xbffff0c0

所以我们执行si(执行一条汇编指令)指令看一下会发生什么

他会把add(1,2)这两个数值存放到栈里面去,存放的顺序是先存2,后存1,即传参入栈的顺序是从右往左的,即越右边的参数,越最早进入栈。即他的地址越大,因为栈是递减的,存放栈里面的地址越大的话,说明越在底下,出栈的时候,越晚出来。

c语言传参输的时候,越右边的参数,越先进入栈里面

但是执行完这两条指令之后

=> 0x8048407 <main+13>:movl $0x2,0x4(%esp)0x804840f <main+21>:movl $0x1,(%esp)

sp的值并没有变化,说明只是暂存到栈上面

(gdb) info reg espesp 0xbffff0c00xbffff0c0

2存放在 0xbffff0c4 的地方, 1 存放在0xbffff0c0的地方

(gdb) x /2 0xbffff0c00xbffff0c0:0x000000010x00000002

其实这个时候a,b的值就已经完成了初始化了

(gdb) p /x &a$3 = 0xbffff0c0(gdb) p /x &b$4 = 0xbffff0c4(gdb)

接下来就要去执行 call 指令了,这个时候啊,a,b默认被赋值为栈上面的数值了

赋值的顺序也是从左边先开始的,即a先拿到栈上的数据。

(gdb) siadd (a=1, b=2) at test.c:44{1: x/4i $pc=> 0x80483ed <add>:push %ebp0x80483ee <add+1>:mov %esp,%ebp0x80483f0 <add+3>:mov 0xc(%ebp),%eax0x80483f3 <add+6>:mov 0x8(%ebp),%edx(gdb)

这个时候我们再去查看一下sp的值

(gdb) info reg spsp 0xbffff0bc0xbffff0bc

返回地址是由硬件自动压栈的。压入的地址是call指令的下一条地址

0x8048416 <main+28>:call 0x80483ed <add>0x804841b <main+33>:mov %eax,-0x4(%ebp)

所以我们查看一下

(gdb) x /3x 0xbffff0bc0xbffff0bc:0x0804841b0x000000010x00000002(gdb)

所以栈顶是存放返回地址的地方

+| 栈底方向 | 高位地址| ... || ... || 参数3 || 参数2 || 参数1 || 返回地址 || 上一层[ebp] | <-------- [ebp]| 局部变量 | 低位地

add函数最先开始执行的两条汇编指令

1: x/4i $pc=> 0x80483ed <add>:push %ebp0x80483ee <add+1>:mov %esp,%ebp

执行完之后的esp,ebp的值,以及堆栈空间的情况

(gdb) x /4x 0xbffff0b8main函数的ebp call指令的返回地址两个形参0xbffff0b8:0xbffff0d80x0804841b0x000000010x00000002(gdb)

(gdb) info reg esp ebpesp 0xbffff0b80xbffff0b8ebp 0xbffff0b80xbffff0b8

现在ebp指向的是add函数的栈顶,epb这个地址存放的是main函数的ebp,ebp+4 存放的是返回值,即call add之后,应该返回执行的地方

add函数内部的实现

0x80483f0 <add+3>: mov 0xc(%ebp),%eax

0x80483f3 <add+6>: mov 0x8(%ebp),%edx

0x80483f6 <add+9>: add %edx,%eax

从 (ebp + 0xc)----0xbffff0c4 地址的地方取出里面的内容就是0x2,存放到eax寄存器里面去从 (ebp+8)------->0xbffff0c0 地址的地方取出里面的内容就是 0x1, 存放edx寄存器里面edx + eax ----> eax // edx的值加上eax的值得结果在存放到eax里面

一般函数的返回值存放在eax寄存器里面,里面存放的是返回值3

所以函数去取形参的顺序确实与入栈顺序相反

取出参数的时候,

(gdb) info reg eaxeax 0x33(gdb)

看下面的执行命令

1: x/4i $pc=> 0x80483f8 <add+11>:pop %ebp0x80483f9 <add+12>:ret 0x80483fa <main>:push %ebp0x80483fb <main+1>:mov %esp,%ebp(gdb) info reg eaxeax 0x33(gdb) si0x080483f96}1: x/4i $pc=> 0x80483f9 <add+12>:ret 0x80483fa <main>:push %ebp0x80483fb <main+1>:mov %esp,%ebp0x80483fd <main+3>:sub $0x18,%esp(gdb) info reg espesp 0xbffff0bc0xbffff0bc(gdb) info reg ebpebp 0xbffff0d80xbffff0d8(gdb)

ret指令会自动把返回地址取出来,跳回去执行,我们执行一下si试一下

(gdb) si0x0804841b in main () at test.c:1010i = add(1, 2);1: x/4i $pc=> 0x804841b <main+33>:mov %eax,-0x4(%ebp)0x804841e <main+36>:mov $0x0,%eax0x8048423 <main+41>:leave 0x8048424 <main+42>:ret(gdb) info reg espesp 0xbffff0c00xbffff0c0

(gdb) info reg ebpebp 0xbffff0d80xbffff0d8

说明ret自动把返回地址出栈了,并且程序跳转到了call指令的下一条指令去执行, ebp会指向当前函数的栈顶。你会发现他比0xbffff0c0 大很多。

最后leave指令相当于这两条指令

movl ebp, esppop ebp

执行完之后看一下,ebp和esp的值

(gdb) info reg ebp espebp 0x00x0esp 0xbffff0dc0xbffff0dc(gdb)

查看一下原先ebp地址存放的内容,结果是符合上面执行的指令的。ebp一开始是0xbffff0d8,然后 esp赋值之后也等于0xbffff0d8, 之后又把0xbffff0d8地址存放的数据pop出来, 所以ebp等于0, esp = esp(old) + 4 = 0xbffff0dc

(gdb) x /2x 0xbffff0d80xbffff0d8:0x000000000xb7e28af3

结合我们之前说的,再往上一个栈存放的就是main函数的返回地址,也就是调用main函数的地方,

(gdb) info symbol 0xb7e28af3__libc_start_main + 243 in section .text of /lib/i386-linux-gnu/libc.so.6(gdb)

你会发面main函数确实是被c库所调用。

总结一下

add(1,2)堆栈上会做哪一些操作

高地址21返回地址 <--------esp低地址

执行到add(int a, int b)内部,a和b怎么取值,是a = 1还是2

其实很明显,取栈上面的数据的时候,是先从左边的变量开始取的

函数调用入栈的时候,是从右往左的变量依次入栈,如add(i, j), 先将j入栈, 再将i入栈函数执行获取形参的时候, 是从左往右获取的,如get(i, j), 先是i变量获取堆栈上的数据

如果觉得《c语言函数调用与ebp esp的关系》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。