• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            Khan's Notebook GCC/GNU/Linux Delphi/Window Java/Anywhere

            路漫漫,長修遠(yuǎn),我們不能沒有錢
            隨筆 - 173, 文章 - 0, 評論 - 257, 引用 - 0
            數(shù)據(jù)加載中……

            囫圇C語言(轉(zhuǎn)自天涯)



            作者:deepgray?提交日期:2007-1-10 09:03:00
                寫在前面的話
                
                寫本文的目的有:
                1. 獻(xiàn)給三躍媽
                2. 筆者自認(rèn)為至少將脫離技術(shù)道路若干年甚至從此脫離,寫此文以作紀(jì)念
                3. 目前的教科書有一些嚴(yán)重的問題影響了很多的人,至少筆者學(xué)習(xí)的道路比較坎坷
                4. 希望本文能夠給一些想了解嵌入式的有志青年
                5. 計(jì)算機(jī)的書籍很少把一些東西攪合到一起說,講操作系統(tǒng)的書專講操作系統(tǒng),講 C 語言的書專講語法,甚至有些程序員寫了5年程序不能把自己的程序的來龍去脈講清楚。C是把這些問題串到一起的切入點(diǎn)。
                
                本文的標(biāo)題也意味著其中沒有過多的細(xì)節(jié)講解,也不是一章講解C語言的教程。(其實(shí)是筆者歲數(shù)大了懶得翻閱資料了,筆者已經(jīng)30高齡了,希望大家諒解)。另外筆者希望讀者有 C 語言的基礎(chǔ),了解至少一種 CPU 的匯編語言。
                
                另外,目前關(guān)于 C 語言是不是過時(shí)的爭論很多,我不想?yún)⑴c這些爭論,正所謂蘿卜白菜各有所愛。
              

            ???   囫圇C語言(一):可執(zhí)行文件的結(jié)構(gòu)和加載
                
                看到這個(gè)標(biāo)題很多人可能想,老家伙老糊涂了。可執(zhí)行文件的結(jié)構(gòu)和 C 語言有什么關(guān)系。
                
                我們先來看一個(gè)程序:
                
                /////////////////////////////////////////////////////////////////
                
                int global_a = 0x5; /* 01 */
                int global_b; /* 02 */
                 /* 03 */
                int main() /* 04 */
                { /* 05 */
                 char *q = "123456789"; /* 06 */
                 /* 07 */
                 q[3] = 'A'; /* 08 */
                 /* 09 */
                 global_a = 0xaaaaaaaa; /* 10 */
                 global_b = 0xbbbbbbbb; /* 11 */
                 /* 12 */
                // strcmp(q, NULL); /* 13 */
                 return 0x0; /* 14 */
                } /* 15 */
                
                1. 你能說出程序中出現(xiàn)的變量和常量在可執(zhí)行程序的哪個(gè)段中么?
                2. 程序運(yùn)行的結(jié)果是什么?
                
                /////////////////////////////////////////////////////////////////
                
                能正確回答上面問題者,此節(jié)可以跳過不讀:
                
                如果有人問筆者第一個(gè)問題,筆者會響亮的回答:“不知道”!。因?yàn)槟銢]告訴我目標(biāo) CPU,編譯器,鏈接器。
                如果有人問筆者第二個(gè)問題,筆者會更響亮的回答:“不知道”!。因?yàn)槟銢]告訴我鏈接器,鏈接參數(shù),目標(biāo)操作系統(tǒng)。
                
                比如 "123456789" 在某些編譯環(huán)境下出現(xiàn)在 ".text" 中,某些編譯環(huán)境下出現(xiàn)在 ".data" 中。
                再比如,如果用 VC6.0 環(huán)境,編譯時(shí)加上 /GF 選項(xiàng),該程序會崩潰(第 8 行)。
                再比如第 13 行,這種錯(cuò)誤極為愚蠢,但是在某些操作系統(tǒng)下居然執(zhí)行得挺順利,至少不會崩潰(一種HP的UNIX操作系統(tǒng)上,可惜筆者沒有留意版本號)。
                
                 所以 C 程序嚴(yán)重依賴于,CPU,編譯器,鏈接器,操作系統(tǒng)。正是因?yàn)檫@種不確定性,所以為了保證你寫的程序能在各種環(huán)境下運(yùn)行,或者你想能夠在任何環(huán)境下 debug 你的 C 程序。你必須知道可執(zhí)行文件的格式和操作系統(tǒng)如何加載。否則當(dāng)你在介紹自己的時(shí)候,只能使用類似:“我是X86平臺上,VC6.0集成開發(fā)環(huán)境下的 C 語言高手” 之類的描述。頗為尷尬。
                
                為了說明方便我們的討論建立在一套虛擬的環(huán)境上。當(dāng)然了這僅限于宏觀的討論,一些具體的例子我會給出我調(diào)試所用的環(huán)境。我們假設(shè)虛擬環(huán)境滿足下列條件:
                1. 足夠物理內(nèi)存
                2. 操作系統(tǒng)不允許缺頁中斷
                3. 物理頁面 4K
                4. 二級頁表映射
                5. 4G 虛擬地址空間
                6. 操作系統(tǒng)不支持 swap 機(jī)制
                7. I/O 使用獨(dú)立的地址空間
                8. 有若干通用寄存器 r0,r1,r2,r3,......
                9. 函數(shù)的返回值放在 r0 中
                10. 單 CPU
                
                (哈哈,沒有具體的環(huán)境,我說錯(cuò)了也沒人知道)
                
                言歸正傳,過于古老的文件結(jié)構(gòu)我們不提(入門的格式請參考 a.out 格式)。現(xiàn)在比較常用的文件格式是 ELF 和 PE/COFF。嵌入式方面 ELF 比較主流。
                
                 可執(zhí)行文件基本上的結(jié)構(gòu)如下圖:
                
                 +----------------------------------+
                 | |
                 | 文件頭 |
                 | |
                 +----------------------------------+
                 | |
                 | 段描述表 |
                 | |
                 +----------------------------------+
                 | |
                 | 段1 |
                 | |
                 +----------------------------------+
                 | |
                 | : |
                 | |
                 +----------------------------------+
                 | |
                 | 段n |
                 | |
                 +----------------------------------+
                
                 其中這些段中常見的段有 .text,.rodata,.rwdata,.bss。還有一些段因?yàn)榫幾g器和文件格式有細(xì)微差別我們不再一一說明。
                 參考:1. Executable and Linkable Format Specification
                 2. PE/COFF Sepcification
                
                 .text:正文段,也稱為程序段,可執(zhí)行的代碼
                 .rodata:只讀數(shù)據(jù)段,存放只讀數(shù)據(jù)
                 .rwdata:可讀寫數(shù)據(jù)段,
                 .bss段:未初始化數(shù)據(jù) (下文詳述)
                
                 有了虛擬的環(huán)境就好蒙了:就上面的例子來說,我們先回答第一個(gè)問題:
                 1. a 在 .rwdata 中
                 2. b 在 .bss 中
                 3. q 程序運(yùn)行的時(shí)候從 stack 中分配
                 4. 'A',0x5,0xaaaaaaaa,0xbbbbbbbb,0x0 在 .text 段。
                 5. "123456789" 在 .rodata 中
                
                 第二個(gè)問題,程序在第 8 行會崩潰。程序?yàn)槭裁磿罎⒛兀恳卮疬@個(gè)問題我們要知道可執(zhí)行程序的加載。
                
                 可執(zhí)行程序的加載
                
                 當(dāng)操作系統(tǒng)裝載一個(gè)可執(zhí)行文件的時(shí)候,首先操作系統(tǒng)盤但該文件是否是一個(gè)合法的可執(zhí)行文件。如果是操作系統(tǒng)將按照段表中的指示為可執(zhí)行程序分配地址空間。操作系統(tǒng)的內(nèi)存管理十分復(fù)雜,我們不在這里討論。
                
                就上面的例子來說可執(zhí)行文件在磁盤中的 layout 如下:(假設(shè)程序的虛擬地址從 0x00400000 開始,該平臺的頁面大小是 4K)
                
                 +----------------------------------+
                 | |
                 | 文件頭 |
                 | |
                 +----------------------------------+------------------
                 | .text 描述 | ^
                 | 虛擬地址起始位置 : 0x00400000 | |
                 | 占用虛擬空間大小 : 0x00001000 | |
                 | 實(shí)際大小 : 0x00000130 | |
                 | 屬性 :執(zhí)行/只讀 | |
                 +----------------------------------+ |
                 | .rwdata 描述 | |
                 | 虛擬地址起始位置 : 0x00401000 | |
                 | 占用虛擬空間大小 : 0x00001000 |
                 | 實(shí)際大小 : 0x00000004 | 段描述表
                 | 屬性 :讀寫 | |
                 +----------------------------------+
                 | .rodata 描述 | |
                 | 虛擬地址起始位置 : 0x00402000 | |
                 | 占用虛擬空間大小 : 0x00001000 | |
                 | 實(shí)際大小 : 0x0000000A | |
                 | 屬性 :只讀 | |
                 +----------------------------------+ |
                 | .bss 描述 | |
                 | 虛擬地址起始位置 : 0x00403000 | |
                 | 占用虛擬空間大小 : 0x00001000 | |
                 | 實(shí)際大小 : 0x00000000 | |
                 | 屬性 :讀寫 | v
                 +----------------------------------+-----------------
                 | |
                 | .text 段 | <- 4K對齊,不滿補(bǔ) 0
                 | |
                 +----------------------------------+-----------------
                 |0x5 |
                 | .rwdata 段 | <- 4K對齊,不滿補(bǔ) 0
                 | |
                 +----------------------------------+-----------------
                 |123456789 |
                 | .rodata 段 | <- 4K對齊,不滿補(bǔ) 0
                 | |
                 +----------------------------------+-----------------
                
                 請注意,.bss 段僅僅有描述,在文件中并不存在。為什么呢?.bss 專用于存放未初始化的數(shù)據(jù)。因?yàn)槲闯跏蓟臄?shù)據(jù)缺省是 0,所以只需要標(biāo)記出長度就可以了。操作系統(tǒng)會在加載的時(shí)候?yàn)樗峙淝?0 的頁面。這種技術(shù)好像叫做 ZFOD (Zero Filled On Demand)。
                
                操作系統(tǒng)首先將文件讀入物理頁面中(物理頁面的管理比較復(fù)雜,不屬于本文討論的范圍),反正大家就認(rèn)為操作系統(tǒng)找到了一批空閑的物理頁面,將可執(zhí)行文件全部裝載。如圖:
                
                 :
                 +----------------------------------+ <---- 物理頁面對齊
                 | |
                 | .text 段 |
                 | |
                 +----------------------------------+
                 :
                 :
                 +----------------------------------+ <---- 物理頁面對齊
                 |0x5 |
                 | .rwdata 段 |
                 | |
                 +----------------------------------+
                 :
                 :
                 +----------------------------------+ <---- 物理頁面對齊
                 |123456789 |
                 | .rodata 段 |
                 | |
                 +----------------------------------+
                 :
                 :
                
                在物理地址中,這幾個(gè)段并不連續(xù),順序也不能保證,甚至如果一個(gè)段占用幾個(gè)頁面的時(shí)候,段內(nèi)的連續(xù)性和順序都不能保證。實(shí)際上我們也不程序關(guān)心在物理內(nèi)存中的 layout。只需要頁面對齊即可。
                
                 最后操作系統(tǒng)為程序創(chuàng)建虛擬地址空間,并建立虛擬地址-物理地址映射(虛擬地址的管理十分復(fù)雜,反正大就認(rèn)為映射建好了。另外:注意我們的假設(shè),系 統(tǒng)不支持缺頁機(jī)制和 swap 機(jī)制,否則沒有這么簡單)。然后我們從虛擬地址空間看來,程序的 layout 如下圖:
                
                 +----------------------------------+ 0x00400000
                 | |
                 | .text 段 |
                 | |
                 +----------------------------------+ 0x00401000
                 |0x5 |
                 | .rwdata 段 |
                 | |
                 +----------------------------------+ 0x00402000
                 |123456789 |
                 | .rodata 段 |
                 | |
                 +----------------------------------+ 0x00403000
                 | |
                 | .bss 段 |
                 | |
                 +----------------------------------+
                
                同時(shí)操作系統(tǒng)會根據(jù)段的屬性設(shè)置頁面的屬性,這就是為什么通常程序的段是頁面對齊的,因?yàn)闄C(jī)器只能以頁面為單位設(shè)置屬性。
                
                 所以第二個(gè)問題自然就有了答案。程序會 crash。因?yàn)?.rodata 段所屬的頁面是只讀的。其實(shí)有些編譯器會將常量 "123456789" 放在 ".text" 中,其實(shí)是一樣的,兩個(gè)段都是只讀的,寫操作都會導(dǎo)致非法訪問,甚至同一種編譯器,不同的變異參數(shù),這個(gè)常量也會出現(xiàn)在不同的位置。實(shí)際上這個(gè)保護(hù)由編譯 器,鏈接器,操作系統(tǒng),CPU串通好了,共同完成的。
                
                所以說計(jì)算機(jī)有些具體問題并沒有一定之規(guī),但是他們基本的原理是一樣的。我們掌握了基本原理,具體問題可以具體分析。

                囫圇C語言(二):陷阱,中斷和異常
                
                上一章懷疑筆者老糊涂的讀者,看到這個(gè)標(biāo)題,基本上已經(jīng)打消了疑慮:老家伙確實(shí)糊涂了。這三個(gè)概念和C語言有什么關(guān)系呢?
                
                中斷這個(gè)詞恐怕人民群眾都不陌生。很多人把中斷分為兩種:硬件中斷和軟件中斷。其實(shí)怎么叫關(guān)系都不大,關(guān)鍵是我們要明白他們之間的異同點(diǎn)。
                
                筆者本身比較喜歡把 “中斷”,分為三種即陷阱,中斷和異常,我似乎記得Intel是這么劃分的(這句話我不保證正確啊,有興趣的讀者自己看一下 Intel 的手冊)。他們的英文分別是 trap,interrupt 和 exception。
                
                陷阱 (trap):
                 大家都知道,現(xiàn)代的CPU都是有優(yōu)先級概念的,用戶程序運(yùn)行在低優(yōu)先級,操作系統(tǒng)運(yùn)行在高優(yōu)先級。高優(yōu)先級的一些指令低優(yōu)先級無法執(zhí)行。有一些操作 只能由操作系統(tǒng)來執(zhí)行,用戶想要執(zhí)行這些操作的時(shí)候就要通知操作系統(tǒng),讓操作系統(tǒng)來執(zhí)行。用戶態(tài)的程序就是用這種方法來通知操作系統(tǒng)的。
                
                 具體怎樣做的呢?操作系統(tǒng)會把這些功能編號,比如向一個(gè)端口寫一個(gè)字符的功能調(diào)用編號 12,有兩個(gè)參數(shù),端口號 port 和寫入的字符 bytevalue。我們可以如下實(shí)現(xiàn):(這個(gè)例子無法編譯,但是這種匯編和 C 混合編程的風(fēng)格微軟的編譯器支持,十分好用,順便夸一句微軟,他們的編譯器是我用過得最優(yōu)秀的商業(yè)編譯器)
                
                int outb(int port, int bytevalue)
                {
                 __asm mov r0, 12; /* 功能號 */
                 __asm mov r1, port; /* 參數(shù) port */
                 __asm mov r2, bytevalue; /* 參數(shù) bytevalue */
                 __asm trap /* 陷入內(nèi)核 */
                
                 return r0; /* 返回值 */
                }
                
                在操作系統(tǒng)的 trap 處理的 handler 里面,相信大家已經(jīng)知道怎么辦了。有些敏感的讀者可能已經(jīng)明白了,原來一部分 C 的庫函數(shù)是用這種方法實(shí)現(xiàn)的。
                
                中斷:
                 中斷我們這里專指來自于硬件的中斷,通常分為電平觸發(fā)和邊沿觸發(fā)(請參考數(shù)字電路)。簡單的說就是 CPU 每執(zhí)行完一條都去檢測一條管腿的電平是否變化。如果滿足條件,CPU 轉(zhuǎn)向事先注冊好的函數(shù)。系統(tǒng)中最重要的一個(gè)中斷就是我們經(jīng)常說的時(shí)鐘中斷。為什么要說這個(gè)呢?這和 C 程序有什么關(guān)系呢?書上說了中斷是由操作系統(tǒng)處理的,操作系統(tǒng)會保存程序的現(xiàn)場啊,用戶程序根本感覺不到中斷的存在啊。書上說得沒錯(cuò),但是它有兩件事情沒 有告訴你:
                1. 線程調(diào)度策略。
                2. 程序的現(xiàn)場不包括什么?
                
                這里筆者想插一句話表達(dá)對國內(nèi)操作系統(tǒng)教材作者的敬仰,他們是怎么把操作系統(tǒng)拆成一塊一塊兒的呢?因?yàn)椋M(jìn)程管理,線程調(diào)度,內(nèi)存管理,中斷管理,IPC,都是互相關(guān)聯(lián)的。筆者十分懷疑分塊討論的意義到底有多大。
                
                言歸正傳,先回答第一個(gè)問題,線程調(diào)度時(shí)機(jī)。在哪些情況下操作系統(tǒng)會運(yùn)行 scheduler 呢?現(xiàn)代操作系統(tǒng)調(diào)度的基本單位都是線程,所以我們不討論進(jìn)程的概念。
                
                1. 一些系統(tǒng)調(diào)用
                2. I/O 操作
                3. 一個(gè)線程創(chuàng)建
                4. 一個(gè)線程結(jié)束
                5. mutex lock
                6. P semaphore
                7. 硬件中斷 / 時(shí)鐘中斷
                8. 主動(dòng)放棄 CPU,比如 sleep(), yield()
                9. 給另外一個(gè)線程發(fā)消息,信號
                10. 主動(dòng)喚醒另外一個(gè)線程
                11. 進(jìn)程結(jié)束
                :
                :
                歡迎大家來電來函補(bǔ)充 (我記不住那么多了)
                
                第二個(gè)問題,現(xiàn)場不包括什么。至少不包括全局變量。
                
                于是就有了一個(gè)經(jīng)典的面試題:
                
                
                int a;
                
                void thread_1()
                {
                 for (;;)
                 {
                 do something;
                 a++;
                 }
                }
                
                void thread_2()
                {
                 for (;;)
                 {
                 do something;
                 a--;
                 }
                }
                
                main()
                {
                 create_thread(thread_1);
                 create_thread(thread_2);
                }
                
                現(xiàn)在大家應(yīng)該明白這種寫法的錯(cuò)誤了吧。因?yàn)?a++,a--,并不是一條匯編語言,它會被中斷打斷,而中斷又會引起線程調(diào)度。有可能將另外一個(gè)線程投入運(yùn)行。所以結(jié)果是無法預(yù)測的。討論這個(gè)問題的文章很多,筆者也就不多費(fèi)口舌了。
                
                提個(gè)思考題,操作系統(tǒng)內(nèi)部,中斷和中斷之間,中斷和線程之間,怎么保護(hù)臨界資源的呢?多個(gè) CPU 之間呢?
                
                異常:exception
                異常是指一條指令會引起 CPU 的不快,比如除零。有群眾說了,如果我除零錯(cuò)了,操作系統(tǒng)把我終止了不就完了,我回去改程序,改對了重新運(yùn)行不就行了么。
                
                但是有時(shí)候 CPU 希望操作系統(tǒng)能夠排除這個(gè)異常,然后 CPU 重新嘗試去執(zhí)行這條引起異常的指令。這有什么用呢?下面我給大家介紹一個(gè)十分重要的異常,缺頁異常。
                
                 大家都知道,現(xiàn)代的 CPU 都支持虛擬內(nèi)存管理,我們還是在我們的虛擬 CPU 上討論這個(gè)問題,上面我們說過了,我們的 CPU 使用 2 級頁表映射,葉面大小 4K。我實(shí)在懶得寫如何映射了,請大家參考 Intel 的手冊。因?yàn)槲覀兊闹攸c(diǎn)不在這里。看下面的語句:
                
                 char *p = (char *)malloc(100 * 1024 * 1024);
                
                 有人說,沒什么不同啊,只不過申請的內(nèi)存稍微有點(diǎn)兒多啊。但操作系統(tǒng)真地給你那么多內(nèi)存了么?如果這樣的程序來上幾個(gè),系統(tǒng)內(nèi)存豈不是早被耗光,但 實(shí)際上并沒有。所以操作系統(tǒng)采用了在我國盛行的一種機(jī)制:打白條!其實(shí)我們申請內(nèi)存的時(shí)候操作系統(tǒng)僅僅在虛空間中分配了內(nèi)存,也就是說僅僅是標(biāo)記著,這 100M的內(nèi)存歸你用,但是我先不給你,當(dāng)你真的用的時(shí)候我再給你分配,這個(gè)分配指的就是實(shí)實(shí)在在的物理頁面了。具體怎么實(shí)現(xiàn)的呢?我們看下面的語句發(fā)生 了什么?
                
                 p[0x4538] = 'A';
                
                有人疑問了,普通的賦值語句啊。沒錯(cuò),但 是這條賦值語句執(zhí)行了兩次(這可不一定啊,我沒說絕對,我只是在介紹一種機(jī)制),第一次沒成功,因?yàn)榘l(fā)生了缺頁異常,我們剛才說了操作系統(tǒng)僅僅是把這 100M 內(nèi)存分配給用戶了,但是沒有對應(yīng)真正的物理頁面。操作系統(tǒng)并沒有為 p+0x4538 所在的頁面建立頁表映射。所以缺頁異常發(fā)生了。然后操作系統(tǒng)一看這個(gè)地址是已經(jīng)分配給你了,我給你找個(gè)物理頁面,給你建立好映射,你再執(zhí)行一次試試。就這 一點(diǎn)來說,操作系統(tǒng)比我們的某些官老爺信譽(yù)要良好的多,白條兌現(xiàn)了。
                
                于是第二次執(zhí)行成功了。有人看到這里已經(jīng)滿頭霧水了,這個(gè)老家伙到底想說什么?
                
                 注意到了么,操作系統(tǒng)要給他臨時(shí)找一個(gè)頁面,找不到怎么辦?對,頁面交換,找個(gè)倒霉蛋,把它的一部分頁面寫到硬盤上,實(shí)際上操作系統(tǒng)只要空閑物理頁 面少于一定的程度就會做 swap。那么,如果你有個(gè)程序需要較高的效率,較好的反應(yīng)速度,算法寫得再好也沒用,一個(gè)頁面被交換出去全完。
                
                現(xiàn)在明白了吧,優(yōu)化程序,了解操作系統(tǒng)的運(yùn)行機(jī)制是必不可少的。當(dāng)然了優(yōu)化程序絕不僅僅是這些。所以一個(gè)優(yōu)秀的程序員十分有必要知道,你的程序到底運(yùn)行在 “什么” 上面。
                
                稍微總結(jié)一下:
                陷阱:由 trap 指令引起,恢復(fù)后 CPU 執(zhí)行下一條指令
                中斷:由硬件電平引起,恢復(fù)后 CPU 執(zhí)行下一條指令
                異常:由軟件指令引起,恢復(fù)后 CPU 重新執(zhí)行該條指令
                
                有個(gè)牛人說過,Oracle 的數(shù)據(jù)庫為什么總比別人的快一點(diǎn)點(diǎn)呢?因?yàn)槟桥耸菍懖僮飨到y(tǒng)的。
                
               囫圇C語言(三):誰調(diào)用了我的 main?
                
                現(xiàn)在最重要的是要跟得上潮流,所以套用比較時(shí)髦的話,誰動(dòng)了我的 奶酪。誰調(diào)用了我的 main?不過作為計(jì)算機(jī)工作者,我勸大家還是不要趕時(shí)髦,今天Java熱,明天 .net 流行,什么時(shí)髦就學(xué)什么。我的意思是先花幾年把基本功學(xué)好,等你趕時(shí)髦的時(shí)候也好事半功倍。廢話不多說了。
                
                我們都聽說過 一句話:“main是C語言的入口”。我至今不明白為什么這么說。就好像如果有人說:“掙錢是泡妞”,肯定無數(shù)磚頭拍過來。這句話應(yīng)該是“掙錢是泡妞的一 個(gè)條件,只不過這個(gè)條件特別重要”。那么上面那句話應(yīng)該是 “main是C語言中一個(gè)符號,只不過這個(gè)符號比較特別。”
                
                我們看下面的例子:
                
                /* file name test00.c */
                
                int main(int argc, char* argv)
                {
                 return 0;
                }
                
                編譯鏈接它:
                cc test00.c -o test.exe
                會生成 test.exe
                
                但是我們加上這個(gè)選項(xiàng): -nostdlib (不鏈接標(biāo)準(zhǔn)庫)
                cc test00.c -nostdlib -o test.exe
                鏈接器會報(bào)錯(cuò):
                undefined symbol: __start
                
                也就是說:
                1. 編譯器缺省是找 __start 符號,而不是 main
                2. __start 這個(gè)符號是程序的起始點(diǎn)
                3. main 是被標(biāo)準(zhǔn)庫調(diào)用的一個(gè)符號
                
                再來思考一個(gè)問題:
                 我們寫程序,比如一個(gè)模塊,通常要有 initialize 和 de-initialize,但是我們寫 C 程序的時(shí)候?yàn)槭裁从行┠K沒有這兩個(gè)過程么呢?比如我們程序從 main 開始就可以 malloc,free,但是我們在 main 里面卻沒有初始化堆。再比如在 main 里面可以直接 printf,可是我們并沒有打開標(biāo)準(zhǔn)輸出文件啊。(不知道什么是 stdin,stdout,stderr 以及 printf 和 stdout 關(guān)系的群眾請先看看 C 語言中文件的概念)。
                
                有人說,這些東西不需要初始化。如果您真得這么想,請您不要再往下看了,我個(gè)人認(rèn)為計(jì)算機(jī)軟件不適合您。
                
                 聰明的人民群眾會想,一定是在 main 之前干了些什么。使這些函數(shù)可以直接調(diào)用而不用初始化。通常,我們會在編譯器的環(huán)境中找到一個(gè)名字類似于 crt0.o 的文件,這個(gè)文件中包含了我們剛才所說的 __start 符號。(crt 大概是 C Runtime 的縮寫,請大家?guī)椭_認(rèn)一下。)
                
                那么真正的 crt0.s 是什么樣子呢?下面我們給出部分偽代碼:
                
                ///////////////////////////////////////////////////////
                section .text:
                __start:
                
                 :
                 init stack;
                 init heap;
                 open stdin;
                 open stdout;
                 open stderr;
                 :
                 push argv;
                 push argc;
                 call _main; (調(diào)用 main)
                 :
                 destory heap;
                 close stdin;
                 close stdout;
                 close stderr;
                 :
                 call __exit;
                ////////////////////////////////////////////////////
                
                實(shí)際上可能還有很多初始化工作,因?yàn)槎际呛筒僮飨到y(tǒng)相關(guān)的,筆者就不一一列出了。
                
                注意:
                1. 不同的編譯器,不一定缺省得符號都是 __start。
                2. 匯編里面的 _main 就是 C 語言里面的 main,是因?yàn)閰R編器和C編譯器對符號的命名有差異(通常是差一個(gè)下劃線'_')。
                 3. 目前操作系統(tǒng)結(jié)構(gòu)有兩個(gè)主要的分支:微內(nèi)核和宏內(nèi)核。微內(nèi)核的優(yōu)點(diǎn)是,結(jié)構(gòu)清晰,簡單,內(nèi)核組件較少,便于維護(hù);缺點(diǎn)是,進(jìn)程間通信較多,程序頻繁進(jìn)出內(nèi) 核,效率較低。宏內(nèi)核正好相反。我說這個(gè)是什么目的是:沒辦法保證每個(gè)組件都在用戶空間(標(biāo)準(zhǔn)庫函數(shù))中初始化,有些組件確實(shí)可能不要初始化,操作系統(tǒng)在 創(chuàng)建進(jìn)程的時(shí)候在內(nèi)核空間做的。這依賴于操作系統(tǒng)的具體實(shí)現(xiàn),比如堆,宏內(nèi)核結(jié)構(gòu)可能在內(nèi)核初始化,微內(nèi)核結(jié)構(gòu)在用戶空間;即使同樣是微內(nèi)核,這個(gè)東東也 可能會被拿到內(nèi)核空間初始化。
                
                隨著 CPU 技術(shù)的發(fā)展,存儲量的迅速擴(kuò)展,代碼復(fù)雜程度的增加,微內(nèi)核被越來越多的采用。你會為了 10% 的效率使代碼復(fù)雜度增加么?要知道每隔 18 個(gè)月 CPU 的速度就會翻一番。所以我對程序員的要求是,我首先不要你的代碼效率高,我首先要你的代碼能讓 80% 的人迅速看懂并可以維護(hù)。

              囫圇C語言(四):static 和 volatile
              
              這兩個(gè)關(guān)鍵字其實(shí)并不沾邊。只是從英文上看他們似乎是一對兒。下面我們分別解釋這兩個(gè)關(guān)鍵字。請注意,本章的程序您要想試驗(yàn)一把,恐怕安裝一個(gè)盜版的 VC 6.0了。什么,您問我的?我的是花了 5 塊錢買的正版軟件。
              
              一:static
              
              首先,static。這個(gè)關(guān)鍵字的語法問題筆者不想多說,因?yàn)橛懻撨@個(gè)關(guān)鍵字的文章非常多。我們先看下面的一個(gè)小程序:
              
              ////////////////////////////////////////////////////////////////
              
              /*
               * 使用 Visual C++ 6.0
               * 編譯器:Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 12.00.8168 for 80x86
               * 鏈接器:Microsoft (R) Incremental Linker Version 6.00.8168
               *
               * 程序是 release 版本
               */
              
              int global_a;
              static int global_b;
              
              int main(int argc, char* argv[])
              {
               int local_a;
               static int local_b;
              
               printf("local_b is at \t%p\n", &local_b);
               printf("global_b is at \t%p\n", &global_b);
               printf("global_a is at \t%p\n", &global_a);
               printf("local_a is at \t%p\n", &local_a);
              
               return 0;
              }
              
              編譯運(yùn)行的結(jié)果是:
              local_b is at 00406910
              global_b is at 00406914
              global_a is at 00406918
              local_a is at 0012FF80
              
              ///////////////////////////////////////////////////////////////
              
               從結(jié)果我們可以很直觀地看到 global_a,global_b,local_b 在可執(zhí)行程序中的位置是連續(xù)的,根據(jù)我們前面的介紹,section是頁面對齊的,我們可以得知,這三個(gè)變量處于同一個(gè) section。這就是 static 局部變量具有“記憶功能”的根本原因,因?yàn)榫幾g器將local_b 當(dāng)作一個(gè)全局變量來看待的。而變量 local_a 才是在棧上分配的,所以他的地址離其他幾個(gè)變量很遠(yuǎn)。
              
              那么它們的差別又是什么呢?
              我們使用 Visual C++ 6.0 中自帶的 dumpbin 程序察看一下 obj 文件的符號表:
              D:\test000\Release> dumpbin /SYMBOLS test000.obj
              
              結(jié)果如下:(我只摘出了和我們論題有關(guān)系的部分)
              External | _global_a
              Static | _global_b
              Static | _?local_b@?1??main@@9@9
              
              從中我們可以看出:
              1. static 變量同樣出現(xiàn)在符號表中。
              2. static 變量的屬性和普通的全局變量不同。
              3. local_b 似乎變成了一個(gè)很奇怪的名字 _?local_b@?1??main@@9@9
              
              第一點(diǎn),只有全局變量才會出現(xiàn)在符號表中,所以毫無疑問 local_b 被編譯器看作是全局變量。
              第二點(diǎn),External屬性表明,該符號可以被其他的 obj 引用(做鏈接),static 屬性表明該符號不可以被其他的 obj 引用,所以所謂 static 變量不能被其他文件引用不僅僅是在 C 語法中做了規(guī)定,真正的保證是靠鏈接器完成的。
              第三點(diǎn),_?local_b@?1??main@@9@9 就是 local_b,鏈接器為什么要換名字呢?很簡單,如果不換名字,如果有一個(gè)全局變量與之重名怎么辦,所以這個(gè)變量的名字不但改變了,而且還有所在函數(shù)的名字作為后綴,以保證唯一性。
              
              
              二:volatile
              
               說道 volatile 這個(gè)關(guān)鍵字,恐怕有些人會想,這玩意兒是 C 語言的關(guān)鍵字么?你老兄不會忽悠俺吧?嘿嘿,這個(gè)關(guān)鍵字確實(shí)是C語言的關(guān)鍵字,只不過比較少用,他的作用很奇怪:編譯器,不要給我優(yōu)化用這個(gè)關(guān)鍵字修飾的 符號所涉及的程序。有看官會說這不是神經(jīng)病么?讓編譯器優(yōu)化有什么不好,我巴不得自己用的編譯器優(yōu)化算法世界第一呢。
              
              好,我們來看幾個(gè)例子:(Microsoft Visual C++ 6.0, 程序編譯成 release 版本)
              
              例一:
              
              /*
               * 第一個(gè)程序
               */
              int main(int argc, char* argv[])
              {
               int i;
              
               for (i = 0; i < 0xAAAA; i++);
              
               return 0;
              }
              
              /*
               * 匯編碼
               */
              _main:
              00401011 xor eax,eax
              00401013 ret
              
              通過觀察這個(gè)程序的匯編碼我們發(fā)現(xiàn),編譯器發(fā)現(xiàn)程序的執(zhí)行結(jié)果不會影響任何寄存器變量,就將這個(gè)循環(huán)優(yōu)化掉了,我們在匯編碼里面沒有看到任何和循環(huán)有關(guān)的部分。這兩句匯編碼僅僅相當(dāng)于 return 0;
              
              /*
               * 第二個(gè)程序
               */
              int main(int argc, char* argv[])
              {
               volatile int i;
              
               for (i = 0; i < 0xAAAA; i++);
              
               return 0;
              }
              
              /*
               * 匯編碼
               */
              _main:
              00401010 push ebp
              00401011 mov ebp,esp
              00401013 push ecx
              00401015 mov dword ptr [ebp-4],0
              0040101C mov eax,0AAAAh
              00401021 mov ecx,dword ptr [ebp-4]
              00401024 cmp ecx,eax
              00401026 jge _main+26h (00401036)
              00401028 mov ecx,dword ptr [ebp-4]
              0040102B inc ecx
              0040102C mov dword ptr [ebp-4],ecx
              0040102F mov ecx,dword ptr [ebp-4]
              00401032 cmp ecx,eax
              00401034 jl _main+18h (00401028)
              00401036 xor eax,eax
              00401038 mov esp,ebp
              0040103A pop ebp
              0040103B ret
              
               我們用 volatile 修飾變量 i,然后重新編譯,得到的匯編碼如上所示,這回好了,循環(huán)回來了。有人說,這有什么意義呢,這個(gè)問題問得好。在通常的應(yīng)用程序中,這個(gè)小小的延遲循環(huán)通常 沒有用,但是寫過驅(qū)動(dòng)程序的朋友都知道,有時(shí)候我們寫外設(shè)的時(shí)候,兩個(gè)命令字之間是需要一些延遲的。
              
              
              
              例二:
              
              /*
               * 第一個(gè)程序
               */
              int main(int argc, char* argv[])
              {
               int *p;
              
               p = 0x100000;
               *p = 0xCCCC;
               *p = 0xDDDD;
              
               return 0;
              }
              
              /*
               * 匯編碼
               */
              _main:
              00401011 mov eax,100000h
              00401016 mov dword ptr [eax],0DDDDh
              0040101C xor eax,eax
              0040101E ret
              
              這個(gè)程序中,編譯器認(rèn)為 *p = 0xCCCC; 沒有任何意義,所以被優(yōu)化掉了。
              
              /*
               * 第二個(gè)程序
               */
              int main(int argc, char* argv[])
              {
               volatile int *p;
              
               p = 0x100000;
               *p = 0xCCCC;
               *p = 0xDDDD;
              
               return 0;
              }
              
              /*
               * 匯編碼
               */
              _main:
              00401011 mov eax,100000h
              00401016 mov dword ptr [eax],0CCCCh
              0040101C mov dword ptr [eax],0DDDDh
              00401022 xor eax,eax
              00401024 ret
              
               重新聲明這個(gè)變量,*p = 0xCCCC; 被執(zhí)行了,同樣,這主要用于驅(qū)動(dòng)外設(shè),有的外設(shè)要求連續(xù)向一個(gè)地址寫入多個(gè)不同的數(shù)據(jù),才能外成一個(gè)完整的操作。有的群眾迷惑了,為啥驅(qū)動(dòng)外設(shè)要寫內(nèi)存 啊?我估計(jì)那是您僅僅了解 Intel 的 CPU,Intel 的CPU 外設(shè)地址和內(nèi)存地址分開,訪問外設(shè)的時(shí)候使用特殊指令,比如 inb, outb。但有一些 CPU 比如 ARM,外設(shè)是和內(nèi)存混合編址的,訪問外設(shè)看上去就像讀寫普通的內(nèi)存。具體細(xì)節(jié)請參考 Intel 和 ARM 的手冊。
              
              大家注意到一個(gè)精彩的細(xì)節(jié)了么,在例二中編譯器發(fā)現(xiàn)一個(gè)寄存器 eax 可以完成整個(gè)函數(shù),所以并沒有給變量 p 在棧上分配內(nèi)存。省略了棧的操作,節(jié)省了不少時(shí)間,通常棧的操作使用 5 條以上的匯編碼。
              
              又有群眾反映了,你說的兩個(gè)例子,都是寫驅(qū)動(dòng)程序用到的,我寫應(yīng)用程序是不是就沒必要知道了呢?不是,請您繼續(xù)看下面的例子:
              
              例三:
              
              int run = 1;
              
              void t1()
              {
               run = 0;
              }
              
              void t2()
              {
               while (run)
               {
               printf("error .\n");
               }
              }
              
              int main(int argc, char* argv[])
              {
               t1();
               t2();
              
               return 0;
              }
              
               這個(gè)程序乍看沒什么問題,在某些編譯器,或者某些優(yōu)化參數(shù)下,while (run)會被優(yōu)化成一個(gè)死循環(huán),是因?yàn)榫幾g器不知道會有外部的線程要改變這個(gè)變量,當(dāng)他看到 run 的定義時(shí),認(rèn)為這個(gè)循環(huán)永遠(yuǎn)不會退出,所以直接將這個(gè)循環(huán)優(yōu)化成了死循環(huán)。解決的辦法是用 volatile 聲明變量 i。
              
              我手頭的編譯器這個(gè)例子不會出錯(cuò),我也懶得找讓這個(gè)例子成立的編譯器了,大家自己動(dòng)手試驗(yàn)一下看看吧。
              
              如果大家靜下心來讀上一些匯編碼就會發(fā)現(xiàn),微軟的編譯器經(jīng)常會有一些精彩的表現(xiàn)。哎,拍微軟的馬屁也沒用,給微軟發(fā)過簡歷,連面試的機(jī)會都沒給,就被拒了,太郁悶了!
              

            posted on 2007-03-02 09:30 Khan 閱讀(1527) 評論(0)  編輯 收藏 引用 所屬分類: GCC/G++跨平臺開發(fā)

            久久久久久精品成人免费图片 | 国产成年无码久久久久毛片| 欧洲成人午夜精品无码区久久| 国产精品无码久久综合| 久久se精品一区二区影院| 东方aⅴ免费观看久久av| 国产精品九九久久精品女同亚洲欧美日韩综合区 | 久久久精品国产亚洲成人满18免费网站| 亚洲精品乱码久久久久久蜜桃不卡| 精品免费tv久久久久久久| 色老头网站久久网| 久久久久久久尹人综合网亚洲 | 99国产欧美精品久久久蜜芽 | 国产美女久久精品香蕉69| 性高朝久久久久久久久久| 久久精品国内一区二区三区| 久久综合亚洲鲁鲁五月天| 久久久久国产一级毛片高清板| 国产成年无码久久久久毛片| 狠狠色狠狠色综合久久| 亚洲精品久久久www| 久久国产免费| 精品久久久久久无码人妻热| 久久99免费视频| 韩国三级大全久久网站| 久久久久久久精品妇女99| 狠狠精品久久久无码中文字幕| 久久亚洲私人国产精品| 久久国语露脸国产精品电影| 欧美伊人久久大香线蕉综合69| 久久久久亚洲爆乳少妇无| 99热成人精品热久久669| 亚洲精品无码成人片久久| 亚洲欧洲中文日韩久久AV乱码| 97精品伊人久久久大香线蕉| 国产精品一区二区久久不卡| 99久久精品日本一区二区免费| 97久久国产亚洲精品超碰热| 国产午夜精品久久久久免费视 | 中文字幕乱码久久午夜| 久久九九兔免费精品6|