C++應用程式在Windows下的編譯、連結:第一部分 概述

yangxi_001發表於2016-11-17

本文是對C++應用程式在Windows下的編譯、連結的深入理解和分析,文章的目錄如下:

   

    我們先看第一章概述部分。

1概述

1.1編譯工具簡介

cl.exe是windows平臺下的編譯器,link.exe是Windows平臺下的連結器,C++原始碼在使用它們編譯、連結後,生成的可執行檔案能夠在windows作業系統下執行。cl.exe和link.exe整合在Visual Studio中,隨著開發工具Visual Studio的安裝,它們也被安裝到與VC相關的目錄下。

使用該編譯器的方式有兩種,一種是在Visual Studio開發環境中,直接點選命令按鈕,通過Visual Studio啟動編譯器;另外一種方式是在命令列視窗中通過c l命令編譯C++原始碼檔案。

在整合開發環境Visual Studio中,已經設定好了c l命令的各種預設引數,當使用Visual Studio編譯C++原始碼的時候,最終會呼叫到這個編譯工具,並且使用這些事先設定好的預設引數。

在安裝Visual Studio的時候,安裝程式在命令列工具“Visual Studio 2008 Command Prompt”中設定了編譯器(cl.exe)和連結器(link.exe)需要的各種引數和變數,因此,在“Visual Studio 2008 Command Prompt”工具的命令列視窗中,可以使用c l命令編譯C++原始碼。Visual Studio 2008 Command Prompt工具的路徑是:開始-》所有程式-》Visual Studio-》Visual Studio Tools-》Visual Studio 2008 Command Prompt。

在編譯C++原始碼的時候,編譯器需要使用到三個環境變數,它們分別是:

  • Path,用於設定編譯器cl.exe的路徑,以及該編譯器所依賴的一個動態連結庫(mspdb80.dll)所在的路徑。設定了這個環境變數以後,就可以在命令列視窗直接鍵入c l命令,而不需要把當前目錄定位到cl.exe的安裝目錄;比如:“C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin;C:\Program Files (x86)\Microsoft Visual Studio 9.0\Team Tools\Performance Tools”。前一個地址指定cl.exe所在的路徑,後一個地址指定mspdb80.dll的路徑。
  • Include,用於設定C執行庫標頭檔案的路徑。設定了這個環境變數以後,在C++原始碼中,就可以使用“#include <stdio.h>”的形式引入執行庫的標頭檔案;如果不設定這個變數,就必須使用“#include<C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include\stdio.h>”的形式引入執行庫的標頭檔案,否則在編譯的時候,編譯器就無法找到這些要被引入的標頭檔案。比如:“C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include”,該路徑指定了C執行庫標頭檔案的位置。
  • Lib,用於設定C執行庫目標檔案的路徑。連結器使用該環境變數定位C執行庫的目標檔案。比如:“C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\lib”,該路徑指定了C執行庫目標檔案的位置。

如果我們在系統環境變數中設定了這三個環境變數,那麼就可以在普通的命令列視窗中使用cl命令編譯C++原始碼,而不是使用Visual Studio的整合工具“Visual Studio 2008 Command Prompt”。

1.2應用程式示例      

1.2.1C++原始碼

本文將以如下應用程式示例展開論述,通過對應用程式的編譯,連結過程的介紹,著重講解PE檔案的資料格式,以及在應用程式載入的過程中,作業系統是如何進行“重定基地址”,以及執行各個DLL之間的“動態連結”。

示例應用程式各模組之間的呼叫關係如下圖所示:

在示例應用程式中,各原始碼檔案的說明如下表:

序號

檔名稱

描述

1

DemoDef.h

定義函式的匯入,匯出;定義全域性變數和全域性函式

2

DemoMath.h

數學操作類的定義

3

DemoOutPut.h

資訊輸出類的定義

4

DemoMath.cpp

數學操作類的實現,全域性函式的定義,全域性變數的定義

5

DemoOutPut.cpp

資訊輸出類的實現

6

main.cpp

主函式

 

示例應用程式的原始碼如下:

------------------------------------main.cpp--------------------------------------------

#include "DemoDef.h"

#include "DemoMath.h"

#include <iostream>

using namespace std;

int nGlobalData = 5;

int main()

{

     DemoMath objMath;

     objMath.AddData(10,15);

     objMath.SubData(nGlobalData,3);

     objMath.DivData(10,0);

     objMath.DivData(10,nGlobalData);

     objMath.Area(2.5);

     int ntimes =  GetOperTimes();

     cout << "操作次數為:" << ntimes << endl;

     //用於停止命令列

     int k = 0;

     cin >> k;

}

----------------------------------------DemoDef.h------------------------------------

#ifndef _DemoDef_H

#define _DemoDef_H

#include <stdio.h>

//定義函式的匯入,匯出

#ifdef DEMODLL_EXPORTS

#define DemoDLL_Export _declspec(dllexport)

#else

#define DemoDLL_Export _declspec(dllimport)

#endif

 

//檔案作用域中的符號常量,將要執行常量摺疊

const double PI = 3.14;

 

//宣告全域性變數,記錄操作的次數

extern int nOperTimes;

 

//宣告全域性函式,返回操作的次數

int DemoDLL_Export GetOperTimes();

#endif

-------------------------------------DemoMath.h-------------------------------------

#ifndef _DemoMath_H

#define _DemoMatn_H

 

#include "DemoDef.h"

 

class DemoOutPut;

 

class DemoDLL_Export DemoMath

{

public:

     DemoMath();

     ~DemoMath();

 

     void AddData(double a,double b);

     void SubData(double a,double b);

     void MulData(double a,double b);

     void DivData(double a,double b);

     void Area(double r);

 

private:

     DemoOutPut * m_pOutPut;

};

#endif

 

--------------------------------------------------DemoOutPut.h-----------------------------------------

#ifndef _DemoOutPut_H

#define _DemoOutPut_H

 

//執行資訊輸出

class DemoOutPut

{

public:

     DemoOutPut();

     ~DemoOutPut();

 

     //輸出數值

     void OutPutInfo(double a);

     //輸出字串

     void OutPutInfo(const char* pStr);

};

#endif

 

-----------------------------------------------------DemoMath.cpp-----------------------------------------

#include "DemoMath.h"

#include "DemoOutPut.h"

 

//全域性變數的定義

int nOperTimes = 0;

 

//全域性函式的定義

 int  GetOperTimes()

{

     return nOperTimes;

}

 

//類方法的實現

DemoMath::DemoMath()

{

     m_pOutPut = new DemoOutPut();

}

 

DemoMath::~DemoMath()

{

     if(m_pOutPut != NULL)

     {

         delete m_pOutPut;

         m_pOutPut = NULL;

     }

}

 

void DemoMath::AddData(double a, double b)

{

     nOperTimes++;

     m_pOutPut->OutPutInfo(a + b);

}

 

void DemoMath::SubData(double a, double b)

{

     nOperTimes++;

     m_pOutPut->OutPutInfo(a - b);

}

 

void DemoMath::MulData(double a, double b)

{

     nOperTimes++;

     m_pOutPut->OutPutInfo(a * b);

}

 

void DemoMath::DivData(double a, double b)

{

     if (b == 0)

     {

         m_pOutPut->OutPutInfo("除數不能為零");

         return;

     }

 

     nOperTimes++;

     m_pOutPut->OutPutInfo(a / b);

}

 

void DemoMath::Area(double r)

{

     nOperTimes++;

     m_pOutPut->OutPutInfo( r * r * PI);

}

 

---------------------------------------------------------DemoOutPut.cpp---------------------------------------------

#include <iostream>

#include "DemoOutPut.h"

 

DemoOutPut::DemoOutPut()

{

}

 

DemoOutPut::~DemoOutPut()

{

}

 

void DemoOutPut::OutPutInfo(double a)

{

     std::cout << "計算的結果為:" << a << std::endl;

}

 

void DemoOutPut::OutPutInfo(const char *pStr)

{

     std::cout << pStr << std::endl;

}

1.2.2Visual Studio對C++原始碼的支援

在編寫C++原始碼的時候,如果要使用一個類庫,那麼就必須引入這個類庫的標頭檔案,就必須知道這個型別標頭檔案的具體路徑。在上面的程式碼示例中,使用了“#include <stdio.h>”這種形式引入了一個C執行庫的標頭檔案。在這個引用中,我們沒有設定該標頭檔案的具體路徑,也沒有在其他位置設定該標頭檔案的具體路徑,但是整合開發環境Visual Studio能夠找到該檔案的具體位置。具體原因是這樣的:在安裝Visual Studio的時候,安裝程式已經在Visual Studio中設定了C執行庫標頭檔案的具體位置。通過選單“Tool-Options->Projects and Solutions->VC++Directories”可以檢視到這些事先設定的資訊。具體情況如下圖:

在上圖中,通過下拉視窗“Show directories for”,可以選擇要設定的路徑的型別,包括:標頭檔案的路徑(Include files),lib檔案的路徑(Library files),原始碼檔案(Source files)的路徑等。

在標頭檔案路徑的設定中,一共設定了四類標頭檔案的路徑,分別是C執行庫標頭檔案的路徑,MFC類庫標頭檔案的路徑,Win32API開發相關的標頭檔案路徑,以及與FrameWork相關的標頭檔案的路徑。

除了系統事先設定好的各種路徑外,我們也可以在該視窗中設定我們需要的各種其他路徑。

1.3C++原始碼的編譯過程

1.3.1編譯過程概述

在編譯C++原始碼的時候,整個編譯過程可以劃分為兩個階段,分別是編譯階段和連結階段。在編譯階段,以程式設計師編寫的C++原始碼(標頭檔案+原始檔)為輸入,經過編譯器的處理後,輸出COFF格式的二進位制目標檔案;在連結階段,以編譯階段輸出的目標檔案為輸入,經過連結器的連結,輸出PE格式的可執行檔案。整個編譯的過程如下圖所示:

編譯階段又可以進一步細分為三個子階段,分別是:預編譯,編譯和彙編。在每一個子階段中,都會對應不同的工作內容,以及輸出不同的輸出物。

由於程式設計師是在C/C++執行庫的基礎上開發出來的C++應用程式,所以在連結階段,除了要將編譯階段輸出的目標檔案進行連結外,還要加入對C/C++執行庫中相關目標檔案的連結。這種連結分為兩種情況。一種情況是:由於C++原始碼中顯式地呼叫了C/C++執行庫中的函式而引起的連結。例如:在C++原始碼中呼叫了C/C++執行庫中的函式:printf(),那麼在連結的時候,就需要把printf()所在的目標檔案也連結進來。另外一種情況是隱式地,由連結器自動完成。在C++應用程式執行的時候,它必須要得到C/C++執行庫的支援,因此在連結的時候,那些支援C++應用程式執行的庫檔案也被連結器自動地連結過來。無論哪種情況,C/C++執行庫都必須被連結到C++應用程式中。

在命令列視窗中,可以使用c l命令對C++原始碼進行編譯。在編譯的時候,可以設定不同的編譯選項,進而獲得不同的輸出結果。比如:可以一步完成編譯工作,直接獲得PE格式的可執行檔案。在這種情況下,cl.exe在完成編譯後,會自動呼叫link.exe執行連結工作。也可以通過分階段編譯的方式獲得不同階段的編譯結果,通過設定不同的c l命令選項,可以將編譯過程細分。比如:/C命令表示只編譯,不連結,通過這個命令就可以獲得目標檔案;/P命令表示只執行預編譯,通過它可以檢視預編譯的結果;/Fa命令表示執行彙編操作,通過它可以獲得組合語言格式的程式檔案。

在C++原始碼的編譯過程中,各個步驟的詳細描述如下表所示:

序號

步驟

輸入

輸出

描述

C l命令

1

預編譯

C++原始檔

.i檔案

輸出經過預處理後的檔案。

Cl /P xxx.cpp

2

編譯

C++原始檔

.asm檔案

輸出彙編檔案。

Cl /Fa xxx.cpp

3

彙編

C++原始檔

.obj檔案

輸出目標檔案

Cl /C xxx.cpp

4

連結

.obj檔案

.exe檔案

輸出可執行檔案

Cl xxx.obj

 

1.3.2編譯階段

1.3.2.1概述

每一種高階程式語言都有它自己的編譯器,在特定的作業系統平臺上,編譯器為該程式語言提供執行庫的支援,並且將該程式語言編寫的原始檔編譯成目標檔案。

通過提供C/C++執行庫的方式,Cl編譯器支援C/C++應用程式的開發。C/C++執行庫是由編譯器廠商提供的,每支援在一個作業系統系下的編譯,編譯器就需要提供一個能夠在該作業系統下執行的C/C++執行庫。通過對作業系統API的封裝,C/C++執行庫實現了C/C++標準庫的介面。由於標準庫的介面是統一的,原則上來說,使用C++語言開發出來的應用程式是可以執行在不同作業系統平臺上的。只需要針對該作業系統實現其執行庫。

不同的CPU硬體可能會要求不同的指令格式。編譯器在將高階語言翻譯成機器語言的時候,是依賴於計算機系統硬體的。根據不同的硬體,會產生不同的指令格式。編譯器遮蔽了計算機系統硬體的細節。

對上支援高階語言的程式編寫工作,對下封裝計算機系統硬體的細節,編譯器負責將高階語言編寫的源程式翻譯成底層計算機系統硬體能夠識別的二進位制機器程式碼,並將這些二進位制機器程式碼以統一的格式輸出,這個檔案格式就是COFF格式。

通過產生統一格式的COFF檔案,使編譯和連結能夠互相隔離。也就是說,連結器的實現不會依賴具體的編譯器。連結器只關注COFF格式的目標檔案,只要目標檔案的格式統一,那麼連結器就可以連結由不同編譯器編譯出來的目標檔案。

1.3.2.2預編譯

在預編譯階段,主要是處理那些原始碼檔案中以“#”開頭的預編譯指令,如:“#include”,“#define”等,主要的處理規則描述如下:

  • 將所有的“#define”指令刪除,並且將巨集定義展開;
  • 處理所有的條件編譯指令;
  • 處理#include預編譯指令,將被包含的標頭檔案插入到預編譯指令的位置。這可能是一個遞迴操作,如果被包含的標頭檔案中又包含其他標頭檔案;
  • 刪除所有的註釋;
  • 新增行號和檔案標識;
  • 保留所有的#program編譯器指令,後續的編譯步驟中要用到該指令。

經過預編譯的處理以後,標頭檔案被合併到原始檔中,並且所有的巨集定義都被展開。

 

示例一:對原始檔“DemoOutPut.cpp”進行預編譯操作,命令格式如下:

Cl /P DemoOutPut.cpp

執行預編譯以後,將會輸出“demooutput.i”檔案,該檔案的部分內容如下:

#line 2 "demooutput.cpp"

#line 1 "e:\\demo\\DemoOutPut.h"

class DemoOutPut

{

public:

         DemoOutPut();

         ~DemoOutPut();

         void OutPutInfo(double a);

         void OutPutInfo(const char* pStr);

};

#line 18 "e:\\demo\\DemoOutPut.h"

#line 3 "demooutput.cpp"

DemoOutPut::DemoOutPut()

{

}

DemoOutPut::~DemoOutPut()

{

}

void DemoOutPut::OutPutInfo(double a)

{

         std::cout << "計算的結果為:" << a << std::endl;

}

void DemoOutPut::OutPutInfo(const char *pStr)

{

         std::cout << pStr << std::endl;

}

 

   在“demooutput.i”檔案中,除了加入了行號資訊外,類DemoOutPut的標頭檔案和原始檔已經合併到了一起。在編寫C++原始碼的時候,如果我們無法確定巨集定義是否正確,那麼就可以輸出“.i”檔案,進而確定問題。

1.3.2.3編譯

以預編譯的輸出為輸入,將C++原始碼翻譯成計算機系統應將能夠識別的二進位制機器指令,並將編譯的輸出結果儲存在COFF格式的目標檔案中。在編譯的中間過程中,還可以通過c l命令選擇性地輸出組合語言格式的中間檔案。

編譯器在編譯的時候,一般會分為如下步驟,具體情況如下表描述:

序號

步驟

描述

1

詞法分析

掃描C++原始碼,識別各種符號。這些被識別的符號包括:C++系統關鍵字,函式名稱,變數名稱,字面值常量,以及特殊字元。函式名稱,變數名稱將被儲存到符號表中,字面值常量將被儲存到文字表中。

2

語法分析

將詞法分析階段產生的各種符號進行語法分析,產生語法樹。每個語法樹的節點都是一個表示式。

3

語義分析

此階段開始分析C++語句的真正意義。編譯器只能進行靜態語義分析,包括:宣告和型別的匹配,型別轉換等。經過語義分析,語法樹的表示式都被標識了型別。

4

原始碼級優化

執行原始碼級別的優化。比如:表示式3+8會被求值成11。

將語法樹轉換成中間程式碼,它是語法樹的順序表達。這個中間程式碼已經非常接近目的碼了,但是它和目標機器以及執行時環境是無關的。比如:不包含資料的尺寸,變數的地址,暫存器的名稱等。

中間程式碼將編譯器劃分成兩部分,第一部分負責產生與機器無關的中間程式碼;第二部分將中間程式碼轉化成目標機器程式碼。

5

目的碼生成及優化

將中間語言程式碼轉化成目標機器相關的機器程式碼。同時執行一些優化。

1.3.2.4COFF檔案中的段種類

在執行編譯的時候,編譯器以“.cpp”檔案為單位,對於每一個“.cpp”檔案,編譯器都會輸出一個目標檔案。在COFF格式的目標檔案中,按照二進位制檔案內容的功能和屬性的不同,會將檔案內容劃分成不同的段。COFF檔案所包含的段種類如下圖所示:

各個主要段的詳細資訊描述如下表:

序號

段名

描述

1

.text

在該段中包含C++程式的原始碼,這些原始碼已經被編譯成計算機系統硬體能夠識別的二進位制指令。每一個二進位制指令都必須對應一個虛擬記憶體地址

2

.data

已初始化的全域性變數,靜態變數儲存在該段中

3

.bss

未初始化的全域性變數儲存在該段中

4

.rdata

只讀的資料儲存在該段中

5

.debug$S

包含與除錯符號相關的除錯資訊

6

.debug$T

包含與型別相關的除錯資訊

7

.drectve

包含連結指示資訊,如採用哪個版本的執行庫,以及函式的匯出等。

85

重定位表

在該段中儲存著屬於其他段的重定位資訊。在編譯階段,某些二進位制指令的虛擬記憶體地址是暫時無法確定的,在重定位段將會記錄這些無法確定虛擬記憶體地址的位置。在連結階段,將使用這些重定位資訊。在重定位段中,主要的資訊欄位包括:需要重定位的位置,重定位地址的型別。對應的符號表索引等

9

行號表

在行號表中儲存的資訊描述了二進位制程式碼和C++原始碼之間的對應關係,應用於程式除錯。

10

符號表

在編譯的時候,函式名稱,變數名稱都會被當作符號來處理。編譯器將C++原始碼中出現的符號統一地儲存在符號表中。連結階段需要使用符號表中的資訊。

11

字串表

字串表用於輔助符號表。如果符號表中符號名的長度超過8個位元組,那麼這個名稱將被儲存到符號表中。而在符號表中,符號名稱的位置儲存了字串表中相關項的地址。

 

1.3.3連結階段

1.3.3.1連結的目標

在C++程式的開發過程中,程式程式碼是以“.cpp“檔案為單位來組織的。在各個檔案之間又會存在呼叫關係。比如:A.CPP檔案呼叫B.CPP檔案中的函式。

在C++程式的編譯階段,編譯器是以“.CPP”檔案為單位進行編譯的。也就是說,對於每一個“.CPP”檔案,都會生成一個“.obj”目標檔案。在目標檔案中,對於每一條指令或者指令要操作的資料,都應該生成一個虛擬記憶體的地址。如果一個目標檔案中要使用的函式或者資料被定義在另外一個目標檔案中,如:在A.obj檔案中呼叫了B.obj檔案中定義的函式。在將A.CPP生成A.obj的過程中,是無法馬上確定該被呼叫函式的地址的。因為該函式的地址記錄在B.obj檔案中。

連結器執行連結的過程就是將多個目標檔案合併在一起,形成可執行檔案的過程。在形成可執行檔案的過程中,連結器需要將在編譯階段無法確定的被呼叫符號(函式,變數)的虛擬記憶體地址確定下來。這就是連結的主要目標。

 

注:關於每個指令的虛擬記憶體地址,在目標檔案中,該地址以相對於檔案某個位置的偏移來表示;直到PE檔案生成的時候,才會將這些偏移值轉換成虛擬記憶體地址。

1.3.3.2連結的型別

首先看一個示例,在使用Visual Studio開發C++應用程式的時候,首先會建立一個解決方案,然後在解決方案中包含若干個專案,這些C++原始碼是以專案的形式組織在一起的。它們的關係如下圖所示:

解決方案“DemoDLL”中包含了兩個專案,分別是:“DemoDLL”,“DemoExe”。在專案“DemoDLL”中包含了兩個原始檔,分別是:“DemoMath.cpp”,“DemoOutPut.cpp”。專案“DemoExe”引用了專案“DemoDLL”中的函式。在編譯的時候,專案“DemoDLL”被編譯成了動態連結庫;專案“DemoExe”被編譯成了可執行檔案。在編譯這兩個專案的時候,C執行庫和C++執行庫也被連結了進來。

由上一節的描述可以得知,連結的主要目的是確定被呼叫函式的地址。即:使主調函式知道被呼叫函式的位置。在處理這個問題的時候,可以採用不同的方式和方法,因此也就有了不同的連結型別,具體的連結分類如下圖所示:

連結可以被分為靜態連結和動態連結兩種情況。而動態連結又被進一步劃分為隱式動態連結和顯式動態連結。

在上面的示例中,將原始檔“DemoMath.cpp”和原始檔“DemoOutPut.cpp”編譯成動態連結庫的時候,這兩個原始檔之間採用的連結型別是靜態連結。靜態連結的特點描述如下:

  • 在編譯時刻完成目標檔案之間的連結;
  • 所有的目標檔案的內容都被合併到一起,包括:程式碼,資料等,然後將這些合併後的內容輸出成一個PE格式的檔案。在上面的示例中,在“DemoMath.cpp”中呼叫了“DemoOutPut.cpp”中的函式,在執行連結的時候,主調函式的程式碼和被呼叫函式的定義都被寫入到了同一個檔案中,即:DemoDLL。

在上面的示例中,在專案“DemoExe”中呼叫了專案“DemoDLL”中的函式,在編譯的時候,這兩個專案之間的連結型別是動態連結。動態連結的特點描述如下:

  • 在編譯時刻,僅將被呼叫函式的符號寫入到主調函式所在的檔案中,主調函式和被呼叫函式分別位於不同的檔案中。在上面的示例中,主調函式位於可執行檔案“DemoExe.exe”,而被呼叫函式位於“DemoDLL.dll”中。
  • 在程式釋出的時候,需要將可執行檔案和動態連結庫一同釋出,缺一不可。在上面的示例中,“DemoExe.exe”和“DemoDLL.dll”必須一同提供給使用者,否則程式執行不起來;
  • 在程式載入的時候,由作業系統的載入器完成最終的連結。也就是說,在程式載入的時候,主調函式才能確定被呼叫函式的地址。所以,這種連結方式才叫動態連結,而“DemoDLL.dll”才被叫做動態連結庫。
  • 這種連結方式也叫做隱式動態連結,是預設的動態連結型別。
      

一般情況下,在同一個專案中,比如專案“DemoDLL”中,由程式設計師編寫的C++原始碼之間的連結方式是靜態連結;在多個專案之間,比如:專案“DemoExe”和專案“DemoDLL”之間,採用的連結方式是動態連結。

由程式設計師開發出來的可執行程式或動態連結庫,在執行的時候,它們是需要C/C++執行庫支援的。這些專案和執行庫之間的連結方式可以是靜態連結,也可以是動態連結。可以在編譯源程式的時候進行設定,確定是採用靜態連結方式還是採用動態連結方式。如果採用靜態連結方式,C/C++執行庫中的相關函式的程式碼被加入到目標專案中,然後合併成一個檔案釋出,這個檔案相對較大;如果是採用動態連結,只是將C/C++執行庫中的相關函式的符號寫入到了目標專案中。在程式釋出的時候,需要將生成可執行檔案和C/C++執行庫的動態連結庫檔案一同釋出。這時候生成的可執行檔案相對較小。

在Visual Studio中,可以通過如下方式更改C++應用程式與C/C++執行庫的連結方式,具體情況如下圖所示。

該窗體的開啟路徑如下:在解決方案中選擇一個專案,然後滑鼠右鍵選擇“屬性”選項,在彈出的窗體中,選擇C/C++標籤中的“程式碼生成”項。

在上圖“執行時庫”專案中,可以設定要連結的方式。一共有四種可以被選擇的連結方式,分別是:多執行緒靜態連結,多執行緒靜態連結除錯版,多執行緒動態連結,多執行緒動態連結除錯版。預設連結的型別為多執行緒動態連結。

如果選擇了動態連結方式,將會使用C/C++執行庫的動態連結版本,使用工具Dependency將生成的可執行檔案開啟後,各個元件之間的關係如下圖所示:

MSVCR90.dll是C執行庫所在的動態連結庫,MSVCP90.dll是C++執行庫所在的動態連結庫,Kerner32.dll和NTDLL.dll是作業系統的元件,它們以動態連結庫的形式提供。C/C++執行庫與Kerner32.dll之間採用動態連結的方式。在上圖中,可執行檔案DemoExe除了與DemoDll進行了動態連結外,還與C執行庫,C++執行庫,以及元件Kerner32.dll進行了動態連結;由程式設計師開發出來的動態連結庫DemoDLL.dll也與C執行庫,C++執行庫,以及元件Kerner32.dll進行了動態連結。C/C++執行庫又動態連結了元件Kerner32.dll,元件Kerner32.dll動態連結了元件NTDLL.dll。

如果選擇了靜態連結方式,將會使用C/C++執行庫的靜態連結版本。使用工具Dependency將可執行檔案開啟,各個元件之間的關係如下圖所示:

由於設定了靜態連結的方式,DemoExe和DemoDLL與C/C++執行庫之間的連結方式變成了靜態連結。但是DemoExe與DemoDLL之間的連結方式,已經C/C++執行庫與元件Kerner32.dll之間的連結方式依然是動態連結。所以,在上圖中可以看出,C/C++執行庫的相關程式碼已經被合併到DemoDLL.dll以及DemoExe中,已經看不到MSVCR90.dll和MSVCP90.dll的存在。但是由於C/C++執行庫與元件Kerner32.dll之間是動態連結,所以DemoExe和DemoDLL繼承了種連結方式,它們與元件Kerner32.dll之間的連結方式依然是動態連結。

 由上面的分析可以看出,在Visual Studio中設定的連結方式,只能影響應用程式與C/C++執行庫之間的連結。程式設計師開發出來的C++應用程式與其他元件之間的關係如下圖所示:

程式設計師開發的應用程式受到C/C++執行庫的支援,而C/C++執行庫在實現C/C++標準庫介面的時候,是需要受到作業系統元件的支援的。在Windows平臺上,它們分別是Kerner32.dll,以及NTDLL.dll。這些元件包含了對win32API的封裝,也就是說,在實現C/C++標準庫介面的時候,C/C++執行庫呼叫了Win32API中的相關函式。

動態連結的另外一種方式是顯式動態連結。當進行這種動態連結的時候,只要當真正執行函式的呼叫的時候,才會確定被呼叫函式的地址。隱式動態連結與顯式動態連結的區別是:隱式動態連結在程式載入的時候確定被呼叫函式的地址,而顯式動態連結將這個過程推後到具體函式呼叫的時候。可以使用函式LoadLibrary和函式GetProcAddress實現顯示動態連結。

1.3.3.3PE檔案中的段種類

在執行了連結以後,將多個目標檔案合併在一起,輸出了可執行檔案或者是動態連結庫。可執行檔案和動態連結庫的二進位制內容是以PE格式儲存的。在PE檔案中所包含的段的種類如下圖所示:

各個主要段的詳細資訊描述如下表:

序號

段名

描述

1

.text

在該段中包含C++程式的原始碼,這些原始碼已經被編譯成計算機系統硬體能夠識別的二進位制指令。每一個二進位制指令都必須對應一個虛擬記憶體地址

2

.data

已初始化的全域性變數,靜態變數儲存在該段中

3

.textbss

該段為程式碼段,在PE檔案中不佔用儲存空間,在虛擬記憶體中佔用虛擬記憶體的地址空間。在執行增量連結的時候,新修改過的函式的程式碼可能會被放到該段中。用於debug模式下。

4

.rdata

只讀的資料儲存在該段中,例如:字串文字。匯入,匯出表會被合併到該段中。

5

.idata

匯入表。在建立release版本的時候,該節經常被合併到.rdata節中。

6

.edata

匯出表。在建立一個包含匯出 API 或資料的時候,連結器會生成一個 .EXP 檔案。這個 .EXP 檔案包含一個最終會被新增到可執行檔案裡的 .edata 節。和 .idata 節一樣,.edata 節經常會被合併到 .text 或 .rdata 節中。

7

.rsrc

資源節,該節只讀,不能被合併到其他節。

8

reloc

基址重定位節。

9

.crt

為支援 C++ 執行時(CRT)而新增的資料。比如,用來呼叫靜態 C++ 物件的構造器和析構器的函式指標

 

相關文章