CSAPP大作业论文 程序人生
計算機系統
大作業
題 目 程序人生-Hello’s P2P
專 業 計算機
學 號 1190200721
班 級 1936602
學 生 張少卿
指 導 教 師 劉宏偉
計算機科學與技術學院
2021年6月
摘 要
一個c語言程序到它完整地在計算機系統中實現需要經歷過許多過程。首先是要將.c文件轉換為可執行文件。這其中經歷了預處理、編譯、匯編、鏈接4個過程,形成可執行文件后,計算機就能讀懂我們的.c代碼去執行其中的語句。
計算機在執行文件時候又有許多學問。例如:進程的概念,計算機存儲的管理,IO的管理。一個文件的執行其背后是計算機許許多多不同且重要的功能合作完成的。
關鍵詞:預處理;匯編;進程;存儲管理;IO管理;
文章目錄
- 摘 要
- 第1章 概述
- 1.1 Hello簡介
- 1.2 環境與工具
- 1.3 中間結果
- 1.4 本章小結
- 第2章 預處理
- 2.1 預處理的概念與作用
- 2.2在Ubuntu下預處理的命令
- 2.3 Hello的預處理結果解析
- 2.4 本章小結
- 第3章 編譯
- 3.1 編譯的概念與作用
- 3.2 在Ubuntu下編譯的命令
- 3.3 Hello的編譯結果解析
- 3.3.1 變量/常量 and 賦初值/不賦初值 and 類型轉換
- 3.3.2 sizeof
- 3.3.3 算術操作:+ - * / % ++ -- 取正/負 符合
- 3.3.4邏輯/位操作
- 3.3.5關系操作
- 3.3.6數組/指針/結構操作
- 3.3.7 循環
- 3.3.8函數
- 3.4 本章小結
- 第4章 匯編
- 4.1 匯編的概念與作用
- 4.2 在Ubuntu下匯編的命令
- 4.3 可重定位目標elf格式
- 4.4 Hello.o的結果解析
- 4.5 本章小結
- 第5章 鏈接
- 5.1 鏈接的概念與作用
- 5.2 在Ubuntu下鏈接的命令
- 5.3 可執行目標文件hello的格式
- 5.4 hello的虛擬地址空間
- 5.5 鏈接的重定位過程分析
- 5.6 hello的執行流程
- 5.7 Hello的動態鏈接分析
- 5.8 本章小結
- 第6章 hello進程管理
- 6.1 進程的概念與作用
- 6.2 簡述殼Shell-bash的作用與處理流程
- 6.3 Hello的fork進程創建過程
- 6.4 Hello的execve過程
- 6.5 Hello的進程執行
- 6.6 hello的異常與信號處理
- 6.7本章小結
- 第7章 hello的存儲管理
- 7.1 hello的存儲器地址空間
- 7.2 Intel邏輯地址到線性地址的變換-段式管理
- 7.3 Hello的線性地址到物理地址的變換-頁式管理
- 7.4 TLB與四級頁表支持下的VA到PA的變換
- 7.5 三級Cache支持下的物理內存訪問
- 7.6 hello進程fork時的內存映射
- 7.7 hello進程execve時的內存映射
- 7.8 缺頁故障與缺頁中斷處理
- 7.9動態存儲分配管理
- 7.10本章小結
- 第8章 hello的IO管理
- 8.1 Linux的IO設備管理方法
- 8.2 簡述Unix IO接口及其函數
- 8.3 printf的實現分析
- 8.4 getchar的實現分析
- 8.5本章小結
- 附件
- 參考文獻
第1章 概述
1.1 Hello簡介
根據Hello的自白,利用計算機系統的術語,簡述Hello的P2P,020的整個過程。
P2P:Hello的生命從hello.c開始,我們的hello.c首先經過第一道關卡——預處理,預處理器cpp將它預處理,將它變形為hello.i。于是我們的hello.i懵懵懂懂地來到了第二關卡——編譯,編譯器“咔咔咔”將它徹底改頭換面,我們的hello.i就變為了hello.s。可是我們的發廊總裁“匯編器”對它還是不太滿意。于是稍加修改。Hello.o文件問世。最后,hello.o滿意地來到了最后一關。鏈接器ld將一身衣服“庫函數”穿戴在它的身上,我們的hello就從原來的鄉下小子hello.c變為了大明星hello可執行目標文件,實現P2P過程。
020:子進程調用execve,映射虛擬內存并載入物理內存,進入程序入口處開始執行,同時,CPU為運行的hello分配時間片并執行邏輯控制流。在中途調用異常處理函數處理可能出現的異常。最后當hello進程終止,父進程shell將回收hello,接著內核刪除相關數據結構的整個過程叫做020。
1.2 環境與工具
硬件:Intel? Core? i5-9300H CPU @ 2.40GHz 8G RAM
虛擬機VUbuntu18.4
開發工具:gcc ld readelf gedit objdump edb gdb hexedit
Visual Studio 2019;CodeBlocks 64位Mware Workstation Pro15.0
1.3 中間結果
Hello 可執行文件
Hello.i 預處理后文件
Hello.s 編譯后文件
Hello.o 匯編后文件
Hello_.s 反匯編文件
1.4 本章小結
本章簡要介紹了hello的P2P,020的過程,列出了運行的環境和使用的工具,以及中間結果。
第2章 預處理
2.1 預處理的概念與作用
預處理概念:預處理器(cpp)根據以字符#開頭的命令,修改原始的c程序。例如:#include<stdio.h>告訴預處理器讀取系統頭文件stdio.h的內容,并把它插入到程序文本中。得到一個新的c程序,通常是以.i命名。
預處理作用:
1.處理文件包含
2.處理宏定義
3.處理注釋
4.處理條件編譯
5.處理一些特殊控制指令
2.2在Ubuntu下預處理的命令
gcc hello.c -E -o hello.i
2.3 Hello的預處理結果解析
.i文件的開頭:
其中數字1、 2、 3、 4表示:
1:表示新文件的開始
2:表示返回一個文件(包含另外一個文件之后)
3:表示一下文本來自系統頭文件,因此應該抑制某些警告
4:表示一下文本應該被視為包含在隱式的extern“C”塊中。
Stdio.h文件預處理后:
定義了些奇怪的符號:
關鍵字extern標示變量或者函數的定義在別的文件中:
可以發現.i文件中有許多的extern。
拉到.i文件的最下方,可以發現我們的函數體,其中#的注釋都被刪除了。
2.4 本章小結
預處理是為了將我們原始的.c文件拓展成更大的c文件,添加了許多的代碼是為了方便后續編譯器的操作。
第3章 編譯
3.1 編譯的概念與作用
編譯的概念:編譯器(ccl)將文本文件.i翻譯成文本文件.s,它包含了一個匯編語言程序。
編譯的作用:
1.掃描(詞法分析)
將源代碼程序輸入掃描器,將源代碼的字符序列分割成一系列記號。
2.語法分析
基于詞法分析得到的一系列記號,生成語法樹。
3.語義分析
由語義分析器完成,指示判斷是否合法,并不判斷對錯。又分:靜態語義:隱含浮點型到整形的轉換,會報warning;動態語義:在運行時才能確定。
4.源代碼優化(中間語言生成)
中間代碼(語言)使得編譯器分為前端和后端,前端產生與機器(或環境)無關的中間代碼,編譯器的后端將中間代碼轉換為目標機器代碼,目的:一個前端對多個后端,適應不同平臺。
5.代碼生成,目標代碼優化
編譯器后端主要包括:代碼生成器:依賴于目標機器,依賴目標機器的不同字長,寄存器,數據類型等。
目標代碼優化器:選擇合適的尋址方式,左移右移代替乘除,刪除多余指令。
3.2 在Ubuntu下編譯的命令
編譯的命令:gcc -S hello.i -o hello.s
3.3 Hello的編譯結果解析
第一行解釋了文件的由來
第二行解釋了全局變量名
第四行解釋了對齊的大小是4字節
這些應該是說明了這些變量存儲的位置。
例如:LC0存儲了字符串“Usage: Hello 學號 姓名!”,其存儲在.string節中。
3.3.1 變量/常量 and 賦初值/不賦初值 and 類型轉換
全局變量:全局變量一般都存儲在.rodata節
Hello.c中的Sleepsecs全局變量存放在.long節和.rodata中,存儲的值是2,但我們觀察c程序,發現其賦值是2.5。所以在這里有一個隱式的轉換!將2.5轉為int的2!
局部變量:賦初值局部變量一半都存儲在.bss節中,未賦初值的局部變量則不存放在某個位置,要是用時才存放在寄存器中
在hello.c中局部變量i在前面的編譯語句中沒有找到,我們觀察對i的調用是在循環中,所以我們去找尋相關的位置。
i就保存在這個位置,看來局部變量在這里是存儲在了棧中-4(%rbp)位置。
3.3.2 sizeof
Sizeof函數一般是在編譯的時候就處理完成了,它在編譯的時候就將變量類型對應的立即數改寫到匯編指令中。
3.3.3 算術操作:+ - * / % ++ – 取正/負 符合
算術操作都是在寄存器的基礎上完成的。以hello.c為例,在hello.c中出現了++運算。它執行是在循環中,我們去循環體中尋找:
++就是把對應的寄存器+1,同理–就是把對應的寄存器-1。
從上面的例子我們很容易猜到加法運算的指令就是add,至于其他算術操作指令如下:
-
- Sub;
-
- imul;
- / 一般是通過加減法來實現;
- % 同除法;
- NEG 取負
- 復合 拆分開一步一步計算
3.3.4邏輯/位操作
同樣也是對寄存器進行操作。
與& and;
或| or;
異或^ xor;
非~ not;
左移<<:
算術左移 sal;
邏輯左移 shl;
右移>>:
算術左移 sar;
邏輯左移 shr;
3.3.5關系操作
在hello.c中的循環體中就有關系操作,一般是用cmp進行比較然后設定條件碼,然后用j*語句執行相關的語句
例如將i和9進行比較,若小于等于則進入.L4塊。以下是所有j*指令:
指令 同義名 跳轉條件 描述
Jmp 1 直接跳轉
Je Jz ZF 相等/零
Jne Jnz ~ZF 不相等/非零
Js SF 負數
Jns ~SF 非負數
Jg Jnle ~(SF^OF)&ZF 大于(有符號)
Jge Jnl ~(SF^OF) 大于等于(有符號)
Jl Jnge SF^OF 小于(有符號)
Jle Jng (SF^OF)|ZF 小于等于(有符號)
Ja Jnbe CF&ZF 超過(無符號)
Jae Jnb ~CF 超過相等(無符號)
Jb Jnae CF 低于(無符號)
Jbe Jna CF|ZF 低于等于(無符號)
3.3.6數組/指針/結構操作
數組和指針都是以地址來進行操作,在hello.c中有argv數組和argv。在hello.c中argv的地址存放在了-32(%rbp)的位置(具體分析看下面的函數操作)。
所以%rax存放的就是argv[0]的地址。所以%rax+16=argv[2],%rax+8=argv[1],這里的8是一個字節,16是兩個字節,剛好是char的大小的倍數。
3.3.7 循環
很顯然cmpl指令就是循環體內部的比較語句,如果-4(%rbp)小于等于9則進入.L4塊,這是一種guarded-do寫法。接下來我們來觀察.L4塊。
.L4是循環中的語句(具體分析看下面函數調用),兩個函數執行完畢后,第一次循環結束,i++,于是addl指令就執行了該操作。然后再此進入.L3塊。
3.3.8函數
在hello.c中有許多的函數調用的例子
函數的參數傳遞:
Main函數
這兩條指令執行了main函數的參數傳遞,函數的參數傳遞通常是保存在%rdi和%rsi寄存器中。根據.c文件,我們清楚在%edi中保存的是第一個參數argc,在%rsi中保存的是第二個參數*argv。
然后是函數的調用:
我們知道循環中主要是printf和調用sleep函數。這里就涉及了數組的偏移操作,我們知道數組首地址存放在-32(%rbp)中,所以%rax存放的就是argv[0]的地址。所以%rax+16=argv[2],%rax+8=argv[1],這里的8是一個字節,16是兩個字節,剛好是char的大小。再將.LC1存放于%edi寄存器,參數設置完畢,接下來就是調用printf函數了。執行完后再將sleepsecs存放于%edi寄存器調用sleep函數。
函數的調用有兩種方式:1. 直接調用,2. 間接調用。直接調用的目標是作為指令的一部分編碼,間接調用涉及了相對尋址,相對于call指令的下一條指令的地址。
函數的返回return:
當循環體結束后,調用getchar函數,由于return 0,將0存放于%eax寄存器,因為%eax是指定的返回函數存儲的寄存器。然后leave和ret,函數調用結束返回。Leave是對棧的操作,取出數據然后%rsp+8/+16。
3.4 本章小結
編譯這一步,將hello.c文件從高級語言轉變到了匯編語言,我們發現,匯編語言是機器代碼的文本表示,更方便跟機器“溝通”。在這一步后,匯編器才能理解我們的程序,從而將我們的程序轉變為更底層的機器語言。
第4章 匯編
4.1 匯編的概念與作用
匯編的概念:匯編器(as)將hello.s翻譯成機器語言指令,并把這些指令打包成一種叫做可重定位目標程序。將結果保存在目標文件hello.o中
匯編的作用:
將匯編指令轉成機器可以直接識別的機器指令
4.2 在Ubuntu下匯編的命令
匯編的指令:gcc -c hello.s -o hello.o
4.3 可重定位目標elf格式
指令:readelf -a hello.o
相關的解讀
Magic:魔數 E45 L4C F46
Data數據:補碼表示,小端
Entry point address入口點地址:ELF起始位置
Start of program headers程序頭起點:程序頭表起始位置
Start of section headers:偏移量
Number of program headers:表象個數
Size of section headers節頭大小:每個節的大小
節頭數量:節個數
字符串表索引頭:.strtab節的位置
2.節頭部表,不同節的位置和大小都是節頭部表描述的,其中每個節都一個固定大小的條目。
相關的說明:
大小:節的字節數
偏移量:節相對于自己的起始位置的偏移量(起始位置跟對齊有關)
對齊:限制了節的起始地址,4就是代表了對齊最小單位是1000。
3.重定位信息
在鏈接的時候需要對符號重新定義,定義的信息就來自于重定位節。可以看到函數和全局變量都在其中。
4.符號表
存放了程序中定義的全局變量和函數的信息。
4.4 Hello.o的結果解析
在hello.o的反匯編出來的匯編語言中,可以發現其將符號解析并增加了重定位信息,例如原本的.L2塊中的jmp .L3轉換為了地址。
還有就是全局變量sleepsecs和函數調用。
由于每條指令都有了自己的相對地址,所以分支轉移函數調用就可以直接用相關指令的相對地址來進行跳轉,同時增加了重定位信息,方便后續鏈接的時候修改。
4.5 本章小結
在進行匯編過程后,hello.s文件轉換為了二進制可重定位文件hello.o,在hello.o中我們hello.s本來是符號的地方都進行了解析轉換為了一串地址。所以hello.o文件與hello.s文件有著不同之處,同時也更有利于機器理解。
第5章 鏈接
5.1 鏈接的概念與作用
鏈接的概念:鏈接器(ld)將多個.o文件合并,得到一個可執行目標文件(或者簡稱為可執行文件),可以被加載到內存中,由系統執行。
鏈接的好處:
一個程序可以分成很多源程序文件
可以構建共享函數庫
時間上,可分開編譯
空間上,無需包含共享庫所有代碼
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的格式
這里ELF所顯示的信息與hello.s的ELF的顯示的都是一樣的就不再解讀,不過我們可以發現hello中節頭的數量比hello.s的多得多。
各節頭的信息:
節頭多了許多,不過閱讀的方式沒有改變。主要關注地址,偏移量和對齊
共享庫的地址:
5.4 hello的虛擬地址空間
使用edb加載hello,查看本進程的虛擬地址空間各段信息,并與5.3對照分析說明。
我們的首要目標是.interp段的內容。有節頭部表,可以知道該段的起始地址為0x402e0,那我們現在來查看該段的內容,我們發現其內容為:/lib64/ld-linux-x86-64.so.2。我們知道這是鏈接器的信息。
接下來搜索.dynstr節,結果為:.libc.so.6.exit.puts.printf.getchar.atoi.sleep.libc_strat_main.GLIBC_2.2.5.gmon_start。顯然這些都是共享庫的信息。
再來查看.rodata節,里面是字符串常量。
5.5 鏈接的重定位過程分析
相比于hello.o的反匯編,hello的反匯編多了許多函數,而不僅有main。例如_init、puts、printf等等。同時在hello的反匯編中,地址全部轉換為了虛擬地址,起始從400000開始,而不是0開始。
而原先hello.s反匯編中的重定位信息也都使用上改為了符號所對應的虛擬地址,由于符號和指令的相對位置是不變的,所以當求出指令的地址時,符號的地址也可以由這個重定位信息計算出來。
5.6 hello的執行流程
ld-2.31.so!_dl_start
ld-2.31.so!_dl_init
hello!_start 0x400500
libc-2.31.so!__libc_start_main
-libc-2.31.so!__cxa_atexit
-libc-2.31.so!__libc_csu_init
hello!_init
libc-2.31.so!_setjmp
-libc-2.31.so!_sigsetjmp
–libc-2.31.so!__sigjmp_save
hello!main
hello!puts@plt
hello!exit@plt
*hello!printf@plt
*hello!sleep@plt
*hello!getchar@plt
ld-2.31.so!_dl_runtime_resolve_xsave
ld-2.31.so!_dl_fixup
ld-2.31.so!_dl_lookup_symbol_x
libc-2.31.so!exit
5.7 Hello的動態鏈接分析
(以下格式自行編排,編輯時刪除)
分析hello程序的動態鏈接項目,通過edb調試,分析在dl_init前后,這些項目的內容變化。要截圖標識說明。
hello程序對動態鏈接庫的引用,利用代碼段和數據段之間距離不變這一個事實,因此代碼段中任何指令和數據段中任何變量之間的距離都是一個運行時常量。
GNU編譯系統使用了一種有趣的延遲綁定技術來解決動態庫函數模塊調用的問題,將過程地址的綁定推遲到了第一次調用該過程時。
延遲綁定通過全局偏移量表(GOT)和過程鏈接表(PLT)的交互實現。如果一個目標模塊調用定義在共享庫中的任何函數,那么就有自己的GOT和PLT。前者是數據段的一部分,后者是代碼段的一部分。下圖是PLT數組。
GOT是一個數組,其中每個條目是8字節地址。和PLT 聯合使用時, GOT[O] 和GOT[l] 包含動態鏈接器在解析函數地址時會使用的信息。GOT[2] 是動態鏈接器在ld-linux.so 模塊中的入口點。其余的每個條目對應于一個被調用的函數,其地址需要在運行時被解析。每個條目都有一個相匹配的PLT 條目。
下面以printf為例子來解釋動態鏈接:
第一次調用printf:
第一步:程序進入printf對應的PLT條目。
第二步:第一條PLT指令通過相對應的GOT進行簡介跳轉,因為每個GOT條目初始都指向它對應的PLT條目的第二條指令,這個簡介跳轉知識簡單地把控制傳送回PLT[2]中的下一條指令。
第三步:把printf的ID壓入棧中,PTL跳轉到PTL[0]
第四步:PTL[0]通過GOT[1]間接地吧動態鏈接器的一個參數壓入棧中,然后通過GOT[2]間接跳轉進動態鏈接器中。動態連接器使用兩個棧條目來確定printf的運行地址,將地址重寫回printf對應的GOT,再把控制傳遞給printf。
后續再調用printf時,GOT的間接跳轉會直接將控制轉移到printf。
Printf調用前GOT表
Printf調用后GOT表
5.8 本章小結
鏈接器將我們需要的.o文件合并成一個可執行文件,這樣的文件就可以在shell中執行了,我們的文件成功從原來的.c文件轉換為了一個能在計算機中執行的文件。
第6章 hello進程管理
6.1 進程的概念與作用
進程的概念:進程是計算機科學中最深刻、最成功的概念之一。進程是正在運行的程序的實例。
進程的作用:
2.一個私有的地址空間,它提供一個假象,好像我們的程序獨占地使用內存系統。
6.2 簡述殼Shell-bash的作用與處理流程
Shell:shell 是一個交互型應用級程序,代表用戶運行其他程序。是系統的用戶界面,提供了用戶與內核進行交互操作的一種接口。他就收喲洪湖輸入的命令并把它送入內核去執行。
2.功能:其實 shell 也是一支程序,它由輸入設備讀取命令,再將其轉為計算機可以了解的機械碼,然后執行它。各種操作系統都有它自己的 shell,以 DOS 為例,它的 shell 就是 command.com 文件。如同 DOS 下有 NDOS,4DOS,DRDOS 等不同的命令解譯程序可以取代標準的 command.com ,UNIX 下除了 Bourne shell(/bin/sh)外還有 C shell(/bin/csh)、Korn shell(/bin/ksh)、Bourne again shell(/bin/bash)、Tenex C shell(tcsh)等其它的 shell。UNIX/linux 將 shell 獨立于核心程序之外,使得它就如同一般的應用程序, 可以在不影響操作系統本身的情況下進行修改、更新版本或是添加新的功能。
Shell 是一個命令解釋器,它解釋由用戶輸入的命令并且把它們送到內核。不僅如此,Shell 有自己的編程語言用于對命令的編輯,它允許用戶編寫由 shell 命令組成的程序。Shell 編程語言具有普通編程語言的很多特點,比如它也有循環結構和分支控制結構等,用這種編程語言編寫的 Shell 程序與其他應用程序具有同樣的效果。
3.處理流程: shell 首先檢查命令是否是內部命令,若不是再檢查是否是一個應用程序(這里的 應用程序可以是 Linux 本身的實用程序,如 ls 和 rm,也可以是購買的商業程序, 如 xv,或者是自由軟件,如 emacs)。然后 shell 在搜索路徑里尋找這些應用程序(搜 索路徑就是一個能找到可執行程序的目錄列表)。如果鍵入的命令不是一個內部命令并且在路徑里沒有找到這個可執行文件,將會顯示一條錯誤信息。如果能夠成 功找到命令,該內部命令或應用程序將被分解為系統調用并傳給 Linux 內核。
6.3 Hello的fork進程創建過程
父進程通過調用fork函數創建一個新的運行的子進程。調用fork函數后,新創建的子進程幾乎但不完全與父進程相同。子進程得到與父進程用戶級虛擬地址空間相同的(但是獨立的)一份副本,包括代碼、數據段、堆、共享庫以及用戶棧。子進程還獲得與父進程任何打開文件描述符相同的副本,這意味著當父進程調用fork時,子進程可以讀寫父進程中打開的任何文件。父進程和新創建的子進程之間最大的區別在于他們有不同的PID。fork被調用一次,卻返回兩次,子進程返回0,父進程返回子進程的PID。子進程有不同于父進程的PID。
6.4 Hello的execve過程
Execve函數在當前的進程的上下文中加載并運行一個新程序。Execve函數加載并運行可執行目標文件filename,且帶參數列表argv和環境變量列表envp。只有當出現錯誤時,例如找不到filename,execve才會返回到調用程序。所以,execve調用一次并從不返回。
子進程調用execve函數,在當前進程的上下文中加載并運行一個新程序即hello程序,execve調用駐留在內存中的被稱為啟動加載器的操作系統代碼來執行hello程序,并映射私有區域,為程序的代碼,數據,bss,棧區域創建新的區域結構。新的棧和堆段被初始化為零,新的代碼和數據段被初始化為可執行文件中的內容。最后加載器設置PC指向_start地址,_start最終調用hello中的main函數。。注意,execve函數再當前進程的上下文中加載并運行一個新程序。它會覆蓋當前進程的地址空間,但是并沒有創建一個新進程。新進程仍然有相同的PID,并繼承了調用exceve函數時已打開的所有文件。
6.5 Hello的進程執行
進程調度的過程:操作系統內核使用一種稱為上下文切換的較高層形式的異常控制流來實現多任務。內核為每一個上下文維持一個上下文,上下文就是內核重新啟動一個被搶占的進程所需的狀態。在進程執行的某些時刻,內核可以決定搶占當前進程,并重新開始一個先前被搶占了的進程。這種決策就叫調度。
在內核調度一個新的進程,上下文切換,實現
進程時間片:一個繼承執行它的控制流的一部分的每一時間段叫做時間片,當時間片的時間用盡后,若當前進程還沒執行完畢,控制會轉移給內核,有內核選擇是否仍執行該進程。
用戶態與核心態轉換:處理器使用一個寄存器提供兩種模式的區分,該寄存器描述了進程當前享有的特權。如果沒有設置模式位,進程就處于用戶模式;設置模式位,進程就處于內核模式。用戶模式的進程不允許執行特權指令,不允許直接應用地址空間中內核區內的代碼和數據;內核模式下該進程可以執行指令集中的任何命令,并且可以訪問系統中的任何內存位置。
6.6 hello的異常與信號處理
亂按包括回車:
亂按的字符會保存在緩沖區內,如果在sleep這期間我們輸入了字符和回車回車,則當我們的hello進程結束后,這些字符會被執行,例如圖中的sd、fa、123等
Ctrl+c:會直接終止當前的進程
Ctrl+z:會停止當前的進程
停止后輸入ps:
輸出了執行進程的PID,TTY,TIME和我們輸入的CMD。
停止后輸入jobs:
顯示了我們當前的所有進程,包括停止的。
停止后輸入pstree:
停止后輸入fg:
將我們的hello進程調度會前臺繼續執行。
停止后執行kill -n 9 21378:
Hello進程被殺死。
6.7本章小結
本章介紹了進程與shell執行的相關知識。進程是計算機中最偉大的概念,有了進程才有了我們現在所身處的計算機世界。不同的進程在內核的調度下執行者自己的指令,而沒有錯誤。在進程中遇到異常時,操作系統有著自己的方式來處理。
第7章 hello的存儲管理
7.1 hello的存儲器地址空間
邏輯地址:在有地址變換功能的計算機中,訪內指令給出的地址 (操作數) 叫邏輯地址,也叫相對地址。
線性地址:線性地址是邏輯地址到物理地址變換之間的中間層。在分段部件中邏輯地址是段中的偏移地址,然后加上基地址就是線性地址。
虛擬地址:CPU啟動保護模式后,程序運行在虛擬地址空間中,也就是程序在磁盤中使用的地址。Hello可執行文件反匯編后,每條指令前的地址就是虛擬地址
物理地址:放在尋址總線上的地址。放在尋址總線上,如果是讀,電路根據這個地址每位的值就將相應地址的物理內存中的數據放到數據總線中傳輸。如果是寫,電路根據這個地址每位的值就在相應地址的物理內存中放入數據總線上的內容。物理內存是以字節(8位)為單位編址的。
7.2 Intel邏輯地址到線性地址的變換-段式管理
邏輯地址由段選擇符和偏移量組成,線性地址為段首地址和邏輯地址中的偏移量組成。其中,段首地址存放在段描述符中。而段描述符存放在段描述符表中。
一般邏輯地址實際是由48位組成,前16位包括[段選擇符],后32位[段內偏移量]。其中段指的是可執行文件中的代碼段,數據段等等。
段選擇符用于尋找段描述符表,段內偏移量是指令地址相對于段基址的偏移量。
段選擇符的16位格式如下:
索引:描述符表的索引
TI:如果 TI 是 0。「描述符表」是「全局描述符表(GDT)」,如果 TI 是 1。「描述符表」是「局部描述表(LDT)」
RPL:段的級別。為 0,位于最高級別的內核態。為 11,位于最低級別的用戶態。在 linux 中也僅有這兩種級別。
流程大致如下圖:
其中GDT和LDT的首地址,存放在用戶不可見的起存起中。
這樣子我們就拿到了我們要的段基地址,再加上我們的偏移量,得到了線性地址。
7.3 Hello的線性地址到物理地址的變換-頁式管理
從線性地址到物理地址簡單來說就是下面這張圖:
虛擬地址分為兩部分:虛擬頁號(VPN)以及和虛擬頁偏移量(VPO),其中虛擬頁偏移量與物理也偏移量是相同的,也就是說我們只需要找到物理頁號(PPN)就可以得到我們想要的物理地址。
那么物理頁號存放在哪呢?它就存放在PTE頁表中。我們通過VPN去頁表中尋找,如果命中則取出物理頁號,如果不命中,則替換該位置的PTE,再取出我們需要的物理頁號。
當然實際過程沒這么簡單,這里面還涉及了TLB的小緩存,一種關于PTE的緩存,稱為翻譯后備緩沖器。一般會先去TLB中尋找物理頁號,若不命中則去PTE中尋找。這點接下來詳細介紹。
7.4 TLB與四級頁表支持下的VA到PA的變換
我們知道虛擬地址分為VPN和VOP,以intel Core i7為例,Core i7 支持48位虛擬地址和52位物理地址。以下圖來解釋:
48位的虛擬地址被劃分為36位的VPN和12位的VPO。
而對于TLB來說,36位的VPN有可以看成是32位的TLBT(標記)和4位的TLBI(組索引)。若命中則取出其中的PPN。若不命中,則用36位的VPN去頁表中尋找,頁表分為4級頁表,每一頁的頁表對應9位的VPN片。每個片被用作到一個頁表的偏移量。而2,3,4級的頁表基地址由前一級的頁表存儲,1級的頁表基地址由CR3提供。如果在PTE中仍然不命中,則替換該頁。取出PPN后和VPO合并由此得到了物理地址。
7.5 三級Cache支持下的物理內存訪問
在我們得到物理地址后,就需要去訪問物理內存,物理地址被分為3個部分,分別是CT(高速緩存標記),CI(高速緩存組索引),CO(高速緩存塊偏移)。
CT用來判斷我們需要的內存塊是否在緩存中,CI用來定位高速緩存中的組標號,CO是緩存塊中的偏移。
仍以該圖為例,64組,所以CI=6,每一塊64字節,所以CO=6,那么CT=52-6-6=40。如果在L1cache中命中,則取出結果,如果未命中則去L2,L3,和主存中尋找。
7.6 hello進程fork時的內存映射
Mm_struct(內存描述符):描述了一個進程的整個虛擬內存空間。
Vm_area_struct(區域結構描述符):描述了進程的虛擬內存的一個空間。
在fork創建虛擬內存的時候,要經歷一下步驟:
7.7 hello進程execve時的內存映射
4.設置程序計數器PC,指向代碼區域的入口
7.8 缺頁故障與缺頁中斷處理
缺頁故障:
缺頁中斷處理:MMU在試圖翻譯某個虛擬地址A時,觸發了一個缺頁。這個異常導致控制轉移到內核的缺頁處理程序,執行以下步驟:
7.9動態存儲分配管理
動態內存分配器為我們提供額外的虛擬內存。
動態內存分配器維護者一個進程的虛擬內存區域,稱為堆。分配器將堆視為一組不同大小的塊的集合來維護。每個塊就是一個連續的虛擬內存片,要么是已分配的,要么是空閑的。
分配器有兩種基本的風格,兩種風格都要求應用顯示地分配塊。它們的不同之處在于由哪個實體來負責釋放已分配的塊。
顯示分配器,要求應用顯示地釋放任何已分配的塊。例如,c標準庫的malloc程序包
隱式分配器,要求分配器檢測一個已分配塊合適不再被程序所使用,那么久釋放這個塊。隱式分配器也叫作垃圾收集器。
A. 找到一個空閑塊,有以下適配方法:
首次適配 (First fit):從頭開始搜索空閑鏈表,選擇第一個合適的空閑塊。此時搜索時間與總塊數是線性關系,且傾向在靠近鏈表起始處留下小空閑塊的“碎片”,增加對較大塊的搜索時間
下一次適配 (Next fit):和首次適配相似,是從鏈表中上一次查詢結束的地方開始,這種適配比首次適應更快,可以避免重復掃描那些無用塊。
最佳適配 (Best fit):查詢鏈表,檢查每一個空閑塊,選擇適合所需請求大小的最小空閑塊,保證碎片最小,提高內存利用率,運行速度通常會慢于首次適配。
B. 分割 (splitting):申請空間比空閑塊小,可以把空閑塊分割成兩部分。
C. 釋放并分配:清除已分配標志,合并相鄰的空閑塊,和下一個空閑塊合并或者雙向合并。
顯式空閑鏈表采用的方式是維護空閑塊鏈表,而不是所有塊。在空閑塊中儲存前/后指針,而不僅僅是大小,此外還需要邊界標記,用于塊合并。幸運的是,只需跟蹤空閑塊,因此可以使用有效載荷區域。
A.維護顯式空閑鏈表方法:
LIFO(last-in-first-out)策略:后進先出法。將新釋放的塊放置在鏈表的開始處。此方法優點是簡單,常數時間,缺點是研究表明碎片比地址順序法更糟糕。
地址順序法(Address-ordered policy):按照地址順序維護鏈表。addr(前一個塊) < addr(當前回收塊) < addr(下一個塊)。此方法優點是研究表明碎片要少于LIFO,缺點是需要搜索。
3. 分離的空閑鏈表:
分離存儲,是一種流行的減少分配時間的方法。一般思路是將所有可能的塊大小分成一些等價類/大小類。
分配器維護著一個空閑鏈表數組,每個大小類一個空閑鏈表,按照大小的升序排列。
維護方法:
A. 簡單分離存儲
每個大小類的空閑鏈表包含大小相等的塊,每個塊的大小就是這個大小類中最大元素的大小。
B. 分離適配
每個空閑鏈表是和一個大小類相關聯的,并且被組織成某種類型的顯示或隱式鏈表,每個鏈表包含潛在的大小不同的塊,這些塊的大小是大小類的成員。
(以下格式自行編排,編輯時刪除)
4. 塊按大小排序
在每個空閑塊中使用一個帶指針的平衡樹,并使用長度作為權值。
7.10本章小結
本章首先介紹了4種存儲器的地址模式,然后是地址變換的方法,例如:邏輯地址到線性地址的變換,再從線性地址到物理地址的變換,這對應了如何將虛擬內存的數據映射到了物理內存中。其中線性地址到物理地址的變換設計了TLB緩存和4級頁表,這兩者的存在是為了提高轉換的效率以及降低內存空間的使用。得到物理地址后,我們就需要根據物理地址去內存中尋找我們需要的內容。為了提高訪問速率,三級Cache由此出現。接下來介紹了fork和execve的內存映射以及遇到缺頁時,內核該怎么處理。最后就是內存的動態管理,這其中涉及了顯示分配器和隱式分配器兩種方式。
第8章 hello的IO管理
8.1 Linux的IO設備管理方法
設備的模型化:文件
設備管理:unix io接口
一個Linux文件就是一個m個字節的序列B0,B1,……,Bm-1。所以的I/O設備(例如網絡、磁盤和終端)都被模型化為文件,而所有的輸入和輸出都被當作對相應文件的讀和寫來執行。這個設備映射為文件的方式,允許Linux內核引出一個簡單、低級的應用接口,稱為Unix I/O,這使得輸入和輸出都能以一種統一且一致的方式的來執行。
8.2 簡述Unix IO接口及其函數
打開文件:進程是通過調用open函數來打開一個已存在或者創建一個新文件的
int open(char *filename, int flags, mode_t mode);
open函數將filename轉換為一個文件描述符,并且返回描述符數字,返回的描述符總是在進程中當前沒有打開的最小描述符,flags參數指明了進程打算如何訪問這個文件,flags參數也可以是一個或者更多位掩碼的或,給寫提供一些額外的指示。mode參數指定了新文件的訪問權限位。
關閉文件:進程通過調用close函數關閉一個打開的文件。
int close(int fd);
關閉一個已關閉的描述符會出錯。
讀和寫文件:應用程序是通過分別調用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的當前文件位置。
8.3 printf的實現分析
(以下格式自行編排,編輯時刪除)
我們知道printf函數的參數是不確定的,所以我們就需要確定具體的參數個數。
va_list的定義: ,說明它是一個字符指針。
(char *)(&fmt)+4表示的是第二個參數,也就是……中的第一個參數。因為&fmt表示的是fmt的地址,是char *類型的指針,其大小為4,所以我們加上4后表示的就是第二個參數的首地址。
vsprintf的作用是格式化。它接受確定輸出格式的格式字符串fmt。用格式字符串對個數變化的參數進行格式化,產生格式化輸出。然后sys_call顯示格式化了的字符串。
內核會通過字符顯示子程序,根據傳入的ASCII碼到字模庫讀取字符對應的點陣,然后通過vram(顯存)對字符串進行輸出。顯示芯片將按照刷新頻率逐行讀取vram,并通過信號線向液晶顯示器傳輸每一個點(RGB分量),最終實現printf中字符串在屏幕上的輸出。
8.4 getchar的實現分析
(以下格式自行編排,編輯時刪除)
異步異常-鍵盤中斷的處理:鍵盤中斷處理子程序。接受按鍵掃描碼轉成ascii碼,保存到系統的鍵盤緩沖區。
getchar等調用read系統函數,通過系統調用讀取按鍵ascii碼,直到接受到回車鍵才返回。
8.5本章小結
本章介紹了Unix I/O接口以及I/O函數,分析了printf和getchar的實現。
(第8章1分)
結論
Hello的一生:
1.程序員通過I/O設備,往計算機里敲入代碼,編寫出hello.c文件
2.hello.c在預處理器下經過預處理形成hello.i文件
3.hello.i在編譯器下經過編譯形成hello.s文件
4.hello.s在匯編器下經過匯編形成hello.o文件
5.hello.o在鏈接器下與其他.o文件鏈接形成hello可執行文件
6.shell為hello文件分配空間形成進程,到前臺執行
7.shell通過fork()函數創建一個子進程,在子進程中通過execve函數加載hello城西,建立hello可執行文件到虛擬內存的映射。
8.在執行hello時,發生了缺頁中斷,觸發了缺頁中斷處理程序。
9.內核將hello文件的虛擬內存映射為物理內存,將虛擬地址翻譯為物理地址。再根據這個物理地址去Cache/主存內讀取數據、指令。
10.printf執行,將格式化結果顯示到了shell中
11.hello進程終止,向父程序shell發送SIGCHLD信號
12.將hello進程進行回收
感悟:一個文件的執行,不像我們看起來的那么簡單,他需要涉及許多復雜且偉大的過程,許多創新性地概念。例如進程的提出
用計算機系統的語言,逐條總結hello所經歷的過程。
你對計算機系統的設計與實現的深切感悟,你的創新理念,如新的設計與實現方法。
(結論0分,缺失 -1分,根據內容酌情加分)
附件
Hello 可執行文件
Hello.i 預處理后文件
Hello.s 編譯后文件
Hello.o 匯編后文件
Hello_.s 反匯編文件
參考文獻
為完成本次大作業你翻閱的書籍與網站等
[1] 林來興. 空間控制技術[M]. 北京:中國宇航出版社,1992:25-42.
[2] 辛希孟. 信息技術與信息服務國際研討會論文集:A集[C]. 北京:中國科學出版社,1999.
[3] 趙耀東. 新時代的工業工程師[M/OL]. 臺北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 諶穎. 空間交會控制理論與方法研究[D]. 哈爾濱:哈爾濱工業大學,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.
(參考文獻0分,缺失 -1分)
總結
以上是生活随笔為你收集整理的CSAPP大作业论文 程序人生的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Redis实现实时排行榜
- 下一篇: 分布式系统工程实践