「實例+圖解」一文帶你徹底看懂linux

摘 要

關鍵詞:LINUX;預處理;編譯;鏈接;進程管理;存儲管理;IO管理

本文的主要內容是介紹了在linux環境下hello程序從預處理到編譯再到鏈接,最後執行的全過程以及進程管理,存儲管理及IO管理的實現方式。本文結合hello程序的生成詳細地講述了預處理、編譯、彙編、進程管理、存儲管理、IO管理的概念、作用、命令等。本文的目的是幫助程序員瞭解在C語言的“外衣”下,程序是如何從產生、預處理、編譯、彙編,到最後被執行的。對於深入瞭解操作系統和計算機編譯原理具有重要意義。


「實例+圖解」一文帶你徹底看懂linux


第1章 概述

1.1 Hello簡介

根據Hello的自白,利用計算機系統的術語,簡述Hello的P2P,020的整個過程。

P2P: Hello.c經過cpp的預處理,ccl的編譯、as的彙編、ld的鏈接最終成為可執行目標程序Hello,在shell中鍵入啟動命令後,shell通過fork產生子進程, hello便從program變成了process

020: shell通過execve映射虛擬內存,進入程序入口後載入物理內存,然後進入 main函數執行目標代碼,CPU為運行的hello分配時間片執行邏輯控制流,當結束後,shell父進程負責回收hello進程

1.2 環境與工具

列出你為編寫本論文,折騰Hello的整個過程中,使用的工具

軟件環境:

Visual studio Community2017, Windows10 64位, Vmware 14;Ubuntu 16.04 LTS 64位;

硬件環境:

X64 CPU;2GHz;2G RAM;256GHD Disk 以上

開發與調試工具:

vim,gcc,as,ld,edb,readelf,HexEditGDB/OBJDUMP;DDD/EDB等

1.3 中間結果

列出你為編寫本論文,生成的中間結果文件的名字,文件的作用等。


「實例+圖解」一文帶你徹底看懂linux


1.4 本章小結

本章介紹根據Hello的自白,利用計算機系統的術語,簡述Hello的P2P,020的整個過程。列出使用的軟硬件環境和開發與調試工具。列出了生成的中間結果文件的名字,文件的作用等

第2章 預處理

2.1 預處理的概念與作用

2.11 概念:預處理一般是指在程序源代碼被翻譯為目標代碼的過程中,生成二進制代碼之前的過程。由預處理器對程序源代碼文本進行處理,把源代碼分割或處理成為特定的單位,得到的結果再由編譯器核心進一步編譯。這個過程並不對程序的源代碼進行解析。

2.12 作用:1: 宏定義。宏定義是用一個標識符來表示一個字符串,這個字符串可以是常量、變量或表達式。在宏調用中將用該字符串代換宏名。2:文件包含。文件包含是預處理的一個重要功能,它可用來把多個源文件連接成一個源文件進行編譯,結果將生成一個目標文件。3:條件編譯。條件編譯允許只編譯源程序中滿足條件的程序段,使生成的目標程序較短,從而減少了內存的開銷並提高了程序的效率。

2.2 在Ubuntu下預處理的命令

預處理命令:gcc –E hello.c > hello.i

「實例+圖解」一文帶你徹底看懂linux

圖2.2 在Ubuntu下預處理的命令

2.3 Hello的預處理結果解析

「實例+圖解」一文帶你徹底看懂linux

圖2.3 hello.i文件

用文本編輯器打開hello.i,main函數的預處理解析結果如上圖。在main函數前出現的是stdio.h unistd.h stdlib.h頭文件。.i程序中是沒有#define的,並使用了大量的#ifdef #ifndef的語句。預處理指令會對條件值進行判斷來決定是否執行包含其中的邏輯。

2.4 本章小結

本章介紹了預處理的概念與作用、命令,並展示了Hello的預處理結果解析。

第3章 編譯

3.1 編譯的概念與作用

  1. 編譯的概念:利用編譯程序從源語言編寫的源程序產生目標程序的過程,用編譯程序產生目標程序。 編譯程序把一個源程序翻譯成目標程序的工作過程分為五個階段:詞法分析;語法分析;語義檢查和中間代碼生成;代碼優化;目標代碼生成。
  2. 編譯的作用:把高級語言變成計算機可以識別的2進制語言,詞法分析、語法分析、語義檢查和中間代碼生成、代碼優化、目標代碼生成。

3.2 在Ubuntu下編譯的命令

<code>gcc -S hello.i -o hello.s
/<code>
「實例+圖解」一文帶你徹底看懂linux

圖3.2 在Ubuntu下編譯的命令

3.3 Hello的編譯結果解析

在linux用文本編輯器打開hello.s查看編譯結果


「實例+圖解」一文帶你徹底看懂linux


字符串表示以null結尾的字符串序列

3.31 數據

(1)字符串:


「實例+圖解」一文帶你徹底看懂linux

圖3.311 字符串

(2)整數 sleepsecs


「實例+圖解」一文帶你徹底看懂linux

圖3.312 整數 sleepsecs

3.32 賦值

(1) 全局變量sleepsecs =2


「實例+圖解」一文帶你徹底看懂linux

圖3.321

(2) 局部變量i =0


「實例+圖解」一文帶你徹底看懂linux


圖3.322 局部變量i =0

3.33 類型轉換

隱式類型轉換的是:int sleepsecs=2.5,將浮點數類型的2.5轉換為int類型

3.34 算術操作

「實例+圖解」一文帶你徹底看懂linux


圖3.340 算術操作符號


(1) 相加操作

<code>addq    $16, %rax
addq $8, %rax
/<code>
「實例+圖解」一文帶你徹底看懂linux


(2) 相減操作

<code>subq    $32, %rsp
/<code>


「實例+圖解」一文帶你徹底看懂linux



3.35 控制轉移

「實例+圖解」一文帶你徹底看懂linux


圖3.340 指令助記符

(1)比較i<10是否成立,若成立繼續循環,否則退出循環


「實例+圖解」一文帶你徹底看懂linux


圖3.341 i<10對應的彙編代碼

3.36 函數操作

a) int main(int argc, char *argv[])

(1)參數傳遞:從內核中獲取命令行參數和環境變量地址

(2)函數調用:內核執行程序時調用特殊的啟動例程,執行main函數

(3)函數返回:當命令行參數數量不為3時輸出提示信息並調用exit(1)退出main函數;當命令行參數數量為3執行循環和getchar函數後return 0的方式退出函數。

argc: 傳給main()的命令行參數個數argv: 命令行參數字符型指針數組的首地址

b) exit()

(1)參數傳遞:getchar()函數無參數

(2)函數傳遞:main函數通過call指令調用getchar()

(3)函數返回:返回值類型為int,如果成功返回用戶輸入的ASCII碼,出錯返回-1

c) printf()

「實例+圖解」一文帶你徹底看懂linux


圖3.363 printf彙編代碼

(1)參數傳遞:getchar()函數無參數(2)函數傳遞:main函數通過call指令調用getchar()(3)函數返回:返回值類型為int,如果成功返回用戶輸入的ASCII碼,出錯返回-1

d) sleep()


「實例+圖解」一文帶你徹底看懂linux

圖3.364 sleep彙編代碼


(1)參數傳遞:getchar()函數無參數(2)函數傳遞:main函數通過call指令調用getchar()(3)函數返回:返回值類型為int,如果成功返回用戶輸入的ASCII碼,出錯返回-1

e) getchar()


「實例+圖解」一文帶你徹底看懂linux

圖3.365 getchar彙編代碼


(1)參數傳遞:getchar()函數無參數(2)函數傳遞:main函數通過call指令調用getchar()(3)函數返回:返回值類型為int,如果成功返回用戶輸入的ASCII碼,出錯返回-1

3.37關係操作

(1)argc!=3


「實例+圖解」一文帶你徹底看懂linux


圖3.371 !=彙編代碼

(2)i<10


「實例+圖解」一文帶你徹底看懂linux


圖3.371

3.4 本章小結

本章介紹了編譯的概念與作用,展示了編譯命令的使用,對編譯結果解析,並說明編譯器處理C語言的各個數據類型以及各類操作的過程。

第4章 彙編

4.1 彙編的概念與作用

  1. 概念:把彙編語言翻譯成機器語言的過程稱為彙編。在彙編語言中,用助記符代替操作碼,用地址符號或標號代替地址碼。通過用符號代替機器語言的二進制碼,可以把機器語言變成彙編語言。
  2. 作用:將彙編語言翻譯成機器語言。

編譯 VS 彙編編譯:將高級語言程序變成計算機能識別的二進制語言彙編:將彙編語言翻譯成機器語言

4.2 在Ubuntu下彙編的命令

彙編的命令:as hello.s -o hello.o

「實例+圖解」一文帶你徹底看懂linux


圖4.2 在Ubuntu下彙編的命令


4.3 可重定位目標elf格式

分析hello.o的ELF格式,用readelf等列出其各節的基本信息,特別是重定位項目分析。

(1) ELF頭


「實例+圖解」一文帶你徹底看懂linux

圖4.311 ELF頭


ELF頭包括一個16字節的序列、ELF頭的大小、目標文件的類型(如可重定位、可執行或共享的)、機器類型(如x86-64)、節頭部表(section header table)的文件偏移,以及節頭部表中條目的大小和數量。其結構體表示:

define EI_NIDENT 16

typedef struct{  

unsigned char e_ident[EI_NIDENT];  

Elf32_Half e_type;  Elf32_Half e_machine;  

Elf32_Word e_version;  

Elf32_Addr e_entry;  

Elf32_Off e_phoff;  

Elf32_Off e_shoff;  

Elf32_Word e_flags;  

Elf32_Half e_ehsize;  

Elf32_Half e_phentsize;  

Elf32_Half e_phnum;  

Elf32_Half e_shentsize;  

Elf32_Half e_shnum;  

Elf32_Half e_shstrndx;

}Elf32_Ehdr;


數據格式:

「實例+圖解」一文帶你徹底看懂linux

圖4.312 數據格式

(2) 節頭部表:文件中出現的各個節的語義,包括節的類型、位置和大小


「實例+圖解」一文帶你徹底看懂linux

圖4.32 節頭部表

根據節頭部表可知,當號=1,符號在.text;當號=3,符號在.data,以此類推。三個特殊偽節:ABS:不該被重定位的符號,如main()函數。UND:其它文件中定義,本文件中引用的符號,如swap()函數。COM:還未分配位置的未初始化數據目標,如buf2,它最終放在.bss。(3) 重定位節

(a)普通重定位由以下數據結構定義:

typedef struct{Elf32_Addr r_offset; //指定需要重定位的項的位置Elf32_Word r_info; //提供了符號表中的一個位置,包括重定位類型信息。r_info == int symbol:24,type:8;} Elf32_Rel;

(b)在ELF定義了32種不同的重定位類型,其中最基本的兩種是:

R X86_ 64 PC32。 重定位一個使用32位PC相對地址的引用。一個PC相對地址就是距程序計數器(PC)的當前運行時值的偏移量。當CPU執行一條使用PC相對尋址的指令時,它就將在指令中編碼的32位值加上PC的當前運行時值,得到有效地址(如call指令的目標),PC值通常是下一條指令在內存中的地址。R X86_ 64 _32。 重定位一個使用32位絕對地址的引用。通過絕對尋址,CPU直接使用在指令中編碼的32位值作為有效地址,不需要進一步修改。

(c)代碼重定位條目放在.rel.text中。已經初始化數據的重定位條目放在.rel.data中。main.c源文件引用了一個全局sleepsecs符號。sleepsecs的重定位類型為相對重定位並且由圖4.33(1)可以得到:sleepsecs的r_offset : 000000000060重定位的字節處, 由圖4.33(2)可以得到:sleepsecs的大小為4個字節計算sleepsecs的重定位後的地址:Result = S-P+AA代表加數值,S是符號表中保存的符號的值,P代表重定位的位置偏移量


「實例+圖解」一文帶你徹底看懂linux


圖4.331重定位節

「實例+圖解」一文帶你徹底看懂linux

圖4.332

(4) 符號表:存放著程序中定義和引用函數和全局變量的信息,不包含局部變量的條目


「實例+圖解」一文帶你徹底看懂linux

圖4.34 符號表

Value:在對應節的偏移。Size:目標大小。Type:是數據或函數。Bind:本地或全局。Vis:預留。Ndx:符號所在的節,其實是節頭部表中條目的索引。Name:符號名,為空的為鏈接器內部使用的本地符號,可以忽略。

4.4 Hello.o的結果解析

用命令行得到,比較hello.objdump與hello.o,進行對照分析


「實例+圖解」一文帶你徹底看懂linux

圖4.40 命令行

(1) hello.objdump記錄了文件格式和.text代碼段:而hello.s中除了記錄了文件格式和.text代碼段還包括.type .size .align以及.rodata


「實例+圖解」一文帶你徹底看懂linux

圖4.41 hello.objdump與hello.s文件內容對比

(2) 分支轉移:

hello.objdump跳轉中地址為已確定的實際指令地址;hello.s跳轉中地址為助記符如.L2,通過使用例如.L2等的助記符進行跳轉。


「實例+圖解」一文帶你徹底看懂linux

圖4.42 hello.objdump與hello.s分支轉移對比

(3)函數調用在.s文件中,call的地址是函數名稱,如puts@PLT,而在反彙編程序中,call的目標地址是指令,如callq 21 <main>。因為hello.c中調用的函數都是共享庫中的函數,共享庫函數調用需要通過鏈接時重定位才能確定地址/<main>


「實例+圖解」一文帶你徹底看懂linux

圖4.43 hello.objdump與hello.s函數puts調用對比


(4)全局變量訪問
hello.objdump使用0+%rip訪問全局變量sleepsecs,如lea 0x0(%rip),%rdi。hello.s使用段名稱+%rip訪問全局變量sleepsecs,如leaq .LC0(%rip), %rdi

「實例+圖解」一文帶你徹底看懂linux

圖4.44 hello.objdump與hello.s全局變量sleepsecs訪問對比

4.5 本章小結

本章介紹了彙編的概念與作用,在linux下進行彙編的指令,可重定位目標文件elf的格式,將hello.o的結果解析與hello.s進行對照分析,分析了機器語言的構成以及與彙編語言的映射關係。

第5章 鏈接

5.1 鏈接的概念與作用

鏈接的概念:Linux 鏈接分兩種,一種被稱為硬鏈接(Hard Link),另一種被稱為符號鏈接(Symbolic Link)。默認情況下,ln 命令產生硬鏈接。

(1)硬連接指通過索引節點來進行連接。在 Linux 的文件系統中,保存在磁盤分區中的文件不管是什麼類型都給它分配一個編號,稱為索引節點號(Inode Index)。在 Linux 中,多個文件名指向同一索引節點是存在的。

(2)軟連接。軟鏈接文件是一個特殊的文件。在符號連接中,文件實際上是一個文本文件,其中包含的有另一文件的位置信息。作用:鏈接操作給系統中已有的某個文件指定另外一個可用於訪問它的名稱。我們可以為這個新的文件名指定不同的訪問權限。鏈用戶可以利用鏈接直接進入被鏈接的目錄。即使刪除這個鏈接,也不會破壞原來的目錄。硬連接的作用是允許一個文件擁有多個有效路徑名,用戶就可以建立硬連接到重要文件,以防止“誤刪”的功能。

5.2 在Ubuntu下鏈接的命令

使用ld的鏈接命令,應截圖,展示彙編過程! 注意不只連接hello.o文件

鏈接的命令:ld -o OUTPUT /lib/crt0.o hello.o –lc

鏈接的命令行: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

「實例+圖解」一文帶你徹底看懂linux

圖5.2 在Ubuntu下鏈接的命令

5.3 可執行目標文件hello的格式

分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。使用命令行readelf -a hello > hello1.elf生成hello1.elf文件節頭表中包含了各段的起始地址,大小等信息。

「實例+圖解」一文帶你徹底看懂linux

圖5.31 節頭表

「實例+圖解」一文帶你徹底看懂linux

圖5.32 節頭表

5.4 hello的虛擬地址空間

使用edb加載hello,查看本進程的虛擬地址空間各段信息,並與5.3對照分析說明。

(1) 分析程序頭部表。

PHDR:程序頭表

INTERP:程序執行前需要調用的解釋器

LOAD:程序目標代碼和常量信息

DYNAMIC:動態鏈接器所使用的信息

NOTE::輔助信息

GNU_EH_FRAME:保存異常信息

GNU_STACK:使用系統棧所需要的權限信息

GNU_RELRO:保存在重定位之後只讀信息的位置

VirtAddr:本段首字節的虛擬地址

PhysAddr指出本段首字節的物理地址

pFileSiz指出本段在文件中所佔的字節數,可以為0

MemSiz指出本段在存儲器中所佔字節數,可以為0

Flags指出存取權限,Align指出對齊方式


「實例+圖解」一文帶你徹底看懂linux

圖5.41 程序頭部表


(2) 在edb查看hello的虛擬地址空間的各段信息


「實例+圖解」一文帶你徹底看懂linux

圖5.42 hello的虛擬地址空間


(3) 程序頭與Datadump的映射關係:例如PHDR對應的虛擬內存地址是0x400000—— 0x4001c0


「實例+圖解」一文帶你徹底看懂linux

圖5.43 程序頭與Datadump的映射關係

5.5 鏈接的重定位過程分析

通過命令行objdump –d –r hello > hello.txt得到反彙編文件hello.txt。

(1) hello的反彙編結果與hello.o的反彙編結果相比,hello.txt多了以下節頭表:

_init 程序初始化代碼gmon_start call_gmon_start函數初始化gmon profiling system,程序通過gprof可以輸出函數調用等信息_dl_relocate_static_pie 靜態庫鏈接.plt 動態鏈接-過程鏈接表Puts(等函數)@plt 動態鏈接各個函數_start 編譯器為可執行文件加上了一個啟動例程__libc_csu_init 程序調用libc庫用來對程序進行初始化的函數,一般先於main函數執行_fini 當程序正常終止時需要執行的代碼

(2) 函數個數:在使用ld命令鏈接的時候,指定了動態鏈接器為64的/lib64/ld-linux-x86-64.so.2,crt1.o、crti.o、crtn.o中主要定義了程序入口_start、初始化函數_init,_start程序調用hello.c中的main函數,libc.so是動態鏈接共享庫,鏈接器加入了以下函數printf、sleep、getchar、exit函數和_start中調用的__libc_csu_init,__libc_csu_fini,__libc_start_main。

函數調用:鏈接器解析重定條目時發現對外部函數調用的類型為R_X86_64_PLT32的重定位,此時動態鏈接庫中的函數已經加入到了PLT中,.text與.plt節相對距離已經確定,鏈接器計算相對距離,將對動態鏈接庫中函數的調用值改為PLT中相應函數與下條指令的相對地址,指向對應函數。rodata引用:鏈接器解析重定條目時發現兩個類型為R_X86_64_PC32的對.rodata的重定位(printf中的兩個字符串),.rodata與.text節之間的相對距離確定,因此鏈接器直接修改call之後的值為目標地址與下一條指令的地址之差,指向相應的字符串。

(3) 重定位過程。hello反彙編文件中對應全局變量已通過重定位絕對引用被替換為固定地址。

5.6 hello的執行流程

(以下格式自行編排,編輯時刪除)使用edb執行hello,說明從加載hello到_start,到call main,以及程序終止的所有過程。請列出其調用與跳轉的各個子程序名或程序地址。

ld-2.27.so!_dl_start

ld-2.27.so!_dl_init

hello!_start

libc-2.27.so!__libc_start_main

libc-2.27.so!__cxa_atexit

libc-2.27.so!__libc_csu_init

hello!_init

libc-2.27.so!_setjmp

libc-2.27.so!_sigsetjmp

libc-2.27.so!__sigjmp_save

hello!main

hello!puts@plt

hello!exit@plt

hello!printf@plt

hello!sleep@plt

hello!getchar@plt

ld-2.27.so!_dl_runtime_resolve_xsave

ld-2.27.so!_dl_fixup

ld-2.27.so!_dl_lookup_symbol_x

libc-2.27.so!exit

5.7 Hello的動態鏈接分析

(1)編譯器無法確定動態鏈接庫中的函數地址,因為動態鏈接庫中的函數在程序執行的時候才會確定地址。GNU編譯系統採用延遲綁定技術來解決動態庫函數模塊調用的問題。

(2)延遲綁定通過全局偏移量表(GOT)和過程鏈接表(PLT)實現。

(a)PLT是一個數組,其中每個條目是16字節代碼。每個庫函數都有自己的PLT條目,PLT[0]是一個特殊的條目,跳轉到動態鏈接器中。從PLT[2]開始的條目調用用戶代碼調用的函數。

(b)GOT同樣是一個數組,每個條目是8字節的地址,和PLT聯合使用時,GOT[2]是動態鏈接在ld-linux.so模塊的入口點,其餘條目對應於被調用的函數,在運行時被解析。每個條目都有匹配的PLT條目。

(3)延遲綁定的實現步驟如下:a.建立一個 GOT.PLT 表,用來放全局函數的實際地址b.對每一個全局函數,鏈接器生成一個與之相對應的函數,如 puts@plt。c.所有的puts都換成對 puts@plt。

(4)下面分析在dl_init調用前後,項目的內容的變化a)dl_init調用前GOT條目


「實例+圖解」一文帶你徹底看懂linux

圖5.71

b)dl_init調用後, GOT條目初始時指向其PLT條目的第二條指令的地址


「實例+圖解」一文帶你徹底看懂linux

圖5.72 dl_init調用後GOT條目

5.8 本章小結

本章介紹了鏈接的概念作用,分析hello的ELF格式和虛擬地址空間,通過實例分析了hello的動態鏈接、執行流程、重定位過程、加載以及運行時函數調用順序,深入理解鏈接和重定位的過程。

第6章 hello的進程管理

6.1 進程的概念與作用

進程的概念:進程是正在運行的程序的實例,是一個具有一定獨立功能的程序關於某個數據集合的一次運行活動。進程是操作系統動態執行的基本單元,在傳統的操作系統中,進程既是基本的分配單元,也是基本的執行單元。進程的作用:進程提供兩個假象,程序獨佔地使用處理器和程序在獨佔地使用系統內存。

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

Shell-bash的作用:shell和其他軟件一樣都是和內核打交道,直接服務於用戶。但和其他軟件不同,shell主要用來管理文件和運行程序。處理流程:shell對命令行的處理流程(1)讀取輸入的命令行。(2)解析引用並分割命令行為各個單詞,其中重定向所在的單詞會被保存下來,直到擴展步驟(5)結束後才進行相關處理。(3)檢查命令行結構。(4)對第一個單詞進行別名擴展。(5)進行各種擴展。擴展順序為:大括號擴展;波浪號擴展;參數、變量和命令替換、算術擴展;單詞拆分;文件名擴展。(6)引號去除。(7)搜索和執行命令。(8)返回退出狀態碼。

6.3 Hello的fork進程創建過程

普通的系統調用,調用一次就返回一次,而fork()調用一次,會返回兩次,一次是父進程,另一個是子進程,互不干擾,調用的先後順序由操作系統的調度算法決定。子進程永遠返回0,父進程則返回子進程的ID。

fork的進程圖為:


「實例+圖解」一文帶你徹底看懂linux

圖6.3 fork的進程圖

6.4 Hello的execve過程

execve 函數加載並運行可執行目標文件 filename, 且帶參數列表 argv 和環境變量列表 envp 。只有當出現錯誤時,例如找不到 filename, execve 才會返回到調用程序。所以,與 fork 一次調用返回兩次不同, execve 調用一次並從不返回。

6.5 Hello的進程執行

結合進程上下文信息、進程時間片,闡述進程調度的過程,用戶態與核心態轉換等等。

(1)上下文及上下文切換:進程的物理實體(代碼和數據等)和支持進程運行的環境。系統通過處理器調度讓處理器輪流執行多個進程,實現不同進程中指令交替執行的機制稱為進程的上下文切換。

(2)進程時間片:連續執行同一個進程的時間段稱為時間片

(3)用戶態與核心態轉換:處理器通過某個控制寄存器中的一個模式位來提供限制一個應用可以執行的指令以及它可以訪問的地址空間範圍的功能。當設置了模式位時,進程就運行在內核模式中。沒有設置模式位時,進程就運行在用戶模式中。

(4)Hello進程調度的過程以及用戶態與核心態的轉換調度是在進程執行的某些時刻,內核可以決定搶佔當前進程並重新開始一個先前被搶佔了的進程的決策。在切換的第一部分中,內核代表進程A在內核模式下執行指令。然後在某一時刻,shell加載可執行目標文件hello。在上下文切換之後,內核代表進程hello在用戶模式下執行指令。之後進程hello在用戶模式下運行,直到磁盤發出一箇中斷信號,執行一個從進程hello到進程A的上下文切換,將控制返回給進程A,進程A繼續運行,直到下一次異常發生。


「實例+圖解」一文帶你徹底看懂linux

圖6.5 Hello進程調度的過程以及用戶態與核心態的轉換

6.6 hello的異常與信號處理

hello執行過程中會出現哪幾類異常,會產生哪些信號,又怎麼處理的。程序運行過程中可以按鍵盤,如不停亂按,包括回車,Ctrl-Z,Ctrl-C等,Ctrl-z後可以運行ps jobs pstree fg kill 等命令,請分別給出各命令及運行結截屏,說明異常與信號的處理。Hello執行過程出現的異常為:中斷、故障會產生的信號為:SIGSTP 來自終端的停止信號,SIGINT 來自鍵盤的中斷

(1) 正常終止


「實例+圖解」一文帶你徹底看懂linux

圖6.61 正常終止

(2) Ctrl + C當按下ctrl-c之後,shell父進程收到SIGINT信號,信號處理程序結束hello,並回收hello進程。


「實例+圖解」一文帶你徹底看懂linux

圖6.62 按Ctrl + C時

(3) Ctrl + Z當按下ctrl-z之後,(a)shell父進程收到SIGSTP信號,(b)信號處理程序打印並將hello進程掛起,(c)通過ps命令看到hello進程沒有被回收,通過jobs命令看到hello進程的號為1,通過pstree命令可以看出:之後調用fg 1將其調到前臺,執行相應命令行


「實例+圖解」一文帶你徹底看懂linux

圖6.63 按Ctrl + Z時

(4) 中途亂按中途亂按不導致異常和產生信號


「實例+圖解」一文帶你徹底看懂linux

圖6.64 中途亂按時

6.7 本章小結

本章首先介紹了進程的概念與作用,並簡述殼Shell-bash的作用與處理流程,講解了Hello的fork進程創建過程和execve過程,以及Hello的進程是如何執行的,如何處理hello的異常與產生的信號

第7章 hello的存儲管理

7.1 hello的存儲器地址空間

(1) 邏輯地址:是指由程式產生的和段相關的偏移地址部分。在反彙編hello得到的調用puts函數的指令是call 21<main>,邏輯地址是[puts的代碼的段標識符:21<main>]/<main>/<main>


「實例+圖解」一文帶你徹底看懂linux

圖7.1 puts函數的地址

(2) 線性地址:是邏輯地址到物理地址變換之間的中間層。程式代碼會產生邏輯地址,或說是段中的偏移地址,加上相應段的基地址就生成了一個線性地址。如果啟用了分頁機制,那麼線性地址能再經變換以產生一個物理地址。若沒有啟用分頁機制,那麼線性地址直接就是物理地址。

(3)虛擬地址:也叫線性地址,是一個不真實的地址。

(4)物理地址:是指出目前CPU外部地址總線上的尋址物理內存的地址信號,是地址變換的最終結果地址,用於內存芯片級的單元尋址,與地址總線相對應。

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

「實例+圖解」一文帶你徹底看懂linux

圖7.2 邏輯地址到線性地址的變換-段式管理

1)基本原理。在段式存儲管理中,將程序的地址空間劃分為若干個段,在段式存儲管理系統中,為每個段分配一個連續的分區,而進程中的各個段可以不連續地存放在內存的不同分區中。程序加載時,操作系統為所有段分配其所需內存,物理內存的管理採用動態分區的管理方法。在為某個段分配物理內存時,可以採用首先適配法、下次適配法、最佳適配法等方法。在回收某個段所佔用的空間時,要注意將收回的空間與其相鄰的空間合併。段式存儲管理也需要硬件支持,實現邏輯地址到物理地址的映射。2)段式管理的數據結構。為了實現段式管理,操作系統需要進程段表、系統段表和空閒段表來實現進程的地址空間到物理內存空間的映射。3)段式管理的地址變換。在段式管理系統中,其邏輯地址由段號和段內地址兩部分組成。處理器會查找內存中的段表,由段號得到段的首地址,加上段內地址,得到實際的物理地址,從而完成邏輯地址到物理地址的映射。

7.3 Hello的線性地址到物理地址的變換-頁式管理

(1)頁式存儲管理的基本原理:1)分頁存儲器將主存劃分成多個大小相等的頁架;2)程序的邏輯地址分成頁;3)不同的頁可以放在不同頁架中,不需要連續4)頁表用於維繫進程的主存完整性


「實例+圖解」一文帶你徹底看懂linux

圖7.31 進程頁表

(2)頁式存儲管理的邏輯地址由兩部分組成:


「實例+圖解」一文帶你徹底看懂linux

圖7.32頁式存儲管理的邏輯地址
(3) 頁式存儲管理的物理地址由兩部分組成:

「實例+圖解」一文帶你徹底看懂linux

圖7.33 頁式存儲管理的物理地址


(4) 頁式存儲管理的地址轉換思路:

「實例+圖解」一文帶你徹底看懂linux

圖7.34 從邏輯地址映射到物理地址

(5) 頁的共享:頁式存儲管理能夠實現多個進程共享程序和數據,包括數據共享和程序共享

(6)頁式虛擬存儲管理的基本思想:把進程全部頁面裝入虛擬存儲器,執行時先把部分頁面裝入實際內存,然後根據執行行為,動態調入不在主存的頁,同時進行必要的頁面調出7.4 TLB與四級頁表支持下的VA到PA的變換


「實例+圖解」一文帶你徹底看懂linux

圖7.4 VA到PA的映射過程


(1) 首先介紹以下VA和PA。VA:virtual address稱為虛擬地址,PA:physical address稱為物理地址。MMU是內存管理單元。MMU將VA翻譯成為PA發到CPU芯片的外部地址引腳上,也就是將VA映射到PA中。MMU將VA映射到PA是以頁為單位的,對於32位的CPU,通常一頁為4k,物理內存中的一個物理頁面稱頁為一個頁框。


(2) TLB與四級頁表支持下的VA到PA的變換和TLB與二級頁表支持下的VA到PA的變換的原理相同,為了結合實例分析,下面介紹二級頁表的變換。如圖7.4,首先將CPU內核發送過來的32位VA[31:0]分成三段,前兩段VA[31:20]和VA[19:12]作為兩次查表的索引,第三段VA[11:0]作為頁內的偏移,查表的步驟如下:
a)從協處理器CP15的寄存器2(TTB寄存器)中取出保存在其中的第一級頁表的基地址PA
b)以TTB中的內容為基地址,以VA[31:20]為索引值在一級頁表中查找出一項,一級頁表中保存著第二級頁表的基地址。
c)以VA[19:12]為索引值在第二級頁表中查出一項,第二級頁表中保存著物理頁面的基地址,從這裡可以印證一個虛擬內存的頁映射到一個物理內存的頁框,因為查表是以頁為單位來查的。
d)有了物理頁面的基地址之後,加上VA[11:0]偏移量就可以取出相應地址上的數據

7.5 三級Cache支持下的物理內存訪問


「實例+圖解」一文帶你徹底看懂linux

圖7.5 CPU訪問內存時的硬件操作順序

(1) 以VA為索引到cache中查找是否緩存了要讀取的數據,如果cache中已經緩存了該數據則直接返回給CPU內核,如果cache中沒有緩存該數據,則發出PA從物理內存中讀取數據並緩存到cache中,同時返回給CPU內核。cache不只是緩存CPU內核所需要的數據,同時緩存相鄰的數據。

(2) 高速緩存確定一個請求是否命中,然後抽取出被請求的字的過程,分為三步,(1)組選擇、(2)行匹配、(3)字抽取。下面是物理內存的讀策略和寫策略。

a) 直接映射高速緩存E=1,即每組只有一行。組選擇是通過組索引位標識組。高速緩存從w的地址中間抽取出s個組索引位,這些位被解釋為一個對應於一個組號的無符號整數,來進行組索引。行匹配中,確定了某個組i,接下來需要確定是否有字w的一個副本存儲在組i包含的一個高速緩存行裡,因為直接映射高速緩存只有一行,如果有效位為1且標誌位相同則緩存命中,根據塊偏移位即可查找到對應字的地址並取出;若有效位為1但標誌位不同則衝突不命中,有效位為0則為冷不命中,此時都需要從存儲器層次結構下一層取出被請求的塊,然後將新的塊存儲在組索引位指示的組中的一個高速緩存行中。

b) 組相聯高速緩存每個組都會保存多餘一個的高速緩存行,組選擇與直接映射高速緩存的組選擇一樣,通過組索引位標識組。行匹配時需要找遍組中所有行,找到標記位有效位均相同的一行則緩存命中;如果CPU請求的字不在組的任何一行中,則緩存不命中,選擇替換時如果存在空行選擇空行,如果不存在空行則通過替換策略替換其中一行。

c) 全相聯高速緩存只包含一個組,其行匹配和字選擇與組相聯高速緩存中一樣

d)寫策略:分為直寫和寫回。

7.6 hello進程fork時的內存映射

函數fork()若成功調用一次則返回兩個值,子進程返回0,父進程返回子進程ID。內核為子進程創建各種數據結構,並分配給它一個唯一的PID,新創建的子進程獲得與父進程完全相同的虛擬存儲空間中的一個備份這個進程的每個頁面都標記為只讀。

7.7 hello進程execve時的內存映射

「實例+圖解」一文帶你徹底看懂linux

圖7.7進程的內存映像

execve函數加載並運行hello需要以下幾個步驟:1.刪除已存在的用戶區域2.映射私有區域,為新程序創建所有新的區域結構3.映射共享區域4.設置當前進程上下文的程序計數器

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

(1)缺頁中斷及處理:在主存中查找頁表時相應頁表條目有效位為0且物理頁號為NULL,則該頁表條目處於未分配,屬於缺頁中斷。缺頁中斷的異常處理程序為終止

(2)缺頁故障及處理:在主存中查找頁表時相應頁表條目有效位為0但是物理頁號指向磁盤,屬於缺頁故障,缺頁故障的異常處理程序是從磁盤裝入相應頁到內存並更新頁表,再返回到故障指令開始執行。

7.9 動態存儲分配管理

「實例+圖解」一文帶你徹底看懂linux

圖7.9塊的表示圖

(1)實現動態內存分配要考慮空閒塊組織、放置、分割和合並。

(2)分配器分為兩種:顯式分配器、隱式分配器。顯式分配器:要求應用顯式地釋放任何已分配的塊。隱式分配器:要求分配器檢測一個已分配塊何時不再使用,那麼就釋放這個塊,自動釋放未使用的已經分配的塊的過程叫做垃圾收集。隱式空閒鏈表的優點是簡單,缺點是任何操作的開銷,例如放置分配的塊,要求空閒鏈表的搜索與堆中已分配塊和空閒塊的總數呈線性關係。

(3)當接收到一個內存分配請求時,從頭開始遍歷堆,找到一個空閒的滿足大小要求的塊,若有剩餘,將剩餘部分變成一個新的空閒塊,更新相關塊的控制信息。調整起始位置,返回給用戶。釋放內存時,僅需把使用情況標記為空閒即可。(4)搜索可以滿足請求的空閒塊時,策略有以下幾種:首次適應法、最佳適應法、最壞適應法和循環首次適應法

7.10 本章小結

本章結合hello介紹了邏輯地址、線性地址、虛擬地址、物理地址的概念,對段式管理與頁式管理進行比較分析,分析了進程 fork 和 execve 時的內存映射的內容,描述了系統應對缺頁異常的方法,最後描述了 malloc 的內存分配管理機制

第8章 hello的IO管理

8.1 Linux的IO設備管理方法

IO設備管理方法:一個Linux文件就是一個m字節的序列:B1,B2,……,Bk,……,Bm-1。所有的I/O設備(例如網絡、磁盤和終端)都被模型化為文件,而所有的輸入和輸出都被當做對相應文件的讀和寫來執行。這種將設備優雅地映射為文件的方式,允許Linux內核引出一個簡單、低級的應用接口,稱為Unix I/O。這使得所有輸入和輸出都能以一種統一且一致的方式來執行:a)打開文件b)Linux Shell創建的每個進程開始時都有三個打開的文件:標準輸入、標準輸出、標準錯誤。c)改變當前文件的位置。d)讀寫文件。e)關閉文件。

8.2 簡述Unix IO接口及其函數

(1) 打開和關閉文件。open函數的函數原型是int open(char * path,int flags,mode_t mode) open函數將filename轉換成一個文件描述符,並返回描述符數字。進程是通過調用open函數來打開一個已經存在的文件或者創建一個新文件。最後進程通過調用close函數關閉一個打開的文件。close函數原型是int close(int fd)(2)讀和寫文件應用程序是通過分別調用read和write函數來執行輸入和輸出的。read函數函數原型是ssize_t read(int fd ,void* buf , size_t n),從描述符為fd的當前文件位置複製最多n個字節到內存位置buf。write函數函數原型是ssize_t write(int fd , const void* buf,size_t n),從內存位置buf複製最多n個字節到描述符為fd的當前文件位置。(3)lseek函數off_t lseek(int fd, off_t offset , int whence)應用程序通過lseek函數能夠顯示地修改當前文件的位置

8.3 printf的實現分析

(1)首先來看看printf函數的函數體


「實例+圖解」一文帶你徹底看懂linux


va_list是一個字符指針

(2)printf函數中調用了vsprintf函數,來看看vsprintf(buf, fmt, arg)的代碼

<code>int vsprintf(char *buf, const char *fmt, va_list args)  

{
char* p;
char tmp[256];
va_list p_next_arg = args;

for (p=buf;*fmt;fmt++) {
if (*fmt != '%') {
*p++ = *fmt;
continue;
}
fmt++;
switch (*fmt) {
case 'x':
itoa(tmp, *((int*)p_next_arg));
strcpy(p, tmp);
p_next_arg += 4;
p += strlen(tmp);
break;
case 's':
break;
default:
break;
}
}
return (p - buf);
}
/<code>

vsprintf返回的是要打印出來的字符串的長度

(3)然後看printf中的一句:write(buf, i);printf函數調用了write函數,把緩衝區的元素的值打印。

<code>write: 
mov eax, _NR_write
mov ebx, [esp + 4]
mov ecx, [esp + 8]
int INT_VECTOR_SYS_CALL
/<code>

在write () 函數對應的指令序列中,有用於系統調用的陷阱指令system_call。write通過執行syscall指令調用系統服務,執行打印操作。內核會通過字符顯示子程序,根據傳入的ASCII碼到字模庫讀取字符對應的點陣,然後通過vram對字符串進行輸出。顯示芯片將按照刷新頻率逐行讀取vram,並通過信號線向液晶顯示器傳輸每一個點,最終在終端輸出字符串。

8.4 getchar的實現分析

(1) 首先來看一下getchar函數:getchar由宏實現:#define getchar() getc(stdin),從標準輸入裡讀取下一個字符,返回類型為int型,為用戶輸入的ASCII碼或EOF。

<code>int getchar(void)
{
static char buf[BUFSIZ];
static char* bb=buf;
static int n=0;
if(n==0)
{
n=read(0,buf,BUFSIZ);
bb=buf;
}
return(--n>=0)?(unsigned char)*bb++:EOF;
}
/<code>

(2)可以看到n=read(0,buf,BUFSIZ);語句調用了read函數。read函數可以通過sys_call中斷來調用內核中的系統函數。鍵盤中斷處理子程序會接受按鍵掃描碼並將其轉換為ASCII碼後保存在緩衝區,然後對緩衝區ASCII碼進行讀取直到接受回車鍵返回。

8.5 本章小結

本章介紹了Linux的IO設備管理方法,並簡述了Unix IO接口及其函數,對printf和getchar的實現進行分析。

結論

在linux環境下hello程序從預處理到編譯再到鏈接,最後執行的全過程以及進程管理,存儲管理及IO管理的實現方式。hello經歷的過程如下:

(1)首先通過各種預處理命令對C程序進行處理,由hello.c得到hello.i。

(2)通過編譯由hello.i得到hello.s

(3)通過彙編由hello.s得到hello.o

(4)通過鏈接得到可執行目標文件hello,然後運行hello,在shell下輸入命令./hello 1170300826 ,shell調用fork創建子進程,然後將構造好的參數列表傳給execve作為參數,啟動加載器並開始執行hello

(5)訪問虛擬內存,通過虛擬地址在TLB和主存頁表中查找轉換為相應物理地址,從在虛擬內存中讀取hello程序所需要的數據

(6)異常處理,對中斷產生信號進行處理

(7)回收回收進程

附件

列出所有的中間產物的文件名,並予以說明起作用。


「實例+圖解」一文帶你徹底看懂linux

參考文獻

為完成本次大作業你翻閱的書籍與網站等

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

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

[3] 關於unix系統接口 普通文件io的小結

https://www.cnblogs.com/chentest/p/5448483.html

[4] printf 函數實現的深入剖析

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

https://baike.baidu.com/item/getchar/919709?fr=aladdin

[6] LINUX 邏輯地址、線性地址、物理地址和虛擬地址https://www.cnblogs.com/zengkefu/p/5452792.html

[7] shell解析命令行的過程以及eval命令

https://www.cnblogs.com/f-ck-need-u/p/7426371.html


分享到:


相關文章: