還是從HelloWorld開始說吧... #include <stdio.h> int main(int argc, char* argv[]) { printf("Hello World!\n"); return 0; } 從源文件Hello.cpp編譯鏈接成Hello.exe,需要經歷如下步驟: ...
還是從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 unsigned char __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調用約定在輸出函數名前加上一個下劃線首碼,後面加上一個“@”符號和其參數的位元組數,格式為_functionname@number。
2、__cdecl調用約定僅在輸出函數名前加上一個下劃線首碼,格式為_functionname。
3、__fastcall調用約定在輸出函數名前加上一個“@”符號,後面也是一個“@”符號和其參數的位元組數,格式@functionname@number。
它們均不改變輸出函數名中的字元大小寫,這和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”標識結束。
其格式為“?functionname@@yg*****@z”或“?functionname@@yg*xz”,例如
int test1-----“?test1@@yghpadk@z”
void test2-----“?test2@@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; int __attribute__((weak)) weak2 = 2; // 強制變數weak2為弱符號 int main() { return 0; }
以上,weak1與weak2是弱符號,strong與main是強符號。
針對強弱符號的概念,鏈接器會按照以下規則處理與選擇被多次定義的全局符號:
(1) 不允許強符號被多次定義,否則鏈接器報符號重覆定義的錯誤
(2) 如果一個符號在某個目標文件中是強符號,在其他文件中是弱符號,則選擇強符號
(3) 如果一個符號在所有目標文件中都是弱符號,那麼選擇其中占用空間最大的一個
弱引用與強引用
對外部目標文件的符號引用在目標文件被最終鏈接成可執行文件時,須被正確決議,如果沒有找到該符號的定義,編譯器就會報符號為定義的錯誤,這種被稱為強引用;
與之對應還有一種弱引用,在處理弱引用時,即使該符號未被定義,鏈接器也不會報錯,預設其為0或一個特殊的值。
GCC可以通過"__attribute__((weakref))"來聲明一個外部函數的引用為弱引用。
__attribute__ ((weakref)) void fun(); int main() { if (NULL != fun) { fun(); } }
這種弱符號和弱引用對於庫來說十分有用,庫中定義的弱符號可以被用戶定義的強符號所覆蓋,從而使得程式可以使用自定義版本的庫函數;
或者程式可以對某些擴展功能模塊的引用定義為弱引用,當我們將擴展模塊與程式鏈接在一起時,功能模塊就可以正常使用;
如果我們去掉了某些功能模塊,那麼程式也可以正常鏈接,只是缺少了相應的功能,這使得程式的功能更加容易裁剪和組合。
#include <stdio.h> #include <math.h> // 將math系統庫函數abs聲明為弱符號 int __attribute__((weak)) abs(int); // 重新實現一個abs函數 int abs(int a) { return 0; } int main(int argc, char* argv[]) { int s = abs((int)-5); printf("s=%d\n", s); // s=0 return 0; }
對於鏈接器來說,整個鏈接過程,就是將多個輸入目標文件合成一個可執行二進位文件。
現代鏈接器,基本都是採用兩步鏈接的方法:
(1) 空間與地址分配
掃描所有的輸入目標文件,並且獲得它們的各個段的長度、屬性和位置,並且將輸入目標文件中的符號表中所有的符號定義和符號引用收集起來,統一放到一個全局符號表中。
這一步中,鏈接器將能夠獲得所有輸入目標文件的段長度,並且將它們合併,計算出輸出文件中各個段合併後的長度和位置,並建立映射關係。
(2) 符號解析與重定位
使用上面第一步中收集的所有信息,讀取輸入文件中段的數據、重定位信息(有一個重定位表Relocation Table),並且進行符號解析與重定位、調整代碼中的地址(外部符號)等。
參考
《程式員的自我修養鏈接、裝載與庫》