1. 程式人生 > >C++編譯連結原理簡介

C++編譯連結原理簡介

在實習的過程中,偶爾會在編譯程式碼的時候出現莫名其妙的連結錯誤,或者更慘的是,編譯連結通過了,執行的時候出現莫名其妙的coredump,查了半天原來是.a靜態庫更新了導致.h檔案和.o檔案不一致。

受夠了被這些錯誤支配的恐懼,所以決定補充一下這方面的知識。

以下內容參考自網路。

幾個概念:

1、編譯:編譯器對原始檔進行編譯,就是把原始檔中的文字形式存在的原始碼翻譯成機器語言形式的目標檔案的過程,在這個過程中,編譯器會進行一系列的語法檢查。如果編譯通過,就會把對應的CPP轉換成OBJ檔案。

2、編譯單元:根據C++標準,每一個CPP檔案就是一個編譯單元。每個編譯單元之間是相互獨立並且互相不可知。

3、目標檔案:由編譯所生成的檔案,以機器碼的形式包含了編譯單元裡所有的程式碼和資料,還有一些其他資訊,如未解決符號表匯出符號表地址重定向表等。目標檔案是以二進位制的形式存在的。

我們知道,在預編譯的時候,.h標頭檔案會被複制、擴充套件到包含它的.cpp檔案裡,然後編譯器編譯該.cpp檔案為一個.obj檔案,該.cpp檔案作為一個編譯單元獨立編譯。當編譯器將一個工程裡的所有.cpp檔案以分離的方式編譯完畢後,再由連結器進行連結成為一個可執行檔案。

編譯器的工作過程:

這裡我們只關注下目標檔案的生成。

假設有一個A.cpp檔案,如下定義:

    int n = 1;

    void
FunA() { ++n; }
它編譯出來的目標檔案A.obj就會有一個區域(或者說是段),包含以上的資料和函式,其中就有n、FunA,以檔案偏移量形式給出可能就是下面這種情況:

偏移量    內容    長度

0x0000    n       4

0x0004    FunA    ??

說明:實際目標檔案的佈局可能不是這樣,這裡只是方便學習才這樣表示,??表示長度未知,目標檔案的各個資料可能不是連續的,也不一定是從0x0000開始。

FunA函式的內容可能如下:

    0x0004 inc DWORD PTR[0x0000]
    0x00?? ret

有另外一個B.cpp檔案,定義如下:

    extern int n;

    void FunB()
    {
        ++n;
    }

它對應的B.obj的二進位制:

偏移量    內容    長度

0x0000    FunB    ??

由於n被宣告為extern,而extern關鍵字告訴編譯器n已經在別的編譯單元裡定義了,在這個單元裡不用定義。由於編譯單元之間是互不相關的,所以編譯器就不知道n究竟在哪裡,所以在函式FunB中就沒有辦法生成n的地址,那麼函式FunB中就是這樣的:

0x0000 inc DWORD PTR[????]

0x00?? ret

為了讓各個編譯單元結合起來,就需要連結器了。為了能讓連結器知道哪些地方的地址沒有填好(也就是還????),那麼目標檔案中就要有一個表來告訴連結器,這個表就是“未解決符號表”(unresolved symbol table)。同樣,提供n的目標檔案也要提供一個“匯出符號表”(exprot symbol table),來告訴連結器自己可以提供哪些地址。

因此,一個目標檔案不僅要提供資料和二進位制程式碼,還要提供兩個表:未解決符號表和匯出符號表,來告訴連結器自己需要什麼和自己能提供些什麼。

那麼這兩個表是怎麼建立對應關係的呢?

在C/C++中,每一個變數及函式都會有自己的符號,如變數n的符號就是n,函式的符號會更加複雜,根據編譯器不同而不同。

A.obj的匯出符號表為

符號    地址

n       0x0000

_FunA   0x0004

未解決符號為空。

B.obj的匯出符號表為

符號    地址

_FunB   0x0000

未解決符號表為

符號    地址

n       0x0001

這個表告訴連結器,在本編譯單元0x0001位置有一個地址,該地址不明,但符號是n。

在連結的時候,連結器在B.obj中發現了未解決符號,就會在所有的編譯單元中的匯出符號表去查詢與這個未解決符號相匹配的符號名,如果找到,就把這個符號的地址填到B.obj的未解決符號的地址處。如果沒有找到,就會報連結錯誤。在此例中,在A.obj中會找到符號n,就會把n的地址填到B.obj的0x0001處。

但是,如果是這樣的話,B.obj的函式FunB的內容就會變成

inc DWORD PTR[0x000](因為n在A.obj中的地址是0x0000)

如果每個編譯單元的地址都是從0x0000開始,那麼最終多個目標檔案連結時就會導致地址重複。所以連結器在連結時就會對每個目標檔案的地址進行調整。比如B.obj的0x0000被定位到可執行檔案的0x00001000上,而A.obj的0x0000被定位到可執行檔案的0x00002000上,這樣就可以保證地址不會重複。為實現這一點,目標檔案還要提供一個表,叫地址重定向表(address redirect table)。

總結:

目標檔案至少要提供三個表:未解決符號表,匯出符號表和地址重定向表。

未解決符號表:列出了本單元裡有引用但是不在本單元定義的符號及其出現的地址。

匯出符號表:提供了本編譯單元具有定義,並且可以提供給其他編譯單元使用的符號及其在本單元中的地址。

地址重定向表:提供了本編譯單元所有對自身地址的引用記錄。

連結器的工作順序:

當連結器進行連結的時候,首先決定各個目標檔案在最終可執行檔案裡的位置。然後訪問所有目標檔案的地址重定義表,對其中記錄的地址進行重定向(加上一個偏移量,即該編譯單元在可執行檔案上的起始地址)。然後遍歷所有目標檔案的未解決符號表,並且在所有的匯出符號表裡查詢匹配的符號,並在未解決符號表中所記錄的位置上填寫實現地址。最後把所有的目標檔案的內容寫在各自的位置上,再作一些其他工作,就生成一個可執行檔案。

說明:實現連結的時候會更加複雜,一般實現的目標檔案都會把資料,程式碼分成好向個區,重定向按區進行,但原理都是一樣的。

幾個經典的連結錯誤

unresolved external link..

這個很顯然,是連結器發現一個未解決符號,但是在匯出符號表裡沒有找到對應的項。

解決方案就是在某個編譯單元裡提供這個符號的定義。(注意,這個符號可以是一個變數,也可以是一個函式),也可以看看是不是有什麼該連結的檔案沒有連結。

duplicated external simbols...

這個則是匯出符號表裡出現了重複項,因此連結器無法確定應該使用哪一個。這可能是使用了重複的名稱,也可能有別的原因。

C/C++針對這些而提供的特性:

extern:告訴編譯器,這個符號在別的編譯單元裡定義,也就是要把這個符號放到未解決符號表裡去。(外部連結)

static:如果該關鍵字位於全域性函式或者變數的宣告的前面,表明該編譯單元不匯出這個函式/變數的符號。因此無法在別的編譯單元裡使用。(內部連結)。如果是static區域性變數,則該變數的儲存方式和全域性變數一樣,但是仍然不匯出符號。

預設連結屬性:對於函式和變數,預設外部連結,對於const變數,預設內部連結。(可以通過新增extern和static改變連結屬性)

外部連結的利弊:外部連結的符號,可以在整個程式範圍內使用(因為匯出了符號)。但是同時要求其他的編譯單元不能匯出相同的符號(不然就是duplicated external simbols)

內部連結的利弊:內部連結的符號,不能在別的編譯單元內使用。但是不同的編譯單元可以擁有同樣名稱的內部連結符號。

一些問題的解答

為什麼標頭檔案裡一般只可以有宣告不能有定義?

標頭檔案可以被多個編譯單元包含,如果標頭檔案裡有定義,那麼每個包含這個標頭檔案的編譯單元就都會對同一個符號進行定義,如果該符號為外部連結,則會導致duplicated external simbols。因此如果標頭檔案裡要定義,必須保證定義的符號只能具有內部連結。

為什麼類的靜態變數不可以就地初始化?

所謂就地初始化就是類似於這樣:

    class A
    {
        static char msg[] = "aha";
    };

由於class的宣告通常是在標頭檔案裡,如果允許這樣做,其實就相當於在標頭檔案裡定義了一個非const變數。

為什麼公共使用的行內函數要定義於標頭檔案裡?

因為編譯時編譯單元之間互相不知道,如果行內函數被定義於.cpp檔案中,編譯其他使用該函式的編譯單元時沒有辦法找到函式的定義,因此無法對函式進行展開。所以說如果行內函數定義於.cpp檔案裡,那麼就只有這個cpp檔案可以使用這個函式。

標頭檔案裡的行內函數被拒絕會怎樣?

記住,內聯只是給編譯器的一個建議,如果定義於標頭檔案裡的行內函數被拒絕,那麼編譯器會自動在每個包含了該標頭檔案的編譯單元裡定義這個函式並且不匯出符號。

相關推薦

C++編譯連結原理簡介

在實習的過程中,偶爾會在編譯程式碼的時候出現莫名其妙的連結錯誤,或者更慘的是,編譯連結通過了,執行的時候出現莫名其妙的coredump,查了半天原來是.a靜態庫更新了導致.h檔案和.o檔案不一致。 受夠了被這些錯誤支配的恐懼,所以決定補充一下這方面的知識。

c++ 編譯連結執行原理及虛擬地址空間佈局

當我們寫好.c/.cpp檔案時 此時檔案還不能執行 因為他要經過以下的四步才可以執行   .c/.cpp(生成.i)     編譯(生成.s)      彙編(生成.o)  &nbs

ubuntu下c++編譯連結caffe的工程

最近在做深度網路相關的專案,我們通常可以從github上download很多相關的原始碼,但是在我們的機子上編譯的時候通常會遇到很多問題,將我最近踩的坑做了一些總結,希望對大家有所幫助。 1.如果直接g++  ×××.cpp 出現下面或者是出現某種語法錯誤之類的 [Cli

編譯連結原理

虛擬地址空間        32位計算機,每個程式都有4G的虛擬地址空間。首先虛擬地址空間分為兩大塊,一個是使用者空間,一個是核心空間。使用者空間佔3G的大小,並且它是每個程序所獨有的,它的開頭128M存放的是我們無法訪問的地方。 .text段:它也叫指令段,顧名思義

編譯連結原理(二)——編譯階段

       一、.o檔案         編譯階段經過預編譯、編譯和彙編處理後生成一個.o檔案(以Linux系統為例),又編譯器編譯原始碼後生成的檔案叫做目標檔案。則目標檔案就是原始碼編譯後但未進行連線的那些中間檔案(windows下的.obj和Linux下的.o),它跟

C&C++編譯連結過程

本文講解編譯連結過程,因為才疏學淺可能有些不準確。 使用c檔案的編譯連結過程,使用Linux系統用來檢視檔案資訊。 (1)從原始檔main.c編譯連結成main.exe,需要經歷如下步驟:     (2)其中符號和符號表是什麼呢?段又是什麼? 段:在一個程

μc/os-II原理簡介(筆記)

第一章 1、實時作業系統必須是多工系統,任務的切換時間應與系統中的任務數無關,並且中斷延遲的時間應該可預知並儘可能短。 第二章 3.1.1 1、從任務的儲存結構上看,μc/os-II的任務由:任務程式程式碼、任務堆疊和任務控制塊組成。 2、μc/os-II是所有的任務都是執

C++編譯連結全過程

今天博文主要討論的問題是:我們編寫的程式程式碼是怎樣執行起來的?到底執行的是什麼內容?平時我們所說的編譯主要包括預編譯、編譯、彙編三部分,這三部分分別都幹什麼工作,主要職能有哪些,接下來我們一步步探討總結。 (一)預編譯 (1)由原始檔“.cpp/.c”生成“.i”檔案,

C++編譯連結的那些小事

最近,有同事向我多次問及C++關於編譯連結方面的問題,包括如下: 1:什麼樣的函式以及變數可以定義在標頭檔案中 2:extern "C"的作用 3:防止重複包含的巨集的作用 4:函式之間是怎麼連結起來的 我認為,這些問題不難,書上基本上都有,但要是沒有真正思考過,就憑死記硬

C程式編譯連結】gcc使用命令介紹 gcc的使用簡介與命令列引數說明

1.gcc或者g++安裝rpm -qa|grep gcc ==>檢查gcc是否安裝gcc -v ==>檢查gcc版本 編譯器會在可執行檔案中植入一些資訊,可執行檔案會變大。一般開發時候使用 -g ,編譯一個 “release 版本” 時不使用 -g 編譯。gcc如果是最新的則不重

C++_編譯連結原理及基礎

基礎知識點: 馮諾依曼體系:                          計算機 運算器、控制器、記憶體、輸入裝置、輸出裝置          CPU            記憶體                I/O 計算機識別:0、1程式碼 CPU識別:指令、資料

C++編譯原理(VS環境)

規範 編譯原理 win 編譯 找不到 編譯器 linu inux lin VS是一個編譯器,它的功能 1、可視化的代碼編輯器; 2、可視化的代碼編譯器; 3、方便的代碼調試器; 4、做好了windows操作系統擁有的庫文件和接口; 編譯的詳細步驟 1、編譯單個的.c文件生成

C/C++靜態庫連結原理

前面我們學習了編譯連結的一些知識,現在來看看靜態庫連結的一些知識~ 靜態庫本質上就是使用ar命令打包一堆.o檔案: $ ar -r test.a myObj1.o myObj2.o   靜態庫沒有標準,不同的linux下都會有些細微的差別。大致的格式: Glo

C程式編譯連結】gcc使用命令介紹 GCC編譯器編譯連結  

1.gcc安裝 rpm -qa|grep gcc ==>檢查gcc是否安裝 gcc -v ==>檢查gcc版本 yum -y install gcc ==>安裝gcc  2.基本語法 gcc最基本的用法是:gcc [options]

編譯連結執行原理

編譯連結執行原理 編譯階段一共分為3部:預編譯階段,編譯階段,和彙編階段。   我們先來看第一階段: 預編譯:將原始碼檔案.c和相關的標頭檔案.h等 預編譯成一個.i檔案 gcc -E hello.c -o hello.i (-E表示只預編譯)   預

C指針原理(11)-編譯原理-小型計算器實現

$2 計算器 tex 打印 turn The 行號 clist 指針 我們接著完善這個計算器程序,讓算式能顯示出來,修改calculator.l 我們接著完善這個計算器程序,讓算式能顯示出來,修改calculator.l 通過加入printf語句,打印詞法分析器解析到的字符

C指針原理(10)-編譯原理-小型計算器實現

x文件 計算器 window 因此 pre rac 忽略 ken 命名 、打開cygwin,進入home目錄,home目錄在WINDOWS系統的cygwin安裝目錄映射為home目錄。 2、首先,在home目錄中新建文件夾,在文件夾中放置如下內容的test1.l /*統計字

Ubuntu上的C/C++編譯,基於cmake(附例項連結

1. apt-get安裝cmake,版本應該到3.5以上 2. 建立工程資料夾,命名為專案名稱,ProjectName 3. 分別在ProjectName下建立src、bin、build三個資料夾,存放原始檔、執行程式、編譯檔案 4. ProjectName下建立頂層C

C++編譯連結(1)-編譯連結過程

大家知道計算機使用的一系列的1和0 那個一個C++語言程式又是如何從一個個.h和.cpp檔案變成包含1和0的可執行檔案呢? 可以認為有以下的幾個環節 源程式->預處理->編譯和優化->生成目標檔案->連結->可執行檔案

GCC 程式的編譯過程和連結原理

一、C/C++檔案的編譯過程: 先來看一下gcc的使用方法和常用選項 提示:gcc --help Ⅰ、使用方法: gcc [選項] 檔名