main函數及其子函數之間的棧
1 工具及實驗程序
本文的實驗在一個虛擬機中進行,虛擬機模擬的cpu是x86-64(Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz),運行的是64bit ubuntu,安裝了ARM64的工具鏈:
$sudo apt-get install gcc-aarch64-linux-gnu
$sudo apt-get install gcc-arm-linux-gnueabi
實驗使用的程序爲:
#include<stdio.h>
#include <stdlib.h>
int func_C(int x1, int x2, int x3, int x4, int x5, int x6){
int sum = 0;
sum = x1 + x2;
sum = sum + x3 + x4;
sum = sum + x5 + x6;
return sum;
}
int func_B(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8, char x9){
int sum = 0;
sum = func_C(x1, x2, x3, x4, x5,x6);
sum = sum + x7;
sum = sum + x8;
sum += x9;
return sum;
}
void func_A(void){
int sum = 0;
int x1 = 1;
int x2 = 2;
int x3 = 3;
int x4 = 4;
int x5 = 5;
int x6 = 6;
int x7 = 7;
int x8 = 8;
char x9 = 'c';
sum = func_B(x1, x2, x3, x4, x5, x6, x7, x8, x9);
printf("sum = %d\n", sum);
}
int main(int argc, char *argv[], char *envp[])
{
int count = argc;
char **p_argv = argv;
char **p_env = envp;
func_A();
return 0;
}
2 x86-64
2.1 main函數的棧
int main(int argc, char *argv[], char *envp[])
{
int count = argc;
char **p_argv = argv;
char **p_env = envp;
func_A();
return 0;
}
首先,編譯源程序 $gcc test.c -o test -fno-stack-protector
,然後反彙編出main函數 $gdb test
:
(gdb) disas main
Dump of assembler code for function main:
0x0000000000000790 <+0>: push %rbp
0x0000000000000791 <+1>: mov %rsp,%rbp
0x0000000000000794 <+4>: sub $0x40,%rsp
0x0000000000000798 <+8>: mov %edi,-0x24(%rbp)
0x000000000000079b <+11>: mov %rsi,-0x30(%rbp)
0x000000000000079f <+15>: mov %rdx,-0x38(%rbp)
0x00000000000007a3 <+19>: mov -0x24(%rbp),%eax
0x00000000000007a6 <+22>: mov %eax,-0x4(%rbp)
0x00000000000007a9 <+25>: mov -0x30(%rbp),%rax
0x00000000000007ad <+29>: mov %rax,-0x10(%rbp)
0x00000000000007b1 <+33>: mov -0x38(%rbp),%rax
0x00000000000007b5 <+37>: mov %rax,-0x18(%rbp)
0x00000000000007b9 <+41>: callq 0x6fd <func_A>
0x00000000000007be <+46>: mov $0x0,%eax
0x00000000000007c3 <+51>: leaveq
0x00000000000007c4 <+52>: retq
End of assembler dump.
從上面對main
函數棧的分析可以知道,一個函數棧大致會做以下幾件事:
- 保存上一個棧幀的信息。
0x0000000000000790 <+0>: push %rbp 0x0000000000000791 <+1>: mov %rsp,%rbp
在x86-64上rbp和rsp完全可以描繪出一個棧幀,rbp被稱爲幀指針(frame pointer),rsp被稱爲棧指針(stack pointer);rbp+0x08指向當前棧幀的底部,rsp指向棧幀的頂部。下面的第一句是將上一個棧幀的幀指針rbp壓棧保存起來;rbp保存起來後,緊接着下一句彙編就爲rbp賦予一個新值,將棧指針rsp的值賦給幀指針rbp,讓rbp指向當前棧幀的底部,其實rbp+0x08纔是當前棧幀的底部,只不過rbp+0x08處是上一個函數運行call指令時硬件自動存放的rip,對軟件不可見。
- 開棧。
0x0000000000000794 <+4>: sub $0x40,%rsp
也就是開闢當前棧幀的空間,開闢的棧幀空間主要用於接收參數、存放局部變量以及運算的場所,下面的彙編開闢0x40個字節的空間。
- 接收參數。
0x0000000000000798 <+8>: mov %edi,-0x24(%rbp) 0x000000000000079b <+11>: mov %rsi,-0x30(%rbp) 0x000000000000079f <+15>: mov %rdx,-0x38(%rbp)
前面也說過,x86-64參數傳遞的規則是rdi傳遞第一個參數、rsi傳遞第二個參數、rdx傳遞第三個參數....。
mian
函數的一個形參是argc,第二個形參是argv,第三個形參是envp。從下面的彙編也可以看出,實參在rdi、rsi和rdx中,然後分別放到rbp - 0x24 、rbp - 0x30和rbp -0x38形參的位置處。
- 棧上運算。
0x00000000000007a3 <+19>: mov -0x24(%rbp),%eax 0x00000000000007a6 <+22>: mov %eax,-0x4(%rbp) 0x00000000000007a9 <+25>: mov -0x30(%rbp),%rax 0x00000000000007ad <+29>: mov %rax,-0x10(%rbp) 0x00000000000007b1 <+33>: mov -0x38(%rbp),%rax 0x00000000000007b5 <+37>: mov %rax,-0x18(%rbp) 0x00000000000007b9 <+41>: callq 0x6fd <func_A>
棧的最大作用就是作爲運算場所,
main
的棧上並沒有安排太多的運算,僅僅做了三次賦值,然後主要工作就就轉給子函數了。下面的一二兩句的作用是將argc的值賦值給count;三四句的作用是將argv的值賦給p_argv;五六兩句的作用是將envp賦值給p_envl;最後一句就是調用子函數,我們也把他看做是運算的一部分。
-
設置返回值。x86-64的函數返回值使用rax傳遞。從
return 0
可知函數的返回值是0,因此將0賦給eax。0x00000000000007be <+46>: mov $0x0,%eax
- 恢復上一個棧幀。先看一下相反的操作保存棧幀的兩個步驟:一是將幀指針(rbp)壓棧;二是將棧指針(rsp)的值賦值給幀指針(rbp),可知,上一個棧幀結構可以由當前的幀指針rbp推導出,也即下面彙編語句
leaveq
的作用,該語句的作用有兩個:一是將幀指針(rbp)的值賦值給棧指針(rsp),即mov %rbp, %rsp
;二是將幀指針(rbp)出棧,即pop %rbp
。正好和保存上一個棧幀結構的操作相反。leaveq
指令執行完後,幀指針(rbp)已經切換回caller的棧了,也即數據存儲區已經切換完成,只差函數控制切換。0x00000000000007c3 <+51>: leaveq
爲了更加深入的瞭解如何恢復棧幀,畫了一個如下所示的圖,rsp和rbp指向了current frame,也就是說寄存器rbp和rsp並沒有指向任何"previous frame",恢復上一個棧幀的核心問題在於如何讓rbp和rsp指向上一個棧幀,答案的鑰匙就是rbp寄存器。rbp指向的就是上一個rbp,rbp-16就是上一個rsp, 直覺上恢復上一個棧幀就是將rbp-16賦值給rsp,並將rbp指向的值賦值給rbp,顯示上述方法可以恢復rsp和rbp,但是事實上並沒有使用上述方法,而是利用棧自然而然的恢復上一個棧幀,所謂的自然而然就是怎麼來怎麼回去。首先讓rbp賦值給rsp,然後pop一次棧即可恢復rbp,再pop棧一次即可恢復rsp。“首先讓rbp賦值給rsp,然後pop一次棧即可恢復rbp”是人類發明的指令
leaveq
乾的,"再pop棧一次即可恢復rsp"是人類發明的指令ret
乾的。 - 函數控制轉移。完成函數控制切換,說白了也就是讓CPU接着執行caller函數中callee函數後面的指令。下面的指令使用棧指針指向的值恢復rip,功能可以按照
mov (%sp), %rip; addq $8,%rsp
或pop %rip
來理解。該指令執行完後,函數的控制以及棧指針(rsp)切換完成。retq
指令改變了rsp 和 rip的值。0x00000000000007c4 <+52>: retq
2. 2 子函數的棧
2.2.1 func_A的棧
void func_A(void){
int sum = 0;
int x1 = 1;
int x2 = 2;
int x3 = 3;
int x4 = 4;
int x5 = 5;
int x6 = 6;
int x7 = 7;
int x8 = 8;
char x9 = 'c';
sum = func_B(x1, x2, x3, x4, x5, x6, x7, x8, x9);
printf("sum = %d\n", sum);
}
首先,編譯源程序 $gcc test.c -o test -fno-stack-protector
,然後反彙編出func_A函數 $gdb test
:
(gdb) disas func_A
Dump of assembler code for function func_A:
0x00000000000006fd <+0>: push %rbp
0x00000000000006fe <+1>: mov %rsp,%rbp
0x0000000000000701 <+4>: sub $0x30,%rsp
0x0000000000000705 <+8>: movl $0x0,-0x4(%rbp)
0x000000000000070c <+15>: movl $0x1,-0x8(%rbp)
0x0000000000000713 <+22>: movl $0x2,-0xc(%rbp)
0x000000000000071a <+29>: movl $0x3,-0x10(%rbp)
0x0000000000000721 <+36>: movl $0x4,-0x14(%rbp)
0x0000000000000728 <+43>: movl $0x5,-0x18(%rbp)
0x000000000000072f <+50>: movl $0x6,-0x1c(%rbp)
0x0000000000000736 <+57>: movl $0x7,-0x20(%rbp)
0x000000000000073d <+64>: movl $0x8,-0x24(%rbp)
0x0000000000000744 <+71>: movb $0x63,-0x25(%rbp)
0x0000000000000748 <+75>: movsbl -0x25(%rbp),%edi
0x000000000000074c <+79>: mov -0x1c(%rbp),%r9d
0x0000000000000750 <+83>: mov -0x18(%rbp),%r8d
0x0000000000000754 <+87>: mov -0x14(%rbp),%ecx
0x0000000000000757 <+90>: mov -0x10(%rbp),%edx
0x000000000000075a <+93>: mov -0xc(%rbp),%esi
0x000000000000075d <+96>: mov -0x8(%rbp),%eax
0x0000000000000760 <+99>: push %rdi
0x0000000000000761 <+100>: mov -0x24(%rbp),%edi
0x0000000000000764 <+103>: push %rdi
0x0000000000000765 <+104>: mov -0x20(%rbp),%edi
0x0000000000000768 <+107>: push %rdi
0x0000000000000769 <+108>: mov %eax,%edi
0x000000000000076b <+110>: callq 0x699 <func_B>
0x0000000000000770 <+115>: add $0x18,%rsp
0x0000000000000774 <+119>: mov %eax,-0x4(%rbp)
0x0000000000000777 <+122>: mov -0x4(%rbp),%eax
0x000000000000077a <+125>: mov %eax,%esi
0x000000000000077c <+127>: lea 0xd1(%rip),%rdi # 0x854
0x0000000000000783 <+134>: mov $0x0,%eax
0x0000000000000788 <+139>: callq 0x520 <printf@plt>
0x000000000000078d <+144>: nop
0x000000000000078e <+145>: leaveq
0x000000000000078f <+146>: retq
End of assembler dump.
上述彙編做的事情也是那幾個,保存上一個棧幀的信息、開棧、接受參數、棧上運算....,下面將進行分析。
- 保存上一個棧幀的信息
0x00000000000006fd <+0>: push %rbp 0x00000000000006fe <+1>: mov %rsp,%rbp
保存上一個棧幀的作用就是爲了該函數被調用完成後還能再回到caller函數的棧幀繼續執行,需要把caller的棧幀保存起來,保存地點就是callee的棧幀上。上述彙編的第一句就是把幀指針rbp入棧保存在棧上,第二句把棧指針rsp保存在幀指針rbp中。上述兩句執行完後,func_C的棧佈局如下圖所示。
- 開棧
0x0000000000000701 <+4>: sub $0x30,%rsp
開棧的操作比較簡單,就是把rsp的值減小,開闢出一片連續的內存區域用作接收參數,存放局部變量以及棧上運算。不過func_C沒有參數和棧上運算。執行完上述彙編後,棧的佈局如下圖所示。
- 接受參數。func_A不涉及。
- 棧上運算
0x0000000000000705 <+8>: movl $0x0,-0x4(%rbp) 0x000000000000070c <+15>: movl $0x1,-0x8(%rbp) 0x0000000000000713 <+22>: movl $0x2,-0xc(%rbp) 0x000000000000071a <+29>: movl $0x3,-0x10(%rbp) 0x0000000000000721 <+36>: movl $0x4,-0x14(%rbp) 0x0000000000000728 <+43>: movl $0x5,-0x18(%rbp) 0x000000000000072f <+50>: movl $0x6,-0x1c(%rbp) 0x0000000000000736 <+57>: movl $0x7,-0x20(%rbp) 0x000000000000073d <+64>: movl $0x8,-0x24(%rbp) 0x0000000000000744 <+71>: movb $0x63,-0x25(%rbp)
這裏的棧上運算比較簡單,只是對局部變量進行賦值。局部變量賦值過後的棧空間如下圖所示:
- 參數傳遞
0x0000000000000748 <+75>: movsbl -0x25(%rbp),%edi 0x000000000000074c <+79>: mov -0x1c(%rbp),%r9d 0x0000000000000750 <+83>: mov -0x18(%rbp),%r8d 0x0000000000000754 <+87>: mov -0x14(%rbp),%ecx 0x0000000000000757 <+90>: mov -0x10(%rbp),%edx 0x000000000000075a <+93>: mov -0xc(%rbp),%esi 0x000000000000075d <+96>: mov -0x8(%rbp),%eax 0x0000000000000760 <+99>: push %rdi 0x0000000000000761 <+100>: mov -0x24(%rbp),%edi 0x0000000000000764 <+103>: push %rdi 0x0000000000000765 <+104>: mov -0x20(%rbp),%edi 0x0000000000000768 <+107>: push %rdi 0x0000000000000769 <+108>: mov %eax,%edi
前面也說過,在X86-64平臺,當參數小於7個時使用寄存器傳參 。當參數個數大於等於7時,參數arg1~arg6分別使用寄存器rdi,rsi, rdx, rcx, r8 and r9傳參,其餘參數使用棧傳遞。如下圖所示,實參x1~x6使用寄存器rdi,rsi, rdx, rcx, r8 and r9傳參,實參x7~x9使用棧傳遞。
2.2.2 func_B的棧
int func_B(int x1, int x2, int x3, int x4, int x5, int x6, int x7, int x8, char x9){
int sum = 0;
sum = func_C(x1, x2, x3, x4, x5,x6);
sum = sum + x7;
sum = sum + x8;
sum += x9;
return sum;
}
首先,編譯源程序 $gcc test.c -o test -fno-stack-protector
,然後反彙編出func_B函數 $gdb test
:
(gdb) disas func_B
Dump of assembler code for function func_B:
0x0000000000000699 <+0>: push %rbp
0x000000000000069a <+1>: mov %rsp,%rbp
0x000000000000069d <+4>: sub $0x30,%rsp
0x00000000000006a1 <+8>: mov %edi,-0x14(%rbp)
0x00000000000006a4 <+11>: mov %esi,-0x18(%rbp)
0x00000000000006a7 <+14>: mov %edx,-0x1c(%rbp)
0x00000000000006aa <+17>: mov %ecx,-0x20(%rbp)
0x00000000000006ad <+20>: mov %r8d,-0x24(%rbp)
0x00000000000006b1 <+24>: mov %r9d,-0x28(%rbp)
0x00000000000006b5 <+28>: mov 0x20(%rbp),%eax
0x00000000000006b8 <+31>: mov %al,-0x2c(%rbp)
0x00000000000006bb <+34>: movl $0x0,-0x4(%rbp)
0x00000000000006c2 <+41>: mov -0x28(%rbp),%r8d
0x00000000000006c6 <+45>: mov -0x24(%rbp),%edi
0x00000000000006c9 <+48>: mov -0x20(%rbp),%ecx
0x00000000000006cc <+51>: mov -0x1c(%rbp),%edx
0x00000000000006cf <+54>: mov -0x18(%rbp),%esi
0x00000000000006d2 <+57>: mov -0x14(%rbp),%eax
0x00000000000006d5 <+60>: mov %r8d,%r9d
0x00000000000006d8 <+63>: mov %edi,%r8d
0x00000000000006db <+66>: mov %eax,%edi
0x00000000000006dd <+68>: callq 0x64a <func_C>
0x00000000000006e2 <+73>: mov %eax,-0x4(%rbp)
0x00000000000006e5 <+76>: mov 0x10(%rbp),%eax
0x00000000000006e8 <+79>: add %eax,-0x4(%rbp)
0x00000000000006eb <+82>: mov 0x18(%rbp),%eax
0x00000000000006ee <+85>: add %eax,-0x4(%rbp)
0x00000000000006f1 <+88>: movsbl -0x2c(%rbp),%eax
0x00000000000006f5 <+92>: add %eax,-0x4(%rbp)
0x00000000000006f8 <+95>: mov -0x4(%rbp),%eax
0x00000000000006fb <+98>: leaveq
0x00000000000006fc <+99>: retq
End of assembler dump.
一個函數的彙編做的還是那幾件事,下面分析:
- 保存上一個棧幀信息
0x0000000000000699 <+0>: push %rbp
0x000000000000069a <+1>: mov %rsp,%rbp
上一個棧幀的幀指針rbp保存在當前棧幀上, 上一個棧幀的棧指針rsp保存在當前棧幀的幀指針rbp寄存器中。
- 開棧
0x000000000000069d <+4>: sub $0x30,%rsp
在棧上開闢一塊連續的地址用於接收參數,局部變量,棧上運算。
- 接收參數
0x00000000000006a4 <+11>: mov %esi,-0x18(%rbp) 0x00000000000006a7 <+14>: mov %edx,-0x1c(%rbp) 0x00000000000006aa <+17>: mov %ecx,-0x20(%rbp) 0x00000000000006ad <+20>: mov %r8d,-0x24(%rbp) 0x00000000000006b1 <+24>: mov %r9d,-0x28(%rbp) 0x00000000000006b5 <+28>: mov 0x20(%rbp),%eax 0x00000000000006b8 <+31>: mov %al,-0x2c(%rbp)
函數總共有9個參數,寄存器傳遞6個參數,棧傳遞三個參數。
- 傳遞參數
0x00000000000006c2 <+41>: mov -0x28(%rbp),%r8d 0x00000000000006c6 <+45>: mov -0x24(%rbp),%edi 0x00000000000006c9 <+48>: mov -0x20(%rbp),%ecx 0x00000000000006cc <+51>: mov -0x1c(%rbp),%edx 0x00000000000006cf <+54>: mov -0x18(%rbp),%esi 0x00000000000006d2 <+57>: mov -0x14(%rbp),%eax 0x00000000000006d5 <+60>: mov %r8d,%r9d 0x00000000000006d8 <+63>: mov %edi,%r8d 0x00000000000006db <+66>: mov %eax,%edi
調用函數有6個參數,這6個參數都使用寄存器傳遞。
- 棧上運算
0x00000000000006dd <+68>: callq 0x64a <func_C> 0x00000000000006e2 <+73>: mov %eax,-0x4(%rbp) 0x00000000000006e5 <+76>: mov 0x10(%rbp),%eax 0x00000000000006e8 <+79>: add %eax,-0x4(%rbp) 0x00000000000006eb <+82>: mov 0x18(%rbp),%eax 0x00000000000006ee <+85>: add %eax,-0x4(%rbp) 0x00000000000006f1 <+88>: movsbl -0x2c(%rbp),%eax 0x00000000000006f5 <+92>: add %eax,-0x4(%rbp)
運算第一步就是將func_A的返回值%eax賦值給sum;第二步是將x7的值和sum相加放在sum中;第三步是將x8的和sum相加結果放在sum中;第三步是將x9的值擴展成32bit,和sum相加結果放在sum中。
2.2.3 func_C的棧
int func_C(int x1, int x2, int x3, int x4, int x5, int x6){
int sum = 0;
sum = x1 + x2;
sum = sum + x3 + x4;
sum = sum + x5 + x6;
return sum;
}
首先,編譯源程序 $gcc test.c -o test -fno-stack-protector
,然後反彙編出func_A函數 $gdb test
:
(gdb) disas func_C
Dump of assembler code for function func_C:
0x000000000000064a <+0>: push %rbp
0x000000000000064b <+1>: mov %rsp,%rbp
0x000000000000064e <+4>: mov %edi,-0x14(%rbp)
0x0000000000000651 <+7>: mov %esi,-0x18(%rbp)
0x0000000000000654 <+10>: mov %edx,-0x1c(%rbp)
0x0000000000000657 <+13>: mov %ecx,-0x20(%rbp)
0x000000000000065a <+16>: mov %r8d,-0x24(%rbp)
0x000000000000065e <+20>: mov %r9d,-0x28(%rbp)
0x0000000000000662 <+24>: movl $0x0,-0x4(%rbp)
0x0000000000000669 <+31>: mov -0x14(%rbp),%edx
0x000000000000066c <+34>: mov -0x18(%rbp),%eax
0x000000000000066f <+37>: add %edx,%eax
0x0000000000000671 <+39>: mov %eax,-0x4(%rbp)
0x0000000000000674 <+42>: mov -0x4(%rbp),%edx
0x0000000000000677 <+45>: mov -0x1c(%rbp),%eax
0x000000000000067a <+48>: add %eax,%edx
0x000000000000067c <+50>: mov -0x20(%rbp),%eax
0x000000000000067f <+53>: add %edx,%eax
0x0000000000000681 <+55>: mov %eax,-0x4(%rbp)
0x0000000000000684 <+58>: mov -0x4(%rbp),%edx
0x0000000000000687 <+61>: mov -0x24(%rbp),%eax
0x000000000000068a <+64>: add %eax,%edx
0x000000000000068c <+66>: mov -0x28(%rbp),%eax
0x000000000000068f <+69>: add %edx,%eax
0x0000000000000691 <+71>: mov %eax,-0x4(%rbp)
0x0000000000000694 <+74>: mov -0x4(%rbp),%eax
0x0000000000000697 <+77>: pop %rbp
0x0000000000000698 <+78>: retq
End of assembler dump.
func_A的棧結構和前幾個函數類型,但是編譯器識別到該函數時葉子函數(leaf function),其棧指針rsp不再被使用,就會少一修改rsp的指令和一個恢復rsp的指令。
- 保存上一個棧幀信息
0x0000000000000699 <+0>: push %rbp
0x000000000000069a <+1>: mov %rsp,%rbp
不會有開棧的操作,即不會修改rsp指針。
- 接收參數
0x000000000000064e <+4>: mov %edi,-0x14(%rbp) 0x0000000000000651 <+7>: mov %esi,-0x18(%rbp) 0x0000000000000654 <+10>: mov %edx,-0x1c(%rbp) 0x0000000000000657 <+13>: mov %ecx,-0x20(%rbp) 0x000000000000065a <+16>: mov %r8d,-0x24(%rbp) 0x000000000000065e <+20>: mov %r9d,-0x28(%rbp)
- 棧上運算
0x0000000000000662 <+24>: movl $0x0,-0x4(%rbp) 0x0000000000000669 <+31>: mov -0x14(%rbp),%edx 0x000000000000066c <+34>: mov -0x18(%rbp),%eax 0x000000000000066f <+37>: add %edx,%eax 0x0000000000000671 <+39>: mov %eax,-0x4(%rbp) 0x0000000000000674 <+42>: mov -0x4(%rbp),%edx 0x0000000000000677 <+45>: mov -0x1c(%rbp),%eax 0x000000000000067a <+48>: add %eax,%edx 0x000000000000067c <+50>: mov -0x20(%rbp),%eax 0x000000000000067f <+53>: add %edx,%eax 0x0000000000000681 <+55>: mov %eax,-0x4(%rbp) 0x0000000000000684 <+58>: mov -0x4(%rbp),%edx 0x0000000000000687 <+61>: mov -0x24(%rbp),%eax 0x000000000000068a <+64>: add %eax,%edx 0x000000000000068c <+66>: mov -0x28(%rbp),%eax 0x000000000000068f <+69>: add %edx,%eax 0x0000000000000691 <+71>: mov %eax,-0x4(%rbp)
彙編語言描述的和C語言一致。