glibc与MSVC CRT(转载)
glibc與MSVC CRT
運行庫是平臺相關的,因為它與操作系統結合得非常緊密。C語言的運行庫從某種程度上來講是C語言的程序和不同操作系統平臺之間的抽象層,它將不同的操作系統API抽象成相同的庫函數。比如我們可以在不同的操作系統平臺下使用fread來讀取文件,而事實上fread在不同的操作系統平臺下的實現是不同的,但作為運行庫的使用者我們不需要關心這一點。雖然各個平臺下的C語言運行庫提供了很多功能,但很多時候它們畢竟有限,比如用戶的權限控制、操作系統線程創建等都不是屬于標準的C語言運行庫。于是我們不得不通過其他的辦法,諸如繞過C語言運行庫直接調用操作系統API或使用其他的庫。Linux和Windows平臺下的兩個主要C語言運行庫分別為glibc(GNU C Library)和MSVCRT(Microsoft Visual C Run-time),我們在下面將會分別介紹它們。
值得注意的是,像線程操作這樣的功能并不是標準的C語言運行庫的一部分,但是glibc和MSVCRT都包含了線程操作的庫函數。比如glibc有一個可選的pthread庫中的pthread_create()函數可以用來創建線程;而MSVCRT中可以使用_beginthread()函數來創建線程。所以glibc和MSVCRT事實上是標準C語言運行庫的超集,它們各自對C標準庫進行了一些擴展。
glibc
glibc即GNU C Library,是GNU旗下的C標準庫。最初由自由軟件基金會FSF(Free Software Foundation)發起開發,目的是為GNU操作系統開發一個C標準庫。GNU操作系統的最初計劃的內核是Hurd,一個微內核的構架系統。Hurd因為種種原因開發進展緩慢,而Linux因為它的實用性而逐漸風靡,最后取代Hurd成了GNU操作系統的內核。于是glibc從最初開始支持Hurd到后來漸漸發展成同時支持Hurd和Linux,而且隨著Linux的越來越流行,glibc也主要關注Linux下的開發,成為了Linux平臺的C標準庫。
20世紀90年代初,在glibc成為Linux下的C運行庫之前,Linux的開發者們因為開發的需要,從Linux內核代碼里面分離出了一部分代碼,形成了早期Linux下的C運行庫。這個C運行庫又被稱為Linux libc。這個版本的C運行庫被維護了很多年,從版本2一直開發到版本5。如果你去看早期版本的Linux,會發現/lib目錄下面有libc.so.5這樣的文件,這個文件就是第五個版本的Linux libc。1996年FSF發布了glibc 2.0,這個版本的glibc開始支持諸多特性,比如它完全支持POSIX標準、國際化、IPv6、64-位數據訪問、多線程及改進了代碼的可移植性。在此時Linux libc的開發者也認識到單獨地維護一份Linux下專用的C運行庫是沒有必要的,于是Linux開始采用glibc作為默認的C運行庫,并且將2.x版本的glibc看作是Linux libc的后繼版本。于是我們可以看到,glibc在/lib目錄下的.so文件為libc.so.6,即第六個libc版本,而且在各個Linux發行版中,glibc往往被稱為libc6。glibc在Linux平臺下占據了主導地位之后,它又被移植到了其他操作系統和其他硬件平臺,諸如FreeBSD、NetBSD等,而且它支持數十種CPU及嵌入式平臺。目前最新的glibc版本號是2.8(2008年4月)。
glibc的發布版本主要由兩部分組成,一部分是頭文件,比如stdio.h、stdlib.h等,它們往往位于/usr/include;另外一部分則是庫的二進制文件部分。二進制部分主要的就是C語言標準庫,它有靜態和動態兩個版本。動態的標準庫我們及在本書的前面章節中碰到過了,它位于/lib/libc.so.6;而靜態標準庫位于/usr/lib/libc.a。事實上glibc除了C標準庫之外,還有幾個輔助程序運行的運行庫,這幾個文件可以稱得上是真正的“運行庫”。它們就是/usr/lib/crt1.o、/usr/lib/crti.o和/usr/lib/crtn.o。是不是對這幾個文件還有點印象呢?我們在第2章講到靜態庫鏈接的時候已經碰到過它們了,雖然它們都很小,但這幾個文件都是程序運行的最關鍵的文件。
glibc啟動文件
crt1.o里面包含的就是程序的入口函數_start,由它負責調用__libc_start_main初始化libc并且調用main函數進入真正的程序主體。實際上最初開始的時候它并不叫做crt1.o,而是叫做crt.o,包含了基本的啟動、退出代碼。由于當時有些鏈接器對鏈接時目標文件和庫的順序有依賴性,crt.o這個文件必須被放在鏈接器命令行中的所有輸入文件中的第一個,為了強調這一點,crt.o被更名為crt0.o,表示它是鏈接時輸入的第一個文件。
后來由于C++的出現和ELF文件的改進,出現了必須在main()函數之前執行的全局/靜態對象構造和必須在main()函數之后執行的全局/靜態對象析構。為了滿足類似的需求,運行庫在每個目標文件中引入兩個與初始化相關的段“.init”和“.finit”。運行庫會保證所有位于這兩個段中的代碼會先于/后于main()函數執行,所以用它們來實現全局構造和析構就是很自然的事情了。鏈接器在進行鏈接時,會把所有輸入目標文件中的“.init”和“.finit”按照順序收集起來,然后將它們合并成輸出文件中的“.init”和“.finit”。但是這兩個輸出的段中所包含的指令還需要一些輔助的代碼來幫助它們啟動(比如計算GOT之類的),于是引入了兩個目標文件分別用來幫助實現初始化函數的crti.o和crtn.o。
在默認情況下,ld鏈接器會將libc、crt1.o等這些CRT和啟動文件與程序的模塊鏈接起來,但是有些時候,我們可能不需要這些文件,或者希望使用自己的libc和crt1.o等啟動文件,以替代系統默認的文件,這種情況在嵌入式系統或操作系統內核編譯的時候很常見。GCC提高了兩個參數“-nostartfile”和“-nostdlib”,分別用來取消默認的啟動文件和C語言運行庫。
其實C++全局對象的構造函數和析構函數并不是直接放在.init和.finit段里面的,而是把一個執行所有構造/析構的函數的調用放在里面,由這個函數進行真正的構造和析構,我們在后面的章節還會再詳細分析ELF/Glib和PE/MSVC對全局對象構造和析構的過程。
除了全局對象構造和析構之外,.init和.finit還有其他的作用。由于它們的特殊性(在main之前/后執行),一些用戶監控程序性能、調試等工具經常利用它們進行一些初始化和反初始化的工作。當然我們也可以使用“__attribute__((section(“.init”)))”將函數放到.init段里面,但是要注意的是普通函數放在“.init”是會破壞它們的結構的,因為函數的返回指令使得_init()函數會提前返回,必須使用匯編指令,不能讓編譯器產生“ret”指令。
GCC平臺相關目標文件
就這樣,在第2章中我們在鏈接時碰到過的諸多輸入文件中,已經解決了crt1.o、crti.o和crtn.o,剩下的還有幾個crtbeginT.o、libgcc.a、libgcc_eh.a、crtend.o。嚴格來講,這幾個文件實際上不屬于glibc,它們是GCC的一部分,它們都位于GCC的安裝目錄下:
l?????????? /usr/lib/gcc/i486-Linux-gnu/4.1.3/crtbeginT.o
l?????????? /usr/lib/gcc/i486-Linux-gnu/4.1.3/libgcc.a
l?????????? /usr/lib/gcc/i486-Linux-gnu/4.1.3/libgcc_eh.a
l?????????? /usr/lib/gcc/i486-Linux-gnu/4.1.3/crtend.o
首先是crtbeginT.o及crtend.o,這兩個文件是真正用于實現C++全局構造和析構的目標文件。那么為什么已經有了crti.o和crtn.o之后,還需要這兩個文件呢?我們知道,C++這樣的語言的實現是跟編譯器密切相關的,而glibc只是一個C語言運行庫,它對C++的實現并不了解。而GCC是C++的真正實現者,它對C++的全局構造和析構了如指掌。于是它提供了兩個目標文件crtbeginT.o和crtend.o來配合glibc實現C++的全局構造和析構。事實上是crti.o和crtn.o中的“.init”和“.finit”提供一個在main()之前和之后運行代碼的機制,而真正全局構造和析構則由crtbeginT.o和crtend.o來實現。我們在后面的章節還會詳細分析它們的實現機制。
由于GCC支持諸多平臺,能夠正確處理不同平臺之間的差異性也是GCC的任務之一。比如有些32位平臺不支持64位的long long類型的運算,編譯器不能夠直接產生相應的CPU指令,而是需要一些輔助的例程來幫助實現計算。libgcc.a里面包含的就是這種類似的函數,這些函數主要包括整數運算、浮點數運算(不同的CPU對浮點數的運算方法很不相同)等,而libgcc_eh.a則包含了支持C++的異常處理(Exception Handling)的平臺相關函數。另外GCC的安裝目錄下往往還有一個動態鏈接版本的libgcc.a,為libgcc_s.so。
MSVC CRT
相比于相對自由分散的glibc,一直伴隨著不同版本的Visual C++發布的MSVC CRT(Microsoft Visual C++ C Runtime)倒看過去更加有序一些。從1992年最初的Visual C++ 1.0版開始,一直到現在的Visual C++ 9.0(又叫做Visual C++ 2008),MSVC CRT也從1.0版發展到了9.0版。
同一個版本的MSVC CRT根據不同的屬性提供了多種子版本,以供不同需求的開發者使用。按照靜態/動態鏈接,可以分為靜態版和動態版;按照單線程/多線程,可以分為單線程版和多線程版;按照調試/發布,可分為調試版和發布版;按照是否支持C++分為純C運行庫版和支持C++版;按照是否支持托管代碼分為支持本地代碼/托管代碼和純托管代碼版。這些屬性很多時候是相互正交的,也就是說它們之間可以相互組合。比如可以有靜態單線程純C純本地代碼調試版;也可以有動態的多線程純C純本地代碼發布版等。但有些組合是沒有的,比如動態鏈接版本的CRT是沒有單線程的,所有的動態鏈接CRT都是多線程安全的。
這樣的不同組合將會出現非常多的子版本,于是微軟提供了一套運行庫的命名方法。這個命名方法是這樣的,靜態版和動態版完全不同。靜態版的CRT位于MSVC安裝目錄下的lib/,比如Visual C++ 2008的靜態庫路徑為“Program Files/Microsoft Visual Studio 9.0/VC/lib”,它們的命名規則為:
libc [p] [mt] [d] .lib
l?????????? p 表示 C Plusplus,即C++標準庫。
l?????????? mt表示 Multi-Thread,即表示支持多線程。
l?????????? d 表示 Debug,即表示調試版本。
比如靜態的非C++的多線程版CRT的文件名為libcmtd.lib。動態版的CRT的每個版本一般有兩個相對應的文件,一個用于鏈接的.lib文件,一個用于運行時用的.dll動態鏈接庫。它們的命名方式與靜態版的CRT非常類似,稍微有所不同的是,CRT的動態鏈接庫DLL文件名中會包含版本號。比如Visual C++ 2005的多線程、動態鏈接版的DLL文件名為msvcr90.dll(Visual C++ 2005的內部版本號為8.0)。表11-1列舉了一些最常見的MSVC CRT版本(以Visual C++ 2005為例)。
表11-1
?
| 文件名 | 相關的DLL | 屬性 | 編譯器選項 | 預編譯宏 |
| libcmt.lib | 無 | 多線程,靜態鏈接 | /MT | _MT |
| msvcrt.lib | msvcr80.dll | 多線程,動態鏈接 | /MD | _MT, _DLL |
| libcmtd.lib | 無 | 多線程,靜態鏈接,調試 | /MTd | _DEBUG, _MT |
| msvcrtd.lib | msvcr90d.dll | 多線程,動態鏈接,調試 | /MDd | _DEBUG, _MT, _DLL |
| msvcmrt.lib | msvcm90.dll | 托管/本地混合代碼 | /clr | ? |
| msvcurt.lib | msvcm90.dll | 純托管代碼 | /clr:pure | ? |
?
自從Visual C++ 2005(MSVC 8.0)以后,MSVC不再提供靜態鏈接單線程版的運行庫(LIBC.lib、LIBCD.lib),因為據微軟聲稱,經過改進后的新的多線程版的C運行庫在單線程的模式下運行速度已經接近單線程版的運行庫,于是沒有必要再額外提供一個只支持單線程的CRT版本。
默認情況下,如果在編譯鏈接時不指定鏈接哪個CRT,編譯器會默認選擇LIBCMT.LIB,即靜態多線程CRT,Visual C++ 2005之前的版本會選擇LIBC.LIB,即靜態單線程版本。關于CRT的多線程和單線程的問題,我們在后面的章節還會再深入分析。
除了使用編譯命令行的選項之外,在Visual C++工程屬性中也可以設置相關選項。如圖11-9所示。
?
圖11-9? Visual C++ 2003 .NET工程屬性的截圖
我們可以從圖11-9中看到,除了多線程庫以外,還有單線程靜態/ML、單線程靜態調試/MLd的選項。
C++ CRT
表11-1中的所有CRT都是指C語言的標準庫,MSVC還提供了相應的C++標準庫。如果你的程序是使用C++編寫的,那么就需要額外鏈接相應的C++標準庫。這里“額外”的意思是,如表11-2所列的C++標準庫里面包含的僅僅是C++的內容,比如iostream、string、map等,不包含C的標準庫。
表11-2
?
| 文件名 | 相應DLL | 屬性 | 編譯選項 | 宏定義 |
| LIBCPMT.LIB | 無 | 多線程,靜態鏈接 | /MT | _MT |
| MSVCPRT.LIB | MSVCP90.dll | 多線程,動態鏈接 | /MD | _MT, _DLL |
| LIBCPMTD.LIB | 無 | 多線程,靜態鏈接,調試 | /MTd | _DEBUG, _MT |
| MSVCPRTD.LIB | MSVCP90D.dll | 多線程,動態鏈接,調試 | /MDd | _DEBUG, _MT, _DLL |
?
當你在程序里包含了某個C++標準庫的頭文件時,MSVC編譯器就認為該源代碼文件是一個C++源代碼程序,它會在編譯時根據編譯選項,在目標文件的“.drectve”段(還記得第2章中的DIRECTIVE吧?)相應的C++標準庫鏈接信息。比如我們用C++寫一個“Hello World”程序:
// hello.cpp
#include <iostream>
int main()
{
??? std::cout << "Hello world" << std::endl;
??? return 0;
}
然后將它編譯成目標文件,并查看它的“.drectve”段的信息:
cl /c hello.cpp
dumpbin /DIRECTIVES hello.obj
Microsoft (R) COFF/PE Dumper Version 9.00.21022.08
Copyright (C) Microsoft Corporation.? All rights reserved.
Dump of file msvcprt.obj
File Type: COFF OBJECT
?? Linker Directives
?? -----------------
?? /DEFAULTLIB:"libcpmt"
?? /DEFAULTLIB:"LIBCMT"
?? /DEFAULTLIB:"OLDNAMES"
cl /c /MDd hello.cpp
dumpbin /DIRECTIVES hello.obj
Microsoft (R) COFF/PE Dumper Version 9.00.21022.08
Copyright (C) Microsoft Corporation.? All rights reserved.
Dump of file msvcprt.obj
File Type: COFF OBJECT
?? Linker Directives
?? -----------------
?? /manifestdependency:"type='win32'
?? name='Microsoft.VC90.DebugCRT'
?? version='9.0.21022.8'
?? processorArchitecture='x86'
?? publicKeyToken='1fc8b3b9a1e18e3b'"
?? /DEFAULTLIB:"msvcprtd"
?? /manifestdependency:"type='win32'
?? name='Microsoft.VC90.DebugCRT'
?? version='9.0.21022.8'
?? processorArchitecture='x86'
?? publicKeyToken='1fc8b3b9a1e18e3b'"
?? /DEFAULTLIB:"MSVCRTD"
?? /DEFAULTLIB:"OLDNAMES"
可以看到,hello.obj須要鏈接libcpmt.lib、LIBCMT.lib和OLDNAMES.lib。當我們使用“/MDd”參數編譯時,hello.obj就需要msvcprtd.lib、MSVCRTD.lib和OLDNAMES.lib,除此之外,編譯器還給鏈接器傳遞了“/manifestdependency”參數,即manifest信息。
Q&A
Q:如果一個程序里面的不同obj文件或DLL文件使用了不同的CRT,會不會有問題?
A:這個問題實際上分很多種情況。如果程序沒有用到DLL,完全靜態鏈接,不同的obj在編譯時用到了不同版本的靜態CRT。由于目前靜態鏈接CRT只有多線程版,并且如果所有的目標文件都統一使用調試版或發布版,那么這種情況下一般是不會有問題的。因為我們知道,目標文件對靜態庫引用只是在目標文件的符號表中保留一個記號,并不進行實際的鏈接,也沒有靜態庫的版本信息。
?????? 但是,如果程序涉及動態鏈接CRT,這就比較復雜了。因為不同的目標文件如果依賴于不同版本的msvcrt.lib和msvcrt.dll,甚至有些目標文件是依賴于靜態CRT,而有些目標文件依賴于動態CRT,那么很有可能出現的問題就是無法通過鏈接。鏈接器對這種情況的具體反應依賴于輸入目標文件的順序,有些情況下它會報符號重復定義錯誤:
?????? MSVCRTD.lib(MSVCR80D.dll) : error LNK2005: _printf already defined in LIBCMTD.lib (printf.obj)
?????? 但是有些情況下,它會使鏈接順利通過,只是給出一個警告:
?????? LINK : warning LNK4098: defaultlib 'LIBCMTD' conflicts with use of other libs; use /NODEFAULTLIB:library
?????? 如果碰到上面這種靜態/動態CRT混合的情況,我們可以使用鏈接器的/NODEFAULTLIB來禁止某個或某些版本的CRT,這樣一般就能使鏈接順利進行。
?????? 最麻煩的情況應該屬于一個程序所依賴的DLL分別使用不同的CRT,這會導致程序在運行時同時有多份CRT的副本。在一般情況下,這個程序應該能正常運行,但是值得注意的是,你不能夠在這些DLL之間相互傳遞使用一些資源。比如兩個DLL A和B分別使用不同的CRT,那么應該注意以下問題:
???? l???? 不能在A中申請內存然后在B中釋放,因為它們分屬于不同的CRT,即擁有不同的堆,這包括C++里面所有對象的申請和釋放;
???? l???? 在A中打開的文件不能在B中使用,比如FILE*之類的,因為它們依賴于CRT的文件操作部分。
?????? 還有類似的問題,比如不能相互共享locale等。如果不違反上述規則,可能會使程序發生莫名其妙的錯誤并且很難發現。
?????? 防止出現上述問題的最好方法就是保證一個工程里面所有的目標文件和DLL都使用同一個版本的CRT。當然有時候事實并不能盡如人意,比如很多時候當我們要用到第三方提供的.lib或DLL文件而對方又不提供源代碼時,就會比較難辦。
?????? Windows系統的system32目錄下有個叫msvcrt.dll的文件,它跟msvcr90.dll這樣的DLL有什么區別?
Q:為什么我用Visual C++ 2005/2008編譯的程序無法在別人的機器上運行?
A:因為Visual C++ 2005/2008編譯的程序使用了manifest機制,這些程序必須依賴于相對應版本的運行庫。一個解決的方法就是使用靜態鏈接,這樣就不需要依賴于CRT的DLL。另外一個解決的方法就是將相應版本的運行庫與程序一起發布給最終用戶。
?
轉載自C/C++運行庫轉載于:https://www.cnblogs.com/nami-code/p/3941642.html
總結
以上是生活随笔為你收集整理的glibc与MSVC CRT(转载)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 西门子 S7-200以太网模块连接MCG
- 下一篇: poj3685 二分套二分