1. 程式人生 > >HIT CSAPP 計算機系統大作業 《程式人生 - Hello’s P2P》

HIT CSAPP 計算機系統大作業 《程式人生 - Hello’s P2P》

HIT CSAPP 計算機系統大作業 《程式人生 - Hello’s P2P》

計算機系統

大作業

題 目 程式人生-Hello’s P2P
專 業 軟體工程
學   號 1173710104
班   級 1737101
學 生 滕濤    
指 導 教 師 吳銳

電腦科學與技術學院
2018年12月
摘 要
hello.c是每一個程式設計師所寫出的人生中的第一個程式,但是不要看hello.c簡短,其在計算機中執行卻有著許許多多的奧妙值得每一個程式設計師去探索,由一個簡單的程式開始去一步一步瞭解更加複雜且值得去探索的程式,才會得到一步一步的成長,這次的大作業就從hello開始第一步。
關鍵詞:CSAPP;Hello程式;生命週期;

(摘要0分,缺失-1分,根據內容精彩稱都酌情加分0-1分)

目 錄

第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簡介
在Editor中鍵入程式碼得到hello.c程式,在linux中,hello.c經過cpp的預處理、ccl的編譯、as的彙編、ld的連結最終成為可執行目標程式hello,在shell中鍵入啟動命令後,shell為其fork,fork產生一個shell程序的子程序,於是Program變成Process,這是P2P(From Program to Process)的過程。之後shell為其execve,載入器為其分配記憶體空間,程式執行在實體記憶體中,CPU 為其分配時間片執行指令。當程式執行結束後,shell父程序負責回收hello程序,這便是020 (From Zero-0 to Zero -0)的過程。
1.2 環境與工具
硬體環境:Intel Core i7-7700HQ x64CPU,8G RAM,256G SSD +1T HDD. 軟體環境:Ubuntu18.04.1 LTS
開發與除錯工具:文字編輯器,gcc,as,ld,edb,readelf,HexEdit
1.3 中間結果
列出你為編寫本論文,生成的中間結果檔案的名字,檔案的作用等。
檔名稱 檔案作用

hello.c 原始碼
hello.i 預處理之後文字檔案
hello.s 編譯之後的彙編檔案
hello.o 彙編之後的可重定位目標執行
hello 連結之後的可執行目標檔案
hello2.c 測試程式程式碼
hello2 測試程式
helloo.objdmp Hello.o的反彙編程式碼
helloo.elf Hello.o的ELF格式
hello.objdmp Hello的反彙編程式碼
hello.elf Hellode ELF格式
hmp.txt 存放臨時資料

1.4 本章小結
本章主要簡單介紹了hello的p2p,020過程,列出了本次實驗資訊:環境、中間結果。
(第1章0.5分)

第2章 預處理
2.1 預處理的概念與作用
概念:前處理器cpp根據以字元#開頭的命令,修改原始的C程式。主要功能如下:

  1. 將原始檔中用#include形式宣告的檔案複製到新的程式中。比如hello.c 第 6-8 行中的#include<stdio.h> 等命令告訴前處理器讀取系統標頭檔案stdio.h unistd.h stdlib.h 的內容,並把它直接插入到程式文字中。結果就得到了另一個C程式,為hello.i。
    1. 用實際值替換用#define定義的字串
    2. 根據#if後面的條件決定需要編譯的程式碼

2.2在Ubuntu下預處理的命令
預處理指令及其生成結果

2.3 Hello的預處理結果解析
因為 hello.c 包含的標頭檔案中還包含有其他標頭檔案,因此係統會遞迴式的定址和展開,直到檔案中不含巨集定義且相關的標頭檔案均已被引入。故使用文字編輯器開啟hello.i後發現main函式之前出現的是stdio.h , unistd.h和stdlib.h,以stdio.h的展開為例,cpp到預設的環境變數下尋找stdio.h,開啟/usr/include/stdio.h 發現其中依然使用了#define 語句,cpp 對此遞迴展開,所以最終.i 程式中是沒有#define 的。而且發現其中使用了大量的#ifdef #ifndef的語句,cpp 會對條件值進行判斷來決定是否執行包含其中的邏輯。其他類似。
2.4 本章小結
本章主要介紹了預處理的定義與作用、並結合預處理之後的程式對預處理結果
進行了解析。

(第2章0.5分)

第3章 編譯
3.1 編譯的概念與作用
編譯是利用編譯程式從源語言編寫的源程式產生目標程式的過程,也是用編譯程式產生目標程式的動作。編譯器將文字檔案hello.i翻譯成文字檔案hello.s,它包含一個組合語言程式。這個過程稱為編譯,同時也是編譯的作用。
編譯程式把一個源程式翻譯成目標程式的工作過程分為五個階段:詞法分析;語法分析;語義檢查和中間程式碼生成;程式碼優化;目的碼生成。

注意:這兒的編譯是指從 .i 到 .s 即預處理後的檔案到生成組合語言程式

3.2 在Ubuntu下編譯的命令
在這裡插入圖片描述
3.3 Hello的編譯結果解析

3.3.1 彙編指令
指令 含義
.file 宣告原始檔
.text 宣告程式碼段
.data 宣告資料段
.section .rodata 以下是rodata節
.globl 宣告一個全域性變數
.type 宣告指令及資料存放地址的對齊方式
.size 宣告大小
.long、.string 宣告一個long、string型別
.align 宣告對指令或者資料的存放地址進行對齊的
方式
3.3.2 資料
一、字串
組合語言中,輸出字串作為全域性變數儲存,因此儲存於. rodata 節中。彙編檔案 hello.s 中,共有兩個字串,均作為 printf 引數,分別為:
在這裡插入圖片描述
1) “Usage: Hello 學號 姓名!\n”,第一個printf傳入的輸出格式化引數,在hello.s中宣告,可以發現字串被編碼成UTF-8格式,一個漢字在utf-8編碼中佔三個位元組,一個\代表一個位元組。
2)“Hello %s %s\n”,第二個printf傳入的輸出格式化引數,在hello.s中宣告

二、整型
在 hello.s檔案中,sleepsecs 的定義如下:
在這裡插入圖片描述
程式中涉及的整數資料有:
1) int sleepsecs:sleepsecs 在 C 程式中被宣告為全域性變數,且已經被賦值,編譯器處理時在.data節宣告該變數,.data節存放已經初始化的全域性和靜態 C 變數。在圖 3.3 中,可以看到,編譯器首先將 sleepsecs在.text 程式碼段中將其宣告為全域性變數,其次在.data段中,設定對齊方式為 4、設定型別為物件、設定大小為 4 位元組、設定為 long 型別其值為2。
2) int i:編譯器將其值儲存在暫存器或者棧空間中,兩者選擇具體決定於暫存器的數目以及優化程度,在 hello.s 編譯器將 i 儲存在棧上空間-4(%rbp)中,可以看出i佔據了棧中的4B大小空間。編譯器 ccl 會將 int 表示為 long 但對齊方式仍為 int 型的 4 位元組,long 型別表示為雙字 quad,對齊方式仍為 8 位元組。
3) int argc:作為第一個引數傳入
4) 立即數:其他整形資料的出現都是以立即數的形式出現的,直接硬編碼在彙編程式碼中。

三、陣列
在hello.c中運用陣列如下
在這裡插入圖片描述
此部分迴圈對應彙編程式碼段為
在這裡插入圖片描述
argv[]為char型別單個數組元素的大小為8個位元組起始地址為argv,傳入的兩個引數分別記為argv[1],argv[2],值為getchar()在函式執行時獲取的兩個%s,後通過指令取出了陣列儲存值。

3.3.3 賦值
hello.c檔案中賦值操作共有兩次:⑴int sleepsecs = 2.5 ⑵int i = 0
⑵ sleepsecs被定義為全域性變數被定義為long型別,值為2
⑵ i的賦值通過mov操作來完成,即movl $0,-4(%rbp)
對於mov操作來說可分為movb(8位)、movw(16位)、movl(32位)、movq(64位)

3.3.4 型別轉換
程式中涉及隱式型別轉換的是:int sleepsecs=2.5,將浮點數型別的2.5強制轉換為int型別。
當在double或float向int進行強制型別轉換的時候,程式改變數值和位模式的
原則是:值會向零舍入。例如 1.999 將被轉換成 1,-1.999 將被轉換成-1。進一步
來講,可能會產生值溢位的情況,與 Intel相容的微處理器指定位模式[10…000]為整數不確定值,一個浮點數到整數的轉換,如果不能為該浮點數找到一個合適的整數近似值,就會產生一個整數不確定值。
浮點數預設型別為double,所以上述強制轉化是double強制轉化為int型別。遵從向零舍入的原則,將2.5舍入為2。

3.3.5 算數操作
算數操作的彙編指令有
指令 效果
leaq S,D D=&S
INC D D+=1
DEC D D-=1
NEG D D=-D
ADD S,D D=D+S
SUB S,D D=D-S
IMULQ S R[%rdx]:R[%rax]=SR[%rax](有符號)
MULQ S R[%rdx]:R[%rax]=S
R[%rax](無符號)
IDIVQ S
R[%rdx]=R[%rdx]:R[%rax] mod S(有符號)R[%rax]=R[%rdx]:R[%rax] div S
DIVQ S R[%rdx]=R[%rdx]:R[%rax] mod S(無符號)
R[%rax]=R[%rdx]:R[%rax] div S
hello.s中的算數操作有:
⑵ i++ 在for迴圈中i累加,在hello.s中的彙編操作為addl $1,-4(%rbp)
⑵leaq指令中地址%rip被加上了.LC0,.LC1並分別傳給了%rdi進行操作
leaq .LC0(%rip),%rdi 和 leaq .LC1(%rip),%rdi

3.3.6 關係操作
進行關係操作的彙編指令有:
指令 效果 描述
CMP S1,S2 S2-S1 比較-設定條件碼
TEST S1,S2 S1&S2 測試-設定條件碼
SET** D D=** 按照將條件碼設定D
J
—— 根據**與條件碼進行跳轉
程式中涉及的關係運算為:
1)argc!=3:判斷argc不等於3。hello.s中使用cmpl $3,-20(%rbp)比較,計算
argc-3然後設定條件碼,為下一步je利用條件碼進行跳轉作準備。
2)i<10:判斷i小於10。hello.s中使用cmpl $9,-4(%rbp)比較,計算i-9 然後
設定條件碼,為下一步jle使用條件碼進行跳轉做準備。

3.3.7 控制轉移
指令 語法 功能
JN\JE… JN & 跳轉到地址
hello.s中的控制轉移有:
⑴ if (argv!=3):當argv不等於 3 的時候執行程式段中的程式碼。對標誌位ZF進行判斷,ZF=1即相等時跳轉到.L2
在這裡插入圖片描述
⑵ for(i=0;i<10;i++)執行時先無條件跳轉到位於迴圈體.L4之後的比較程式碼,使用cmpl進行比較,如果i<=9,則跳入.L4 for迴圈體執行,否則說明迴圈結束,順序執行for之後的邏輯。
在這裡插入圖片描述
3.3.8 函式操作
函式是一種過程,過程提供了一種封裝程式碼的方式,用一組指定的引數和可選的返回值實現某種功能。P中呼叫函式Q包含以下動作:
1)傳遞控制:進行過程 Q 的時候,程式計數器必須設定為 Q 的程式碼的起始址,然後在返回時,要把程式計數器設定為P中呼叫Q後面那條指令的地址。
2)傳遞資料:P必須能夠向Q提供一個或多個引數,Q必須能夠向P中返回一個值。
3)分配和釋放記憶體:在開始時,Q可能需要為區域性變數分配空間,而在返回前,又必須釋放這些空間。

64位程式引數儲存順序(浮點數使用xmm,不包含):
1 2 3 4 5 6 7
%rdi %rsi %rdx %rcx %r8 %r9 棧空間

程式中涉及函式操作的有:
(1) main函式:
① 引數傳遞:將int argc, char argv[]傳入main函式。
② 函式呼叫:main函式呼叫了printf(),exit(),sleep(),getchar()四個函式
③ 函式返回:當main函式正常執行結束後返回1
(2) printf函式:
① 傳遞資料:第一次printf將%rdi設定為“Usage: Hello 學號 姓名! \n”字串的首地址。第二次printf設定%rdi 為“Hello %s %s\n” 的首地址,設定%rsi為argv[1],%rdx 為argv[2]。
② 控制傳遞:第一次 printf 因為只有一個字串引數,所以 call [email protected];第二次printf使用call [email protected]
(3) exit函式:
① 引數傳遞:傳入了1作為引數。
② 函式返回:exit(1)表示程式出現異常時退出。
(4) sleep函式:
① 引數傳遞:傳入了sleepsecs
② 函式返回:傳入了sleepsecs之後,程式在傳參大小時間過後繼續進行
(5) getchar函式:
① 控制傳遞:call [email protected]
② 函式返回:函式返回輸入字元的ASCII碼或者EOF表示輸入有誤

此部分是重點,說明編譯器是怎麼處理C語言的各個資料型別以及各類操作的。應分3.3.1~ 3.3.x等按照型別和操作進行分析,只要hello.s中出現的屬於大作業PPT中P4給出的參考C資料與操作,都應解析。

3.4 本章小結
本章系統闡述了編譯器將預處理文字檔案 hello.i 翻譯為文字檔案 hello.s 的具體操作,主要就組合語言偽指令、資料型別、組合語言操作、控制轉移,函式操作、型別轉換六方面針對 hello.s 中各部分做出相應的解釋說明。
(第3章2分)

第4章 彙編
4.1 彙編的概念與作用
概念:把組合語言翻譯成機器語言的過程稱為彙編。
作用:彙編器(as)將 hello.s 翻譯成機器語言指令,並把這些指令打包成一種叫做可重定位目標程式的格式,並將結果儲存在二進位制目標檔案 hello.o 中。

注意:這兒的彙編是指從 .s 到 .o 即編譯後的檔案到生成機器語言二進位制程式的過程。
4.2 在Ubuntu下彙編的命令
在這裡插入圖片描述
應截圖,展示彙編過程!
4.3 可重定位目標elf格式
首先使用 readelf –a hello.o > hello.elf 生成 hello.o 檔案的 ELF 格式。分析其組成各部分:

a) ELF 頭

ELF 頭以一個 16 位元組的序列開始,這個序列描述了生成該檔案的系統的字的大小和位元組順序。ELF 頭剩下的部分包含幫助連結器語法分析和解釋目標檔案的資訊。其中包括 ELF 頭的大小、目標檔案的型別(如可重定位、可執行或共享的)、機器型別(如 x86-64)、節頭部表(section header table)的檔案偏移,以及節頭部表中條目的大小和數量。不同節的位置和大小是由節頭部表描述的,其中目標檔案中每個節都有一個固定大小的頭目(entry)。hello.elf 的 ELF 頭如下:
在這裡插入圖片描述
a) 節頭部表
節頭部表記錄了各節名稱、型別、地址、偏移量、大小、全體大小、旗標、連線、資訊、對齊資訊。hello.elf 節頭部表如下:
在這裡插入圖片描述

b) 重定位節

.rela.text 記錄了一個. text 節中位置的列表,當連結器把這個目標檔案和其他檔案組合時需要修改這些位置。一般而言,任何呼叫外部函式或者引用全域性變數的指令都需要修改。另一方面,呼叫本地函式的指令則不需要修改。

在 hello.o 的重定位節中包含了 main 函式呼叫的 puts、exit、printf、sleep、getchar 函式以及全域性變數 sleepsecs,還有. rodata 節(包含 prnitf 的格式串)的偏移量、資訊、型別、符號值、符號名稱及加數。rela.eh_frame 記錄了. text 的資訊。hello.elf 的重定位節如下:
在這裡插入圖片描述
c) 符號表
用來存放程式中定義和引用的函式和全域性變數的資訊。重定位需要引用的符號都在其中宣告。
在這裡插入圖片描述

4.4 Hello.o的結果解析
在這裡插入圖片描述
利用 objdump -d -r hello.o > hello_o_asm.txt 生成 hello.o 對應的反彙編檔案,經過與 hello.s 比較,二者卻產生了細微的差別。如上圖所示。
主要原因在於:
⑴ 分支轉移:反彙編程式碼跳轉指令的運算元使用的不是段名稱如.L3,而段名稱只是在組合語言中便於編寫的助記符,在彙編成機器語言之後變成了確定的地址。
⑵ 函式呼叫:在.s檔案中,函式呼叫之後直接跟著函式名稱,而在反彙編程式中,call的目標地址是當前下一條指令。這是因為hello.c中呼叫的函式都是共享庫中的函式,最終需要通過動態連結器才能確定函式的執行時執行地址,在彙編成為機器語言的時候,對於這些不確定地址的函式呼叫,將其call指令後的相對地址設定為全0(目標地址正是下一條指令),然後在.rela.text節中為其新增重定位條目,等待靜態連結的進一步確定。
⑶ 全域性變數訪問:在.s檔案中,訪問rodata(printf中的字串),使用段名稱+%rip,在反彙編程式碼中0+%rip,因為rodata中資料地址也是在執行時確定,故訪問也需要重定位。所以在彙編成為機器語言時,將運算元設定為全0並新增重定位條目。

4.5 本章小結
本章通過對彙編後產生的 hello.o 的可重定位的 ELF 格式的考察、對重定位專案的舉例分析以及對反彙編檔案與 hello.s 的對比,從原理層次瞭解了彙編這一過程實現的變化。
(第4章1分)

第5章 連結
5.1 連結的概念與作用
概念:連結是將各種程式碼和資料片段收集並組合稱為一個單一檔案的過程,這個檔案可被載入(複製)到記憶體並執行。

作用:連結可以執行於編譯時,也就是在原始碼被翻譯成機器程式碼時;也可以執行於載入時,也就是在程式被載入到記憶體並執行時;甚至執行於執行時,也就是由應用程式來執行。早期計算機系統中連結時手動執行的,在現代系統中,連結器由連結器自動執行。連結器使得分離編譯成為可能。開發過程中無需將大型的應用程式組織委員一個巨大的原始檔,而是可以把它分解為更小、更好管理的模組,可以獨立地修改和編譯這些模組。
注意:這兒的連結是指從 hello.o 到hello生成過程。
5.2 在Ubuntu下連結的命令
命令:
ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
在這裡插入圖片描述

5.3 可執行目標檔案hello的格式
使用 readelf -a hello > hello_out.elf 執行獲得包含 hello 的 ELF 格式的檔案。
節頭部表中包含了各段的基本資訊,包括名稱、型別、地址、偏移量、大小、全體大小、旗標、連結、資訊、對齊等資訊:
在這裡插入圖片描述
5.4 hello的虛擬地址空間
用 edb 開啟 hello,可以在 Data Dump 視窗看見 hello 載入到虛擬地址中的狀況,程式的虛擬地址空間為 0x00000000004000000-0x0000000000401000,如下圖:
在這裡插入圖片描述
檢視 .elf 中的程式頭部分
在這裡插入圖片描述
elf 裡面的 Program Headers:
PHDR:程式頭表
INTERP:程式執行前需要呼叫的直譯器
LOAD:程式目的碼和常量資訊
DYNAMIC:動態連結器所使用的資訊
NOTE::輔助資訊
GNU_EH_FRAME:儲存異常資訊
GNU_STACK:使用系統棧所需要的許可權資訊
GNU_RELRO:儲存在重定位之後只讀資訊的位置

5.5 連結的重定位過程分析
使用objdump -d -r hello > hello.objdump 獲得hello的反彙編程式碼。
與 hello.o 反彙編文字 hello.objdump 相比,在 hello.objdump 中多了許多節
節名稱 描述
.interp 儲存ld.so的路徑
.hash 符號的雜湊表
.gnu.hash GNU拓展的符號的雜湊表
.dynsym 執行時/動態符號表
.dynstr 存放.dynsym節中的符號名稱
.gnu.version 符號版本
.gnu.version_r 符號引用版本
.rela.dyn 執行時/動態重定位表
.rela.plt .plt節的重定位條目
.init 程式初始化需要執行的程式碼
.plt 動態連結-過程連結表
.fini 當程式正常終止時需要執行的程式碼
.eh_frame contains exception unwinding and
source language information. Each entry
in this section is represented by single
CFI
.dynamic 存放被ld.so使用的動態連結資訊
.got 動態連結-全域性偏移量表-存放變數
.got.plt 動態連結-全域性偏移量表-存放函式
.data 初始化了的資料
.comment 一串包含編譯器的 NULL-terminated 字
符串

函式呼叫

hello.o 反彙編檔案中,call 地址後為佔位符(4 個位元組的 0);而 hello 在生成過程中使用了動態連結共享庫,函式呼叫時用到了延時繫結機制。以 puts 為例,簡述其連結過程:

  1. puts第一次被呼叫時程式從過程連結表PLT中進入其對應的條目;
  2. 第一條PLT指令通過全域性偏移量表GOT中對應條目進行間接跳轉,初始時每個GOT條目都指向它對應的PLT條目的第二條指令,這個簡單跳轉把控制傳送回對應PLT條目的下一條指令
  3. 把puts函式壓入棧中之後,對應PLT條目調回PLT[0];
  4. PLT[0]通過GOT[1]間接地把動態連結器的一個引數壓入棧中,然後通過GOT[2]間接跳轉進動態連結器中。動態連結器使用兩個棧條目確定puts的執行時位置,用這個地址重寫puts對應的GOT條目,再把控制傳回給puts。
    第二次呼叫的過程:
  5. puts被呼叫時程式從過程連結串列PLT中進入對應的條目;
  6. 通過對應GOT條目的間接跳轉直接會將控制轉移到puts。

資料訪問 hello.o 反彙編檔案中,對. rodata 中 printf 的格式串的訪問需要通過連結時重定位的絕對引用確定地址,因此在彙編程式碼相應位置仍為佔位符表示,對. data 中已初始化的全域性變數 sleepsecs 為 0x0+%rip 的方式訪問;而 hello 反彙編檔案中對應全域性變數已通過重定位絕對引用被替換為固定地址。

5.6 hello的執行流程
載入程式 ld-2.23.so!_dl_start
ld-2.23.so!_dl_init
LinkAddress!_start
ld-2.23.so!_libc_start_main
ld-2.23.so!_cxa_atexit
LinkAddress!_libc_csu.init
ld-2.23.so!_setjmp
執行 LinkAddress!main
程式終止 ld-2.23.so!exit

使用edb執行hello,說明從載入hello到_start,到call main,以及程式終止的所有過程。請列出其呼叫與跳轉的各個子程式名或程式地址。
5.7 Hello的動態連結分析
(以下格式自行編排,編輯時刪除)
分析hello程式的動態連結專案,通過edb除錯,分析在dl_init前後,這些專案的內容變化。要截圖示識說明。
5.8 本章小結
本章瞭解了連結的概念作用,分析可執行檔案 hello 的 ELF 格式及其虛擬地址空間,同時通過例項分析了重定位過程、載入以及執行時函式呼叫順序以及動態連結過程,深入理解連結和重定位的過程。
(第5章1分)

第6章 hello程序管理
6.1 程序的概念與作用
概念:程序的經典定義就是一個執行中的程式的例項。系統中的每個程式都執行在某個程序的上下文中。上下文是由程式正確執行所需的狀態組成的。這個狀態包括存放在記憶體中的程式的程式碼和資料,它的棧、通用目的暫存器的內容、程式計數器、環境變數、以及開啟檔案描述符的集合。

作用:程序給應用程式提供的關鍵抽象有兩種:

a) 一個獨立的邏輯控制流,提供一個假象,程式獨佔地使用處理器。
b) 一個私有的地址空間,提供一個假象,程式在獨佔地使用系統記憶體。
6.2 簡述殼Shell-bash的作用與處理流程
Shell的作用:Shell是一個用C語言編寫的程式,他是使用者使用 Linux 的橋樑。Shell既是一種命令語言,又是一種程式設計語言,Shell 是指一種應用程式。Shell
應用程式提供了一個介面,使用者通過這個介面訪問作業系統核心的服務。
處理流程:
1)從終端讀入輸入的命令。
2)將輸入字串切分獲得所有的引數
3)如果是內建命令則立即執行
4)否則呼叫相應的程式執行
5)shell應該接受鍵盤輸入訊號,並對這些訊號進行相應處理
6.3 Hello的fork程序建立過程
程序的建立過程:父程序通過呼叫 fork 函式建立一個新的執行的子程序。新建立的子程序幾乎但不完全與父程序相同。子程序得到與父程序使用者級虛擬地址空間相同的(但是獨立的)一份副本,包括程式碼和資料段、堆、共享庫以及使用者棧。子程序還獲得與父程序任何開啟檔案描述符相同的副本,這就意味著當父程序呼叫 fork 時。子程序可以讀寫父程序中開啟的任何檔案。父程序和新建立的子程序最大的區別在於他們有不同的 id。

fork 後呼叫一次返回兩次,在父程序中 fork 會返回子程序的 PID,在子程序中 fork 會返回 0;父程序與子程序是併發執行的獨立程序。核心能夠以任何方式交替執行他們邏輯控制流中的指令。

hello 的 fork 程序建立過程為:系統程序建立 hello 子程序然後呼叫 waitpid() 函式知道 hello 子程序結束
6.4 Hello的execve過程
execve函式在當前程序的上下文中載入並執行一個新程式。函式宣告如下:
int execve(const char *filename, const char *argv[], const char *envp[]);
execve函式載入並執行可執行目標檔案filename,且帶引數列表argv和環境變數列表envp。只有當出現錯誤時,例如找不到filename,execve才會返回到呼叫程式。正常情況下,execve呼叫一次,但從不返回。
在這裡插入圖片描述
從棧底(高地址)到棧頂(低地址),首先是引數和環境字串。棧往上緊隨其後的是以null結尾的指標陣列,其中每個指標都指向棧中的一個環境變數字串。全域性變數environ指向這些之陣中的第一個envp[0]。緊隨環境變數陣列之後的是以null結尾的argv[]陣列,其中每個元素都指向棧中的一個引數字串。在棧的頂部是系統啟動函式libc_start_main的棧幀。
程式執行fork後,子程序呼叫exceve函式在當前程序的上下文中載入並執行一個新程式即 hello 程式,execve 呼叫駐留在記憶體中的被稱為啟動載入器的作業系統程式碼來執行 hello 程式,載入器刪除子程序現有的虛擬記憶體段,並建立一組新的程式碼、資料、堆和棧段。新的棧和堆段被初始化為零,通過將虛擬地址空間中的頁對映到可執行檔案的頁大小的片,新的程式碼和資料段被初始化為可執行檔案中的內容。最後載入器設定 PC 指向_start 地址,_start 最終呼叫 hello中的 main 函式。
在這裡插入圖片描述
6.5 Hello的程序執行
邏輯控制流:一系列程式計數器PC的值的序列叫做邏輯控制流,程序是輪流
使用處理器的,在同一個處理器核心中,每個程序執行它的流的一部分後被搶佔
(暫時掛起),然後輪到其他程序。
時間片:一個程序執行它的控制流的一部分的每一時間段叫做時間片。
使用者模式和核心模式:處理器通常使用一個暫存器提供兩種模式的區分,該寄
存器描述了程序當前享有的特權,當沒有設定模式位時,程序就處於使用者模式中,
使用者模式的程序不允許執行特權指令,也不允許直接引用地址空間中核心區內的
程式碼和資料;設定模式位時,程序處於核心模式,該程序可以執行指令集中的任
何命令,並且可以訪問系統中的任何記憶體位置。
上下文資訊:上下文就是核心重新啟動一個被搶佔的程序所需要的狀態,它由
通用暫存器、浮點暫存器、程式計數器、使用者棧、狀態暫存器、核心棧和各種內
核數據結構等物件的值構成。

在程序執行的某些時刻,核心可以決定搶佔當前程序,並重新開始一個先前被搶佔了的程序,這個決策就叫做排程,是由核心中稱為排程器的程式碼處理的。在內和排程了一個新的程序執行後,它就搶佔當前程序,並使用上文所述的上下文切換的機制將控制轉移到新的程序。核心代表的使用者執行系統呼叫時,可能會發生上下文切換;中斷也有可能引發上下文切換。
通過核心模式使用者模式的切換描述使用者態核心態轉換的過程,在切換的第一部分中,核心代表程序 A 在核心模式下執行指令。然後在某一時刻,它開始代表程序 B(仍然是核心模式下) 執行指令。在切換之後,核心代表程序 B 在使用者模式下執行指令。隨後,程序 B 在使用者模式下執行一會兒,直到磁碟發出一箇中斷訊號,表示資料已經從磁碟傳送到了記憶體。核心判定程序 B 已經運行了足夠長的時間,就執行一個從程序 B 到程序 A 的上下文切換,將控制返回給程序 A 中緊隨在系統呼叫 read 之後的那條指令。程序 A 繼續執行,直到下一次異常發生,依此類推。
在這裡插入圖片描述

6.6 hello的異常與訊號處理
在這裡插入圖片描述
檢視hello.c檔案進行,首先,如果引數不為3,那麼會列印一條預設語句,並異常退出。如果引數是3個,那麼會執行一個迴圈,每次迴圈會使hello程序休眠2.5秒,休眠後又會恢復hello。而且迴圈裡會輸出一條格式字串,其中有輸入的兩個引數字串。迴圈結束後,有一個getchar()等待一個標準輸入,然後就結束了。
6.6.1正常執行
在這裡插入圖片描述
程式正產執行,執行結束後進程被回收
6.6.2不停亂按
在這裡插入圖片描述
在執行時隨意鍵入,輸入的字串被儲存在了標準輸入的緩衝區內在得到’\n’指令時,被認為是指令,在hello執行完後被輸入到終端中執行。
6.6.3 Ctrl+C
在這裡插入圖片描述
shell父程序收到SIGINT訊號,結束hello,並回收hello程序
6.6.4 Ctrl+Z
在這裡插入圖片描述
輸出三條hello資訊後鍵入Ctrl+Z訊號,父程序收到訊號後,將hello程序掛起,檢視可發現hello程序並未被回收,呼叫fg 後再次被調到前臺執行,輸出餘下的字串。
在這裡插入圖片描述
jobs 命令可以檢視當前的關鍵命令(ctrl+Z/ctrl+C 這類)內容,比如這時候就會返回 ctrl+Z 表示暫停命令
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
pstree 是用程序樹的方法把各個程序用樹狀圖的方式連線起來

hello執行過程中會出現哪幾類異常,會產生哪些訊號,又怎麼處理的。
程式執行過程中可以按鍵盤,如不停亂按,包括回車,Ctrl-Z,Ctrl-C等,Ctrl-z後可以執行ps jobs pstree fg kill 等命令,請分別給出各命令及執行結截圖,說明異常與訊號的處理。
6.7本章小結
本章從程序的角度分別描述了 hello 子程序 fork 和 execve 過程,並針對 execve 過程中虛擬記憶體映像以及棧組織結構等作出說明。同時瞭解了邏輯控制流中核心的排程及上下文切換等機制。闡述了 Shell 和 Bash 執行的處理流程以及 hello 執行過程中可能引發的異常和訊號處理。
(第6章1分)

第7章 hello的儲存管理
7.1 hello的儲存器地址空間
邏輯地址:又稱相對地址,是程式執行由 CPU 產生的與段相關的偏移地址部分。他是描述一個程式執行段的地址。

實體地址:程式執行時載入到記憶體地址暫存器中的地址,記憶體單元的真正地址。他是在前端總線上傳輸的而且是唯一的。在 hello 程式中,他就表示了這個程式執行時的一條確切的指令在記憶體地址上的具體哪一塊進行執行。

線性地址:這個和虛擬地址是同一個東西,是經過段機制轉化之後用於描述程式分頁資訊的地址。他是對程式執行區塊的一個抽象對映。

虛擬地址:是Windows程式時執行在386保護模式下,這樣程式訪問儲存器所使用的邏輯地址稱為虛擬地址,與實地址模式下的分段地址類似,虛擬地址也可以寫為“段:偏移量”的形式,這裡的段是指段選擇器。
7.2 Intel邏輯地址到線性地址的變換-段式管理
在這裡插入圖片描述

段式管理(segmentation),是指把一個程式分成若干個段(segment)進行儲存,每個段都是一個邏輯實體(logical entity)。
使用者棧是棧段暫存器,共享庫的記憶體對映區域和執行時堆都是輔助段暫存器,讀/寫段是資料段暫存器,只讀程式碼段是程式碼段暫存器。
段暫存器(16位)用於存放段選擇符:
CS(程式碼段):程式程式碼所在段
SS(棧段):棧區所在段
DS(資料段):全域性靜態資料區所在段
其他3個段暫存器ES、GS和FS可指向任意資料段
段選擇符各欄位含義:
CS暫存器中的RPL欄位表示CPU的當前特權級(Current Privilege Level,CPL) RPL=00,為第0級,位於最高階的核心態,RPL=11,為第3級,位於最低階的使用者態,第0級高於第3級。出於環保護機制,核心工作在第0環,使用者工作在第3環,中間環留給中間軟體用。Linux僅用第0環和第3環。TI=0,選擇全域性描述符表(GDT),TI=1,選擇區域性描述符表(LDT)。
在這裡插入圖片描述
段描述符是一種資料結構,實際上就是段表項,分為使用者的程式碼段和資料段描述符,還有系統控制端描述符。
全域性描述符表GDT:只有一個,用來存放系統內每個任務都可能訪問的描述符,例如,核心程式碼段、核心資料段、使用者程式碼段、使用者資料段以及TSS(任務狀態段)等都屬於GDT中描述的段、區域性描述符表LDT:存放某任務(即使用者程序)專用的描述符
中斷描述符表IDT:包含256箇中斷門、陷阱門和任務門描述符
在這裡插入圖片描述
7.3 Hello的線性地址到實體地址的變換-頁式管理
概念上而言, 虛擬記憶體被組織為一個由存放在磁碟上的N個連續的位元組大小的單元組成的陣列。 每位元組都有一個唯一的虛擬地址,作為到陣列的索引。磁碟上陣列的內容被快取在主存中。和儲存器層次結構中其他快取一樣,磁碟(較低層)上的資料被分割成塊,這些塊作為磁碟和主存(較高層)之間的傳輸單元。 VM系統通過將虛擬記憶體分割為稱為虛 擬頁(VirtualPage, VP)的大小固定的塊來處理這個問題。 每個虛擬頁的大小為P= 護位元組。 類似地,實體記憶體被分割為物理頁(Physical Page, PP) , 大小也為P位元組(物理頁也被稱為頁幀(page frame) )。
為了有助於清晰理解儲存層次結構中不同的快取概念,我們將使用術語SRAM快取 來表示位於CPU和主存之間的Ll、L2和L3快取記憶體,並且用術語DRAM快取來表示虛擬記憶體系統的快取,它在主存中快取虛擬頁。
頁表是一種資料結構,它用於計算機作業系統中的虛擬記憶體系統,其儲存了虛擬地址到實體地址間的對映。每次地址翻譯硬體將一個虛擬地址轉換為實體地址時,都會讀取頁表。作業系統負責維護頁表的內容,以及在磁碟與DRAM之間來回傳送頁。最簡單的分頁表系統通常維護一個幀表和一個分頁表。幀表處理幀對映資訊。更高階系統中,幀表可以處理頁屬於的地址空間,統計資訊和其他背景資訊。分頁表處理頁的虛擬地址和物理幀的對映。還有一些輔助資訊,如當前存在標識位(present bit),髒資料標識位或已修改的標識位,地址空間或程序ID資訊。
在這裡插入圖片描述
頁表就是一個頁表條目(PageTable Entry, PTE)的陣列。虛擬地址空間中的每個頁在頁表中一固定偏移最處都有一個PTE。假設每個PTE是由一個有效位(validbit)和一個n位地址欄位組成的。有效位表明了該虛擬頁當前是否被快取在DRAM中。 如果設定了有效位,那麼地址欄位就表示DRAM中相應的 物理頁的起始位置, 這個物理頁中快取了該虛擬頁。如果沒有設定有效位,那麼一個空地址表示這個虛擬頁還未被分配。否則,這個地址就指向該虛擬頁在磁碟上的起始位置。
形式上來說,地址翻譯是一個N元素的虛擬地址空間(VAS)中的元素和一個M元素 的實體地址空間(PAS)中元素之間的對映。
在這裡插入圖片描述
上圖展示了MMU如何利用頁表來實現這種對映。CPU中的一個控制暫存器,頁表基址暫存器(PageTable Base Register, PTBR)指向當前頁表。n位的虛擬地址包含兩個部分:一個p位虛擬頁面偏移(VirtualPage Offset, VPO)和一個(n-p) 位的虛擬頁號(Virtual age Number, VPN)。MMU利用VPN來選擇適當的PTE。例如,VPN0選擇PTEO, VPN 1選擇PTE1, 以此類推。將頁表條目中物理頁號(PhysicalPage Number, PPN)和虛擬 地址中的VPO串聯起來,就得到相應的實體地址。注意,因為物理和虛擬頁面都是P位元組的,所以物理頁面偏移(PhysicalPage Offset, PPO)和VPO是相同的。
7.4 TLB與四級頁表支援下的VA到PA的變換
在 Intel Core i7環境下研究VA到PA的地址翻譯問題。前提如下:
虛擬地址空間48位,實體地址空間52位,頁表大小4KB,4級頁表。TLB 4 路16組相聯。CR3指向第一級頁表的起始位置(上下文一部分)。
解析前提條件:由一個頁表大小4KB,一個PTE條目8B,共512個條目,使
用9位二進位制索引,一共4個頁表共使用36位二進位制索引,所以VPN共36位,因為VA 48位,所以 VPO 12位;因為TLB共 16組,所以TLBI需4 位,因為VPN
36位,所以TLBT 32 位。
如圖 ,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中新增條目。
如果查詢 PTE 的時候發現不在實體記憶體中,則引發缺頁故障。如果發現許可權不夠,則引發段錯誤。
在這裡插入圖片描述

7.5 三級Cache支援下的實體記憶體訪問
在此僅討論L1級快取情況,由於L2,L3級快取訪問過程與L1相似。
在這裡插入圖片描述
實體記憶體訪問,是基於MMU將虛擬地址翻譯成實體地址之後,向cache中訪問的。
在這裡插入圖片描述
在cache中實體地址定址,按照三個步驟:組選擇、行匹配和字選擇。在衝突不命中時還會發生行替換。
快取記憶體(S, E, B, m)被組織成一個有S=2s個快取記憶體組(cache set)的陣列。 每個組包含E 個快取記憶體行(cache line).每個行是由一個B=2b位元組的資料塊(block)組成的, 一個有效位(valid bit) 指明這個行是否包含有意義的資訊,還有t=m-(b+s)個標記位(tag bit)(是當前塊的記憶體地址的位的一個子集),它們唯一地標識儲存在這個快取記憶體行中的塊。
一般而言,快取記憶體的結構可以用元組(S, E, B, m)來描述。 快取記憶體的大小(或容量)C指的是所有塊的大小的和。標記位和有效位不包括在內。因此,C=S×E×B。
快取記憶體的結構將m個地址位劃分為t個標記位,s個組索引位,和b個塊偏移位。
在組選擇中,cache按照實體地址的s個組索引位(S=2s)來定位該地址對映的組。
選擇好組後,遍歷組中的每一行,比較行的標記和地址的標記,當且僅當這兩者相同,並且行的有效位設為1時,才可以說這一行中包含著地址的一個副本。也就是快取命中了。
最後是字選擇。定位好了要定址的地址在哪一行之後,根據地址的塊偏移量,在行的資料塊中偏移定址,最後得到的字,就是我們定址得到的字。
如果快取不命中,那麼它需要從儲存器層次結構中的下一層取出被請求的塊,然後將新的塊儲存在組索引位指示的組中的一個快取記憶體行中。這個過程,如果有衝突不命中,就會觸發行的替換。
7.6 hello程序fork時的記憶體對映
當 fork 函式被 shell 程序呼叫時,核心為新程序建立各種資料結構,並分配給
它一個唯一的 PID,為了給這個新程序建立虛擬記憶體,它建立了當前程序的mm_struct、區域結構和頁表的原樣副本。它將這兩個程序的每個頁面都標記為只讀,並將兩個程序中的每個區域結構都標記為私有的寫時複製。
7.7 hello程序execve時的記憶體對映
execve 函式代用駐留在核心區域的啟動載入器程式碼,在當前程序中載入並運
行包含在可執行目標檔案 hello 中的程式,用 hello 程式有效地替代了當前程式。載入並執行hello需要以下幾個步驟:

  1. 刪除已存在的使用者區域,刪除當前程序虛擬地址的使用者部分中的已存在的區域結構。
  2. 對映私有區域,為新程式的程式碼、資料、bss和棧區域建立新的區域結構,所有這些新的區域都是私有的、寫時複製的。程式碼和資料區域被對映為hello檔案中的.text和.data區,bss區域是請求二進位制零的,對映到匿名檔案,其大小包含在hello 中,棧和堆地址也是請求二進位制零的,初始長度為零。
  3. 對映共享區域, hello 程式與共享物件 libc.so 連結,libc.so 是動態連結到這個程式中的,然後再對映到使用者虛擬地址空間中的共享區域內。
  4. 設定程式計數器(PC),execve做的最後一件事情就是設定當前程序上下文的程式計數器,使之指向程式碼區域的入口點。
    在這裡插入圖片描述
    7.8 缺頁故障與缺頁中斷處理
    實體記憶體(DRAM)快取不命中成為缺頁。假設CPU引用了磁碟上的一個字,而這個字所屬的虛擬頁並未快取在DRAM中。地址翻譯硬體會從記憶體中讀取虛擬頁對應的頁表,推斷出這個虛擬頁未被快取,然後觸發一個缺頁異常。缺頁異常呼叫核心中的缺頁異常處理程式,該程式會選擇一個犧牲頁。如果被犧牲的頁面被修改了,那麼核心會把它複製回磁碟。總之,核心會修改被犧牲頁的頁表條目,表示它不再快取在DRAM中了。
    之後,核心從磁碟把本來要讀取的那個虛擬頁,複製到記憶體中犧牲頁的那個位置,更新它的頁表條目,隨後返回。當異常處理程式返回時,會重新啟動導致缺頁的指令,該指令會把導致缺頁的虛擬地址重發送到地址翻譯硬體。於是,地址翻譯硬體可以正常處理現在的頁命中了。

7.9動態儲存分配管理
動態記憶體分配器維護著一個程序的虛擬記憶體區 域,稱為堆(heap)系統之間細節不同,但是不失通用性,假設堆是一個請求二進位制零的區域,它緊接在未初始化的資料區域後開始,並向上生長(向更高的地址)。對於每個程序,核心維護著一個變數brk(讀做"break"),它指向堆的頂部。
分配器將堆視為一組不同大小的塊(block)的集合來維護。每個塊就是一個連續的虛擬記憶體片(chunk),要麼是已分配的,要麼是空閒的。已分配的塊顯式地保留為供應用程式使用。空閒塊可用來分配。空閒塊 保持空閒,直到它顯式地被應用所分配。一個已分配 的塊保持已分配狀態,直到它被釋放,這種釋放要麼是應用程式顯式執行的,要麼是記憶體分配器自身隱式執行的。
分配器有兩種基本風格。 兩種風格都要求應用顯式地分配塊。 它們的不同之處在於由哪個實體來負責釋放已分配的塊。
在這裡插入圖片描述
顯式分配器(explicitallocator), 要求應用顯式地釋放任何已分配的塊。例如,C標準庫提供一種叫做ma耳oc程式包的顯式分配器。C程式通過呼叫malloc函式來 分配一個塊,並通過呼叫free函式來釋放一個塊。C++中的new和delete操作符與C中的malloc和free相當。
隱式分配器(implicitallocator) , 另一方面,要求分配器檢測一個已分配塊何時不再被程式所使用,那麼就釋放這個塊。隱式分配器也叫做垃圾收集器(garbagecollec­tor), 而自動釋放未使用的巳分配的塊的過程叫做垃圾收集(garbagecollection)。例如,諸如Lisp、ML以及Java之類的高階語言就依賴垃圾收集來釋放已分配的塊。
⑴ 隱式空閒連結串列—在此介紹帶邊界標籤的隱式空閒連結串列分配器原理
假設想要釋放的塊為當前塊。那麼合併下一個空閒塊很簡單而且高效。當前塊的頭部指向下一個塊的頭部,可以檢查這個指標以判斷下一個塊是否是空閒的。如果是,就將它的大小簡單地加到當前塊頭部的大小上,這兩個塊在常數時間內被合併。
給定一個帶頭的隱式空閒連結串列,唯一的選擇將是搜尋整個連結串列。記住前面塊的位置,直到我們打到當前塊。使用隱式空閒連結串列,這意味著每次呼叫free需要的時間都於堆的大小呈線性關係。即使使用更復雜精細的空閒連結串列組織,搜尋時間也不會是常數。
Knuth提出一種聰明而通用的技術,叫做邊界標記,允許在常數時間內進行對前面塊的合併,這種思想,是在每個塊的結尾處新增一個腳部,其中腳部就是頭部的一個副本。如下所示:
在這裡插入圖片描述
如果每個塊包括這樣一個腳部,那麼分配器就可以通過檢查它的腳部,判斷前面一個塊的起始位置和狀態,這個腳部總是在距當期塊開始位置一個字的距離。那麼,分配器釋放當前塊時存在四種可能情況:
(1) 前面的塊和後面的塊都是已分配的
(2) 前面的塊是已分配的,後面的塊是空閒的
(3) 前面的塊是空閒的,而後面的塊是已分配的
(4) 前面的和後面的塊都是空閒的。
下圖,展示了這四種情況合併的過程:
在這裡插入圖片描述
⑵ 顯示空閒連結串列
顯式空閒連結串列是一種更好的方式是將空閒塊組織為某種形式的顯式資料結構。因為根據定義,程式不需要一個空閒塊的主體,所以實現這個資料結構的指標可以存放在這些空閒塊的主體裡面。例如,堆可以組織成一個雙向空閒連結串列,在每個空閒塊中,都包含一個pred(前驅)和succ(後繼)指標。
使用雙向連結串列而不是隱式空閒連結串列,使首次適配的分配時間從塊總數的線性時間減少到了空閒塊數量的線性時間。不過,釋放一個塊的時間可以是線性的,也可能是常數,這取決於我們所選擇的空閒連結串列中塊的排序策略。
一種方法是用後進先出(LIFO)的順序維護連結串列,將新釋放的塊位置放在連結串列的開始出。使用LIFO的順序和首次適配的放置策略,分配器會最先檢查最近使用過的塊。在這種情況下,釋放一個塊可以在常數時間內完成。如果使用了邊界標記,那麼合併也可以在常數時間內完成。
另一種方法是按照地址順序來維護連結串列,其中連結串列中每個塊的地址都小於它後繼的地址。在這種情況下,釋放一個塊需要線性時間的搜尋來定位合適的前驅。平衡點在於,按照地址排序的首次適配比LIFO排序的首次適配有更高的儲存器利用率,接近最佳適配的利用率。
在這裡插入圖片描述
7.10本章小結
本章從 Linux 儲存器的地址空間起,闡述了 Intel 的段式管理和頁式管理機制,以及 TLB 與多級頁表支援下的 VA 到 PA 的轉換,同時對 cache 支援下的實體記憶體訪問做了說明。針對記憶體對映及管理,簡述了 hello 的 fork 和 execve 記憶體對映,瞭解了缺頁故障與缺頁中斷處理程式,對動態分配管理做了系統闡述。
(第7章 2分)

第8章 hello的IO管理
8.1 Linux的IO裝置管理方法
裝置的模型化:所有的IO裝置都被模型化為檔案,而所有的輸入和輸出都被
當做對相應檔案的讀和寫來執行,這種將裝置優雅地對映為檔案的方式,允許Linux 核心引出一個簡單低階的應用介面,稱為Unix I/O。
8.2 簡述Unix IO介面及其函式
Unix I/O介面統一操作:
裝置可以通過Unix I/O介面被對映為檔案,這使得所有的輸入和輸出都能以一種統一且一致的方式來執行:
⑴ 開啟檔案。一個應用程式通過要求核心開啟相應的檔案,來宣告它想要訪問一個I/O裝置,核心返回一個小的非負整數,叫做描述符,它在後續對此檔案的所有操作中標識這個檔案,核心記錄有關這個開啟檔案的所有資訊。應用程式只需記住這個描述符。
⑵ Linux shell建立的每個程序開始時都有三個開啟的檔案:標準輸入(描述符為0)、標準輸出(描述符為1)和標準錯誤(描述符為2)。標頭檔案<unistd.h>定義了常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO,它們可以用來代替顯式的描述符值。
⑶ 改變當前的檔案位置。對於每個開啟的檔案,核心保持著一個檔案位置k,初始為0,這個檔案位置是從檔案開頭起始的位元組偏移量,應用程式能夠通過執行seek,顯式地將改變當前檔案位置k。
⑷ 讀寫檔案。一個讀操作就是從檔案複製n>0個位元組到記憶體,從當前檔案位置k開始,然後將k增加到k+n。給定一個大小為m位元組的而檔案,當k>=m時執行讀操作會觸發一個成為end-of-file(EOF)的條件,應用程式能檢測到這個條件。在檔案結尾處並沒有明確的“EOF符號”。類似一個寫操作就是從記憶體中複製n>0個位元組到一個檔案,從當前檔案位置k開始,然後更新k。
⑸ 關閉檔案。當應用完成了對檔案的訪問之後,它就通知核心關閉這個檔案。作為響應,核心釋放檔案開啟時建立的資料結構,並將這個描述符恢復到可用的描述符池中。無論一個程序因為何種原因終止時,核心都會關閉所有開啟的檔案並釋放它們的記憶體資源。
Unix I/O介面函式:
⑴ 在Unix I/O介面中,程序是通過呼叫open函式來開啟一個存在的檔案或者建立一個新檔案的,函式宣告如下:
int open(char *filename, int flags, mode_t mode);
open函式將filename轉換為一個檔案描述符,並且返回描述符數字。返回的描述符總是在程序中當前沒有開啟的最小描述符。flags引數指明瞭程序打算如何訪問這個檔案。mode引數指定了新檔案的訪問許可權位。作為上下文的一部分,每個程序都有一個umask,它是通過呼叫umask函式來設定的。當程序通過帶某個mode引數的open函式呼叫來建立一個新檔案時,檔案的訪問許可權位被設定成mode&~umask。
⑵ 程序通過呼叫close函式關閉一個開啟的檔案。函式宣告如下:
int close(int fd);
fd是需要關閉的檔案的描述符,close返回操作結果。成功返回0錯誤返回EOF
⑷ 應用程式是通過分別呼叫read和write函式來執行輸入和輸出的。函式宣告如下:
ssize_t read(int fd, void *buf, size_t n);
ssize_t write(int fd, const void *buf, size_t n);
read函式從描述符為fd的當前檔案位置賦值最多n個位元組到記憶體位置buf。返回值-1表示一個錯誤,0表示EOF,否則返回值表示的是實際傳送的位元組數量。write函式從記憶體位置buf複製至多n個位元組到描述符為fd的當前檔案位置。
⑸ 通過呼叫lseek函式,應用程式能夠顯式地修改當前檔案的位置。函式宣告如下:
off_t lseek(int handle, off_t offset, int fromwhere);

8.3 printf的實現分析
printf函式是在stdio.h標頭檔案中宣告的,具體程式碼實現