摘 要html
摘要是論文內容的高度歸納,應具備獨立性和自含性,即不閱讀論文的全文,就能得到必要的信息。摘要應包括本論文的目的、主要內容、方法、成果及其理論與實際意義。摘要中不宜使用公式、結構式、圖表和非公知公用的符號與術語,不標註引用文獻編號,同時避免將摘要寫成目錄式的內容介紹。linux
計算機系統是高度集成的一個至關複雜的系統,這個系統的實現有多重機制。git
本文經過結束計算機中一個簡單的hello程序從預處理一直到IO管理的整個過程當中的實現細節,粗略介紹了計算機系統的機制,對其中一些關鍵的實現細節進行了相對詳細的探究。基於hello的實現過程,本文梳理了一個計算機系統的總體運行流程,可供參考。程序員
關鍵詞:CSAPP P2P 編譯 彙編 連接 進程 IO 代碼github
(摘要0分,缺失-1分,根據內容精彩稱都酌情加分0-1分)shell
目 錄數據庫
第1章 概述 - 4 -express
3.3.1 數據的處理(常量,變量,表達式等) - 10 -
6.2 簡述殼Shell-bash的做用與處理流程 - 44 -
7.2 Intel邏輯地址到線性地址的變換-段式管理 - 52 -
7.3 Hello的線性地址到物理地址的變換-頁式管理 - 53 -
7.4 TLB與四級頁表支持下的VA到PA的變換 - 53 -
7.7 hello進程execve時的內存映射 - 56 -
首先經過鍵盤向計算機輸入一串代碼,這串代碼組合成了一個hello.c源文件。
接下來將源文件經過gcc編譯器預處理,編譯,彙編,連接,最終完成一個能夠加載到內存執行的可執行目標文件。
接下來經過shell輸入文件名,shell經過fork建立一個新的進程,而後在子進程裏經過execve函數將hello程序加載到內存。虛擬內存機制經過mmap爲hello規劃了一片空間,調度器爲hello規劃進程執行的時間片,使其可以與其餘進程合理利用cpu與內存的資源。
而後,cpu一條一條的從hello的.text取指令執行,不斷從.data段去除數據。異常處理程序監視着鍵盤的輸入。hello裏面的一條syscall系統調用語句使進程觸發陷阱,內核接手了進程,而後執行write函數,將一串字符傳遞給屏幕io的映射文件。
文件對傳入數據進行分析,讀取vram,而後在屏幕上將字符顯示出來。
最後程序運行結束,shell將進程回收,完成了hello程序執行的全過程/
列出你爲編寫本論文,折騰Hello的整個過程當中,使用的軟硬件環境,以及開發與調試工具。
軟件:
Visio Studio 2017
VMware
Ubuntu
edb
gdb
gcc
列出你爲編寫本論文,生成的中間結果文件的名字,文件的做用等。
Hello.c |
Hello的c源代碼 |
Hello.i |
源代碼預編譯產生的ascii文件 |
Hello.s |
Ascii文件編譯後產生的彙編代碼文件 |
Hello.o |
彙編後產生的可重定位目標文件 |
Hello_o-objdump-d.txt |
可重定位目標文件的對應彙編代碼 |
Hello_o-objdump-d-r.txt |
可重定位目標文件的代碼及重定位條目 |
Hello_o-readelf-a.txt |
可重定位目標文件的elf條目 |
Hello-ld |
連接生成的可執行目標文件 |
Hello-ld-readelf-a.txt |
可執行目標文件對應的elf條目 |
Hello-objdump-d-r.txt |
可執行目標文件對應的彙編代碼 |
Hello-linkinfo.txt |
可執行目標文件的連接信息 |
在本章節中,大體描述了一個hello程序從出生到去世的完整過程,以及我在描述整個過程時所使用的軟件環境,和產生的中間文件。
(第1章0.5分)
預處理又稱預編譯,是指在對C源代碼文件進行詞法掃描和語法分析以前所作的工做。
預處理所作的主要工做,就是在對一個C源文件進行編譯操做以前,對其進行一些預先的處理,包括刪除註釋,處理宏定義(#define),添加包含的頭文件(#include),執行條件編譯(#ifdef),一方面可以是生成的與處理文件可以便於編譯器的直接處理,也使得編寫的程序可以便於閱讀,修改,移植和調試,有利於模塊化程序設計。
預處理命令:gcc -E hello.c -o hello.i
2-1預處理的效果
右邊爲預處理前的hello.c源代碼,左邊爲預處理後的hello.i文件,能夠看到,源代碼中的註釋都被刪除,而#include命令所包含的頭文件都被替代爲了相應的代碼,這樣產生的hello.i文件具備可以獨立運行的一套源代碼,而不是實現功能的代碼片斷了。
2-2預處理先後文件的比較
若是說生成一個完整的可執行文件就像是造一輛跑車,那麼c源代碼就是跑車的圖紙,而c文件的預處理就是按照圖紙粗製一批合適的鋼材。
經過對C文件的預處理,咱們將c文件改編成了統一的格式,經過宏的處理對c文件進行了適當的修正,立刻進入下一步的處理。
(第2章0.5分)
限於C語言,編譯就是將用C的語言寫成的源代碼文件,等價的翻譯成彙編語言文件的過程。
編譯可以將.i文件中的c代碼,不改變其所實現的功能的過程和結果,同時又有必定的優化和修改,翻譯成可以同等的完成其任務的一段彙編語言代碼,
編譯以前,C語言編譯器會進行詞法分析、語法分析(-fsyntax-only),接着會把源代碼翻譯成中間語言,即彙編語言。若是想看到這個中間結果,能夠用-S選項。
編譯程序工做時,先分析,後綜合,從而獲得目標程序。所謂分析,是指詞法分析和語法分析;所謂綜合是指代碼優化,存儲分配和代碼生成。爲了完成這些分析綜合任務,編譯程序採用對源程序進行屢次掃描的辦法,每次掃描集中完成一項或幾項任務,也有一項任務分散到幾回掃描去完成的。下面舉一個四遍掃描的例子:第一遍掃描作詞法分析;第二遍掃描作語法分析;第三遍掃描作代碼優化和存儲分配;第四遍掃描作代碼生成。
值得一提的是,大多數的編譯程序直接產生機器語言的目標代碼,造成可執行的目標文件,但也有的編譯程序則先產生彙編語言一級的符號代碼文件,而後再調用匯編程序進行翻譯加工處理,最後產生可執行的機器語言目標文件。
gcc -S hello.c -o hello.s
3-1 編譯的效果
3-2 編譯先後文件的比較
c源代碼中出現的數據以下:
3-1 源代碼數據類型解析
當前代碼中的sleepsecs整型變量就是一個全局變量。全局變量的特色是在C程序的任意函數中都可以直接讀寫,所以全局變量採用獨立於函數以外的存儲位置,彙編代碼中,全局變量會被存放在函數體外的data段,在運行中經過GOT表進行引用。
3-2彙編代碼中的全局變量
彙編代碼中同時也對sleepsecs的數據類型進行描述,由於在彙編中是沒有整型,浮點型這些概念的,有的只是一串連續的數據。
在彙編程序中,常量通常存放在專門的區域,須要的時候直接調用。爲了連接的方便,通常會採起全局偏移量表(GOT)的形式來調用全局變量。
當前的C源代碼中的常量主要是兩個用於在printf中輸出的字符串,這兩個字符串直接存放在彙編程序中的只讀數據域
3-3 彙編代碼中的只讀數據
當前c代碼中的用於計數的整型變量i就是局部變量,與main函數的參數argc和argv同樣,這些數據都是隻會在當前的局部函數中進行讀寫的,外部函數沒有可以正常訪問到這些數據的方法。所以不須要像全局變量那樣在代碼段外獨立的爲這些變量分配空間。
這類數據通常是在程序運行的棧中保存,寄存器中進行傳遞。同時在棧於寄存器中均可以對其進行修改。
所以,彙編代碼爲這些數據專門開闢了存儲的棧空間。
3-4 開闢棧空間的彙編指令
分別將本來存儲於寄存器中的argc於argv變量壓入棧中進行管理
3-5將變量壓入棧中
變量i也在棧中進行讀寫管理:
初始化i
3-6初始化i
對i進行累加:
3-7對變量累加
程序最後的棧空間的示意圖以下:
3-8棧空間示意圖
c源代碼中的賦值語句僅有一處:
3-9賦值語句
因爲此處的局部變量i存放於棧中,彙編語言直接對棧的值進行修改:
3-10彙編代碼中的賦值語句
c源代碼中的算術操做僅在for循環語句中有一處:
3-11算術操做
對應的翻譯到彙編代碼中的形式以下:
3-12彙編代碼中的算術操做
c源代碼中的關係操做共有兩處:
3-13關係操做
分別是argc參數與3進行比較,判斷兩者是否相等,以及局部變量i與10的比較,判斷i的值是否小於10。
c語言中的關係操做表達式的值是根據關係的真僞來肯定的,真爲1,假爲0。
而在c語言中,關係判斷的結果一般用於改變控制流,如做爲if語句的判斷條件,以及for,while等循環語句中的循環條件。
所以在彙編語言當中,可以直接翻譯成相應的條件跳轉命令,來決定控制流的方向。
argc變量的關係操做所對應的彙編語言以下
3-14彙編代碼中的關係操做
在這裏,cmpl命令會怕判斷當即數3與參數argc的關係,而後根據結果設置條件寄存器,然後面的je指令經過條件寄存器的值的組合來決定是否跳轉。
在這裏,若是知足條件argc!=3,就會直接執行下面緊跟着的語句,不然就會跳過這一段語句,直接開始執行L2處的語句。
同理,局部變量i的關係操做對應的彙編代碼以下:
3-15彙編代碼中的關係操做
只要i的值仍然小於10,就會不斷地執行下面的跳轉指令,從程序員的角度來看,控制流一直在for循環體內部不斷地執行。
值得一提的是,c源代碼中的語句是i<10 而這裏的語句的等效C語句確是i<=9
因爲編譯器會對C代碼進行優化,毫無疑問這裏的c代碼也是被優化了的狀態,大概在編譯器的眼中,判斷<=的關係要比判斷<的關係的效率更高吧。
這也提醒了咱們,編譯器產生的彙編代碼不必定是c源代碼的簡單轉換,而是會進行不一樣程度的優化,只是最後產生的運行結果沒有改變罷了。
指針是c語言編譯的一個很是複雜而巧妙的部分。
當前程序中設計到數組/指針操做的代碼如圖所示:
3-16指針操做
經過對傳入的字符串數組argv進行尋址來讀取參數。
argv是從命令行鍵入的字符串的地址數組,裏面按順序存放着命令行輸入的字符串在內存中的存放地址。
因爲數組是在內存中一段連續的內存空間中進行存儲的,因此彙編語言經過索引值與數組基址來對數組內容進行尋址。
argv[1]表明的就是數組中第2個參數的地址,程序員數數都是從0開始的,這難道不是常識嗎?
對應的彙編代碼以下:
3-17彙編代碼中的數組索引處理
經過這樣的轉換來對數組按照索引進行尋址。
在c語言中,產生控制流轉移的狀況有兩種,分別是分支和循環,在當前函數的代碼中對應了if分支判斷語句和for循環語句。
3-18控制轉移語句
在編譯器將c語言中的控制轉移語句翻譯成爲彙編語言時,會使用匯編中的條件判斷與跳轉指令來約束控制流,使控制流按照c語言所描述的行爲來流動。
其中if語句的基本結構:
if(expr)
expression;
若是expr分支判斷表達式爲真,則執行下面的分支體,不然跳過。
翻譯成彙編語言以下:
3-19彙編代碼中的分支語句
for語句的基本結構:
for(init-expr; test-expr; update-expr)
body-statement;
按照這個結果等效產生的用goto語句描述的c語句以下:
init-expr;
goto test;
loop:
body-statement
update-expr;
test:
t=test-expr;
if(!t)
goto loop;
對應的彙編代碼:
3-20彙編代碼中的for循環語句
c語言中的函數調用對應了彙編語言中的call指令,彙編語言與操做系統提供了一整套機制來保證函數多級調用的層進與參數的層層傳遞可以穩定進行。
在程序運行時,系統會爲其提供一個上下文,經過進程機制與虛擬內存機制的配合,在程序看來,本身就好像獨自佔有內存,且獨自佔有cpu資源,有本身獨立的控制流。這個前提保證了咱們能夠忽略系統背後複雜的機制來分析程序自己的運行過程。
基於以上的前提,首先看一下一個程序運行過程當中的內存結構:
3-21程序的運行時內存結構
在程序運行的過程當中,隨着函數層層調用,棧不斷往下生長。每一個函數都會有一個運行時棧,棧中存放着當前函數運行時所須要的信息,包括局部變量,保存的寄存器。
能夠這樣想:一個程序的棧能夠看做這個程序私有的小內存空間。
棧的先進先出的結構特色與函數的多層調用機制完美契合。
在這樣的機制之下,一個函數調用另外一個函數,就在調用函數的棧下面新開闢一個棧空間,而當被調用函數運行結束以後,釋放棧空間,就又回到了原來的調用函數的棧空間。
當一個函數被調用的時候,須要記錄下返回的地址,這樣當這個被調用函數運行結束以後,才能跟順址尋路,順利的回到原來的地方繼續未竟的事業。
所以當一個函數將要調用下一個函數時,就會將下一個函數調用完成後應該回到的地址放在棧頂,也就是下一個函數棧底一牆之隔的位置。
3-22程序的棧空間
這樣當下一個函數執行完成以後,只要順着本身的棧,就可以找到回家的路。
在棧調用機制的支持下,函數之間的傳遞參數也變得格外方便。
一個函數想在調用函數的時候傳遞參數,只須要簡單的將參數放在本身的棧當中,下一個函數在運行的時候就能夠經過先前函數的棧來讀取參數。編譯器以及保證了每一個程序都可以正確的找到參數在本身父程序棧中的位置。
更進一步,當參數小於6個的時候,甚至能夠不須要經過棧來傳遞參數,函數能夠將本身的參數壓入寄存器中,而後由下一個函數到寄存器中去取參數便可。
做爲限制,編譯器默認的設置了6個專門用來傳遞參數的寄存器,分別是rdi,rdi,rdx,rcx,r8,r9.當完成了參數傳遞的任務以後,這幾個寄存器有能夠看成普通的寄存器來使用。
就這樣,經過系統,硬件,編譯器,編程語言的相互配合,實現了一套方便的函數調用機制。
在當前程序中,執行函數操做的語句有這些:
3-23函數調用
調用了printf函數與exit函數
3-24函數調用
調用了printf函數與sleep函數
3-25函數調用
調用了getchar函數
在彙編語言中,簡單的改用call指令就可以執行對函數的調用:
3-26彙編代碼中的函數調用
第一條printf函數調用只傳遞了一個參數,彙編代碼將這個參數傳入參數寄存器rdi中,下一個執行的程序就可以直接從rdi寄存器中取值
3-27彙編代碼中的函數調用
同理,上一條語句先將當即數1傳入參數寄存器中,而後使用call指令調用exit函數。當控制流傳遞到exit當中時,就會從rdi寄存器中取出1這個數,而後看成退出的狀態值。
3-28彙編代碼中的函數調用
同理,這幾條彙編語句總共將3個參數傳入了參數寄存器當中,而後調用printf函數。
3-29彙編代碼中的函數調用
用call指令調用getchar函數。
值得注意的是,在當前函數的彙編代碼中都跟着一個@PLT符號,這個符號的意思是過程連接表,用於動態庫的連接。關於這一部分,將會在後面的連接中繼續討論。
前面我提到預處理後的文件至關於打造出的用於製造跑車的鋼材,那麼編譯這一步就是將鋼材細細打磨,變成尺寸嚴絲合縫的零件。
經過編譯,函數的c代碼變爲了等效的彙編代碼,編譯器分別從c語言的數據,賦值語句,類型轉換,算術操做,邏輯/位操做,關係操做,指針操做,控制轉移與函數操做這幾個關鍵點佈局,從微觀細節上剖析,宏觀上調配,既符合了c的語義和用意,有很好的契合了計算機的底層機制。編譯器簡直就是藝術。
(第3章2分)
彙編會將編譯產生的ascii碼構成的彙編代碼翻譯成相對應的機器代碼,即目標代碼,也就是從人可以讀懂的字符翻譯成爲cpu可以讀懂的二進制程序碼的過程。
當編譯器將c源代碼一路翻譯成彙編代碼以後,仍然不是及其能夠讀懂的格式。cpu在運行程序時經過機器碼來判斷所要執行的指令,所以還須要將ascii格式的彙編代碼轉化爲機器碼。
但須要注意的是,彙編仍然是一箇中間過程。咱們所編寫的程序包含着在外部的庫中定義的函數,同時也缺乏從系統進入程序的中間函數。
更進一步,當代碼越寫越大以後,可能會出現更多的定義和引用分離的狀況,例如一個函數在一個.c源文件中定義,而被另外一個.c文件中的函數引用。在這種狀況下,預處理到編譯,不過是將單個的.c文件進行了翻譯。
要想程序完整可用,還須要一個將多個文件合併成一個完整的可執行文件的過程,這個過程就是連接,而彙編就是在文件中根據彙編代碼生成一些可以指引連接過程進行的數據結構。
形象的說,咱們已經造好了一臺汽車的全部零件,在將零件組裝起來以前,咱們如今要作的就是打造螺絲釘。
gcc -c hello.s -o hello.o
4-1彙編的效果
分析hello.o的ELF格式,用readelf等列出其各節的基本信息,特別是重定位項目分析。
4-2 elf格式
4-3 elf文件內容圖示
一個典型的elf文件的格式如上圖所示,根據這個模型,能夠簡單的分析hello.o文件的基本組成。
首先是ELF頭,這個節存儲了整個.o文件的一些基本定信息,具體以下圖。
4-4 elf文件格式解析
接下來看看節頭部表,這張表中存儲了elf表中每個節的具體信息,包括類型,名稱,偏移值等。以此爲索引,可以對elf文件中每個具體的節進行訪問。
4-5 elf文件的節頭部表
.text節包含着已編譯程序的機器代碼,具體結構以下:
4-6 elf文件.text段
.rodata節含有例如printf語句中字符串這樣的只讀數據
.data存放已初始化的全局和靜態C變量
.bss存放未初始化的全局和靜態C變量,以及全部被初始化爲0的全局或靜態變量。
.symtab節存放着程序中的全部符號,包括被引用的以及被定義的。連接器能夠經過這張表來獲取當前可重定位目標文件中的符號信息,並以此來對文件進行連接。
其具體結構以下:
4-7 e文件.symtab節
.rel.text是代碼段的重定位條目,每當彙編器發現程序中有未定義的引用或者在當前程序中定義而可能被外部程序所引用的符號(非靜態的全局符號),就會爲其生成一條重定位條目。
4-8 elf文件的重定位條目
在重定位節當中的每個條目的每一條信息都會在連接的過程當中用於重定位符號,修改引用,將多個可重定位目標文件鏈接成一個完整的可執行文件。經過可重定位條目中信息的指引,可使連接器準確無誤的對多個可重定位目標文件進行合併和修改,具體細節將在連接過程當中具體探討。
經過objdump指令能夠看到hello.o文件的.text段的具體狀況,此時的.text段只是一串由1和0構成的機器碼,將其對應的轉化爲彙編指令,會發現一些不一樣之處
4-9 可重定位目標文件與彙編代碼的區別
共有如下幾點不一樣:
對比兩端代碼中的相同跳轉語句:
4-10 11跳轉語句對比
能夠看到,在.o文件中,跳轉的位置已經由符號指代變成了具體的數值。
因爲不一樣文件代碼連接合並和,一個文件自己的代碼的相對地址不會改變,因此不須要與外部重定位,而能夠直接計算出具體的數值,所以這裏就已經完成了全部的操做,這條語句將以這種形式加載到內存中被cpu讀取與執行。
4-11 重定位條目對比
能夠看見,彙編代碼文件中的call對函數調用的語句都是直接以函數名來指代,而在.o文件中取而代之的是一條重定位條目指引的信息。
因爲調用的這些函數都是未在當前文件中定義的,因此必定要與外部連接纔可以執行。
在連接時,連接器將依靠這些重定位條目對相應的值進行修改,以保證每一條語句都可以跳轉到正確的運行時位置。
4-12 全局變量引用的重定位條目對比
因爲全局變量在運行時的內存位置是未知的,因此一樣須要生成一條重定位條目,提醒連接器在連接時謹慎的計算運行時的內存地址,而後分配給每一條引用,保證每一條引用最終都可以指向正確的位置
在.o文件當中,當即數都變爲16進制。由於計算機是基於二進制運行的,十六進制能夠很方便的與二進制相互轉化,所以這裏更換成了16進制。
另外一方面,咱們利用objdump看到了翻譯過來的彙編代碼,但真實的.o文件裏保存的其實只有機器碼。
在現有的系統中,每個彙編指令都與1個字節的十六進制碼一一對應。
好比在這條語句中,mov指令對應的機器碼是48,%rsp與%rbp寄存器對應的機器碼分別是89和e5,當cpu讀取到mov指令後,就立刻解析出這是mov指令,並且後面會跟兩個寄存器,所以又會繼續讀取後面的兩個字節,並將其翻譯成對應的寄存器,並進行操做。
每個彙編指令對應的操做數的個數與種類都是肯定的,所以一段彙編的機器代碼只要肯定一個起始位置,最終解析出來的操做序列是沒有二義性的。
要想造出一輛跑車,精密耐用的零件只是一個必要的方面。當零件齊全了以後,如何將零件組裝起來,使得每一個零件之間穩固,所以在組裝以前,還須要將零件打磨一番。
彙編器對編譯器生成的彙編代碼文件更深一層,翻譯成機器代碼文件,也就是可重定位目標文件。因爲每一個文件中只有一部分的函數,且文件直接互相引用,互相依賴。與此同時,對於連接器來講,每一個文件不過是一個字節塊,要想解決這些字節塊內部之間的互聯邏輯,就須要彙編器多作一些,再將彙編代碼翻譯成機器代碼時加入一些可以引導連接器進行連接的數據結構。至此,彙編器的工做就結束了,離成功不過寸步之遙。
(第4章1分)
編譯器與彙編器將C源文件簡單的改寫成等效的彙編代碼文件,可是這個文件仍然不夠完整,不是一個可以加載到內存中直接開始運行的狀態。
由於此時的.o文件,便可重定位目標文件只是一個代碼片斷,包含了不完整的定義,要想將其變爲一個徹底可執行的狀態,還須要進行連接
連接是將各類代碼和數據片斷收集並組合成一個單一文件的過程。
ld -o hello-ld -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
使用ld的連接命令,應截圖,展現彙編過程! 注意不僅鏈接hello.o文件
5-1 連接的過程
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
hello可執行文件的基本結構以下:
5-2 典型的elf可執行目標文件
經過readelf指令讀出hello可執行目標文件的elf格式:
elf頭:
5-3 可執行目標文件的elf頭
節頭部表:
5-4 節頭
重定位節.rela.text:
5-5 重定位節
重定位節.rela.eh_frame:
5-6 符號節
與可重定位目標文件相比,可執行目標文件被設計成很容易加再到內存的格式。
使用edb加載hello,查看本進程的虛擬地址空間各段信息,並與5.3對照分析說明。
根據linux進程運行時加載到內存的格式的示意圖:
5-7 linux進程的虛擬內存映射
經過readelf讀取可執行目標文件的程序頭表
5-8 目標文件的程序頭表
LOAD段起始於0x400000, 表示一個從二進制文件映射到虛擬地址空間的段。其中保存了常量數據(如字符串),程序的目標代碼等等。
能夠看到,內存中從地址0x0400040開始的一段區域是PHDR段,這一段主要用於保存程序頭表
INTERP段起始於0x400200,一樣也是隻讀數據,其主要做用是指定在程序已經從可執行映射到內存以後,必須調用解釋器。在這裏解釋器並不意味着二進制文件的內存必須由另外一個程序解釋。它指的是這樣的一個程序:經過連接其餘庫,來知足未解決的引用。
DYNAMIC段起始於0x600e50,保存了其餘動態連接器(即,INTERP中指定的解釋器)使用的信息。
NOTE保存了專有信息。
程序運行時,就會將相應的信息加載到內存中的對應位置
經過edb動態調試工具查看hello運行時的狀態。
從data dump窗口能夠看到地址0x0400000到地址0x0401000的運行時內容。
5-9 地址0x0400000到地址0x0401000的運行時內容
能夠知道,在程序運行時,這一段區域所存儲的內容就是隻讀數據,即.init,.test,.rodata段,包含程序運行的入口,程序開始運行時配置環境所要調用的系統函數,程序的主體代碼以及執行程序時所要用到的一些只讀數據。
查看0x0600000到0x0601000的內存內容
5-10 0x0600000到0x0601000的內存內容
查看0x0601000到0x0602000的內存內容
5-11 查看0x0601000到0x0602000的內存內容
以前在解析hello.o文件時,提到了在重定位節當中存放的重定位條目。同時前面也提到過,咱們經過預處理,彙編,編譯產生的文件仍然只是最終完整的可執行文件的一部分。完成了hello.o文件的生成,至關於造出了跑車的發動機,發動機確實是跑車最重要的部分,可是隻有發動機,跑車也沒法工做。hello.o文件中的重定位條目就是指導連接器將其組裝起來的說明書。
hello.o文件中的重定位條目以下:
5-12 hello.o文件中的重定位條目
經過objdump指令觀察hello.o文件的.text段:
5-13 hello.o文件的.text段
爲了方便,objdump工具已經自動將重定位條目放在了相應的位置。
hello可執行目標文件中多出了.init段和.plt段,前者用於初始化程序執行環境,後者用於程序執行時的動態連接,這裏再也不贅述。
5-14 兩個文件的區別
進行連接以後兩個文件的區別如上圖,全部的重定位條目都被修改成了肯定的運行時內存地址。
在執行這個連接過程以前,連接器已經經過可重定位目標文件中的符號表信息,肯定的將每一個符號引用都與一處符號定義對應了起來。彙編器生成的重定位條目指明瞭須要被修改的符號引用的位置,以及有關如何計算被引用修改的一些信息。
對於相對地址的引用,即圖中的類型爲R_X86_64_PC32的引用條目。
對於這類條目,首先肯定其定義所在的節以及其相對於節的偏移量,經過這兩個量計算出符號定義的地址,即ADDR(r.symbol)
接下來經過重定位條目指向的內存位置,對引用信息進行修改,使其指向內存運行時的地址,基本計算公式以下:
5-15 基本計算公式
對於R_X86_64_PLT32類型的引用是動態連接的,也就是在靜態連接過程當中只是簡單的構造過程連接表(PLT)和全局偏移量表(GOT),而後在程序加載到內存裏運行的過程當中纔會完成最終的重定位工做。
使用edb執行hello,說明從加載hello到_start,到call main,以及程序終止的全部過程。請列出其調用與跳轉的各個子程序名或程序地址。
使用gdb觀察Hello的執行流程:
_init |
0x7ffff7a05920 |
_dl_vdso_vsym |
0x7ffff7b4b3b0 |
_dl_lookup_symbol_x |
0x7ffff7de00b0 |
do_lookup_x |
0x7ffff7ddf240 |
strcmp |
0x7ffff7df2360 |
do_lookup_x |
0x7ffff7ddf240 |
_dl_vdso_vsym |
0x7ffff7b4b3b0 |
_dl_lookup_symbol_x |
0x7ffff7de00b0 |
_dl_vdso_vsym |
0x7ffff7b4b3b0 |
__strrchr_avx2 |
0x7ffff7b723c0 |
__init_misc |
0x7ffff7b056f0 |
__GI___ctype_init |
0x7ffff7a148f0 |
_dl_init |
0x7ffff7de5630 |
init_cacheinfo |
0x7ffff7a05470 |
handle_intel |
0x7ffff7a9fe80 |
intel_check_word |
0x7ffff7a9fb80 |
_start |
0x400500 |
__libc_start_main |
0x7ffff7a05ab0 |
__new_exitfn |
0x7ffff7a27220 |
__GI___cxa_atexit |
0x7ffff7a27430 |
__libc_csu_init |
0x4005c0 |
__sigsetjmp |
0x7ffff7a22b70 |
Main |
0x400536 |
printf@plt |
0x4004c0 |
_dl_runtime_resolve_xsavec |
0x7ffff7dec750 |
_dl_fixup |
0x7ffff7de4df0 |
malloc |
0x7ffff7a052c6 |
sleep@plt |
0x4004f0 |
getchar@plt |
0x4004d0 |
動態連接是一項有趣的技術。考慮一個簡單的事實,printf,getchar這樣的函數實在使用的太過頻繁,所以若是每一個程序連接時都要將這些代碼連接進去的話,一份可執行目標文件就會有一份printf的代碼,這是對內存的極大浪費。爲了遏制這種浪費,對於這些使用頻繁的代碼,系統會在可重定位目標文件連接時僅僅建立兩個輔助用的數據結構,而直到程序被加載到內存中執行的時候,纔會經過這些輔助的數據結構動態的將printf的代碼重定位給程序執行。便是說,直到程序加載到內存中運行時,它才知曉所要執行的代碼被放在了內存中的哪一個位置。
這種有趣的技術被稱爲延遲綁定,將過程地址的綁定推遲到第一次調用該過程時。而那兩個輔助的數據結構分別是過程連接表(PLT)和全局偏移量表(GOT),前者存放在代碼段,後者存放在數據段。
首先經過readelf分析可執行目標文件,獲得文件的GOTPLT的運行時位置:
使用edb對hello的運行過程進行解析,能夠看到在運行_dl_start與_dl_init以前,GOTPLT表的內容如圖所示:
5-17 GOTPLT表的內容
此時的PLT表還空空如也,由於程序尚未執行動態連接。
PLT時一個數組,PLT[0]跳轉到動態連接器中,PLT[1]調用系統啓動函數來初始化執行環境。直到PLT[2]開始的每一個條目纔是負責具體函數的連接的。
執行完dl start後。發現GOT表中的數據發生了改變。
5-18 GOT表中的數據發生了改變
GOT[1]= 0x00007f3198405170 指向重定位條目
GOT[2]= 0x00007f31981f3750 指向動態連接器
GOT[1]所指向的重定位表以下:
5-19 GOT[1]所指向的重定位表
GOT[2]指向的動態連接器以下所示:
5-20 GOT[2]指向的動態連接器
當程序須要調用一個動態連接庫內定義的函數時(例如printf),call指令並無讓控制流直接跳轉到對應的函數中去,因爲延遲綁定的機制,此時的printf還不知道在哪兒呢。取而代之的是,控制流會跳轉到該函數對應的PLT表中,而後經過PLT表將當前將要調用的函數的序號壓入棧中,下一步,調用動態連接器。
接下來,動態連接器會根據棧中的信息忠實的執行重定位,將真實的printf的運行時地址寫入GOT表,取代了GOT原先用來跳轉到PLT的地址,變爲了真正的函數地址。
因而,上一次控制流找過來時,GOT給它指的路是動態連接器,動態連接器將真正的地址給GOT表。
這一次控制流再找上門來的時候,GOT就能夠放心的將真正的函數執行時地址傳達過去,完成了動態連接的過程。
分析hello程序的動態連接項目,經過edb調試,分析在dl_init先後,這些項目的內容變化。要截圖標識說明。
終於完成了全部的部件,組裝跑車的過程老是激動人心的。但一樣不容懈怠,哪怕有一絲疏忽,有一個零件裝錯了,在跑車高速運轉的時候都會出現難以估計的災難。
連接器在這裏經過可重定位目標文件中的數據結構,解析每一個文件中的符號,仔細比對了符號的定義和引用,最終爲每一個符號的引用都找到了正確的符號定義的位置。重定位的過程須要更加當心謹慎,連接器須要在特定的位置修改值,使得程序在運行時可以指哪打哪而不會誤差。畢竟在cpu中哪怕是一個字節的誤差,失之毫釐,差之千里。
(第5章1分)
進程是計算機科學中最深入、最成功的概念之一。
當hello程序在計算機中開始執行時,操做系統給了它一種假象,彷彿它是當前系統中惟一正在運行的程序同樣,它獨自佔有一塊完整的內存空間,cpu對它指令有求必應,處理器彷彿一直在執行hello這一個程序的指令。
這種狀態就成爲進程。
進程就是一個執行中的程序的實例,系統中每個程序都運行在某個進程的上下文中,系統始終維護着這個上下文,使進程與上下文之間的互動完美無缺。在操做系統的辛苦維持下,纔給予了程序獨自佔用全部計算資源的假象。
進程提供給應用程序的關鍵抽象以下:
shell是一個交互型的應用級程序,它表明用戶運行其餘程序。
shell首先打印一個命令行提示符,等待用戶輸入命令行,而後對命令行進行求值。shell的基本流程是讀取命令行,解析命令行,而後表明用戶運行程序。
shell首先調用parseline函數,經過這個函數解析以空格分隔的命令行參數,並構造最終會傳遞給execve的argv向量。
若第一個參數是內置的shell命令名,立刻就會解釋這個命令。若是不是,shell就會假定這是一個可執行程序,而後在一個新的子進程的上下文中加載並運行這個文件。
若最後一個參數是&,那麼這個程序將會在後臺執行,即shell不會等待其完成。
若沒有,則這是一個將要在前臺執行的程序,shell會顯式地等待這個程序執行完成。
看成業終止時,shell就會開始下一輪迭代。
Hello的執行是經過在終端中輸入./Hello來完成的。
在linux系統下的終端中始終運行着一個Shell來執行用戶輸入的操做,做爲用戶與系統之間的媒介。
當咱們在終端中輸入./Hello時,shell會先判斷髮現這個參數並非Shell內置的命令,因而久把這條命令看成一個可執行程序的名字,它的判斷顯然是對的。
接下了shell會執行fork函數。
fork函數的做用是建立一個與當前進程平行運行的子進程。系統會將父進程的上下文,包括代碼,數據段,堆,共享庫以及用戶棧,甚至於父進程打開的文件的描述符,都建立一份副本。而後利用這個副本執行子進程。從這個角度上來講,子進程的程序內容與父進程是徹底相同的。
在父進程fork後,父進程重拾本身的老本行,繼續運行shell的程序,而子進程將經過execve加載用戶輸入的程序。因爲Hello是前臺運行的,因此shell會顯式的等待hello運行結束。
execve函數加載並運行可執行目標文件,且帶參數列表argv和環境變量envp。只有當出現錯誤時,execve纔會返回到調用程序,不然execve調用一次而從不返回。
execve的參數列表以下圖:
6-1 環境變量列表的組織結構
在execve加載了Hello以後,它會調用系統提供的啓動代碼,啓動代碼設置棧,啓動程序運行初始化代碼。系統會用execve構建的數據結構覆蓋其上下文,替換成Hello的上下文,而後將控制傳遞給新程序的主函數。
execve只是簡單的更換了本身所處進程的上下文,並無改變進程的pid,也沒有改變進程的父子歸屬關係。
對於正在運行的Hello來講,除了本身的父進程是Shell以外,其它的一切都與調度運行沒有區別。
在Hello進程執行的時候,操做系統爲其維持着上下文。Hello進程就是在其上下文中穩定運行的。
上下文是內核從新啓動一個被搶佔的進程所需的狀態,它由一些對象的值組成,這些對象包括通用目的寄存器、浮點寄存器、程序計數器、用戶棧、狀態寄存器、內核棧和各類內核數據結構,好比描述地址空間的頁表,包含有關當前進程信息的進程表,以及包含進程已打開文件的信息的文件表。
Hello進程在內存中執行的過程當中,並非一直佔用着cpu的資源。由於當內核表明用戶執行系統調用時,可能會發生上下文切換,好比說Hello中的sleep語句執行時,或者當Hello進程以及運行足夠久了的時候。每到這時,內核中的調度器就會執行上下文切換,將當前的上下文信息保存到內核中,恢復某個先前被搶佔的進程的上下文,而後將控制傳遞給這個新恢復的進程。
6-2 進程上下文切換的剖析
結合進程上下文信息、進程時間片,闡述進程調度的過程,用戶態與核心態轉換等等。
在Hello運行的過程當中會屢次出現異常,經過linux系統的信號機制來使Hello正常運行。
首先,即便是Hello正常運行的時候,也會出現異常控制流。好比說,全部的系統都有某種週期性定時器中斷的機制,一般爲1毫秒或每10毫秒,當每次發生定時器中斷時,內核就能斷定當前進程已經運行了足夠長的時間,這時就會調度運行另外一個進程,而將當前Hello進程擱置。
事實上的linux系統時至關複雜和繁忙的,即便開了電腦後什麼也不作,系統也在後臺不斷地運行着幾千個進程。經過調度器的調度使這些進程井井有理的使用cpu資源。
咱們須要重點討論的是Hello函數自己的異常。
6-3 hello程序源代碼
Hello程序的main函數如上圖,其中的sleep函數就會像進程自己發送一個STPSIG使其休眠一段時間。在程序中,這個時間是2.5秒。
當請求的時間到了,或者sleep函數被一個信號中斷,進程就會繼續執行,繼續調用printf函數。
當程序正常執行直到結束時,顯示以下:
6-4
若是在程序運行到中途時按下ctrl+z,產生狀況以下:
6-5
因爲鍵盤輸入的ctrl+z給程序傳入了一個SIGSTP信號,這個信號使程序暫時掛起。此時能夠輸入ps命令查看進程。
6-6
能夠看到,此時hello-ld程序仍然在後臺進程當中而沒有停止。
此時若是繼續輸入fg,就能使hello-ld程序繼續執行。以下圖
6-7
若是在程序運行的時候鍵入ctrl+c,就會給進程發送一個終止信號。以下圖
6-8
能夠看到,此時hello-ld已經不在做業列表當中了。
若是在程序執行時亂按鍵盤,程序仍然會正常執行:
6-9
在程序執行到一半的時候將其中止,輸入pstree,可以看到當前計算機正在執行的全部進程的關係:
6-10
hello執行過程當中會出現哪幾類異常,會產生哪些信號,又怎麼處理的。
程序運行過程當中能夠按鍵盤,如不停亂按,包括回車,Ctrl-Z,Ctrl-C等,Ctrl-z後能夠運行ps jobs pstree fg kill 等命令,請分別給出各命令及運行結截屏,說明異常與信號的處理。
有了跑車還不算完成任務,由於如何駕駛跑車也是一個大問題,就算是老司機也不免翻車,進程管理就是爲了約束程序的運行而存在的。
程序從加載的內存中開始就獨自享有一份上下文,在本身的進程裏自由的運行。可是爲了可以有效的管理進程,系統中有稱爲異常的機制,可以改變控制流,使程序在本身的進程出現問題時不會一籌莫展,而是得到來自外部的幫助。一樣的,不一樣進程之間須要溝通,信號就是爲此而存在的。信號時管理程序運行的一大利器。
(第6章1分)
機器語言指令中出現的內存地址,都是邏輯地址,須要轉換成線性地址,再通過MMU(CPU中的內存管理單元)轉換成物理地址纔可以被訪問到。
邏輯地址:包含在機器語言中用來指定一個操做數或一條指令的地址。每個邏輯地址都由一個段(segment)和偏移量(offset)組成,偏移量指明瞭從段開始的地方到實際地址之間的距離。
通俗的說:邏輯地址是給程序員設定的,底層代碼是分段式的,代碼段、數據段、每一個段最開始的位置爲段基址,放在如CS、DS這樣的段寄存器中,再加上偏移,這樣構成一個完整的地址。
Linux中邏輯地址等於線性地址。爲何這麼說呢?由於Linux全部的段(用戶代碼段、用戶數據段、內核代碼段、內核數據段)的線性地址都是從 0x00000000 開始,長度4G,這樣 線性地址=邏輯地址+ 0x00000000,也就是說邏輯地址等於線性地址了。
虛擬地址將貯存當作是一個存儲在磁盤上的地址空間的高速緩存,再主存中只保存活動區域,並根據須要再磁盤和主存之間來回傳送數據,經過這種方式,它高效的使用了主存。同時,它爲每一個進程提供了一致的地址空間,從而簡化了內存管理。最後,它保護了每一個進程的地址空間不被其餘進程破壞。
而物理地址則是對應於主存的真實地址,是可以用來直接在主存上進行尋址的地址。因爲在系統運行時,主存被不一樣的進程不斷使用,分區狀況很複雜,因此若是要用物理地址直接訪問的話,地址的處理會至關麻煩。
在多段模式下,每一個程序都有本身的局部段描述符表,而每一個段都有獨立的地址空間
在80386 的段機制中,邏輯地址由兩部分組成,即段部分(選擇符)及偏移部分。
段是造成邏輯地址到線性地址轉換的基礎。若是咱們把段當作一個對象的話,那麼對它的描述以下。
(1)段的基地址(Base Address):在線性地址空間中段的起始地址。
(2)段的界限(Limit):表示在邏輯地址中,段內可使用的最大偏移量。
(3)段的屬性(Attribute): 表示段的特性。例如,該段是否可被讀出或寫入,或者該段是否做爲一個程序來執行,以及段的特權級等。
7-1
分頁管理是地址翻譯的一個基本思路。
概念上而言,虛擬內存被組織爲一個由存放在磁盤上的N個連續的字節大小的單元組成的數組。每字節都有一個惟一的虛擬地址做爲到數組的索引。磁盤上數組的內容被緩存在主存中。和存儲器層次結構中其餘緩存同樣,磁盤(較低層)上的數據被分割成塊,這些塊做爲自盤和主存(較高層)之間的傳輸單元。VM系統經過將虛擬內存分割爲成爲虛擬頁的大小固定的塊來處理這個問題,對這些虛擬頁的管理與調度就是頁式管理。
同任何緩存同樣,虛擬內存系統必須有某種方法來斷定一個虛擬頁是否緩存在DRAM中的某個地方。若是是,系統還必須肯定這個虛擬頁存放在哪一個物理頁中。若是不命中,系統必須判斷這個虛擬頁存放在磁盤的那個位置,在物理內存中選擇一個犧牲頁,並將虛擬頁從磁盤複製到DRAM中,替換這個犧牲頁。
首先討論單級頁表下的VA到PA的變換。
當一個進程執行一條訪存指令時,它發出的內存地址是虛擬地址,由內存管理單元(Memory Management Unit MMU)將虛擬地址轉化爲物理地址,並訪問主存,取出所要讀取的數據。
頁表的地址映射規則以下:
7-2 使用頁表的地址翻譯
在這個過程當中,cpu硬件將會執行如下步驟:
7-3 頁命中和缺頁的操做圖
不一樣存儲技術的訪問時間差別很大,速度較快的計數每字節的成本要比速度較慢的計數高,並且容量較小。計算的另外一個特色就是局部性,即計算機程序傾向於訪問最近訪問過的某一塊程序。存儲器的這些基本屬性相互補充使得計算機能夠經過採用構建存儲器層次結構來提高運行效率。
三級Cache的核心思想就是每次訪問數據的時候都將一個數據塊存放到更高一層的存儲器中,根據計算的局部性,程序在後面的運行之中有很大的機率再次訪問這些數據,高速緩存器就可以提升讀取數據的速度。
7-4
當fork函數被當前進程調用的時候,內核會爲新進程建立各類數據結構,並分配給它一個惟一的PID。爲了給這個新進場建立虛擬內存,它建立了當前進程的mm_struct、區域結構和頁表的原樣副本,它將兩個進程中的每一個頁面都標記爲只讀,並將兩個進程中的每一個區域結構都標記位私有的寫時複製。
7-5 一個私有的寫時複製對象
虛擬內存的機制使得fork函數能夠快速的運行,由於當咱們fork了一個新進程的時候,系統事實上並無將原進程的整個上下文複製一遍,它僅僅只是建立了份如出一轍的描述地址空間的數據結構,而後將這個數據結構給予子進程。當子進程執行只讀代碼時,它與父進程實際上共用了物理內存中的同一片區域的內容。
當fork在新進程中返回時,新進場如今的虛擬內存恰好和調用fork時存在的虛擬內存相同。當這兩個進程中的任一個後來進行寫操做時,寫時複製機制就會建立信也米娜。所以,經過虛擬內存這種巧妙的機制爲每一個進程都保持了私有地址空間的抽象概念。
同理,在虛擬內存的機制下,execve也能夠簡單快速的實現。
經過execve函數在當前進程中加載並運行包含之可執行目標文件中的程序,用a.out程序有效地替代了當前程序。這個過程有如下幾個步驟:
刪除當前進程虛擬地址的用戶部分中已存在的區域結構。
爲新程序的代碼、數據、bss和棧區域建立新的區域結構。全部這些新的區域都是私有的,寫時複製的。代碼和數據區域被映射爲a.out文件中的.test和.data區。bss區域是請求二進制0的,映射到匿名文件,其大小包含在a.out當中。棧和堆區域也是請求二進制零的,初始長度爲零。下圖歸納了私有區域的不一樣映射。
7-6 加載器是如何映射用戶地址空間區域
若是a.out程序與共享對象(或目標)連接,好比標準C庫libc.so,那麼這些對象都是動態連接到這個程序的,而後再映射到用戶虛擬地址空間中的共享區域內。
execve作的最後一件事情就是設置當前進程上下文中的程序計數器,食指指向代碼區域的入口點。
同任何緩存同樣,虛擬內存系統必須有某種方法來斷定一個虛擬頁是否緩存在DRAM中的某個地方。若是是,系統還必須肯定這個虛擬頁存放在哪一個物理頁中。若是不命中,系統必須判斷這個虛擬頁存放在磁盤的那個位置,在物理內存中選擇一個犧牲頁,並將虛擬頁從磁盤複製到DRAM中,替換這個犧牲頁。
當CPU想要讀取虛擬內存中的某個數據,而這一片數據剛好存放在主存當中時,就稱爲頁命中。相對的,若是DRAM緩存不命中,則稱之爲缺頁。若是CPU嘗試讀取一片內存而這片內存並無緩存在主存當中時,就會觸發一個缺頁異常,這個異常的類型是故障。此時控制流轉到內核中,由內核來嘗試解決這個問題。
7-7 觸發缺頁
缺頁異常會調用內核中的缺頁異常處理程序,該程序會選擇一個犧牲頁,而後用磁盤中將要讀取的頁來替代犧牲頁。處理程序解決了這個故障,將控制流轉移會原先觸發缺頁故障的指令,當cpu再次執行這條指令時,對應的頁已經緩存到主存當中了。這就是缺頁故障與缺頁中斷的處理。
7-8 解決缺頁異常
動態內存分配器經過維護一個存放着堆的分配狀況的數據結構來實現動態的內存分配。
mem_init函數將對於堆來講可用的虛擬內存模型化爲一個大的,雙字對齊的字節數組。在mem_heap和mem_brk之間的字節表示已分配的虛擬內存。mem_brk以後的字節表示未分配的虛擬內存。分配器經過調用mem_sbrk函數來請求額外的堆內存。
分配器須要知足下列要求
處理任意請求序列
當即響應請求:分配器必須當即響應請求。所以,不容許分配器爲了提升性能重行排列或者緩衝請求。
只使用堆:爲了使分配器能夠拓展,分配器使用的任何非標量數據結構都要保存到堆裏。
對齊塊:使得其能夠保存任何類型的數據對象。
不修改已經分配的塊。
隱式空閒鏈表分配中,內存塊的基本結構以下:
7-9 使用邊界標記的堆塊的格式
其中頭部和腳部分別存放了當前內存塊的大小與是否已分配的信息。
經過這種結構,隱式動態內存分配器會對堆進行掃描,經過上圖中的頭部和腳部的結構來實現查找。
顯式空間鏈表的一種實現的基本結構以下:
7-10 空閒塊
將一個空閒內存塊的有效載荷利用起來,存放着指向下一個以及上一個空閒塊的指針。
經過這種結構能夠實現將內存塊以不按順序的形式組織成合適的結構,好比說遞增序列。一般會在初始化堆的時候額外開闢一塊對空間,用於存放用來維護鏈表的數據結構。
馬路上不可能只有一輛車,所以車行的前後,車輛的避讓須要一套規則來管理。所以開車光是掌握了車輛的駕駛技術還不行,還須要交通規則。交通規則能夠類比爲計算機中的虛擬內存機制,管理着存儲資源的調度。
爲了更加有效地管理內存而且少出錯,現代系統提供了一種對主存的抽象概念,叫作虛擬內存(VM)。虛擬內存是硬件異常、硬件地址翻譯、主存、磁盤文件和內核軟件的完美交互,它爲每一個進程提供了一個大的,一致的和私有的地址空間。經過一個很清晰的機制,虛擬內存提供了三個重要的能力:
(第7章 2分)
一個Linux文件就是一個m個字節的序列,全部的I/O設備(例如網絡,磁盤和終端)都被模型化爲文件,而全部的輸入和輸出都被看成對相應文件的讀和寫來執行。這種將設備優雅地映射爲文件的方式,容許Linux內核引出一個簡單的、低級的應用皆可,稱爲Unix I/O,這使得全部的輸入和輸出都能以一種統一且一致的方式來執行:
設備的模型化:文件
設備管理:unix io接口
相似的,寫操做就是從內存複製n>0個字節到一個文件,從當前文件位置k開始,而後更新k。
unix io 函數(須要包含頭文件 <sys/types.h><sys/stat.h><fcntl.h>):
open函數將filename轉換爲一個文件描述符,而且返回描述符數字,返回的描述符老是在進程中當前沒有打開的最小描述符。flags參數指明瞭進程打算如何訪問這個文件;
只讀
只寫
可讀可寫
若是文件不存在,就建立它的一個截斷的文件
若是文件已存在,就截斷它
在每次寫操做前,設置文件位置到文件的結尾處
關閉一個打開的文件
從描述符爲fd的當前文件位置複製最多n個字節到內存位置buf。返回值-1表示一個錯誤,而返回值0表示EOF。不然,返回值表示實際傳送的字節數量
從內存位置buf複製最多n個字節到描述符fd的當前文件位置。
談起printf的具體實現,首先看看printf函數的函數體:
8-1 printf函數體
注意到函數體的參數列表裏面有一個」…」,這個符號表達的意思是參數的個數不肯定。那麼printf函數所要作的第一件事,就是確認函數的參數到底有多少。
注意到函數體中有這樣一條定義:
而由函數的棧幀結構:
8-2 函數的棧幀結構
能夠推斷出,arg指針指向了傳遞給printf的第一個參數的地址。
接下來函數調用了vsprintf函數,其函數體以下:
8-3 vsprintf函數體
閱讀函數體能夠知道,這個函數的做用就是格式化。它接受肯定輸出格式的格式字符串fmt,用格式字符串堆個數變化的參數進行格式化,產生格式化輸出。
接下來,printf函數會調用系統io函數:write
write是一個系統函數,其做用就是從內存buf位置複製最多i個字節到一個文件位置。而在linux系統中,系統IO被抽象爲文件,包括屏幕。對於系統來講,咱們的顯示屏也是一個文件,咱們只須要將數據傳送到顯示屏對應的文件,就已經完成了系統端的任務,餘下的工做獨立的由顯示器來進行了。
因而在這裏,write會給寄存器傳遞幾個參數,初始化執行環境,而後執行sys call指令,這條指令的做用是產生陷阱異常。
陷阱是有意的異常,用戶程序執行了系統調用的命令(syscall)以後,就致使了一個到異常處理程序的陷阱,這個處理程序解析參數,並調用適當的內核程序。
8-4 陷阱處理
須要注意,這裏的系統調用試運行在內核模式中的。
接下來,系統已經肯定了所要顯示在屏幕上的符號。根據每一個符號所對應的ascii碼,系統會從字模庫中提取出每一個符號的vram信息。
顯卡使用的內存分爲兩部分,一部分是顯卡自帶的顯存稱爲VRAM內存,另一部分是系統主存稱爲GTT內存(graphics translation table和後面的GART含義相同,都是指顯卡的頁表,GTT 內存能夠就理解爲須要創建GPU頁表的顯存)。在嵌入式系統或者集成顯卡上,顯卡一般是不自帶顯存的,而是徹底使用系統內存。一般顯卡上的顯存訪存速度數倍於系統內存,於是許多數據若是是放在顯卡自帶顯存上,其速度將明顯高於使用系統內存的狀況(好比紋理,OpenGL中分普通紋理和常駐紋理)。
顯示芯片按照刷新頻率逐行讀取vram,並經過信號線向液晶顯示器傳輸每個點(RGB份量)。
getchar 由宏實現:#define getchar() getc(stdin)。
getchar 有一個int型的返回值.當程序調用getchar時.程序就等着用戶按鍵.用戶輸入的字符被存放在鍵盤緩衝區中.直到用戶按回車爲止(回車字符也放在緩 衝區中).當用戶鍵入回車以後,getchar纔開始從stdin流中每次讀入一個字符.getchar函數的返回值是用戶輸入的第一個字符的ASCII 碼,如出錯返回-1,且將用戶輸入的字符回顯到屏幕.如用戶在按回車以前輸入了不止一個字符,其餘字符會保留在鍵盤緩存區中,等待後續getchar調用 讀取.也就是說,後續的getchar調用不會等待用戶按鍵,而直接讀取緩衝區中的字符,直到緩衝區中的字符讀完爲後,纔等待用戶按鍵.
getchar函數的功能是從鍵盤上輸入一個字符。其通常形式爲: getchar(); 一般把輸入的字符賦予一個字符變量,構成賦值語句。
進入getchar以後,進程會進入阻塞狀態,等待外界的輸入。系統開始檢測鍵盤的輸入。此時若是按下一個鍵,就會產生一個異步中斷,這個中斷會使系統回到當前的getchar進程,而後根據按下的按鍵,轉化成對應的ascii碼,保存到系統的鍵盤緩衝區。
接下來,getchar調用了read函數。read函數會產生一個陷阱,經過系統調用,將鍵盤緩衝區中存儲的剛剛按下的按鍵信息讀到回車符,而後返回整個字符串。
接下來getchar會對這個字符串進行處理,只取其中第一個字符,將其他輸入簡單的丟棄,而後將字符做爲返回值,並結束getchar的短暫一輩子。
IO是複雜的計算機內部與外部溝通的通道。儘管咱們時時刻刻都在使用着IO:經過鍵盤輸入,經過屏幕閱讀。可是系統IO實現的細節一樣也是至關複雜的。
本章介紹了linux系統下的IO的基本知識,討論了IO在linux系統中的形式以及實現的模式。而後對printf和getchar兩個函數的實現進行了深刻的探究。
(第8章1分)
首先經過鍵盤向計算機輸入一串代碼,這串代碼組合成了一個hello.c源文件。
接下來將源文件經過gcc編譯器預處理,編譯,彙編,連接,最終完成一個能夠加載到內存執行的可執行目標文件。
接下來經過shell輸入文件名,shell經過fork建立一個新的進程,而後在子進程裏經過execve函數將hello程序加載到內存。虛擬內存機制經過mmap爲hello規劃了一片空間,調度器爲hello規劃進程執行的時間片,使其可以與其餘進程合理利用cpu與內存的資源。
而後,cpu一條一條的從hello的.text取指令執行,不斷從.data段去除數據。異常處理程序監視着鍵盤的輸入。hello裏面的一條syscall系統調用語句使進程觸發陷阱,內核接手了進程,而後執行write函數,將一串字符傳遞給屏幕io的映射文件。
文件對傳入數據進行分析,讀取vram,而後在屏幕上將字符顯示出來。
最後程序運行結束,shell將進程回收,完成了hello程序執行的全過程
從鍵盤上敲出hello.c的源代碼程序不過幾分鐘,從編譯到運行,從敲下gcc到終端打印出hello信息,可能甚至不須要1秒鐘。
這短短的1秒,聚集了計算機工做者們幾十年的智慧與心血。
高低電平傳遞着信息,這些信息被複雜而嚴謹的機器邏輯捕捉。cpu不知疲倦的取指與執行。對於hello的實現細節,哪怕把這篇論文再擴充一倍仍講不清楚。正由於如此,我意識到本身還有很長的路要走。
(結論0分,缺失 -1分,根據內容酌情加分)
Hello.c |
Hello的c源代碼 |
Hello.i |
源代碼預編譯產生的ascii文件 |
Hello.s |
Ascii文件編譯後產生的彙編代碼文件 |
Hello.o |
彙編後產生的可重定位目標文件 |
Hello_o-objdump-d.txt |
可重定位目標文件的對應彙編代碼 |
Hello_o-objdump-d-r.txt |
可重定位目標文件的代碼及重定位條目 |
Hello_o-readelf-a.txt |
可重定位目標文件的elf條目 |
Hello-ld |
連接生成的可執行目標文件 |
Hello-ld-readelf-a.txt |
可執行目標文件對應的elf條目 |
Hello-objdump-d-r.txt |
可執行目標文件對應的彙編代碼 |
Hello-linkinfo.txt |
可執行目標文件的連接信息 |
列出全部的中間產物的文件名,並予以說明起做用。
(附件0分,缺失 -1分)
爲完成本次大做業你翻閱的書籍與網站等
[1]Eteran, Evan. 「Eteran/Edb-Debugger.」 GitHub, 2018, github.com/eteran/edb-debugger/wiki/Data-View.
[2]flood, rain. 「編譯並鏈接從helloworld.c生成的彙編代碼的方法步驟.」 爲何版本控制如此重要? - CSDN博客, 2017, blog.csdn.net/rainflood/article/details/75635447.
[3]stx, piani. 「Pianistx.」 數字故宮(360全景+紀錄片+數據庫+公開課) - Zeroassetsor - 博客園, 2014, www.cnblogs.com/pianist/p/3315801.html.
[4]toeic, clover. 「clover_toeic.」 數字故宮(360全景+紀錄片+數據庫+公開課) - Zeroassetsor - 博客園, 2014, www.cnblogs.com/clover-toeic/p/3851102.html.
[5]Xu, Mike. 「Linux中的邏輯地址,線性地址和物理地址轉換關係.」 爲何版本控制如此重要? - CSDN博客, 2014, blog.csdn.net/u011253734/article/details/41173849.
(參考文獻0分,缺失 -1分)