[CSAPP大作業] 程序人生-Hello's P2P

摘  要

本文主要介紹hello程序在linux下是如何從一個.c文件一步步變成可執行文件的。對於在運行的過程中可能會出現的一些比較重要的問題,例如虛擬內存,IO等操作進行探究。

 

關鍵詞:程序執行 CSAPP                           

 

 

 

 

 

 

 

 

 

 

目  錄

 

第1章 概述................................................................................................................ - 4 -

1.1 Hello簡介......................................................................................................... - 4 -

1.2 環境與工具........................................................................................................ - 4 -

1.3 中間結果............................................................................................................ - 4 -

1.4 本章小結............................................................................................................ - 4 -

第2章 預處理............................................................................................................ - 5 -

2.1 預處理的概念與作用........................................................................................ - 5 -

2.2在Ubuntu下預處理的命令............................................................................. - 5 -

2.3 Hello的預處理結果解析................................................................................. - 5 -

2.4 本章小結............................................................................................................ - 5 -

第3章 編譯................................................................................................................ - 6 -

3.1 編譯的概念與作用............................................................................................ - 6 -

3.2 在Ubuntu下編譯的命令................................................................................ - 6 -

3.3 Hello的編譯結果解析..................................................................................... - 6 -

3.4 本章小結............................................................................................................ - 6 -

第4章 彙編................................................................................................................ - 7 -

4.1 彙編的概念與作用............................................................................................ - 7 -

4.2 在Ubuntu下彙編的命令................................................................................ - 7 -

4.3 可重定位目標elf格式.................................................................................... - 7 -

4.4 Hello.o的結果解析.......................................................................................... - 7 -

4.5 本章小結............................................................................................................ - 7 -

第5章 鏈接................................................................................................................ - 8 -

5.1 鏈接的概念與作用............................................................................................ - 8 -

5.2 在Ubuntu下鏈接的命令................................................................................ - 8 -

5.3 可執行目標文件hello的格式........................................................................ - 8 -

5.4 hello的虛擬地址空間..................................................................................... - 8 -

5.5 鏈接的重定位過程分析.................................................................................... - 8 -

5.6 hello的執行流程............................................................................................. - 8 -

5.7 Hello的動態鏈接分析..................................................................................... - 8 -

5.8 本章小結............................................................................................................ - 9 -

第6章 hello進程管理....................................................................................... - 10 -

6.1 進程的概念與作用.......................................................................................... - 10 -

6.2 簡述殼Shell-bash的作用與處理流程........................................................ - 10 -

6.3 Hello的fork進程創建過程......................................................................... - 10 -

6.4 Hello的execve過程..................................................................................... - 10 -

6.5 Hello的進程執行........................................................................................... - 10 -

6.6 hello的異常與信號處理............................................................................... - 10 -

6.7本章小結.......................................................................................................... - 10 -

第7章 hello的存儲管理................................................................................... - 11 -

7.1 hello的存儲器地址空間................................................................................ - 11 -

7.2 Intel邏輯地址到線性地址的變換-段式管理............................................... - 11 -

7.3 Hello的線性地址到物理地址的變換-頁式管理.......................................... - 11 -

7.4 TLB與四級頁表支持下的VA到PA的變換................................................ - 11 -

7.5 三級Cache支持下的物理內存訪問............................................................. - 11 -

7.6 hello進程fork時的內存映射..................................................................... - 11 -

7.7 hello進程execve時的內存映射................................................................. - 11 -

7.8 缺頁故障與缺頁中斷處理.............................................................................. - 11 -

7.9動態存儲分配管理........................................................................................... - 11 -

7.10本章小結........................................................................................................ - 12 -

第8章 hello的IO管理.................................................................................... - 13 -

8.1 Linux的IO設備管理方法............................................................................. - 13 -

8.2 簡述Unix IO接口及其函數.......................................................................... - 13 -

8.3 printf的實現分析........................................................................................... - 13 -

8.4 getchar的實現分析....................................................................................... - 13 -

8.5本章小結.......................................................................................................... - 13 -

結論............................................................................................................................ - 14 -

附件............................................................................................................................ - 15 -

參考文獻.................................................................................................................... - 16 -

 

 

 

第1章 概述

1.1 Hello簡介

       P2P過程:首先先有個hello.c的c程序文本,經過預處理->編譯->彙編->鏈接四個步驟生成一個hello的二進制可執行文件,然後由shell新建一個進程給他執行。

       020過程:shell執行他,爲其映射出虛擬內存,然後在開始運行進程的時候分配並載入物理內存,開始執行hello的程序,將其output的東西顯示到屏幕,然後hello進程結束,shell回收內存空間。

1.2 環境與工具

硬件環境:X64 CPU Intel Core i7 6700HQ; 3.2GHz; 16G RAM; 1TB HD Disk

軟件環境:Microsoft Windows10 Home 64位; VMware Workstation 14 Pro; Ubuntu 18.04

開發工具:gcc,readelf,edb

1.3 中間結果

文件名稱

作用

hello.c

源代碼

hello.i

預處理之後的文本文件

hello.s

編譯之後的彙編文件

hello.o

彙編之後的可重定位目標執行

hello

連接之後的可執行目標文件

 

1.4 本章小結

       本章主要簡單介紹了hello的P2P,020過程,列出了本次實驗信息:環境、中 間結果。

 

 

 

第2章 預處理

2.1 預處理的概念與作用

       預處理是計算機在處理一個程序時所進行的第一步,他直接對.c文件進行初步處理將處理後的結果保存在.i文件中,隨後計算機再利用其它部分接着對.i文件進行處理。

2.1.1預處理的概念

計算機用預處理器(cpp)來執行預處理操作,操作的對象就是原始代碼中以字符#開頭的命令,hello.c中就包含了三條這樣會被預處理的語句,如下圖2-1中所示的代碼;除了調用庫這樣的操作之外,程序中的宏定義也會在預處理的時候處理,如圖2-2所示;最後預處理階段會將程序中的註釋刪除掉,因爲這對程序接下來的操作是沒有用的。

       

圖2-1 hello.c中的頭文件               圖2-2 宏定義

2.1.2預處理的作用

       預處理的過程中,對於引用一些封裝的庫或者代碼的這些命令來說,他會告訴預處理器讀取頭文件中用到的庫的代碼,將這段代碼直接插入到程序文件中;對於宏定義來說,會完成對宏定義的替換;註釋會直接刪除掉。最後將處理過後的新的文本保存在hello.i中,後面計算機將直接對hello.i進行操作。

       預處理階段的作用是讓編譯器在隨後對文本進行編譯的過程中,更加方便,因爲訪問庫函數這類操作在預處理階段已經完成,減少了編譯器的工作。

2.2在Ubuntu下預處理的命令

       首先先來介紹一下如何在shell中執行對.c文件的預處理操作:

       linux> gcc –E –o hello.i hello.c

       在這裏運用-o操作,將結果輸出到hello.i文件中,方便我們對預處理過後的文件進行查看。我們可以看一下預處理前後兩個文件大小的差距,如圖2-3所示,預處理前的hello.c文件只有534字節,而預處理後的hello.i文件有66102字節。可見預處理工作中對文本做了很大的改動和補充。

圖2-3 預處理前後文件大小

2.3 Hello的預處理結果解析

我們在2.1節中說到過,預處理只對開頭是#的命令進行操作。也就是說,對於我們程序中定義的變量、寫的函數等這些操作,預處理階段是不會管的,我們首先就來對比一下這一部分。如圖2-4所示,在預處理之前,程序中包含開始的註釋內容、頭文件、全局變量和主函數。而右側是預處理過後的文件,這裏展示了文件的最後幾行,可以看到,從全局變量的定義開始,與預處理之前的文件完全相同,這與2.1中的概念相符。

圖2-4 預處理前後的程序文本

       接下來我們回到hello.i文件的開頭,從圖2-5中可以看出,hello.i程序中並沒有了註釋部分。最後我們再來看hello.i文本的中間部分,首先我們看到左側的圖中從第13行開始有很多的地址,還有如右側圖中的一些代碼部分,右側圖中就是一個結構體變量。這說明,預處理階段,預處理器將需要用到的庫的地址和庫中的函數加入到了文本中,與我們原來不需要預處理的代碼一同構成了hello.i文件,用來被編譯器繼續進行編譯。

圖2-5 hello.i文件內容

2.4 本章小結

       預處理過程是計算機對程序進行操作的起始過程,在這個過程中預處理器會對hello.c文件進行初步的解釋,對頭文件、宏定義和註釋進行操作,將程序中涉及到的庫中的代碼補充到程序中,將註釋這個對於執行沒有用的部分刪除,最後將初步處理完成的文本保存在hello.i中,方便以後的內核器件直接使用。

 

第3章 編譯

3.1 編譯的概念與作用

3.1.1編譯的概念

編譯階段是在預處理之後的下一個階段,在預處理階段過後,我們獲得了一個hello.i文件,編譯階段就是編譯器(ccl)對hello.i文件進行處理的過程。此階段編譯器會完成對代碼的語法和語義的分析,生成彙編代碼,並將這個代碼保存在hello.s文件中。

3.1.2編譯的作用

       編譯器會在編譯階段對代碼的語法進行檢查,如果出現了語法上的錯誤,會在這一階段直接反饋回來,造成編譯的失敗。如果在語法語義等分析過後,不存在問題,編譯器會生成一個過渡的代碼,也就是彙編代碼,在隨後的步驟中,彙編器可以繼續對生成的彙編代碼進行操作。

這裏有一個問題,就是爲什麼我們在預處理的過程中生成的比較大的hello.i文件,在進行完彙編過程後生成的hello.s文件又變小了。我們可以發現,hello.s文件中只存儲了頭文件之後的彙編代碼,至於之前加入的頭文件的代碼具體去了哪裏,會在第五章鏈接進行介紹。

3.2 在Ubuntu下編譯的命令

我們可以利用如下指令來對hello.i文本繼續進行編譯:

linux> gcc –S hello.i –o hello.s

從圖3-1中我們可以看到,利用上述命令編譯過後,我們得到了一個hello.s

文件。

圖3-1 在Ubuntu下編譯的命令

3.3 Hello的編譯結果解析

我們將代碼分成數據、賦值、類型轉換、算數操作、控制轉移數組/指針/結構操作和函數操作這麼幾個部分來具體分析一下。

3.3.1數據

關於數據的定義,我們可以看到hello.c中有一條如圖3-2中的語句,這條語句定義了一個sleepsecs的全局變量。對應到hello.s文件中,就是圖中右側的部分。我們可以看到定義的過程中用.globl聲明瞭這是一個全局變量;.type說明了類型是一個數據;.size說明了這個變量的大小,這裏sleepsecs變量佔了4個字節。關於.text和.type的具體含義及其作用會在第5章連接中進行講解。

需要注意的一點是其實main函數中還定義了i變量,但是i由於是局部變量,所以彙編器並沒有單獨的對他進行處理,而是直接將在這個變量放到了寄存器中。具體是如何控制這個變量的,會在下一小節中介紹。

圖3-2 數據的定義

3.3.2賦值

       我們從上面的圖中可以看到,在定義sleepsecs變量的過程中,同時對其進行了賦值在hello.s中。圖3-3中的語句就是賦值操作轉化成彙編之後的語句。可以看到一共有三行,第一行聲明瞭變量名,第二行中保存的是變量的值,第三行中是存儲的位置,也就是.rodata節中(第五章鏈接的內容)。有一個問題,那就是上圖中的賦值語句中是將2.5賦值給了這個變量,爲什麼在彙編之後就變成了2。注意sleepsecs這個變量的類型是整型,但是2.5是一個浮點型,所以只保存了整數的部分,具體我們會在下一小節中介紹。

圖3-3 sleepsecs的賦值

       接下來我們看一下局部變量i是如何進行賦值的。可以注意到hello.s的36行中有一個對寄存器的尋址操作,這個操作將0放入到了這個地址中。可以確定這個就是對變量i的初始化,因爲i是局部變量,所以直接可以在棧中用一個單元保存這個值,就不需要單獨進行處理了。

圖3-4 局部變量i的賦值

3.3.3類型轉換

       這裏的類型轉換採用的是隱式的轉換。我們可以從上一節的sleepsecs的賦值中可以看出,彙編器並沒有對類型轉換做特別的代碼上的處理,而是直接將2賦值給了sleepsecs。這說明彙編器在對hello.i文件進行彙編的過程中,直接在這個過程中進行了代碼的優化,也就是說在編譯的過程中就完成了浮點型的轉換,將其賦值給了整型,而並沒有在彙編代碼中通過具體的代碼實現,所以對於類型轉換而言,是隱式的。

3.3.4算數操作

       在hello.c的代碼中,只涉及到了一處算數操作,就是在for循環中每次對i進行的加一操作。在3.3.2節中,我們已經看到了,i存儲在寄存器中保存的一個地址中,所以如圖3-5所示,對於i的運算,我們可以直接對寄存器保存的地址中的值進行操作,每次我們將這個值增加1。需要注意的一點是,每次我們進行的是尋址操作,是將地址中的值加1,而不是將地址加一。

圖3-5 算數操作

3.3.5關係

       在hello.c中有兩個關係操作,分別如圖3-6所示,一個是不等於操作,另一個是小於操作。具體到彙編代碼中我們看到他們分別對應了兩句操作。一個是cmpl另一個是一個j加上一些字母。

圖3-6 邏輯操作

這裏就需要用到彙編語言的相關知識了。首先cmpl是一個比較函數,這個函數中將比較的結果保存在條件碼中。條件碼中一共有四位,每一位都有不同的含義。如圖3-7中所示。對於不同的比較結果,操作碼中就保存了不同的值。關於下一行中保存的信息的具體作用,將在下一小節中進行介紹。

圖3-7 操作碼

3.3.6控制轉移

       我們看到,上一節中在每一個cmpl的操作之後,都緊跟着一個j的操作,這個j的含義就是jmp,起到控制函數跳轉的作用,j後面跟的參數,就對應了在滿足什麼條件的時候進行跳轉。圖3-8中列出了不同跳轉指令的含義。我們可以看到,對於每一種跳轉指令都對應了跳轉碼的一種形式。所以我們就可以知道,爲什麼在上一節中,cmpl和j這兩條語句總是同時出現。是因爲在執行條件跳轉的時候,我們必須利用到操作碼中的值。所以在每個條件跳轉之前,都肯定有一個比較指令對操作碼進行設置。

圖3-8 條件跳轉指令

       瞭解了條件跳轉指令是如何執行的之後,我們可以看一下hello.s中具體的編譯結果了。我們看圖3-9中,右側的彙編代碼的29和30行在執行左側代碼中的if判斷操作,通過3-8中的表格我們可以知道je是相等時跳轉,也就是說,當argc等於3的時候,那麼就跳轉到.L2處執行,否則就繼續向下執行。通過對L2的閱讀以及L3中的操作我們可以知道,這是一個循環操作,也就正好對應了左側代碼中的情況。

       通過上面對彙編代碼的分析,我們可以瞭解彙編是如何控制程序在不同地方進行跳轉的了。

圖3-9 彙編中的條件跳轉

3.3.7數組/指針/結構操作

       hello.c中在輸出的時候調用了argv數組中的元素。如圖3-10所示,我們可以看到,在彙編中,我們已經沒有了數組、結構等概念,我們有的只是地址和地址中存儲的值。所以對於一個數組的保存,在彙編中我們只保存了他的起始地址,對應的也就是argv[0]的地址,對於數組的中其他元素,我們利用了數組在申請的過程中肯定是一段連續的地址這樣的性質,直接用起始地址加上偏移量就得到了我們想要的元素的值。

圖3-10 數組操作

3.3.8函數操作

       圖3-11中展示瞭如何對函數進行調用。首先我們應該先了解一下調用函數的過程中我們會用到哪些東西。

       %eax寄存器中保存了函數的返回值。作爲一個函數,我們肯定需要向函數內進行傳參操作,對於參數比較少的情況來說,就直接存儲在特定的寄存器中,如%rdi,%rsi,%rdx,%rcx就分別用來存儲第一至四個參數。X86的及其一共爲我們提供了6個寄存器來保存參數。如果參數多於6個,那麼就只能放在棧中保存了。

       如圖中56行所示,我們直接利用call指令,後面加上調用函數的名稱,就直接可以去到被調用的函數的位置。在被調用的函數執行完畢之後,程序會將函數的返回值存在%eax中,然後執行ret語句,將函數程序返回到調用的地方。這樣就完成了整個的函數調用。

圖3-11 函數操作

3.4 本章小結

本章我們主要介紹了編譯器是如何將文本編譯成彙編代碼的。可以發現,編譯器並不是死板的按照我們原來文本的順序,逐條語句進行翻譯下來的。編譯器在編譯的過程中,不近會對我們的代碼做一些隱式的優化,而且會將原來代碼中用到的跳轉,循環等操作操作用控制轉移等方法進行解析。最後生成我們需要的hello.s文件。

 

第4章 彙編

4.1 彙編的概念與作用

4.1.1彙編的概念

       彙編器(as)將hello.s文件翻譯成機器語言指令,把這些指令打包成一種叫做可重定位目標程序的格式,並將結果保存在hello.o中。這裏的hello.o是一個二進制文件。

4.1.2彙編的作用

       我們知道,彙編代碼也只是我們人可以看懂的代碼,而機器並不能讀懂,真正機器可以讀懂並執行的是機器代碼,也就是二進制代碼。彙編的作用就是將我們之前再hello.s中保存的彙編代碼翻譯成可以攻機器執行的二進制代碼,這樣機器就可以根據這些01代碼,真正的開始執行我們寫的程序了。

4.2 在Ubuntu下彙編的命令

我們可以利用如下指令來對hello.s進行彙編:

linux> gcc –c hello.s –o hello.o

從圖4-1中我們可以看到,利用上述命令編譯過後,我們得到了一個hello.o

文件。

圖4-1 Ubuntu下彙編的命令

4.3 可重定位目標elf格式

       首先先來了解一下ELF格式中都存儲了哪些文件,如圖4-2所示,ELF中存儲了很多不同的節的信息,每一個節中保存了程序中對應的一些變量或者重定位等這些信息,至於爲什麼要保存這些信息,是因爲程序在鏈接的時候會用到這些信息。這些信息的含義以及鏈接的作用我們會在下一章中進行介紹。

圖4-2 典型的ELF可重定位目標文件

圖4-3 各個節節頭的信息

       根據readelf命令的結果,可以獲得ELF文件的一些信息。圖4-3中展示了ELF可重定位文件中各個節節頭的信息。偏移量這一欄中保存了在hello.o這個二進制文件中,對應的節保存在相對於起始地址偏移了這麼多的地方,也就是每一節存在了hello.o中得到哪一個位置上。

       圖4-4中保存了hello.o中的兩個可重定位節中保存的具體信息,分別是.rela.text和.tela.eh.frame。

       .rela.text中保存了代碼的重定位信息,也就是.text節中的信息的重定位信息。可以看到這裏面有.rodata,puts等很多代碼的重定位信息。我們就拿第一條的信息來做分析。首先偏移量中保存了這個重定位信息在當前重定位節中的偏移量,也就是這個重定位信息的存儲位置。第二個是信息這個裏面保存了兩個信息,前面的2個字節的信息保存了這個代碼在彙編代碼中被引用的時候的地址相對於所有彙編代碼的偏移量,也就是這個代碼具體在那個位置被調用了。後面4個字節保存了重定位類型,一個是絕對引用,另一個是相對引用。這也與後面一欄的類型相對應。後面的符號值和符號名稱就比較好理解了,保存了代碼段中具體符號的信息。

圖4-4 可重定位節

       最後還有一個符號表.symtab的信息。這個節中存放了在程序中定義和引用的函數和全局變量的信息。我們在圖4-5中可以看到,有兩個比較明顯的變量,一個就是sleepsecs,另一個是main。這兩個分別是全局變量和定義的函數。size這一欄中保存了他們的大小,可以看到因爲main是一個函數,所以內容相對較多,佔得空間比較大。後面的type保存了變量類型,可以看到main中對應的類型就是FUNC,也就是函數。由於後面的一些符號還沒有進行鏈接這一步所以暫時沒有信息。

圖4-5 .symtab節

4.4 Hello.o的結果解析

       用objdump命令進行反彙編過後,得到了如圖4-6中所示的代碼:

圖4-6 hello.o的反彙編代碼

       對比第三章中的彙編代碼可以發現,hello.o的反彙編程序多出來了上圖中框出來的三個部分,我們依次分析一下多了的這些信息。

       首先是紅框中的信息。紅框中保存了每一條指令的運行時地址,可以看到main函數的初始地址是0,然後依次向下增加。其實在後面的章節中會講到,這個地址只是一個虛擬地址,而不是程序真正執行的地址。

       藍色的框中保存了一些16進制的代碼。可以發現每一個紅框中的指令地址的變化量都是藍色框中一行的字節數。這就可以確定,藍色框中保存的是16進制下的機器指令。我們可以看到地址爲1和8的這兩行,都對應了mov操作,所以對應的16進制的機器碼都是48,這也就說明mov的機器碼是48。

       黃色框中的這段代碼,我們看到底下多了一行信息。這一行代碼聲明瞭這個變量的具體類型。同時注意到偏移地址爲26的這一指令,這個call操作也不是想第三章一樣直接call對應的函數名字了,而是一個具體的相對地址,能夠讓程序在跑的過程中,直接跳轉到的地方。

       總體來說,hello.o文件的反彙編代碼中最主要的就是增加了地址這個概念,將代碼中的一切信息與地址聯繫起來。這樣做的目的是因爲在程序運行的過程中,都是在進行地址操作,所以hello.o文件可以說更接近了計算機可以執行的文件。

4.5 本章小結

       彙編器將彙編代碼處理成機器可以看懂的機器碼,也就是二進制代碼。二進制代碼較彙編代碼來說,雖然可讀性變得比較差,但是在執行效率方面有了非常大的提升,彙編代碼雖然已經在原來的文本的基礎上進行了優化,但是還是存在着一些字符等不能夠直接處理的數據。但是二進制代碼中,已經將所有的指令、函數名字等量變成了相應的存儲地址,這樣機器就可以直接讀取這些代碼並執行。所以總的來說hello.o已經非常接近一個機器可以執行的代碼了。

 

5章 鏈接

5.1 鏈接的概念與作用

5.1.1鏈接的概念

       鏈接是通過鏈接器(ld)將各種代碼和數據片斷收集並組合成一個單一文件的過程。這個文件可以被加載(複製)到內存並執行。

5.1.2鏈接的作用

       因爲有了鏈接這個概念的存在,所以我們的代碼纔回變得比較方便和簡潔,同時可移植性強,模塊化程度比較高。因爲鏈接的過程可以使我們將程序封裝成很多的模塊,我們在變成的過程中只用寫主程序的部分,對於其他的部分我們有些可以直接調用模塊,就像C中的printf一樣。

       作爲編譯的多後一步鏈接,就是處理當前程序調用其他模塊的操作,將該調用的模塊中的代碼組合到相應的可執行文件中去。

5.2 在Ubuntu下鏈接的命令

我們可以利用如下指令來對hello.o進行彙編:

linux> ld /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbeginT.o -L/usr/lib/gcc/x86_64-linux-gnu/5 hello.o -lc -lgcc -dynamic-linker /lib64/ld-linux-x86-64.so.2  /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -o hello

 

從圖5-1中我們可以看到,利用上述命令編譯過後,我們得到了一個hello.o

文件。

圖5-1 在Ubuntu下鏈接的命令

5.3 可執行目標文件hello的格式

圖5-2 ELF各節信息

       圖5-2中保存了可執行文件hello中的各個節的信息。可以看到hello文件中的節的數目比hello.o中多了很多,說明在鏈接過後有很多文件有添加了進來。下面列出每一節中各個信息條目的含義:

       名稱和大小這個條目中存儲了每一個節的名稱和這個節在重定位文件種所佔的大小。

       地址這個條目中,保存了各個節在重定位文件中的具體位置也就是地址。     偏移量這一欄中保存的是這個節在程序裏面的地址的偏移量,也就是相對地址。

5.4 hello的虛擬地址空間

       圖5-3中分別是edb顯示的hello文件的信息和5.3中輸出的ELF文件的信息。我們可以着重看一下紅框中的部分,這一部分中存儲的ELF頭信息,也就是ELF文件最開始存的數據。可以看到通過這兩種方式得到的信息是完全相同的。再來說一下這個信息的含義,我們看到右側的ELF頭下面有很多文字註釋,這其實就是ELF頭中存儲的信息,即ELF整個文件的基本信息。

圖5-3 ELF文件信息對比

       有一個比較奇怪的問題,就是在5.3中我們可以看到,在第17項開始的時候,地址就發生了一個比較大的變化。而edb中也沒有顯示出來這些節的信息。這是因爲這些節中保存了共享庫中的信息。在edb中如果想獲得這些信息,需要單獨進行查看,如圖5-4所示,就是從.dynamic節開始的信息。

圖5-4 .dynamic節及其後面節的信息

5.5 鏈接的重定位過程分析

首先先看一下main函數中有哪些不一樣的地方,如圖5-5所示,圖中上面的程序是hello.o文件中main函數中的的一部分代碼,下面的代碼是hello文件中對應部分的反彙編代碼。可以發現,在鏈接之前,hello.o中的註釋僅僅是對main函數的一個便宜量,並且相應的彙編代碼中lea後對於%rip的偏移量也是0,也就是說對於hello.o來說,我們並不能準確的瞭解到這段代碼的含義。

再看鏈接之後的反彙編,可以看到反彙編之後,代碼註釋中的內容直接變成了系統的IO庫中的函數,lea後面跟的偏移量也是正確的偏移量了。接下來的call指令也是一樣,在hello中準確的指明瞭具體調用的函數,而hello.o文件中也只有一個main函數的偏移量。

可以看到,在鏈接的過程中,鏈接器會將我們鏈接的庫函數或者其他文件在可執行文件中準確的定位出來。

圖5-5 hello.o與hello的反彙編

       可以看到,在hello.o的反彙編代碼中,只有一個main函數,但是對於hello的反彙編代碼來說,可以看到很多如_init樣子的函數。這些函數都是在鏈接的過程中,被加載到可執行文件中的。

       通過上面的對比可以看到,在鏈接的過程中,鏈接器會進行如下幾個過程:

       將代碼、符號、變量、函數等進行重定位,使這些元素在可執行文件中可以有明確的虛擬內存地址。具體的執行方式就是用.o文件中的重定位條目,這個條目告訴鏈接器應該如何修改這個引用的地址。

       將調用的函數都加載到可執行文件中,使其變成一個完整的文件,在文件中涉及到的任何符號或者函數等信息在文件中都有定義。

       接下來分析一下鏈接器是如何進行重定位的。我們就對圖5-6中紅框中的語句的重定位進行分析。首先左側存儲了hello.o中代碼節的重定位條目。紅框中的第一個信息偏移量存儲的是這個符號的出現位置的偏移量,這裏是0x1d,對應於右側紅框中的我們可以看到是這個call函數的位置,這段指令的起始地址是0x1c,由於call函數的機器碼是e8佔了一個字節,所以真正的符號出現的地址是0x1d,這兩個地址相同,說明這個重定位條目對應的是這個符號。

      

圖5-6 hello.o的重定位條目

       第二個信息的前兩個字節保存了這個符號在符號表中的偏移量,可以看到紅框中的信息爲0xc,對應在符號表中可以看到,puts在符號表中的偏移量是12,對應的十六進制的值就是0xc,說明了被重定位的符號應該是puts。後面的類型保存了是相對地址還是絕對地址。

圖5-7 hello.o中的符號表

       總體來說,重定位的過程就是應用重定位文件中存儲的信息,在對應的符號表和彙編代碼中將要重定位的符號或者函數的位置準確的放到可執行文件中。

5.6 hello的執行流程

       hello在執行的過程中一共要執行三個大的過程,分別是載入、執行和退出。載入過程的作用是將程序初始化,等初始化完成後,程序才能夠開始正常的執行。如圖5-7所示,由於hello程序只有一個main函數,所以在程序執行的時候主要都是在main函數中。又因爲main函數中調用了很多其它的庫函數,所以可以看到,在main函數執行的過程中,會出現很多其他的函數。

圖5-7 hello的執行流程

5.7 Hello的動態鏈接分析

       如圖5-8中所示,在執行函數dl_init的前後,地址0x600ff0中的值由0發生了變化。我們可以藉助圖5-2中的信息,得到這個地址是.got節的開始,而got中是一個全局函數表。這就說明,這個表中的信息是在程序執行的過程中動態的鏈接進來的。也就是說,我們在之前重定位等一系列工作中,用到的地址都是虛擬地址,而我們需要的真實的地址信息會在程序執行的過程中用動態鏈接的方式加入到程序中。當我們每次從PLT表中查看數據的時候,會首先根據PLT表訪問GOT表,得到了真實地址之後再進行操作。

圖5-8 dl_init前後文件變化

分析hello程序的動態鏈接項目,通過edb調試,分析在dl_init前後,這些項目的內容變化。要截圖標識說明。

5.8 本章小結

鏈接的過程,是將原來的只保存了你寫的函數的代碼與代碼用所用的庫函數合併的一個過程。在這個過程中鏈接器會爲每個符號、函數等信息重新分配虛擬內存地址,方法就是用每個.o文件中的重定位節與其它的節想配合,算出正確的地址。同時,將你會用到的庫函數加載(複製)到可執行文件中。這些信息一同構成了一個完整的計算機可以運行的文件。鏈接讓我們的程序做到了很好的模塊化,我們只需要寫我們的主要代碼,對於讀入、IO等操作,可以直接與封裝的模塊相鏈接,這樣大大的簡化了代碼的書寫難度。

 

 

6章 hello進程管理

6.1 進程的概念與作用

6.1.1進程的概念

       進程的經典定義就是一個執行中的程序的實例。

6.1.2進程的作用

       通過進程這個概念,我們在運行一個程序的過程中會得到一個假象,就好像我們的程序時系統中當前運行的唯一的程序一樣。我們的程序好像是獨佔的使用處理器和內存。處理器好像就是無間斷的一條接一條的執行我們程序中的指令。最後我們程序中的代碼和數據好像是系統內存中唯一的對象。

6.2 簡述殼Shell-bash的作用與處理流程

       shell是一個linux中提供的應用程序,他在操作系統中爲用戶與內核之間提供了一個交互的界面,用戶可以通過這個界面訪問操作系統的內核服務。他的處理流程如下:

  1. 從界面中讀取用戶的輸入。
  2. 將輸入的內容轉化成對應的參數。
  3. 如果是內核命令就直接執行,否則就爲其分配新的子進程繼續運行。
  4. 在運行期間,監控shell界面內是否有鍵盤輸入的命令,如果有需要作出相應的反應

6.3 Hello的fork進程創建過程

       首先先來了解一下fork函數的機制。父進程通過調用fork函數創建一個新的子進程。新創建的子進程幾乎但不完全與子進程相同。在創建子進程的過程中,內核會將父進程的代碼、數據段、堆、共享庫以及用戶棧這些信息全部複製給子進程,同時子進程還可以讀父進程打開的副本。唯一的不同就是他們的PID,這說明,雖然父進程與子進程所用到的信息幾乎是完全相同的,但是這兩個程序卻是相互獨立的,各自有自己獨有的用戶棧等信息。

       fork函數雖然只會被調用一次,但是在返回的時候卻有兩次。在父進程中,fork函數返回子進程的PID;在子進程中,fork函數返回0。這就提供了一種用fork函數的返回值來區分父進程和子進程的方法。

       同時fork在使用的過程中,有一個令人比較頭疼的問題,就是父進程和子進程是併發執行的所以我們不能夠準確的知道那個進程先執行或者先結束。這也就造成了每次執行的輸出結果可能是不同的,也是不可預測的。

       圖6-1中展示了一個程序在調用了fork函數之後的行爲。原程序中,在子進程中新型了++x操作,在父進程中進行了—x操作。通過進程圖我們可以看到,兩個進程的輸出分別爲2和0,證實了我們上面說過的兩個進程是獨立的。

圖6-1 一個調用fork函數代碼的進程圖

6.4 Hello的execve過程

       execve函數的作用是在當前進程的上下文中加載並運行一個新的程序。與fork函數不同的是,fork函數創建了一個新的進程來運行另一個程序,而execve直接在當前的進程中刪除當前進程中現有的虛擬內存段,並穿件一組新的代碼、數據、堆和用戶棧的段。將棧和堆初始化爲0,代碼段與數據段初始化爲可執行文件中的內容,最後將PC指向_start的地址。在CPU開始引用被映射的虛擬頁的時候,內核纔會將需要用到的數據從磁盤中放入內存中。

       圖6-2中展示了相應的系統映像。

圖6-2 系統映像

6.5 Hello的進程執行

我們在之前的小節中已經提到過了,當前的CPU中並不是只有我們一個程序在運行,這只是一個假象,實際上有很多進程需要執行。要了解具體是怎樣進行的,首先先了解幾個概念。

上下文信息:上下文就是內核重新啓動一個被搶佔的進程所需要的狀態,它由 通用寄存器、浮點寄存器、程序計數器、用戶棧、狀態寄存器、內核棧和各種內 核數據結構等對象的值構成。

進程時間片:一個進程執行它的控制流的一部分的每一時間段叫做時間片。

用戶模式與內核模式:處理器通常使用一個寄存器提供兩種模式的區分,該寄 存器描述了進程當前享有的特權,當沒有設置模式位時,進程就處於用戶模式中, 用戶模式的進程不允許執行特權指令,也不允許直接引用地址空間中內核區內的 代碼和數據;設置模式位時,進程處於內核模式,該進程可以執行指令集中的任 何命令,並且可以訪問系統中的任何內存位置。

瞭解了一些基本概念之後,我們來分析一下hello程序中的具體執行情況。圖6-3中展示了hello的代碼中會主動引起中斷的一個代碼。

圖6-3 hello文件的部分代碼

這段代碼中調用了sleep函數, 我們知道這個函數中用到的參數的值爲2,所以這個sleep函數的作用就是當運行到這一句的時候,程序會產生一箇中斷,內核會將這個進程掛起,然後運行其它程序,當內核中的計時器到了2秒鐘的時候,會傳一個時間中斷給CPU,這時候CPU會將之前掛起的進程放到運行隊列中繼續執行。

從圖6-4中我們可以比較清晰的看出CPU是如何在程序建進行切換的。假設hello進程在sleep之前一直在順序執行。在執行到sleep函數的時候,切換到內核模式,將hello進程掛起,然後切換到用戶模式執行其它進程。當到了2秒之後,發生一箇中斷,切換到內核模式,繼續運行之前被掛起的進程。最後切換回用戶模式,繼續運行hello進程。

圖6-4 hello進程的上下文切換

6.6 hello的異常與信號處理

圖6-5中顯示了hello程序正常運行的結果。可以看到在執行ps命令之後,程序後臺並沒有hello進程正在執行了,說明進程正常結束,已經被回收了。

圖6-5 正常運行

       圖6-6展示了在進程運行的過程中從鍵盤輸入Ctrl+Z命令後的結果。可以看到在執行完三次循環之後,按下鍵盤,shell父進程會收到一個SIGSTP信號,這個信號的功能是將程序掛起並且放到後臺。通過ps命令我們可以看到,hello命令並沒有結束。接下來我們用fg命令將JID最大的放到前臺,也就是剛剛掛起的hello進程,可以看到進程又繼續執行完了剩下的7次循環。

圖6-6 執行Ctrl+Z後

       圖6-7中是輸入Ctrl+C操作後的結果。從鍵盤中輸入Ctrl+C後,shell父進程會收到一個SIGINT信號,這個信號的功能是直接將子進程結束。可以在ps中看到,已經沒有了hello進程。說明Ctrl+C會直接將進程結束並回收掉。

圖6-7 執行Ctrl+C操作

       jobs命令可以查看當前執行的關鍵操作是什麼,比如我們執行了一個Ctrl+Z命令將進程掛起後,用jobs命令就能看到Ctrl+Z這條命令。

圖6-8 jobs命令

       如圖6-9所示,pstree命令將所有的進程按照樹狀結構打印出來。這樣我們就可以知道不同進程之間的關係。

圖6-9 pstree命令

6.7本章小結

       本章中闡述了進程的概念以及他在計算機中具體是如何在使用的。其次,還介紹瞭如何利用shell這個平臺來對進程進行監理調用或發送信號等一系列操作。

 

7章 hello的存儲管理

7.1 hello的存儲器地址空間

邏輯地址:又稱相對地址,是程序運行由CPU產生的與段相關的偏移地址部分。他是描述一個程序運行段的地址。

物理地址:程序運行時加載到內存地址寄存器中的地址,內存單元的真正地址。他是在前端總線上傳輸的而且是唯一的。在hello程序中,他就表示了這個程序運行時的一條確切的指令在內存地址上的具體哪一塊進行執行。

線性地址:是經過段機制轉化之後用於描述程序分頁信息的地址。他是對程序運行區塊的一個抽象映射。

虛擬地址:其實虛擬地址跟線性地址是一個東西,都是對程序運行區塊的相對映射。

就hello而言,他是在物理地址上運行的,但是對於CPU而言,CPU看到的hello運行的地址是邏輯地址,在具體操作的過程中,CPU會將邏輯地址轉換成線性地址再變成物理地址。

7.2 Intel邏輯地址到線性地址的變換-段式管理

一個邏輯地址由兩部份組成,段標識符: 段內偏移量。段標識符是由一個16位長的字段組成,稱爲段選擇符。其中前13位是一個索引號。後面3位包含一些硬件細節,如圖7-1所示:

圖7-1 段選擇符

       索引號是“段描述符(segment descriptor)”的索引,很多個段描述符,就組了一個數組,叫“段描述符表”,這樣,可以通過段標識符的前13位,直接在段描述符表中找到一個具體的段描述符,這個描述符就描述了一個段,每一個段描述符由8個字節組成,如圖7-2:

圖7-2 段選擇符

       其中Base字段,它描述了一個段的開始位置的線性地址,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每個進程自己的,就放在所謂的“局部段描述符表(LDT)”中,由段選擇符中的T1字段表示選擇使用哪個,=0,表示用GDT=1表示用LDT。GDT在內存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT則在ldtr寄存器中。如下圖:

圖7-3 概念關係說明

       具體的轉換步驟如下:

  1. 給定一個完整的邏輯地址[段選擇符:段內偏移地址。
  2. 看段選擇符的T1=0還是1,知道當前要轉換是GDT中的段,還是LDT中的段,再根據相應寄存器,得到其地址和大小。可以得到一個數組。
  3. 取出段選擇符中前13位,在數組中查找到對應的段描述符,得到Base,也就是基地址。
  4. 線性地址 = Base + offset。

7.3 Hello的線性地址到物理地址的變換-頁式管理

       線性地址到物理地址的轉換是通過頁的這個概念完成的。線性地址被分爲以固定長度爲單位的組,稱爲頁。

首先 Linux 系統有自己的虛擬內存系統,其虛擬內存組織形式如圖 7-4所示,Linux 將虛擬內存組織成一些段的集合,段之外的虛擬內存不存在因此不需要記錄。內核爲hello進程維護一個段的任務結構即圖中的task_struct,其中條目mm指向一個mm_struct,它描述了虛擬內存的當前狀態,pgd 指向第一級頁表的基地址(結合一個進程一串頁表),mmap指向一個vm_area_struct 的鏈表,一個鏈表條目對應一個段,所以鏈表相連指出了 hello 進程虛擬內存中的所有段。

圖7-4 Linux是如何組織虛擬內存的

       CPU芯片上有一個專門的硬件叫做內存管理單元(MMU),這個硬件的功能就是動態的將虛擬地址翻譯成物理地址的。這個表示如何工作的呢,如圖7-5所示。N爲的虛擬地址包含兩個部分,一個p位的虛擬頁面偏移(VPO)和一個(n-p)位的虛擬頁號(VPN)。MMU利用VPN來選擇適當的PTE(頁表條目)。接下來在對應的PTE中獲得PPN(物理頁號),將PPN與VPO串聯起來,就得到了相應的物理地址。

圖7-5 使用頁表的地址翻譯

7.4 TLB與四級頁表支持下的VA到PA的變換

       首先來介紹一下TLB具體是什麼東西。我們注意到,每次在進行虛擬地址翻譯的過程中都會有訪問PTE的操作,如果在比較極端的情況下,就會存在訪存的操作,這樣的效率是很低的。TLB的運用,就可以將PTE上的數據緩存在L1中,也就是TLB這樣一個專用的部件,他會將不同組中的PTE緩存在不同的位置,提高地址翻譯的效率。

       其次我們來介紹一下多級頁表的概念。在前面我們瞭解了一級頁表是如何進行工作的。可以發現一級頁表有一個弊端,就是對於每一個程序,內核都會給他分配一個固定大小的頁表,這樣有一些比較小的程序會用不到開出的頁表的一些部分,就造成了空間的浪費,多級頁表就很好的解決了這個問題。以二級頁表爲例,首先我們先開一個比較小的一級頁表,我們將完整的頁表分組,分別對應到開出來的一節頁表的一個PTE中,在執行程序的過程中,如果我們用到了一個特定的頁表,那麼我們就在一級頁表後面動態的開出來,如果沒用到就不開,這樣就大大的節省了空間。

       知道了上述概念之後,我們就來看一下虛擬地址是如何在四級頁表中轉換的。如圖 7-6,CPU產生虛擬地址VA,VA傳送給MMU,MMU使用前36位VPN作爲TLBT(前32位+TLBI(後4位)向TLB中匹配,如果命中,則得到 PPN (40bit)與VPO(12bit)組合成 PA(52bit)。如果TLB中沒有命中,MMU 向頁表中查詢,CR3確定第一級頁表的起始地址,VPN1(9bit)確定在第一級頁表中的偏移量,查詢出 PTE,如果在物理內存中且權限符合,確定第二級頁表的起始地址,以此類推,最終在第四級頁表中查詢到 PPN,與VPO組合成PA,並且向TLB 中添加條目。

圖7-6 四級頁表下的地址翻譯情況

7.5 三級Cache支持下的物理內存訪問

       在上一節中我們已經獲得了物理地址VA,我們接着圖7-6的右側部分進行說明。使用CI(後六位再後六位)進行組索引,每組8路,對8路的塊分別匹配 CT(前40位)如果匹配成功且塊的valid標誌位爲1,則命中(hit),根據數據偏移量CO(後六位)取出數據返回。 如果沒有匹配成功或者匹配成功但是標誌位是 1,則不命中(miss),向下一級緩存中查詢數據(L2 Cache->L3 Cache->主存)。查詢到數據之後,一種簡單的放置策略如下:如果映射到的組內有空閒塊,則直接放置,否則組內都是有效塊,產生衝突(evict),則採用最近最少使用策略 LFU 進行替換。

7.6 hello進程fork時的內存映射

       在7.3節中我們已經提到過了mm_struct和vm_area_struct這兩個標記符,這裏我們就需要用到他們。先來介紹一下:

       mm_struct(內存描述符):描述了一個進程的整個虛擬內存空間。

       vm_area_struct(區域結構描述符):描述了進程的虛擬內存空間的一個區間。

       在用fork創建內存的時候,我們需要以下三個步驟:

  1. 創建當前進程的mm_struct,vm_area_struct和頁表的原樣副本。
  2. 兩個進程的每個頁面都標記爲只讀頁面。
  3. 兩個進程的每個vm_area_struct都標記爲私有,這樣就只能在寫入時複製。

7.7 hello進程execve時的內存映射

       execve函數在shell中加載並運行包含在可執行目標文件hello中的程序,用hello程序有效的替代了當前程序。加載並運行hello需要以下幾個步驟:

  1. 刪除已存在的用戶區域。刪除shell虛擬地址的用戶部分中的已存在的區域結構。
  2. 映射私有區域。爲hello的代碼、數據、bss 和棧區域創建新的區域結構。所有這些新的區域都是私有的、寫時複製的。代碼和數據區域被映射爲hello 文件中的.text和.data 區。bss 區域是請求二進制零的,映射到匿名文件,其大小包含在hello 中。棧和堆區域也是請求二進制零的,初始長度爲零。圖7.7 概括了私有區域的不同映射。
  3. 映射共享區域。如果hello程序與共享對象(或目標)鏈接,比如標準C 庫libc. so, 那麼這些對象都是動態鏈接到這個程序的,然後再映射到用戶虛擬地址空間中的共享區域內。
  4. 設置程序計數器(PC) 。execve 做的最後一件事情就是設置當前進程上下文中的程序計數器,使之指向代碼區域的入口點。

7.8 缺頁故障與缺頁中斷處理

       缺頁現象的發生是由於頁表只相當於磁盤的一個緩存,所以不可能保存磁盤中全部的信息,對於有些信息的查詢就會出現查詢失敗的情況,也就是缺頁。

       對於一個訪問虛擬內存的指令來說,如果發生了缺頁現象,CPU就會觸發一個缺頁異常。缺頁異常會調用內核中的缺頁異常處理程序,該程序會選擇一個犧牲頁,例如圖7-7中存放在PP3中的VP4,如果VP4已經被更改,那就先將他存回到磁盤中。

       找到了要存儲的頁後,內核會從磁盤中將需要訪問的內存,例如圖7-7所示的VP3放入到之前已經操作過的PP3中,並且將PTE中的信息更新,這樣就成功的將一個物理地址緩存在了頁表中。當異常處理返回的時候,CPU會重新執行訪問虛擬內存的操作,這個時候就可以正常的訪問,不會發生缺頁現象了。

圖7-7 缺頁現象

7.9動態存儲分配管理

7.9.1動態內存分配器的基本原理

動態內存分配器維護着一個進程的虛擬內存區域,稱爲堆。分配器將堆視爲 一組不同大小的塊的集合來維護。每個塊就是一個連續的虛擬內存片,要麼是已 分配的,要麼是空閒的。已分配的塊顯式地保留爲供應用程序使用。空閒塊可用 來分配。空閒塊保持空閒,直到它顯式地被應用所分配。一個已分配的塊保持已 分配狀態,直到它被釋放,這種釋放要麼是應用程序顯式執行的,要麼是內存分 配器自身隱式執行的。  分配器分爲兩種基本風格:顯式分配器、隱式分配器。 

顯式分配器:要求應用顯式地釋放任何已分配的塊。例如C程序中的malloc和free。 

隱式分配器:要求分配器檢測一個已分配塊何時不再使用,那麼 就釋放這個塊, 自動釋放未使用的已經分配的塊的過程叫做垃圾收集。 例如Lisp、ML以及Java之類的高級語言就依賴垃圾收集來釋放已分配的內存。

7.9.2隱式空閒鏈表分配器原理

       隱式空閒鏈表有兩種形式,我們分別來介紹一下。

圖8-8展示的是第一種形式。首先說明一下每個部分的意義。頭部一共四個字節,前三個字節存儲的是塊的大小,最後一個字節存儲的是當前這個塊是空閒塊還是已分配的塊,0代表空閒塊,1代表已分配的塊。中間的有效載荷就是用於存放已分配的塊中的信息用的。最後的填充部分是爲了地址對齊等一些要求用的。

圖7-8 一個簡單的堆塊格式

既然是鏈表,隱式鏈表的結構就是根據地址從小到大進行連接的,如圖7-9。其中的每一個元素表示的是一個空閒塊或者一個分配塊,由於空閒塊會合並我的特性,鏈表中的元素的連接一定是空閒塊的分配塊交替連接的。

至於空閒塊是如何進行合併的,因爲有了 Footer,所以我們可以方便的對前面的空閒塊進行合併。合併的 情況一共分爲四種:前空後不空,前不空後空,前後都空,前後都不空。對於四 種情況分別進行空閒塊合併,我們只需要通過改變 Header 和 Footer 中的值就可以 完成這一操作。

圖7-9 隱式空閒鏈表結構

圖7-10是隱式的另一種結構,可以看到與上面的不同的是,只一種結構在最後多了一個與頭部相同的結構,這個結構叫做腳部。這個新的結構的作用就是爲了在空閒塊合併的時候比較方便高效。因爲如果利用之前的結構,在合併前面的空閒塊的時候,由於我們不知道前面的塊的大小,所以我們不能獲得前面塊的起始位置,這樣就只能從鏈表的開始來找一遍。有了腳部,我們就可以利用腳部中存儲的信息來獲得前一個塊中的地址。

圖7-10 使用邊界標記的堆塊格式

7.9.3顯式空閒鏈表基本原理

圖7-11是顯示空閒鏈表的格式,可以看到,與隱式結構不同的是,顯示結構在空閒塊中增加了8個字節,分別保存當前空閒塊的前驅空閒塊的地址和後繼空閒塊的地址。也就是說,顯式的結構比隱式結構多維護了一個鏈表,就是空閒塊的鏈表。這樣做的好處就是在我們在malloc的時候,隱式的方法是要遍歷所有的塊,包括空閒塊了分配塊。但是顯式的結構只需要在空閒塊中維護的鏈表檢索就可以了,這樣降低了在malloc時候的複雜度。

關於空閒塊的維護方式一共有兩種,一種是後進先出的方式,另一種是按照地址的方式。按照地址維護很好理解,與隱式的結構大致相同。後進先出的方式的思想是,當一個分配塊被free之後,將這個塊放到鏈表的最開頭,這樣在malloc的時候會首先看一下最後被free的塊是否符合要求。這樣的好處是釋放一個塊的時候比較高效,直接放在頭部就可以。

圖7-11 使用雙向空閒鏈表的堆塊格式

 

7.10本章小結

       本章介紹了儲存器的地址空間,講述了虛擬地址、物理地址、線性地址、邏輯地址的概念,還有進程fork和execve時的內存映射的內容。描述了系統如何應對那些缺頁異常,最後描述了malloc的內存分配管理機制(C語言爲例)。可以看到真正高效的運行起一個程序來是很複雜的。

 

8章 hello的IO管理

8.1 Linux的IO設備管理方法

設備的模型化:文件

設備管理:unix io接口

所有的I/O設備(例如網絡、磁盤和終端)都被模型化爲文件,而所有的輸入和輸出都被當作對相應文件的讀和寫來執行。這種將設備優雅地映射爲文件的方式,允許Linux內核引出一個簡單、低級的應用接口,稱爲Unix I/O,這使得所有的輸入和輸出都能以一種統一且一致的方式來執行,這就是Unix I/O接口。

8.2 簡述Unix IO接口及其函數

Unix I/O 接口統一操作:

  1. 打開文件。一個應用程序通過要求內核打開相應的文件,來宣告它想要訪問一個I/O設備,內核返回一個小的非負整數,叫做描述符,它在後續對此文件的所有操作中標識這個文件,內核記錄有關這個打開文件的所有信息。
  2. Shell創建的每個進程都有三個打開的文件:標準輸入,標準輸出,標準錯誤。
  3. 改變當前的文件位置:對於每個打開的文件,內核保持着一個文件位置k,初始爲0,這個文件位置是從文件開頭起始的字節偏移量,應用程序能夠通過執行seek,顯式地將改變當前文件位置k。
  4. 讀寫文件:一個讀操作就是從文件複製n>0個字節到內存,從當前文件位置k開始,然後將k增加到k+n,給定一個大小爲m字節的而文件,當k>=m時,觸發EOF。類似一個寫操作就是從內存中複製n>0個字節到一個文件,從當前文件位置k開始,然後更新k。
  5. 關閉文件,內核釋放文件打開時創建的數據結構,並將這個描述符恢復到可用的描述符池中去。

Unix I/O 函數:

  1. int open(char* filename,int flags,mode_t mode)

這個函數會打開一個已經存在的文件或者創建一個新的文件。

  1. int close(fd)

這個函數會關閉一個打開的文件。

  1. ssize_t read(int fd,void *buf,size_t n)

這個函數會從當前文件位置複製字節到內存位置。

  1. ssize_t wirte(int fd,const void *buf,size_t n)

這個函數從內存複製字節到當前文件位置。

8.3 printf的實現分析

       printf需要做的事情是:接受一fmt的格式,然後將匹配到的參數按照fmt格式輸出。圖8-1是printf的代碼,我們可以發現,他調用了兩個外部函數,一個是vsprintf,還有一個是write。

圖8-1 printf函數的代碼

       從圖8-2中的vsprintf函數可以看出,這個函數的作用是將所有的參數內容格式化後存入buf,然後返回格式化數組的長度。

write函數是將buf中的i個元素寫到終端的函數。

Printf的運行過程:

從vsprintf生成顯示信息,顯示信息傳送到write系統函數,write函數再陷阱-系統調用 int 0x80或syscall.字符顯示驅動子程序。從ASCII到字模庫到顯示vram(存儲每一個點的RGB顏色信息)。顯示芯片按照刷新頻率逐行讀取vram,並通過信號線向液晶顯示器傳輸每一個點(RGB分量)。

圖8-2 vsprintf函數

8.4 getchar的實現分析

       異步異常-鍵盤中斷的處理:當用戶按鍵時,鍵盤接口會得到一個代表該按鍵 的鍵盤掃描碼,同時產生一箇中斷請求,中斷請求搶佔當前進程運行鍵盤中斷子 程序,鍵盤中斷子程序先從鍵盤接口取得該按鍵的掃描碼,然後將該按鍵掃描碼 轉換成ASCII碼,保存到系統的鍵盤緩衝區之中。

       圖8-3中展示了getchar的代碼,可以看出,這裏面的getchar調用了一個read函數,這個read數是將整個緩衝區都讀到了buf裏面,然後將返回值是緩衝區的長度。我們可以發現,如果buf長度爲0,getchar纔會調用read函數,否則是直接將保存的buf中的最前面的元素返回。

圖8-3 getchar函數代碼

8.5本章小結

       本章節講述了一下linux的I/O設備管理機制,瞭解了開、關、讀、寫、轉移文件的接口及相關函數,簡單分析了printf和getchar函數的實現方法以及操作過程。

結論

       hello程序終於走完了他一生的過程,然我們來回顧一下他從一個.c文件是怎樣一步一步的變成可以輸出我們想看到的結果的程序:

  1. 我們首先通過各種各樣的文本編輯器,將我們用高級語言編寫的程序存到了hello.c文件中。
  2. 預處理器將hello.c文件經過初步的修改變成了hello.i文件。
  3. 接着編譯器將hello.i文件處理成爲了彙編代碼並保存在了hello.s文件中。
  4. 然後彙編器將hello.s文件處理成了可重定位的目標程序,也就是hello.o文件,這個時候,我們的程序離可以運行就只差一步了。
  5. 最後鏈接器將我們的hello.o與外部文件進行鏈接,終於我們得到了可以跑起來的hello文件了。
  6. 當我們在shell中輸入運行hello文件的命令的時候,內核會爲我們分配好運行程序所需要的堆、用戶棧、虛擬內存等一系列信息。方便我們的hello程序能夠正常的運行。
  7. 當我們需要從外部對hello程序進行操控的時候,我們只需要在鍵盤上給一個相應的信號,他就會按照我們的指令來執行。
  8. 當我們的hello需要訪問磁盤中的信息的時候,這時候CPU看到了他找不到的地址VA,他利用自己的工具MMU將他翻譯成了可以看懂的地址。
  9. 最後當我們的hello執行完所有工作之後,他也就結束了字節的一生,最後被shell回收掉了。

通過這次大作業,我更加全面系統的瞭解了這門課程,對書中的知識有了更加全面的認識。同時感受到了計算機系統的複雜性以及嚴密性。我們一個程序的成功運行需要多少計算機硬件和軟件的共同配合。

 

附件

文件名稱

作用

hello.c

源代碼

hello.i

預處理之後的文本文件

hello.s

編譯之後的彙編文件

hello.o

彙編之後的可重定位目標執行

hello

連接之後的可執行目標文件

 

 

參考文獻

[1] Bryant,R.E. 深入理解計算機系統

[2] LINUX 邏輯地址、線性地址、物理地址和虛擬地址

:https://www.cnblogs.com/zengkefu/p/5452792.html

[3] Linux內核中的printf實現

https://blog.csdn.net/u012158332/article/details/78675427

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章