以一個例子開始分析:(以下所有實驗都是在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下找,再找不到就要報錯了。