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

    本文是對C++應用程序在Windows下的編譯、連接的深刻理解和分析,文章的目錄以下:ios

   

    咱們先看第一章概述部分。程序員

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++源代碼文件。windows

在集成開發環境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++ 對象的構造器和析構器的函數指針

相關文章
相關標籤/搜索