【轉載】 C/C++運行庫

http://book.csdn.net/bookfiles/1017/100101730949.shtml

 

呵呵,這篇真是好東西,先把它弄在我的地盤再慢慢咀嚼……

原來這是一本書上的《程序員的自我修養》,好東西,crt我迷惑了好久……

11.2  C/C++運行庫

11.2.1  C語言運行庫

任何一個C程序,它的背後都有一套龐大的代碼來進行支撐,以使得該程序能夠正常運行。這套代碼至少包括入口函數,及其所依賴的函數所構成的函數集合。當然,它還理應包括各種標準庫函數的實現。

這樣的一個代碼集合稱之爲運行庫(Runtime Library)。而C語言的運行庫,即被稱爲C運行庫(CRT)。

如果讀者擁有Visual Studio,可以在VC/crt/src裏找到一份C語言運行庫的源代碼。然而,由於此源代碼過於龐大,僅僅.c文件就有近千個,並且和C++的STL代碼一起毫無組織地堆放在一起,以至於實際上沒有什麼仔細閱讀的可能性。同樣,Linux下的libc源代碼讀起來也如同啃磚頭。所幸的是,在本章的最後,我們會一起來實現一個簡單的運行庫,讓大家更直觀地瞭解它。

一個C語言運行庫大致包含了如下功能:

l           啓動與退出:包括入口函數及入口函數所依賴的其他函數等。

l           標準函數:由C語言標準規定的C語言標準庫所擁有的函數實現。

l           I/O:I/O功能的封裝和實現,參見上一節中I/O初始化部分。

l           堆:堆的封裝和實現,參見上一節中堆初始化部分。

l           語言實現:語言中一些特殊功能的實現。

l           調試:實現調試功能的代碼。

在這些運行庫的組成成分中,C語言標準庫佔據了主要地位並且大有來頭。C語言標準庫是C語言標準化的基礎函數庫,我們平時使用的printf、exit等都是標準庫中的一部分。標準庫定義了C語言中普遍存在的函數集合,我們可以放心地使用標準庫中規定的函數而不用擔心在將代碼移植到別的平臺時對應的平臺上不提供這個函數。在下一章節裏,我們會介紹C語言標準庫的函數集合,並對一些特殊的函數集合進行詳細介紹。

標準庫的歷史

在計算機世界的歷史中,C語言在AT&T的貝爾實驗室誕生了。初生的C語言在功能上非常不完善,例如不提供I/O相關的函數。因此在C語言的發展過程中,C語言社區共同意識到建立一個基礎函數庫的必要性。與此同時,在20世紀70年代C語言變得非常流行時,許多大學、公司和組織都自發地編寫自己的C語言變種和基礎函數庫,因此當到了80年代時,C語言已經出現了大量的變種和多種不同的基礎函數庫,這對代碼遷移等方面造成了巨大的障礙,許多大學、公司和組織在共享代碼時爲了將代碼在不同的C語言變種之間移植搞得焦頭爛額,怨聲載道。於是對此慘狀忍無可忍的美國國家標準協會(American National Standards Institute, ANSI)在1983年成立了一個委員會,旨在對C語言進行標準化,此委員會所建立的C語言標準被稱爲ANSI C。第一個完整的C語言標準建立於1989年,此版本的C語言標準稱爲C89。在C89標準中,包含了C語言基礎函數庫,由C89指定的C語言基礎函數庫就稱爲ANSI C標準運行庫(簡稱標準庫)。其後在1995年C語言標準委員會對C89標準進行了一次修訂,在此次修訂中,ANSI C標準庫得到了第一次擴充,頭文件iso646.h、wchar.h和wctype.h加入了標準庫的大家庭。在1999年,C99標準誕生,C語言標準庫得到了進一步的擴充,頭文件complex.h、fenv.h、inttypes.h、stdbool.h、stdint.h和tgmath.h進入標準庫。自此,C語言標準庫的面貌一直延續至今。

11.2.2  C語言標準庫

在本章節裏,我們將介紹C語言標準庫的基本函數集合,並對其中一些特殊函數進行詳細的介紹。ANSI C的標準庫由24個C頭文件組成。與許多其他語言(如Java)的標準庫不同,C語言的標準庫非常輕量,它僅僅包含了數學函數、字符/字符串處理,I/O等基本方面,例如:

l           標準輸入輸出(stdio.h)。

l           文件操作(stdio.h)。

l           字符操作(ctype.h)。

l           字符串操作(string.h)。

l           數學函數(math.h)。

l           資源管理(stdlib.h)。

l           格式轉換(stdlib.h)。

l           時間/日期(time.h)。

l           斷言(assert.h)。

l           各種類型上的常數(limits.h & float.h)。

除此之外,C語言標準庫還有一些特殊的庫,用於執行一些特殊的操作,例如:

l           變長參數(stdarg.h)。

l           非局部跳轉(setjmp.h)。

相信常見的C語言函數讀者們都已經非常熟悉,因此這裏就不再一一介紹,接下來讓我們看看兩組特殊函數的細節。

1. 變長參數

變長參數是C語言的特殊參數形式,例如如下函數聲明:

int printf(const char* format, ...);

如此的聲明表明,printf函數除了第一個參數類型爲const char*之外,其後可以追加任意數量、任意類型的參數。在函數的實現部分,可以使用stdarg.h裏的多個宏來訪問各個額外的參數:假設lastarg是變長參數函數的最後一個具名參數(例如printf裏的format),那麼在函數內部定義類型爲va_list的變量:

va_list ap;

該變量以後將會依次指向各個可變參數。ap必須用宏va_start初始化一次,其中lastarg必須是函數的最後一個具名的參數。

va_start(ap, lastarg);

此後,可以使用va_arg宏來獲得下一個不定參數(假設已知其類型爲type):

type next = va_arg(ap, type);

在函數結束前,還必須用宏va_end來清理現場。在這裏我們可以討論這幾個宏的實現細節。在研究這幾個宏之前,我們要先了解變長參數的實現原理。變長參數的實現得益於C語言默認的cdecl調用慣例的自右向左壓棧傳遞方式。設想如下的函數:

int sum(unsigned num, ...);

其語義如下:

第一個參數傳遞一個整數num,緊接着後面會傳遞num個整數,返回num個整數的和。

當我們調用:

int n = sum(3, 16, 38, 53);

參數在棧上會形成如圖11-7所示的佈局。

 

圖11-7  函數參數在棧上分佈

在函數內部,函數可以使用名稱num來訪問數字3,但無法使用任何名稱訪問其他的幾個不定參數。但此時由於棧上其他的幾個參數實際恰好依序排列在參數num的高地址方向,因此可以很簡單地通過num的地址計算出其他參數的地址。sum函數的實現如下:

int sum(unsigned num, ...)

{

    int* p = &num + 1;

    int ret = 0;

    while (num--)

        ret += *p++;

    return ret;

}

在這裏我們可以觀察到兩個事實:

(1)sum函數獲取參數的量僅取決於num參數的值,因此,如果num參數的值不等於實際傳遞的不定參數的數量,那麼sum函數可能取到錯誤的或不足的參數。

(2)cdecl調用慣例保證了參數的正確清除。我們知道有些調用慣例(如stdcall)是由被調用方負責清除堆棧的參數,然而,被調用方在這裏其實根本不知道有多少參數被傳遞進來,所以沒有辦法清除堆棧。而cdecl恰好是調用方負責清除堆棧,因此沒有這個問題。

printf的不定參數比sum要複雜得多,因爲printf的參數不僅數量不定,而且類型也不定。所以printf需要在格式字符串中註明參數的類型,例如用%d表明是一個整數。printf裏的格式字符串如果將類型描述錯誤,因爲不同參數的大小不同,不僅可能導致這個參數的輸出錯誤,還有可能導致其後的一系列參數錯誤。

【小實驗】

printf的狂亂輸出

#include <stdio.h>

int main()

{

    printf("%lf/t%d/t%c/n", 1, 666, 'a');

}

在這個程序裏,printf的第一個輸出參數是一個int(4字節),而我們告訴printf它是一個double(8字節以上),因此printf的輸出會錯誤,由於printf在讀取double的時候實際造成了越界,因此後面幾個參數的輸出也會失敗。該程序的實際輸出爲(根據實際編譯器和環境可能不同):

0.000000     97    '

下面讓我們來看va_list等宏應該如何實現。

va_list實際是一個指針,用來指向各個不定參數。由於類型不明,因此這個va_list以void*或char*爲最佳選擇。

va_start將va_list定義的指針指向函數的最後一個參數後面的位置,這個位置就是第一個不定參數。

va_arg獲取當前不定參數的值,並根據當前不定參數的大小將指針移向下一個參數。

va_end將指針清0。

按照以上思路,va系列宏的一個最簡單的實現就可以得到了,如下所示:

#define va_list char*

#define va_start(ap,arg) (ap=(va_list)&arg+sizeof(arg))

#define va_arg(ap,t) (*(t*)((ap+=sizeof(t))-sizeof(t)))

#define va_end(ap) (ap=(va_list)0)

【小提示】

變長參數宏

在很多時候我們希望在定義宏的時候也能夠像print一樣可以使用變長參數,即宏的參數可以是任意個,這個功能可以由編譯器的變長參數宏實現。在GCC編譯器下,變長參數宏可以使用“##”宏字符串連接操作實現,比如:

#define printf(args…) fprintf(stdout, ##args)

那麼printf(“%d %s”, 123, “hello”)就會被展開成:

fprintf(stdout, “%d %s”, 123, “hello”)

而在MSVC下,我們可以使用__VA_ARGS__這個編譯器內置宏,比如:

#define printf(…) fprintf(stdout,__VA_ARGS__)

它的效果與前面的GCC下使用##的效果一樣。

2. 非局部跳轉

非局部跳轉即使在C語言裏也是一個備受爭議的機制。使用非局部跳轉,可以實現從一個函數體內向另一個事先登記過的函數體內跳轉,而不用擔心堆棧混亂。下面讓我們來看一個示例:

#include <setjmp.h>

#include <stdio.h>

jmp_buf b;

void f()

{

    longjmp(b, 1);

}

int main()

{

    if (setjmp(b))

        printf("World!");

    else

    {

        printf("Hello ");

        f();

    }

}

這段代碼按常理不論setjmp返回什麼,也只會打印出“Hello ”和“World!”之一,然而事實上的輸出是:

Hello World!

實際上,當setjmp正常返回的時候,會返回0,因此會打印出“Hello ”的字樣。而longjmp的作用,就是讓程序的執行流回到當初setjmp返回的時刻,並且返回由longjmp指定的返回值(longjmp的參數2),也就是1,自然接着會打印出“World!”並退出。換句話說,longjmp可以讓程序“時光倒流”回setjmp返回的時刻,並改變其行爲,以至於改變了未來。

是的,這絕對不是結構化編程。K

11.2.3  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。

與此同時,爲了支持新的庫和可執行文件格式,crt0.o也進行了升級,變成了crt1.o。crt0.o和crt1.o之間的區別是crt0.o爲原始的,不支持“.init”和“.finit”的啓動代碼,而crt1.o是改進過後,支持“.init”和“.finit”的版本。這一點我們從反彙編crt1.o可以看到,它向libc啓動函數__libc_start_main()傳遞了兩個函數指針“__libc_csu_init”和“__libc_csu_fini”,這兩個函數負責調用_init()和_finit(),我們在後面“C++全局構造和析構”的章節中還會詳細分析。

爲了方便運行庫調用,最終輸出文件中的“.init”和“.finit”兩個段實際上分別包含的是_init()和_finit()這兩個函數,我們在關於運行庫初始化的部分也會看到這兩個函數,並且在C++全局構造和析構的章節中也會分析它們是如何實現全局構造和析構的。crti.o和crtn.o這兩個目標文件中包含的代碼實際上是_init()函數和_finit()函數的開始和結尾部分,當這兩個文件和其他目標文件安裝順序鏈接起來以後,剛好形成兩個完整的函數_init()和_finit()。我們用objdump可以查看這兩個文件的反彙編代碼:

$ objdump -dr /usr/lib/crti.o

crti.o:     file format elf32-i386

Disassembly of section .init:

00000000 <_init>:

   0:   55                      push   %ebp

   1:   89 e5                   mov    %esp,%ebp

   3:   53                      push   %ebx

   4:   83 ec 04                sub    $0x4,%esp

   7:   e8 00 00 00 00          call   c <_init+0xc>

   c:   5b                      pop    %ebx

   d:   81 c3 03 00 00 00       add    $0x3,%ebx

                        f: R_386_GOTPC  _GLOBAL_OFFSET_TABLE_

  13:   8b 93 00 00 00 00       mov 0x0(%ebx),%edx

                        15: R_386_GOT32 __gmon_start__

  19:   85 d2                   test   %edx,%edx

  1b:   74 05                   je     22 <_init+0x22>

  1d:   e8 fc ff ff ff          call   1e <_init+0x1e>

                        1e: R_386_PLT32 __gmon_start__

Disassembly of section .fini:

00000000 <_fini>:

   0:   55                      push   %ebp

   1:   89 e5                   mov    %esp,%ebp

   3:   53                      push   %ebx

   4:   83 ec 04                sub    $0x4,%esp

   7:   e8 00 00 00 00          call   c <_fini+0xc>

   c:   5b                      pop    %ebx

   d:   81 c3 03 00 00 00       add    $0x3,%ebx

                        f: R_386_GOTPC  _GLOBAL_OFFSET_TABLE_

$ objdump -dr /usr/lib/crtn.o

crtn.o:     file format elf32-i386

Disassembly of section .init:

00000000 <.init>:

   0:   58                      pop    %eax

   1:   5b                      pop    %ebx

   2:   c9                      leave

   3:   c3                      ret

Disassembly of section .fini:

00000000 <.fini>:

   0:   59                      pop    %ecx

   1:   5b                      pop    %ebx

   2:   c9                      leave

   3:   c3                      ret

於是在最終鏈接完成之後,輸出的目標文件中的“.init”段只包含了一個函數_init(),這個函數的開始部分來自於crti.o的“.init”段,結束部分來自於crtn.o的“.init”段。爲了保證最終輸出文件中“.init”和“.finit”的正確性,我們必須保證在鏈接時,crti.o必須在用戶目標文件和系統庫之前,而crtn.o必須在用戶目標文件和系統庫之後。鏈接器的輸入文件順序一般是:

ld crt1.o crti.o [user_objects] [system_libraries] crtn.o

由於crt1.o(crt0.o)不包含“.init”段和“.finit”段,所以不會影響最終生成“.init”和“.finit”段時的順序。輸出文件中的“.init”段看上去應該如圖11-8所示(對於“.finit”來說也一樣)。

 

圖11-8  .init段的組成

在默認情況下,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。另外一個解決的方法就是將相應版本的運行庫與程序一起發佈給最終用戶。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章