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

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

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

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

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

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

INTEL 								AT&T
MOV EAX,EBX 				movl %ebx,%eax

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

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

INTEL 						AT&T
MOV 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&T
mov al, bl 										movb %bl,%al
mov ax,bx 										movw %bx,%ax
mov 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:10
10		i = add(1, 2);
(gdb) display /4i $pc---每次显示 当前位置往后的 4个反汇编命令

执行display /4i $pc 这个命令,就能看出来附近的反汇编
马上要开始执行add函数的汇编指令

(gdb) display /4i $pc
1: 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 sp
sp             0xbffff0c0	0xbffff0c0

所以我们执行si(执行一条汇编指令)指令看一下会发生什么
他会把add(1,2)这两个数值存放到栈里面去,存放的顺序是先存2,后存1,即传参入栈的顺序是从右往左的,即越右边的参数,越最早进入栈。即他的地址越大,因为栈是递减的,存放栈里面的地址越大的话,说明越在底下,出栈的时候,越晚出来。

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

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

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

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

(gdb) info reg esp
esp            0xbffff0c0	0xbffff0c0

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

(gdb) x /2 0xbffff0c0
0xbffff0c0:	0x00000001	0x00000002

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

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

接下来就要去执行 call 指令了,这个时候啊,a,b默认被赋值为栈上面的数值了
赋值的顺序也是从左边先开始的,即a先拿到栈上的数据。

(gdb) si
add (a=1, b=2) at test.c:4
4	{
1: x/4i $pc
=> 0x80483ed <add>:	push   %ebp
   0x80483ee <add+1>:	mov    %esp,%ebp
   0x80483f0 <add+3>:	mov    0xc(%ebp),%eax
   0x80483f3 <add+6>:	mov    0x8(%ebp),%edx
(gdb) 

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

(gdb) info reg sp
sp             0xbffff0bc	0xbffff0bc

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

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

所以我们查看一下

(gdb) x /3x 0xbffff0bc
0xbffff0bc:	0x0804841b	0x00000001	0x00000002
(gdb) 

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

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

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

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

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

(gdb) x /4x 0xbffff0b8
					main函数的ebp   call指令的返回地址		两个形参
0xbffff0b8:	0xbffff0d8	0x0804841b	0x00000001	0x00000002
(gdb) 
(gdb) info reg esp ebp
esp            0xbffff0b8	0xbffff0b8
ebp            0xbffff0b8	0xbffff0b8

现在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

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

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

取出参数的时候,
(gdb) info reg eax
eax            0x3	3
(gdb) 

看下面的执行命令

1: x/4i $pc
=> 0x80483f8 <add+11>:	pop    %ebp
   0x80483f9 <add+12>:	ret    
   0x80483fa <main>:	push   %ebp
   0x80483fb <main+1>:	mov    %esp,%ebp
(gdb) info reg eax
eax            0x3	3
(gdb) si
0x080483f9	6	}
1: x/4i $pc
=> 0x80483f9 <add+12>:	ret    
   0x80483fa <main>:	push   %ebp
   0x80483fb <main+1>:	mov    %esp,%ebp
   0x80483fd <main+3>:	sub    $0x18,%esp
(gdb) info reg esp
esp            0xbffff0bc	0xbffff0bc
(gdb) info reg ebp
ebp            0xbffff0d8	0xbffff0d8
(gdb) 

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

(gdb) si
0x0804841b in main () at test.c:10
10		i = add(1, 2);
1: x/4i $pc
=> 0x804841b <main+33>:	mov    %eax,-0x4(%ebp)
   0x804841e <main+36>:	mov    $0x0,%eax
   0x8048423 <main+41>:	leave  
   0x8048424 <main+42>:	ret
(gdb) info reg esp
esp            0xbffff0c0	0xbffff0c0
(gdb) info reg ebp
ebp            0xbffff0d8	0xbffff0d8

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

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

movl ebp, esp
pop ebp

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

(gdb) info reg ebp esp
ebp            0x0	0x0
esp            0xbffff0dc	0xbffff0dc
(gdb) 

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

(gdb) x /2x 0xbffff0d8
0xbffff0d8:	0x00000000	0xb7e28af3

结合我们之前说的,再往上一个栈存放的就是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)堆栈上会做哪一些操作

高地址			
					2
					1	
					返回地址 <--------esp
低地址

执行到add(int a, int b)内部,a和b怎么取值,是a = 1还是2
其实很明显,取栈上面的数据的时候,是先从左边的变量开始取的

函数调用入栈的时候,是从右往左的变量依次入栈,如add(i, j), 先将j入栈, 再将i入栈
函数执行获取形参的时候, 是从左往右获取的,如get(i, j), 先是i变量获取堆栈上的数据
发布了17 篇原创文章 · 获赞 3 · 访问量 3552

猜你喜欢

转载自blog.csdn.net/sgy1993/article/details/89219767