MSVC與CRT的恩怨情仇

很久沒有寫程式設計入門知識的相關文章了,這篇文章要來談談程式庫 (Library) 連結,以及關於 MSVC 與 CRT 之間的種種恩怨情仇。

如果你使用的作業系統是 Linux、Mac 或其他非 Windows 平台,你可以忽略這篇文章;如果你使用的作業系統是 Windows 平台,但沒有用 Microsoft Visual Studio C++(以下簡稱為 MSVC)軟體撰寫 C++ 程式的話,這篇文章對你的幫助可能很有限;但如果你的作業系統是 Windows,而且你使用的程式整合開發環境是 MSVC 軟體撰寫 C++ 程式的話,這篇文章應該能夠幫助你釐清一些重要的基礎觀念。

身為程式設計者,在學習程式設計的過程中,你是否曾經遇過某些看起來不知所云的錯誤訊息,卻不知該如何解決?例如當你快快樂樂地寫完程式,並且確認所有的程式碼都能成功通過編譯之後,接著執行「建置方案」(Build Solution) 的步驟,結果卻跑出一堆莫名其妙的錯誤:

LIBCMTD.lib(mlock.obj) : error LNK2005: __lock 已在 MSVCRTD.lib(MSVCR80D.dll) 中定義過了
LIBCMTD.lib(mlock.obj) : error LNK2005: __unlock 已在 MSVCRTD.lib(MSVCR80D.dll) 中定義過了
LIBCMTD.lib(crt0.obj) : error LNK2005: _mainCRTStartup 已在 MSVCRTD.lib(crtexe.obj) 中定義過了

…………

LINK : warning LNK4098: 預設的程式庫 ‘MSVCRTD’ 與其他使用的程式庫衝突,請使用 /NODEFAULTLIB:library
LINK : warning LNK4098: 預設的程式庫 ‘LIBCMTD’ 與其他使用的程式庫衝突,請使用 /NODEFAULTLIB:library
D:\Workspace\CrtLibTest\Debug\CrtLibTest.exe : fatal error LNK1169: 找到有一或多個已定義的符號

以一般的情況來說,如果在你的程式專案中有使用某些由他人所撰寫的第三方程式庫或是開源專案的程式庫,比較容易會發生上述的錯誤狀況。從上述這些看似離奇而令人摸不著頭緒的錯誤訊息中,我們大概可以猜測問題點應該在於 LIBCMTD.lib 與 MSVCRTD.lib 這兩個程式庫身上。但到底什麼是 LIBCMTD.lib 和 MSVCRTD.lib?在我們的程式碼中有使用這些程式庫嗎?

答案是肯定的。

熟悉 C 語言的程式設計者都知道,如果要使用 printf()、scanf() 或者 fopen() 等等 C 語言的基本 I/O 操作函式時,首先必須用 #include 語法將 stdio.h 這個標頭檔納入我們的程式碼中。藉由 stdio.h 中對這些 I/O 操作函式所做出的函式宣告 (function declaration),編譯器 (Compiler) 才得以確認 printf、scanf 以及 fopen 等等都是合法可用的函式。

而當我們撰寫的程式碼經過編譯器產出 OBJ 形式的檔案之後,需要再經由連結器 (Linker) 的處理程序,將程式碼中全部有使用到的函式定義 (function definition) 連結建置起來,才能夠產生出最後的程式執行檔。問題來了,我們知道 printf、scanf 以及 fopen 的函式宣告存在於 stdio.h 當中,但是這些傢伙的函式定義,也就是真正的實做程式碼,究竟存放在什麼地方呢?

在 C 語言的標準程式庫中。

由 C 語言所制訂的標準程式庫,稱之為「執行階段程式庫」,也就是 C Run-Time Library,通常可簡稱為 CRT。在 C 語言的標準程式庫中,包含了一組常用的基礎函式,例如 I/O 處理與字串操作程序等等,所以只要我們使用 C 語言撰寫程式碼,就一定要將編譯完成後的程式碼 OBJ 檔,連結至 C 語言的執行階段程式庫,才能夠產生出合法的 C 語言程式執行檔。

而 CRT 並非只有單一一種版本存在。事實上,除了可以依「除錯」與「釋出」用途分成兩個版本之外,兩者又可分別衍生分出「靜態連結」與「動態連結」兩種形式:

靜態連結

  • LIBCMTD.lib(除錯版本)
  • LIBCMT.lib

動態連結

  • MSVCRTD.lib(除錯版本)
  • MSVCRT.lib

雖然這四個 CRT 版本的用途與使用方式各不相同,但卻有個共通的特點,就是它們都是滿足執行緒安全需求,可在多執行緒程式碼中安全使用的程式庫版本。事實上,在過去 MSVC 6 的版本中,本來還有另外兩個 LIBCD.lib(除錯版本)與 LIBC.lib 程式庫,是專門給單執行緒程式使用的 CRT 版本,但是這兩個選項自 MSVC 2005 開始就從設定選項中被刪除掉了,所以現在大多數程式設計者使用的都是多執行緒的 CRT 版本。

在程式庫連結 (library linking) 的行為中,靜態連結和動態連結的分別,在於使用靜態連結時,會直接將程式庫的函式定義嵌入執行檔之中,而使用動態連結時,程式庫的函式定義則存在於另外的獨立檔案,通常是 DLL 格式的檔案中,然後與程式執行檔一同發佈給使用者。因此在檔案的尺寸上,使用動態連結的執行檔檔案,通常會比使用靜態連結的執行檔檔案來得更小一些。

使用動態連結 CRT 版本的好處,是能夠將經常使用到的標準程式庫們獨立出來,放在 Windows 的系統資料夾中,以減少我們建置出來的執行檔檔案尺寸。但反過來說,使用動態連結 CRT 版本的缺點也在於這些與執行檔相依為命的 DLL 檔案上。舉例來說,如果程式以 MSVC 2005 建置出 Debug 組態的執行檔,則此執行檔需要有 msvcr80d.dll 存在才能順利執行;如果是 Release 組態,則相依於 msvcr80.dll。但是如果你把相同的程式碼拿到 MSVC 2008 上建置,產生出來的執行檔則相依於 msvcr90d.dll 與 msvcr90.dll 兩個不同的 DLL 檔案。不同版本的 MSVC,都會有各自不同的相依 DLL 檔案。

在 MSVC 的程式專案中,如何指定程式碼要使用靜態連結或者動態連結的 CRT 版本?其實很容易,只要在專案屬性的「C/C++」頁面中,選擇「程式碼產生」(Code Generation) 子頁面,其中有個「執行階段程式庫」(Runtime Library) 的項目,也就是專案中用來設定 CRT 連結版本的地方。其中總共有四個選項,正好對應於上述靜態連結與動態連結的四個不同程式庫版本。

  • 多執行緒偵錯 (/MTd):對應 LIBCMTD.lib
  • 多執行緒 (/MT):對應 LIBCMT.lib
  • 多執行緒偵錯 DLL (/MDd):對應 MSVCRTD.lib
  • 多執行緒 DLL (/MD):對應 MSVCRT.lib

如果你沒有做任何設定就開始建置程式的話,MSVC 的預設選項則會使用動態連結的版本。

c-runtime-library
C Runtime Library

請注意,以上只是單純 C 語言的程式庫而沒有包含 C++ 語言在內。如果你的程式系統中,有包含 C++ 語言的程式碼的話,那又是另外一回事了。但是在專案屬性的頁面中,為什麼找不到相關的設定選項呢?因為 MSVC 悄悄地幫程式設計者代勞處理掉了。只要在程式碼中使用 #include 語法納入任何一個 C++ 的標頭檔,例如 iostream 或 fstream,MSVC 就會在連結器的運作階段中,自動幫我們連結 C++ 的執行階段程式庫。而 C++ 的執行階段程式庫,同樣可分為四個版本:

靜態連結

  • LIBCPMTD.lib(除錯版本)
  • LIBCPMT.lib

動態連結

  • MSVCPRTD.lib(除錯版本):執行檔相依於 MSVCP90D.dll
  • MSVCPRT.lib:執行檔相依於 MSVCP90.dll

至於程式執行檔使用的是靜態連結或者動態連結的版本,就仰賴於 C 語言的版本設定選項了。舉個例子來說,如果你撰寫了一個 Debug 組態的 C++ 程式,並且保留專案原先預設的建置選項(動態連結),那麼最終建置出來的程式執行檔將會相依於 MSVCR90D.dll 以及 MSVCP90D.dll 兩個 DLL 檔案。如果將相同的程式以 Release 組態建置完成,則會相依於 MSVCR90.dll 以及 MSVCP90.dll 二者。

standard-cpp-library
Standard C++ Library

剛學習程式設計的入門者,經常會在滿心歡喜地完成一件程式作品並且傳給其他人使用時,卻發現不能在別人的電腦上啟動程式,其實就是陷入了使用者電腦缺少 DLL 檔案而無法執行程式的窘境。有三種方法可以解決這個令人困擾的問題:

  1. 使用者的電腦,必須先安裝「Visual C++ 可轉發套件」(MSVC 2008MSVC 2005 )。
  2. 將所需的 DLL 檔案,例如 MSVCR90D.dll 與 MSVCP90D.dll,直接附在程式的下載包當中。
  3. 以靜態連結方式建置程式執行檔。

當你無法確定自己的程式或別人的程式,是否相依於某些特定的 DLL 檔案時,有一個非常好用的免費工具程式 Dependency Walker,可以開啟 EXE 格式的執行檔或者 DLL 格式的動態程式庫,然後詳細地條列出它們所相依的 DLL 檔案。

瞭解了幾種不同的 CRT 版本選項之後,回到最前面的錯誤訊息問題,相信各位現在應該能夠很清楚地理解,原來會發生這些奇怪的錯誤狀況,是因為程式同時連結了 LIBCMTD.lib 與 MSVCRTD.lib 所以造成函式定義版本衝突。也就是說,程式連結器已經在其中一個 CRT 的版本中找到所需的函式定義,但此時卻又跳出另外一位 CRT,也給了一份相同函式的實作版本,所以連結器無法判斷應該忽略誰並且選擇誰。

而這個狀況的發生原因,就是你的程式與程式所連結的外部程式庫,使用了不同的 CRT 版本之故。例如,當你的程式使用了 Lua,自然必須連結至 Lua 的程式庫 lua5.1.lib,但如果 lua5.1.lib 是以靜態連結版本的 CRT 建置而成,而你的程式卻是以預設選項,動態連結 CRT 來建置程式執行檔的話,如此一來就會產生上述這些錯誤訊息了。至此,問題的答案已昭然若揭,解決方法有二種:其一是將 Lua 重新以動態連結 CRT 的方式建置出一個新的程式庫,其二則是將自己的程式專案改成以靜態連結 CRT 方式建置。

換個角度想,當你身為一位程式庫的設計開發者,想要將自己寫的東西分享給其他人,但又不想要完全開放自己撰寫的程式源碼時,至少可以同時提供以下四種版本的程式庫,以妥善滿足使用者的各種不同需求:

  • Debug:動態連結除錯版本
  • Release:動態連結版本
  • Debug_Static:靜態連結除錯版本
  • Release_Static:靜態連結版本

然而,有時候世界並不會運作得如此理想。在某些特殊的狀況下,當我們使用他人所寫的第三方程式庫時,有時可能只拿得到其中某個特定的版本,例如 Release_Static 版本時,就很有可能會遇到程式庫衝突的錯誤情形。此時就需要視專案的實際需求而定,可以在專案屬性中指定「忽略特定程式庫」(Ignore Specific Library) 這個選項,讓程式碼連結器忽略某些程式庫,以此化解動靜程式庫或新舊程式庫之間的恩怨衝突。

小測驗:你所撰寫的程式,必須連結某個以靜態多執行緒 (/MT) CRT 建置而成的程式庫。如果你的程式在 Debug 組態下以多執行緒偵錯 (/MTd) 選項建置,是否會產生衝突?如果你的程式在 Release 組態下以多執行緒 (/MT) 選項建置,是否會產生衝突?是的話,應該如何解決?

延伸閱讀:

33 Replies to “MSVC與CRT的恩怨情仇”

  1. 我常碰到 LIBCMT.lib 和 MSVCRT.lib 相衝的問題,卻不曾深究原因,今天看到您的文章才深入了解,受教了,謝謝!

  2. @Todd:
    很高興這篇文章對你有幫助,謝謝你的回應~ :)

    @zii:
    97 bytes 的執行檔… 真不可思議。想起以前曾看過只有 64 KB 大小的 3D 遊戲,不過 97 bytes 實在太殺了,我想這才是真正可稱為 Geek 的東西吧!

  3. 果然是這樣啊…
    上學期的作業就發生過這種事
    那時老師丟了個Windows版踩地雷的GUI frame給我們, 作主程式部分的填空當加分題
    結果做完後就是發生這些問題…新手根本無法解決
    這篇文章對我有很大的幫助, 真是感謝

  4. @Necromancer:
    以前在學校時我也遇過這種狀況,老師給的 library 可能是用很舊的編譯器建置出來的版本,對剛開始學習程式設計的學生來說,真的會不知道怎麼解決哩。 囧rz

  5. 簡單的小問題很容易被人忽略, 但是背後通常都隱藏著大學問 :)

    最後的小測驗答案應該是:
    1. 會, 解決方法是重新把程式庫以/MTd重新build一次
    2. 不會

  6. @G.J.:
    答案沒錯~

    第一項的另一個解決方式,是在「忽略特定程式庫」裡輸入 LIBCMT.lib,這樣就不需要重新建置程式庫,也不會和 LIBCMTD.lib 相互衝突囉。

  7. 版大你好,
    因為最近在學Win32 programming, 找資料時看到這篇文章, 但有些觀念還不是很清楚, 煩請版大指教.
    請問 xxx.lib 和xxx.dll 他們之間有什麼差別?
    尤其是動態連結版本的lib和 DLL之間有什麼不一樣嗎?
    這個問題困擾我很久, 雖然照用就是了, 但還是想弄清楚.

    另, 請問版大, 要把Win32 programming學好, 版大有推薦的書籍嗎? 謝謝.

  8. @CHL:
    你好,

    在 Windows 平台下,一般常見以 .lib 副檔名結尾的檔案,即是所謂的「靜態連結程式庫」(statically linked library);而以 .dll 副檔名結尾的檔案,則是「動態連結程式庫」(dynamically linked library)。兩者間最大的不同之處,就是靜態連結程式庫會在程式碼的建置時期,與你所寫的程式碼進行連結,並寫入程式的可執行檔 (.exe) 內,所以你的執行檔不需要包含 .lib 檔案一起發佈給使用者。而動態連結程式庫則會以獨立的形式存在,你寫的程式碼會在執行時期才去載入 .dll 內的程式庫,因此執行檔需要包含著 .dll 檔案一起發佈給使用者。

    關於 Win32 Programming,必讀的書當然就是 Charles Petzold 所寫的聖經《Programming Windows》囉!這本書有中文版,然後具有跟磚頭差不多的厚度,看起來非常嚇人。不過我認為,初學者只要先把前面六、七個章節讀完學會,就可以有不錯的基礎了~ :)

  9. 這星期發生了在Ubuntu 8.04 build的程式不能在Debian 4執行的問題
    查了一下得知是libc版本不同的關係,Ubuntu build出來的版本需要函式庫裡有GLIBC_2.4這個符號,但Debian的只有到GLIBC_2.0(Debian的軟體通常都比較舊)
    雖然已經知道Linux是個幾乎無版本間相容性的系統,但發生這種事還是覺得很鳥,我當然不希望自己寫的程式拿到其他發行版就不能用,決定把Ubuntu 8.04有提供的GCC 3.3到4.2版全部下載來研究,可能也需要研究其他發行版的libc版本

    用Linux一段時間後覺得Linux普及不起來不是沒有原因的,版權自由但使用上一點都不自由

  10. @Shark:
    我覺得 Linux 使用上很自由阿~

    但是為了享受自由的好處,也必須付出相對的代價,深入學習如何善用 Linux 的威力。在 Linux 上開發程式,除了需要注意最基本的 kernel 版本以外,GCC 的版本以及其他相依函式庫的版本,也是不可不注意的重要議題。

  11. Linux glibc的問題找到一個解決方法,其實很簡單,找不到符號自己定義一個就好了
    例如用objdump -T查到有GLIBC_2.4 __stack_chk_fail就在自己的code找個地方像下面定義
    void __stack_chk_fail(){
    return;
    }
    build出來再檢查就沒有GLIBC_2.4了,也能在glibc 2.3執行,總之記得用objdump檢查一下

    對想寫Linux程式的人應該會有用

  12. @Shark
    你不希望自己寫的程式拿到其他發行版就不能用,這解法很簡單阿,怎麼會變成抱怨linux使用上一點都不自由?這表示你對linux環境不熟悉

    1.最簡單的解法,靜態連結glibc_2.4,這樣到任何發行版都不會有問題
    2.複雜的解法,在打包上(tarball,RPM)下功夫,缺什麼lib在安裝時自動下載搞定。不過各大發行版的打包機制不同,這解法的適用範圍也小

    你用hack的方式繞過glibc版本檢查,使得可以在低版本的glibc執行。這可能會發生無法預期的”執行時錯誤”,結果還不是要回頭用glibc_2.4

    @半路
    我在XP上測試系統是否會自動尋找相容的crt(更高的版本),例如firefox依賴msvcrt.dll, msvcr70.dll。

    之後將msvcr70.dll改名為msvcrt.dll.bak,再將firefox重新啟動,看看會不會自動載入msvcr80.dll。

    結果非常有趣,firefox可以正常啟動,但msvcr70.dll或msvcr80.dll都不見蹤影,似乎少了這個dll也不會任何影響,不過還是無法證實原本的假設。

  13. @Larz:
    我不是已經找到解法了嗎?

    1.其他函式庫如libpng也要用靜態連結至glibc的版本,不然無法build
    2.package會綁函式庫和發行版版本,會對玩家造成困擾,而且使用package會修改系統資訊,不符合綠色軟體的精神
    我的要求是安裝時光碟放進去按幾下滑鼠搞定,移除時刪除資料夾即可

    正是因為Linux有使用上不方便的缺點所以要設法改良
    我做到現在需要避開的符號就只有__stack_chk_fail一個,實際試過沒問題,放心
    而且這只是一時的,等使用glibc 2.3的發行版(如Debian 4)漸漸被淘汰就不用再這麼做

  14. @Larz:
    謝謝你提供的測試資訊。不知道你有沒有搜尋所有檔案目錄下的 dll 呢?據我的使用經驗,Microsoft 的這些 dll 經常不只出現在一個檔案目錄底下而已。另外,應用程式的 dll 搜尋路徑也有可能不同於預設值。

    @Shark:
    即使是在 Linux 環境中,函式庫的版本相容性依舊是個棘手問題,只希望未來的作業系統,能夠實作出更創新更完善的解決方案。

  15. 這裡,還有這個文章,我只能說真棒,還有這些同好們,互相討論、研究,希望你能繼續下去阿!!
    我喜歡這裡!!
    默默的淺水夫 Jazzy James

  16. 您好,本人在建置c++ 2008的程式時也發生了檔案完成卻出現檔案短缺而不能執行的問題,所缺少的檔案也是MSVCP90D.dll,也根據版主提供的連結下載轉發套件,但是下載後依然無法執行,搜尋檔案後在
    C:\Program Files\Microsoft Visual Studio 9.0\VC\redist\Debug_NonRedist\x86\Microsoft.VC90.DebugCRT

    這個資料夾發現了msvcp90d.dll這個檔案,想請教版這這問題怎麼解決

  17. @jackial:
    把 msvcp90d.dll 複製到 exe 所在的目錄下,應該就可以執行了。

    或是使用文中提到的 Dependency Walker 工具,檢查看看 exe 所需的 DLL 檔案有哪些。

  18. 我想請問我執行 MSVC 2008 編譯之後,執行exe檔,跑出”無法找到程序輸入點 ?_Xfunc@tr1@std@@YAXXZ (在動態連結程式庫MSVCP90D.dll)”該如何解決,謝謝

  19. 很久沒上猴子靈藥了, 自從半路跑去到處演講後

    今天給編譯的問題搞得整個很火大, 總算在這邊找到解答
    感恩THX.

Leave a Reply