简单的介绍一下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的关系》对你有帮助,请点赞、收藏,并留下你的观点哦!