过去我一直以为,函数参数就只是通过栈传递的,最近看一些反汇编代码,发现好多时候都是通过寄存器。做了个实验,最终明白了,函数的参数怎样传递,其实是和参数个数有关。
在linux下的objdump反汇编的结果:如果参数很多,有一些参数会通过栈传递,否则通过寄存器。
在windows下的VS反汇编的结果:都通过栈。
C代码:
#include <stdio.h> void func1(int a, int b) { printf("%d", a+b); } void func2(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k) { printf("%d",a+b+c+d+e+f+g+h+i+j+k); } void main () { int a = 1; int b = 1; int c = 1; int d = 1; int e = 1; int f = 1; int g = 1; int h = 1; int i = 1; int j = 1; int k = 1; func1(a, b); func2(a, b, c, d, e, f, g, h, i, j, k); }
linux下通过objdump反汇编:
[root@VM_120_194_centos C]# gcc -E 20170704.c -o 20170704.i [root@VM_120_194_centos C]# gcc -S 20170704.i -o 20170704.s [root@VM_120_194_centos C]# gcc -c 20170704.s -o 20170704.o [root@VM_120_194_centos C]# objdump -S 20170704.o 20170704.o: 文件格式 elf64-x86-64 Disassembly of section .text: 0000000000000000 <func1>: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 10 sub $0x10,%rsp 8: 89 7d fc mov %edi,-0x4(%rbp) b: 89 75 f8 mov %esi,-0x8(%rbp) e: 8b 45 f8 mov -0x8(%rbp),%eax 11: 8b 55 fc mov -0x4(%rbp),%edx 14: 01 d0 add %edx,%eax 16: 89 c6 mov %eax,%esi 18: bf 00 00 00 00 mov $0x0,%edi 1d: b8 00 00 00 00 mov $0x0,%eax 22: e8 00 00 00 00 callq 27 <func1+0x27> 27: c9 leaveq 28: c3 retq 0000000000000029 <func2>: 29: 55 push %rbp 2a: 48 89 e5 mov %rsp,%rbp 2d: 48 83 ec 20 sub $0x20,%rsp 31: 89 7d fc mov %edi,-0x4(%rbp) 34: 89 75 f8 mov %esi,-0x8(%rbp) 37: 89 55 f4 mov %edx,-0xc(%rbp) 3a: 89 4d f0 mov %ecx,-0x10(%rbp) 3d: 44 89 45 ec mov %r8d,-0x14(%rbp) 41: 44 89 4d e8 mov %r9d,-0x18(%rbp) 45: 8b 45 f8 mov -0x8(%rbp),%eax 48: 8b 55 fc mov -0x4(%rbp),%edx 4b: 01 c2 add %eax,%edx 4d: 8b 45 f4 mov -0xc(%rbp),%eax 50: 01 c2 add %eax,%edx 52: 8b 45 f0 mov -0x10(%rbp),%eax 55: 01 c2 add %eax,%edx 57: 8b 45 ec mov -0x14(%rbp),%eax 5a: 01 c2 add %eax,%edx 5c: 8b 45 e8 mov -0x18(%rbp),%eax 5f: 01 c2 add %eax,%edx 61: 8b 45 10 mov 0x10(%rbp),%eax 64: 01 c2 add %eax,%edx 66: 8b 45 18 mov 0x18(%rbp),%eax 69: 01 d0 add %edx,%eax 6b: 89 c6 mov %eax,%esi 6d: bf 00 00 00 00 mov $0x0,%edi 72: b8 00 00 00 00 mov $0x0,%eax 77: e8 00 00 00 00 callq 7c <func2+0x53> 7c: c9 leaveq 7d: c3 retq 000000000000007e <main>: 7e: 55 push %rbp 7f: 48 89 e5 mov %rsp,%rbp 82: 48 83 ec 60 sub $0x60,%rsp 86: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp) 8d: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp) 94: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%rbp) 9b: c7 45 f0 01 00 00 00 movl $0x1,-0x10(%rbp) a2: c7 45 ec 01 00 00 00 movl $0x1,-0x14(%rbp) a9: c7 45 e8 01 00 00 00 movl $0x1,-0x18(%rbp) b0: c7 45 e4 01 00 00 00 movl $0x1,-0x1c(%rbp) b7: c7 45 e0 01 00 00 00 movl $0x1,-0x20(%rbp) be: c7 45 dc 01 00 00 00 movl $0x1,-0x24(%rbp) c5: c7 45 d8 01 00 00 00 movl $0x1,-0x28(%rbp) cc: c7 45 d4 01 00 00 00 movl $0x1,-0x2c(%rbp) d3: 8b 55 f8 mov -0x8(%rbp),%edx d6: 8b 45 fc mov -0x4(%rbp),%eax d9: 89 d6 mov %edx,%esi db: 89 c7 mov %eax,%edi dd: e8 00 00 00 00 callq e2 <main+0x64> e2: 44 8b 4d e8 mov -0x18(%rbp),%r9d e6: 44 8b 45 ec mov -0x14(%rbp),%r8d ea: 8b 4d f0 mov -0x10(%rbp),%ecx ed: 8b 55 f4 mov -0xc(%rbp),%edx f0: 8b 75 f8 mov -0x8(%rbp),%esi f3: 8b 45 fc mov -0x4(%rbp),%eax f6: 8b 7d d4 mov -0x2c(%rbp),%edi f9: 89 7c 24 20 mov %edi,0x20(%rsp) fd: 8b 7d d8 mov -0x28(%rbp),%edi 100: 89 7c 24 18 mov %edi,0x18(%rsp) 104: 8b 7d dc mov -0x24(%rbp),%edi 107: 89 7c 24 10 mov %edi,0x10(%rsp) 10b: 8b 7d e0 mov -0x20(%rbp),%edi 10e: 89 7c 24 08 mov %edi,0x8(%rsp) 112: 8b 7d e4 mov -0x1c(%rbp),%edi 115: 89 3c 24 mov %edi,(%rsp) 118: 89 c7 mov %eax,%edi 11a: e8 00 00 00 00 callq 11f <main+0xa1> 11f: c9 leaveq 120: c3 retq
func1和func2两个函数的反汇编代码不用管,我们看main函数里的。可以清晰看到,86 - cc是给变量(栈空间内存)赋值的过程,也就是
int a = 1;int b = 1;int c = 1;int d = 1;int e = 1;int f = 1;int g = 1;int h = 1;int i = 1;int j = 1;int k = 1;
d3 - dd是func1调用的过程。需要两个参数,赋值给edi和esi寄存器。也就是说,这里函数参数是存在寄存器里的。
func1里面,则是先将edi和esi的内容copy到自己的栈空间。这就是C语言书里说的所谓“值传递”吧?? 也不知道谁发明的这种词。以汇编视角看,就是内存的copy而已。
func1的调用是,main函数的e2 - 11a,这个很有趣,前一部分变量是存在寄存器里(和上面讲的一个道理),而寄存器不够的时候(参数太多了),就通过rsp栈指针寄存器,直接存到下一个要调用的函数的栈空间里面。好有趣。
windows下VS反汇编代码:
void main () { 00BF1540 push ebp 00BF1541 mov ebp,esp 00BF1543 sub esp,144h 00BF1549 push ebx 00BF154A push esi 00BF154B push edi 00BF154C lea edi,[ebp-144h] 00BF1552 mov ecx,51h 00BF1557 mov eax,0CCCCCCCCh 00BF155C rep stos dword ptr es:[edi] int a = 1; 00BF155E mov dword ptr [a],1 int b = 1; 00BF1565 mov dword ptr [b],1 int c = 1; 00BF156C mov dword ptr [c],1 int d = 1; 00BF1573 mov dword ptr [d],1 int e = 1; 00BF157A mov dword ptr [e],1 int f = 1; 00BF1581 mov dword ptr [f],1 int g = 1; 00BF1588 mov dword ptr [g],1 int h = 1; 00BF158F mov dword ptr [h],1 int i = 1; 00BF1596 mov dword ptr [i],1 int j = 1; 00BF159D mov dword ptr [j],1 int k = 1; 00BF15A4 mov dword ptr [k],1 func1(a, b); 00BF15AB mov eax,dword ptr [b] 00BF15AE push eax 00BF15AF mov ecx,dword ptr [a] 00BF15B2 push ecx 00BF15B3 call func1 (0BF11F4h) 00BF15B8 add esp,8 func2(a, b, c, d, e, f, g, h, i, j, k); 00BF15BB mov eax,dword ptr [k] 00BF15BE push eax 00BF15BF mov ecx,dword ptr [j] 00BF15C2 push ecx 00BF15C3 mov edx,dword ptr [i] 00BF15C6 push edx 00BF15C7 mov eax,dword ptr [h] 00BF15CA push eax 00BF15CB mov ecx,dword ptr [g] 00BF15CE push ecx 00BF15CF mov edx,dword ptr [f] 00BF15D2 push edx 00BF15D3 mov eax,dword ptr [e] 00BF15D6 push eax 00BF15D7 mov ecx,dword ptr [d] 00BF15DA push ecx 00BF15DB mov edx,dword ptr [c] 00BF15DE push edx 00BF15DF mov eax,dword ptr [b] 00BF15E2 push eax 00BF15E3 mov ecx,dword ptr [a] 00BF15E6 push ecx 00BF15E7 call func2 (0BF11FEh) 00BF15EC add esp,2Ch } 00BF15EF xor eax,eax 00BF15F1 pop edi 00BF15F2 pop esi 00BF15F3 pop ebx 00BF15F4 add esp,144h 00BF15FA cmp ebp,esp 00BF15FC call __RTC_CheckEsp (0BF1140h) 00BF1601 mov esp,ebp 00BF1603 pop ebp 00BF1604 ret
dword ptr [x]是指取标号为x,地址内的值,大小是双字double word。
这里代码清晰的多,看到push指令就明白了。
可以看出,这里反汇编代码,不管参数多少,都是通过栈来传参的。
至于【在linux下的objdump反汇编的结果】与【在windows下的VS反汇编的结果】为何有这种差别?我就不清楚了。
具体区别也有:
(1)为何VS下反汇编函数调用时,固定要将ebx、esi、edi寄存器入栈(函数运行结束再pop),不管有没有必要。而linux objdump反汇编就没有这个过程,可能这个程序这样做没有意义就把这过程优化了吧。
(2)为何VS下反汇编是通过push指令入栈,而linux objdump是通过操作rsp栈指针入栈。虽然作用都是一样的。
其实二者都是大同小异了。不管是通过寄存器传递,还是通过栈传递,最后被调用函数内部都是只用栈内存来存参数的。
附一张之前画的函数调用栈内存开辟、回收过程示意图:
这张图有一个没描绘到的部分,就是ip的入栈,是通过call指令达到的。