寫在前面的話
寫本文的目的有:
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。
我手頭的編譯器這個例子不會出錯,我也懶得找讓這個例子成立的編譯器了,大家自己動手試驗一下看看吧。
如果大家靜下心來讀上一些匯編碼就會發現,微軟的編譯器經常會有一些精彩的表現。哎,拍微軟的馬屁也沒用,給微軟發過簡歷,連面試的機會都沒給,就被拒了,太郁悶了!