淺析操作系統函數調用原理-附實例
最近在研究二進制,研究到函數調用部分,將自己理解的原理做個記錄。
首先需要了解系統棧的工作原理,棧可以理解成一種先進後出的數據結構,這就不用多說了。
在操作系統中,系統棧也起到用來維護函數調用、參數傳遞等關系的一個作用。嗯,這是我的理解。
在高級語言編程中,函數調用的底層原理是對用戶屏蔽的,所以不用過多的糾結於底層的實現。而對於
匯編研究者來說,了解這個原理就很重要了。
首先可以想象一下,匯編語言在內存中是以指令的形式存在的,這些指令是按照順序存儲和執行的,高級語言中
編寫的循環、調用,到了底層都會變成一些最基本的判斷和跳轉,如何在線性的結構上完成“非線性”的過程調度,
理解了這些,就理解了匯編。
這裏先拋出高級語言的一個例子:
/*20160701*/
#include <stdio.h>
int funcA( int arg ){
arg += 1;
return arg;
}
int main(){
int a;
a = funcA(1);
printf("%d", a);
}
在這個程序中,main函數調用了函數funcA,funcA對傳入的數據進行+1然後返回。
這個程序在編譯之後,main函數變成這樣:
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000400516 <+0>: push rbp
0x0000000000400517 <+1>: mov rbp,rsp
0x000000000040051a <+4>: sub rsp,0x10
0x000000000040051e <+8>: mov edi,0x1
0x0000000000400523 <+13>:call 0x400506 <funcA>
0x0000000000400528 <+18>:mov DWORD PTR [rbp-0x4],eax
0x000000000040052b <+21>:mov eax,DWORD PTR [rbp-0x4]
0x000000000040052e <+24>:mov esi,eax
0x0000000000400530 <+26>:mov edi,0x4005d4
0x0000000000400535 <+31>:mov eax,0x0
0x000000000040053a <+36>:call 0x4003e0 <printf@plt>
0x000000000040053f <+41>:leave
0x0000000000400540 <+42>:ret
End of assembler dump.
其中rbp是調用main函數的函數的棧楨的底部,這麽說有點繞,簡單的來說,main函數調用了funcA,那funcA中首先要做的一件事情就是把調用它的main函數棧楨的底部保存,所以在main函數被操作系統裝載執行之後,main要做的首先是把調用它的函數的棧楨的底部保存,不然怎麽返回呢?
第二個步驟把rsp的值傳遞給rbp,這是替換當前棧楨的底部,因為調用了funcA,所以要為funcA創建獨立的棧楨,於是擡高棧底,怎麽擡高呢,把棧頂傳給指向棧楨底部的指針就可以了。
下一步是擡高棧頂,這是為funcA創建棧楨空間。
接著將參數傳遞給edi,因為這裏只有一個參數,所以不涉及到參數順序的問題,關於這個問題,可以去了解一下函數調用約定
調用了funcA,再來觀察一下funcA的內部機制:
(gdb) disassemble funcA
Dump of assembler code for function funcA:
0x0000000000400506 <+0>: push rbp
0x0000000000400507 <+1>: mov rbp,rsp
0x000000000040050a <+4>: mov DWORD PTR [rbp-0x4],edi
0x000000000040050d <+7>: add DWORD PTR [rbp-0x4],0x1
0x0000000000400511 <+11>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000400514 <+14>: pop rbp
0x0000000000400515 <+15>: ret
End of assembler dump.
同樣的,在funcA中,首先保存上一個函數,即main函數棧楨的棧底,然後將rsp的值賦給rbp,擡高棧楨底部。
接著從edi中取得參數,並放入位於自身棧楨空間中,rbp之後的雙字單元內。
然後執行操作,將其自增。
執行完成之後,將返回值保存在eax中,等待返回。
彈出上一個函數的棧楨的底部,重新回到main函數的空間。
PS:
直到目前為止,這個程序反編譯出來的結果和書上說的原理還是有一些出入的,還有下面幾個問題:
0x01 書上說的是,傳遞參數,會將參數按照一定順序壓棧,而不是像本程序中這樣使用edi
0x02 在main函數調用funcA函數之後,將棧頂指針esp擡高了,但是在funcA函數執行完成需要返回到main函數的時候,只恢復了ebp指針,並沒有恢復esp指針,這是為什麽?
希望接下來可以搞懂上面的兩個問題。
本文中用到的相關代碼:
/*20160701*/
#include <stdio.h>
int funcA( int arg ){
arg += 1;
return arg;
}
int main(){
int a;
a = funcA(1);
printf("%d", a);
}
淺析操作系統函數調用原理-附實例