• <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

            路漫漫,長修遠,我們不能沒有錢
            隨筆 - 173, 文章 - 0, 評論 - 257, 引用 - 0
            數據加載中……

            囫圇C語言(轉自天涯)



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

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

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

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

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

            国产成人久久777777| 久久久久亚洲AV综合波多野结衣| 国产成人久久777777| 久久国产精品一区二区| 人妻无码精品久久亚瑟影视| AV无码久久久久不卡蜜桃| 久久亚洲欧美国产精品| 久久www免费人成看片| 亚洲欧洲中文日韩久久AV乱码| 久久国产免费| 久久婷婷五月综合成人D啪| 国产亚洲精久久久久久无码AV| 久久免费国产精品一区二区| 国产精品一久久香蕉产线看| 99久久精品国产高清一区二区| 久久99精品久久久久子伦| 国产三级久久久精品麻豆三级| 97久久久精品综合88久久| 大伊人青草狠狠久久| 久久综合狠狠综合久久激情 | 久久久精品一区二区三区| 91精品久久久久久无码| 国产精品综合久久第一页| 久久国产美女免费观看精品| 久久久精品久久久久影院| 午夜精品久久久久久久| 久久综合丝袜日本网| 狠狠色伊人久久精品综合网| 久久精品国产亚洲7777| 亚洲国产视频久久| 久久久久亚洲AV无码专区体验| 久久免费小视频| 久久久久噜噜噜亚洲熟女综合| 久久99九九国产免费看小说| 色综合久久综合中文综合网| 久久青青草原国产精品免费| 精品久久久久久99人妻| 亚洲AV成人无码久久精品老人 | 狠狠狠色丁香婷婷综合久久俺| 久久青草国产手机看片福利盒子| 久久影院午夜理论片无码|