1. 程式人生 > >淺析操作系統函數調用原理-附實例

淺析操作系統函數調用原理-附實例

做的 調用 ble pri 用戶 過多 tdi 獨立 簡單的

最近在研究二進制,研究到函數調用部分,將自己理解的原理做個記錄。

首先需要了解系統棧的工作原理,棧可以理解成一種先進後出的數據結構,這就不用多說了。
在操作系統中,系統棧也起到用來維護函數調用、參數傳遞等關系的一個作用。嗯,這是我的理解。
在高級語言編程中,函數調用的底層原理是對用戶屏蔽的,所以不用過多的糾結於底層的實現。而對於
匯編研究者來說,了解這個原理就很重要了。
首先可以想象一下,匯編語言在內存中是以指令的形式存在的,這些指令是按照順序存儲和執行的,高級語言中
編寫的循環、調用,到了底層都會變成一些最基本的判斷和跳轉,如何在線性的結構上完成“非線性”的過程調度,
理解了這些,就理解了匯編。

這裏先拋出高級語言的一個例子:

    /*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);
}
    

淺析操作系統函數調用原理-附實例