1. 程式人生 > >程式的編譯連結過程

程式的編譯連結過程

原文連結: 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 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呼叫約定在輸出函式名前加上一個下劃線字首,後面加上一個“@”符號和其引數的位元組數,格式為[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;