首先我们需要了解一下在函数调用时候栈的结构
栈的生长方向由高地址向低地址生长,栈顶指针由sp或者esp确定,当压栈时sp减法操作 |
每一个函数都是一个栈框架(frame stack)。
我们简单来分析一下下来函数,对压栈,以及汇编语言,调试有进一步的了解
Section 1
Int sum (int x , int y)
{
Intz = x + y;
Returnz;
}
Section 2
Main()
{
Intx=2;
Inty=3;
Intt=sum(2,3);
}
当我们对section 1 进行编译时
Gcc –S –O1 sum.c // 生成 sum.s 文件, 进行优化后的文件
Cat sum.s
08048394
8048394: 55 push %ebp
8048395: 89e5 mov %esp,%ebp
8048397: 83ec 10 sub $0x10,%esp
804839a: 8b45 0c mov 0xc(%ebp),%eax
804839d: 8b55 08 mov 0x8(%ebp),%edx
80483a0: 8d04 02 lea (%edx,%eax,1),%eax
80483a3: 8945 fc mov %eax,-0x4(%ebp)
80483a6: 8b45 fc mov -0x4(%ebp),%eax
80483a9: c9 leave
80483aa: c3 ret
080483ab
80483ab: 55 push %ebp
80483ac: 89e5 mov %esp,%ebp
80483ae: 83ec 18 sub $0x18,%esp // 8+16,完成预留空间和16字节对齐
80483b1: c745 fc 02 00 00 00 movl $0x2,-0x4(%ebp)
80483b8: c745 f8 03 00 00 00 movl $0x3,-0x8(%ebp)
80483bf: c744 24 04 03 00 00 movl $0x3,0x4(%esp)
80483c6: 00
80483c7: c704 24 02 00 00 00 movl $0x2,(%esp)
80483ce: e8 c1 ff ff ff call 8048394
80483d3: 8945 f4 mov %eax,-0xc(%ebp)
80483d6: c9 leave
80483d7: c3 ret
80483d8: 90 nop
80483d9: 90 nop
80483da: 90 nop
80483db: 90 nop
80483dc: 90 nop
80483dd: 90 nop
80483de: 90 nop
80483df: 90 nop
假如函数A调用函数B,函数B调用函数C ,则函数栈框架及调用关系如下图所示:
+-------------------------+----> 高地址
| EIP (上级函数返回地址) |
+-------------------------+
+--> |EBP (上级函数的EBP) | --+ <------当前函数A的EBP (即SFP框架指针)
| +-------------------------+ +-->偏移量A
| | LocalVariables | |
| |.......... | --+ <------ESP指向函数A新分配的局部变量,局部变量可以通过A的ebp-偏移量A访问
| f +-------------------------+
| r | Arg n(函数B的第n个参数) |
| a +-------------------------+
| m | Arg .(函数B的第.个参数) |
| e +-------------------------+
| | Arg 1(函数B的第1个参数) |
| o +-------------------------+
| f | Arg 0(函数B的第0个参数) | --+ <------ B函数的参数可以由B的ebp+偏移量B访问
| +-------------------------+ +--> 偏移量B
| A | EIP (A函数的返回地址) | |
| +-------------------------+ --+
+--- | EBP (A函数的EBP) |<--+ <------ 当前函数B的EBP (即SFP框架指针)
+-------------------------+ |
| LocalVariables | |
|.......... | | <------ ESP指向函数B新分配的局部变量
+-------------------------+ |
| Arg n(函数C的第n个参数) | |
+-------------------------+ |
| Arg .(函数C的第.个参数) | |
+-------------------------+ +--> frame of B
| Arg 1(函数C的第1个参数) | |
+-------------------------+ |
| Arg 0(函数C的第0个参数) | |
+-------------------------+ |
| EIP (B函数的返回地址) | |
+-------------------------+ |
+--> |EBP (B函数的EBP) | --+ <------ 当前函数C的EBP (即SFP框架指针)
| +-------------------------+
| | LocalVariables |
| |.......... | <------ ESP指向函数C新分配的局部变量
| +-------------------------+----> 低地址
frame of C
手册上介绍:
Thestack pointer for a stack segment should be aligned on 16-bit (word) or 32-bit(double-word)
boundaries, depending on the width of the stack segment. The D flag in thesegment descriptor
for the current code segment sets the stack-segment width (refer to Chapter 3,Protected-Mode
Memory Management of the Intel Architecture Software Developer’s Manual, Volume3). The
PUSH and POP instructions use the D flag to determine how much to decrement orincrement
the stack pointer on a push or pop operation, respectively. When the stackwidth is 16 bits, the
stack pointer is incremented or decremented in 16-bit increments; when thewidth is 32 bits, the
stack pointer is incremented or decremented in 32-bit increments.
The processor does not check stack pointer alignment. It is the responsibilityof the programs,
tasks, and system procedures running on the processor to maintain properalignment of stack
pointers. Misaligning a stack pointer can cause serious performance degradationand in some
instances program failures.
在实际测试时,需要视编译器而定,即便是相同的编译器,不同的优化策略得出的结果也有可能不同。
# vi test4.c
int main()
{
char str1[50];
char str2[100];
return 0;
}
这里分为三个部分:
1: 0x8
这里假设在进入main函数之前,栈是16字节对齐的话.
call main函数的时候,需要把eip和ebp压入堆栈,此时栈地址(esp)最末4位二进制位必定是1000,esp-8则恰好使后4位地址二进制位为0000。所以这里为分配了0x8个字节,是为保证栈16字节对齐的。
2: 0x40
由于你定义了一个char str1[50];如果直接分配50个字节,那么将破坏栈16字节对齐规则,所以我们得分配一个同时满足空间需要而且保持16字节栈对齐的,最接近的就是0x40(0x30 <50<0x40)。
3: 0x70
如2所说的,最接近的就是0x70(0x60 <100<0x70)。
为什么说:“此时栈地址(esp)最末4位二进制位必定是1000,”
当然这里只是针对:gcc默认的编译是要16字节栈对齐的而言的
这里假设在进入main函数之前,栈是16字节对齐的话. 假设地址是100000(0x20)
| | <- 100000
| |
| |
| |
| |
| |
| |
在调用main函数的时候,要把main函数的返回地址压入stack中,还要把EBP压入进入,而这两个都是指针,在32位机上都是4个字节(1000),即push这两个值之后,ESP的值就是11000(0x20 - 0x08 =0x18),这时候栈地址(ESP)最末4位二进制位是不是就是1000了?所以就再sub 8个字节,保证之后使用的ESP还是16字节对齐的。
__________100000 (ESP)
| EIP |
| EBP|__________011000
| |
| |__________010000
| |
| |
| |
如我们所知:
1)IA32的栈是用来存放临时数据,而且是LIFO,即后进先出的。栈的增长方向是从高地址向低地址增长,按字节为单位编址。
2) EBP是栈基址的指针,永远指向栈底(高地址),ESP是栈指针,永远指向栈顶(低地址)。
3) PUSH一个long型数据时,以字节为单位将数据压入栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4的地址单元。
4) POP一个long型数据,过程与PUSH相反,依次将ESP-4、ESP-3、ESP-2、ESP-1从栈内弹出,放入一个32位寄存器。
5) CALL指令用来调用一个函数或过程,此时,下一条指令地址会被压入堆栈,以备返回时能恢复执行下条指令。
6) RET指令用来从一个函数或过程返回,之前CALL保存的下条指令地址会从栈内弹出到EIP寄存器中,程序转到CALL之前下条指令处执行
7) ENTER是建立当前函数的栈框架,即相当于以下两条指令:
pushl %ebp
movl %esp,%ebp
8) LEAVE是释放当前函数或者过程的栈框架,即相当于以下两条指令:
movl ebp esp
popl ebp
“参数从右到左入栈”,“局部变量在栈上分配空间”,听的耳朵都起茧子了。
最近做项目涉及C和汇编互相调用,写代码的时候才发现没真正弄明白。
自己写了个最简单的函数,用gdb跟踪了调用过程,才多少懂了一点。
参考资料:(感谢liigo和eno_rez两位作者)
http://blog.csdn.net/liigo/archive/2006/12/23/1456938.aspx
http://blog.csdn.net/eno_rez/archive/2008/03/08/2158682.aspx
编译:(Fedora6,gcc 4.1.2)
[test]$ gcc -g -Wall -o stack stack.c
反汇编:
这里的汇编的格式是AT&T汇编,它的格式和我们熟悉的汇编格式不太一样,尤其要注意源操作数和目的操作数的顺序是反过来的
[test]$ objdump -d stack > stack.dump
[test]$ cat stack.dump
......
08048354
8048354: 55 push %ebp ;保存调用者的帧指针
8048355: 89e5 mov %esp,%ebp ;把当前的栈指针作为本函数的帧指针
8048357: 83 ec10 sub $0x10,%esp ;调整栈指针,为局部变量保留空间
804835a: c7 45 fc 00 00 0000 movl $0x0,0xfffffffc(%ebp) ;把a置0。ebp-4的位置是第一个局部变量
8048361: 8b 4508 mov 0x8(%ebp),%eax ;把参数x保存到eax。ebp+8的位置是最后一个入栈的参数,也就是第一个参数
8048364: 89 45fc mov %eax,0xfffffffc(%ebp) ;把eax赋值给变量a
8048367: 8b 450c mov 0xc(%ebp),%eax ;把参数y保存到eax。ebp+C的位置是倒数第二个入栈的参数,也就是第二个参数
804836a: 01 45fc add %eax,0xfffffffc(%ebp) ;a+=y
804836d: 8b 45fc mov 0xfffffffc(%ebp),%eax ;把a的值作为返回值,保存到eax
8048370: c9 leave
8048371: c3 ret
08048372
8048372: 8d 4c 2404 lea 0x4(%esp),%ecx ;????
8048376: 83 e4f0 and $0xfffffff0,%esp ;把栈指针16字节对齐
8048379: ff 71fc pushl 0xfffffffc(%ecx) ;????
804837c: 55 push %ebp ;保存调用者的帧指针
804837d: 89e5 mov %esp,%ebp ;把当前的栈指针作为本函数的帧指针
804837f: 51 push %ecx ;????
8048380: 83 ec18 sub $0x18,%esp ;调整栈指针,为局部变量保留空间
8048383: c7 45 f0 12 00 0000 movl $0x12,0xfffffff0(%ebp) ;x=0x12。ebp-16是局部变量x
804838a: c7 45 f4 34 00 0000 movl $0x34,0xfffffff4(%ebp) ;y=0x34。ebp-12是局部变量y
8048391: 8b 45f4 mov 0xfffffff4(%ebp),%eax ;y保存到eax
8048394: 89 44 2404 mov %eax,0x4(%esp) ;y作为最右边的参数首先入栈
8048398: 8b 45f0 mov 0xfffffff0(%ebp),%eax ;x保存到eax
804839b: 89 0424 mov %eax,(%esp) ;x第二个入栈
804839e: e8 b1 ff ffff call 8048354
80483a3: 89 45f8 mov %eax,0xfffffff8(%ebp) ;把保存在eax的add的返回值,赋值给位于ebp-8的第三个局部变量result。注意这条指令的地址,就是add的返回地址
80483a6: b8 00 00 0000 mov $0x0,%eax ;0作为main的返回值,保存到eax
80483ab: 83 c418 add $0x18,%esp ;恢复栈指针,也就是讨论stdcall和cdecl的时候总要提到的“调用者清栈”
80483ae: 59 pop %ecx ;
80483af: 5d pop %ebp ;
80483b0: 8d 61fc lea 0xfffffffc(%ecx),%esp ;
80483b3: c3 ret
80483b4: 90 nop
......
有一点值得注意的是main在调用add之前把参数压栈的过程。
它用的不是push指令,而是另一种方法。
在main入口调整栈指针的时候,也就是位于8048380的这条指令 sub $0x18,%esp
不但象通常函数都要做的那样给局部变量预留了空间,还顺便把调用add的两个参数的空间也预留出来了。
然后把参数压栈的时候,用的是mov指令。
我不太明白这种方法有什么好处。
另外一个不明白的就是main入口的四条指令8048372、8048376、8048379、804837f,还有与之对应的main返回之前的指令。
貌似main对esp要求16字节对齐,所以先把原来的esp压栈,然后强行把esp的低4位清0。等到返回之前再从栈里恢复原来的esp
准备工作都做好了,现在开始gdb
对gdb不太熟悉的同学要注意一点,stepi命令执行之后显示出来的源代码行或者指令地址,都是即将执行的指令,而不是刚刚执行完的指令。
我在每个stepi后面都加了注释,就是刚执行过的指令。
[test]$ gdb -q stack
(gdb) break main
Breakpoint 1 at 0x8048383: file stack.c, line 11.
gdb并没有把断点设置在main的第一条指令,而是设置在了调整栈指针为局部变量保留空间之后
(gdb) run
Starting program: /home/brookmill/test/stack
Breakpoint 1, main () at stack.c:11
11 x = 0x12;
(gdb) stepi // 注释: movl $0x12,0xfffffff0(%ebp)
12 y = 0x34;
(gdb) stepi // 注释: movl $0x34,0xfffffff4(%ebp)
13 result = add(x, y);
(gdb) info registers esp
esp 0xbf8df8ac 0xbf8df8ac
(gdb) info registers ebp
ebp 0xbf8df8c8 0xbf8df8c8
(gdb) x/12 0xbf8df8a0
0xbf8df8a0: 0x002daff4 0x002d9220 0xbf8df8d8 0x080483e9
0xbf8df8b0: 0x001ca8d5 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
这就是传说中的栈。在main准备调用add之前,先看看这里有些什么东东
0xbf8df8c8(ebp)保存的是上一层函数的帧指针:0xbf8df938,距离这里有112字节
0xbf8df8cc(ebp+4)保存的是main的返回地址0x001b4dec
0xbf8df8b8(ebp-16)是局部变量x,已经赋值0x12;
0xbf8df8bc(ebp-12)是局部变量y,已经赋值0x34;
0xbf8df8c0(ebp-8)是局部变量result。值得注意的是,因为我们没有给result赋值,这里是一个不确定的值。局部变量如果不显式的初始化,初始值不一定是0。
现在开始调用add
(gdb) stepi // 注释:mov 0xfffffff4(%ebp),%eax
0x08048394 13 result = add(x, y);
(gdb) stepi // 注释:mov %eax,0x4(%esp)
0x08048398 13 result = add(x, y);
(gdb) x/12 0xbf8df8a0
0xbf8df8a0: 0x002daff4 0x002d9220 0xbf8df8d8 0x080483e9
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
y首先被压栈,在0xbf8df8b0
(gdb)stepi // 注释: mov 0xfffffff0(%ebp),%eax
0x0804839b 13 result = add(x, y);
(gdb) stepi // 注释:mov %eax,(%esp)
0x0804839e 13 result = add(x, y);
(gdb) x/12 0xbf8df8a0
0xbf8df8a0: 0x002daff4 0x002d9220 0xbf8df8d8 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
x第二个进栈,在0xbf8df8ac
(gdb) stepi // 注释:call 8048354
add (x=18, y=52) at stack.c:2
2 {
刚刚执行了call指令,现在我们进入了add函数
(gdb) info registers esp
esp 0xbf8df8a8 0xbf8df8a8
(gdb) info registers ebp
ebp 0xbf8df8c8 0xbf8df8c8
(gdb) x/12 0xbf8df8a0
0xbf8df8a0: 0x002daff4 0x002d9220 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
现在esp指向0xbf8df8a8,这里保存的是add函数的返回地址,它是由call指令压栈的。
(gdb) stepi // 注释: push %ebp
0x08048355 2 {
(gdb) stepi // 注释:mov %esp,%ebp
0x08048357 2 {
(gdb) stepi // 注释:sub $0x10,%esp
3 int a = 0;
(gdb) info registers esp
esp 0xbf8df894 0xbf8df894
(gdb) info registers ebp
ebp 0xbf8df8a4 0xbf8df8a4
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x002daff4 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
刚刚执行完的3条指令是函数入口的定式。
现在我们可以看到,main的栈还是原样,向下增长之后就是add的栈。
0xbf8df8a4(ebp)保存的是上层函数main的帧指针
0xbf8df8a8(ebp+4)保存的是返回地址
0xbf8df8ac(ebp+8)保存的是最后一个入栈的参数x
0xbf8df8b0(ebp+C)保存的是倒数第二个入栈的参数y
0xbf8df8a0(ebp-4)保存的是局部变量a,现在是一个不确定值
接下来add函数就真正开始干活了
(gdb) stepi // 注释: movl $0x0,0xfffffffc(%ebp)
4 a = x;
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x00000000 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
可以看到a被置0了
(gdb) stepi // 注释: mov 0x8(%ebp),%eax
0x08048364 4 a = x;
(gdb) stepi // 注释:mov %eax,0xfffffffc(%ebp)
5 a += y;
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x00000012 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
参数x(ebp+8)的值通过eax赋值给了局部变量a(ebp-4)
(gdb) stepi // 注释: mov 0xc(%ebp),%eax
0x0804836a 5 a += y;
(gdb) stepi // 注释:add %eax,0xfffffffc(%ebp)
6 return a;
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x00000046 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
参数y(ebp+C)的值通过eax加到了局部变量a(ebp-4)
现在要从add返回了。返回之前把局部变量a(ebp-4)保存到eax用作返回值
(gdb) stepi // 注释:mov 0xfffffffc(%ebp),%eax
7 }
(gdb) stepi // 注释: leave
0x08048371 in add (x=1686688, y=134513616) at stack.c:7
7 }
(gdb) stepi // 注释: ret
0x080483a3 in main () at stack.c:13
13 result = add(x, y);
现在我们回到了main,栈现在是这样的
(gdb) info registers esp
esp 0xbf8df8ac 0xbf8df8ac
(gdb) info registers ebp
ebp 0xbf8df8c8 0xbf8df8c8
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x00000046 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x001903d0 0xbf8df8e0 0xbf8df938 0x001b4dec
可以看到,esp和ebp都已经恢复到了调用add之前的值。
但是,调用add的两个参数还在栈里(0xbf8df8ac、0xbf8df8b0,都在esp以上)。
也就是说,被调用的函数add没有把它们从栈上清出去,需要调用方main来清理。这就是著名的“调用者清栈”,cdecl调用方式的特点之一。
(gdb) stepi // 注释: mov %eax,0xfffffff8(%ebp)
14 return 0;
(gdb) x/16 0xbf8df890
0xbf8df890: 0x00000000 0x08049574 0xbf8df8a8 0x08048245
0xbf8df8a0: 0x00000046 0xbf8df8c8 0x080483a3 0x00000012
0xbf8df8b0: 0x00000034 0xbf8df96c 0x00000012 0x00000034
0xbf8df8c0: 0x00000046 0xbf8df8e0 0xbf8df938 0x001b4dec
从eax得到函数add的返回值,赋值给了局部变量result(ebp-8)
(gdb) stepi // 注释: mov $0x0,%eax ;把eax置0作为main的返回值
15 }
(gdb) stepi // 注释:add $0x18,%esp ; 调用者清栈
0x080483ae 15 }
(gdb) continue
Continuing.
Program exited normally.
(gdb) quit
[test]$