寫在前面的話
寫本文的目的有:
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ī)會都沒給,就被拒了,太郁悶了!