程式的編譯鏈接過程

来源:http://www.cnblogs.com/shouce/archive/2016/05/27/5533344.html
-Advertisement-
Play Games

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

 

參考

     《程式員的自我修養鏈接、裝載與庫》 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • mysql.data.dll下載_c#連接mysql必要插件 全部版本下載:http://hovertree.com/h/bjaf/0sft36s9.htm mysql.data.dll是C#操作MYSQL的驅動文件,是c#連接mysql必要插件,使c#語言更簡潔的操作mysql資料庫。當你的電腦彈 ...
  • 對於一些企業內部核心系統,特別是外網訪問的時候,為了信息安全,可能需要對外部訪問的IP地址作限制,雖然IIS中也提供了根據IP地址或IP地址段進行限制或允許,但並沒有提供根據IP地址所在的城市進行限制或允許。本文主要通過自定義擴展IHttpModule介面,考慮到性能IP資料庫主要採用QQwry純真 ...
  • DateTime dt = DateTime.Now;Label1.Text = dt.ToString();//2009-07-5 13:21:25Label2.Text = dt.ToFileTime().ToString();//127756416859912816Label3.Text = ...
  • C# 導出CSV文件 由於工作需要,需要在Web端請求後,將查詢的數據寫入CSV文檔,返回給Web。 註意點: 1.長字元串的的數字,比如身份證號碼之類的,在csv中顯示的是用科學計數法顯示的,因此需要轉換一下,例如:字元串“12345678987654321”, 寫入時為"=\"123456789 ...
  • 前言 前幾天看一個朋友的博客時,看他用到了C#6的特性,而6出來這麼長時間還沒有正兒八經看過它,今兒專門看了下新特性,說白了也不過是語法糖而已。但是用起來確實能讓你的代碼更加乾凈些。Let's try it. 1、集合初始化器 public class Post { public DateTime ...
  • 1. JVM相關(包括了各個版本的特性) 對於剛剛接觸Java的人來說,JVM相關的知識不一定需要理解很深,對此裡面的概念有一些簡單的瞭解即可。不過對於一個有著3年以上Java經驗的資深開發者來說,不會JVM幾乎是不可接受的。 JVM作為java運行的基礎,很難相信對於JVM一點都不瞭解的人可以把j ...
  • 需要在程式中使用二維數組,網上找到一種這樣的用法: 1 2 3 4 5 6 #創建一個寬度為3,高度為4的數組 #[[0,0,0], # [0,0,0], # [0,0,0], # [0,0,0]] myList = [[0] * 3] * 4 1 2 3 4 5 6 #創建一個寬度為3,高度為4的 ...
  • 探完閉包[查看],再探命名空間。 對於命名空間,官方文檔已經說得很詳細[查看],我在這裡做了一下實踐和總結。 命名空間一個最明確的目的就是解決重名問題,PHP中不允許兩個函數或者類出現相同的名字,否則會產生一個致命的錯誤。這種情況下只要避免命名重覆就可以解決,最常見的一種做法是約定一個首碼。 例:項 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...