段(Segment):
對象文件/可執行文件:
SVr4 UNIX上被稱為ELF(起初"Extensible Linker Format", 現在"Executable and Linking Format")文件。BSD UNIX上被稱為a.out。這些格式都具有段的概念
section是存放特定類型二進制文件區域,section是ELF文件的最小組織單元,段通常由多個section組成
段主要有:
- BSS段:Block Started by Symbol,放置全局的但是沒有初始化的變量。由于BSS段的變量沒有任何值,所以不會真的在a.out文件中存儲,所以BSS在a.out文件中不占空間(除了需要指明大小需要部分外),而是在運行時申請
- text段:代碼段,放置指令
- data段:數據段,放置全局的,而且已經初始化的變量。局部變量不會放到a.out中,而是在運行時創建
示例:
只有main函數:
? ?text ? ?data ? ? bss ? ? dec ? ? hex filename
? 14292 ? ?1532 ? ? 112 ? 15936 ? ?3e40 Hello.exe
定義全局未初始化int:
? ?text ? ?data ? ? bss ? ? dec ? ? hex filename
? 14292 ? ?1532 ? ? 116 ? 15940 ? ?3e44 Hello.exe
定義全局初始化int:
? ?text ? ?data ? ? bss ? ? dec ? ? hex filename
? 14292 ? ?1536 ? ? 112 ? 15940 ? ?3e44 Hello.exe
定義局部未初始化int:
? ?text ? ?data ? ? bss ? ? dec ? ? hex filename
? 14292 ? ?1532 ? ? 112 ? 15936 ? ?3e40 Hello.exe
定義局部初始化int:
? ?text ? ?data ? ? bss ? ? dec ? ? hex filename
? 14308 ? ?1532 ? ? 112 ? 15952 ? ?3e50 Hello.exe
結論:
- 全局未初始化的變量放到了BSS段
- 全局初始化的放到了data段
- 局部未初始化變量只意味著在運行時為其分配空間,而不會在生成可執行文件的時候分配空間或者生成相關的語句,所以不涉及BSS\DATA\TEXT段
- 局部初始化變量不涉及BSS和DATA段,而是生成語句執行初始化
操作系統如何處理可執行文件:
段會生成運行時鏈接器可以直接加載的對象,加載器直接把每個段對應到內存中的一部分
這些段就成為執行中程序的一塊實際的內存區域
highest memory address
+------------------------+
?| ? ?stack segment? |
?| ? ? ? ? ? ? ? .? ? ? ? ? ? ?|
?| ? ? ? ? ? ? ? .? ? ? ? ? ? ?|
?| ? ? ? ? ? ? ? .? ? ? ? ? ? ?|
+------------------------+
?| ? ? ?BSS segment? |--未初始化的全局變量
+------------------------+
?| ? ? ?data segment ?|--初始化的全局變量
+------------------------+
?| ? ? ?text segment ? |
+------------------------+
?| ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
+------------------------+
lowest memory address
text段:
放置程序指令,是加載器直接從文件中復制過來的。一般情況下,text段不會改變,某些操作系統和鏈接器可以給段的不同section設置不同的權限。比如:text是只讀和只執行的,某些data是只讀的。某些data是讀寫但是不可執行的
data段:
包含了初始化的全局變量和靜態變量,并進行了初始化賦值
BSS段:
在data段生成之后,加載器就從可執行文件中獲取BSS段的大小并獲取相應的空間放到data段后面。通常BSS段和data段會合并在一起,由于段在OS內存管理中只是一段連續的虛擬地址空間,所以相連的會被合并。所以數據段通常是最大的段
局部變量、臨時存儲單元、函數調用的參數傳遞等就需要用到分配的棧空間
對于動態分配的空間還需要堆空間,堆空間是在需要的時候創建的,在第一次malloc()函數調用的時候
需要注意的是虛擬地址空間的最低的一部分沒有映射到物理地址空間,所以任何對這部分地址的引用都是非法的。這部分一般是從0開始的幾個字節空間,null指針或含有很小整數值的指針將指向這里
如果考慮到共享庫文件,那么實際的映射將是這樣的:
highest memory address
+--------------------------+
?| ? ? stack segment ? ?|
?| ? ? ? ? ? ? ? ?. ? ? ? ? ? ? ? |
?| ? ? ? ? ? ? ? ?. ? ? ? ? ? ? ? |
?| ? ? ? ? ? ? ? ?. ? ? ? ? ? ? ? |
+--------------------------+
?| ? ? ? ? ? ?linker ? ? ? ? ? ?|
+---------------------------+
?| unmapped segment|
?| ? ? ? ? ? ? ? ?. ? ? ? ? ? ? ? ?|
+---------------------------+
?| ? ? ? data segment ? ?|
+---------------------------+庫文件
?| ? ? ? text segment ? ? ?|
+----------------------------+
?| ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
+----------------------------+
?| ? ? ? data segment ? ? ?|
+----------------------------+庫文件
?| ? ? ? text segment ? ? ? |
+----------------------------+
?| ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
+----------------------------+
?| ? ? ? data segment ? ? ?|
+-----------------------------+執行代碼
?| ? ? ? text segment ? ? ? |
+-----------------------------+
?|? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?|
+-----------------------------+
lowest memory address
C運行時如何處理可執行文件:
C在運行時會維護很多數據結構,比如棧、活動記錄、數據、堆等等
棧段:
棧段只包含一個數據結構——棧
經典的棧定義是先進后出的隊列,只有push和pop操作。但是這里的棧不僅可以push和pop還可以改變棧中某位置的值
運行時維護一個指針,通常位于寄存器中被稱為sp,指向棧頂
棧段的作用主要有三個,兩個關于函數,一個關于表達式計算:
- 棧為函數中的局部變量提供存儲空間,這些變量被成為"自動變量"
- 棧保存函數調用時需要的維護信息,被稱為"程序活動記錄"。包含調用結束時的返回地址、不能放到寄存器中的參數和保存調用前的寄存器狀態
- 棧也可以作為高速暫存寄存器,當程序需要臨時存儲的時候使用。如長表達式的計算,中間結果會被放到棧中并在使用的時候取出
alloca()函數分配的空間也在棧中,但是這部分空間會被下一次函數調用重寫
如果不是有函數遞歸調用,棧是不被需要的。如果沒有遞歸調用,局部變量、參數需要的空間和返回地址都可以在編譯器知道并且在BSS段分配
程序活動記錄
活動記錄的目的是追蹤調用鏈,每次調用函數都會在棧中生成一個活動記錄,活動記錄支持函數的調用以及記錄調用結束后需要恢復的狀態。具體活動記錄的設計和實現相關,活動記錄內部各區域的順序可能各不相同,也可能有一個區域存儲函數調用之前的寄存器值
大多數現代程序語言都支持函數內部定義函數(和數據一起)。但是C不允許函數嵌套聲明,所有的函數都必須在詞法頂層。這種限制能夠一定程度的簡化C編譯器實現
在允許嵌套函數的語言中,活動記錄會包含一個指向其外部函數的指針,這個指針被稱為靜態鏈接(static link)(和編譯文件時的靜態鏈接區分一下),這個指針允許內部函數獲取外部函數的棧幀
雖然可能一個外部函數同時被多次調用,但是靜態鏈接總能指向正確的棧幀,訪問到正確的局部數據
對外部函數數據的獲取被稱為上層引用(uplevel reference)
之所以被稱為靜態鏈接,是因為對其外部函數的指向是在編譯期確定的,而動態鏈接是運行時被調用時指向其調用者的棧幀
典型的活動記錄如下:
+-------------------------+
?| ? ? ? ? local vars? ? ? ?| -- 存儲如調用結束時需要恢復得寄存器值等
+-------------------------+
?| ? ? ? arguments ? ? ? | -- 參數
+-------------------------+
?| ? ? ? prev frame ? ? ? | -- 調用者的棧幀
+-------------------------+
?| ? ? ? return addr ? ? ?| -- 返回地址
+-------------------------+
每次調用函數都會生成一個這樣的活動記錄。但是編譯器作者會盡量的減少存儲的信息以提高程序的性能,比如:
- 用寄存器存儲某些信息而不是在棧中
- 對于葉函數(不會調用其他函數的函數)不生成完整的棧幀,調用者不再存儲寄存器的值而是讓被調用者存儲
- 使用指向調用者棧幀的指針可以簡化函數返回時彈棧到先前記錄的工作
auto和static
auto的變量是函數調用時在棧中分配空間存儲的,函數調用結束的時候對應的棧空間就會被釋放并且可以被重寫。所以如果函數返回一個指針指向局部變量就會返回一個"懸空指針",指向的值不是有效的。
如果想要返回一個函數中定義的變量,可以將其定義為static。static變量不是存儲在棧中,而是在數據段分配空間存儲。這樣變量就會在程序的生命周期中存在,即便函數調用結束數據也依然存在,下一次函數調用還可以訪問
棧幀不一定在棧中
如果把活動記錄放到寄存器中會有更好的性能。SPARC架構以"寄存器窗口"來提高棧幀的性能。芯片中有一組專門用來存放活動記錄中的參數的寄存器。空的棧幀還會被壓入棧中,如果調用鏈過長導致寄存器窗口被用光,那么就會通過把寄存器中的值填充到對應的空棧幀中來釋放寄存器?
線程控制:
每個線程都會有自己的棧并且用red zone和其他線程的棧結構區分
setjmp和longjmp
它們通過操縱活動記錄實現,這個特性彌補了C在跳轉能力上的不足
工作方式:
- setjmp(jmp_buf j)首先被調用,用變量j記錄當前語句所在的位置,調用后返回0
- longjmp(jmp_buf j,int i)在setjmp之后調用,返回j記錄的地址,使之看起來像是從函數setjmp()返回,而且返回值是整數i,用以區分這是從longjmp()返回的
- j的內容在使用longjmp()之后被銷毀
setjmp()保存了當前程序計數器和當前棧頂指針的內容。然后longjmp()恢復這些內容,高效的將控制流轉移到原來的位置恢復保存的狀態。并且會回退所有保存的棧頂之前的棧空間
和goto的不同:
- goto語句無法跳出當前函數,但是longjmp甚至可以調到另一個文件的函數
- longjmp只能回到之前控制流到過的某個地方,即設置了setjmp()并且被執行過的地方
setjmp/longjmp最有用的地方是錯誤恢復。只要你沒有從函數返回,如果發現了一個不可恢復的錯誤,你就可以移動控制流到之前的某個節點,然后從那里重新開始。可以用來從多重函數調用中立即返回,也可以用來預防危險的代碼
如:
switch(setjmp(jbuf)) {?case 0: ?apple = *suspicious; ?break; ?case 1: ?printf("suspicious is indeed a bad pointer\n"); ?break; ?default: ?die("unexpected value returned by setjmp"); ?
}
如果在某個地方檢測到了這個指針危險,就可以返回到這里
就像goto語句,setjmp/longjmp也會導致程序難以理解難以調試,所以要盡量避免使用
UNIX下的棧段:
棧隨著程序需要增長,程序員可以認為棧是無限大的
UNIX使用某種虛擬內存模式,當嘗試獲取超過分配的空間的空間的時候就會產生一個頁錯誤,處理方式依賴于引用是否有效。內核處理非法引用的方式通常是向產生錯誤引用的程序發送一個信號。在棧頂之后有一個red zone,對這里的引用不會產生錯誤,操作系統相應的會增加棧空間的大小,虛擬地址空間會相應的增加
MS-DOS的棧段:
DOS中棧的大小是由可執行文件指定的,而且不能再運行時修改,對超過空間的訪問會導致程序失效。如果打開了檢查,就會產生棧溢出錯誤。如果超出了段的限制,也會在編譯器產生這個錯誤。Turbo C如果數據段或代碼段過大,會產生Segment overflowed ? maximum ?size ?<lsegname>,80x86架構限制為64Kbytes