1. 程式人生 > >《深入理解計算機系統》第三章學習筆記

《深入理解計算機系統》第三章學習筆記

並發 錯誤 ia32 庫函數 容易 簡單 linux 嚴重 格式

通過本周的學習,總結出一下知識內容

機器級代碼

計算機系統使用了多種不同形式的抽象,利用更簡單的抽象模型來隱藏實現的細節。

對於機器級編程來說,其中兩種抽象尤為重要:

1、指令集體系結構(Instruction set architecture ISA)

它定義了處理器狀態、指令的格式,以及每條指令對狀態的影響。

IA32將程序的行為描述成好像每條指令時按順序執行的,一條指令結束後,下一條再開始。(實際上處理器並發地執行許多指令,但是可以采取措施保證整體行為與ISA指定的順序執行完全一致)

2、機器級程序使用的存儲器地址是虛擬地址

提供的存儲器模型看上去是一個非常大的字節數組。存儲器系統的實際實現是將多個硬件存儲器和操作系統軟件組合起來。

程序存儲器(program memory)包含:程序的可執行機器代碼、操作系統需要的一些信息、棧、堆。程序存儲器用虛擬地址來尋址(此虛擬地址不是機器級虛擬地址)。操作系統負責管理虛擬地址空間(程序級虛擬地址),將虛擬地址翻譯成實際處理器存儲器中的物理地址(機器級虛擬地址)。

寄存器使用慣例

程序寄存器組是唯一能被所有函數共享的資源。

雖然在給定時刻只能有一個函數是活動的,但是我們必須保證當一個函數調用另一個函數時,被調用者不會覆蓋某個調用者稍後會用到的值。為此,IA32采用了一組統一的寄存器使用規則,所有的函數都必須遵守,包括程序庫中的函數。

根據慣例:寄存器%eax、%edx、%ecx被劃分為調用者保存寄存器。當過程P調用Q時,Q可以覆蓋這些寄存器,不會破壞任何P所需要的數據。

另一方面,寄存器%ebx、%esi、%edi被劃分為被調用者寄存器。這意味著Q必須在覆蓋這些寄存器的值之前,先把它們保存到棧中,並在返回前恢復它們。此外還必須保持寄存器%ebp和%esp。

轉移控制

call Label 過程調用

call *Operand 過程調用

leave 為返回準備棧

ret 從過程調用中返回

call指令的效果是將返回地址入棧,並跳轉到被調用過程的起始處。(返回地址是在程序正文中緊跟在call後面的那條指令的地址,這樣當被調用過程返回時,執行流會從此處繼續。)

ret指令從棧中彈出地址,並跳轉到這個位置。(使用這條指令前,要使棧做好準備,棧頂指針要指向前面call指令存儲返回地址的位置)

leave指令 使棧做好返回的準備。它等價於:

movl %ebp, %esp ; 把寄存器%ebp中的值復制到寄存器%esp中(回收本函數的棧空間)

popl %ebp

leave指令的使用在返回前,既重置了棧指針,也重置了基址指針。

緩沖區溢出

通常,在棧中分配某個字節數組來保存一個字符串,但是字符串的長度超出了為數組分配的空間。C對於數組引用不進行任何邊界檢查,而且局部變量和狀態信息,都存在棧中。這樣,對越界的數組元素的寫操作會破壞存儲在棧中的狀態信息。當程序使用這個被破壞的狀態,試圖重新加載寄存器或執行ret指令時,就會出現很嚴重的錯誤。

緩沖區溢出的一個更加致命的使用就是讓程序執行它本來不願意執行的函數。這是一種最常見的通過計算機網絡攻擊系統安全的方法。通常,輸入給程序一個字符串,這個字符串包含一些可執行代碼的字節編碼,稱為攻擊代碼,另外還有一些字節會用一個指向攻擊代碼的指針覆蓋返回地址。那麽,執行ret指令的效果就是跳轉到攻擊代碼。

通常,使用gets或其他任何能導致存儲溢出的函數,都不是好的編程習慣。不幸的是,很多常用庫函數,包括strcpy、strcat、sprintf,都有一個屬性——不需要告訴它們目標緩沖區的大小,就產生一個字節序列。

對抗緩沖區溢出攻擊

1、棧隨機化

為了在系統中插入攻擊代碼,攻擊者不但要插入代碼,還要插入指向這段代碼的指針,這個指針也是攻擊字符串的一部分。產生這個指針需要知道這個字符串放置的棧地址。在過去,程序的棧地址非常容易預測,在不同的機器之間,棧的位置是相當固定的。

棧隨機化的思想使得棧的位置在程序每次運行時都有變化。因此,即使許多機器都運行相同的代碼。它們的棧地址都是不同的。

實現的方式是:程序開始時,在棧上分配一段0--n字節之間的隨機大小空間。程序不使用這段空間,但是它會導致程序每次執行時後續的棧位置發生了變化。

在Linux系統中,棧隨機化已經變成了標準行為。(在linux上每次運行相同的程序,其同一局部變量的地址都不相同)

2、棧破壞檢測

在C語言中,沒有可靠的方法來防止對數組的越界寫,但是,我們能夠在發生了越界寫的時候,在沒有造成任何有害結果之前,嘗試檢測到它。

最近的GCC版本在產生的代碼中加入了一種棧保護者機制,用來檢測緩沖區越界,其思想是在棧中任何局部緩沖區與棧狀態之間存儲一個特殊的金絲雀值。這個金絲雀值是在程序每次運行時隨機產生的,因此,攻擊者沒有簡單的辦法知道它是什麽。

在恢復寄存器狀態和從函數返回之前,程序檢查這個金絲雀值是否被該函數的某個操作或者函數調用的某個操作改變了。如果是,那麽程序異常終止。

3、限制可執行代碼區域

限制那些能夠存放可執行代碼的存儲器區域。在典型的程序中,只有保存編譯器產生的代碼的那部分存儲器才需要是可執行的,其他部分可以被限制為只允許讀和寫。

現在的64位處理器的內存保護引入了”NX”(不執行)位。有了這個特性,棧可以被標記為可讀和可寫,但是不可執行,檢查頁是否可執行由硬件來完成,效率上沒有損失。

總結

這一章主要是介紹高級語言,例如C語言編寫的程序,經過編譯後轉換為匯編程序。以往在程序員對機器進行操作主要都是使用匯編語言。

匯編語言的使用和掌握能夠很好地幫助程序員對系統資源進行把控,同時也要求了程序編寫邏輯的盡量地靠近機器的思考,對於程序員打下良好的編程基礎具有很好地學習作用。

在這一章裏面,由於時代的進步,目前開源社區或者廠商已經提供了能夠很好地對高級語言進行優化的編譯器GCC,這樣即使是邏輯更為抽象的高級語言開發的程序也能夠很好地轉換為性能良好的匯編程序,更何況,抽象化程度更高的高級語言對於開發復雜的程序更有效率。因此目前對於程序員的要求已經從能夠寫匯編程序到能夠讀懂匯編程序。

那麽為什麽要會讀會匯編程序呢?
1.同一個程序,使用不同的GCC,會產生不同的匯編程序,同一個GCC,相同功能的一個程序,不同寫法會產生不同的匯編程序,程序性能的優化,單純從高級語言上很難做出優化,這時候就需要對程序編譯後產生的匯編程序進行解讀和重構。
2.在程序編寫過程中,很多錯誤是存在的

《深入理解計算機系統》第三章學習筆記