相同的c/c++语言的函数调用,在编译成汇编代码之后,会存在不同的解析方式,这些解析方式一方面依赖于编译器本身(比如gcc和g++编译出的代码就不尽相同),另一方面也会受到编译选项的影响(比如是否打开-O2 -fno-stack-protector这些编译开关)。网上很多其他的资料无视了实现函数调用的汇编代码的多样性,使得读者会在对照理解时出现一些困难。
在这篇文章中,我将通过几个小例子,介绍不同情况汇编实现C/C++语言函数调用的方式。具体的代码可以在 我的github 上面看到。
下图是一个简单的读入字符串的程序(注意,这个程序故意引入了不安全的gets函数)的C语言版本和C++版本——
#include<stdio.h> #define BUFFER_SIZE 5 int getbuf() { char buf[BUFFER_SIZE]; gets(buf); return 1; } int main() { int t = getbuf(); printf("%d\n",t); }
#include<cstdio> #include<iostream> using namespace std; #define BUFFER_SIZE 5 int getbuf() { char buf[BUFFER_SIZE]; gets(buf); return 1; } int main() { int t = getbuf(); cout<<t<<endl; }
这个程序中的getbuf函数就是我们今天讨论的主题
通过栈帧实现的函数调用
简单的例子
这是C语言代码版本通过编译命令gcc c.c -o c -fno-stack-protector 生成的汇编代码——
0000000000400566 <getbuf>: 400566: 55 push %rbp 400567: 48 89 e5 mov %rsp,%rbp 40056a: 48 83 ec 10 sub0x0,%eax 40057a: e8 d1 fe ff ff callq 400450 <gets@plt> 40057f: b8 01 00 00 00 mov
0x10,%rsp 分配整块的空间。
- 调用函数时,%rbp 仍然指向的是上一层函数的帧,需要更新。为了之后可以恢复,需要将当前的%rbp 的值使用push指令,放在栈顶,然后将%rbp 赋值为%rsp ,标记为“当前帧指针就是刚进入函数,没有分配任何内容的栈顶指针”。同时,新的%rbp 指向的位置存有%rbp 在函数退出时需要恢复的值。
- 退出函数时,需要将%rbp更新回去。
汇编代码解读
回到之前的汇编代码,我们看看调用getbuf之后,究竟干了些啥:0000000000400586 <main>: 400586: 55 push %rbp 400587: 48 89 e5 mov %rsp,%rbp 40058a: 48 83 ec 10 sub0x0,%eax 400593: e8 ce ff ff ff callq 400566 <getbuf> 400598: 89 45 fc mov %eax,-0x4(%rbp) 40059b: 8b 45 fc mov -0x4(%rbp),%eax 40059e: 89 c6 mov %eax,%esi 4005a0: bf 44 06 40 00 mov
0x0,%eax 4005aa: e8 81 fe ff ff callq 400430 <printf@plt> 4005af: b8 00 00 00 00 mov
0x10,%rsp 40056e: 48 8d 45 f0 lea -0x10(%rbp),%rax 400572: 48 89 c7 mov %rax,%rdi 400575: b8 00 00 00 00 mov
0x1,%eax 400584: c9 leaveq 400585: c3 retq
整个getbuf的调用依次是——
- 【位于main中】call指令,将程序结束下一条指令地址%rip push至栈顶,并将call的函数入口地址赋值给%rip ,实现指令流的跳转。
- 保存旧的%rbp 至栈顶,此时push使得%rsp 加了1
- 将栈顶指针%rsp 赋值到%rbp 中,此时新的%rbp 可以理解成main函数内部变量和getbuf函数内部变量的分界线,而这个位置之后会作为getbuf函数内部变量定位的基准。
- 函数内部变量需要使用接下来0x10大小的空间,在栈中预先分配
- 我们将分配的那一块空间用来存buf数组,那么buf指针的值就应该是%rbp-0x10 了
- 将存有buf数组的寄存器%rax 赋值给%rdi
- 将零时储存器%rax 清零
- 调用gets函数
- 将储存器%rax 赋值为1作为返回值
- 使用leaveq指令恢复栈帧%rbp
- 使用ret指令将栈顶储存的“函数退出后下一条语句地址”弹出,并重定向为下一句执行位置。
C语言与C++语言
C++语言的getbuf函数汇编代码如下,可以发现,除了函数符号名有些区别外,没有其他的区别。
0000000000400866 <_Z6getbufv>: 400866: 55 push %rbp 400867: 48 89 e5 mov %rsp,%rbp 40086a: 48 83 ec 10 sub0x1,%eax 40087f: c9 leaveq 400880: c3 retq
只使用栈指针实现函数调用
倘若在编译时,添加-O2优化,则C语言代码的getbuf部分变成了:
00000000004005c0 <getbuf>: 4005c0: 48 83 ec 18 sub0x1,%eax 4005d3: 48 83 c4 18 add
0x30,%rsp 40056e: 89 7d ec mov %edi,-0x14(%rbp) 400571: 89 75 e8 mov %esi,-0x18(%rbp) 400574: 89 55 e4 mov %edx,-0x1c(%rbp) 400577: 89 4d e0 mov %ecx,-0x20(%rbp) 40057a: 44 89 45 dc mov %r8d,-0x24(%rbp) 40057e: 44 89 4d d8 mov %r9d,-0x28(%rbp) 400582: 8b 45 ec mov -0x14(%rbp),%eax 400585: 88 45 f0 mov %al,-0x10(%rbp) 400588: 8b 45 e8 mov -0x18(%rbp),%eax 40058b: 88 45 f1 mov %al,-0xf(%rbp) 40058e: 8b 45 e4 mov -0x1c(%rbp),%eax 400591: 88 45 f2 mov %al,-0xe(%rbp) 400594: 8b 45 e0 mov -0x20(%rbp),%eax 400597: 88 45 f3 mov %al,-0xd(%rbp) 40059a: 8b 45 dc mov -0x24(%rbp),%eax 40059d: 88 45 f4 mov %al,-0xc(%rbp) 4005a0: 8b 45 d8 mov -0x28(%rbp),%eax 4005a3: 88 45 f0 mov %al,-0x10(%rbp) 4005a6: 8b 45 10 mov 0x10(%rbp),%eax 4005a9: 88 45 f1 mov %al,-0xf(%rbp) 4005ac: 8b 45 18 mov 0x18(%rbp),%eax 4005af: 88 45 f2 mov %al,-0xe(%rbp) 4005b2: 8b 45 20 mov 0x20(%rbp),%eax 4005b5: 88 45 f3 mov %al,-0xd(%rbp) 4005b8: 8b 45 28 mov 0x28(%rbp),%eax 4005bb: 88 45 f4 mov %al,-0xc(%rbp) 4005be: 48 8d 45 f0 lea -0x10(%rbp),%rax 4005c2: 48 89 c7 mov %rax,%rdi 4005c5: b8 00 00 00 00 mov
0x1,%eax 4005d4: c9 leaveq 4005d5: c3 retq
以及调用者main函数的反汇编代码。
00000000004005d6 <main>: 4005d6: 55 push %rbp 4005d7: 48 89 e5 mov %rsp,%rbp 4005da: 48 83 ec 10 sub0xa 4005e0: 6a 09 pushq
0x8 4005e4: 6a 07 pushq
0x6,%r9d 4005ec: 41 b8 05 00 00 00 mov
0x4,%ecx 4005f7: ba 03 00 00 00 mov
0x2,%esi 400601: bf 01 00 00 00 mov
0x20,%rsp 40060f: 89 45 fc mov %eax,-0x4(%rbp) 400612: 8b 45 fc mov -0x4(%rbp),%eax 400615: 89 c6 mov %eax,%esi 400617: bf b4 06 40 00 mov
0x0,%eax 400621: e8 0a fe ff ff callq 400430 <printf@plt> 400626: b8 00 00 00 00 mov
0x10,%rsp 4005de: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 4005e5: 00 00 4005e7: 48 89 45 f8 mov %rax,-0x8(%rbp) 4005eb: 31 c0 xor %eax,%eax 4005ed: 48 8d 45 f0 lea -0x10(%rbp),%rax 4005f1: 48 89 c7 mov %rax,%rdi 4005f4: b8 00 00 00 00 mov
0x1,%eax 400603: 48 8b 55 f8 mov -0x8(%rbp),%rdx 400607: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx 40060e: 00 00 400610: 74 05 je 400617 <getbuf+0x41> 400612: e8 79 fe ff ff callq 400490 <__stack_chk_fail@plt> 400617: c9 leaveq 400618: c3 retq
多出来的一部分代码被称为金丝雀,是防止gets操作产生危险后果的,具体原理有时间再更~
环境与配置
gcc版本 (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609
g++版本 (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609
Ubuntu版本 16.04 64位
《解析汇编中实现函数调用的若干方式》有一个想法