程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> 關於C >> Hello World程序背後的故事解密(一)—— 編譯器的選項和C運行時庫

Hello World程序背後的故事解密(一)—— 編譯器的選項和C運行時庫

編輯:關於C

作為一個程序員,想必大家都會對HelloWorld這個程序是深有感觸吧。是的,就是這個程序第一次帶我們進入了神奇的計算機編程的世界,指引我們開始走上了程序員這條充滿了艱辛和快樂的路。HelloWorld對我們這群程序員來講意義是非比尋常的,因此我想更加深入的研究一下C語言版的HelloWorld程序,撕開它的外衣,將隱藏在簡單表象下的運行時秘密拿出來給各位看看。

  這個系列文章預計將通過對CRT源代碼的分析,結合對C程序反匯編和動態調試得出的結果,試圖闡述一個程序整個生命周期中CRT所起的作用,從而使各位對C語言的各種底層機制有所了解。但是值得注意的是,C語言的底層是高度依賴具體的OS來實現其功能的,所以我們的分析不能夠脫離實際環境。我在撰寫本文時所用的環境是Windows 7,並且使用Visual Studio 2010的C語言編譯器。

1、WinMain還是main?

  作為windows下的開發人員,我們都知道,M$在標准C定義的入口函數main之外還定義了一個入口函數WinMain,這個函數是你編寫win32程序的默認程序入口。那麼WinMain和main到底有什麼不一樣呢?

  我們知道在VS建立工程的時候,我們可以指定建立工程的類型(console還是win32),這個實際上就決定了我們程序的入口點是什麼。但是,兩種類型在編譯器和連接器看來到底有什麼差異呢?通過對比編譯鏈接的命令行可以看出,實際上兩種工程主要有兩點差異:

  1. console工程定義_CONSOLE宏,而win32工程會定義_WINDOWS宏
  2. console工程在鏈接選項裡指定/subsystem:console,而win32工程在連接選項裡指定/subsystem:windows

  實際上,第一點差異影響的是編譯器的工作,通過定義_WINDOWS和_CONSOLE宏,可以實現針對控制台和GUI程序在編譯時的配置。這就像我們通過定義_DEBUG宏實現在調試時顯示調試信息而在發布時取消調試信息的原理有異曲同工之妙。

  第二點差異影響的是連接器的工作。由於不同的工程類型對應不同的入口點,通過設置連接器subsystem的參數,我們可以指定入口點到底是哪一個。我們分析一下微軟提供的CRT的實現源文件,在crt0.c裡是c運行時的初始化相關例程(實際上不止crt0.c,有關這一系列初始化源文件,我會在下一篇文章中分析)。我們可以看到實際上我們的C程序第一個載入的函數是mainCRTStartup或者WinMainCRTStartup,這兩個入口函數分別對應Windows下控制台程序和圖形界面程序的入口點。在他兩本身的實現中,他們各自調用了main和WinMain函數。那麼mainCRTStartup或WinMainCRTStartup又是誰指定的呢?實際上就是連接器,連接器在生成PE文件的時候,會指明代碼段程序的入口地址,這個信息會被操作系統的loader使用,在裝載我們的可執行程序的時候,loader根據連接器指定的入口點指導程序開始執行。

2、Unicode還是MBCS?

  時過境遷,現代的軟件開發早已不是一個人的游戲,也不再是在某個小網站上發布然後收取注冊費用的小打小鬧的盈利方式。我們開始追求是國際化的發展,不僅要在國內市場推廣我們的產品,還想在在國外的市場中分一杯羹。這就要求我們的軟件有Globalization的意識,而其中最重要的就是語言問題。

  想當初ANSI制定文字編碼ASCII的時候,沒有想過國際化的潛在需求,也沒有考慮各領域符號的需求,從而使得ASCII難以表示世界上其他國家的文字符號,也不能表示數學化學等學科的領域符號。為了解決這個問題,各地區在引入問題編碼時根據各自的國情作了修改,當然還得兼容原始的ASCII編碼,畢竟這才是老祖宗級別的人物。但是各地區之間的編碼卻不能兼容(比如台灣使用BIG5繁體編碼和咱們使用的GBK就不能相互兼容),這也是微軟在其操作系統裡面引入code page機制的直接原因——為了統籌協調各地區文字編碼的差異。

  後來有人站出來表示不能再這樣下去了,我們需要一個世界統一的編碼,然後你懂的——Unicode誕生了。

  咳,說遠了。回到正題,由於我們在開發程序時需要考慮程序最終部署在不同的地區,照理說最好的實踐就是將程序編譯為Unicode版本的,畢竟現在Windows系統內核天生是使用Unicode的,你使用MBCS還得在交給內核前被轉換一下,影響效率。但是由於各種現實因素的影響,我們還是不能拋棄MBCS,這就要求我們能夠編譯兩種版本的能力,我們不討論這其中的方法,但是我們應該知道最基本的,我們可以在程序中指定我們使用哪種編碼。

  在VS中,有兩個地方是與文字編碼相關的:

  1. 定義_UNICODE和_MBCS宏
  2. 使用wmain和wWinMain函數

  首先對於第一個,類似上面提到的_CONSOLE和_WINDOWS宏的功能,它也是制定了我們程序在編譯是的一些配置,比如ASCII版本的API還是Widechar版本的API。

  第二個實際上是影響連接器的行為。我們知道程序的實際入口函數是mainCRTStartup和WinMainCRTStartup,實際上還有兩個是wmainCRTStartup和wWinMainCRTStartup分別對應Unicode版本的入口。通過將幾次試驗,可知控制台和圖形界面(main和WinMain)是由編譯選項決定的,最終由連接器將其鏈接進可執行檔。但是是否是寬字符版卻不是由編譯選項決定的,它是由我們源碼中出現的是那種版本的入口函數決定的。打個比方,即是我們設置使用unicode,但若是我們的入口函數只定義了main,則鏈接器還是鏈接mainCRTStartup版本的函數。由此可知,編譯器選項中的Unicode或是MBCS只是在編譯時增加相應的宏定義而已,不會影響鏈接器的功能(即是具體鏈接那個版本的CRTStartup函數。

3、動態鏈接版本還是靜態鏈接版本?

  在編譯器選項中同樣我們能夠指定使用動態鏈接還是靜態鏈接的CRT庫函數,在微軟的MSDN上我們可以看到以下的對應關系:libcmt.lib對應靜態版本c運行時函數的靜態實現,而msvcrt.lib對應動態版本的c運行時函數的導入庫。

  實際上,如果我們使用動態版本的CRT,那麼其初始化代碼實現可參照crtexe.c,而若是使用靜態鏈接版本的話,其初始化代碼則可在crt0.c中找到。本文探討的只是靜態版本的CRT,而暫不涉及動態鏈接的內容。

 

  我們現在知道了C編譯器選項對於C程序底層的一些影響(子系統決定了入口是main還是WinMain,Unicode宏卻不能決定入口是否是寬字符版本的,靜態和動態編譯的差異等),在接下來的幾篇文章中,我將抽絲剝繭,一點一點破解CRT隱藏在台面下的一些實現細節

 

作者 死亡的飛翔

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved