1. 程式人生 > >《深入理解計算機系統》筆記(一)棧【插圖】

《深入理解計算機系統》筆記(一)棧【插圖】

歡迎檢視《深入理解計算機系統》系列部落格

《深入理解計算機系統》筆記(一)棧(本篇)

--------------------------------------------------------------------------------------------------------------------

讀後感

        這本書是美國“卡內基-梅隆大學(CMU)”的教科書,邏輯嚴謹。雖然是教科書,還是有些晦澀難懂啊,不太形象。第二章主要講整數,浮點數,很是晦澀,全是數學公式。作者的思維數學的思維,動不動就是n、m、k、∑等等,讓我們數學很爛的同學如何是好。如果能以普通人的思維把數學知識加進去就好了。

        該書確實系統的介紹了計算機,很完善。它能給你以下幾個重要級別的模型和過程:

1.函式的呼叫棧模型——第三章(函式不一定都會建立棧幀,本文章將解釋此現象)

    2.a.out或者exe可執行檔案的結構——第七章點選開啟連結

    3.程式載入器和連結——第八章 點選開啟連結

    4.malloc和虛擬儲存器原理——第九章點選開啟連結

    5.執行緒,在儲存器中模型——第12章

    對於處於成長期的程式設計師來說,真是欣喜若狂!有了這些知識還需要《C專家程式設計》這本書麼?這本書就是《C專家程式設計》的全覆蓋啊,哈哈!

    翻譯者很是用心,但是讀者不一定領情。比如:可以直接翻譯流行的記憶體、硬碟和固態硬碟,完全沒有必要用主存、磁碟和固態儲存磁碟。還比如:沒有必要把shell翻譯成“外殼”多彆扭啊。這些翻譯者應該像“侯捷”學習。

    這本說內容大而散,感覺沒有盡頭一樣。老外怎麼學這種課程,費腦子啊。從計算機結構、二進位制表示、到組合語言函式的呼叫、然後cpu的結構、再有聯結器儲存器、還有程序,併發、更有網路程式設計,基本大學四年也就學了這麼多東西。

    這本書中有句話很有意思:儲存器的一個有趣的屬性是不論系統中有多大的儲存器,他總是一種稀缺資源。磁碟空間和垃圾桶同樣有這個屬性。

    工作2年多的時間裡,每每都是在網上搜系統方面的知識、編譯、連結和虛擬儲存器malloc等等。只有讀了這本書才能系統得學到計算機知識。

一、計算機漫遊

---》利用直接儲存器(DMA)的技術,資料可以不通過cpu而直接從磁碟到達記憶體。

---》根據機械原理,較大的儲存器比較小的儲存器執行慢,一個暫存器只能儲存幾百個Byte,而且記憶體可以存放GB以上。加快處理器的執行速度比加快記憶體執行速度更容易。

---》快取記憶體至關重要,一個簡單的helloworld揭示了一個重要的問題。系統花費大量時間把資訊從一個地方挪到另一個地方。helloworld最初放在硬碟上,然後載入到記憶體,進而進入cpu中。下圖說明了一個儲存器層次結構:

二、資訊的表示和處理

講的是計算機原理,二進位制,補碼和浮點數等。因為大學課程已經學習過了,沒有細讀。

---》浮點數,規格化、非規格化和無窮大。

    一般來說我們沒把發用小數表示1/3、7/10等這些不能整出的數字,那麼如果用二進位制表示十進位制的小數,更多的表示不出來。二進位制甚至不能表示十進位制的0.1和0.2

三、程式的機器級表示(其實就是組合語言)

---》講的是《組合語言》,頭都大了!個人覺得組合語言不用花時間瞭解,即使是本書中的組合語言也有文字解析。IA32X86-64兩種組合語言。

---》彙編程式碼不區分有符號和無符號甚至指標型別。

---》下圖展示了,彙編程式碼字尾的含義:


    大多數GCC生成的彙編指令都有一個字元字尾,表示運算元的大小。例如資料傳送指令有三個變種:movb(傳送位元組)、movw(傳送字)和movl(傳送雙字)。注意,彙編程式碼使用字尾'l'來表示4個位元組整數和8個位元組雙精度浮點數,這不會產生歧義,因為浮點數使用的是一組完全不同的指令和暫存器。

    運算元指示符,運算元一共有三類:1)立即數(immediate)也就是常數值,立即數的書寫方式是$。例如:$0x1F。2)暫存器,3)儲存器(memory).由於三種運算元的存在所以定址方式就有很多種。

---》一個32位cpu中暫存器的結構如下:


    上圖是IA32的整數暫存器。所有8個暫存器都可以作為32位和16位使用,例如%eax和%ax。並且前四個暫存器可以訪問其兩個低位元組。如:%ah和%al。

下圖是64位cpu的暫存器結構圖:

紅色框內,是相容32為cpu的結果。

---》暫存器使用慣例:%eax、%edx和%ecx是呼叫者儲存暫存器,%ebx、%esi和%edi是被呼叫者儲存暫存器。那麼,一個函式f()可能被別人呼叫,也可以呼叫其他函式,所以當f()執行時需要將%ebx、%esi和%edi儲存到棧中,並在返回前再恢復它們。(p151)---》64位%rax暫存器用來儲存函式的返回值,(p198)

    在x86-64組合語言,中%rax用來儲存函式的返回值,而在結果返回之前,%rax可以重複利用。

---》棧在處理函式呼叫中起到至關重要的作用。下圖棧的示意圖,棧頂朝下,由於IA32 的棧竟然是往低地址延伸生長,直讓我崩潰。(p115)


圖片的上半部分,說明了實際效果,即將%eax的值移動到%edx中,圖片的下半部分是棧移動步驟。棧頂的變化最後關鍵。從0x108 -> 0x104 -> 0x108

---》棧幀結構,IA32程式用程式棧來支援函式呼叫。機器用棧來傳遞函式引數、返回值、儲存暫存器用於以後恢復和本地儲存。為單個過程分配的那部分棧成為棧幀(stack frame)。下圖說了棧幀的結構。


---》call指令。call指令的效果是將返回值地址入棧,並跳轉到被呼叫過程的起始處。返回地址是在程式中緊跟在call後面的那條指令的地址。這樣當被呼叫函式返回時,執行會從此處繼續。ret指令從棧中彈出地址,並跳轉到這個位置。例如下面的程式碼:

int accum = 0;
int sum(int x,int y);
int main()
{
    return sum(1.3);
}
int sum(int x,int y)
{
    int t = x + y;
    accum += t;
    return t;
}
經過反彙編後,節選處call部分的程式碼如下圖所示:

第一行call指令的效果就是將0x80483e1壓入棧中,同時將%eip(程式計數器)的值設定為sum的第一條指令0x8048394.最後一行的ret指令彈出0x80483e1給%eip,並跳轉到這個地址。如圖所示:

ret指令的效果就是讓0x080483e1彈出,調整棧指標,並且0x080483e1賦給%eip,程式繼續執行。

---》函式呼叫例項

int swap_add(int* xp,int* yp);
int caller()
{
    int arg1 = 534;
    int arg2 = 1057;
    int sum = swap_add(&arg1 , &arg2);
    int diff = arg1 - arg2;

    retur sum * diff;
}
int swap_add(int* xp,int* yp)
{
    int x = * xp;
    int y = * yp;
    *xp = y;
    *yp = x;
    return x + y;
}

    (藍色箭頭是“指向”,紅色箭頭是“偏移量”,綠色箭頭是解釋說明)

    arg1和arg2必須存放在棧中,因為我們必須為它們生成地址。swap_add中的變數int x和int y可以存放在暫存器中。

    分配在棧上的24個位元組,8個用於區域性變數,8個用於引數,8個未使用,這是因為GCC認識所有的棧空間都應該是16的整數倍。這樣保證資料放的嚴格對齊。

    經過呼叫swap_add之後棧的資訊又恢復到最初的狀態。

---》許多函式編譯後不需要棧幀。如果所有的區域性變數都能儲存在暫存器中,而且這個函式又不會呼叫其他函式(葉子過程),那麼需要棧的唯一原因就是用來儲存返回值。特別是dui'yu所以,雖然C語言中有暫存器變數,但是如果這個函式的變數很少的話,及時不標明這個變數是暫存器,它也會被載入到暫存器中去。(p196)

---》函式需要棧幀的原因有如下幾個:

    區域性變數太多,不能都放在暫存器中。

    有些區域性變數是陣列或者結構。

    函式用&來計算一個區域性變數的地址。

    函式必須將棧上的某些引數傳遞給另外一個函式

    在修改一個被呼叫著儲存暫存器之前,函式需要儲存其他狀態。

---》棧破壞檢測和棧保護(p181)

    在C語言中,沒有可靠的方法來防止對陣列的越界寫操作。陣列越界,是棧溢位後發現這個錯誤然後丟擲。

    

echo是一個函式,存放了char buf[8]的一個區域性變數。

思想:在棧幀中任何區域性緩衝區與棧狀態之間儲存一個特殊的金絲雀(canary)值,也成為哨兵值(guard value)是在程式每次執行時隨機產生的。因此如果這個哨兵值改變了說明棧溢位了。

---》棧隨機化(p180)

    計算機

    比如,多次執行下面的程式碼,本地變數的地址是不變的。

    int main()
{
    int local;
    printf("local at %p\n",&local);
    return 0;
} 

    一個現實生活中的例子,但是這個例子說的是每次堆上開闢空間可能是一致的。

    曾經在做Symbian專案的時候,發現一個不是必現的bug,後來發現是野指標。但是問題是為什麼不是必現呢?是因為Symbian作業系統每次在上開闢的空間,在短時間內是一個地址。舉例:假如,ptr這個指標,現在成為野指標了。但是,之後它指向的記憶體又被重新malloc了,等同於ptr指向了新的物件。但是,這個巧合並不是每次復現。

---》將IA32擴充套件到64位。(p183)

    X86-64是AMD提出來,並命名的。現在一般簡寫X64

    通用目的暫存器組從8個擴充套件到16個。而且名字也變成了%rax,%rbx。其中%rax用來存放返回值。

    許多程式狀態都儲存在暫存器中,而不是棧上。整形和指標型別的引數通過暫存器傳遞。所以,有些過程根本不需要建立棧。

    如果可能,條件操作作用條件傳送指令實現,會得到比傳統分支程式碼更好的效能。

    浮點操作用面向暫存器的指令集來實現,而不是IA32支援的基於棧的方法來實現。

    X86-64沒有幀暫存器。

---》函式指標的值是該函式機器程式碼表示中的第一條指令的地址。(p173)

第二個讀書筆記,點選檢視