原創 C++應用程序在Windows下的編譯、鏈接:第一部分 概述


    本文是對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++ 對象的構造器和析構器的函數指針


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM