C++應用程式在Windows下的編譯、連結:第三部分 靜態連結(二)
3.5.2動態連結庫的建立
3.5.2.1動態連結庫的建立流程
動態連結庫的建立流程如下圖所示:
在系統設計階段,主要的設計內容包括:類結構的設計以及功能類之間的關係,動態連結庫的介面。在動態連結庫中,包含兩類函式:一類是內部函式,一類是外部函式。內部函式只能在動態連結庫的內部使用,不能被動態連結庫以外的模組呼叫;外部函式是該動態連結庫的介面,可以被外部模組呼叫。
為了使外部函式能夠被系統外的模組呼叫,在進行C++程式碼編寫的時候,必須對外部函式執行匯出。匯出的級別有兩種:函式級別的匯出和類級別的匯出。在函式級別的匯出中,只將該函式匯出;在類級別的匯出中,將這個類所屬的函式和資料匯出。在進行匯出的時候,使用關鍵字“_declspec(dllexport)”。
如果外部模組要呼叫動態連結庫中的函式,那麼必須對該函式執行匯入。匯入的級別有兩種:函式級別的匯入和類級別的匯入。在函式級別的匯入中,只能將該函式匯入;在類級別的匯入中,可以將整個類所屬的函式和資料匯入,在進行匯入的時候,使用關鍵字“_declspec(dllimport)”。
在使用Visual Studio建立動態連結庫的時候,首先是建立工程專案,並且選擇專案型別為動態連結庫型別,即:Application type的DLL選項。Static Library表示建立靜態連結庫,Windows application表示建立到視窗的可執行程式,Console application表示建立帶命令列的可執行程式。具體情況如下圖所示:
建立完畢工程專案以後,向工程專案中新增各個類的標頭檔案,以及原始檔,開始各個功能類的編寫。在執行函式級別的匯出的時候,具體的C++程式碼樣式如下:
#ifndef _DemoMath_H #define _DemoMatn_H
class DemoOutPut; class DemoDLL_Export DemoMath { public: DemoMath(); ~DemoMath(); _declspec(dllexport) void AddData(double a,double b); //成員函式AddData被匯出 _declspec(dllexport) void SubData(double a,double b);//成員函式SubData被匯出 void MulData(double a,double b); //成員函式沒有被匯出,不能被該dll之外的函式呼叫 void DivData(double a,double b); void Area(double r); private: DemoOutPut * m_pOutPut; }; #endif |
執行類級別匯出的時候,具體的C++程式碼的樣式如下:
#ifndef _DemoMath_H #define _DemoMatn_H
class DemoOutPut; class _declspec(dllexport) 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 |
為了方便對匯入,匯出的管理,一般會將匯入,匯出的資訊定義到一個標頭檔案中,在需要進行匯入,匯出的時候,將這個標頭檔案引入即可。
這個標頭檔案包含了匯入和匯出兩方面的功能,在使用該標頭檔案之前定義巨集DeMODLL_EXPORTS,則執行匯出功能;如果沒有定義該巨集,則執行匯入功能。具體的定義內容如下:
#ifndef _DemoDef_H #define _DemoDef_H //定義函式的匯入,匯出 #ifdef DEMODLL_EXPORTS #define DemoDLL_Export _declspec(dllexport) #else #define DemoDLL_Export _declspec(dllimport) #endif #endif |
在類級別的匯出中,該標頭檔案的使用方式描述如下:
#ifndef _DemoMath_H #define _DemoMatn_H #include "DemoDef.h" //引入標頭檔案 class DemoOutPut; class DemoDLL_Export DemoMath //類級別匯出,使用該標記前,必須定義巨集:DEMODLL_EXPORTS { 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 |
在執行匯出的時候,在使用標頭檔案DemoDef.h之前,必須定義DemoDLL_EEPORTS。有兩種方式定義該巨集,一種方式是直接手工填寫程式碼,如:
#ifndef _DemoMath_H #define _DemoMatn_H
#define DEMODLL_EXPORTS //手工定義巨集,必須位於include “DemoDef.h” 之前 #include “DemoDef.h” Class ... #endif |
另外一種方式是在專案屬性中定義該巨集,如下圖所示:
開啟工程專案屬性視窗,在C/C++標籤的Preprocessor Definitions專案中,新增巨集定義即可。
3.5.2.2編譯動態連結庫
當動態連結庫的程式碼編寫完畢以後,就可以通過編譯、連結來生成該動態連結庫,在執行編譯、連結的時候,具體的輸入,輸出情況如下圖所示:
如果在建立該動態連結庫的時候引用了第三方靜態連結庫中的函式,那麼在連結的時候,需要將該靜態連結庫中相關的函式合併到輸出的PE檔案中;如果在建立該動態連結庫的時候引用了另外的動態連結庫,那麼在執行連結的時候,被引用動態連結庫的匯入庫檔案需要參與連結;目標檔案中包含的內容是整個動態連結庫的核心內容,被引入的第三方庫,無論是靜態連結庫,還是動態連結庫,都是為目標檔案中的功能程式碼提供支援的;另外,在連結的時候,也可以提供連結空間檔案Def,在該檔案中定義了連結選項的各個方面。
編譯、連結執行完畢以後,在預設情況下,連結器除了輸出PE檔案以外,還會輸出.exp檔案,.lib匯入庫檔案,以及符號檔案.pdb。通過連結配置,還可以輸出其他功能的檔案,如:.map檔案。
被輸出的PE檔案有兩種型別,可執行檔案和動態連結庫檔案。輸出的型別可以在建立專案的時候,在application type欄選擇。
3.5.2.3匯出表的建立
對於一個動態連結庫來說,至少要存在一個匯出函式。一般情況下,在一個動態連結庫中,會存在匯出若干個函式。這些被匯出函式的資訊被存放到匯出表中。當編譯、連結完成以後,匯出表中的資訊被儲存到PE檔案中。
匯出表由一個被資料目錄所指向的資料結構IMAGE_EXPORT_DIRECTORY開始,並關聯到三個陣列,這三個陣列分別儲存被匯出函式的地址,被匯出函式的名稱,以及被匯出函式名稱與序號之間的關係。在程式載入的時候,匯出表中的資訊被載入器用來執行動態連結。
連結器生成匯出表的過程如下圖所示:
連結器在執行第一遍掃描的時候,會以各個目標檔案為輸入。在掃描的過程中,連結器收集匯出函式的資訊,然後將這些資訊寫入到一個臨時檔案中。該臨時檔案以.exp為副檔名,但實際上它是一個COFF格式的檔案,該檔案格式與目標檔案是一致的。在.exp檔案中,連結器建立了匯出表“.edata”。
連結器在進行第二次掃描的時候,會將各個目標檔案和第一掃描生成的.exp檔案連結到一起。將.exp檔案中的匯出表的資訊提取出來,並寫入到PE檔案中。在PE檔案中,匯出表不會單獨存在,它一般會被合併到.rdata節,該節儲存只讀資料。
當連結執行完畢以後,.exp檔案的使命結束,在後續的工作中,一般不會使用到它。
3.5.2.4匯入庫
在編譯、連結動態連結庫的時候,另一個重要的輸出物就是匯入庫,匯入庫的副檔名是.lib。該副檔名與靜態連結庫的副檔名一致。但是,匯入庫與靜態連結庫是不同的。
靜態連結庫是一系列目標檔案的集合,這些目標檔案以特定的格式打包、壓縮在靜態連結庫中。當執行靜態連結的時候,靜態連結庫中的相關程式碼和資料需要被複制到要生成的PE檔案中。程式執行的時候不需要靜態連結庫參與。
匯入庫中儲存的不是動態連結庫的程式碼和資料,而是動態連結庫中被匯出函式的描述資訊,每一個動態連結庫都會對應一個匯入庫。在連結階段,匯入庫參與連結;在程式執行階段,動態連結庫參與執行。
當在一個目標檔案中引用了一個動態連結庫中函式的時候,在編譯階段,就需要將該目標檔案和被引用的動態連結庫的匯入庫一起連結。在匯入庫的支援下,將會生成PE檔案的匯入表。實際上,PE檔案的匯入表就是由多個匯入庫中的資訊合併到一起而生成的。
在匯入庫中,主要包含如下內容:
在匯入庫中,主要包含了三部分內容,分別是:樁程式碼,啟動程式碼,以及匯出函式的符號。在執行連結的時候,啟動程式碼,樁程式碼要被合併到輸出的PE檔案中;而被匯出的函式的符號則以兩種形式提供,用於支援在PE檔案中匯入表的建立,以及對外部符號的解析工作。
樁程式碼包含了一系列jump指令,每條jump指令都會跳轉到匯出函式的地址處,用於支援函式呼叫。樁程式碼,匯出函式的符號(兩種形式),以及匯出函式的地址之間的關係如下圖所示:
在匯入庫中,被匯出的函式名稱以兩種方式提供,方式一:_imp_函式名 + 修飾;方式二:函式名 + 修飾。例如上圖中的_imp_Fun1和Fun1形式。
使用工具dumpbin匯出DemoDlld.lib的內容,在該內容中,一個符號匯出了兩種形式,具體情況如下:
Dump of file demodlld.lib
File Type: LIBRARY
Archive member name at 8: / 51C17BC5 time/date Wed Jun 19 17:37:09 2013 uid gid 0 mode 2A2 size correct header end
21 public symbols
5CE __IMPORT_DESCRIPTOR_DemoDLLd 7FC __NULL_IMPORT_DESCRIPTOR 934 DemoDLLd_NULL_THUNK_DATA B6C ??4DemoMath@@QAEAAV0@ABV0@@Z B6C __imp_??4DemoMath@@QAEAAV0@ABV0@@Z D50 ?GetOperTimes@@YAHXZ D50 __imp_?GetOperTimes@@YAHXZ A88 ??0DemoMath@@QAE@XZ A88 __imp_??0DemoMath@@QAE@XZ AFA ??1DemoMath@@QAE@XZ AFA __imp_??1DemoMath@@QAE@XZ BE6 ?AddData@DemoMath@@QAEXNN@Z BE6 __imp_?AddData@DemoMath@@QAEXNN@Z E3C ?SubData@DemoMath@@QAEXNN@Z E3C __imp_?SubData@DemoMath@@QAEXNN@Z DC2 ?MulData@DemoMath@@QAEXNN@Z DC2 __imp_?MulData@DemoMath@@QAEXNN@Z CD6 ?DivData@DemoMath@@QAEXNN@Z CD6 __imp_?DivData@DemoMath@@QAEXNN@Z C60 ?Area@DemoMath@@QAEXN@Z C60 __imp_?Area@DemoMath@@QAEXN@Z |
在方式二中,每一個函式名稱都會對應一個jump指令,該函式名稱的地址是jump指令的入口。通過jump指令,可以跳轉到方式一形式的函式名稱處。
在方式一形式的函式名稱處,每一個函式名稱都會對應到動態連結庫中的一個函式的地址。
在使用該動態連結庫的時候,可以通過兩種方式來呼叫該動態連結庫中的函式,具體情況如下:
方式一: Call _imp_Fun //高效方式,需要關鍵字_declspec(dllimport)支援。
方式二: Call Fun //低效方式,執行了額外的jump指令跳轉
Fun: Jump _imp_Fun |
3.5.2.5動態連結庫的釋出
在釋出動態連結庫的時候,要包含如下檔案:
- 標頭檔案。標頭檔案中定義了動態連結庫的介面,在編碼階段中,在使用動態連結庫的時候,其他模組需要引入動態連結庫的標頭檔案;
- 匯入庫檔案。匯入庫檔案包含了動態連結庫的匯出函式的資訊,在執行連結的時候,連結器需要匯入庫中的資訊,為PE檔案建立匯入表,以及執行符號解析;
- 動態連結庫檔案。動態連結庫檔案中包含了函式和資料,在程式執行階段,動態連結庫檔案要被載入到記憶體,並且和呼叫該動態連結庫檔案的可執行程式執行動態連結。
3.5.3動態連結庫的使用
3.5.3.1在編碼階段對動態連結庫標頭檔案的使用
在釋出動態連結庫的時候,動態連結庫的釋出者需要同時釋出該動態連結庫的標頭檔案。在編碼階段,當需要呼叫動態連結庫中的方法的時候,程式設計師需要使用#include命令將該動態連結庫的標頭檔案引入。
在使用動態連結庫中的函式的時候,可以有兩種方式。一種方式是:使用關鍵字_declspec(dllimport)將需要的函式顯式地匯入,這些被匯入的函式必須位於動態連結庫被匯出函式的集合中;另外一種方式是:直接使用動態連結庫中被匯出的函式,不做任何顯式地匯入。
使用_declspec(dllimport)匯入動態連結庫中的函式的時候,將會使用高效地函式呼叫方式,而不使用_declspec(dllimport)匯入動態連結庫中的函式的時候,將會使用低效地函式呼叫方式。
當使用關鍵字_declspec(dllimport)修飾被呼叫函式名稱的時候,在編譯階段,函式的名稱將被處理成“_imp_函式名稱 + 修飾”的形式。當執行靜態連結的時候,Call指令後面的運算元被解析成被呼叫函式的地址。因此,通過一次對Call指令的執行,就可以完成對動態連結庫中被呼叫函式的呼叫。
如果不使用關鍵字_declspec(dllimport)修飾被呼叫函式的名稱,那麼在編譯階段,函式的名稱將被處理成“函式名稱 + 修飾”的形式。當執行靜態連結的時候,匯入庫中的樁程式碼被合併到PE檔案中。而Call指令後面的運算元被解析成一段樁程式碼的地址,在這段樁程式碼中,通過jump指令才能跳轉到被呼叫函式的地址處。具體情況可參見3.5.2.4節的描述。因此,通過執行兩步指令才能完成對動態連結庫中函式的呼叫。
為了實習那高效地函式呼叫,在使用動態連結庫中的匯出函式的時候,需要明確地將這些函式匯入。如3.5.2.1節描述的那樣,首先將匯入、匯出的資訊定義到一個標頭檔案中,在實現動態連結庫的時候,將這個包含定義資訊的標頭檔案引入。在實現動態連結庫,在執行函式匯出的時候,需要特別定一個巨集標記;在使用被髮布的動態連結庫的時候,需要明確地執行函式的匯入,這時候,只需要引入隨該動態連結庫一起釋出的標頭檔案即可,不需要做任何巨集定義。
3.5.3.2在靜態連結階段對動態連結庫的匯入庫的處理
在靜態連結階段,連結器引入動態連結庫的匯入庫,並執行連結的過程如下圖所示:
從上圖可以看出,在執行靜態連結的時候,輸入的資料為多個目標檔案和多個匯入庫(如果該模組呼叫了多個動態連結庫中的函式),連結器經過處理以後,會輸出PE格式的檔案(可以是可執行檔案或者動態連結庫),同時還可能會輸出一些其他用途的檔案。
連結器在執行靜態連結的時候,經歷了兩次掃描的過程。除了要處理目標檔案之間的連結問題外,如果目標檔案引用了其他動態連結庫中的函式,那麼連結器還需要進行額外的處理工作,這些工作主要是:匯入表的建立,程式碼的加入,以及外部符號解析。這些被連結器額外加入到PE檔案中的程式碼來自於匯入庫中,主要是樁程式碼,以及動態連結庫啟動的程式碼。
匯入表的建立過程如下圖所示:
PE檔案的匯入表是一個陣列,陣列元素的型別是IMAGE_IMPORT_DESCRIPTOR型別的資料結構。該資料結構有兩個指標,分別指向匯入地址表(IAT)和匯入名稱表(INT)這兩個陣列。在連結階段,這兩個陣列中儲存的都是符號的名稱資訊。
對於匯入表陣列中,每一個陣列元素都會對應一個動態連結庫的資訊。具體的情況是,在掃描的時候,用匯入庫中的資訊去填寫IMAGE_IMPORT_DESCRIPTOR結構體中的欄位。如:動態連結庫的名稱,建立時間等。
在匯入庫中,包含一些命名為.idata$4,.idata$5形式的節,這些節中包含的資訊就是動態連結庫中被匯出的函式的資訊,如名稱,地址等。在生成IAT,以及INT陣列的時候,這些節中的資料要被合併到IAT或者INT陣列中來。
在當前模組中被引用,而定義在動態連結庫中的符號,相對與當前模組來說,它是外部符號。外部符號的解析過程如下圖所示:
在上圖中,通過重定位表和全域性符號表可以查詢到外部符號,以及外部符號出現在程式碼中的位置。存在於這些位置上的指令的格式為:
Call dword ptr[xxxx] //xxxx是記憶體中的某個地址的值。 dword ptr[xxxx]表示取記憶體中某個地址開始處的內容,大小4位元組。該地址由“xxxx”這個運算元指定。 |
在編譯階段,由於是外部符號,在目標檔案中,佔位於“xxxx”的位置上的運算元的值未知;在靜態連結階段,需要修正xxxx的值,使之與動態連結庫中被呼叫的函式建立關係即:使Call指令直接地或者間接地指向IAT陣列中的某各位置。在程式載入到記憶體的過程中,載入器會將IAT陣列中的函式名稱更改成動態連結庫中相關函式的地址,這時候才真正完成了動態連結。
如果函式的名稱是“函式名+修飾”的形式(該函式沒有使用_declspec(dllimport)匯入),那麼在靜態連結的時候,xxxx被解析成樁程式碼中某個jump指令的地址。如:Fun1的地址,或者Fun2的地址等;如果函式的名稱是“_imp_函式名+修飾”的形式(該函式使用了_declspec(dllimport)匯入),那麼在靜態連結的時候,xxxx被解析成IAT陣列中某個陣列元素的地址。在當前階段,該陣列元素中儲存的是對應函式的函式名稱。
經過這些處理,Call指令後面的地址被修正正確,它直接地或者間接地指向了IAT陣列中某各陣列元素的位置,該陣列元素儲存的是動態連結庫中被呼叫函式的資訊。在靜態連結完成以後,這裡儲存的還是函式的名稱,因此在執行程式的時候,是無法正確跳轉到動態連結庫中被呼叫函式的位置。這個問題將在程式載入時,由動態連結來解決。具體情況,參見第四章。
3.6目標檔案與靜態連結庫之間的靜態連結
目標檔案與靜態連結庫之間的靜態連結過程與目標檔案之間的靜態連結過程類似。靜態連結庫也是由一系列的目標檔案組成的,在執行靜態連結的時候,靜態連結庫中的相關目標檔案會被拷貝到輸出的PE檔案中。
3.7增量連結
3.7.1程式碼除錯的流程
一般情況下,程式設計師在除錯C++程式的時候,其操作流程如下圖所示:
由上圖可以看出,程式碼除錯的過程是一個不斷迴圈,迭代的過程。試想一下,如果當前被除錯的C++程式是一個很龐大的C++程式,它包含相當多的原始檔,比如:上千個原始檔。那麼執行第二步和第三步的時候,將會耗費相當多的時間,而程式設計師所關心的是第六步和第八步。為了解決這個問題,在Visual Studio中引入了“Edit and Continue”功能。
在使用“Edit and Continue”功能的時候,程式設計師的除錯流程有所更改,具體的流程過程如下圖所示:
在該流程中,初次啟動除錯的時候,其操作過程與非“Edit and Continue”模式下是一致的,都需要設定斷點,啟動除錯(如果需要編譯,則執行之),以及程式的載入和執行。當程式執行的設定的斷點後,程式設計師開始單步執行程式程式碼,並跟蹤各個變數的狀態直到發現程式碼錯誤。
在非“Edit and Continue”模式下,程式設計師需要關閉除錯,然後修改程式碼錯誤,重新編譯原始碼,然後再次啟動除錯。在程式碼量小,程式載入和執行所需要的時間少的時候,是可以這麼做的;
在“Edit and Continue”模式下,當程式設計師發現程式碼錯誤的時候,不需要關閉當前除錯。在開啟除錯的情況下,程式設計師直接更改錯誤程式碼,更改完畢以後,設定新的斷點,然後按F10鍵繼續執行程式。在上圖中,藍色部分描述了“Edit and Continue”模式下程式設計師的除錯流程。在程式設計師修改錯誤程式碼的時候,該錯誤程式碼必須位於當前執行點之下。具體情況如下圖所示:
在上圖中,當前執行點是程式碼“nOpertimes++”所在的位置,新修改的程式碼必須位於該行程式碼之下。
為了使用“Edit and Continue”功能,必須在專案的屬性視窗中進行設定,具體情況如下圖所示:
在“C/C++”分組的“General”標籤中,需要將“Debug Information Format”選項的值設定為:“Program Database for Edit & Continue(/ZI)”。
當程式設計師按下F10鍵以後,為了支援“Edit and Continue”功能,編譯器開始執行增量連結。為了使用增量連結功能,必須在專案的屬性視窗中進行設定,具體情況如下圖所示:
在“Linker”分組的“General”標籤中,需要將“Enable Incremental Linking”的值設定為:Yes(/INCREMENTAL)。設定該值後,在執行編譯的時候,編譯器將開啟增量連結功能。
3.7.2增量連結的原理
3.7.2.1場景描述
在C++程式碼中,我們實現了兩個函式:Fun1和Fun2。在將C++原始檔編譯成PE檔案以後,我們假設這兩個函式被緊挨著放到了一起。Fun1被放到了地址:0x40002000處,Fun1的大小為0x100。那麼Fun2的地址就應該是:0x40002000 + 0x100 = 0x40002100。
在程式設計師除錯C++程式碼的時候,需要不斷地修改程式碼,並執行編譯,連結。當程式設計師將Fun1函式的內容修改以後,函式的大小發生了變化,比如變化為0x200。在這種情況下,由於兩個函式被緊緊地放在了一起,那麼Fun2的地址就要被被向後推移0x100,變成了0x400022000。在這種情況下,一般的解決思路是:重新洗牌,將現有的編譯好的exe刪除,然後重新編譯原始檔,執行連結。在這個過程中,需要重新佈局所有的函式,重新生成全域性符號表,重新執行符號解析和重定位工作…。對於大型的軟體專案來說,這個過程是漫長而痛苦的。在極端情況下,程式設計師因為修改了一小段程式碼,就必須要等待一個漫長的編譯、連結過程。
為了解決這個問題,visual Stuio在debug模式下,使用了增量連結的功能。在使用了該功能以後,我們獲得了更快的編譯速度,由此帶來的副作用是:被編譯出來的PE檔案更加龐大,該PE檔案的執行效率更加低下。由於debug模式下編譯出來的程式是供程式設計師除錯使用的,而不是提供給終端使用者的,所以我們可以忽略這些副作用。
在Release模式下,由於沒有使用增量連結,所以在被編譯出來的PE中,其內容更加緊湊,其執行效率更加高效。
3.7.2.2 Padding 以及.textbss段
當一個函式被修改以後,由於其大小發生了變化,所以可能會引起其他函式入口地址的更改。在增量連結模式下,為了避免由於一個函式大小的變化而影響到其他函式的入口地址的問題。採用了兩種解決方式:在函式間以及段間填充二進位制資料“0xcc”,以及使用.textbss段。
在函式間以及段間填充二進位制資料“0xcc”方式
該方式主要是為了加快編譯速度。在該方式下,連結器不會將各個函式緊湊地存放在一起,而是在各個函式之間填充一定數量的二進位制資料“0xcc”。在各個段之間,比如:.text段和.data段之間,填充大量的二進位制資料“0xcc”。該二進位制資料代表彙編指令:INT 3。該windows下,執行該指令會導致異常,從而中斷程式的執行。這是出於安全方面的考慮,由於一些原因,使程式執行到了填充空間中,由於INT 3指令的存在,程式被終止執行。
在修改函式的時候,當函式的大小發生較小的變化的時候,將會在函式間被填充的空間之中,為更改後的函式分配新空間。因此,各個函式的入口地址都不會發生變化,只是被填充的空間的大小發生了變化。在編譯的時候,只需要編譯發生變化的原始檔,由於函式地址均未發生變化,所以也不需要重新執行地址解析和重定位工作。因此加快了編譯速度。
在修改函式的時候,當函式的大小發生較大變化的時候,即:函式間被填充的空間不足以支援被更改函式的大小。在這種情況下,使用段間填充的空間為新函式分配空間。這時候,新函式的入口地址放生變化。將會使用增量連結表處理函式入口地址發生變化的問題。
使用dumpbin工具解析debug模式下編譯出來的PE檔案,其部分內容如下:
10011490: CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC ìììììììììììììììì 100114A0: CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC ìììììììììììììììì 100114B0: CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC ìììììììììììììììì 100114C0: CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC ìììììììììììììììì 100114D0: CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC ìììììììììììììììì 100114E0: 55 8B EC 81 EC CC 00 00 00 53 56 57 51 8D BD 34 U.ì.ìì...SVWQ.?4 100114F0: FF FF FF B9 33 00 00 00 B8 CC CC CC CC F3 AB 59 ???13...?ììììó?Y 10011500: 89 4D F8 8B 45 08 8B 08 8B 55 F8 89 0A 8B 45 F8 .M?.E....U?...E? 10011510: 5F 5E 5B 8B E5 5D C2 04 00 CC CC CC CC CC CC CC _^[.?]?..ììììììì 10011520: CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC ìììììììììììììììì 10011530: 55 8B EC 81 EC C0 00 00 00 53 56 57 8D BD 40 FF U.ì.ìà...SVW.?@? 10011540: FF FF B9 30 00 00 00 B8 CC CC CC CC F3 AB A1 40 ??10...?ììììó??@ 10011550: 91 01 10 5F 5E 5B 8B E5 5D C3 CC CC CC CC CC CC ..._^[.?]?ìììììì 10011560: CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC ìììììììììììììììì 10011570: 55 8B EC 6A FF 68 AE 56 01 10 64 A1 00 00 00 00 U.ìj?h?V..d?.... 10011580: 50 81 EC E8 00 00 00 53 56 57 51 8D BD 0C FF FF P.ìè...SVWQ.?.?? 10011590: FF B9 3A 00 00 00 B8 CC CC CC CC F3 AB 59 A1 04 ?1:...?ììììó?Y?. 100115A0: 90 01 10 33 C5 50 8D 45 F4 64 A3 00 00 00 00 89 ...3?P.E?d£..... 100115B0: 4D EC 6A 01 E8 4A FC FF FF 83 C4 04 89 85 20 FF Mìj.èJü??.?... ? 100115C0: FF FF C7 45 FC 00 00 00 00 83 BD 20 FF FF FF 00 ???Eü.....? ???. 100115D0: 74 13 8B 8D 20 FF FF FF E8 C7 FB FF FF 89 85 0C t... ???è????... 100115E0: FF FF FF EB 0A C7 85 0C FF FF FF 00 00 00 00 8B ????.?..???..... 100115F0: 85 0C FF FF FF 89 85 14 FF FF FF C7 45 FC FF FF ..???...????Eü?? 10011600: FF FF 8B 4D EC 8B 95 14 FF FF FF 89 11 8B 45 EC ??.Mì...???...Eì 10011610: 8B 4D F4 64 89 0D 00 00 00 00 59 5F 5E 5B 81 C4 .M?d......Y_^[.? 10011620: F4 00 00 00 3B EC E8 92 FB FF FF 8B E5 5D C3 CC ?...;ìè.???.?]?ì |
在上面的程式碼示例中,紅色的部分表示的是被填充的二進位制資料。
使用.textbss段方式
該方式是為了實現“Edit and Continue”功能。.textbss段只存在於debug模式下生成的PE檔案中,用於存放程式程式碼的二進位制資料。在PE檔案中,該段只是一個邏輯概念,它不佔用檔案中的儲存空間;在程式載入執行的時候,在程式的虛擬地址空間中,需要為該段分配地址空間。
在除錯階段,“Edit and Continue”模式下,當程式設計師修改了某個函式並按F10鍵繼續執行該程式以後,編譯器開始執行增量連結。在該情況下,連結器會將更改後的函式的二進位制程式碼存放到記憶體中為.textbss段分配的地址空間中。因此該函式的入口地址放生了變化。將會使用增量連結表處理函式入口地址發生變化的問題。這些過程都是在程式執行階段發生的。
3.7.2.3增量連結表(ITL)
無論是使用段間填充的方式,還是使用.textbss段的方式,函式的入口地址都發生了變化。因此,在執行連結的時候,對於每一處呼叫該函式的地方,都需要修正該函式的地址值。這必然會影響程式的編譯速度,為了解決這個問題,引入了增量連結表的概念。
增量連結表存在於debug模式下生成的PE檔案中,位於.text段。該表中儲存一系列jump指令。每個指令後面都會跟隨一個函式的相對地址。使用dumpbin工具解析debug模式下生成的PE檔案以後,增量連結表的部分內容如下所示:
10011000: CC CC CC CC CC E9 76 08 00 00 E9 9B 36 00 00 E9 ......v.....6... @ILT+11(_DebugBreak@0): 10011010: EA 36 00 00 E9 A7 1E 00 00 E9 08 2C 00 00 E9 BD .6.........,.... @ILT+27(??4DemoMath@@QAEAAV0@ABV0@@Z): 10011020: 04 00 00 E9 68 15 00 00 E9 C5 36 00 00 E9 3E 13 ....h.....6...>. @ILT+43(??Bsentry@?$basic_ostream@DU?$char_traits@D@std@@@std@@QBE_NXZ): 10011030: 00 00 E9 B9 2B 00 00 E9 C8 36 00 00 E9 BF 2F 00 ....+....6..../. @ILT+59(_DllMain@12): 10011040: 00 E9 1A 16 00 00 E9 35 1F 00 00 E9 60 1E 00 00 .......5....`... |
在上面的程式碼中,如:“E9 B9 2B 00 00”,表示執行近跳轉,要跳轉的地址是:00002bb9,這是一個相對地址;E9是彙編指令jump的機器碼。
在未引入增量連結表之前,在Call指令中是直接呼叫函式的。函式呼叫的指令格式描述如下:
Call foo。//Foo為被呼叫函式的相對地址。 |
在引入了增量連結表之後,通過增量連結表間接呼叫函式。函式呼叫的指令格式描述如下:
Call foo_stub //foo_stub為增量連結表中某一個表項的相對地址 Foo_stub: Jump foo //foo為被呼叫函式的相對地址。 |
由此可以看出,在引入了增量連結表以後,Call指令會呼叫增量連結表中的某一個表項,在增量連結表的表項中,再通過jump指令跳轉到該函式的真正位置。在引入了增量連結表以後,當函式的入口地址放生變化時,只需要修改jump指令後面的地址資料,而不是修改每一個函式呼叫處的地址資料。通過這種方式,將需要修改n處程式碼位置的情況,簡化為只需要修改一處程式碼位置。
3.7.2.4增量連結的流程
在“Edit and Continue”模式下,執行增量連結的流程如下圖所示:
首先需要編譯被更改過的原始檔,然後將被更改後的函式的二進位制程式碼儲存到.textbss段所在的虛擬記憶體的地址空間中,最後還需要修改增量連結表中的某個表項的資料。
在“Edit and Continue”模式下,由於被除錯程式還處於執行狀態。因此,當修改完畢增量連結表的表項以後,還需要檢查所有執行緒的TIB,如果該執行緒的EIP還指向老的函式的地址(函式被修改前該函式的地址),就需要將該地址修正為新函式的地址。
以上所有的操作均是對被除錯程式記憶體的操作,包括對記憶體資料的讀取和修改。而完成這項工作的,是Visual Studio偵錯程式。
3.8目標檔案之間的靜態連結示例分析
3.8.1場景描述
在1.2.1C++原始碼示例中,類DemoMath中的成員函式AddData呼叫了全域性變數nOpertimes,以及類DemoOutPut中的成員函式OutPutInfo。具體程式碼格式如下:
void DemoMath::AddData(double a, double b) { nOperTimes++; m_pOutPut->OutPutInfo(a + b); } |
在執行編譯,連結的時候,在DemoMath.cpp生成的目標檔案DemoMath.obj中,全域性變數nOpertimes的地址需要執行重定位操作;相對於目標檔案DemoMath.obj,目標檔案DemoOutPut.obj中定義的符號OutPutInfo是外部符號,它也需要被解析和重定位。
在地址重定位的時候,變數的地址是絕對型別,函式的地址是相對型別。
3.8.2輸出的檔案
3.8.2.1目標檔案的輸出
在編譯階段,編譯器將C++原始檔編譯成目標檔案。使用工具dumpbin將目標檔案DemoMath.obj中關於函式AddData所在的程式碼段的內容匯出,該內容包括:摘要資訊,二進位制程式碼,以及符號表,具體內容如下:
//這是程式碼段關於AddData函式部分的摘要資訊 SECTION HEADER #14 //段名稱 .text name 0 physical address //實體地址,尚未分配,應該為零 0 virtual address //虛擬地址,尚未分配,應該為零 5C size of raw data //該段二進位制資料的大小 1EB2 file pointer to raw data (00001EB2 to 00001F0D) //該段距離檔案首位置的偏移 1F0E file pointer to relocation table //重定位表距離檔案首位置的偏移 0 file pointer to line numbers //行號表的位置,為零表示沒有行號表 4 number of relocations //重定位表中元素的個數 0 number of line numbers 60501020 flags //標記 Code //表示該段為程式碼 COMDAT; sym= "public: void __thiscall DemoMath::AddData(double,double)" (?AddData@DemoMath@@QAEXNN@Z) 16 byte align //16位元組對齊 Execute Read //可執行,可讀 //以下內容為程式碼段關於AddData函式的二進位制內容 RAW DATA #14 相對於程式碼段偏移量 二進位制內容 00000000: 55 8B EC 81 EC CC 00 00 00 53 56 57 51 8D BD 34 U.ì.ìì...SVWQ.?4 00000010: FF FF FF B9 33 00 00 00 B8 CC CC CC CC F3 AB 59 ???13...?ììììó?Y 00000020: 89 4D F8 A1 00 00 00 00 83 C0 01 A3 00 00 00 00 .M??.....à.£.... 00000030: DD 45 08 DC 45 10 83 EC 08 DD 1C 24 8B 45 F8 8B YE.üE..ì.Y.$.E?. 00000040: 08 E8 00 00 00 00 5F 5E 5B 81 C4 CC 00 00 00 3B .è...._^[.?ì...; 00000050: EC E8 00 00 00 00 8B E5 5D C2 10 00 ìè.....?]?..
//以下為重定位表的內容。從左到右,各欄位的含義是: //offset 需要重定位的位置。這些位置位於“RAW DATA #14”所描述的二進位制程式碼中,紅色顯示部分。 //Type 重定位的型別。Dir32表示絕對定位;Rel32表示相對定位 //Applied To 未知 //Symbol Index 需要重定位的符號在符號表中的索引。通過此欄位,將符號表和重定位表關聯 //Symbol Name 符號名稱。 RELOCATIONS #14 Symbol Symbol Offset Type Applied To Index Name -------- ---------------- ----------------- -------- ------ 00000024 DIR32 00000000 F ?nOperTimes@@3HA (int nOperTimes) 0000002C DIR32 00000000 F ?nOperTimes@@3HA (int nOperTimes) 00000042 REL32 00000000 59 ?OutPutInfo@DemoOutPut@@QAEXN@Z (public: void __thiscall DemoOutPut::OutPutInfo(double)) 00000052 REL32 00000000 3F __RTC_CheckEsp |
(表一)
在上面被匯出的內容中,二進位制程式碼不容易閱讀,所以將其內容轉換為彙編格式,具體內容如下:
?AddData@DemoMath@@QAEXNN@Z (public: void __thiscall DemoMath::AddData(double,double)): 00000000: 55 push ebp 00000001: 8B EC mov ebp,esp 00000003: 81 EC CC 00 00 00 sub esp,0CCh 00000009: 53 push ebx 0000000A: 56 push esi 0000000B: 57 push edi 0000000C: 51 push ecx 0000000D: 8D BD 34 FF FF FF lea edi,[ebp-0CCh] 00000013: B9 33 00 00 00 mov ecx,33h 00000018: B8 CC CC CC CC mov eax,0CCCCCCCCh 0000001D: F3 AB rep stos dword ptr es:[edi] 0000001F: 59 pop ecx 00000020: 89 4D F8 mov dword ptr [ebp-8],ecx //此處引用了全域性變數nOperTimes。該符號尚未解析,地址暫時用零填充 00000023: A1 00 00 00 00 mov eax,dword ptr [?nOperTimes@@3HA] 00000028: 83 C0 01 add eax,1 0000002B: A3 00 00 00 00 mov dword ptr [?nOperTimes@@3HA],eax 00000030: DD 45 08 fld qword ptr [ebp+8] 00000033: DC 45 10 fadd qword ptr [ebp+10h] 00000036: 83 EC 08 sub esp,8 00000039: DD 1C 24 fstp qword ptr [esp] 0000003C: 8B 45 F8 mov eax,dword ptr [ebp-8] 0000003F: 8B 08 mov ecx,dword ptr [eax] //該地址為函式OutPutInfo的地址,該地址尚未解析,暫時用零填充 00000041: E8 00 00 00 00 call ?OutPutInfo@DemoOutPut@@QAEXN@Z 00000046: 5F pop edi 00000047: 5E pop esi 00000048: 5B pop ebx 00000049: 81 C4 CC 00 00 00 add esp,0CCh 0000004F: 3B EC cmp ebp,esp 00000051: E8 00 00 00 00 call __RTC_CheckEsp 00000056: 8B E5 mov esp,ebp 00000058: 5D pop ebp 00000059: C2 10 00 ret 10h |
(表二)
在上面的程式碼中,紅色部分為符號nOperTimes的虛擬記憶體地址,由於該符號尚未解析,所以該地址未知,暫時用零代替。藍色部分為符號OutPutInfo的虛擬記憶體地址,由於該符號尚未解析,所以該地址未知,暫時用零代替。需要重定位的位置與重定位表的描述吻合。
目標檔案DemoMath.obj的符號表的部分內容如下:
//符號nOpertimes在符號表中的內容 //00F表示符號在符號表中的索引。 //SECT4表示該符號位於第四個段中,也就是說,該符號位於當前目標檔案中 //notype表示該符號為變數 //External表示該符號為全域性符號,未見外部可見。Static表示該符號只檔案內可見 //?nOperTimes@@3HA (int nOperTimes)是符號名稱 00F 00000000 SECT4 notype External | ?nOperTimes@@3HA (int nOperTimes)
//符號OutPutInfo在目標檔案DemoMath.obj所屬符號表的內容 //UNDEF表示該符號未定義,該符號的定義位於其他目標檔案中。該符號需要解析 034 00000000 UNDEF notype () External | ??0DemoOutPut@@QAE@XZ
|
(表三)
在目標檔案中,彙編內容顯示了編譯以後,連結之前各個需要重定位符號的資訊情況;結合重定位表中的資訊,可以很容易地找到需要重定位的位置;重定位表與通過索引欄位與符號表關聯,符號表中各個符號的值,就是符號的地址。在該階段,這些地址尚未使用虛擬地址表示。在連結階段,將會根據各個目標檔案中的符號表生成全域性符號表。在全域性符號表中,各個符號的值使用虛擬地址表示。
3.5.2.2增量連結模式下PE檔案的輸出
開啟增量連結模式,在連結階段,將目標檔案連結在一起,輸出PE檔案。使用工具dumpbin將DemoDlld.dll的內容解析為彙編格式,其中函式AddData的彙編程式碼的內容如下:
?AddData@DemoMath@@QAEXNN@Z: 10011780: 55 push ebp 10011781: 8B EC mov ebp,esp 10011783: 81 EC CC 00 00 00 sub esp,0CCh 10011789: 53 push ebx 1001178A: 56 push esi 1001178B: 57 push edi 1001178C: 51 push ecx 1001178D: 8D BD 34 FF FF FF lea edi,[ebp-0CCh] 10011793: B9 33 00 00 00 mov ecx,33h 10011798: B8 CC CC CC CC mov eax,0CCCCCCCCh 1001179D: F3 AB rep stos dword ptr es:[edi] 1001179F: 59 pop ecx 100117A0: 89 4D F8 mov dword ptr [ebp-8],ecx //符號nOperTimes已經被解析,地址為0x10019140。該地址為絕對地址 100117A3: A1 40 91 01 10 mov eax,dword ptr [?nOperTimes@@3HA] 100117A8: 83 C0 01 add eax,1 100117AB: A3 40 91 01 10 mov dword ptr [?nOperTimes@@3HA],eax 100117B0: DD 45 08 fld qword ptr [ebp+8] 100117B3: DC 45 10 fadd qword ptr [ebp+10h] 100117B6: 83 EC 08 sub esp,8 100117B9: DD 1C 24 fstp qword ptr [esp] 100117BC: 8B 45 F8 mov eax,dword ptr [ebp-8] 100117BF: 8B 08 mov ecx,dword ptr [eax] //符號OutPutInfo已經被解析,地址為0xFFFFF9CA。該地址為相對地址 100117C1: E8 CA F9 FF FF call @ILT+395(?OutPutInfo@DemoOutPut@@QAEXN@Z) 100117C6: 5F pop edi 100117C7: 5E pop esi 100117C8: 5B pop ebx 100117C9: 81 C4 CC 00 00 00 add esp,0CCh 100117CF: 3B EC cmp ebp,esp 100117D1: E8 E7 F9 FF FF call @ILT+443(__RTC_CheckEsp) 100117D6: 8B E5 mov esp,ebp 100117D8: 5D pop ebp 100117D9: C2 10 00 ret 10h |
(表四)
在增量連結模式下,編譯器會生成增量連結表,其部分內容如下所示:
@ILT+395(?OutPutInfo@DemoOutPut@@QAEXN@Z): 10011190: E9 0B 09 00 00 E9 40 35 00 00 E9 FF 34 00 00 E9 //紅色部分的彙編程式碼為:jump 0B 09 00 00 @ILT+411(_QueryPerformanceCounter@4): 100111A0: 7E 35 00 00 E9 77 08 00 00 E9 84 34 00 00 E9 81 |
(表五)
在增量連結模式下,全域性符號表的相關內容為:
符號型別 |
符號地址 |
長度 |
符號名稱 |
Function |
11AA0 |
126 |
DemoOutPut::OutPutInfo |
Data |
19140 |
4 |
nOperTimes |
(表六)
在符號表中,符號地址為相對於預設載入位置的相對地址。真正的符號地址應該是0x10000000 + 符號地址。
3.5.2.3在非增量連結模式下PE檔案的輸出
關閉增量連結模式,重新編譯檔案,將輸出的PE檔案內容匯出。函式AddData的彙編內容描述如下:
?AddData@DemoMath@@QAEXNN@Z: 10001220: 55 push ebp 10001221: 8B EC mov ebp,esp 10001223: 81 EC CC 00 00 00 sub esp,0CCh 10001229: 53 push ebx 1000122A: 56 push esi 1000122B: 57 push edi 1000122C: 51 push ecx 1000122D: 8D BD 34 FF FF FF lea edi,[ebp-0CCh] 10001233: B9 33 00 00 00 mov ecx,33h 10001238: B8 CC CC CC CC mov eax,0CCCCCCCCh 1000123D: F3 AB rep stos dword ptr es:[edi] 1000123F: 59 pop ecx 10001240: 89 4D F8 mov dword ptr [ebp-8],ecx //變數nOpertimes的地址已經被解析。該地址為絕對地址0x10006030 10001243: A1 30 60 00 10 mov eax,dword ptr [?nOperTimes@@3HA] 10001248: 83 C0 01 add eax,1 1000124B: A3 30 60 00 10 mov dword ptr [?nOperTimes@@3HA],eax 10001250: DD 45 08 fld qword ptr [ebp+8] 10001253: DC 45 10 fadd qword ptr [ebp+10h] 10001256: 83 EC 08 sub esp,8 10001259: DD 1C 24 fstp qword ptr [esp] 1000125C: 8B 45 F8 mov eax,dword ptr [ebp-8] 1000125F: 8B 08 mov ecx,dword ptr [eax] //函式OutPut的地址已經被解析。該地址為相對地址00 00 02 2A 10001261: E8 2A 02 00 00 call ?OutPutInfo@DemoOutPut@@QAEXN@Z 10001266: 5F pop edi 10001267: 5E pop esi 10001268: 5B pop ebx 10001269: 81 C4 CC 00 00 00 add esp,0CCh 1000126F: 3B EC cmp ebp,esp 10001271: E8 0A 0B 00 00 call __RTC_CheckEsp 10001276: 8B E5 mov esp,ebp 10001278: 5D pop ebp 10001279: C2 10 00 ret 10h 1000127C: CC CC CC CC |
(表七)
在非增量連結模式下,全域性符號表的部分內容為:
符號型別 |
符號地址 |
長度 |
符號名稱 |
Function |
1490 |
126 |
DemoOutPut::OutPutInfo |
Data |
6030 |
4 |
nOperTimes |
(表八)
在符號表中,符號地址為相對於預設載入位置的相對地址。真正的符號地址應該是0x10000000 + 符號地址。
3.8.3地址解析和重定位
3.8.3.1變數地址的解析和重定位
在表一中,根據重定位表的資訊可以得知,位於00000024位置的符號nOperTimes需要被重定位。在目標檔案中,它的內容為:00000020: 89 4D F8 A1 00 00 00 00。
執行靜態連結以後,輸出PE格式的Dll檔案,其內容被匯出到表四中。符號nOperTimes已經完成了重定位,其內容為:100117A3: A1 40 91 01 10。符號nOperTimes的地址被解析成:0x10019140。
在全域性符號表(表五)中,符號nOperTimes的地址是:19140。該地址為基於預設載入位置的相對地址,加上0x10000000即為符號的虛擬地址0x10019140。
在變數型別的符號的解析和重定位的時候,用全域性符號表中的符號值(即變數地址:0x10019140)去重寫目標檔案中需要重定位的位置(00000020: 89 4D F8 A1 00 00 00 00),得到PE檔案中重定位後的結果(100117A3: A1 40 91 01 10)。
變數的地址型別為絕對地址,在重定位的時候,使用全域性符號表中符號的虛擬地址直接重寫需要重定位的位置即可。
3.8.3.2增量連結模式下函式地址的解析和重定位
在增量連結模式下,函式呼叫的過程如下圖所示:
在增量連結的模式下,經過兩級呼叫才能完成對被呼叫函式的呼叫。首先在函式呼叫點,以call指令方式執行一次函式呼叫。經過對Call指令的呼叫,控制流程轉到了增量連結表中jump指令的位置。然後開始執行jump指令,經過對jump指令的呼叫,控制流程才真正到達被呼叫函式處。
在表四中,符號(函式)OutPutInfo已經被解析和重定位,該函式呼叫點處的機器碼為:100117C1: E8 CA F9 FF FF。E8是Call指令的二進位制機器碼,其後是四個位元組的運算元,表示增量連結表中jump指令的相對位置。Call指令後的運算元的計算公式為:運算元 = jump指令地址 – IP暫存器內容。當前IP暫存器內容為:Call指令的地址 + 5,即:100117C1 + 5 = 100117C6。經表五的內容(10011190: E9 0B 09 00 00)得知,jump指令的地址為:0x10011190。那麼運算元 = 0x10011190 - 100117C6 = FF FF F9 CA。與預期結果符合。
在表五中,jump指令的內容為:10011190: E9 0B 09 00 00。經過jump指令,呼叫流程才真正轉到被呼叫函式處。Jump指令的運算元的計算公式為:運算元 = 被呼叫函式的地址 – IP暫存器的內容。當前IP暫存器的內容為:jump指令的地址 + 5,即:10011190 + 5 = 10011195。通過表六得知,被呼叫函式的地址為:0x10011AA0。因此,jump指令的運算元 = 10011AA0 – 10011195 = 00 00 090B。該結果與預期符合。
3.8.3.3非增量連結模式下函式地址的解析和重定位
在非增量連結模式下,經過一次Call指令的呼叫,即可完成從主調函式到被調函式控制流程的轉換。通過表七得知,函式呼叫點處的機器指令碼為:10001261: E8 2A 02 00 00。E8為Call指令的二進位制碼,其後為四個位元組的運算元,該運算元是被呼叫函式OutPutInfo的相對地址。
Call指令運算元的計算公式為:運算元 = 符號的地址 – IP暫存器的內容。當前IP暫存器的內容為:Call指令的地址 + 5,即:0x10001261 + 0x5 = 0x10001266。通過表八得知,函式OutPutInfo的地址為:0x10001490。因此,運算元 = 0x10001490 – 0x10001266 = 00 00 02 2A。該值預期結果符合。
相關文章
- C++應用程式在Windows下的編譯、連結:第三部分 靜態連結(一)C++Windows編譯
- C++應用程式在Windows下的編譯、連結(四)動態連結C++Windows編譯
- C++應用程式在Windows下的編譯、連結:第二部分COFF/PE檔案結構C++Windows編譯
- C++應用程式在Windows下的編譯、連結:第一部分 概述C++Windows編譯
- 在AndroidStudio下使用cmake編譯出靜態連結庫的方法Android編譯
- Linux下的靜態連結與動態連結Linux
- 【連結 1】與靜態連結庫連結
- Linux環境下:程式的連結, 裝載和庫[靜態連結]Linux
- 《程式設計師的自我修養》(一)——編譯與靜態連結程式設計師編譯
- 動態連結庫與靜態連結庫
- 靜態連結動態連結的連結順序問題和makefile示例
- C++編譯連結的那些小事 .C++編譯
- 動態連結庫和靜態連結庫的區別
- 關於go程式的靜態連結編譯是否可以不依賴系統C庫Go編譯
- linux下靜態連結庫和動態連結庫的區別有哪些Linux
- NDK 連結第三方靜態庫的方法
- 程式的編譯和連結原理分析編譯
- 關於程式的編譯和連結編譯
- Linux系統 g++ 連結 libopencv_world.a 靜態庫編譯程式LinuxOpenCV編譯
- Ubuntu中編譯連結Opencv應用的簡便方式Ubuntu編譯OpenCV
- Windows下的VC++動態連結庫程式設計WindowsC++程式設計
- Win32動態連結庫與靜態連結庫的區別Win32
- 編譯連結過程編譯
- C語言編寫靜態連結庫及其使用C語言
- 編譯、連結學習筆記(一)簡述編譯連結過程編譯筆記
- 從編譯連結到cmake編譯
- C#資料結構-靜態連結串列C#資料結構
- 程式的連結和裝入及Linux下動態連結的實現Linux
- C/C++預處理、編譯、連結過程【Z】C++編譯
- 程式設計師的自我修養-編譯連結程式設計師編譯
- (轉)編譯和連結的區別編譯
- iOS 應用下載連結獲取iOS
- cmake 連結動態連結庫
- GCC編譯和連結過程GC編譯
- C語言中編譯和連結C語言編譯
- 實戰資料結構(6)_靜態連結串列的使用資料結構
- 編譯、彙編、連結、載入、顯示編譯
- C++的動態繫結和靜態繫結C++