先看一个最简单的程序:
点击(此处)折叠或打开
- /*test.c*/
- #include <stdio.h>
- int foo1(int m,int n,int p)
- {
- int x = m + n + p;
- return x;
- }
- int main(int argc,char** argv)
- {
- int x,y,z,result;
- x=11;
- y=22;
- z=33;
- result = foo1(x,y,z);
- printf("result=%d\n",result);
- return 0;
- }
点击(此处)折叠或打开
- .file "test.c"
- .text
- .globl foo1
- .type foo1, @function
- foo1:
- pushl %ebp
- movl %esp, %ebp
- subl $16, %esp
- movl 12(%ebp), %eax
- movl 8(%ebp), %edx
- leal (%edx,%eax), %eax
- addl 16(%ebp), %eax
- movl %eax, -4(%ebp)
- movl -4(%ebp), %eax
- leave
- ret
- .size foo1, .-foo1
- .section .rodata
- .LC0:
- .string "result=%d\n"
- .text
- .globl main
- .type main, @function
- main:
- pushl %ebp
- movl %esp, %ebp
- andl $-16, %esp
- subl $32, %esp
- movl $11, 16(%esp)
- movl $22, 20(%esp)
- movl $33, 24(%esp)
- movl 24(%esp), %eax
- movl %eax, 8(%esp)
- movl 20(%esp), %eax
- movl %eax, 4(%esp)
- movl 16(%esp), %eax
- movl %eax, (%esp)
- call foo1
- movl %eax, 28(%esp)
- movl $.LC0, %eax
- movl 28(%esp), %edx
- movl %edx, 4(%esp)
- movl %eax, (%esp)
- call printf
- movl $0, %eax
- leave
- ret
- .size main, .-main
- .ident "GCC: (GNU) 4.4.4 20100726 (Red Hat 4.4.4-13)"
- .section .note.GNU-stack,"",@progbits
[root@maple 1]# gcc -g -o test test.s [root@maple 1]# objdump -D test > testbin [root@maple 1]# vi testbin //… 省略部分不相关代码 80483c0: ff d0 call *%eax 80483c2: c9 leave 80483c3: c3 ret
080483c4 : 80483c4: 55 push %ebp 80483c5: 89 e5 mov %esp,%ebp 80483c7: 83 ec 10 sub $0x10,%esp 80483ca: 8b 45 0c mov 0xc(%ebp),%eax 80483cd: 8b 55 08 mov 0x8(%ebp),%edx 80483d0: 8d 04 02 lea (%edx,%eax,1),%eax 80483d3: 03 45 10 add 0x10(%ebp),%eax 80483d6: 89 45 fc mov %eax,-0x4(%ebp) 80483d9: 8b 45 fc mov -0x4(%ebp),%eax 80483dc: c9 leave 80483dd: c3 ret
080483de :80483de: 55 push %ebp 80483df: 89 e5 mov %esp,%ebp 80483e1: 83 e4 f0 and $0xfffffff0,%esp 80483e4: 83 ec 20 sub $0x20,%esp 80483e7: c7 44 24 10 0b 00 00 movl $0xb,0x10(%esp) 80483ee: 00 80483ef: c7 44 24 14 16 00 00 movl $0x16,0x14(%esp) 80483f6: 00 80483f7: c7 44 24 18 21 00 00 movl $0x21,0x18(%esp) 80483fe: 00 80483ff: 8b 44 24 18 mov 0x18(%esp),%eax 8048403: 89 44 24 08 mov %eax,0x8(%esp) 8048407: 8b 44 24 14 mov 0x14(%esp),%eax 804840b: 89 44 24 04 mov %eax,0x4(%esp) 804840f: 8b 44 24 10 mov 0x10(%esp),%eax 8048413: 89 04 24 mov %eax,(%esp) 8048416: e8 a9 ff ff ff call 80483c4 804841b: 89 44 24 1c mov %eax,0x1c(%esp) 804841f: b8 04 85 04 08 mov $0x8048504,%eax 8048424: 8b 54 24 1c mov 0x1c(%esp),%edx 8048428: 89 54 24 04 mov %edx,0x4(%esp) 804842c: 89 04 24 mov %eax,(%esp) 804842f: e8 c0 fe ff ff call 80482f4 8048434: b8 00 00 00 00 mov $0x0,%eax 8048439: c9 leave 804843a: c3 ret 804843b: 90 nop 804843c: 90 nop //… 省略部分不相关代码 |
用GDB调试可执行程序test:
在 main 函数第一条指令执行前我们看一下进程 test 的栈空间布局。因为我们最终的可执行程序是通过 glibc 库启动的,在 main 的第一条指令运行前,其实还有很多故事的,这里就不展开了,以后有时间再细究,这里只要记住一点: main 函数执行前,其进程空间的栈里已经有了相当多的数据。我的系统里此时栈顶指针 esp 的值是 0xbffff63c ,栈基址指针 ebp 的值 0xbffff6b8 ,指令寄存器 eip 的值是 0x80483de 正好是下一条马上即将执行的指令,即 main 函数内的第一条指令“ push %ebp ”。那么此时, test 进程的栈空间布局大致如下:
点击(此处)折叠或打开
- 25 pushl %ebp //将原来ebp的值0xbffff6b8如栈,esp自动增长4字节
- 26 movl %esp, %ebp //用ebp保存当前时刻esp的值
- 27 andl $-16, %esp //内存地址对其,可以忽略不计
然后main函数里的变量x,y,z的值放到栈上,就是接下来的三条指令:
点击(此处)折叠或打开
- 29 movl $11, 16(%esp)
- 30 movl $22, 20(%esp)
- 31 movl $33, 24(%esp)
这是三条寄存器间接寻址指令,将立即数11,22,33分别放到esp寄存器所指向的地址0xbffff610向高位分别偏移16、20、24个字节处的内存单元里,最后结果如下:
注意:这三条指令并没有改变 esp 寄存器的值。
接下来main函数里就要为了调用foo1函数而做准备了。由于mov指令的两个操作数不能都是内存地址,所以要将x,y和z的值传递给foo1函数,则必须借助通用寄存器来完成,这里我们看到eax承担了这样的任务:
点击(此处)折叠或打开
- 32 movl 24(%esp), %eax
- 33 movl %eax, 8(%esp)
- 34 movl 20(%esp), %eax
- 35 movl %eax, 4(%esp)
- 36 movl 16(%esp), %eax
- 37 movl %eax, (%esp)
当foo1函数所需要的所有输入参数都已经按正确的顺序入栈后,紧接着就需要调用call指令来执行foo1函数的代码了。前面的博文说过,call指令执行时分两步:首先会将call指令的下一条指令(movl %eax, 28(%esp))的地址(0x0804841b)压入栈,然后跳转到函数foo1入口处开始执行。当第38条指令“call foo1”执行完后,栈空间布局如下:
点击(此处)折叠或打开
- 3 .globl foo1
- 4 .type foo1, @function
- 5 foo1:
- 6 pushl %ebp
- 7 movl %esp, %ebp
- 8 subl $16, %esp
- 9 movl 12(%ebp), %eax
- 10 movl 8(%ebp), %edx
- 11 leal (%edx,%eax), %eax
- 12 addl 16(%ebp), %eax
- 13 movl %eax, -4(%ebp)
- 14 movl -4(%ebp), %eax
- 15 leave
- 16 ret
- 17 .size foo1, .-foo1
因为我们 foo1() 函数的 C 代码中,最终计算结果是保存到 foo1() 里的局部变量 x 里,最后用 return 语句将 x的值通过eax寄存器 返回到 mian 函数里,所以 我们看到接下来的第 13 、 14 条指令有些“多此一举”。这足以说明 gcc 人家还是相当严谨的, C 源代码的函数里如果有给局部变量赋值的语句,生成汇编代码时确实会在栈上为本地变量预留的空间里的正确位置为其赋值。当然 gcc 还有不同级别的优化技术来提高程序的执行效率,这个不属于本文所讨论的东西。 让我们继续,当第13、14条指令执行完后,栈布局如下:
我们发现,虽然栈顶从0xbffff5f8移动到0xbffff60c了,但栈上的数据依然存在。也就是说,此时你通过esp-8依旧可以访问foo1函数里的局部变量x的值。当然,这也是说得通的,因为函数此时还没有返回。我们看栈布局可以知道当前的栈顶0xbffff60c处存放的是下一条即将执行的指令的地址,对照反汇编结果可以看到这正是main函数里的第18条指令(在整个汇编源文件test.s里的行号是39)“movl %eax, 28(%esp)”。leave指令其实完成了两个任务:
1、将栈上为函数预留的空间“收回”;
2、恢复ebp;
也就是说leave指令等价于下面两条指令,你将leave替换成它们编译运行,结果还是对的:
点击(此处)折叠或打开
- movl %ebp,%esp
- popl %ebp
前面我们也说过,ret指令会自动到栈上去pop数据,相当于执行了“popl %eip”,会使esp增大4字节。所以当执行完第16条指令ret后,esp从0xbffff60c增长到0xbffff610处,栈空间结构如下:
点击(此处)折叠或打开
- 40 movl $.LC0, %eax
- 41 movl 28(%esp), %edx
- 42 movl %edx, 4(%esp)
- 43 movl %eax, (%esp)
所以,最后关于 C 的函数调用,我们可以总结一下:
1 、函数输入参数的入栈顺序是函数原型中形参从右至左的原则;
2 、汇编语言里调用函数通常情况下 都用 call指令来完成 ;
3、汇编语言里的函数大部分情况下都符合以下的函数模板:
点击(此处)折叠或打开
- .globl fun_name
- .type fun_name, @function
- fun_name:
- pushl %ebp
- movl %esp, %ebp
- <函数主体代码>
- leave
- ret
如果我们有个函数原型:int funtest(int x,int y int z char* ptr) , 在汇编层面,当调用它时栈的布局结构一般是下面这个样子:
而有些资料上将ebp指向函数返回地址的地方,这是不对的。正常情况下应该是ebp指向old ebp才对,这样函数末尾的leave和ret指令才可以正常工作。