c/c++ 從原始碼到可執行檔案,可執行檔案如何執行
以一個例子開始分析:(以下所有實驗都是在linux下完成)
//test.c
int g_num=2000;
char g_string[10000]="hello c";
int multi(int a,int b){
int result= a*b;
int dummy[10000];
return result;
}
int main(){
multi(5,8);
return 0;
}
我們都知道c原始檔要想編譯生成可執行檔案,要包括兩個主要部分:
1、 編譯生成,目標檔案(本例中,生成test.o)
2、目標檔案,連線生成可執行檔案(本例中,生成test)
下面我們生成test.o和test,進行分析:
gcc -c test.c -o test.o
gcc test.o -o test
test.o 和 test中包含的都是二進位制碼。不嚴謹的說,test.o ,test中主要包含兩部分,程式碼段和資料段。
程式碼段,就是cpu需要執行的每一條指令;而,資料段,就是我們通常說的,靜態儲存區(不同於堆疊)。
要想詳細的檢視test或test.o中包含了什麼,需要將test.c 轉換為彙編程式碼檢視,下面,我們將生成彙編程式碼:
gcc -S test.c -o test.s
生成的彙編程式碼如下:
.file "test.c" .globl g_num .data .align 4 .type g_num, @object .size g_num, 4 g_num: .long 2000 .globl g_string .align 32 .type g_string, @object .size g_string, 10000 g_string: .string "hello c" .zero 9992 .text .globl multi .type multi, @function multi: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $3904, %rsp movl %edi, -4020(%rbp) movl %esi, -4024(%rbp) movl -4020(%rbp), %eax imull -4024(%rbp), %eax movl %eax, -4004(%rbp) movl -4004(%rbp), %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size multi, .-multi .globl main .type main, @function main: .LFB1: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $8, %esi movl $5, %edi call multi movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1: .size main, .-main .ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.1) 4.8.4" .section .note.GNU-stack,"",@progbits
通過生成的彙編程式碼,我們可以看到,引數傳遞是通過暫存器傳遞的(在某些情況會通過棧來傳遞,教科書上,多討論棧傳遞)。
下面我們看一些通常在c中說的概念:
全域性變數(以及靜態變數):在程式碼中對應 g_num. g_string 在彙編程式碼中的資料段(data segment)。
區域性變數: 在函式中定義的變數,在棧中,(本例不太明顯,主要原因是multi是該程式呼叫的最後一個函式,所以編譯器做了優化),如果該函式中,再呼叫另一個函式,會看到函式先將sp減小,即是分配了區域性變數的空間。
在教科書中,函式呼叫時,會將引數按相反的方向push到棧中,然後,把返回地址push到棧中,然後進入函式,進入函式後,將bp,push到棧中,然後將sp賦值到bp。下面進行計算,計算完畢,將bp,pop出來,再把返回地址pop出來,返回到呼叫函式,然後,將sp恢復。返回值是通過暫存器傳遞的,多是eax。
返回值是如何傳遞的呢? 通常返回值是通過暫存器傳遞eax 或 edx等,但是,如果返回的是一個非常大的物件,將如何實現呢? 如果返回的是一個非常大的物件,caller 函式將把返回的地址傳遞到edi暫存器,called函式,將把計算的返回值填充到edi地址內,這樣就實現了函式返回值的傳遞。
檢視test.o 和 test 兩個檔案:可以看出: test.o 中包含函式名字 multi (雖然是二進位制的),test中不包含函式名字,但是可以看到/usr/lib/libc.so的字樣。
這個對於之後,我們理解連線過程非常有幫助。(注意:用g++編譯,函式名字會加上一些字首和字尾)
連線過程 : 其實就是將多個目標檔案合併起來,從main函式開始,依次找到需要的函式名(是真正的字串函式名,我們之前看到.o檔案中包含函式名),如果找不到函式名,就要到動態連結庫中去找了,gcc預設的動態連結庫的目錄是/lib,如果找不到,就雞雞了。當然,可以加上編譯選項, —Lx即加入搜尋路徑。如果所有函式都找到,就會編譯成功,生成可執行檔案。
但是,編譯成功,與可執行檔案可以執行(即使程式碼都是正確的)是兩件事,雖然,往往編譯成功,可執行檔案就可以執行。
在命令列中,輸入可執行檔案的命令:
首先先要做地址對映,將程式碼段和資料段分別對映到虛擬記憶體,以及實體記憶體中。這個階段,完成了全域性變數的空間分配(包括靜態變數)。
然後,就是一條指令一條指令的執行了,執行中有時候會遇到找不到的函式,這時就需要尋找動態連結庫,動態連結庫的尋找方法是,現在LD_LIBRARY_PATH下找,找不到就到/etc/ld.so.conf.d下找,再找不到就要報錯了。