1. 程式人生 > >c/c++ 從原始碼到可執行檔案,可執行檔案如何執行

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下找,再找不到就要報錯了。