天天看點

ICS大作業--hello程式人生

摘  要
           

Hello是每個程式員第一個接觸的程式,在本文中利用計算機系統所學的知識,基于Linux平台,通過gcc、objdump、gdb、edb等工具,從c源程式的預處理開始,跟蹤分析程式編譯、彙編、連結、程序管理、存儲管理、I\O管理整個過程,完整的參與hello的程式人生。走過C語言程式生命周期的四季,見證shell指令之春,fork程序之夏,加載執行之秋,回收釋放之冬,感悟計算機系統之美。

關鍵詞:計算機系統 彙編 連結 程序 虛拟記憶體

**第1章 概述**
           

1.1 Hello簡介

在linux中,hello.c經過cpp的預處理、ccl的編譯、as的彙編、ld的連結最終成為可執行目标程式hello,在shell中鍵入相應指令行參數後,shell解析指令并建構argc,argv[]和envp[]參數清單,shell再調用fork()函數建立一個子程序,子程序獲得與父程序完全相同的虛拟存儲空間的一個備份。将解析得到的argc,argv[]和envp[]參數清單傳給execve()函數作為參數,execve()函數啟動加載器,在目前的上下文中開始執行可執行檔案hello第一條指令即将控制轉移到子程序,于是hello便從Program搖身一變成為Process,這便是P2P的過程。

開始執行程式後,随着虛拟存儲的通路程式開始載入實體記憶體,然後進入 main函數執行目标代碼,CPU通過上下文切換為運作的hello配置設定時間片執行邏輯控制流。當hello程序運作結束後,核心将子程序的退出狀态傳遞給父程序,父程序shell(或init程序)負責回收hello程序,核心删除相關資料結構,以上全部便是020的過程。

1.2 環境與工具

硬體環境:Intel Core i5-8300H x64CPU 2.3GHZ,8G RAM,128G SSD +1T HDD.

軟體環境:Ubuntu18.04.1 LTS

開發與調試工具:gdb,gcc,as,ld,edb,readelf,HexEdit

1.3 中間結果

hello.i 預處理之後文本檔案

hello.s 編譯之後的彙編檔案

hello.o 彙編之後的可重定位目标執行

hello 連結之後的可執行目标檔案

hello.o.objdump Hello.o的反彙編檔案

hello.objdump Hello的反彙編檔案

1.4 本章小結

程式有四季,往來知春秋。Hello波瀾壯闊的人生畫卷即将展開。。。

(第1章0.5分)

第2章 預處理

2.1 預處理的概念與作用

預處理:預處理程式(CPP)對源程式中以字元#開頭的指令進行處理。

作用:c預處理程式為cpp(C Preprocessor),主要用于C語言編譯器對各種預處理指令進行處理,包括對頭檔案的包含,宏定義的擴充,條件編譯的選擇等,例如,對于#include訓示的處理結果,就是将相應的.h檔案的内容插入到源程式檔案中。

2.2在Ubuntu下預處理的指令

預處理指令:

ICS大作業--hello程式人生

具體過程:

ICS大作業--hello程式人生
ICS大作業--hello程式人生

2.3 Hello的預處理結果解析

ICS大作業--hello程式人生

Hello.i檔案部分截圖

解析:預處理主要是對各種預處理指令進行處理,具體包括對頭檔案的包含,宏定義的擴充,條件編譯的選擇等。預處理主要是根據#符号進行處理,在hello中,對于#include訓示的處理結果,就是将相應的.h檔案的内容插入到源程式檔案中,上圖中的主要是對stdio.h unistd.h stdlib.h的依次展開,之後再對宏定義進行拓展,再進行條件編譯的選擇,即根據條件值來決定是否執行包含其中的邏輯。

2.4 本章小結

預處理開啟程式編譯之路,#符譜寫hello人生初章。字字句句頭檔案,點點滴滴宏定義,條件編譯一線牽。

(第2章0.5分)

**第3章 編譯**
           

3.1 編譯的概念與作用

編譯:編譯程式(CCL)對預處理後的源程式進行編譯,生成一個彙編語言源程式檔案。

作用:主要是将C語言檔案翻譯為彙編語言檔案。C編譯器在進行具體的程式翻譯之前,會先對源程式進行詞法分析和文法分析,然後根據分析的結果進行代碼優化和存儲配置設定,最終把C語言源程式翻譯為彙編語言程式。

3.2 在Ubuntu下編譯的指令

編譯指令:

ICS大作業--hello程式人生

編譯過程:

ICS大作業--hello程式人生
ICS大作業--hello程式人生

3.3 Hello的編譯結果解析

部分編譯結果:

ICS大作業--hello程式人生

彙編指令:

指令 含義

.file 聲明源檔案

.text 以下是代碼段

.globl 聲明一個全局變量

.data 表明在.data節

.section .rodata 以下是.rodata節

.align 聲明對指令或者資料的存放位址進行對齊的方式

.type 用來指定是函數類型或是對象類型

.size 聲明大小

.long、.string 聲明一個long、string類型

結果解析:

3.3.1 整數(int 型)

1.全局變量 sleepsecs:

ICS大作業--hello程式人生

由代碼可見,首先在.text節中被聲明為global變量,之後在.data節中聲明對齊方式為4位元組對齊,為對象類型,聲明大小為4位元組,另外設定為long類型其值為2(long類型在linux下與int相同為4B,将int聲明為long應該是編譯器偏好)。

2.局部變量 i: 在棧中配置設定記憶體,隻讀段和讀寫資料段均不做聲明。如下所示,64位下通過rbp配置設定棧空間給i

ICS大作業--hello程式人生
3.指令行參數 argc: 由shell解析後構造出,傳遞給execve作為參數啟動加載器,存放在main的棧幀之上。
	4.立即數:在代碼段中,運作程式時放入棧或寄存器中。
           

3.3.2 數組

Argv和envp: 由shell解析後構造出,傳遞給execve作為參數啟動加載器,存放在main的棧幀之上。

ICS大作業--hello程式人生

3.3.3 字元串

ICS大作業--hello程式人生

Printf函數的指令行格式串,首先聲明在.rodata節中,再聲明類型為string,分别用.LC0和.LC1指代

3.3.4 指派

程式中涉及的指派操作有:

1.int sleepsecs=2.5 :因為sleepsecs是全局變量,是以直接在.data節中将sleepsecs聲明為值2的long類型資料。

2.i=0:整型資料的指派使用mov指令完成,在棧或寄存器中配置設定存儲空間,根據資料的大小不同使用不同字尾

3.3.5 類型轉換(隐式)

ICS大作業--hello程式人生

隐式類型轉換,将float轉換為int型。當在double或float向int進行類型轉換的時候,程式改變數值和位模式的原則是:值會向零舍入,直接舍掉小數點後的部分。如果溢出則為NAN, 與Intel相容的微處理器指定位模式[10…000]為整數不确定值,故會産生一個不确定值。

3.3.6 算術操作

ICS大作業--hello程式人生

i++為運算操作,在彙編中通過add或lea實作,在程式中為:

ICS大作業--hello程式人生

3.3.7 關系操作

ICS大作業--hello程式人生
ICS大作業--hello程式人生

< , !=為關系操作,在彙編中通過cmp,test,set等實作,例如cmp a,b,通過(b-a)設定标志位,如果結果為負則SF置為1,結果為0則ZF置為1,有符号數進位則CF置為1,無符号數溢出則OF置為1(還要考慮取反的影響),再根據設定好的符号為邏輯運算後确定比較結果。

ICS大作業--hello程式人生
ICS大作業--hello程式人生

3.3.8 數組/指針/結構操作

ICS大作業--hello程式人生

argv數組是shell由指令行參數構造出的,傳遞給main函數的第二個參數,具體在棧中的記憶體映像見3.3.2中的圖。在彙編中,棧如下:

32(rbp)

28 i

24

20

16

12 argc

8

4

0(rsp) 指向argv

通過rbp在棧中尋找argv的首位址,由于是char *類型,故分别加8加16得到argv[1],argv[2],取記憶體内容後分别放入rsi和rdx,傳給printf函數作為參數。

ICS大作業--hello程式人生

3.3.9 控制轉移

ICS大作業--hello程式人生
ICS大作業--hello程式人生

在彙編中通過j進行控制轉移,由cmp和j語句構成條件判斷語句,在這個程式中,

ICS大作業--hello程式人生
ICS大作業--hello程式人生

由rbp在棧中分别找到argc和i,用cmp設定符号位後,再根據符号為進行跳轉,進而實作控制流的轉移。

3.3.10 函數操作

ICS大作業--hello程式人生
ICS大作業--hello程式人生
ICS大作業--hello程式人生
ICS大作業--hello程式人生

1.main函數

ICS大作業--hello程式人生

Main符号首先聲明在.text節中,再聲明為global變量,類型為函數類型。兩個參數argc和argv[]由shell構造好後,傳入execve作為參數,調用加載器,在之前的上下文中運作程式,故main的棧幀上方就是argc和argv[](envp[]),具體在棧中的記憶體映像見3.3.2中的圖。

2.printf函數

ICS大作業--hello程式人生
ICS大作業--hello程式人生

第一次調用printf函數,由于隻輸出一個字元串,實際調用的是puts函數(更快),在彙程式設計式開頭表明了.LC0指代第一個字元串,将它的位址放入rdi中構造好參數後,調用Puts函數。

第二次調用printf函數,分别用rdi,rsi,rdx構造好三個參數後,調用printf函數。

3.exit函數

ICS大作業--hello程式人生

exit(x)則将x作為參數放在rdi中,再控制轉移調用exit函數即可

4.sleep函數

ICS大作業--hello程式人生

将參數放到rdi中,再調用sleep函數即可。

5.getchar函數

ICS大作業--hello程式人生

由于getchar沒有參數,故直接調用getchar進行控制流轉移即可。

3.4 本章小結

編譯後在檔案頭部對各個變量的屬性進行解釋,之後才是彙編源程式。

在用rbp作為幀指針的彙程式設計式中,首先将rsp賦給rbp再儲存rbp值,由rsp配置設定棧空間,rbp值不變,作為索引為各變量配置設定棧空間并進行查找。了解了這一點後可以友善的畫出棧結構并了解彙程式設計式與C語言源程式的對應關系。

最是文本留不住,風騷彙編譜新篇。手握bp摘星辰,棧幀頻仍一紙定。

(第3章2分)

第4章 彙編

4.1 彙編的概念與作用

彙編:彙程式設計式(as)對彙編語言源程式進行彙編,生成一個擴充名為.o的可重定位目标檔案。

作用:将編譯生成的彙編語言代碼轉換為機器語言代碼。

4.2 在Ubuntu下彙編的指令

彙編指令:

ICS大作業--hello程式人生

彙編過程:

ICS大作業--hello程式人生
ICS大作業--hello程式人生

4.3 可重定位目标elf格式

ICS大作業--hello程式人生
ICS大作業--hello程式人生
ICS大作業--hello程式人生
ICS大作業--hello程式人生
ICS大作業--hello程式人生
ICS大作業--hello程式人生

分析:

  1. ELF頭:以16B的序列Magic開始,Magic描述了生成該檔案的系統的字的大小和位元組順序,ELF頭剩下的部分包含幫助連結器文法分析和解釋目标檔案的資訊,其中包括ELF頭的大小、目标檔案的類型、機器類型、位元組頭部表(section header table)的檔案偏移,以及節頭部表中條目的大小和數量等資訊。32位的ELF頭資料結構如下:
    ICS大作業--hello程式人生
    在32 位 ELF 頭的資料結構中, 字段 e_ident是一個長度為16 的位元組序列, 其中, 最開始的4位元組用來辨別是否為ELF 檔案, 第一個位元組為Ox7F, 後面三個位元組分别為 ’ E ’ 、 L’ 、’ F ’ 。再後面的12 個位元組中, 主要包含一些辨別資訊, 例如, 辨別是32 位還是64 位格式、辨別資料按小端還是大端方式存放、辨別 ELF 頭的版本号等。字段 e_type 用于說明目标檔案的類型是可重定檔案、可執行檔案、共享庫檔案, 還是其他類型檔案。字段e_ machine 用于指定機器結構類型,如從 32、SPARC V9 、AMD64 等。字段 e_ version 用千辨別目标檔案版本。字段 e_entry用于指定系統将控制權轉移到的起始虛拟位址 (入口點), 如果檔案沒有關聯的入口點, 則為零。例如,對于可重定位檔案, 此字段為0。字段 e_ehsize 用千說明ELF 頭的大小 (以位元組為機關)。字段e_shoff 指出節頭表在檔案中的偏移扯(以位元組為機關)。字段 e_shentsi ze 表示節頭表中一個表項的大小(以位元組為機關,)所有表項大小相同。字段e_shnum 表示節頭表中的項數。是以 e_shentsize和 e_shnum 共同指定了節頭表的大小(以位元組為機關)。僅 ELF 頭在檔案中具有固定位置, 即總是在最開始的位置, 其他部分的位置由ELF 頭和節頭表指出, 不需要具有固定的順序。

2. 節頭表

節頭表由若幹個表項組成,每個表項描還相應的一個節的節名,位置和長度等資訊,目标檔案中的每個節都有一個表項與之對應。

3. 重定位節

1…rela.text:.text節相關的可重定位資訊。當連結器将某個目标檔案和其他目标檔案組合時,. text節中的代碼被合并後,一些指令中引用的操作數位址資訊或跳轉目标指令位置資訊等都可能要被修改。通常,調用外部函數或者引用全局變量的指令中的位址字段需要修改。

如下圖,.rela.text節中記錄了各個函數名和變量名對應的偏移,值,類型,符号值等資訊。

ICS大作業--hello程式人生

各個屬性的意義如下表:

偏移量 需要進行重定向的代碼在.text或.data節中的偏移位置,8個位元組。

資訊 包括symbol和type兩部分,其中symbol占前4個位元組,type占後4個位元組,symbol代表重定位到的目标在.symtab中的偏移量,type代表重定位的類型

類型 重定位到的目标的類型

符号值 重定向到的目标的值

符号名稱 重定向到的目标的名稱

加數 計算重定位位置的輔助資訊,共占8個位元組

2..rela.en_frame:應該為.en_frame節相關的可重定位資訊。其他和上面類似。

3.rela.data:一般而言,任何已初始化的全局變量,如果他的初始值是一個全局變量位址或者外部定義函數的位址,都需要修改。但是這個源程式中沒有,故此節省略。
           

4.4 Hello.o的結果解析

ICS大作業--hello程式人生
ICS大作業--hello程式人生

1.機器語言的構成,與彙編語言的映射關系:可以看出,機器語言就是一堆16進制序列(實際是01序列),指令和寄存器由特定的值代表,立即數由小端表示的16進制數表示,由指令分隔開連續的16進制序列,進而與彙編語言一一對應。

2. 機器語言中的操作數與彙編語言不一緻:每條指令對應的01序列的含義有不同的規定,例如push %rbp,指令為55H = 01010101B,其中高5位01010為push的操作碼,為小端法,即A0,後三位101為rbp的編号,為5。再例如leave指令為C9H = 11001001B,沒有顯示操作數,故8位都是指令操作碼。

3. hello.o的反彙編與hello.s的對比:

對比: 1.反彙編出的代碼沒有全局變量的資訊

2.分支轉移:反彙編出的代碼跳轉處都采用相對尋址,而在編譯出的.s檔案裡用标志代替。因為标志隻是在彙編語言中便于編寫的助記符,是以在彙編成機器語言之後顯然不存在,而是确定的位址。

3.操作數:反彙編出的代碼中數字采用十六進制,而編譯出的.s檔案裡數字采用十進制

4. 函數調用:在.s檔案中,函數調用之後直接跟着函數名稱,而在反彙程式設計式中,call的目标位址是目前下一條指令。這是因為hello.c中調用的函數都是共享庫中的函數,最終需要通過動态連結器才能确定函數的運作時執行位址,在彙編成為機器語言的時候,對于這些不确定位址的函數調用,将其call指令後的相對位址設定為全0(目标位址正是下一條指令),然後在.rela.text節中為其添加重定位條目,等待靜态連結的進一步确定。

4.5 本章小結

彙編過程利用彙編語言與機器語言的一一映射關系将彙編語言轉換為機器語言,生成可重定位目标檔案。可重定位目标檔案開啟了目标檔案階段,使得機器可以直接識别并執行。

彙編不是無情物,化作機器更護花。

(第4章1分)

第5章 連結

5.1 連結的概念與作用

連結:将關聯到的所有目标代碼檔案結合到一起,形成一個具有統一位址空間的可執行檔案的過程稱為連結。

作用:1.子產品化。它能使一個程式被劃分為多個子產品,由不同的程式員進行編

寫,而且可以建構公共的函數庫以提供給不同的程式重用

2.效率高。每個子產品可以分開編譯在程式修改時隻需重新編譯修改過的源程式檔案,再重新連結,時間效率更高

3,提高了空間使用率。源程式檔案中無需包含共享庫的所有代碼,隻要直接調用即可,而且可執行檔案運作時的記憶體中,也隻需要包含所調用的函數的代碼而不需要包含整個共享庫,空間使用率高。

5.2 在Ubuntu下連結的指令

連結的指令:

ICS大作業--hello程式人生

連結的過程:

ICS大作業--hello程式人生

5.3 可執行目标檔案hello的格式

ICS大作業--hello程式人生
ICS大作業--hello程式人生
ICS大作業--hello程式人生
起始位置	大小
           

隻讀記憶體段(代碼段) 0x400000 0x710

讀/寫記憶體段(資料段) 0x600000 0x42

不加載到記憶體的符号表和調試資訊 0x0 0x954

5.4 hello的虛拟位址空間

用edb打開,可以看到再Data Dump中,自虛拟位址0x400000開始,到0x400fff結束,這之間每個節(.interp ~ .eh_frame節)的排列(開始結束)即為上圖中Address中聲明。在0x400fff之後虛拟位址段0x6000000x602000存放的是.dynamic.shstrtab節。

ICS大作業--hello程式人生

可執行檔案在記憶體中連續存放,因而這些連續的片段被映射到虛拟位址空間中的一個存儲段,程式頭表(也稱段頭表)用于描述這種映射關系,一個表項說明一個連續的片段或者一個特殊的節。32位(64位類似)的程式頭表中每個表項具有以下資料結構:

ICS大作業--hello程式人生

p_type(對應下圖中的Type)描述存儲段的類型或特殊節的類型。例如,是否為可裝入段 ( PT_LOAD),是否是特殊的動态節 ( PT_DYNAMIC) ,是否是特殊的解釋程式節 ( PT_INTERP) 。p_offset (對應下圖中的offset)指出本段的首位元組在檔案中的偏移位址。p_ vaddr (對應下圖中的VirtAddr)指出本段首位元組的虛拟位址。p_paddr (對應下圖中的PhysAddr)指出本段首位元組的實體位址,因為實體位址由作業系統根據情況動态确定, 因而該資訊通常是無效的。p_filesz(對應下圖中的FileSiz)指出本段在檔案中所占的位元組數,可以為0。p_memsz(對應下圖中的MemSiz)指出本段在存儲器中所占位元組數,也可以為0。p_flags(對應下圖中的Flags)指出存取權限。p_align(對應下圖中的Align) 指出對齊方式,用 一個模數表示, 為 2 的正整數幕, 通常模數與頁面大小相關,若頁面大小為4KB , 則模數為2^12

ICS大作業--hello程式人生

5.5 連結的重定位過程分析

ICS大作業--hello程式人生

部分反彙編結果

Hello的反彙編結果與hello.o的反彙編結果相比,多了以下的節:

_init 程式初始化代碼

gmon_start call_gmon_start函數初始化gmon profiling system,這個系統是在編譯程式時加上-pg選項,程式通過gprof可以輸出函數調用等資訊

_dl_relocate_static_pie 靜态庫連結

.plt 動态連結-過程連結表

Puts(等函數)@plt 動态連結各個函數

_start 編譯器為可執行檔案加上了一個啟動例程

__libc_csu_init 程式調用libc庫用來對程式進行初始化的函數,一般先于main函數執行

_fini 當程式正常終止時需要執行的代碼

連結的過程:

1.函數個數:在使用ld指令連結的時候,指定了動态連結器為64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定義了程式入口_start、初始化函數_init,_start程式調用hello.c中的main函數,libc.so是動态連結共享庫,其中定義了hello.c中用到的printf、sleep、getchar、exit函數和_start中調用的__libc_csu_init,__libc_csu_fini,__libc_start_main。連結器将上述函數加入。

2.函數調用:連結器解析重定條目時發現對外部函數調用的類型為R_X86_64_PLT32的重定位,此時動态連結庫中的函數已經加入到了PLT中,.text與.plt節相對距離已經确定,連結器計算相對距離,将對動态連結庫中函數的調用值改為PLT中相應函數與下條指令的相對位址,指向對應函數。對于此類重定位連結器為其構造.plt與.got.plt。

3…rodata引用:連結器解析重定條目時發現兩個類型為R_X86_64_PC32的對.rodata的重定位(printf中的兩個字元串),.rodata與.text節之間的相對距離确定,是以連結器直接修改call之後的值為目标位址與下一條指令的位址之差,指向相應的字元串。

基本的重定位類型有:1.R_386_PC32:相對尋址方式 2.R_386_32:絕對尋址方式。此處以相對尋址方式為例解析重定位過程。

重定位過程:

資訊(r_info)可分為高位的索引值和低位的重定位類型。如下圖中,以第二個表項為例,偏移量(r_offset)為0x1b,類型為R_386_PC32,即為相對尋址方式,索引值為0xb,故所引用的符号為符号表中的第11項,即為puts。

ICS大作業--hello程式人生

下圖為hello.o的反彙編結果,可以看出符号puts從.text節中偏移量為0x1b處開始,類型為R_386_PC32,與前面分析的一緻。不妨假設puts在main函數之後0x29處,則puts的位址為0x8048380+0x29 = 0x80483a9,對齊後為0x80483aa。

ICS大作業--hello程式人生

轉移目标位址計算公式為:轉移目标位址 = Pc+ 偏移位址。Call指令中的重定位位址就是偏移位址,故重定位值 = 轉移目标位址-PC。轉移目标位址就是符号puts定義的首位址,前面計算為0x80483aa,PC值為0x8048380+0x1f = 0x804839f,是以,重定位值應該為0x80483aa-0x804839f = 0xb,即重定位代碼應改為e8 0b 00 00 00。

由上圖中可以看出,e8 00 00 00 00,puts的位址初始值(init)為0x0。

由上面分析可以總結出公式:重定位值 = ADDR(r_sym) – ((ADDR(.text)+r_offset) – init)

5.6 hello的執行流程

edb觀察hello的執行流程,其調用與跳轉的各個子程式名或程式位址如下:

程式名稱 程式位址

ld-2.27.so!_dl_start 0x7fce 8cc38ea0

ld-2.27.so!_dl_init 0x7fce 8cc47630

hello!_start 0x400500

libc-2.27.so!__libc_start_main 0x7fce 8c867ab0

-libc-2.27.so!__cxa_atexit 0x7fce 8c889430

-libc-2.27.so!__libc_csu_init 0x4005c0

hello!_init 0x400488

libc-2.27.so!_setjmp 0x7fce 8c884c10

-libc-2.27.so!_sigsetjmp 0x7fce 8c884b70

–libc-2.27.so!__sigjmp_save 0x7fce 8c884bd0

hello!main 0x400532

[email protected] 0x4004b0

[email protected] 0x4004e0

*[email protected] –

*[email protected] –

*[email protected] –

ld-2.27.so!_dl_runtime_resolve_xsave 0x7fce 8cc4e680

-ld-2.27.so!_dl_fixup 0x7fce 8cc46df0

–ld-2.27.so!_dl_lookup_symbol_x 0x7fce 8cc420b0

libc-2.27.so!exit 0x7fce 8c889128

5.7 Hello的動态連結分析

1.加載時進行動态連接配接

動态連結過程如上圖所示。整個過程被分成兩步:首先,進行靜态連結以生成部分連結的可執行目标檔案 hello , 該檔案中僅包含共享庫(包括指定的共享目标檔案 mylib.so 和預設的标準共享庫檔案libc.so) 中的符号表和重定位表資訊,而共享庫中的代碼和資料并沒有被合并到 hello 中;然後,在加載hello 時,由加載器将控制權轉移到指定的動态連結器,由動态連結器對共享目标檔案libc.so、mylib.so和hello 中的相應子產品内的代碼和資料進行重定位并加載共享庫,以生成最終的存儲空間中完全連結的可執行目标,在完成重定位和加載共享庫後,動态連結器把控制權轉移到程式hello。在執行hello的過程中,共享庫中的代碼和資料在存儲空間的位置一直是固定的。

ICS大作業--hello程式人生

2.還可以在程式運作時進行動态連結,不做展開。

3.延遲加載

動态庫是在程序啟動的時候加載進來的,加載後,動态連結器需要對其作一系列的初始化,如符号重定位(動态庫内以及可執行檔案内),這些工作是比較費時的,特别是對函數的重定位,是以延遲重定位可以提高效率,具體來說,就是應該等到第一次發生對該函數的調用時才進行符号綁定 – 此謂之延遲綁定。

延遲綁定的實作步驟如下:

a.建立一個 GOT.PLT 表,該表用來放全局函數的實際位址,但最開始時,該裡面放的不是真實的位址而是一個跳轉。

b.對每一個全局函數,連結器生成一個與之相對應的影子函數,如 [email protected]。

c.所有對 puts的調用,都換成對 [email protected] 的調用。

ICS大作業--hello程式人生

dl_init

ICS大作業--hello程式人生

dl_init調用前後GOT的變化

跳轉到這個位址,發現正式動态連結庫的入口位址。

ICS大作業--hello程式人生

GOT[2]指向的位址

為了驗證延遲綁定的實作,可以檢視printf調用前後[email protected]的指令跳轉位址,也就是對應GOT中的值,可以發現,調用後确實連結到了動态庫。

5.8 本章小結

連結器位于編譯器、指令集體系結構和作業系統的交叉點上,涉及指令系統、代碼生成、機器語言、程式轉換和虛拟存儲管理等諸多概念,囊括三種目标檔案格式,分為靜态連結和動态連結兩種,主要需要符号解析和重定位兩方面的工作。

大賢者(ld)合并捕食者和暴食者(可重定位目标檔案)誕生暴食之王(可執行目标檔案),大賢者(可執行目标檔案)加載時動态連結魔王進化(動态庫)進化成智慧之王,智慧之王(可執行目标檔案)運作時動态連結暴食之王(動态庫)誕生虛空之神,恭喜hello殿下(萌王)。

(第5章1分)

第6章 hello程序管理

6.1 程序的概念與作用

程序:程序的經典定義就是一個執行中程式的執行個體。簡單來說,程序是程式的一次運作過程,更确切地說,程序是一個具有一定獨立功能的程式關于某個資料集合的一次運作活動,具有動态含義。

作用:“程序”的引入為應用程式提供了以下兩方面的抽象:一個獨立的邏輯控制流和一個私有的虛拟位址空間。每個程序擁有一個獨立的邏輯控制流,使得程式員以為自己的程式在執行過程中獨占使用處理器;每個程序擁有一個私有的虛拟位址空間,使得程式員以為自己的程式在執行過程中獨占存儲器。

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

功能:1.它接收使用者指令,能解釋使用者輸入的指令,将它傳遞給核心,還可以然後調用相應的應用程式

2.調用其他程式,給其他程式傳遞資料或參數,并擷取程式的處理結果;

3.在多個程式之間傳遞資料,把一個程式的輸出作為另一個程式的輸入;

4.Shell 本身也可以被其他程式調用。

流程:1.shell指令行解釋器輸出指令行提示符,接受使用者指令

2.解析指令,建構argv,envp參數清單和參數個數argc

3. 如果是内置指令則立即執行,否則fork子程序

4.以建構的argc,arhv,envp為參數調用execve以啟動加載器,進而在目前程序上下文中加載并運作程式

5.shell接受鍵盤輸入信号,并對這些信号進行相應處理

6.3 Hello的fork程序建立過程

過程: 1.shell指令行解釋器輸出指令行提示符,接受使用者指令

2.解析指令,建構argv,envp參數清單和參數個數argc

3.fork子程序,新建立的子程序幾乎但不完全與父程序相同,子程序得到與父程序使用者級虛拟位址空間相同的(但是獨立的)一份副本,這就意味着,當父程序調用fork時,子程序可以讀寫父程序中打開的任何檔案。父程序與子程序之間最大的差別在于它們擁有不同的PID。

4.以建構的argc,arhv,envp為參數調用execve以啟動加載器,進而在目前程序上下文中加載并運作程式

6.4 Hello的execve過程

可執行檔案執行時,會通過加載器進行加載。在Linux/Unix中,可以通過execve()啟動加載器,execve()函數的功能是在目前程序的上下文中加載并運作一個新程式。execve () 函數的用法如下:

ICS大作業--hello程式人生

該函數的具體運作如下:該函數用來加載并運作可執行目标檔案filenam,可帶參數清單argv[]和環境變量清單envp[],若出現錯誤,如找不到指定檔案filename,則傳回-1,并将控制權傳回給調用程式;若函數執行成功,則不傳回,最終将控制權傳遞到可執行檔案的main函數。此時main函數的虛拟記憶體中使用者棧結構如下:

ICS大作業--hello程式人生

加載過程如下:

1.shell 指令行解釋器輸出一個指令行提示符(如:unix >),并開始接受使用者輸入的指令行。

2.當使用者在指令行提示符 後輸入指令行 "./ hello [ Enter] "後,開始對指令行進行解析,獲得各個指令行參數并構造傳遞給函數 execve () 的參數清單argv ,将參數個數送 argc。

3.調用函數fork (),建立一個子程序,新建立的子程序獲得與父程序完全相同的虛拟存儲空間中的一個備份,包括隻讀段、可讀寫資料段、堆以及使用者棧等 。

4.以第2 步指令行解析得到的參數個數argc、參數清單 argv以及全局變量 environ作為參數,調用函數 execve () ,進而實作在目前程序(新建立的子程序)的上下文中加載并運作 hello程式。在函數execve () 中,通過啟動加載器執行加載任務,将可執行目标檔案 hello 中的.text、. data、.bss節等内容加載到目前程序的虛拟位址空間(實際上并沒有将 hello 檔案中的代碼和資料從磁盤讀入主存,而是修改了目前程序上下文中關于存儲映像的一些資料結構)。當加載器執行完加載任務後,便開始轉到 hello 程式的第一條指令執行,從此, hello 程式開始在一個程序的上下文中運作。

6.5 Hello的程序執行

上下文:程序的實體實體(代碼和資料等)和支援程序運作的環境合稱為程序的上下文

上下文切換:作業系統通過處理器排程讓處理器輪流執行多個程序。實作不同程序中指令交替執行的機制稱為程序的上下文切換。

程序時間片:連續執行同一個程序的時間段稱為時間片( time slice )。

時間片輪轉處理器排程:每個時間片結束時,通過程序的上下文切換,換一個新的程序到處理器上執行 ,進而開始一個新的時間片,這個過程稱為時間片輪轉處理器排程。

ICS大作業--hello程式人生

程序的上下文切換具體實作:

上下文切換發生在作業系統排程一個新程序到處理器上運作時,它需要完成以下三件事:1.将目前程序的寄存器上下文儲存到目前程序的系統級上下文的現場資訊中;2.将新程序系統級上下文中的現場資訊作為新的寄存器上下文恢複到處理器的各個寄存器中;3.将控制轉移到新程序執行。這裡,一個重要的上下文資訊是PC的值.目前程序被打斷的斷點處的 PC作為寄存器上下文的一部分被儲存在程序現場資訊中,這樣,下次該程序再被排程到處理器上執行時,就能從現場資訊中獲得端點處的PC值,進而能從斷點處開始執行。

具體到Hello程序,則首先由shell通過加載器加載可執行目标檔案hello,由作業系統完成上下文切換,進而進入hello程序(使用者态),hello程序調用sleep函數後進入核心态,核心處理休眠請求主動釋放目前程序,并将hello程序從運作隊列中移出加入等待隊列,定時器開始計時,上下文切換後控制轉移到shell程序,定時器到時後發送一個中斷信号,由信号處理函數完成處理,将hello程序從等待隊列中移出重新加入到運作隊列,進而進行上下文切換進入到Hello程序,10次調用sleep函數則重複以上過程10次。

當hello調用getchar的時候,實際是執行輸入流是stdin的系統調用read(通過syscall調用),hello之前運作在使用者模式,syscall調用read之後進入陷阱,此時進行上下文切換,開始執行shell程序,在進行相應處理後(鍵盤輸入後),再進行上下文切換,轉回執行hello程序。

ICS大作業--hello程式人生

6.6 hello的異常與信号處理

可能出現的異常種類:中斷 故障(缺頁故障,調用缺頁異常處理函數即可) 可能出現的信号:SIGINT SIGSTP

正常執行:運作結束後hello程序被回收

ICS大作業--hello程式人生

Ctr-Z: 當按下ctrl-c之後,shell父程序收到SIGSTP信号,信号處理程式将hello程序挂起,放到背景,ps看到hello程序并沒有被回收,jobs找到hello的job号,fg 1将hello程序調到前台執行

ICS大作業--hello程式人生

Ctr-C: 當按下ctrl-c之後,shell父程序收到SIGINT信号,由信号處理函數結束hello,并回收hello程序。

ICS大作業--hello程式人生

6.7本章小結

程序是一個具有一定獨立功能的程式關于某個資料集合的一次運作活動,每個程序都有其獨立的邏輯控制流和私有的虛拟位址空間。作業系統通過程序的處理器排程,進行上下文切換,使得系統在目前程序的執行過程中發生了一個異常控制流。對于程序運作中的各種異常,相應的異常處理程式會向目前程序發送一個特定的信号,目前程序接收信号後調用相應的信号處理程式進行處理。

一片shell(貝殼)幾多愁,解析構造參數流(解析指令,構造參數清單)。Fork完後又加載(調用fork,execve),唯有hello空悠悠(hello的代碼和資料并沒有加載到記憶體)。

(第6章1分)

第7章 hello的存儲管理

7.1 hello的存儲器位址空間

實體位址:在存儲器裡以位元組為機關存儲資訊,為正确地存放或取得資訊,每一個位元組單元給以一個唯一的存儲器位址,稱為實體位址(Physical Address),又叫實際位址或絕對位址。CPU通過位址總線的尋址,找到真實的實體記憶體對應位址。在前端總線上傳輸的記憶體位址都是實體記憶體位址。

邏輯位址:CPU啟動保護模式後,這樣程式通路存儲器所使用的邏輯位址稱為虛拟位址。簡而言之就是虛拟記憶體空間中的位址。

虛拟位址:同邏輯位址。

線性位址:線性位址(Linear Address)是邏輯位址到實體位址變換之間的中間層。在分段部件中邏輯位址是段中的偏移位址,然後加上基位址就是線性位址。

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

邏輯位址到線性位址:

ICS大作業--hello程式人生

邏輯位址包含 16位的段選擇符和32位的段内偏移量。轉換過程中MMU首先根據段選擇符中的TI 确定選擇全局描述符表 ( GDT) 還是局部描述符表 ( LDT) 。若 TI = 0 , 選用GDT; 否則,選用 LDT。确定描述符表後,再通過段選擇符内的13 位索引值,從被選中的描述符表中找到對應的段描述符。因為每個段描述符占 8 個位元組,是以位移量為索引值乘 8,加上描述符表首位址(其中, GDT 首位址從 GDTR 的高 32 位獲得,LDT首位址從 LDTR 對應的LDT 描述符cache 中高32 位獲得),就可以确定選中的段描述符的位址,從中取出32 位的基位址 ( B31 - BO), 與邏輯位址中 32 位的段内偏移量相加,就得到 32 位線性位址。MMV 在計算線性位址 LA 的過程中,可以根據段的限界和段的通路權限判斷是否“位址越界”或“ 通路越權”,以實作存儲保護。

通常情況下 , MMU并不需要到主存中去通路 GDT 或 LDT, 而隻要根據段寄存器對應的描述符cache中的基位址、限界和通路(存取)權限來進行邏輯位址到線性位址的轉換,如圖所示。

ICS大作業--hello程式人生

邏輯位址中32位的段内偏移量即是有效位址 EA,它由指令中的尋址方式來确定如何得到。從上圖可看出,IA- 32 中有效位址的形成方式有以下幾種:偏移量、基址、變址、比例變址、基址加偏移量、基址加變址、基址加比例變址、基址加變址加偏移量、基址加比例變址加偏移量等。比例變址時,變址值等于變址寄存器的内容乘以比例因子。

7.3 Hello的線性位址到實體位址的變換-頁式管理

IA-32和x86-64采用段頁式虛拟存儲管理方式,通過分段方式完成邏輯位址到線性位址的轉換後,再進一步通過分頁方式将線性位址轉換為實體位址。

下圖所示的是分頁部件将線性位址轉換為實體位址的基本過程,為了解決頁表過大的問題,采用了兩級頁表方式。

ICS大作業--hello程式人生

從圖 6. 41 可看出,在一個兩級頁表分頁方式中,線性位址由三個字段組成, 它們分别是10 位頁目錄索引 ( DIR ) 、10 位頁表索引 ( PAGE ) 和12位頁内偏移量( OFFSET)。頁目錄項和頁表項的格式如圖:

ICS大作業--hello程式人生

頁目錄項和頁表項中部分字段的含義簡述如下。

P: P= 1表示頁表或頁在主存中;p = 0表示頁表或頁不在主存中,此時發生頁故障(缺頁異常),需将頁故障線性位址記錄在CR2 中。作業系統在處理頁故障時會将缺失的頁表或頁從磁盤裝入主存中,并重新執行引起頁故障的指令。

R/W: 該位為0時表示頁表或頁隻能讀不能寫;為1時表示可讀可寫。

UI S : 該位為0時表示使用者程序不能通路;為1時允許使用者程序通路。該位可以保護作業系統所使用的頁不受使用者程序的破壞 。

PWT: 用來控制頁表或頁對應的cache寫政策是全寫( write through )還是回寫( write back)。

PCD: 用來控制頁表或頁能否被緩存到cache中。

A : A= 1表示指定頁表或頁被通路過,初始化時作業系統将其清0。利用該标志,作業系統可清楚地了解哪些頁表或頁正在使用,一般選擇長期未用的頁或近來最少使用的頁調出主存。由MMU在進行位址轉換時将該位置1。

D: 修改位或稱髒位( dirty bit)。該位在頁目錄項中沒有意義,隻在頁表項中有意義。D =1 表示頁被修改過;否則說明頁面内容未被修改,因而在作業系統将頁面替換出主存時,無需将頁面寫入磁盤。初始化時作業系統将其清0 , 由MMU 在進行寫操作的位址轉換時将該位置 l。

頁目錄項和頁表項中的高20位是頁表或頁在主存中的首位址對應的頁框号, 即首位址的高20位。每個頁表的起始位置都按 4KB 對齊。

從圖中可看出,線性位址向實體位址的轉換過程如下:首先,根據控制寄存器 CR3 中給出的頁目錄表首位址找到頁目錄表,由DIR字段提供的10位頁目錄索引找到對應的頁目錄項,每個頁目錄項大小為 4B; 然後,根據頁目錄項中 20 位基位址指出的頁表首位址找到對應 頁表,再根據線性位址中間的頁表索( PAGE字段)找到頁表中的頁表項;最後, 将頁表項中的20 位基位址和線性位址中的 12 位頁内偏移量組合成 32 位實體位址。上述轉換過程中10 位的頁目錄索引和10 位的頁表索引都要乘以4 , 因為每個頁目錄項和頁表項都是 32 位,占4 個位元組。由千頁目錄索引和頁表索引均為10位;每個頁目錄項和頁表項占 用 4 個位元組,是以頁目錄表和頁表的長度均為 4 KB, 并分别含有1024個表項。這樣,對于 12 位偏移位址,32 位的線性位址所映射的實體位址空間是1024 x 1024 x4KB = 4GB。

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

ICS大作業--hello程式人生

如上圖所示(圖中為二級頁表,隻要将主存框内改為4級頁表即可),首先(以32位為例,64位也是一樣)在TLB中查找是否有頁表項,将虛拟位址根據頁的大小分為虛拟頁号和頁内位址,再根據TLB的條目數分為标記位群組索引,在TLB(在cache裡)中周遊各個條目(由頁表基址寄存器值+組索引),如果标記位相同且有效位為1則讀出相應的實體頁号,和頁内位址拼接則得出實體位址。

如果TLB中沒有找到符合的條目,即TLB缺失,則在主存中查找四級頁表。将虛拟頁号根據四級頁表的頁表項數量分為四個虛拟頁号,根據四個虛拟頁号(前三個為頁目錄索引,第四個為頁表索引),在四級頁表中找出實體頁号,如果相應頁表條目為已緩存,則讀出實體頁号和頁内位址相拼接得到實體位址。否則做缺頁處理。

綜合過程如下:

ICS大作業--hello程式人生

7.5 三級Cache支援下的實體記憶體通路

ICS大作業--hello程式人生

讨論組相聯下的讀取,其他類似。

得到實體位址後,根據cache塊大小和行(組)數将實體位址分為标記位,組索引和塊内位址,如果組索引下的cache行标記位相同,有效位為1,則命中,讀出cache行中第塊内位址個處的位元組。如果不命中,則向下一級緩存(或主存)取相應的cache行(主存行),此時如果原來的組中有空閑行,則直接替換,如果沒有空閑行,則需要替換,常用的替換政策有随機替換,LRU,LFU等。

7.6 hello程序fork時的記憶體映射

Shell通過調用函數fork()建立一個子程序,核心為新程序建立各種資料結構,并配置設定給它一個唯一的PID,新建立的子程序獲得與父程序完全相同的虛拟存儲空間中的一個備份,包括隻讀段,可讀寫資料段,堆以及使用者棧等。

7.7 hello程序execve時的記憶體映射

在函數execve () 中,通過啟動加載器執行加載任務,将可執行目标檔案 hello 中的.text、. data、.bss節等内容加載到目前程序的虛拟位址空間(實際上并沒有将 hello 檔案中的代碼和資料從磁盤讀入主存,而是修改了目前程序上下文中關于存儲映像的一些資料結構)。當加載器執行完加載任務後,便開始轉到 hello 程式的第一條指令執行。

ICS大作業--hello程式人生

具體步驟有:

1.删除已存在的使用者區域,删除目前程序虛拟位址的使用者部分中的已存在的區域結構。

2映射私有區域,為新程式的代碼、資料、bss和棧區域建立新的區域結構,所有這些新的區域都是私有的、寫時複制的。代碼和資料區域被映射為hello檔案中的.text和.data區,bss區域是請求二進制零的,映射到匿名檔案,其大小包含在hello中,棧和堆位址也是請求二進制零的,初始長度為零。

3.映射共享區域, hello程式與共享對象libc.so連結,libc.so是動态連結到這個程式中的,然後再映射到使用者虛拟位址空間中的共享區域内。

4.設定程式計數器(PC),execve做的最後一件事情就是設定目前程序上下文的程式計數器,使之指向代碼區域的入口點。

5.将main的參數傳入棧,如圖,将構造好的argc,argv[],envp[],寫入棧作為參數。

ICS大作業--hello程式人生

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

首先要厘清缺頁故障和段故障:在主存中查找頁表時,如果相應頁表條目有效位為0且實體頁号為NULL,則該頁表條目處于未配置設定,此時應該是屬于段故障的一種,相應異常處理程式為終止;如果相應頁表條目有效位為0但是實體頁号指向磁盤,則為真正的缺頁故障,此時調用相應的異常處理程式,從磁盤裝入相應頁到記憶體并更新頁表,再傳回到故障指令開始執行。

ICS大作業--hello程式人生

缺頁故障為傳回到目前指令

7.9動态存儲配置設定管理

動态存儲配置設定管理由動态記憶體配置設定器完成。動态記憶體配置設定器維護着一個程序的虛拟記憶體區域,稱為堆。堆是一個請求二進制零的區域,它緊接在未初始化的資料區後開始,并向上生長(向更高的位址)。配置設定器将堆視為一組不同大小的塊的集合來維護。

每個塊就是一個連續的虛拟記憶體片,要麼是已配置設定的,要麼是空閑的。已配置設定的塊顯式地保留為供應用程式使用。空閑塊可以用來配置設定。空閑塊保持空閑,直到它顯示地被應用程式所配置設定。

一個已配置設定的塊保持已配置設定狀态,直到它被釋放,這種釋放要麼是應用程式顯式執行的,要麼是記憶體配置設定器自身隐式執行的。

動态記憶體配置設定器從堆中獲得空間,将對應的塊标記為已配置設定,回收時将堆标記為未配置設定。而配置設定和回收的過程中,往往涉及到分割、合并等操作。

動态記憶體配置設定器的目标是在對齊塊的基礎上,盡可能地提高吞吐率及空間占用率,即減少因為記憶體配置設定造成的碎片。其實作常見的資料結構有隐式空閑連結清單、顯式空閑連結清單、分離空閑連結清單,常見的放置政策有首次适配、下一次适配和最佳适配。

為了更好的介紹動态存儲配置設定的實作思想,以隐式空閑配置設定器的實作原理為例進行介紹。

ICS大作業--hello程式人生

隐式空閑連結清單配置設定器的實作涉及到特殊的資料結構。其所使用的堆塊是由一個子的頭部、有效載荷,以及可能的一些額外的填充組成的。頭部含有塊的大小以及是否配置設定的資訊。有效載荷用來存儲資料,而填充塊則是用來對付外部碎片以及對齊要求。基于這樣的基本單元,便可以組成隐式空閑連結清單。

通過頭部記錄的堆塊大小,可以得到下一個堆塊的大小,進而使堆塊隐含地連接配接着,進而配置設定器可以周遊整個空閑塊的集合。在連結清單的尾部有一個設定了配置設定位但大小為零的終止頭部,用來标記結束塊。

當請求一個k位元組的塊時,配置設定器搜尋空閑連結清單,查找足夠大的空閑塊,其搜尋政策主要有首次适配、下一次适配、最佳适配三種。

一旦找到空閑塊,如果大小比對的不是太好,配置設定器通常會将空閑塊分割,剩下的部分形成一個新的空閑塊。如果無法搜尋到足夠空間的空閑塊,配置設定器則會通過調用sbrk函數向核心請求額外的堆記憶體。

當配置設定器釋放已配置設定塊後,會将釋放的堆塊自動與周圍的空閑塊合并,進而提高空間使用率。為了實作合并并保證吞吐率,往往需要在堆塊中加入腳部進行帶邊界标記的合并。

7.10本章小結

虛拟存儲機制的引入,使得每個程序具有一個一緻的、極大的、私有的虛拟位址空間。虛拟位址空間按照等長的頁來劃分,主存也相應劃分。通過頁表建立虛拟頁和主存之間的對應關系。虛拟存儲器有分頁式、分段式、段頁式三種。虛拟位址需轉換為實體位址才能訪存,為減少通路記憶體中頁表的次數,通常将活躍頁的頁表項放到一個高速緩存TLB中。

(第7章 2分)

第8章 hello的IO管理

8.1 Linux的IO裝置管理方法

裝置的模型化:檔案

裝置管理:unix io接口

檔案就是一個位元組序列,所有的IO裝置都被模型化為檔案,而所有的輸入和輸出都被當做對相應檔案的讀和寫來執行,這種将裝置優雅地映射為檔案的方式,允許Linux核心引出一個簡單低級的應用接口,稱為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.關閉檔案,核心釋放檔案打開時建立的資料結構,并将這個描述符恢複到可用的描述符池中去。

系統級I/O函數:

  1. create系統調用

    用法:int creat(char•name, mode_t perms) ;

第一個參數 name為需建立的新檔案的名稱,是一個表示路徑名和檔案名的字元串;第二個參數perms用于指定所建立檔案的通路權限,共有 9 位,分别指定檔案擁有者、擁有者所在組成員以及其他使用者各自所擁有的讀、寫和執行權限。通常用一個8進制數字中的三位分别表示讀、寫和執行權限,例如,perms = 0755, 表示擁有者具有讀,寫和執行權限,而擁有者所在組成員和其他使用者都隻有讀和執行權限。正常情況下,該函數傳回一個檔案描述符,若出錯,則傳回- 1。若檔案巳經存在,則該函數将把檔案截斷為長度為0的檔案,也即,将檔案原先的内容全部丢棄,是以,建立一個已存在的檔案不會發生錯誤。

2. open系統調用

用法:int open( char name, int flags , mode_t perms);

除了預設的标準輸入、标準輸出和标準錯誤三種檔案是自動打開以外,其他檔案必須用相應的函數顯式建立或打開後才能讀寫,可以用 open 系統調用顯式打開檔案。正常情況下,open () 函數傳回一個檔案描述符,它是一個用以唯一辨別被打開檔案的非負整數,若出錯,則傳回- 1。第一個參數name 為需打開檔案的名稱,是一個表示路徑名和檔案名的字元串;第二個參數flags 指出使用者程式将會如何通路這個打開檔案。第三個參數perms 用于指定所建立檔案的 通路權限,通常在open () 函數中該參數總是 0 ,除非以建立方式打開,此時,參數flags中應帶有O_C REAT 标志。不以建立方式打開一個檔案時,若檔案不存在,則發生錯誤。對于不存在的檔案,可用 creat 系統調用來打開。

3. read系統調用

用法:ssize_t read(int fd, void *buf, size_t n);

該函數功能是将檔案肛中從目前讀寫位置k開始讀取n個位元組到 buf 中,讀操作後檔案目前讀寫位置為k + n。假定檔案長度為m, 當k + n > m 時,則真正讀取的位元組數為m - k < n , 并且讀操作後檔案目前讀寫位置為檔案尾 。函數傳回值為實際讀取 位元組數,因而,當 m = k(EOF)時,傳回值為 0;出錯時傳回值為 - 1。

  1. write系統調用

    用法:ssize_t write(int fd, const void *buf, size_t n);

    該函數功能是将 buf中的n位元組寫到檔案fd中,從目前讀寫位置k處開始寫入。傳回值為實際寫入位元組數 m , 寫入後檔案目前讀寫位置為 k + m。對于普通的磁盤檔案,實際寫入位元組數 m 等于指定寫入位元組數 n。出錯時傳回值為 - 1。對于 read 和 write 系統調用,可以一次讀(寫) 任意位元組,例如,每次讀(寫)一個位元組或一個實體塊大小,如一個磁盤 扇區 (512 位元組)或一個記錄大小等。顯然,按照一個實體塊大小來讀(寫) 比較好,可以減少系統調用的次數。有些情況下 , read和 write 真正讀(寫)的位元組數比使用者程式設定的所需位元組數要少,這 種 情況并不被看成是一種錯誤 。通常,在讀(寫)磁盤檔案時,除非遇到 EOF, 否則不會出現這種情況。但是,當讀寫的是終端裝置檔案、網絡套接字檔案、UNIX 管道、Web 服務 器等時,都可能出現這種情況。

  2. lseek系統調用

    用法:long lseek(int fd, long offset , int origin );

    當随機讀寫一個檔案的資訊時,目前讀寫位置可能并非正好是馬上要讀或寫的位置,此時,需要用 lseek () 函數來調整檔案的目前位置。第一個參數fd 指出需調整位置的檔案;第二個參數指出相對位元組數; 第三個參數origin指出offset相對的基準,可以是檔案開頭 (origin = 0 ) 、目前位置 ( origin = 1 ) 和檔案末尾 ( origin= 2 ) 。

    6.stat/fstat系統調用

    用法: int stat(const name, struct statbuf);

    int fstat(int fd, struct stat *buf ) ;

    檔案的所有屬性資訊 ,包括檔案描述符、檔案名、檔案大小、建立時間、目前讀寫位置等都由作業系統内 核來維護, 這些資訊也稱為檔案的中繼資料( metadata ) 。使用者程式可以通過 stat ()或£stat () 函數來檢視檔案中繼資料。stat 第一個參數指出的是檔案名,而fstat指出的是檔案描述符,這兩個函數除了第一個參數類型不同外,其他方面全部一樣。

    7.close系統調用

    用法:lose(int fd ) ;

    該函數的功能就是關閉檔案fd。

8.3 printf的實作分析

ICS大作業--hello程式人生

如圖所示,假定使用者程式中有一個語句調用了庫函數 printf () , 在 printf () 函數中又通過一系列的函數調用,最終轉到調用write () 函數。在write () 函數對應的指令序列中,一定有一條用于系統調用的陷阱指令,即system_call。該陷阱指令執行後,程序就從使用者态陷人到核心态執行。Linux中有一個系統調用的統一入口, 即系統調用處理程式 system_call()。CPU執行陷阱指令後,便轉到system_call ()的第一條指令執行在system_call()中,将根據RAX寄存器中的系統調用号跳轉到目前系統調用對應的系統調用務例程 sys_write () 去執行。system_call()執行結束時,從核心态傳回到使用者态下的陷阱指令後面一條指令繼續執行 。

write通過執行syscall指令實作了對系統服務的調用,進而使核心執行列印操作。核心會通過字元顯示子程式,根據傳入的ASCII碼到字模庫讀取字元對應的點陣,然後通過vram(顯存)對字元串進行輸出。顯示晶片将按照重新整理頻率逐行讀取vram,并通過信号線向液晶顯示器傳輸每一個點(RGB分量),最終實作printf中字元串在螢幕上的輸出。

8.4 getchar的實作分析

getchar定義在stdio.h檔案中,我們在stdio.h中可以找到其相關的定義

ICS大作業--hello程式人生

getc()的函數原型如下:

ICS大作業--hello程式人生

getchar函數實際上就是getc(stdin),即标準輸入下的getc()。通過調用read函數傳回字元。其中read函數的第一個參數是描述符fd,0代表标準輸入。第二個參數輸入内容的指針,這裡也就是字元c的位址,最後一個參數是1,代表讀入一個字元。read函數的傳回值是讀入的字元數,如果為1說明讀入成功,那麼直接傳回字元,否則說明讀到了buf的最後。

read函數同樣通過sys_call中斷來調用核心中的系統函數。鍵盤中斷處理子程式會接受按鍵掃描碼并将其轉換為ASCII碼後儲存在緩沖區。然後read函數調用的系統函數可以對緩沖區ASCII碼進行讀取,直到接受Enter鍵傳回。

8.5本章小結

Linux将I/O輸入都抽象為了檔案,并提供相應的Unix I/O接口,使用者可以通過這些接口實作輸入與輸出。

使用者系統通常通過調用程式設計語言提供的庫函數或者作業系統提供的API函數來實作I/O操作,這些函數最終都會調用系統調用的封裝函數,通過封裝函數中的陷阱指令使使用者程序從使用者态轉到核心态執行。

曆盡九九八十一難,始得修來不滅金身。

(第8章1分)

結論

Hello一路走來,真的很不容易。

  1. 預處理,C語言編譯器對各種預處理指令進行處理,包括對頭檔案的包含,宏定義的擴充,條件編譯的選擇等,将hello.c轉換為hello.i。
  2. 編譯,将C語言檔案hello.i翻譯為彙編語言檔案hello.s
  3. 彙編,将彙編語言代碼檔案hello.s轉換為可重定位目标檔案hello.o
  4. 連結,将多個可重定位目标檔案hello.o、libc.a等經過符号解析和重定位結合,形成一個具有統一位址空間的可執行目标檔案hello
  5. 運作,在shell下輸入指令./hello 1170300916 pyx,shell解析指令并構造參數清單
  6. Fork子程序,shell調用fork建立一個子程序,具有和父程序完全相同的虛拟存儲空間備份
  7. 加載,shell将構造好的參數清單傳給execve作為參數,啟動加載器并開始執行hello的第一條指令,實作在目前程序上下文中運作hello,
  8. 執行,cpu通過上下文切換配置設定時間片
  9. 訪存,hello程式運作中需要的代碼和資料,通過虛拟位址在TLB和主存頁表中查找轉換為相應實體位址,再在cache和主存中讀取
  10. 異常處理,如果通過鍵盤輸入導緻外部中斷,相應異常處理程式發送信号給程序,程序調用相應信号處理函數
  11. 回收,shell父程序回收程序,核心删除為這個程序建立的所有資料結構。

    (結論0分,缺失 -1分,根據内容酌情加分)

附件

hello.i 預處理之後文本檔案

hello.s 編譯之後的彙編檔案

hello.o 彙編之後的可重定位目标執行

hello 連結之後的可執行目标檔案

hello.o.objdump Hello.o的反彙編檔案

hello.objdump Hello的反彙編檔案

hello.c C源檔案

參考文獻

[1] 蘭德爾E.布萊恩特 大衛R.奧哈拉倫. 深入了解計算機系統(第3版).

機械工業出版社. 2018.4.

[2] 袁春風 計算機系統基礎 機械工業出版社,2018.

[3] ZK的部落格. read和write系統調用以及getchar的實作.

https://blog.csdn.net/ww1473345713/article/details/51680017. 2016-6-15.

[4] 虛拟位址、邏輯位址、線性位址、實體位址:

https://blog.csdn.net/rabbit_in_android/article/details/49976101

[5] Pianistx. [轉]printf 函數實作的深入剖析.

https://www.cnblogs.com/pianist/p/3315801.html. 2013-09-11.

[6] gcc 簡單的 hello-world 到底連接配接了什麼

https://blog.csdn.net/hejinjing_tom_com/article/details/32325749

[7] 動态連結

https://blog.csdn.net/shuange3316/article/details/79221941

[8] 動态連結原理分析

https://blog.csdn.net/shenhuxi_yu/article/details/71437167

[9] printf 函數實作的深入剖析

https://www.cnblogs.com/pianist/p/3315801.html

繼續閱讀