程序的運(yùn)行時(shí) 數(shù)據(jù)結(jié)構(gòu)
這篇博文主要內(nèi)容是程序運(yùn)行時(shí)的數(shù)據(jù)結(jié)構(gòu),包括運(yùn)行時(shí)程序中的不同部分如何分配內(nèi)存、函數(shù)調(diào)用的內(nèi)存實(shí)現(xiàn)、
還介紹了一個(gè)c獨(dú)有的強(qiáng)大功能,一個(gè)被稱為“展開(kāi)堆?!保╱nwinding stack)的技術(shù)
運(yùn)行時(shí) 數(shù)據(jù)結(jié)構(gòu),中間的空格是特意留出的,
運(yùn)行時(shí)可以認(rèn)為是程序執(zhí)行的一個(gè)狀態(tài),一般有編譯時(shí),運(yùn)行時(shí)等,他們都是表示一個(gè)處理狀態(tài)。
編程語(yǔ)言的的經(jīng)典對(duì)立之一就是代碼和數(shù)據(jù)的區(qū)別。代碼和數(shù)據(jù)的區(qū)別也可認(rèn)為是運(yùn)行時(shí)和編譯時(shí)的界限,編譯器的絕大部分工作和翻譯代碼有關(guān);必要的數(shù)據(jù)存儲(chǔ)管理的絕大部分都在運(yùn)行時(shí)進(jìn)行。
如果你用過(guò)GCC,就會(huì)知道用GCC編譯程序,都會(huì)得到一個(gè)默認(rèn)名為“a.out”的文件。
簡(jiǎn)單說(shuō)下“a.out”的由來(lái)吧:
他是assembler output(匯編程序輸出)”的縮寫形式。但是,他不是匯編程序輸出,而是鏈接器輸出。
這個(gè)名字曾被解釋為:“新程序就緒,準(zhǔn)備執(zhí)行”它是鏈接器輸出文件。
一般的說(shuō),可以認(rèn)為連接器輸出的是二進(jìn)制文件,這個(gè)文件并不是雜亂無(wú)章的放在一起的,而是由一定的存放規(guī)律。比如說(shuō)分類存放,這就涉及到了我們接下來(lái)要討論的段的概念。
段
目標(biāo)文件和可執(zhí)行文件都可以有多種不同的格式,所有這些不同的格式都有一個(gè)概念,就是段(segments)。
就目標(biāo)文件而言,段就是二進(jìn)制文件的簡(jiǎn)單區(qū)域,里面保存了某種特定類型相關(guān)的所有信息。術(shù)語(yǔ)section也廣泛使用,
他可看作是段的組成部分,一個(gè)段通??梢园瑤讉€(gè)section
不過(guò)這里的段要注意和內(nèi)存模型中的段區(qū)別開(kāi)來(lái),在內(nèi)存模型中,段是內(nèi)存模型設(shè)計(jì)的結(jié)果。
請(qǐng)看段的組成形式:

從上圖中可以看出:a.out包含了magic number,它可以理解為一個(gè)標(biāo)示符,一般是一些特殊的數(shù)字,所謂的特殊數(shù)字也就是有特別意義的,比如
#define FS_MSGIC 0x011954 它是kirk mckusick 的生日。~所以這里不用太注意。
下面的是a.out的其他內(nèi)容,比如一些標(biāo)示符等等。。其它的內(nèi)容在下文有說(shuō)明就不多說(shuō)
操作系統(tǒng)對(duì)段的操作:
段可以方便的映射到鏈接器在運(yùn)行時(shí)可以直接載入的對(duì)象中,載入器只是提取每個(gè)端的印象,直接將他們放入內(nèi)存中。
從本質(zhì)上說(shuō),端在執(zhí)行過(guò)程的程序中是一塊內(nèi)存區(qū)域。
文本段(The text segment )包含程序的指令,鏈接器把指令直接從文件拷貝到內(nèi)存中,以后就用管他,因?yàn)橐话闱闆r下下,文本區(qū)域是不會(huì)改變的,不論是大小還是內(nèi)容。
數(shù)據(jù)段(The data segment)包含經(jīng)過(guò)初始化的全局變量和靜態(tài)變量以及他們的值。BSS段是未初始化的數(shù)據(jù),大小可從可執(zhí)行文件中得到,然后鏈接器得到這個(gè)大小的內(nèi)存塊。
緊跟在數(shù)段之后,包含數(shù)據(jù)段和BSS段的一般統(tǒng)稱為數(shù)據(jù)區(qū)。這是因?yàn)?,操作系統(tǒng)中,段是一塊連續(xù)地址,所以相鄰的端被結(jié)合。一般來(lái)講,數(shù)據(jù)段在任
何進(jìn)程中都是最大的段。
堆棧段(The stack segment)上圖顯示了一個(gè)即將執(zhí)行的進(jìn)程的內(nèi)存布局,我們?nèi)匀恍枰恍┐鎯?chǔ)空間,用于存放臨時(shí)變量,臨時(shí)數(shù)據(jù),傳遞到函數(shù)中的參數(shù)等等(local variables, temporaries, parameter passing in function calls,)。
注意到虛擬地址空間的最低部分未被映射。它位于地址空間內(nèi),但為被賦予物理地址,所以對(duì)它的任何引用都是非法的。他用于捕捉使用空指針和小整形值的制造引用內(nèi)存的情況。
When you take shared libraries into account, a process address space appears,
當(dāng)考慮共享庫(kù)時(shí),進(jìn)程的地址空間的樣子如下圖所示:

C運(yùn)行時(shí)對(duì)a.out的操作
What the C Runtime Does with Your a.out
現(xiàn)在看一下c語(yǔ)言在運(yùn)行時(shí)的數(shù)據(jù)結(jié)構(gòu)是怎么樣的,運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)一般有好幾種,堆棧,活動(dòng)記錄,數(shù)據(jù),(the stack, activation records, data, heap)堆等
下面將分別討論,并分析他們所支持的語(yǔ)言特性:
The Stack Segment 堆棧段:
堆棧段包含一種單一的數(shù)據(jù)結(jié)構(gòu):堆棧。
堆棧為函數(shù)內(nèi)部聲明的局部變量提供存儲(chǔ)空間。
函數(shù)調(diào)用的時(shí)候,堆棧存儲(chǔ)相關(guān)的一些維護(hù)信息。這些信息被稱為堆棧結(jié)構(gòu)(stack frame)也叫做過(guò)程活動(dòng)記錄(precedure activation record)稍后討論。
堆棧也可以作為臨時(shí)存儲(chǔ)區(qū),有時(shí)候進(jìn)程需要一些臨時(shí)存儲(chǔ)空間,比如執(zhí)行一個(gè)復(fù)雜的計(jì)算時(shí),可以把結(jié)果壓到堆棧中。
值得一提的是:除了遞歸調(diào)用之外,堆棧并非必須。
函數(shù)是怎么被調(diào)用的:過(guò)程活動(dòng)記錄(precedure activation record)
What Happens When a Function Gets Called
c運(yùn)行時(shí)系統(tǒng)在他自己的地址空間內(nèi)如何管理程序的呢?這里做一個(gè)簡(jiǎn)單的討論。
c語(yǔ)言自動(dòng)提供一種用于函數(shù)調(diào)用的功能:稱作調(diào)用鏈( keeping track of the call chain)記錄了哪些函數(shù)調(diào)用哪些函數(shù),以及return執(zhí)行后,控制將返回什么地方。
解決這個(gè)問(wèn)題的經(jīng)典機(jī)制就是堆棧中的過(guò)程活動(dòng)記錄,每一個(gè)函數(shù)調(diào)用都會(huì)產(chǎn)生一個(gè)過(guò)程記錄。其實(shí)它就是一種數(shù)據(jù)結(jié)構(gòu),記錄調(diào)用后返回調(diào)用點(diǎn)需要的全部信息。
如下圖就是一個(gè)過(guò)程活動(dòng)記錄的結(jié)構(gòu),不同的編譯器會(huì)有所差別,但目的都是記錄調(diào)用后返回調(diào)用點(diǎn)的信息。

Astonishing C Fact!
C語(yǔ)言中令人震驚的事實(shí):
現(xiàn)在的多數(shù)編程語(yǔ)言都允許在函數(shù)內(nèi)部定義函數(shù),但C語(yǔ)言中所有函數(shù)都是在此法層次中的的最頂層 。
這個(gè)限制稍微簡(jiǎn)化了c編譯器。對(duì)于前一種允許在內(nèi)部定義函數(shù)的,(也即允許嵌套的過(guò)程語(yǔ)言)中,過(guò)程活動(dòng)記錄要包括一個(gè)指向外層活動(dòng)記錄的指針。這個(gè)
指針被稱為靜態(tài)鏈接(static link)它允許內(nèi)層過(guò)程訪問(wèn)外層活動(dòng)記錄,因此也能訪問(wèn)外層的局部數(shù)據(jù)。這種類型的訪問(wèn)被稱為上層引用。
下面的例子顯示了程序執(zhí)行在不同點(diǎn)是堆棧中過(guò)程活動(dòng)記錄的情況。

Static 和 Auto關(guān)鍵字詳解
為什么不能從函數(shù)中返回一個(gè)指向該函數(shù)中局部變量的指針
char * favorite_fruit () {
char deciduous [] = "apple";
return deciduous;
}
進(jìn)入 該函數(shù)的時(shí)候,自動(dòng)為變量deciduous在堆棧中分配空間,當(dāng)函數(shù)結(jié)束后變量不存在了,因?yàn)樗嫉目臻g被堆棧回收了。可能在任何時(shí)候被覆蓋,這樣
返回的指針就指向一個(gè)不確定的堆??臻g,指針失去了有效性,被稱為垂懸指針。
如果想反悔一個(gè)在函數(shù)內(nèi)部定義的指針,聲明為static就行。static的聲明在數(shù)據(jù)段中而不是堆棧段中分配空間,當(dāng)定義的變量退出函數(shù)是依然有效,下次進(jìn)入函數(shù)
依然存在。
存儲(chǔ)類型auto在實(shí)際中基本用不上,因?yàn)槟J(rèn)的聲明就是auto。他表示“進(jìn)入該塊后自動(dòng)分配內(nèi)存”在函數(shù)內(nèi)部什么的數(shù)據(jù)缺省就是這種分配
setjmp and longjmp
現(xiàn)在簡(jiǎn)單討論一下sejmp和longjmp的用途,他們是通過(guò)操作過(guò)程活動(dòng)記錄實(shí)現(xiàn)的。它是c語(yǔ)言獨(dú)有的強(qiáng)大機(jī)制。部分彌補(bǔ)了c語(yǔ)言有限的轉(zhuǎn)移能力。這兩個(gè)函數(shù)協(xié)同工作
? setjmp(jmp_buf j) must be called first. It says use the variable j to remember where
you are now. Return 0 from the call.
? longjmp(jmp_buf j,int i) can then be called. It says go back to the place that
the j is remembering. Make it look like you're returning from the original setjmp(), but
return the value of i so the code can tell when you actually got back here via longjmp().
Phew!
? The contents of the j are destroyed when it is used in a longjmp().
? setjmp(jmp_buf j)要先調(diào)用,它使用變量j記錄現(xiàn)在 的位置,函數(shù)返回0;
longjmp(jmp_buf j ,int i)可以接著被調(diào)用它表示“回到J所記錄的位置”,讓程序看上去“好像什么都沒(méi)發(fā)生一樣”返回i讓代碼知道實(shí)際上是通過(guò)longjmp返回的
當(dāng)使用longjmp()時(shí),j的內(nèi)容被銷毀。
setjmp保存了一份程序計(jì)數(shù)器和當(dāng)前棧頂?shù)闹羔槪€可以保存一些初值。longjmp返回到setjmp設(shè)置的地方,有效的轉(zhuǎn)移控制并把狀態(tài)重置到保存狀態(tài)的時(shí)候。
這被稱作“展開(kāi)堆棧”因?yàn)槟銖亩褩V姓归_(kāi)過(guò)程活動(dòng)記錄,直到取得保存在其中的值。
它和goto語(yǔ)句的區(qū)別:
A goto can't jump out of the current function in C (that's why this is a "longjmp"— you can
jump a long way away, even to a function in a different file).
You can only longjmp back to somewhere you have already been, where you did a setjmp,
and that still has a live activation record. In this respect, setjmp is more like a "come from"
statement than a "go to". Longjmp takes an additional integer argument that is passed back,
and lets you figure out whether you got here from longjmp or from carrying on from the
previous statement.
goto語(yǔ)句不能跳出c語(yǔ)言當(dāng)前的函數(shù)
longjmp只能回到曾經(jīng)到過(guò)的地方,(setjmp設(shè)置的地方)
下面給一個(gè)示例:
#include <setjmp.h>
jmp_buf buf;
#include <setjmp.h>
banana() {
printf("in banana()\n");
longjmp(buf, 1);
/*NOTREACHED*/
printf("you'll never see this, because I longjmp'd");
}
main()
{
if (setjmp(buf))
printf("back in main\n");
else {
printf("first time through\n");
banana();
}
}
輸出結(jié)果:
% a.out
first time through
in banana()
back in main
setjmp/longjmp最大的用途在于恢復(fù)錯(cuò)誤、只要還沒(méi)從函數(shù)中返回,一旦發(fā)現(xiàn)一個(gè)不可恢復(fù)的錯(cuò)誤,可以吧控制轉(zhuǎn)移到主輸入循環(huán)中。
希望能和更多的朋友交流、學(xué)習(xí)
先寫這么多吧、有什么不正確的地方還望大家指出~
參考資料《expert c programming》
http://en.wikipedia.org/wiki/Setjmp.h
http://en.wikipedia.org/wiki/A.out
轉(zhuǎn)載請(qǐng)注明出處:http://www.rzrgm.cn/yanlingyin/
一條魚(yú)~@博客園 2011-11-28

浙公網(wǎng)安備 33010602011771號(hào)