程序的編譯鏈接過程

轉載自:https://www.cnblogs.com/kekec/p/3238741.html

隨筆 - 229  文章 - 0  評論 - 118

程序的編譯鏈接過程

還是從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),並且進行符號解析與重定位、調整代碼中的地址(外部符號)等。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章