HIT计算机系统大作业——hello的一生
摘 要
本文通過一個結構簡單的hello程序,說明了由文本到可執行的文件,再到一個進程的整個過程中,計算機系統都起到了怎樣的作用。在這一過程中,我們會接觸到程序預處理、編譯、匯編、鏈接等過程,同時也介紹了有關進程、存儲、I/O等相關的知識,深入系統底層的軟硬件結合的部分,對計算機系統進行了較為系統和廣泛的探討。
第1章 概述
1.1 Hello簡介
Hello.c是一個以字節序列方式儲存在文件中的源程序(Program),經過預處理器(cpp)變為Hello.i,然后在編譯器(ccl)的處理后變為Hello.s的匯編程序,然后通過匯編器(as),生成了Hello.o的可重定位目標程序(二進制),然后在鏈接器(ld)的鏈接后,生成可執行的Hello目標程序。在編譯系統之后,調用shell命令行輸入,fork產生子進程,Hello從program成為Process計算機完成了From Program to Process的過程:
1.2 環境與工具
1.2.1 硬件環境
AMD Ryzen 7 5800H 3.2GHz; 16G RAM;
1.2.2 軟件環境
Windows 11 21H2; VMware Workstation 16.2.3; Ubantu 20.04
1.2.3 開發工具
Visual Studio 2022; Codeblocks 20.03
1.3 中間結果
列出你為編寫本論文,生成的中間結果文件的名字,文件的作用等。
1.4 本章小結
本章主要介紹了Hello作為一個程序、進程經歷的生命周期,解釋了何為程序的P2P、020,記錄了實驗時的軟硬件環境,以及生成的中間文件。
第2章 預處理
2.1 預處理的概念與作用
預處理是預處理器(cpp)根據#開頭的命令,修改原始的C程序,將源程序及引用的庫合并成完整的文件,得到了另一個C程序,一般以.i作為文件擴展名。
C語言的預處理主要有三個方面的內容:
1.宏定義 #define;
2.文件包含 #include;
3.條件編譯 #if #else #elif #ifndef #ifdef等;[1]
預處理的作用是從系統的頭文件包中將頭文件的源碼插入到目標文件中,在編譯代碼前首先將標識符替換好,確保程序的完整性,生成.i文件后再進行接下來的編譯工作。
2.2在Ubuntu下預處理的命令
在命令行窗口中使用命令gcc -m64 -no-pie -fno-PIC -E hello.c > hello.i,生成hello.i文件,截圖如下:
2.3 Hello的預處理結果解析
hello.c經過預處理后得到了hello.i文件,其文本量大大增加,達到了三千余行,其中main函數處于文檔的尾部。可以發現,該文本文件已經解析了hello.c中引用的頭文件,在文本的前幾行,我們可以看到如下內容:
接下來還有很多對于庫中預置的函數的定義:
2.4 本章小結
本章主要介紹了對.c文件的預處理,通過對于生成的.i文件的分析,我們可以了解到預處理器對于源程序做了哪些處理——例如對于宏的替換,引入各種需要的頭文件等。
第3章 編譯
3.1 編譯的概念與作用
編譯,就是編譯器通過詞法分析和語法分析,確認所有指令都是合法的,然后將其翻譯為等價的匯編代碼。
編譯器會將.i文件翻譯為.s文本文件,其中包含了一個匯編語言程序。匯編語言是一種通用的、接近底層的語言,即使是不同高級語言的不同編譯器,最后也會得到通用的匯編語言,進而進行下一步的實現。
3.2 在Ubuntu下編譯的命令
3.3 Hello的編譯結果解析
此部分是重點,說明編譯器是怎么處理C語言的各個數據類型以及各類操作的。應分3.3.1~ 3.3.x等按照類型和操作進行分析,只要hello.s中出現的屬于大作業PPT中P4給出的參考C數據與操作,都應解析。
3.3.1開始的聲明
在文本的開始,有如下的聲明
其中值得我們注意的是.section .rodata聲明了接下來的內容是只讀類型的,該聲明用于維護只讀數據,比如:常量字符串、帶 const 修飾的全局變量和靜態變量等[2],在本程序中,需要打印的字符串在.rodata中聲明;而.text則是聲明是程序代碼段;.align 8聲明了本匯編語言程序是以8個字節的倍數來進行內存對齊。
3.3.2 轉移控制
在main函數中,匯編語言用.cfi_startproc和.cfi_endproc聲明了函數的起始,其中涉及了條件跳轉語句,例如對于C語言中的if (argc != 4),有如下匯編語句,其中%rbp中存儲位置處是常量4:
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $4, -20(%rbp)
je .L2
對于C程序中for循環中的循環語句for(i=0;i<8;i++)
匯編代碼為
.L3:
cmpl $7, -4(%rbp)
jle .L4
3.3.3 數據
初始化的全局變量儲存在.data節,它的初始化不需要匯編語句,而是直接完成的。而局部變量存儲在寄存器或棧中。在該程序中的局部變量i就用到了棧,匯編代碼如下:
.L2:
movl $0, -4(%rbp)
jmp .L3
3.3.4 算術操作
程序的for循環中有自加操作符,在匯編中每次循環都通過跳轉指令實現,循環體中就包含了addl $1, -4(%rbp),對棧中存儲的i加1
3.3.5數組/指針/結構操作
main函數的參數中含有指針數組char *argv[],在argv數組中,argv[0]指向輸入程序的路徑和名稱,argv[1]和argv[2]分別表示兩個字符串,我們不妨以它們為例,追蹤指針數組存儲在程序中的哪部分。
.LFB6:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp) //argc存儲在edi中
movq %rsi, -32(%rbp)//argv存儲在rsi中
cmpl $4, -20(%rbp)
je .L2
movl $.LC0, %edi
call puts
movl $1, %edi
call exit
棧中%rsi-8和%rax-16的位置,分別存儲著argv[1]和argv[2]兩個字符串。
3.3.6 函數操作
main函數:
參數傳遞:傳入參數argc和argv[],分別位于寄存器rdi和rsi中。
函數調用:被系統啟動函數調用。
函數返回:設置%eax為0并且返回,對應return 0
exit函數:
參數傳遞:傳入的參數為1,再執行退出命令
函數調用:if判斷條件滿足后被調用
函數返回:退出程序,正確退出,返回0;出現錯誤,返回非0值
sleep函數:
參數傳遞:傳入參數atoi(argv[3]),
函數調用:for循環下被調用,call sleep
匯編代碼如下:
.L4:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
movl $.LC1, %edi
movl $0, %eax
call printf
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rax
movq %rax, %rdi
call atoi
movl %eax, %edi
call sleep
addl $1, -4(%rbp)
3.4 本章小結
本章主要分析了hello.s匯編語言文本文件的內容,分析了編譯器對于源程序的操作,深入分析了匯編文件中是如何實現C語言的數據與操作的。
第4章 匯編
4.1 匯編的概念與作用
匯編是編譯后的文件到生成機器語言二進制程序的過程,機器語言指令被打包成可重定位目標程序的格式。
4.2 在Ubuntu下匯編的命令
4.3 可重定位目標elf格式
可以使用指令:readelf -a hello.o > hello_0.elf得到.elf的文件。
ELF頭如下:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
類別: ELF64
數據: 2 補碼,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
類型: REL (可重定位文件)
系統架構: Advanced Micro Devices X86-64
版本: 0x1
入口點地址: 0x0
程序頭起點: 0 (bytes into file)
Start of section headers: 1192 (bytes into file)
標志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 13
節頭如下:
[號] 名稱 類型 地址 偏移量
大小 全體大小 旗標 鏈接 信息 對齊
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
000000000000008e 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 00000358
00000000000000c0 0000000000000018 I 11 1 8
[ 3] .data PROGBITS 0000000000000000 000000ce
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .bss NOBITS 0000000000000000 000000ce
0000000000000000 0000000000000000 WA 0 0 1
[ 5] .rodata PROGBITS 0000000000000000 000000d0
0000000000000033 0000000000000000 A 0 0 8
[ 6] .comment PROGBITS 0000000000000000 00000103
000000000000002c 0000000000000001 MS 0 0 1
[ 7] .note.GNU-stack PROGBITS 0000000000000000 0000012f
0000000000000000 0000000000000000 0 0 1
[ 8] .note.gnu.propert NOTE 0000000000000000 00000130
0000000000000020 0000000000000000 A 0 0 8
[ 9] .eh_frame PROGBITS 0000000000000000 00000150
0000000000000038 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 00000418
0000000000000018 0000000000000018 I 11 9 8
[11] .symtab SYMTAB 0000000000000000 00000188
0000000000000198 0000000000000018 12 10 8
[12] .strtab STRTAB 0000000000000000 00000320
0000000000000032 0000000000000000 0 0 1
[13] .shstrtab STRTAB 0000000000000000 00000430
0000000000000074 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
重定位的信息包括了類型和偏移量,在鏈接階段會用到這些信息來進行地址的計算,需要進行重定位的信息包括了.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar等函數
重定位節如下:
重定位節 ‘.rela.text’ at offset 0x358 contains 8 entries:
偏移量 信息 類型 符號值 符號名稱 + 加數
00000000001a 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
00000000001f 000b00000004 R_X86_64_PLT32 0000000000000000 puts - 4
000000000029 000c00000004 R_X86_64_PLT32 0000000000000000 exit - 4
000000000050 00050000000a R_X86_64_32 0000000000000000 .rodata + 26
00000000005a 000d00000004 R_X86_64_PLT32 0000000000000000 printf - 4
00000000006d 000e00000004 R_X86_64_PLT32 0000000000000000 atoi - 4
000000000074 000f00000004 R_X86_64_PLT32 0000000000000000 sleep - 4
000000000083 001000000004 R_X86_64_PLT32 0000000000000000 getchar - 4
重定位節 ‘.rela.eh_frame’ at offset 0x418 contains 1 entry:
偏移量 信息 類型 符號值 符號名稱 + 加數
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
.symtab是一個符號表,其中存放著程序中定義和引用的函數和全局變量的信息:
Symbol table ‘.symtab’ contains 17 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
9: 0000000000000000 0 SECTION LOCAL DEFAULT 6
10: 0000000000000000 142 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND exit
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND atoi
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sleep
16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND getchar
4.4 Hello.o的結果解析
objdump -d -r hello.o 分析hello.o的反匯編,并請與第3章的hello.s進行對照分析。
分析hello.o的反匯編,并與第3章的 hello.s進行對照分析:
操作數:hello.s中的操作數采用十進制來表示,而hello.o的反匯編代碼中的操作數采用了十六進制。
分支轉移:在hello.s的跳轉語句中采用的是.L2和.LC1等段的名稱,而反匯編代碼中跳轉指令之后是間接地址,是每條語句之間相對偏移的地址。
函數調用:hello.s中,call指令直接使用了函數名稱,而反匯編代碼中call指令使用的是相對于main函數的偏移地址。同時在.rela.text節中為其添加了重定位條目,待在鏈接之后確定物理地址。
4.5 本章小結
本章主要介紹了匯編后程序的文本文件被轉化為可重定位目標程序的過程,然后通過ELF文件格式分析了我們得到的.o文件,然后通過對所得的.o文件進行反匯編,發現了經過匯編后程序的改動——用邏輯地址的相對偏移來約定跳轉指令和call指令。
第5章 鏈接
5.1 鏈接的概念與作用
鏈接是將各種代碼和數據片段收集并組合成一個單一文件的過程,這個文件可被復制到內存并執行。
鏈接使得分離編譯成為可能,我們不用將一個大型的應用程序組織為一個巨大的源文件,而是可以把它分解為更小、更好管理的模塊,并對它們進行獨立地修改和編譯。
5.2 在Ubuntu下鏈接的命令
5.3 可執行目標文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
在命令行中鍵入命令readelf -a hello > hello_1.elf得到hello的ELF格式。
ELF 頭:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
類別: ELF64
數據: 2 補碼,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
類型: EXEC (可執行文件)
系統架構: Advanced Micro Devices X86-64
版本: 0x1
入口點地址: 0x4010f0
程序頭起點: 64 (bytes into file)
Start of section headers: 14208 (bytes into file)
標志: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 12
Size of section headers: 64 (bytes)
Number of section headers: 27
Section header string table index: 26
節頭描述了各個節的大小、偏移量以及其他屬性。鏈接時,會將各個文件的相同段進行合并,并且根據得到的段的大小以及偏移量重新設置各個符號的地址。:
[號] 名稱 類型 地址 偏移量
大小 全體大小 旗標 鏈接 信息 對齊
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 00000000004002e0 000002e0
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.propert NOTE 0000000000400300 00000300
0000000000000020 0000000000000000 A 0 0 8
[ 3] .note.ABI-tag NOTE 0000000000400320 00000320
0000000000000020 0000000000000000 A 0 0 4
[ 4] .hash HASH 0000000000400340 00000340
0000000000000038 0000000000000004 A 6 0 8
[ 5] .gnu.hash GNU_HASH 0000000000400378 00000378
000000000000001c 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 0000000000400398 00000398
00000000000000d8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000400470 00000470
000000000000005c 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 00000000004004cc 000004cc
0000000000000012 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 00000000004004e0 000004e0
0000000000000020 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000400500 00000500
0000000000000030 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000400530 00000530
0000000000000090 0000000000000018 AI 6 21 8
[12] .init PROGBITS 0000000000401000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000401020 00001020
0000000000000070 0000000000000010 AX 0 0 16
[14] .plt.sec PROGBITS 0000000000401090 00001090
0000000000000060 0000000000000010 AX 0 0 16
[15] .text PROGBITS 00000000004010f0 000010f0
0000000000000145 0000000000000000 AX 0 0 16
[16] .fini PROGBITS 0000000000401238 00001238
000000000000000d 0000000000000000 AX 0 0 4
[17] .rodata PROGBITS 0000000000402000 00002000
000000000000003b 0000000000000000 A 0 0 8
[18] .eh_frame PROGBITS 0000000000402040 00002040
00000000000000fc 0000000000000000 A 0 0 8
[19] .dynamic DYNAMIC 0000000000403e50 00002e50
00000000000001a0 0000000000000010 WA 7 0 8
[20] .got PROGBITS 0000000000403ff0 00002ff0
0000000000000010 0000000000000008 WA 0 0 8
[21] .got.plt PROGBITS 0000000000404000 00003000
0000000000000048 0000000000000008 WA 0 0 8
[22] .data PROGBITS 0000000000404048 00003048
0000000000000004 0000000000000000 WA 0 0 1
[23] .comment PROGBITS 0000000000000000 0000304c
000000000000002b 0000000000000001 MS 0 0 1
[24] .symtab SYMTAB 0000000000000000 00003078
00000000000004c8 0000000000000018 25 30 8
[25] .strtab STRTAB 0000000000000000 00003540
0000000000000158 0000000000000000 0 0 1
[26] .shstrtab STRTAB 0000000000000000 00003698
00000000000000e1 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
5.4 hello的虛擬地址空間
使用edb加載hello,查看本進程的虛擬地址空間各段信息,并與5.3對照分析說明。
從Data Dump中可以看到程序從0x400000開始:
edb中程序頭:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000002a0 0x00000000000002a0 R 0x8
INTERP 0x00000000000002e0 0x00000000004002e0 0x00000000004002e0
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000005c0 0x00000000000005c0 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000245 0x0000000000000245 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x000000000000013c 0x000000000000013c R 0x1000
LOAD 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001fc 0x00000000000001fc RW 0x1000
DYNAMIC 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001a0 0x00000000000001a0 RW 0x8
NOTE 0x0000000000000300 0x0000000000400300 0x0000000000400300
0x0000000000000020 0x0000000000000020 R 0x8
NOTE 0x0000000000000320 0x0000000000400320 0x0000000000400320
0x0000000000000020 0x0000000000000020 R 0x4
GNU_PROPERTY 0x0000000000000300 0x0000000000400300 0x0000000000400300
0x0000000000000020 0x0000000000000020 R 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002e50 0x0000000000403e50 0x0000000000403e50
0x00000000000001b0 0x00000000000001b0 R 0x1
其含義如下:
PHDR:程序頭表
INTERP:程序執行前調用的解釋器
LOAD:程序目標代碼和常量信息
DYNAMIC:動態鏈接器所使用的信息
NOTE::輔助信息
GNU_EH_FRAME:異常信息保存
GNU_STACK:使用系統棧所需要的權限信息
GNU_RELRO:保存在重定位之后只讀信息的位置
可以看到最開始是GNU_STACK,而LOAD從0x400000開始,接著是PHDR,INTERP和NOTE。最后是DYNAMIC和GNU_RELRO部分。
5.5 鏈接的重定位過程分析
重定位:鏈接器在完成符號解析以后,會將代碼中的每個符號引用和對應的符號定義關聯起來。此時,鏈接器會獲得輸入目標模塊中的代碼節和數據節的確切大小,進而進行重定位步驟。在這個步驟中,鏈接器會合并輸入模塊,并為每個符號分配運行時的地址。然后在重定位節中的符號引用中,鏈接器會修改hello中的代碼節和數據節中的符號引用,使得他們指向正確的運行地址。
我們可以發現,通過對鏈接后的hello程序進行反匯編,所有指令的地址都變成了絕對地址,而并非hello.o中的相對地址。
5.6 hello的執行流程
使用edb執行hello,從加載hello到_start到call main以及程序終止的所有函數及其地址:
401000 _init>
401020 .plt>
401030 puts@plt>
401040 printf@plt>
401050 getchar@plt>
401060 atoi@plt>
401070 exit@plt>
401080 sleep@plt>
401090 _start>
4010c0 _dl_relocate_static_pie>
4010c1 main>
401150 __libc_csu_init>
4011b0 __libc_csu_fini>
4011b4 _fini>
5.7 Hello的動態鏈接分析
共享鏈接庫代碼是一個動態的目標模塊,在程序開始運行或者調用程序加載時,可以自動加載該代碼到任意的一個內存地址,并和一個在目標模塊內存中的應用程序鏈接了起來,這個過程就是對動態鏈接的重定位過程。
在elf文件中,有:
.got PROGBITS 0000000000403ff0 00002ff0
.got.plt PROGBITS 0000000000404000 00003000
0x0000000000000003 (PLTGOT) 0x404000
在edb中查看:
5.8 本章小結
本章介紹了有關鏈接的概念和作用,分析了hello的ELF格式以及虛擬地址空間是如何進行分配的,介紹了重定位和動態鏈接的過程。
第6章 hello進程管理
6.1 進程的概念與作用
進程是計算機中的程序關于某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。
6.2 簡述殼Shell-bash的作用與處理流程
在Linux系統中,Shell是一個交互型應用級程序,為用戶提供了與系統內核進行交互的方式。
主要功能是:Shell讀取輸入->處理輸入內容,獲取輸入參數->如果是內核命令則直接執行,否則調用程序->當程序運行時,shell會監視用戶輸入并對此做出響應。
具體的處理流程如下:
1.Shell從命令行中讀入特殊字符(元字符),在將元字符翻譯成間隔符號。元字符將命令行劃分成小塊tokens。Shell中的元字符如下所示:
SPACE , TAB , NEWLINE , & , ; , ( , ) ,< , > , |
2.處理tokens塊,檢查看他們是否是shell中所引用到的關鍵字。
3.當程序塊tokens被確定以后,shell根據aliases文件中的列表來檢查命令的第1個關鍵詞。如果成功找到,執行替換操作并且處理過程回到第1步再次處理程序塊tokens。
4.Shell對~符號進行替換,對所有前面帶有符號的變量進行替換。5.將命令行中內嵌的符號的變量進行替換。 5.將命令行中內嵌的符號的變量進行替換。5.將命令行中內嵌的(command)命令表達式替換成命令
6.計算被$(expression)標記的算術表達式。
7.Shell將命令串重新劃分為新的tokens塊。依欄位分割符號,稱為IFS。缺省的IFS變量包含有:SPACE , TAB和\n,并替換通配符,例如:* ? [ ]。
8.shell按照下面的順序檢查命令:
內置命令->用戶自定義函數->按路徑尋找可執行的腳本文件
9.對所有的輸入輸出重定向進行初始化,最終執行命令
6.3 Hello的fork進程創建過程
根據shell的處理流程,可以推斷,輸入命令執行hello后,父進程如果判斷不是內部指令,即會通過fork函數創建子進程。子進程與父進程近似,并得到一份與父進程用戶級虛擬空間相同且獨立的副本——包括數據段、代碼、共享庫、堆和用戶棧。父進程打開的文件,子進程也可讀寫。二者之間最大的不同或許在于PID的不同。Fork函數只會被調用一次,但會返回兩次,在父進程中,fork返回子進程的PID,在子進程中,fork返回0。因為子進程的PID總是為非零,返回值就提供一個明確的方法來分辨程序是在父進程里還是子進程里執行。
6.4 Hello的execve過程
execve函數在加載并運行可執行目標文件Hello,且帶列表argv和環境變量列表envp。該函數的作用就是在當前進程的上下文中加載并運行一個新的程序。
只有當出現錯誤時,例如找不到Hello時,execve才會返回到調用程序,這里與一次調用兩次返回的fork不同。
在execve加載了Hello之后,它調用啟動代碼。啟動代碼設置棧,并將控制傳遞給新程序的主函數,該主函數有如下的原型:
int main(intargc ,char **argv, char *envp)
結合虛擬內存和內存映射過程,可以更詳細地說明exceve函數實際上是如何加載和執行程序Hello:
刪除已存在的用戶區域(自父進程獨立)。
共享區映射:比如Hello程序與標準C庫libc.so鏈接,這些對象都是動態鏈接到Hello的,然后再用戶虛擬地址空間中的共享區域內。
私有區映射:為Hello的代碼、數據、.bss和棧區域創建新的區域結構,所有這些區域都是私有的、寫時才復制的。
設置PC:exceve會設置當前進程的上下文中的程序計數器,指向代碼區的入口。
6.5 Hello的進程執行
邏輯控制流:
一系列程序計數器 PC 的值的序列叫做邏輯控制流。由于進程是輪流使用處理器的,同一個處理器每個進程執行它的流的一部分后被搶占,然后輪到其他進程。
用戶模式和內核模式:
處理器使用一個寄存器提供兩種模式的區分。用戶模式的進程不允許執行特殊指令,不允許直接引用地址空間中內核區的代碼和數據;內核模式進程可以執行指令集中的任何命令,并且可以訪問系統中的任何內存位置。
上下文:
上下文就是內核重新啟動一個被搶占的進程所需要恢復的原來的狀態,由寄存器、程序計數器、用戶棧、內核棧和內核數據結構等對象的值構成。
結合進程上下文信息、進程時間片,闡述進程調度的過程,用戶態與核心態轉換等等。
初始時,控制流位于hello進程內,處于用戶模式
調用系統函數sleep后,進入內核態,此時間片停止。
2s后,發送中斷信號,轉回用戶模式,繼續執行指令。
調度的過程:
在進程執行的某些時刻,內核可以決定搶占當前進程,并重新開始一個先前被搶占了的進程,這種決策就叫做調度,是由內核中稱為調度器的代碼處理的。當內核選擇一個新的進程運行,我們說內核調度了這個進程。在內核調度了一個新的進程運行了之后,它就搶占了當前進程,并使用上下文切換機制來將控制轉移到新的進程。
以執行sleep函數為例,sleep函數請求調用休眠進程,sleep將內核搶占,進入倒計時,當倒計時結束后,hello程序重新搶占內核,繼續執行。
用戶態與核心態轉換:
為了能讓處理器安全運行,不至于損壞操作系統,必然需要先知應用程序可執行指令所能訪問的地址空間范圍。因此,就存在了用戶態與核心態的劃分,核心態擁有最高的訪問權限,處理器以一個寄存器當做模式位來描述當前進程的特權。進程只有故障、中斷或陷入系統調用時才會得到內核訪問權限,其他情況下始終處于用戶權限之中,保證了系統的安全性。
6.6 hello的異常與信號處理
hello執行過程中可能會出現以下異常:
類別 原因 異步/同步 返回行為
中斷 來自I/O設備的信號 異步 返回到下一條指令
陷阱 有意的異常 同步 返回到下一條指令
故障 潛在的可恢復的錯誤 同步 可能返回到當前指令
終止 不可恢復的錯誤 同步 不會返回
運行程序時,參數是學號+姓名+秒數,在命令行中鍵入./hello 120L021327 DAIHONGQIAN 1,可以得到如下結果:
執行ctrl+Z命令,可以將正在運行的hello程序掛起,我們鍵入ps命令,可以查看正在運行的進程的PID,hello進程的PID為3399,再使用jobs命令查看正在運行的后臺任務,也可以看到我們剛剛鍵入的./hello 120L021327 DAIHONGQIAN 1,此時使用fg命令,我們可以把后臺的hello調回到前臺繼續執行。[4]
接下來,我們鍵入ctrl+C的終止指令,進程收到 SIGINT 信號,我們可以結束徹底結束hello進程,這時我們再通過ps和jobs命令檢查正在運行的命令,我們發現hello進程已經結束。
中途隨意從鍵盤鍵入字符,都會被接收至緩沖區,命令行無法解析無意義的字符串,會提示command not found。
kill命令會殺死掛起的程序,執行kill命令后無法查詢到hello的PID。
6.7本章小結
本章主要介紹了有關hello進程管理的內容,通過shell我們可以管理計算機運行的進程。我們還介紹了hello程序的fork、execve過程,然后通過命令行研究了程序運行當中可能會遇見的異常,以及我們的系統是如何處理這些由異常發出的信號的。
第7章 hello的存儲管理
7.1 hello的存儲器地址空間
邏輯地址,是指由程序hello產生的與段相關的偏移地址部分,hello.o文件中的地址就是邏輯地址。
線性地址,是邏輯地址到物理地址變換之間的中間層。程序hello的代碼會產生邏輯地址——也就是段中的偏移地址,它加上相應段的基地址就會生成線性地址。
虛擬地址,是由CPU生成的一個仰賴訪問主存的地址,它會被送到內存之前先轉換成適當的物理地址,地址翻譯的任務由CPU芯片上的內存管理單元MMU來承擔,MMU會用存放在主存中的查詢表來動態翻譯虛擬地址,該表的內容由操作系統管理。在linux系統中,只會分頁而不會分段,因此對于我們的hello程序邏輯地址幾乎就是虛擬地址。
物理地址,是主存上被組織成一個由M個連續的字節大小的單元組成的數組,每個字節都有一個唯一的物理地址,hello的物理地址來自于虛擬地址的地址轉換,它也是程序運行的最終地址。
7.2 Intel邏輯地址到線性地址的變換-段式管理
一個邏輯地址由兩部分組成,段標識符,段內偏移量。段標識符是一個16位長的字段組成,稱為段選擇符,其中前13位是一個索引號。后面三位包含一些硬件細節。
索引號,可以通過段標識符的前13位,直接在段描述符表中找到一個具體的段描述符,這個描述符就描述了一個段。
Base,它描述了一個段的開始位置的線性地址。
全局的段描述符,放在全局段描述符表(GDT)中,而局部的段描述符,處于局部段描述符表(LDT)之中。GDT在內存中的地址和大小會存放在gdtr控制寄存器中,而LDT則在ldtr寄存器中。
一個完整的邏輯地址包括段選擇符+段內偏移地址,
7.3 Hello的線性地址到物理地址的變換-頁式管理
頁式管理,是一種內存空間存儲管理的技術,分為靜態頁式管理和動態頁式管理。頁式管理將各進程的虛擬空間劃分成若干個長度相等的頁(page),再把內存空間按頁的大小劃分成片,進而建立起內存地址與頁式虛擬地址一一對應關系,存儲在頁表之中。為了解決離散地址變換問題,它會調用相應的硬件地址變換構件。頁式管理采用請求調頁或預調頁技術實現了內外存的統一管理與調用。
優點:它有效地解決了碎片問題。由于頁式管理不需要進程的程序段和數據在內存中連續存放,從而提供了內外存統一管理的虛擬內存實現方式,使用戶可以利用的碎片化的空間大大增加,也非常有利于多個進程同時執行。
缺點:需要相應的硬件支持,增加了機器成本,并增加了系統開銷。例如缺頁中斷的產生和選擇淘汰頁面等都要求有相應的硬件支持;缺頁中斷處理時,請求調頁的算法如果不夠恰當,有可能產生抖動現象。碎片式的管理,也使得每個進程內總有一部分空間得不到利用,當頁面比較大的時候,這一部分的損失會非常顯著。
7.4 TLB與四級頁表支持下的VA到PA的變換
TLB:每次CPU產生一個虛擬地址,MMU就必須查閱相應的PTE,每次都必然造成緩存不命中等一系列時間開銷,為了消除這樣的開銷,MMU中存在一個全相聯高速緩存,稱為TLB。
四級頁表:如果只采用兩級的頁表結構,當需要使用的虛擬內存很小,但仍然需要建立其一個龐大的頁表,這很容易造成內存的浪費,檢索巨大的頁表也是非常浪費性能的事。由此,在虛擬地址到物理地址的轉換過程中,又開發出了多級頁表的機制:上一級的頁表映射到下一級頁表,直到頁表映射到虛擬內存。四級頁表,也就指共有四級的多級頁表。
7.5 三級Cache支持下的物理內存訪問
物理地址被分為CT(標記)+CL(索引)+CO(偏移量),然后到1級cache里去找對應的標記位為有效的。如果命中就直接返回想要的數據,如果不命中,就依次去L2,L3,主存判斷是否命中,當命中時,將數據傳給CPU同時更新各級cache的cacheline。當cache已滿時,需要按一定策略,將cache中近期不太可能再次訪問到的數據替換為我們所需要的物理內存訪問索引。
7.6 hello進程fork時的內存映射
當fork函數被當前進程調用時,內核為新進程創建各種數據結構,并分配給它一個唯一的PID,同時為這個新進程創建虛擬內存。它創建了當前進程的mm_struct、區域結構和頁表的副本,將兩個進程中的每個頁面都標記位只讀,并將兩個進程中的每個區域結構都標記為私有的寫時復制。
當fork在新進程中返回時,新進程現在的虛擬內存剛好和調用fork時存在的虛擬內存相同。當這兩個進程中的任一個后來進行寫操作時,寫時復制機制就會創建新頁面。因此,也就為每個進程保持了私有空間地址的抽象概念。
7.7 hello進程execve時的內存映射
1)在bash中的進程中執行了execve調用:execve(“hello”,NULL,NULL);
2)execve函數在當前進程中加載并運行包含在可執行文件hello中的程序,用hello替代了當前bash中的程序。
3)刪除已存在的用戶區域。
4)映射私有區域
5)映射共享區域
6)設置程序計數器(PC)
exceve做的最后一件事是設置當前進程的上下文中的程序計數器,是指指向代碼區域的入口點。而下一次調度這個進程時,他將從這個入口點開始執行。Linux將根據需要換入代碼和數據頁面。
7.8 缺頁故障與缺頁中斷處理
整體的處理流程:
1.處理器生成一個虛擬地址,并將它傳送給MMU
2.MMU生成PTE地址,并從高速緩存/主存請求得到它
3.高速緩存/主存向MMU返回PTE
4.PTE中的有效位是0,所以MMU出發了一次異常,傳遞CPU中的控制到操作系統內核中的缺頁異常處理程序。
5.缺頁處理程序確認出物理內存中的犧牲頁,如果這個頁已經被修改了,則把它換到磁盤。
6.缺頁處理程序頁面調入新的頁面,并更新內存中的PTE
7.缺頁處理程序返回到原來的進程,再次執行導致缺頁的命令。CPU將引起缺頁的虛擬地址重新發送給MMU。因為虛擬頁面已經換存在物理內存中,所以就會命中。
7.9動態存儲分配管理
基本方法與策略:通過維護虛擬內存(堆),一種是隱式空閑鏈表,一種是顯式空閑鏈表。顯式空閑鏈表法是malloc(size_t size)每次聲明內存空間都保證至少分配size_t大小的內存,雙字對齊,每次必須從空閑塊中分配空間,在申請空間時將空閑的空間碎片合并,以盡量減少浪費。分配器一般按以下策略進行分配:首次適配、下一次適配合最佳適配。分配完后可以分割空閑塊減少內部碎片。同時分配器在面對釋放一個已分配塊時,可以合并空閑塊,其中便利用隱式空閑鏈表的邊界標記來進行合并。
7.10本章小結
本章主要介紹了 hello 的存儲器地址空間、 intel 的段式管理、 hello 的頁式管理,在指定環境下介紹了 VA 到 PA 的變換、物理內存訪問,還介紹 hello 進程 fork 時的內存映射、 execve 時的內存映射、缺頁故障與缺頁中斷處理、動態存儲分配管理。
第8章 hello的IO管理
8.1 Linux的IO設備管理方法
設備的模型化
文件(所有的I/O設備都被模型化為文件,甚至內核也被映射為文件)
設備管理
unix io接口
這種將設備映射為系統文件的方式,允許Linux內核引出一個簡單的應用接口,稱為Unix I/O。
文件操作包括:打開和關閉操作;讀寫操作;更改當前文件位置……
8.2 簡述Unix IO接口及其函數
open函數
功能描述:打開或創建文件,可以指定文件的屬性及用戶的權限等各種參數。
函數原型:int open(const char *pathname,int flags,int perms)
參數:pathname:被打開的文件所在路徑,flags:文件打開方式,
返回值:成功:返回文件描述符;失敗:返回-1
close函數
功能描述:用于關閉一個被打開的的文件
所需頭文件: #include <unistd.h>
函數原型:int close(int fd) 參數:fd文件描述符
函數返回值:0成功,-1出錯
read函數
功能描述: 讀取文件中數據
所需頭文件: #include <unistd.h>
函數原型:ssize_t read(int fd, void *buf, size_t count);
參數:fd:目標文件描述符。buf:緩沖區。count:表示調用一次read操作,應該讀取的字符數量。
返回值:返回所讀取的字節數;-1(出錯)。
write函數
功能描述:寫入數據。
所需頭文件:#include <unistd.h>
函數原型:ssize_t write(int fd, void *buf, size_t count);
返回值:寫入文件的字節數(成功);-1(出錯)
lseek()函數
功能描述: 用于在指定的文件描述符中將將文件指針定位到相應位置。
所需頭文件:#include <unistd.h>,#include <sys/types.h>
函數原型:off_t lseek(int fd, off_t offset,int whence);
參數:fd;文件描述符。offset:偏移量,每一個讀寫操作所需要移動的距離,單位是字節,可正可負(向前移,向后移)
返回值:成功:返回當前位移;失敗:返回-1
8.3 printf的實現分析
(以下格式自行編排,編輯時刪除)
printf函數的函數體:
int printf(const char *fmt, …)
{
int i;
char buf[256];
}
vsprintf函數將所有的參數內容格式化之后存入buf,然后返回格式化數組的長度。write函數將buf中的i個元素寫到終端。從vsprintf生成顯示信息,到write系統函數,到陷阱-系統調用 int 0x80或syscall.字符顯示驅動子程序:從ASCII到字模庫到顯示vram(存儲每一個點的RGB顏色信息)。顯示芯片按照刷新頻率逐行讀取vram,并通過信號線向液晶顯示器傳輸每一個點。
顯示芯片按照刷新頻率逐行讀取vram,并通過信號線向液晶顯示器傳輸每一個點(RGB分量),最后就在顯示器上實現了信息的打印。
8.4 getchar的實現分析
異步異常-鍵盤中斷的處理:
getchar可以理解為對于鍵盤中斷的異步異常的處理。當鍵盤中斷處理子程序。系統會接受按鍵輸入的信息,并將其轉換為ascii碼,保存到鍵盤輸入的緩沖區。getchar等調用read系統函數,通過系統調用讀取按鍵輸入數據,當接受到回車\n時才返回。getchar有一個int型的返回值。當程序調用getchar時,程序就等著用戶按鍵,用戶輸入的字符被存放在鍵盤緩沖區中直到用戶按回車為止,回車字符\n也會被讀入緩沖區中。
當用戶鍵入回車之后,getchar開始從stdio流中每次讀入一個字符。getchar函數的返回值是用戶輸入的第一個字符的ascii碼,如出錯返回-1,且將用戶輸入的字符回顯到屏幕。如用戶在按回車之前輸入了不止一個字符,其他字符會保留在鍵盤緩存區中,等待后續getchar調用讀取。也就是說,后續的getchar調用不會等待用戶按鍵,而直接讀取緩沖區中的字符,直到緩沖區中的字符讀完為后,才等待用戶按鍵。
8.5本章小結
本章介紹了 Linux 的 I/O 設備的基本概念和管理方法,以及Unix I/O 接口及其函數。最后分析了printf 函數和 getchar 函數的工作過程。
結論
最初接觸到C 語言的我們,在計算機的屏幕前,通過鍵盤鼠標等I/O設備鍵入hello程序的代碼,將它保存為了.c的文本文件,我們點擊codeblocks中的編譯按鈕,再運行可執行的文件,hello便橫空出世了。對于一個初學者而言,編寫這樣結構簡單的程序似乎不是一件難事,然而計算機系統這一節課把hello的神秘面紗緩緩解開了,我們看到了hello的“漫漫長征路”:
1.hello.c經過預編譯,拓展得到hello.i文本文件
2.hello.i經過編譯,得到匯編代碼hello.s匯編文件
3.hello.s經過匯編,得到二進制可重定位目標文件hello.o
4.hello.o經過鏈接,生成了可執行文件hello
5.bash進程調用fork函數,生成子進程;并由execve函數加載運行當前進程的上下文中加載并運行新程序hello
6.hello的變化過程中,會有各種地址,但最終我們真正期待的是PA物理地址。
7.hello再運行時會調用一些函數,比如printf函數,這些函數與linux I/O的設備模擬化密切相關
8.hello最終被shell父進程回收,內核會收回為其創建的所有信息
hello的一生并不是一條平坦的大路,它更像是個通過神秘穿梭機來回變換的時間旅客,每一步的變換都極端復雜,讓人琢磨不透。然而通過計算機系統的學習,我們可以將這些“魔術”背后的奧秘一一揭曉,由0和1,人類的智慧一步步將它變為高樓大廈,一切復雜的系統背后都是非常質樸的想法,為了貼切地把這個想法落地生根,精妙而復雜的系統就被建立了起來。
我們現在所使用的計算機系統,經過了UI的改革,變得直觀易懂,而繁瑣的系統實現的細節被悄悄地包裝在了我們看不見的深處里,這門課程之所以學習接近底層的那些細節,不厭其煩地探討那些被藏起來的內容,能很好地幫助我們認清計算機系統的實質。也許科技發展到怎樣的地步,我們提起駭客、提起極客這類名詞時,腦海中浮現的都是那個手指飛快地敲擊鍵盤,屏幕上是熒光綠色的命令行,一切可能,皆系于其中。
附件
?預處理后的文件 hello.i
?編譯之后的匯編文件 hello.s
?匯編之后的可重定位目標文件 hello.o
?鏈接之后的可執行目標文件 Hello
?Hello.o 的 ELF 格式 elf.txt
?Hello.o 的反匯編代碼 Disas_hello.s
?hello的ELF 格式 hello1.elf
hello 的反匯編代碼 hello1_objdump.s
參考文獻
[1] 條件編譯,C語言條件編譯詳解. http://c.biancheng.net/view/289.html
[2] .bss、.data 和 .rodata section 之間的區別. https://blog.csdn.net/wohenfanjian/article/details/106007978
[3] 信息安全系統設計基礎. 異常控制流.
[4] linux后臺運行、掛起、恢復進程相關命令https://blog.csdn.net/koberonaldo24/article/details/103136125
[5] printf 函數實現的深入剖析. https://www.cnblogs.com/pianist/p/3315801.html
總結
以上是生活随笔為你收集整理的HIT计算机系统大作业——hello的一生的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Android]安装 Android
- 下一篇: 计算机系统大作业——hello的一生