過去我一直以為,函數參數就只是通過棧傳遞的,最近看一些反匯編代碼,發現好多時候都是通過寄存器。做了個實驗,最終明白了,函數的參數怎樣傳遞,其實是和參數個數有關。
在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指令達到的。