Hello的一生
關鍵詞:Hello,預處理,編譯,彙編,連結,程序管理,存儲管理,IO管理。
摘要:
我曆史長河中一個個菜鳥與我擦肩而過,隻有CS知道我的生、我的死,我的坎坷,“隻有 CS 知道……我曾經……來…………過……”————未來一首關于Hello的歌曲繞梁千日不絕 !!
這篇論文,就将詳盡的分析hello程式從.c到執行完畢這個過程中的每一步,包括:預處理,編譯,彙編,連結,程序管理,存儲管理,IO管理幾個部分。hello程式雖然簡單,雖然隻是我們最開始的程式,但是它也具有着代表性,幾乎所有的程式的運作都與hello相似,或者說建立在hello運作的方式之上,了解hello的處理過程,有助于我們深入了解計算機系統的組成部分,了解計算機執行一個程序的過程中都進行了什麼操作。
通過這個大作業,我又一遍複習了計算機系統這門學科,并更加深入地了解了計算機組成原理,懂得了計算機執行一個程序的過程中具體都進行了什麼操作。這個大作業使我受益匪淺。
目 錄
第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是Program to process的縮寫。
O2O是Zero to Zero的縮寫。
接下來就簡略的介紹一下這兩個過程。
P2P:
首先我們在各種編譯器中寫下hello的代碼,就完成了hello.c,接下來我們需要運作它,也就是将program變成process。以下是這個過程:
1.可執行檔案的生成:
圖1.1.1 hello.c生成hello的過程
hello.c經過預處理、編譯、彙編、連接配接,會生成可執行目标程式hello。
2.運作可執行檔案:
在bash(殼)中,輸入./hello,shell逐一讀取文字,并存至記憶體,然後OS(程序管理)為其fork,建立子程序。
之後為其調用execve,将可執行檔案裝入核心的linux_binprm結構體。程序調用exec時,該程序執行的程式完全被替換,新的程式從main函數開始執行。因為調用exec并不建立新程序,隻是替換了目前程序的代碼區、資料區、堆和棧。子程序通過execve系統調用啟動加載器。加載器删除子程序現有的虛拟記憶體段。
之後為其mmap,使程序之間通過映射同一個普通檔案實作共享記憶體。hello被映射到程序位址空間後,程序可以像通路普通記憶體一樣對檔案進行通路。
最後,加載器跳轉到_start位址,它最終會調用應用程式的main 函數。然後程式從記憶體讀取指令位元組,然後再從寄存器讀入最多兩個數,然後在執行階段算術/邏輯單元要麼執行指令指明的操作,計算記憶體引用的有效位址要麼增加或者減少棧指針。然後在流水線化的系統中,待執行的程式被分解成幾個階段,每個階段完成指令執行的一部分。最後變成一個Process運作在記憶體中。
最後為其分時間片。時間片通常很短(在Linux上為5ms-800ms),用來運作hello這個程序。
O2O:
為了有效使用主存,簡化記憶體管理,獨立位址空間,我們使用了虛拟位址。為了實作從虛拟位址到實體位址的快速翻譯,我們采用了:TLB、4級頁表、3級cache、Pagefile等多個方法為其加速。
在程序結束之後,OS、Bash對其進行回收,shell又回到調用hello之前的狀态,稱其從Zero回到Zero。
1.2 環境與工具
列出你為編寫本論文,折騰Hello的整個過程中,使用的軟硬體環境,以及開發與調試工具。
VMware Workstation Pro
codeblocks
Windows 10
Ubuntu 64位
gredit
edb
1.3 中間結果
列出你為編寫本論文,生成的中間結果檔案的名字,檔案的作用等。
如圖1.1,我們可以看到所有中間結果檔案。
hello.i(修改了的源程式):
預處理階段實作的功能主要有三個:1.加載頭檔案2.進行宏替換3.條件編譯
hello.s(彙程式設計式):包含彙編語言程式。
hello.o(可重定位目标程式):将彙編語言翻譯成機器語言指令,并将指令打包成一種叫做可重定位目标程式的格式。
1.4 本章小結
簡述了Hello的一生。
一個程式被編寫出來到生成可執行檔案,然後可執行檔案執行,結束後系統回收,完成其一生。這個過程有着普适性,大部分程式都會經曆以上幾個步驟。
第2章 預處理
2.1 預處理的概念與作用
概念:
預處理pre-treatment,是指在進行最後加工完善以前進行的準備過程
即在編譯之前進行的處理。 C語言的預處理主要有三個方面的内容: 1.宏定義; 2.檔案包含; 3.條件編譯。預處理指令以符号“#”開頭。
作用:
言預處理程式的作用是根據源代碼中的預處理指令修改你的源代碼。預處理指令是一種指令語句(如#define),它訓示預處理程式如何修改源代碼。在對程式進行通常的編譯處理之前,編譯程式會自動運作預處理程式,對程式進行編譯預處理,這部分工作對程式員來說是不可見的。
理程式讀入所有包含的檔案以及待編譯的源代碼,然後生成源代碼的預處理版本。在預處理版本中,宏和常量辨別符已全部被相應的代碼和值替換掉了。如果源代碼中包含條件預處理指令(如#if),那麼預處理程式将先判斷條件,再相應地修改源代碼。
2.2在Ubuntu下預處理的指令
圖2.2.1 gcc編譯生成hello.i
2.3 Hello的預處理結果解析
圖2.3.1 hello.i内容
圖2.3.2 hello.c與hello.i的比較
通過指令gedit hello.i我們可以打開hello.i檢視其中的内容。
由ANSI标準的定義,該預處理程式應該處理以下指令:
#if
#ifdef
#ifndef
#else #elif
#endif
#define
#undef
#line
#error
#pragma
#include
顯然,上述所有的12個預處理指令都以符号#開始,每條預處理指令必須獨占一行。我們都可以從hello.i中尋找到。
更主要的是:
#include<stdio.h>、#include<unistd.h>、#include<stdlib.h>等頭檔案包含的檔案被插入到該預編譯指令的位置。該過程遞歸進行,及被包含的檔案可能還包含其他檔案。
通過對比,我們可以看到.i檔案中删除了.c中的注釋,并且插入了.c檔案的頭檔案。
2.4 本章小結
本章詳細介紹了hello預處理的階段。由于hello.i的介紹在網上介紹的資料太少,隻能結合自己的分析,盡量的寫出這些:
.cw檔案頭檔案也會又外部檔案,還有一些宏定義和注釋、一些條件編譯和完善程式文本檔案操作都需要預處理來實作。
預處理可以使程式在後邊的操作中不受阻礙,是整個過程中非常重要的一步。
第3章 編譯
3.1 編譯的概念與作用
編譯程式的作用是将進階語言源程式翻譯成目标程式。
編譯程式(Compiler,compiling program)也稱為編譯器,是指把用進階程式設計語言書寫的源程式,翻譯成等價的機器語言格式目标程式的翻譯程式。編譯程式屬于采用生成性實作途徑實作的翻譯程式。其以進階程式設計語言書寫的源程式作為輸入,而以彙編語言或機器語言表示的目标程式作為輸出。編譯出的目标程式通常還要經曆運作階段,以便在運作程式的支援下運作,加工初始資料,算出所需的計算結果。
3.2 在Ubuntu下編譯的指令
3.2.1 gcc編譯生成hello.s
3.3 Hello的編譯結果解析
3.3.1資料:
變量int sleepsecs:
3.3.1.1 int sleepsecs的處理
3.3.2 指派:
sleepsecs = 2.5:
3.3.2.1 sleepsecs = 2.5的處理
i = 0:
3.3.2.2 i = 0的處理
3.3.3 算術操作:
i++:
3.3.3.1 i++的處理
3.3.4 類型轉換:
前邊指派sleepsecs = 2.5就包括了一個類型轉換。因為sleepsecs是int,2.5為浮點型。
3.3.5 比較:
i<10:
3.3.5.1 i<10的處理
argc != 3:
3.3.5.2 argc != 3的處理
3.3.6 數組/指針/結構體:
3.3.6.1 數組的處理
3.3.7 控制轉移:
if(argc != 3):
3.3.7.2 控制轉移的處理(a)
for中的循環判斷:
3.3.7.1 控制轉移的處理(b)
3.3.8 函數操作:
第一個printf:
3.3.8.1 第一個printf處理
第二個printf:
3.3.8.2 第二個printf處理
sleep:
3.3.8.3 sleep的處理
3.4 本章小結
彙編語言是直接面向處理器的程式設計語言。每種處理器都有着自己可以識别的一套指令,稱為指令集。處理器執行指令時,根據不同的指令采取不同的動作,完成不同的功能。
彙編語言另一個特點是其操作的對象不是具體的資料,而是寄存器或者儲存器。
再者,彙編語言指令是及其指令的一種符号表示,不同類型的CPU有不同的及其指令系統,也就有不同的彙編語言。
彙編語言是各種程式設計語言中與硬體關系最密切、最直接的一種,在空間和時間上的效率也最高。
本章介紹了彙編操作的具體概念、作用,以及對其結果進行了解析。
第4章 彙編
4.1 彙編的概念與作用
概念:彙編器(as) 将hello.s 翻譯成機器語言指令,把這些指令打包成一種叫做可重定位目标程式(relocatable object program) 的格式,并将結果儲存在目标檔案hello.o 中。
作用:彙編器是将彙編代碼轉變成機器可以執行的指令,每一個彙編語句幾乎都對應一條機器指令。彙編相對于編譯過程比較簡單,可以根據對照表一一對應進行翻譯。
4.2 在Ubuntu下彙編的指令
圖4.2.1 彙編指令
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各節的基本資訊,特别是重定位項目分析。
1.ELF Header:以16B的序列Magic開始,Magic描述了生成該檔案的系統的字的大小和位元組順序,ELF頭剩下的部分包含幫助連結器文法分析和解釋目标檔案的資訊,其中包括ELF頭的大小、目标檔案的類型、機器類型、位元組頭部表(section header table)的檔案偏移,以及節頭部表中條目的大小和數量等資訊。
圖4.3.1 ELF頭資訊
-
- Section Headers:節頭部表,包含了檔案中出現的各個節的語義,包括節的類型、位置和大小等資訊。
圖4.3.2節頭資訊
3. 重定位節.rela.text ,一個.text節中位置的清單,包含.text節中需要進行重定位的資訊,當連結器把這個目标檔案和其他檔案組合時,需要修改這些位置。如圖4.4.3,圖中8條重定位資訊分别是對.L0(第一個printf中的字元串)、puts函數、exit函數、.L1(第二個printf中的字元串)、printf函數、sleepsecs、sleep函數、getchar函數進行重定位聲明。
其中資訊的前四位是symbol中的偏移量,後四位是type中的偏移量。
圖4.3.3 重定義節的資訊
4.4 Hello.o的結果解析
objdump -d -r hello.o 分析hello.o的反彙編,并請與第3章的 hello.s進行對照分析。
說明機器語言的構成,與彙編語言的映射關系。特别是機器語言中的操作數與彙編語言不一緻,特别是分支轉移函數調用等。
4.4.1 hello.o反彙編和hello.s的比較
1.全局變量通路:
對于hello.s,全局變量的通路方式為:段名稱+%rip,而對于hello.o的反彙編為0+%rip,因為rodata的資料位址是在運作時确定的,故也需要重定位,是以現在是0,并添加了重定位條目。
2.分支轉移:
對于hello.s,跳轉時是跳轉到某個段名稱,而再hello.o的反彙編中,已經變成了真正的位址。
3.函數調用:
對于hello.s,調用時直接跟着函數名稱,而對于hello.o的反彙編,因為函數是要link的時候才能生成位址的,是以現在的call後的相對位址都設定為0,即call的目标位址為下一條語句的位址。
4.5 本章小結
本章介紹了hello從hello.s到hello.o的彙編過程,通過檢視hello.o的elf格式和使用objdump得到反彙編代碼與hello.s進行比較的方式,間接了解到從彙編語言映射到機器語言彙編器的過程中都進行了什麼操作,以及其兩者的異同。
第5章 連結
5.1 連結的概念與作用
連結是将各種代碼和資料片段收集并組合成一個單一檔案的過程。連結器使得分離編譯成為可能。連結操作最重要的步驟就是将函數庫中相應的代碼組合到目标檔案中。
5.2 在Ubuntu下連結的指令
圖5.2.1 連結指令
5.3 可執行目标檔案hello的格式
對readelf中節頭部分進行分析:
在ELF格式檔案中,
節頭對hello中所有的節資訊進行了聲明,其中包括大小以及在程式中的偏移量,是以根據節頭中的資訊我們就可以用HexEdit定位各個節所占的區間(起始位置,大小)。其中位址是程式被載入到虛拟位址的起始位址。
圖5.3.1 hello節頭資訊
圖5.3.2 承接上圖
5.4 hello的虛拟位址空間
使用edb加載hello,檢視本程序的虛拟位址空間各段資訊,并與5.3對照分析說明。
圖5.4.1 edb加載後與5.3對比說明
5.5 連結的重定位過程分析
hello相對于hello.o有如下不同:
1.hello相對hello.o多了很多的節類似于.init,.plt等
2.hello.o中的相對偏移位址到了hello中變成了虛拟記憶體位址
3.hello中相對hello.o增加了許多的外部連結來的函數。
4.hello.o中跳轉以及函數調用的位址在hello中都被更換成了虛拟記憶體位址。
圖5.5.1 hello和hello.o的一些差異
重定位:連結器在完成符号解析以後,就把代碼中的每個符号引用和正好一個符号定義關聯起來。此時,連結器就知道它的輸入目标子產品中的代碼節和資料節的确切大小。
然後就可以開始重定位步驟了,在這個步驟中,将合并輸入子產品,并為每個符号配置設定運作時的位址。在hello到hello.o中,首先是重定位節和符号定義,連結器将所有輸入到hello中相同類型的節合并為同一類型的新的聚合節。
然後,連結器将運作時記憶體位址賦給新的聚合節,賦給輸入子產品定義的每個節,以及賦給輸入子產品定義的每一個符号。
當這一步完成時,程式中的每條指令和全局變量都有唯一的運作時記憶體位址了。
然後是重定位節中的符号引用,連結器會修改hello中的代碼節和資料節中對每一個符号的引用,使得他們指向正确的運作位址。
5.6 hello的執行流程
使用edb執行hello,說明從加載hello到_start,到call main,以及程式終止的所有過程。請列出其調用與跳轉的各個子程式名或程式位址。
程式名稱 程式位址
ld-2.27.so!_start 0x7fd7:8dfda090
ld-2.27.so!_dl_start 0x7fd7:8dfdaea0
ld-2.27.so!_dl_start_user 0x7fd7:8dfda09b
ld-2.27.so!_dl_init 0x7fd7:8dfe9630
hello!_start 0x400500
libc-2.27.so!__libc_start_main 0x7fd7:8dc09ab0
-libc-2.27.so!__cxa_atexit 0x7fd7:8dc2b430
-libc-2.27.so!__libc_csu_init 0x4005c0
hello!_init 0x400488
libc-2.27.so!_setjmp 0x7fd7:8dc26c10
-libc-2.27.so!_sigsetjmp 0x7fd7:8dc28e2b
–libc-2.27.so!__sigjmp_save 0x7fd7:8dc2db30
hello!main 0x400532
[email protected] 0x4004b0
[email protected] 0x4004e0
*[email protected] –
*[email protected] –
*[email protected] –
ld-2.27.so!_dl_runtime_resolve_xsave 0x7fd7:8dc324d0
-ld-2.27.so!_dl_fixup 0x7fd7:8dc34520
–ld-2.27.so!_dl_lookup_symbol_x 0x7fd7:8dd24dc0
libc-2.27.so!exit 0x7fd7:8dd32fd0
圖5.6.1 執行流程
5.7 Hello的動态連結分析
這裡注明:因為每次運作程式,ld的位址都不同,是以截圖中位址和5.6.1中的位址會稍有差別。
圖5.7.1 調用dl_init之前的全局偏移表
圖5.7.2 調用dl_init之後的全局偏移表
圖5.7.3 查找方法
具體這裡進行了什麼操作,我們可以看下圖,主要看變化對比,GOT[3]從指向下一條指令變成了具體的位址,這兒就是發生變化的原因:
圖5.7.3 變化分析
圖5.7.4 接上圖
5.8 本章小結
在本章中主要介紹了連結的概念與作用、hello的ELF格式,分析了hello的虛拟位址空間、重定位過程、執行流程、動态連結過程。
第6章 hello程序管理
6.1 程序的概念與作用
程序(Process)是計算機中的程式關于某資料集合上的一次運作活動,是系統進行資源配置設定和排程的基本機關,是作業系統結構的基礎。在早期面向程序設計的計算機結構中,程序是程式的基本執行實體;在當代面向線程設計的計算機結構中,程序是線程的容器。程式是指令、資料及其組織形式的描述,程序是程式的實體。
程序是一個執行中的程式的執行個體,每一個程序都有它自己的位址空間,一般情況下,包括文本區域、資料區域、和堆棧。文本區域存儲處理器執行的代碼;資料區域存儲變量和程序執行期間使用的動态配置設定的記憶體;堆棧區域存儲區着活動過程調用的指令和本地變量。
程序為使用者提供了以下假象:我們的程式好像是系統中目前運作的唯一程式一樣,我們的程式好像是獨占的使用處理器和記憶體,處理器好像是無間斷的執行我們程式中的指令,我們程式中的代碼和資料好像是系統記憶體中唯一的對象。
6.2 簡述殼Shell-bash的作用與處理流程
圖6.2.1 shell-bash的處理流程
作用:
Shell是一個程式,提供了一個界面,使用者通過這個界面通路作業系統核心的服務。
6.3 Hello的fork程序建立過程
圖6.3.1 fork程序建立
在終端Terminal中鍵入 ./hello 1170300110 lizhenyu,運作的終端程式會讀入這個字元串,然後對這個字元串進行解析,因為hello不是一個内置的指令是以解析之後終端程式判斷執行hello,之後終端程式首先會調用fork函數建立一個新的運作的子程序,子程序得到與父程序使用者級虛拟位址空間相同的(但是獨立的)一份副本,父程序與子程序之間最大的差別在于它們擁有不同的PID。
父程序與子程序是并發運作的獨立程序,核心能夠以任意方式交替執行它們的邏輯控制流的指令。在子程序執行期間,父程序預設選項是顯示等待子程序的完成。
6.4 Hello的execve過程
當fork之後,子程序調用execve函數(傳入指令行參數)在目前程序的上下文中加載并運作一個新程式即hello程式,加載器删除子程序現有的虛拟記憶體段,并建立一組新的代碼、資料、堆和棧段。
新的棧和堆段被初始化為零,通過将虛拟位址空間中的頁映射到可執行檔案的頁大小的片,新的代碼和資料段被初始化為可執行檔案中的内容。
最後加載器設定PC指向_start位址,_start最終調用hello中的main函數。
execve 函數加載并運作可執行目标檔案filename, 且帶參數清單argv 和環境變量清單envp 。隻有當出現錯誤時,例如找不到filename, execve 才會傳回到調用程式。是以,與fork 一次調用傳回兩次不同, execve 調用一次并從不傳回。
圖6.4.1 建立的系統映像
6.5 Hello的程序執行
(以下格式自行編排,編輯時删除)
開始時由6.4内容已經知道:加載器設定PC指向_start位址,_start最終調用hello中的main函數。
通過邏輯控制流,在不同的程序間切換。配置設定給某個程序的時間就叫做程序的時間片。上下文資訊即重新啟動一個被搶奪的程序的條件。使用者态中,程式不允許執行一些特權功能,而核心态中可以,它們之間需要某些條件才能切換。
對hello進行簡單的分析:
圖6.5.1 對hello程序執行的簡單分析
6.6 hello的異常與信号處理
圖6.6.1 正常執行
圖6.6.2 ctrl+c
圖6.6.3 ctrl+z
圖6.6.4 ctrl+z後ps
圖6.6.5 ctrl+z後jobs
圖6.6.6 ctrl+z後pstree
圖6.6.7 ctrl+z後fg并停止
圖6.6.8 ctrl+z後kill再ps
圖6.6.9 執行中亂按
6.7本章小結
在本章中,闡明了程序的定義與作用,介紹了Shell的一般處理流程,調用fork建立新程序,調用execve執行hello,hello的程序執行,hello的異常與信号處理。(第6章1分)
第7章 hello的存儲管理
7.1 hello的存儲器位址空間
邏輯位址是程式代碼經過編譯之後出現在彙程式設計式中的位址,由選擇符和偏移量組成。
線性位址是邏輯位址經過段機制後轉化為線性位址,為描述符:偏移量的形式。
虛拟位址:其實就是這裡的線性位址。
圖7.1.1 邏輯位址、線性位址、實體位址的關系
7.2 Intel邏輯位址到線性位址的變換-段式管理
一個邏輯位址由兩部分組成,段辨別符和段内偏移量。段辨別符由16位字段組成,前13位為索引号。
索引号是段描述符的索引,很多個描述符,組成了一個數組,叫做段描述表,可以通過段描述辨別符的前13位,再這個表中找到一個具體的段描述符,這個描述符就描述了一個段,每個段描述符由八個位元組組成。
段描述符中的base字段,描述了段開始的線性位址,一些全局的段描述符,放在全局段描述符表中,一些局部的則對應放在局部段描述符表中。由T1字段決定使用哪個。
以下是具體的轉化步驟:
- 給定一個完整的邏輯位址。
- 看段選擇符T1,知道要轉換的是GDT中的段還是LDT中的段,通過寄存器得到位址和大小。
- 取段選擇符中的13位,再數組中查找對應的段描述符,得到BASE,就是基位址。
- 線性位址等于基位址加偏移。
7.3 Hello的線性位址到實體位址的變換-頁式管理
線性位址被分以固定長度為機關的組,成為頁。
一個機器會有很多個頁,為了索引到這些不同的頁,我們可以得到一個大叔組,稱為也目錄,頁目錄中的每一個項都是一個指向不同頁的位址。
實體頁與記憶體頁一一對應。
為了減少空間的占用,引入了一個二級管理模式的機器來組織分類單元。
如圖:
圖7.3.1 二級管理模式
轉化步驟:
- 從cr3中去除目錄位址,作業系統再排程程序的時候,把這個位址裝入對應寄存器。
- 根據線性位址前十位,在數組中,找到對應索引項,因為引入二級管理,是以頁目錄中的項,不再是頁的位址,而是一個頁表的位址,頁的位址再這個頁表中。
- 根據線性位址中間的十位,再頁表中找到頁的起始位址(基址+偏移)。
- 将頁的起始位址與線性位址中的後12位相加,得到實體位址。
7.4 TLB與四級頁表支援下的VA到PA的變換
圖7.4.1 頁表翻譯
如圖給出了Core i7 MMU如何使用四級頁表來将虛拟位址翻譯成實體位址。36位的VPN劃分為4個9位的片,每個片對應一個頁表的偏移量。CR3寄存器存有L1頁表的實體位址。VPN1提供一個到L1PET的偏移量,這個PTE包含L2頁表的基位址。VPN2提供一個到L2PET的偏移量,以此類推。
通過四級頁表,我們可以查找到PPN,與VPO組合成PA,或者說VPO直接對應了PPO,它們組成PA并且向TLB中添加條目。
7.5 三級Cache支援下的實體記憶體通路
CPU發出一個虛拟位址再TLB裡搜尋,如果命中,直接發送到L1cache裡,如果沒有命中,就現在也表裡加載到之後再發送過去,到了L1中,尋找實體位址又要檢測是否命中,如果沒有命中,就向L2/L3中查找。這就用到了CPU高速緩存,這種機制加上TLB可以是的機器再翻譯位址的時候性能得以充分發揮。
圖7.5.1 三級cache訪存(a)
圖7.5.2 三級cache訪存(b)
7.6 hello程序fork時的記憶體映射
當fork 函數被shell調用時,核心為hello程序建立各種資料結構,并配置設定給它一個唯一的PID 。并且建立hello程序的mm_struct 、區域結構和頁表的原樣副本。它将兩個程序中的每個頁面都标記為隻讀,并将兩個程序中的每個區域結構都标記為私有的寫時複制。
7.7 hello程序execve時的記憶體映射
execve函數在目前程序中加載并運作包含在可執行目标檔案hello中的程式,用hello程式有效地替代了目前程式。
execve函數執行了以下幾個操作:
1.删除已存在的使用者區域,删除目前程序虛拟位址的使用者部分中的已存在的區域結構。
2.映射私有區域,為新程式的代碼、資料、bss和棧區域建立新的區域結構,所有這些新的區域都是私有的、寫時複制的。代碼和資料區域被映射為hello檔案中的.text和.data區,bss區域是請求二進制零的,映射到匿名檔案,其大小包含在hello中,棧和堆位址也是請求二進制零的,初始長度為零。
3.映射共享區域。
4.設定程式計數器(PC)。execve做的最後一件事情就是設定目前程序上下文的程式計數器,使之指向代碼區域的入口點。
圖7.7.1 execve加載
7.8 缺頁故障與缺頁中斷處理
在虛拟記憶體中,DRAM緩存不命中稱為缺頁。
圖7.8.1 缺頁異常
如圖,引用VP3時發現其不再DRAM中,除法缺頁異常,調用異常處理程式,選擇VP4作為犧牲頁,修改VP4的頁表條目。
圖7.8.2 故障處理
如上圖,進行故障處理。
7.9動态存儲配置設定管理
動态記憶體配置設定器維護着一個程序的虛拟記憶體區域,稱為堆(heap) 。堆是一個請求二進制零的區域,它緊接在未初始化的資料區域後開始,并向上生長(向更高的位址) 。對于每個程序,核心維護着一個變量brk, 它指向堆的頂部。
配器将堆視為一組不同大小的塊(block) 的集合來維護。每個塊就是一個連續的虛拟記憶體片(chunk),要麼是已配置設定的,要麼是空閑的。已配置設定的塊顯式地保留為供應用程式使用。空閑塊可用來配置設定。空閑塊保持空閑,直到它被應用所配置設定。一個已配置設定的塊保持已配置設定狀态,直到它被釋放。
基本方法:這裡指的基本方法應該是在合并塊的時候使用到的方法,有三種分别是:首次适配,下一次适配和最佳适配。首次适配是從開始處往後搜尋,下一次适配是從上一次适配發生處開始搜尋,最佳适配依次檢查所有塊,性能要比首次适配和下一次适配都要高。
政策:分為隐式空閑連結清單和顯示空閑連結清單。任何實際的配置設定器都需要一些資料結構,允許他來差別塊邊界,以及差別已配置設定塊和空閑塊。大多數配置設定器将這些資訊嵌入塊本身。
隐式空閑連結清單:通過頭部中的大小字段隐含的連接配接。配置設定器可以通過周遊堆中的所有塊,進而間接地周遊整個空閑塊地集合。
圖7.9.1 隐式空閑連結清單
顯示空閑連結清單:因為根據定義,程式不需要一個空閑塊地主題,是以實作這個資料結構地指針可以存放在這些空閑塊的主體裡。
圖7.9.2 顯示空閑連結清單
7.10本章小結
本章主要介紹了hello的存儲器位址空間、intel的段式管理、hello的頁式管理,在指定環境下介紹了VA到PA的變換、實體記憶體通路,還介紹了hello程序fork時的記憶體映射、execve時的記憶體映射、缺頁故障與缺頁中斷處理、動态存儲配置設定管理。詳盡的介紹了與hello的存儲管理有關的内容。
第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.Linux shell 建立的每個程序開始時都有三個打開的檔案:标準輸入(描述符為0) 、标準輸出(描述符為1) 和标準錯誤(描述符為2) 。
3.改變目前的檔案位置。對于每個打開的檔案,核心保持着一個檔案位置k, 初始為0。這個檔案位置是從檔案開頭起始的位元組偏移量。
4.讀寫檔案。一個讀操作就是從檔案複制n>0 個位元組到記憶體,從目前檔案位置k 開始,然後将k增加到k+n 。類似地,寫操作就是從記憶體複制n>0 個位元組到一個檔案,從目前檔案位置k開始,然後更新k 。
5.關閉檔案。當應用完成了對檔案的通路之後,它就通知核心關閉這個檔案。
Unix I/O函數:
1.程序是通過調用open 函數來打開一個已存在的檔案或者建立一個新檔案的:
int open(char *filename, int flags, mode_t mode);
open 函數将filename 轉換為一個檔案描述符,并且傳回描述符數字。傳回的描述符總是在程序中目前沒有打開的最小描述符。flags 參數指明了程序打算如何通路這個檔案,mode 參數指定了新檔案的通路權限位。
傳回:若成功則為新檔案描述符,若出錯為-1。
2.程序通過調用close 函數關閉一個打開的檔案。
int close(int fd);
傳回:若成功則為0, 若出錯則為-1。
3.應用程式是通過分别調用read 和write 函數來執行輸入和輸出的。
ssize_t read(int fd, void *buf, size_t n);
read 函數從描述符為fd 的目前檔案位置複制最多n 個位元組到記憶體位置buf 。傳回值-1表示一個錯誤,而傳回值0 表示EOF。否則,傳回值表示的是實際傳送的位元組數量。
傳回:若成功則為讀的位元組數,若EOF 則為0, 若出錯為-1。
ssize_t write(int fd, const void *buf, size_t n);
write 函數從記憶體位置buf 複制至多n 個位元組到描述符fd 的目前檔案位置。圖10-3 展示了一個程式使用read 和write 調用一次一個位元組地從标準輸入複制到标準輸出。
傳回:若成功則為寫的位元組數,若出錯則為-1。
8.3 printf的實作分析
已經給出,不作贅述:
https://www.cnblogs.com/pianist/p/3315801.html
從vsprintf生成顯示資訊,到write系統函數,到陷阱-系統調用 int 0x80或syscall.
字元顯示驅動子程式:從ASCII到字模庫到顯示vram(存儲每一個點的RGB顔色資訊)。
顯示晶片按照重新整理頻率逐行讀取vram,并通過信号線向液晶顯示器傳輸每一個點(RGB分量)。
8.4 getchar的實作分析
已經給出,不作贅述:
異步異常-鍵盤中斷的處理:鍵盤中斷處理子程式。接受按鍵掃描碼轉成ascii碼,儲存到系統的鍵盤緩沖區。
getchar等調用read系統函數,通過系統調用讀取按鍵ascii碼,直到接受到Enter鍵才傳回。
8.5本章小結
本章主要介紹了Linux的IO裝置管理方法、Unix IO接口及其函數,分析了printf函數和getchar函數。
結論
hello程式的一生終于結束了(….)
它的一生經曆許許多多坎坷,以下就是它一生的縮影:
- 被程式猿們編寫出來,也就是hello.c的誕生。
- 預處理,初步處理hello.c将外部庫合并到hello.i檔案中。
- 編譯,将hello.i編譯成hello.s
- 彙編,将hello.s彙編成hello.o
- 連結,将hello.o與可重定位目标檔案以及動态連結庫連結稱為可執行程式hello
- 運作,在shell輸入./hello 1170300110 lzy運作
- 建立子程序,shell調用fork
- 運作程式,shell調用execve
- 執行指令,CPU為hello配置設定時間片,hello在一個時間片中執行自己的邏輯控制流。
- 通路記憶體,MMU将虛拟記憶體映射成實體位址
- 動态記憶體申請,malloc
- 信号,如果遇到ctrl+c或ctrl+z,則分别停止、挂起
- 結束,shell父程序回收子程序。
真實的聽到了Hello之歌,并流下了趕ddl的淚水。
(結論0分,缺失 -1分,根據内容酌情加分)
附件
hello.i:預處理生成的檔案。
hello.s:編譯生成的檔案。
hello.o:彙編生成的可重定位目标檔案。
hello:連結生成的可執行檔案。
hello.txt:存有hello.o的反彙編内容。
參考文獻
為完成本次大作業你翻閱的書籍與網站等
[1] https://blog.csdn.net/qq_44242536/article/details/85231624.
HIT 2018 CS:APP Hello的一生 大作業 小明難亡
[2] https://blog.csdn.net/hahalidaxin/article/details/85144974
HIT CSAPP 2018 大作業 程式人生 Hello’s P2P hahalidaxin
[3] 深入了解計算機系統。
[4] http://www.baidu.com
百度一下