囫圇C語言(轉自天涯)
|
作者:deepgray?提交日期:2007-1-10 09:03:00 |
|
寫本文的目的有:
1. 獻給三躍媽
2. 筆者自認為至少將脫離技術道路若干年甚至從此脫離,寫此文以作紀念
3. 目前的教科書有一些嚴重的問題影響了很多的人,至少筆者學習的道路比較坎坷
4. 希望本文能夠給一些想了解嵌入式的有志青年
5. 計算機的書籍很少把一些東西攪合到一起說,講操作系統(tǒng)的書專講操作系統(tǒng),講 C 語言的書專講語法,甚至有些程序員寫了5年程序不能把自己的程序的來龍去脈講清楚。C是把這些問題串到一起的切入點。
本文的標題也意味著其中沒有過多的細節(jié)講解,也不是一章講解C語言的教程。(其實是筆者歲數(shù)大了懶得翻閱資料了,筆者已經30高齡了,希望大家諒解)。另外筆者希望讀者有 C 語言的基礎,了解至少一種 CPU 的匯編語言。
另外,目前關于 C 語言是不是過時的爭論很多,我不想參與這些爭論,正所謂蘿卜白菜各有所愛。
??? 囫圇C語言(一):可執(zhí)行文件的結構和加載
看到這個標題很多人可能想,老家伙老糊涂了。可執(zhí)行文件的結構和 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. 你能說出程序中出現(xiàn)的變量和常量在可執(zhí)行程序的哪個段中么?
2. 程序運行的結果是什么?
/////////////////////////////////////////////////////////////////
能正確回答上面問題者,此節(jié)可以跳過不讀:
如果有人問筆者第一個問題,筆者會響亮的回答:“不知道”!。因為你沒告訴我目標 CPU,編譯器,鏈接器。
如果有人問筆者第二個問題,筆者會更響亮的回答:“不知道”!。因為你沒告訴我鏈接器,鏈接參數(shù),目標操作系統(tǒng)。
比如 "123456789" 在某些編譯環(huán)境下出現(xiàn)在 ".text" 中,某些編譯環(huán)境下出現(xiàn)在 ".data" 中。
再比如,如果用 VC6.0 環(huán)境,編譯時加上 /GF 選項,該程序會崩潰(第 8 行)。
再比如第 13 行,這種錯誤極為愚蠢,但是在某些操作系統(tǒng)下居然執(zhí)行得挺順利,至少不會崩潰(一種HP的UNIX操作系統(tǒng)上,可惜筆者沒有留意版本號)。
所以 C 程序嚴重依賴于,CPU,編譯器,鏈接器,操作系統(tǒng)。正是因為這種不確定性,所以為了保證你寫的程序能在各種環(huán)境下運行,或者你想能夠在任何環(huán)境下 debug 你的 C 程序。你必須知道可執(zhí)行文件的格式和操作系統(tǒng)如何加載。否則當你在介紹自己的時候,只能使用類似:“我是X86平臺上,VC6.0集成開發(fā)環(huán)境下的 C 語言高手” 之類的描述。頗為尷尬。
為了說明方便我們的討論建立在一套虛擬的環(huán)境上。當然了這僅限于宏觀的討論,一些具體的例子我會給出我調試所用的環(huán)境。我們假設虛擬環(huán)境滿足下列條件:
1. 足夠物理內存
2. 操作系統(tǒng)不允許缺頁中斷
3. 物理頁面 4K
4. 二級頁表映射
5. 4G 虛擬地址空間
6. 操作系統(tǒng)不支持 swap 機制
7. I/O 使用獨立的地址空間
8. 有若干通用寄存器 r0,r1,r2,r3,......
9. 函數(shù)的返回值放在 r0 中
10. 單 CPU
(哈哈,沒有具體的環(huán)境,我說錯了也沒人知道)
言歸正傳,過于古老的文件結構我們不提(入門的格式請參考 a.out 格式)。現(xiàn)在比較常用的文件格式是 ELF 和 PE/COFF。嵌入式方面 ELF 比較主流。
可執(zhí)行文件基本上的結構如下圖:
+----------------------------------+
| |
| 文件頭 |
| |
+----------------------------------+
| |
| 段描述表 |
| |
+----------------------------------+
| |
| 段1 |
| |
+----------------------------------+
| |
| : |
| |
+----------------------------------+
| |
| 段n |
| |
+----------------------------------+
其中這些段中常見的段有 .text,.rodata,.rwdata,.bss。還有一些段因為編譯器和文件格式有細微差別我們不再一一說明。
參考: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)境就好蒙了:就上面的例子來說,我們先回答第一個問題:
1. a 在 .rwdata 中
2. b 在 .bss 中
3. q 程序運行的時候從 stack 中分配
4. 'A',0x5,0xaaaaaaaa,0xbbbbbbbb,0x0 在 .text 段。
5. "123456789" 在 .rodata 中
第二個問題,程序在第 8 行會崩潰。程序為什么會崩潰呢?要回答這個問題我們要知道可執(zhí)行程序的加載。
可執(zhí)行程序的加載
當操作系統(tǒng)裝載一個可執(zhí)行文件的時候,首先操作系統(tǒng)盤但該文件是否是一個合法的可執(zhí)行文件。如果是操作系統(tǒng)將按照段表中的指示為可執(zhí)行程序分配地址空間。操作系統(tǒng)的內存管理十分復雜,我們不在這里討論。
就上面的例子來說可執(zhí)行文件在磁盤中的 layout 如下:(假設程序的虛擬地址從 0x00400000 開始,該平臺的頁面大小是 4K)
+----------------------------------+
| |
| 文件頭 |
| |
+----------------------------------+------------------
| .text 描述 | ^
| 虛擬地址起始位置 : 0x00400000 | |
| 占用虛擬空間大小 : 0x00001000 | |
| 實際大小 : 0x00000130 | |
| 屬性 :執(zhí)行/只讀 | |
+----------------------------------+ |
| .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 專用于存放未初始化的數(shù)據(jù)。因為未初始化的數(shù)據(jù)缺省是 0,所以只需要標記出長度就可以了。操作系統(tǒng)會在加載的時候為它分配清 0 的頁面。這種技術好像叫做 ZFOD (Zero Filled On Demand)。
操作系統(tǒng)首先將文件讀入物理頁面中(物理頁面的管理比較復雜,不屬于本文討論的范圍),反正大家就認為操作系統(tǒng)找到了一批空閑的物理頁面,將可執(zhí)行文件全部裝載。如圖:
:
+----------------------------------+ <---- 物理頁面對齊
| |
| .text 段 |
| |
+----------------------------------+
:
:
+----------------------------------+ <---- 物理頁面對齊
|0x5 |
| .rwdata 段 |
| |
+----------------------------------+
:
:
+----------------------------------+ <---- 物理頁面對齊
|123456789 |
| .rodata 段 |
| |
+----------------------------------+
:
:
在物理地址中,這幾個段并不連續(xù),順序也不能保證,甚至如果一個段占用幾個頁面的時候,段內的連續(xù)性和順序都不能保證。實際上我們也不程序關心在物理內存中的 layout。只需要頁面對齊即可。
最后操作系統(tǒng)為程序創(chuàng)建虛擬地址空間,并建立虛擬地址-物理地址映射(虛擬地址的管理十分復雜,反正大就認為映射建好了。另外:注意我們的假設,系 統(tǒng)不支持缺頁機制和 swap 機制,否則沒有這么簡單)。然后我們從虛擬地址空間看來,程序的 layout 如下圖:
+----------------------------------+ 0x00400000
| |
| .text 段 |
| |
+----------------------------------+ 0x00401000
|0x5 |
| .rwdata 段 |
| |
+----------------------------------+ 0x00402000
|123456789 |
| .rodata 段 |
| |
+----------------------------------+ 0x00403000
| |
| .bss 段 |
| |
+----------------------------------+
同時操作系統(tǒng)會根據(jù)段的屬性設置頁面的屬性,這就是為什么通常程序的段是頁面對齊的,因為機器只能以頁面為單位設置屬性。
所以第二個問題自然就有了答案。程序會 crash。因為 .rodata 段所屬的頁面是只讀的。其實有些編譯器會將常量 "123456789" 放在 ".text" 中,其實是一樣的,兩個段都是只讀的,寫操作都會導致非法訪問,甚至同一種編譯器,不同的變異參數(shù),這個常量也會出現(xiàn)在不同的位置。實際上這個保護由編譯 器,鏈接器,操作系統(tǒng),CPU串通好了,共同完成的。
所以說計算機有些具體問題并沒有一定之規(guī),但是他們基本的原理是一樣的。我們掌握了基本原理,具體問題可以具體分析。
囫圇C語言(二):陷阱,中斷和異常
上一章懷疑筆者老糊涂的讀者,看到這個標題,基本上已經打消了疑慮:老家伙確實糊涂了。這三個概念和C語言有什么關系呢?
中斷這個詞恐怕人民群眾都不陌生。很多人把中斷分為兩種:硬件中斷和軟件中斷。其實怎么叫關系都不大,關鍵是我們要明白他們之間的異同點。
筆者本身比較喜歡把 “中斷”,分為三種即陷阱,中斷和異常,我似乎記得Intel是這么劃分的(這句話我不保證正確啊,有興趣的讀者自己看一下 Intel 的手冊)。他們的英文分別是 trap,interrupt 和 exception。
陷阱 (trap):
大家都知道,現(xiàn)代的CPU都是有優(yōu)先級概念的,用戶程序運行在低優(yōu)先級,操作系統(tǒng)運行在高優(yōu)先級。高優(yōu)先級的一些指令低優(yōu)先級無法執(zhí)行。有一些操作 只能由操作系統(tǒng)來執(zhí)行,用戶想要執(zhí)行這些操作的時候就要通知操作系統(tǒng),讓操作系統(tǒng)來執(zhí)行。用戶態(tài)的程序就是用這種方法來通知操作系統(tǒng)的。
具體怎樣做的呢?操作系統(tǒng)會把這些功能編號,比如向一個端口寫一個字符的功能調用編號 12,有兩個參數(shù),端口號 port 和寫入的字符 bytevalue。我們可以如下實現(xiàn):(這個例子無法編譯,但是這種匯編和 C 混合編程的風格微軟的編譯器支持,十分好用,順便夸一句微軟,他們的編譯器是我用過得最優(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 /* 陷入內核 */
return r0; /* 返回值 */
}
在操作系統(tǒng)的 trap 處理的 handler 里面,相信大家已經知道怎么辦了。有些敏感的讀者可能已經明白了,原來一部分 C 的庫函數(shù)是用這種方法實現(xiàn)的。
中斷:
中斷我們這里專指來自于硬件的中斷,通常分為電平觸發(fā)和邊沿觸發(fā)(請參考數(shù)字電路)。簡單的說就是 CPU 每執(zhí)行完一條都去檢測一條管腿的電平是否變化。如果滿足條件,CPU 轉向事先注冊好的函數(shù)。系統(tǒng)中最重要的一個中斷就是我們經常說的時鐘中斷。為什么要說這個呢?這和 C 程序有什么關系呢?書上說了中斷是由操作系統(tǒng)處理的,操作系統(tǒng)會保存程序的現(xiàn)場啊,用戶程序根本感覺不到中斷的存在啊。書上說得沒錯,但是它有兩件事情沒 有告訴你:
1. 線程調度策略。
2. 程序的現(xiàn)場不包括什么?
這里筆者想插一句話表達對國內操作系統(tǒng)教材作者的敬仰,他們是怎么把操作系統(tǒng)拆成一塊一塊兒的呢?因為,進程管理,線程調度,內存管理,中斷管理,IPC,都是互相關聯(lián)的。筆者十分懷疑分塊討論的意義到底有多大。
言歸正傳,先回答第一個問題,線程調度時機。在哪些情況下操作系統(tǒng)會運行 scheduler 呢?現(xiàn)代操作系統(tǒng)調度的基本單位都是線程,所以我們不討論進程的概念。
1. 一些系統(tǒng)調用
2. I/O 操作
3. 一個線程創(chuàng)建
4. 一個線程結束
5. mutex lock
6. P semaphore
7. 硬件中斷 / 時鐘中斷
8. 主動放棄 CPU,比如 sleep(), yield()
9. 給另外一個線程發(fā)消息,信號
10. 主動喚醒另外一個線程
11. 進程結束
:
:
歡迎大家來電來函補充 (我記不住那么多了)
第二個問題,現(xiàn)場不包括什么。至少不包括全局變量。
于是就有了一個經典的面試題:
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)在大家應該明白這種寫法的錯誤了吧。因為 a++,a--,并不是一條匯編語言,它會被中斷打斷,而中斷又會引起線程調度。有可能將另外一個線程投入運行。所以結果是無法預測的。討論這個問題的文章很多,筆者也就不多費口舌了。
提個思考題,操作系統(tǒng)內部,中斷和中斷之間,中斷和線程之間,怎么保護臨界資源的呢?多個 CPU 之間呢?
異常:exception
異常是指一條指令會引起 CPU 的不快,比如除零。有群眾說了,如果我除零錯了,操作系統(tǒng)把我終止了不就完了,我回去改程序,改對了重新運行不就行了么。
但是有時候 CPU 希望操作系統(tǒng)能夠排除這個異常,然后 CPU 重新嘗試去執(zhí)行這條引起異常的指令。這有什么用呢?下面我給大家介紹一個十分重要的異常,缺頁異常。
大家都知道,現(xiàn)代的 CPU 都支持虛擬內存管理,我們還是在我們的虛擬 CPU 上討論這個問題,上面我們說過了,我們的 CPU 使用 2 級頁表映射,葉面大小 4K。我實在懶得寫如何映射了,請大家參考 Intel 的手冊。因為我們的重點不在這里。看下面的語句:
char *p = (char *)malloc(100 * 1024 * 1024);
有人說,沒什么不同啊,只不過申請的內存稍微有點兒多啊。但操作系統(tǒng)真地給你那么多內存了么?如果這樣的程序來上幾個,系統(tǒng)內存豈不是早被耗光,但 實際上并沒有。所以操作系統(tǒng)采用了在我國盛行的一種機制:打白條!其實我們申請內存的時候操作系統(tǒng)僅僅在虛空間中分配了內存,也就是說僅僅是標記著,這 100M的內存歸你用,但是我先不給你,當你真的用的時候我再給你分配,這個分配指的就是實實在在的物理頁面了。具體怎么實現(xiàn)的呢?我們看下面的語句發(fā)生 了什么?
p[0x4538] = 'A';
有人疑問了,普通的賦值語句啊。沒錯,但 是這條賦值語句執(zhí)行了兩次(這可不一定啊,我沒說絕對,我只是在介紹一種機制),第一次沒成功,因為發(fā)生了缺頁異常,我們剛才說了操作系統(tǒng)僅僅是把這 100M 內存分配給用戶了,但是沒有對應真正的物理頁面。操作系統(tǒng)并沒有為 p+0x4538 所在的頁面建立頁表映射。所以缺頁異常發(fā)生了。然后操作系統(tǒng)一看這個地址是已經分配給你了,我給你找個物理頁面,給你建立好映射,你再執(zhí)行一次試試。就這 一點來說,操作系統(tǒng)比我們的某些官老爺信譽要良好的多,白條兌現(xiàn)了。
于是第二次執(zhí)行成功了。有人看到這里已經滿頭霧水了,這個老家伙到底想說什么?
注意到了么,操作系統(tǒng)要給他臨時找一個頁面,找不到怎么辦?對,頁面交換,找個倒霉蛋,把它的一部分頁面寫到硬盤上,實際上操作系統(tǒng)只要空閑物理頁 面少于一定的程度就會做 swap。那么,如果你有個程序需要較高的效率,較好的反應速度,算法寫得再好也沒用,一個頁面被交換出去全完。
現(xiàn)在明白了吧,優(yōu)化程序,了解操作系統(tǒng)的運行機制是必不可少的。當然了優(yōu)化程序絕不僅僅是這些。所以一個優(yōu)秀的程序員十分有必要知道,你的程序到底運行在 “什么” 上面。
稍微總結一下:
陷阱:由 trap 指令引起,恢復后 CPU 執(zhí)行下一條指令
中斷:由硬件電平引起,恢復后 CPU 執(zhí)行下一條指令
異常:由軟件指令引起,恢復后 CPU 重新執(zhí)行該條指令
有個牛人說過,Oracle 的數(shù)據(jù)庫為什么總比別人的快一點點呢?因為那批人是寫操作系統(tǒng)的。
囫圇C語言(三):誰調用了我的 main?
現(xiàn)在最重要的是要跟得上潮流,所以套用比較時髦的話,誰動了我的 奶酪。誰調用了我的 main?不過作為計算機工作者,我勸大家還是不要趕時髦,今天Java熱,明天 .net 流行,什么時髦就學什么。我的意思是先花幾年把基本功學好,等你趕時髦的時候也好事半功倍。廢話不多說了。
我們都聽說過 一句話:“main是C語言的入口”。我至今不明白為什么這么說。就好像如果有人說:“掙錢是泡妞”,肯定無數(shù)磚頭拍過來。這句話應該是“掙錢是泡妞的一 個條件,只不過這個條件特別重要”。那么上面那句話應該是 “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 之前干了些什么。使這些函數(shù)可以直接調用而不用初始化。通常,我們會在編譯器的環(huán)境中找到一個名字類似于 crt0.o 的文件,這個文件中包含了我們剛才所說的 __start 符號。(crt 大概是 C Runtime 的縮寫,請大家?guī)椭_認一下。)
那么真正的 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;
////////////////////////////////////////////////////
實際上可能還有很多初始化工作,因為都是和操作系統(tǒng)相關的,筆者就不一一列出了。
注意:
1. 不同的編譯器,不一定缺省得符號都是 __start。
2. 匯編里面的 _main 就是 C 語言里面的 main,是因為匯編器和C編譯器對符號的命名有差異(通常是差一個下劃線'_')。
3. 目前操作系統(tǒng)結構有兩個主要的分支:微內核和宏內核。微內核的優(yōu)點是,結構清晰,簡單,內核組件較少,便于維護;缺點是,進程間通信較多,程序頻繁進出內 核,效率較低。宏內核正好相反。我說這個是什么目的是:沒辦法保證每個組件都在用戶空間(標準庫函數(shù))中初始化,有些組件確實可能不要初始化,操作系統(tǒng)在 創(chuàng)建進程的時候在內核空間做的。這依賴于操作系統(tǒng)的具體實現(xiàn),比如堆,宏內核結構可能在內核初始化,微內核結構在用戶空間;即使同樣是微內核,這個東東也 可能會被拿到內核空間初始化。
隨著 CPU 技術的發(fā)展,存儲量的迅速擴展,代碼復雜程度的增加,微內核被越來越多的采用。你會為了 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 在可執(zhí)行程序中的位置是連續(xù)的,根據(jù)我們前面的介紹,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 變量同樣出現(xiàn)在符號表中。
2. static 變量的屬性和普通的全局變量不同。
3. local_b 似乎變成了一個很奇怪的名字 _?local_b@?1??main@@9@9
第一點,只有全局變量才會出現(xiàn)在符號表中,所以毫無疑問 local_b 被編譯器看作是全局變量。
第二點,External屬性表明,該符號可以被其他的 obj 引用(做鏈接),static 屬性表明該符號不可以被其他的 obj 引用,所以所謂 static 變量不能被其他文件引用不僅僅是在 C 語法中做了規(guī)定,真正的保證是靠鏈接器完成的。
第三點,_?local_b@?1??main@@9@9 就是 local_b,鏈接器為什么要換名字呢?很簡單,如果不換名字,如果有一個全局變量與之重名怎么辦,所以這個變量的名字不但改變了,而且還有所在函數(shù)的名字作為后綴,以保證唯一性。
二:volatile
說道 volatile 這個關鍵字,恐怕有些人會想,這玩意兒是 C 語言的關鍵字么?你老兄不會忽悠俺吧?嘿嘿,這個關鍵字確實是C語言的關鍵字,只不過比較少用,他的作用很奇怪:編譯器,不要給我優(yōu)化用這個關鍵字修飾的 符號所涉及的程序。有看官會說這不是神經病么?讓編譯器優(yōu)化有什么不好,我巴不得自己用的編譯器優(yōu)化算法世界第一呢。
好,我們來看幾個例子:(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
通過觀察這個程序的匯編碼我們發(fā)現(xiàn),編譯器發(fā)現(xiàn)程序的執(zhí)行結果不會影響任何寄存器變量,就將這個循環(huán)優(yōu)化掉了,我們在匯編碼里面沒有看到任何和循環(huán)有關的部分。這兩句匯編碼僅僅相當于 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,然后重新編譯,得到的匯編碼如上所示,這回好了,循環(huán)回來了。有人說,這有什么意義呢,這個問題問得好。在通常的應用程序中,這個小小的延遲循環(huán)通常 沒有用,但是寫過驅動程序的朋友都知道,有時候我們寫外設的時候,兩個命令字之間是需要一些延遲的。
例二:
/*
* 第一個程序
*/
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; 沒有任何意義,所以被優(yōu)化掉了。
/*
* 第二個程序
*/
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; 被執(zhí)行了,同樣,這主要用于驅動外設,有的外設要求連續(xù)向一個地址寫入多個不同的數(shù)據(jù),才能外成一個完整的操作。有的群眾迷惑了,為啥驅動外設要寫內存 啊?我估計那是您僅僅了解 Intel 的 CPU,Intel 的CPU 外設地址和內存地址分開,訪問外設的時候使用特殊指令,比如 inb, outb。但有一些 CPU 比如 ARM,外設是和內存混合編址的,訪問外設看上去就像讀寫普通的內存。具體細節(jié)請參考 Intel 和 ARM 的手冊。
大家注意到一個精彩的細節(jié)了么,在例二中編譯器發(fā)現(xiàn)一個寄存器 eax 可以完成整個函數(shù),所以并沒有給變量 p 在棧上分配內存。省略了棧的操作,節(jié)省了不少時間,通常棧的操作使用 5 條以上的匯編碼。
又有群眾反映了,你說的兩個例子,都是寫驅動程序用到的,我寫應用程序是不是就沒必要知道了呢?不是,請您繼續(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;
}
這個程序乍看沒什么問題,在某些編譯器,或者某些優(yōu)化參數(shù)下,while (run)會被優(yōu)化成一個死循環(huán),是因為編譯器不知道會有外部的線程要改變這個變量,當他看到 run 的定義時,認為這個循環(huán)永遠不會退出,所以直接將這個循環(huán)優(yōu)化成了死循環(huán)。解決的辦法是用 volatile 聲明變量 i。
我手頭的編譯器這個例子不會出錯,我也懶得找讓這個例子成立的編譯器了,大家自己動手試驗一下看看吧。
如果大家靜下心來讀上一些匯編碼就會發(fā)現(xiàn),微軟的編譯器經常會有一些精彩的表現(xiàn)。哎,拍微軟的馬屁也沒用,給微軟發(fā)過簡歷,連面試的機會都沒給,就被拒了,太郁悶了!
posted on 2007-03-02 09:30 Khan 閱讀(1535) 評論(0) 編輯 收藏 引用 所屬分類: GCC/G++ 、跨平臺開發(fā)