【UNIX】從一個可執行檔的產生到進程在記憶體中分布 (上)/可執行檔的產生,unix可執行檔
從一個可執行檔的產生到進程在記憶體中分布 (上)
可執行檔的產生
一個來源程式要產生可執行檔,需要通過編譯器來實現,而編譯器所起的作用就是把程式員編寫的進階語言翻譯成機器能讀懂的低級語言(二進位代碼),這裡編譯器起到翻譯的作用。在Linux作業系統中採用的是GCC交叉編譯工具。
雖然說GCC是個編譯器,但是使用GCC從一個C語言程式產生可執行檔不僅僅只是編譯過程,而是要經過四個相互關聯的步驟:預先處理(也稱先行編譯,preprocessing)、編譯(comlilation)、彙編(assembly)和連結(linking)
第一步:預先處理
Gcc -E test.c -o test.i // -o(小歐)表示目標檔案objective目標;O(大歐)表示最佳化係數
#include<stdio.h>
int main()
{
printf("hello kano");
return 0;
}
預先處理之後
typedef int (*__gconv_trans_fct) (struct __gconv_step *,
struct __gconv_step_data *, void *,
__const unsigned char *,
__const unsigned char **,
__const unsigned char *, unsigned char **,
size_t *);
…
# 2 "test.c" 2
int main()
{
printf("hello kano ");
return 0;
}
通上面的代碼可以看出來在預先處理階段GCC編譯器只把源檔案裡的標頭檔合并以及宏定義替換到源檔案中,沒有進行代碼的編譯
第二步:編譯
Gcc -S test.i -o test.s
.file "test.c"
.section .rodata
.align 4
.LC0:
.string"hello kano"
.text
.globl main
.type main,
main:
Pushl p
movl %esp, p
subl $8, %esp
此時已經由C語言編程了組合語言了
第三步:彙編
Gcc -c test.s -o hello.o
編譯器這裡就已經完成了C語言到機器碼的編譯過程,並產生了目標檔案
第四步:連結
Gcc testo -o test
要想一個程式功能的完整實現還需要一些庫函數的載入之後把各個目標檔案模組整合為一個可執行檔。此時我們會主要到哪些printf標準庫函數是怎麼被調用以及載入到我們的來源程式的?在先行編譯中包含進的”stdio.h”中也只有該函數的聲明,而沒有定義函數的實現,系統把這些函數實現都被做到名為libc.so.6的庫檔案中去了,在沒有特別指定時,Gcc會到系統預設的搜尋路徑”/usr/lib”下進行尋找,也就是連結到libc.so.6庫函數中去,這樣就能實現函數”printf”了,而這也就是連結的作用。
函數庫一般分為靜態庫和動態庫兩種。靜態庫是指編譯連結時,把庫檔案的代碼全部加入到可執行檔中,因此產生的檔案比較大,但在運行時也就不再需要庫檔案了。其尾碼名一般為”.a”。動態庫與之相反,在編譯連結時並沒有把庫檔案的代碼加入到可執行檔中,而是在程式執行時由運行時連結檔案載入庫,這樣可以節省系統的開銷。動態庫一般尾碼名為”.so”,如前面所述的libc.so.6就是動態庫。Gcc在編譯時間預設使用動態庫。
完成了連結之後,
即可以產生可執行檔
附錄:
.c為尾碼的檔案,C語言原始碼檔案;
.a為尾碼的檔案,是由目標檔案構成的檔案庫檔案;
.C,.cc或.cxx 為尾碼的檔案,是C++原始碼檔案;
.h為尾碼的檔案,是程式所包含的標頭檔;
.i 為尾碼的檔案,是已經預先處理過的C原始碼檔案;
.ii為尾碼的檔案,是已經預先處理過的C++原始碼檔案;
.m為尾碼的檔案,是Objective-C原始碼檔案;
.o為尾碼的檔案,是編譯後的目標檔案;
.s為尾碼的檔案,是組合語言原始碼檔案;
.S為尾碼的檔案,是經過先行編譯的組合語言原始碼檔案。