程式的編譯連結過程
原文連結: https://www.cnblogs.com/kekec/p/3238741.html
還是從HelloWorld開始說吧...
#include <stdio.h> int main(int argc, char* argv[]) { printf("Hello World!\n"); return 0; }
從原始檔Hello.cpp編譯連結成Hello.exe,需要經歷如下步驟:
可使用以下命令,直接從原始檔生成可執行檔案
linux:
gcc -lstdc++ Hello.cpp -o Hello.out // 要帶上lstdc引數,否則會報undefined reference to '__gxx_personality_v0'錯誤g++ Hello.cpp -o Hello.out
注:字尾為.c的檔案gcc把它當做c程式碼,而g++當做c++程式碼;gcc與g++都是呼叫器,最終呼叫的編譯器為cc1(c程式碼),cc1plus(c++c程式碼)。
另外,連結階段gcc不會自動和c++標準庫連結,需要帶上-lstdc++引數才能連結。
windows:
cl Hello.cpp /link -out:Hello.exe
預處理:主要是做一些程式碼文字的替換工作。(該替換是一個遞迴逐層展開的過程。)
(1)將所有的#define刪除,並展開所有的巨集定義
(2)處理所有的條件預編譯指令,如:#if #ifdef #elif #else #endif
(3)處理#include預編譯指令,將被包含的檔案插進到該指令的位置,這個過程是遞迴的
(4)刪除所有的註釋//與/* */
(5)新增行號與檔名標識,以便產生除錯用的行號資訊以及編譯錯誤或警告時能夠顯示行號
(6)保留所有的#pragma編譯器指令,因為編譯器需要使用它們
linux:
cpp Hello.cpp > Hello.i gcc -E Hello.cpp -o Hello.i g++ -E Hello.cpp -o Hello.i
行號與檔名標識解釋:
# 32 "/usr/include/bits/types.h" 2 3 4 // 表示下面行為types.h的第32行 typedef unsignedchar __u_char; typedef unsigned short int __u_short; typedef unsigned int __u_int; typedef unsigned long int __u_long;
以上,#行的行末的數字2 3 4的含義:
1 - 開啟一個新檔案
2 - 返回上一層檔案
3 - 以下的程式碼來自系統檔案
4 - 以下的程式碼隱式地包裹在extern "C"中
不產生行號與檔名標識:
cpp -P Hello.cpp > Hello.i gcc -E -P Hello.cpp -o Hello.i g++ -E -P Hello.cpp -o Hello.i
windows:
cl /E Hello.cpp > Hello.i
行號與檔名標識解釋:
#line 283 "C:\\Program Files\\Microsoft Visual Studio\\VC98\\include\\stdio.h" // 表示下面行為stdio.h的第283行 void __cdecl clearerr(FILE *); int __cdecl fclose(FILE *); int __cdecl _fcloseall(void);
不產生行號與檔名標識:
cl /EP Hello.cpp > Hello.i
編譯:把預處理完的檔案進行一系列詞法分析(lex)、語法分析(yacc)、語義分析及優化後生成彙編程式碼,這個過程是程式構建的核心部分。
linux:
/usr/lib/gcc/i586-suse-linux/4.1.2/cc1 Hello.cpp
使用cc1生成出來的Hello.s檔案如下(由於Hello.cpp中沒有c++的特性,因此也可以用c語言編譯器進行編譯):
.file "Hello.cpp" .section .rodata .LC0: .string "Hello World!" .text .globl main .type main, @function main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $4, %esp movl $.LC0, (%esp) call puts movl $0, %eax addl $4, %esp popl %ecx popl %ebp leal -4(%ecx), %esp ret .size main, .-main .ident "GCC: (GNU) 4.1.2 20070115 (prerelease) (SUSE Linux)" .section .note.GNU-stack,"",@progbits
對於含c++的特性的cpp檔案,應使用cc1plus進行編譯,或使用gcc命令來編譯(會通過後綴名來選擇呼叫cc1還是cc1plus)
/usr/lib/gcc/i586-suse-linux/4.1.2/cc1plus Hello.cpp gcc -S Hello.cpp -o Hello.s g++ -S Hello.cpp -o Hello.s
windows:
cl /FA Hello.cpp Hello.asm
vc6生成出來的Hello.asm檔案如下:
TITLE Hello.cpp .386P include listing.inc if @Version gt 510 .model FLAT else _TEXT SEGMENT PARA USE32 PUBLIC 'CODE' _TEXT ENDS _DATA SEGMENT DWORD USE32 PUBLIC 'DATA' _DATA ENDS CONST SEGMENT DWORD USE32 PUBLIC 'CONST' CONST ENDS _BSS SEGMENT DWORD USE32 PUBLIC 'BSS' _BSS ENDS _TLS SEGMENT DWORD USE32 PUBLIC 'TLS' _TLS ENDS FLAT GROUP _DATA, CONST, _BSS ASSUME CS: FLAT, DS: FLAT, SS: FLAT endif PUBLIC _main EXTRN _printf:NEAR _DATA SEGMENT $SG579 DB 'Hello World!', 0aH, 00H _DATA ENDS _TEXT SEGMENT _main PROC NEAR ; File Hello.cpp ; Line 7 push ebp mov ebp, esp ; Line 8 push OFFSET FLAT:$SG579 call _printf add esp, 4 ; Line 9 xor eax, eax ; Line 10 pop ebp ret 0 _main ENDP _TEXT ENDS END
彙編:彙編程式碼->機器指令。
linux:
as Hello.s -o Hello.o gcc -c Hello.cpp -o Hello.o g++ -c Hello.cpp -o Hello.o
windows:
cl /c Hello.cpp > Hello.obj
至此,產生的目標檔案在結構上已經很像最終的可執行檔案了。
連結:這裡講的連結,嚴格說應該叫靜態連結。多個目標檔案、庫->最終的可執行檔案(拼合的過程)。
可執行檔案分類:
linux的ELF檔案 -- bin、a、so
windows的PE檔案 -- exe、lib、dll
注:PE檔案與ELF檔案都是COFF檔案的變種
linux:
ld -static /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i586-suse-linux/4.1.2/crtbeginT.o -L/usr/lib/gcc/i586-suse-linux/4.1.2/ -L/usr/lib -L/lib Hello.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/i586-suse-linux/4.1.2/crtend.o /usr/lib/crtn.o -o Hello.out
注:-static:強制所有的-l選項使用靜態連結; -L:連結外部靜態庫與動態庫的查詢路徑;
-l:指定靜態庫的名稱(最後庫的檔名為:libgcc.a、libgcc_eh.a、libc.a);
--start-group ... --end-group:之間的內容只能為檔名或-l選項;為了保證內容項中的符號能被解析,連結器會在所有的內容項中迴圈查詢。
這種用法存在效能開銷,最好是當有兩個或兩個以上內容項之間存在有迴圈引用時才使用。
windows:
link /subsystem:console /out:Hello.exe Hello.obj
靜態庫本質上就是包含一堆中間目標檔案的壓縮包,就像zip等檔案一樣,裡面的各個中間檔案包含的外部符號地址是沒有被連結器修正的。
檢視靜態庫中的內容
linux:
ar -t libc.a
windows:
lib /list libcmt.lib
解壓靜態庫中的內容
linux:【將libc.a中所有的o檔案解壓到當前目錄下】
ar -x /usr/lib/libc.a
windows:【將libcmt.lib中的atof.obj解壓到當前目錄下】
lib libcmt.lib /extract:build\intel\mt_obj\atof.obj
生成靜態庫
linux:
ar -rf test.a main.o fun.o
windows:
lib /out:test.lib main.obj fun.obj
符號(Symbol) -- 連結的介面
每個函式或變數都有自己獨特的名字,才能避免連結過程中不同變數和函式之間的混淆。
在連結中,我們將函式和變數統稱為符號,函式名或變數名就是符號名,函式或變數的地址就是符號值。
每一個目標檔案都有一個符號表,符號有以下幾種:
(1) 定義在本目標檔案的全域性符號,可被其他目標檔案引用
如:全域性變數,全域性函式
(2) 在本目標檔案中引用的全域性符號,卻沒有定義在本目標檔案 -- 外部符號(External Symbol)
如:extern變數,printf等庫函式,其他目標檔案中定義的函式
(3) 段名,這種符號由編譯器產生,其值為該段的起始地址
如:目標檔案的.text、.data等
(4) 區域性符號,內部可見
如:static變數
連結過程中,比較關心的是上面的第一類與第二類。
檢視符號
linux:
nm Hello.o readelf -s Hello.o objdump -t Hello.obj
windows上可以安裝MinGW來獲取這些工具。
windows:
dumpbin /symbols Hello.obj
符號修飾(Name Decoration)
符號修飾實際就是對變數或函式進行重新命名的過程,影響命名的因素有:
(1) 語言的不同,修飾規則有差別
如:foo函式,在C語言中會被修飾成_foo,在Fortran語言中會被修飾成_foo_
(2) 面嚮物件語言(如:C++)引入的特性
如:類、繼承、虛機制、過載、名稱空間(namespace)等
-----------------------------MSVC編譯器-----------------------------
MSVC編譯器預設使用的是__cdecl呼叫約定(在"C/C++" -- "Advanced" -- "Calling Convention"中設定),Windows API使用的__stdcall呼叫約定。
針對c語言和c++語言,MSVC有兩套修飾規則:
c語言函式名修飾約定規則:(被extern "C"包裹的程式碼塊)
1、__stdcall呼叫約定在輸出函式名前加上一個下劃線字首,後面加上一個“@”符號和其引數的位元組數,格式為[email protected]。
2、__cdecl呼叫約定僅在輸出函式名前加上一個下劃線字首,格式為_functionname。
3、__fastcall呼叫約定在輸出函式名前加上一個“@”符號,後面也是一個“@”符號和其引數的位元組數,格式@[email protected]。
它們均不改變輸出函式名中的字元大小寫,這和pascal呼叫約定不同,pascal約定輸出的函式名無任何修飾且全部大寫。
c++語言函式名修飾約定規則:
1、__stdcall呼叫約定:
(1)以“?”標識函式名的開始,後跟函式名;
(2)函式名後面以“@@yg”標識引數表的開始,後跟引數表;
(3)引數表以代號表示:
x--void ,
d--char,
e--unsigned char,
f--short,
h--int,
i--unsigned int,
j--long,
k--unsigned long,
m--float,
n--double,
_n--bool,
....
pa--表示指標,後面的代號表明指標型別,如果相同型別的指標連續出現,以“0”代替,一個“0”代表一次重複;
(4)引數表的第一項為該函式的返回值型別,其後依次為引數的資料型別,指標標識在其所指資料型別前;
(5)引數表後以“@z”標識整個名字的結束,如果該函式無引數,則以“z”標識結束。
其格式為“[email protected]@yg*****@z”或“[email protected]@yg*xz”,例如
int test1-----“[email protected]@[email protected]”
void test2-----“[email protected]@ygxxz”
2、__cdecl呼叫約定:
規則同上面的_stdcall呼叫約定,只是引數表的開始標識由上面的“@@yg”變為“@@ya”。
3、__fastcall呼叫約定:
規則同上面的_stdcall呼叫約定,只是引數表的開始標識由上面的“@@yg”變為“@@yi”。
注:如果輸出了map檔案,可以在該檔案中檢視各函式及變數被修飾後的名稱字串。
-------------------------------------------------------------------------
函式簽名(Function Signature)
函式簽名用於識別不同的函式,包括函式名、它的引數型別及個數、所在的類和名稱空間、呼叫約定型別及其他資訊
Visual C++的符號修飾與函式簽名的規則沒有對外公開,但Microsoft提供了一個UnDecorateSymbolName的API,可以將修飾後名稱轉換成函式原型
使用extern "C",強制C++編譯器用C語言的規則來進行符號修飾
extern "C" int g_nTest1; extern "C" int fun(); #ifdef __cplusplus extern "C" { #endif int g_nTest2 = 0; int add(int a, int b); #ifdef __cplusplus } #endif
弱符號與強符號 [wiki]
對於C/C++語言來說,編譯器預設函式和初始化了的全域性變數為強符號,未初始化的全域性變數為弱符號。
GCC可以通過"__attribute__((weak))"來定義任何一個強符號為弱符號。
extern int __attribute__((weak)) ext; // 將變數ext修改成一個弱符號 int __attribute__((weak)) fun1(); // 將函式fun1修改成一個弱符號 int fun2() __attribute__((weak)); // 將函式fun2修改成一個弱符號 int weak1; int strong = 1;