2021哈工大计算机系统大作业——程序人生-Hello’s P2P
計算機系統
大作業
題 ????目 ?程序人生-Hello’s P2P?
計算機科學與技術學院
2021年6月
摘? 要
本文介紹了hello的整個生命過程。利用gcc,gdb,edb,readelf,HexEdit等工具具體分析了hello從源程序開始,歷經預處理、編譯、匯編、鏈接的一系列步驟變為可執行文件的過程,即P2P的過程。同時還具體分析了hello在運行過程中涉及的進程管理、內存管理、IO管理到最后hello被回收,即020的過程。通過對hello這個簡單程序的詳細分析,我們能夠更加深入地理解計算機系統。
關鍵詞:Hello’s P2P;進程管理;內存管理;I/O管理???????????????????????????
(摘要0分,缺失-1分,根據內容精彩稱都酌情加分0-1分)
目? 錄
第1章 概述... - 5 -
1.1 Hello簡介... - 5 -
1.2 環境與工具... - 5 -
1.3 中間結果... - 5 -
1.4 本章小結... - 6 -
第2章 預處理... - 7 -
2.1 預處理的概念與作用... - 7 -
2.2在Ubuntu下預處理的命令... - 8 -
2.3 Hello的預處理結果解析... - 9 -
2.4 本章小結... - 9 -
第3章 編譯... - 10 -
3.1 編譯的概念與作用... - 10 -
3.2 在Ubuntu下編譯的命令... - 10 -
3.3 Hello的編譯結果解析... - 11 -
3.3.1數據... - 11 -
3.3.2賦值操作... - 13 -
3.3.3類型轉換... - 13 -
3.3.4算術操作... - 13 -
3.3.5關系操作... - 14 -
3.3.6數組操作... - 14 -
3.3.7控制轉移... - 15 -
3.3.8函數操作... - 15 -
3.4 本章小結... - 16 -
第4章 匯編... - 17 -
4.1 匯編的概念與作用... - 17 -
4.2 在Ubuntu下匯編的命令... - 17 -
4.3 可重定位目標elf格式... - 17 -
4.3.1 ELF頭... - 17 -
4.3.2節頭部表... - 18 -
4.4.3符號表... - 19 -
4.3.4重定位節... - 20 -
4.4 Hello.o的結果解析... - 21 -
4.5 本章小結... - 23 -
第5章 鏈接... - 24 -
5.1 鏈接的概念與作用... - 24 -
5.2 在Ubuntu下鏈接的命令... - 24 -
5.3 可執行目標文件hello的格式... - 24 -
5.3.1 ELF頭... - 25 -
5.3.2 節頭部表... - 26 -
5.3.3 程序頭部表... - 27 -
5.3.4 符號表... - 27 -
5.3.5 重定位節... - 29 -
5.4 hello的虛擬地址空間... - 29 -
5.5 鏈接的重定位過程分析... - 31 -
5.6 hello的執行流程... - 33 -
5.7 Hello的動態鏈接分析... - 33 -
5.8 本章小結... - 35 -
第6章 hello進程管理... - 36 -
6.1 進程的概念與作用... - 36 -
6.2 簡述殼Shell-bash的作用與處理流程... - 36 -
6.3 Hello的fork進程創建過程... - 36 -
6.4 Hello的execve過程... - 37 -
6.5 Hello的進程執行... - 37 -
6.6 hello的異常與信號處理... - 38 -
6.6.1可能出現的異常及處理方法... - 38 -
6.6.2可能產生的信號及處理方法... - 39 -
6.7本章小結... - 41 -
第7章 hello的存儲管理... - 42 -
7.1 hello的存儲器地址空間... - 42 -
7.2 Intel邏輯地址到線性地址的變換-段式管理... - 42 -
7.3 Hello的線性地址到物理地址的變換-頁式管理... - 43 -
7.4 TLB與四級頁表支持下的VA到PA的變換... - 43 -
7.5 三級Cache支持下的物理內存訪問... - 45 -
7.6 hello進程fork時的內存映射... - 46 -
7.7 hello進程execve時的內存映射... - 46 -
7.8 缺頁故障與缺頁中斷處理... - 47 -
7.9動態存儲分配管理... - 48 -
7.10本章小結... - 51 -
第8章 hello的IO管理... - 52 -
8.1 Linux的IO設備管理方法... - 52 -
8.2 簡述Unix IO接口及其函數... - 52 -
8.3 printf的實現分析... - 53 -
8.4 getchar的實現分析... - 54 -
8.5本章小結... - 54 -
結論... - 55 -
附件... - 56 -
參考文獻... - 57 -
第1章 概述
1.1?Hello簡介
簡述Hello的P2P,020的整個過程。
Hello的P2P(From Program to Process)過程:在文本編輯器或IDE中編寫C語言代碼,得到最初的hello.c程序,即最初的Program。編譯器驅動程序代表用戶在需要時調用語言預處理器、編譯器、匯編器和鏈接器。驅動程序首先運行C預處理器(cpp),將C的源程序hello.c翻譯成一個ASCII碼的中間文件;然后運行C編譯器(cc1)將中間文件翻譯成一個ASCII匯編語言文件;之后運行匯編器(as)將匯編語言文件翻譯成可重定位目標文件;最后運行鏈接器(ld)創建一個可執行目標文件hello。在shell中輸入執行hello的命令,shell解析命令行,通過fork新建一個子進程來執行hello,這時Hello已經從Program轉換為Process了。
Hello的020(From Zero-0 to Zero-0)過程:子進程調用execve,重新為hello進行內存映射,設置當前進程上下文中的程序計數器,使之指向代碼區域的入口點。進入程序入口后通過存儲管理機制將指令和數據載入內存,CPU以流水線形式讀取并執行指令,執行邏輯控制流。操作系統負責進程調度,為進程分時間片。執行過程中通過L1、L2、L3高速緩存、TLB、多級頁表等進行存儲管理,通過I/O系統進行輸入輸出。當程序運行結束后,shell回收hello進程,刪除和該進程相關的內容,這時hello進程就不存在了。hello從開始的未被內存映射到運行再到回收后不再存在,就是020的過程。
1.2?環境與工具
硬件環境:X64 CPU;2.6GHz;16G RAM;256GHD Disk
軟件環境:Windows10 64位;Vmware 16;Ubuntu 16.04 LTS 64位
開發與調試工具:gcc,gdb,edb,readelf,HexEdit
1.3 中間結果
hello.i:C預處理器產生的一個ASCII碼的中間文件,用于分析預處理過程。
hello.s:C編譯器產生的一個ASCII匯編語言文件,用于分析編譯的過程。
hello.o:匯編器產生的可重定位目標程序,用于分析匯編的過程。
hello:鏈接器產生的可執行目標文件,用于分析鏈接的過程。
hello.txt:hello.o的反匯編文件,用于分析可重定位目標文件hello.o。
hellold.txt:hello的反匯編文件,用于分析可執行目標文件hello。
helloelf.txt:hello.o的ELF格式,用于分析可重定位目標文件hello.o。
helloldelf.txt:hello的ELF格式,用于分析可執行目標文件hello。
1.4 本章小結
本章簡述了Hello的P2P、020的整個過程并介紹了實驗的基本信息:環境、工具以及實驗的中間結果。
(第1章0.5分)
第2章 預處理
2.1 預處理的概念與作用
預處理是指在進行編譯的第一遍掃描之前所做的工作,是C語言的一個重要功能,由預處理程序負責完成。預處理在源代碼編譯之前對其進行的一些文本性質的處理,生成擴展的C源程序。C語言提供了多種預處理功能,包括宏定義、文件包含、條件編譯等。
預處理指令是以‘#’開頭的代碼行。‘#’必須是該行除了空白字符外的第一個字符。‘#’后面是指令關鍵字,整行語句構成一條預處理指令,該指令將在編譯器進行編譯之前對源代碼做某些轉換。下圖是ANSI標準定義的C語言預處理指令。
圖2-1 C語言預處理指令
宏定義(#define)使用宏名來表示一個字符串,宏展開時以該字符串取代宏名。這是一種簡單的文本替換,預處理程序對它不做任何檢查。如有錯誤,只能在后續編譯源程序時發現。文件包含指令(#include)把指定頭文件插入到該指令行的位置取代該指令行,從而把指定的文件和當前的源程序文件連成一個源文件。條件編譯指令(#ifdef,#ifndef,#else,#elif,#endif等)對源程序中一部分內容只在滿足一定條件時才進行編譯,即指定編譯的條件。可以按不同的條件去編譯不同的程序部分,從而產生不同的目標代碼文件。
預處理程序還可以識別一些特殊的符號。__FILE__:包含當前程序文件名的字符串;__LINE__:表示當前行號的整數;__DATE__:包含當前日期的字符串;__STDC__:如果編譯器遵循ANSI C標準,則是非零值;__TIME__:包含當前時間的字符串。預處理程序對于在源程序中出現的這些串將用合適的值進行替換。
合理地使用預處理功能編寫的程序便于閱讀、修改、移植和調試,也有利于模塊化程序設計。
2.2在Ubuntu下預處理的命令
預處理的命令:gcc -E hello.c -o hello.i
預處理過程如圖所示:
圖2-2 預處理命令
圖2-3 預處理結果
2.3 Hello的預處理結果解析
查看預處理產生的hello.i文件,可以發現main函數以及定義全局變量的代碼沒有任何改變,而原來前面的#include語句被替換成了大量的頭文件中的內容,包括外部函數的聲明、結構體等數據結構的定義、數據類型的定義等內容。源程序開頭的注釋也被刪除了。同時,如果有#define的話,還會進行相應的符號替換。但是可以看出,預處理的結果仍然是可以閱讀的C語言程序,預處理只是對源程序進行了一些文本性質的處理,生成的是擴展的C源程序。
圖2-4 hello.i的部分結果
2.4 本章小結
?????? 本章介紹了預處理的概念和作用,結合實際程序分析了預處理的過程,包括宏替換、頭文件引入、刪除注釋、條件編譯等。
(第2章0.5分)
第3章 編譯
3.1 編譯的概念與作用
編譯的過程將預處理產生的ASCII碼中間文件hello.i翻譯成一個ASCII匯編語言文件hello.s。編譯會對預處理文件進行詞法分析、語法分析、優化等操作,將C語言這種高級語言轉換為成更低級、更底層、機器更好理解的匯編語言程序。
詞法分析對由字符組成的單詞進行處理,從左至右逐個字符地對源程序進行掃描,產生一個個的單詞符號,把作為字符串的源程序改造為單詞符號串的中間程序。語法分析以單詞符號作為輸入,分析單詞符號串是否形成符合語法規則的語法單位,如表達式、賦值、循環等,最后看是否構成一個符合要求的程序,按語言的語法規則分析檢查每條語句是否有正確的邏輯結構(語法規則可用上下文無關文法來刻畫)。代碼優化對程序進行等價的變換,使得變換后的程序能產生更有效的目標代碼。這種等價的變換不改變程序的運行結果,同時使得程序運行時間更短,占用的存儲空間更小。如果在編譯的過程中發現源程序有錯誤,會報告錯誤的性質和發生位置。但一般情況下,編譯器只做語法檢查和最簡單的語義檢查,而不檢查程序的邏輯。
匯編語言程序比源程序的層次更低,但是與機器代碼相比程序員更容易理解,匯編語言相當于高級語言和機器語言之間的過渡,是從源程序轉換到機器代碼的關鍵中間環節。
3.2 在Ubuntu下編譯的命令
編譯的命令:gcc -S hello.i -o hello.s
編譯過程如圖所示:
圖3-1 編譯命令
圖3-2 hello.s的部分結果
3.3 Hello的編譯結果解析
3.3.1數據
(1)常量:hello.c源程序中的兩個printf的參數是字符串常量,分別為"Usage: Hello 學號 姓名!\n"和"Hello %s %s\n"。
圖3-3 hello.c中的字符串常量
在編譯生成的hello.s中可以看到,這兩個字符串常量分別由.LC0和.LC1指示,均存放在只讀數據段.rodata中。
圖3-4 hello.s中的字符串常量
(2)全局變量:hello.c源程序中的sleepsecs是全局變量,且已被賦初值。
在編譯生成的hello.s中可以看到,使用.global將sleepsecs標記為全局變量。.data表明全局變量sleepsecs存放在數據段.data中;.align要求4字節對齊;.size表明變量為4字節;最后.long給出了變量的初值為2。
圖3-5 hello.s中的全局變量
(3)局部變量:hello.c源程序中的局部變量包括i,用于循環的計數。
圖3-6 hello.c中的局部變量
分析hello.s中為for循環產生的匯編代碼,可以看出i被存儲在%rbp-4的內存地址處。其中movl為i賦初值0,addl在每次循環時對i增加1,cmpl比較i和9的大小來決定什么時候結束循環。因此局部變量i是存放在棧上的,并通過相對棧頂(%rsp)的偏移量來訪問。
圖3-7 hello.s中的局部變量
hello.c中的其他局部變量還包括argc和argv,同樣地,它們都存放在棧上的,并通過相對棧頂(%rsp)的偏移量來訪問。
(4)關于數據的類型:在編譯過程中,編譯器會根據源程序中數據的類型來選取不同的寄存器以及不同的指令,比如浮點數會選擇XMM寄存器,整數或指針會選擇通用目的寄存器,同時也會根據數據的字節大小選擇寄存器的不同部分以及指令的后綴。但在編譯完成后,所有的類型信息都不復存在了,無法根據產生的匯編代碼推斷某個數據的類型。
3.3.2賦值操作
hello.c源程序中一共包括兩次賦值操作,分別是對全局變量sleepsecs賦初值和對循環變量i賦初值。
圖3-8 hello.c中的賦值操作
對于全局變量賦初值,這個值直接存儲在數據段.data中;而如果不對全局變量賦初值的話,變量會存放在.bss段。而對于其他情況,在不考慮優化的前提下,所有的賦值操作都轉化成mov類的數據傳送指令。指令的后綴取決于操作數據的字節大小,movb:一個字節;movw:兩個字節;movl:四個字節;movq:八個字節。以對i賦值為例,由于i為四字節,因此使用指令movl.
圖3-9 hello.s中對應賦值操作的指令
3.3.3類型轉換
?????? hello.c源程序中只包含一次隱式的類型轉換,出現在全局變量賦初值的時候。
對于隱式類型轉換,編譯器會自己直接進行轉換,在這個例子中,2.5被隱式類型轉換為int型,編譯器直接將轉換后的值2放在了相應的數據段中。
圖3-10 編譯時的隱式類型轉換
3.3.4算術操作
?????? hello.c源程序中只包含一次算術操作,出現在循環變量i每次增加1的時候。算術操作為++。
算術操作++代表自增1的運算,編譯時轉化成add類的加法指令,使用立即數1來實現每次增加1.
??????????????????????
圖3-11 hello.s中的++操作????
其他和算術操作相關的指令還包括inc,dec,neg,sub,imul等等。
3.3.5關系操作
hello.c源程序中出現了兩次關系操作。
(1)在if中判斷argc的取值是否不等于3.
編譯時使用cmpl指令將argc和3進行比較,并設置條件碼。跳轉指令je根據條件碼決定是否跳轉。對于關系操作!=來說,可以選擇je或者jne跳轉指令。
圖3-12 hello.s中的關系操作
(2)在for循環中判斷結束條件,即判斷i是否小于10。
類似地,編譯時使用cmpl指令將i和9進行比較,并設置條件碼。跳轉指令jle根據條件碼決定是否跳轉。這里進行比較的值是9而不是10,與編譯的過程中進行了優化有關。
圖3-13 hello.s中的關系操作
3.3.6數組操作
hello.c源程序中有關數組的操作出現在訪問argv元素的時候,通過argv[1]和argv[2]訪問了字符指針數組中的元素。
匯編代碼中使用首地址+偏移量的方式來訪問數組元素,數組首地址存儲在%rbp-32的位置,通過將首地址加8獲得argv[1]的地址,將首地址加16獲得argv[2]的地址。值得注意的是,編譯器會根據引用的數據類型的大小進行伸縮而不用程序員操心。由于這里的數組是指針數據,因此伸縮因子為8.
圖3-14 hello.s中的數組操作
3.3.7控制轉移
hello.c源程序中出現了兩次控制轉移。
(1)if判斷argc的取值后的控制轉移。
編譯時使用cmpl指令將argc和3進行比較,并設置條件碼。跳轉指令je根據條件碼決定是否跳轉。控制轉移由指令je完成。
圖3-15 hello.s中的控制轉移
(2)每次for循環結束時的控制轉移。
類似地,編譯時使用cmpl指令將i和9進行比較,并設置條件碼。跳轉指令jle根據條件碼決定是否跳轉。控制轉移由指令jle完成。
圖3-16 hello.s中的控制轉移
3.3.8函數操作
(1)函數的調用:hello.c源程序中一共出現了五次函數調用。
圖3-17 hello.c中的函數調用
編譯時,所有的函數調用都轉換成了指令call,后面跟著調用函數的名字。
圖3-18 hello.s中的函數調用
(2)參數的傳遞:大部分的參數傳遞通過寄存器實現,通過寄存器最多傳遞6個參數,按照順序依次為%rdi、%rsi、%rdx、%rcx、%r8、%r9。多余的參數通過棧來傳遞。在hello.c這個例子中,對于第一個函數printf,只有一個參數,通過寄存其%edi傳遞。
圖3-19 hello.s中的參數傳遞
對于第二個函數exit,只有一個參數,通過寄存器%edi傳遞。
圖3-20 hello.s中的參數傳遞
對于第三個函數printf,有三個參數,分別通過寄存器%edi、%rsi、%rdx傳遞。
圖3-21 hello.s中的參數傳遞
對于第四個函數sleep,只有一個參數,通過寄存器%edi傳遞。
圖3-22 hello.s中的參數傳遞
最后一個函數getchar沒有參數,無需傳遞。
(3)函數的返回:編譯時,在函數的最后添加指令ret來實現函數的返回。在hello.c這個例子中,只能看到main函數的返回。
圖3-23 hello.s中的函數返回
3.4 本章小結
本章介紹了編譯的概念和作用,并針對具體的例子hello.s,詳細地分析了編譯器如何處理C語言的各種數據以及各類操作。
(第3章2分)
第4章 匯編
4.1 匯編的概念與作用
?????? 匯編的過程將編譯生成的ASCII匯編語言文件hello.s翻譯成一個可重定位目標文件hello.o。可重定位目標文件包含指令對應的二進制機器語言,這種二進制代碼能夠被計算機理解并執行。因此匯編是將匯編語言轉換成最底層的、機器可理解的機器語言的過程。
4.2 在Ubuntu下匯編的命令
匯編的命令:gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o
匯編過程如圖所示:
圖4-1 匯編命令
4.3 可重定位目標elf格式
使用readelf命令readelf -a hello.o > helloelf.txt查看hello.o的ELF格式,并將結果重定向到helloelf.txt便于查看分析。
圖4-2 查看hello.o ELF格式的命令
4.3.1 ELF頭
ELF頭以一個16字節的目標序列開始,如圖中Magic所示,這個序列描述了生成該文件的系統的字的大小和字節順序。以hello.o為例,這個16字節序列為7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00,描述了系統的字的大小為8字節,字節順序為小端序。
ELF頭剩下的部分包含幫助鏈接器語法分析和解釋目標文件的信息。包括ELF頭的大小、目標文件的類型、機器類型、節頭部表的文件偏移,以及節頭部表中條目的大小和數量。以hello.o為例,ELF頭中包含了ELF頭的大小:64字節;目標文件的類型:REL(可重定位文件);機器類型:Advanced Micro Devices X86-64;節頭部表的文件偏移:1112bytes;節頭部表中條目的數量:13.
圖4-3 hello.o的ELF頭
4.3.2節頭部表
?????? 節頭部表描述不同節的位置和大小,目標文件中的每個節都有一個固定大小的節頭部表條目。
?????? 以hello.s為例,節頭部表一共描述了13個不同節的位置、大小等信息。依次為:
[1].text節:已編譯程序的機器代碼,大小為0x7d字節,類型為PROGBITS,偏移量為0x40,標志為AX(表明該節的數據只讀并且可執行)。
[2] .rela.text節:一個.text節中位置的列表,大小為0xc0字節,類型為RELA,偏移量為0x318,標志為I。
[3].data節:已初始化的全局和靜態C變量,大小為0x4字節,類型為PROGBITS,偏移量為0xc0,標志為WA(表明該節的數據可讀可寫)。
[4].bss節:未初始化的全局和靜態C變量,以及所有被初始化為0的全局或靜態變量。大小為0x0字節,類型為NOBITS,偏移量為0xc4,標志為WA(表明該節的數據可讀可寫)。
[5].rodata節:只讀數據,大小為0x2b字節,類型為PROGBITS,偏移量為0xc4,標志為A(表明該節的數據只讀)。
[6].comment節:包含版本控制信息,大小為0x36字節,類型為PROGBITS,偏移量為0xef,標志為MS。
[7].note.GNU_stack節:標記可執行堆棧,大小為0x0字節,類型為PROGBITS,偏移量為0x125。
[8].eh_frame節:處理異常,大小為0x38字節,類型為PROGBITS,偏移量為0x128,標志為A(表明該節的數據只讀)。
[9].rela.eh_frame節:.eh_frame節的重定位信息,大小為0x18字節,類型為RELA,偏移量為0x3d8,標志為I。
[10].shstrtab節:包含節區名稱,大小為0x61字節,類型為STRTAB,偏移量為0x3f0。
[11].symtab節:一個符號表,存放在程序中定義和引用的函數和全局變量的信息。大小為0x180字節,類型為SYMTAB,偏移量為0x160。
[12].strtab節:一個字符串表,包括.symtab和.debug節中的符號表,以及節頭部中的節名字。大小為0x37字節,類型為STRTAB,偏移量為0x2e0。
圖4-4 hello.o的節頭部表
4.4.3符號表
?????? 符號表存放程序中定義和引用的函數和全局變量的信息,每個符號表是一個條目的數組,每個條目包括value:距定義目標的節的起始位置的偏移;size:目標的大小;type:指明數據還是函數;bind:表示符號是本地的還是全局的等等。
?????? 以hello.s為例,符號表一共描述了16個符號。比如全局變量sleepsecs,Ndx=3表明它在.data節,value=0表明它在.data節中偏移量為0的地方,size=4表明大小為4字節,bind=GLOBAL表明它是全局符號,type=OBJECT:表明它是數據。而對于函數main,Ndx=1表明它在.text節,value=0表明它在.text節中偏移量為0的地方,size=125表明大小為125字節,bind=GLOBAL表明它是全局符號,type=FUNC:表明它是函數。其他的符號如puts、exit、printf、sleep和getchar都是外部的庫函數,需要在鏈接后才能確定。
圖4-5 hello.o的符號表
4.3.4重定位節
??????
匯編器遇到對最終位置未知的目標引用,會產生一個重定位條目,告訴鏈接器在將目標文件合并成可執行文件時如何修改這個引用。代碼的重定位信息就放在重定位節.rel.text中,已初始化數據的重定位條目放在.rel.data中。
?????? 每個重定位條目包括offset:需要被修改的引用的節偏移;symbol:標識被修改引用應該指向的符號;type:重定位類型,告知鏈接器如何修改新的引用;attend:一些重定位要使用它對被修改引用的值做偏移調整。ELF定義了32種不同的重定位類型,兩種最基本的重定位類型包括R_X86_64_PC32(重定位使用32位PC相對地址的引用)和R_X86_64_32(重定位使用32位絕對地址的引用)。
?????? 以hello.s為例,重定位節.rela.text一共描述了8個重定位條目。重定位節.rela.eh_frame描述了1個重定位條目。
圖4-6 hello.o的重定位節
4.4 Hello.o的結果解析
使用命令objdump -d -r hello.o對hello.o進行反匯編,得到結果如圖。
圖4-7 hello.o的反匯編結果
圖4-8 hello.s
與第3章的 hello.s對比可以發現,hello.s中的匯編指令被映射到二進制的機器語言。機器語言完全是二進制代碼構成的,機器可以直接根據二進制代碼執行對應的操作。不同的匯編指令被映射到不同的二進制功能碼,而匯編指令的操作數也被映射成二進制的操作數。因此每一條匯編語言的指令都可以映射到一條機器語言指令,而給出任何一條合法的機器語言指令也可以得知它對應的匯編指令。從匯編語言轉換成機器語言的過程中,一些操作數會出現不一致的情況:
?????? (1)立即數的變化:hello.s中的立即數都是用10進制數表示的。
但是在機器語言中,由于轉換成了二進制代碼,因此立即數都是用16進制數表示的。
?????? (2)分支轉移的不一致:hello.s中的分支轉移(即跳轉指令)直接通過像.LC0,.LC1這樣的助記符進行跳轉,會直接跳轉到相應符號聲明的位置。
助記符只是幫助程序員理解的,從匯編語言轉換成機器語言之后,助記符就不再存在了,因此機器語言中的跳轉使用的是確定的地址。下圖中的main+0x29就表明要跳轉到距main函數偏移量為0x29的位置。
(3)函數調用的不一致:hello.s中的函數調用直接在call指令后面加上要調用的函數名。
但是在機器語言中,call指令后是被調函數的PC相對地址。在這里,由于調用的函數都是庫函數,需要在動態鏈接后才能確定被調函數的確切位置,因此call指令后的二進制碼為全0,同時需要在重定位節中添加重定位條目,在鏈接時確定最終的相對地址。
4.5 本章小結
本章介紹了匯編的概念和作用,通過對比hello.s和hello.o分析了匯編的過程,同時分析了可重定位目標文件的ELF格式。
(第4章1分)
第5章 鏈接
5.1 鏈接的概念與作用
?????? 鏈接是將各種代碼和數據的片段收集并組合成一個單一文件的過程,這個文件可被加載(復制)到內存并執行。鏈接可以執行于編譯時,也就是在源代碼被翻成機器代碼時;也可以執行于加載時,也就是在程序被加載器加載到內存并執行時;甚至執行于運行時,也就是由應用程序來執行。在現代系統中,鏈接由鏈接器程序自動執行。鏈接包括兩個主要任務:符號解析和重定位。
鏈接是十分重要,不可或缺的,在軟件開發中扮演著一個關鍵的角色,因為它使得分離編譯成為可能。無需將一個大型的應用程序組織成一個巨大的源文件,而是可以把它分解為更小、更好管理的模塊,可以獨立地修改和編譯這些模塊,極大地提高了大型程序編寫的效率。
5.2 在Ubuntu下鏈接的命令
鏈接的命令:ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o
鏈接過程如圖所示:
圖5-1 鏈接命令
5.3 可執行目標文件hello的格式
使用readelf命令readelf -a hello > helloldelf.txt查看可執行目標文件hello的ELF格式,并將結果重定向到helloldelf.txt便于查看分析。
圖5-2 查看hello ELF格式的命令
5.3.1 ELF頭
ELF頭以一個16字節的目標序列開始,如圖中Magic所示,這個序列描述了生成該文件的系統的字的大小和字節順序。以hello為例,這個16字節序列為7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00,描述了系統的字的大小為8字節,字節順序為小端序。
ELF頭剩下的部分包括ELF頭的大小、目標文件的類型、機器類型、節頭部表的文件偏移,以及節頭部表中條目的大小和數量。以hello為例,ELF頭中包含了ELF頭的大小:64字節;目標文件的類型:EXEC(可執行文件);機器類型:Advanced Micro Devices X86-64;節頭部表的文件偏移:4032bytes;節頭部表中條目的數量:25。同時,ELF頭中還包括程序的入口點(偏移量64字節),即程序運行時要執行的第一條指令的地址。
圖5-3 hello的ELF頭
5.3.2?節頭部表
節頭部表描述不同節的位置和大小,目標文件中的每個節都有一個固定大小的節頭部表條目。
?????? 與hello.o相比,hello的節頭部表一共描述了25個不同節的位置、大小等信息,比hello.o多出12個節。各節的起始地址由偏移量給出,同時也給出了大小等信息。
圖5-4 hello的節頭部表
5.3.3?程序頭部表
?????? 程序頭部表描述了可執行文件的連續的片映射到連續的內存段的映射關系。包括目標文件的偏移、段的讀寫/執行權限、內存的開始地址、對齊要求、段的大小、內存中的段大小等。
?????? 以hello中的第一個LOAD為例,Offset說明段的偏移量為0;VirtAddr說明映射到的虛擬內存段的開始地址是0x400000;FileSiz說明段的大小為0x720字節;Memsiz說明內存中的段大小也是0x720字節;Flags為R E,標志段的權限為只讀且可執行;Align說明段的對齊要求為200000。
圖5-5 hello的程序頭部表
5.3.4?符號表
符號表存放程序中定義和引用的函數和全局變量的信息,每個符號表是一個條目的數組,每個條目包括value:距定義目標的節的起始位置的偏移;size:目標的大小;type:指明數據還是函數;bind:表示符號是本地的還是全局的等等。
?????? hello的符號表一共描述了48符號,比hello.o多出32個符號。多出的符號都是鏈接后產生的庫中的函數以及一些必要的啟動函數。
圖5-6 hello的符號表
?????? hello中還多出了一個動態符號表,表中的符號都是共享庫中的函數,需要動態鏈接。
圖5-7 hello的動態符號表
5.3.5?重定位節
重定位條目包括offset:需要被修改的引用的節偏移;symbol:標識被修改引用應該指向的符號;type:重定位類型,告知鏈接器如何修改新的引用;attend:一些重定位要使用它對被修改引用的值做偏移調整。ELF定義了32種不同的重定位類型,兩種最基本的重定位類型包括R_X86_64_PC32(重定位使用32位PC相對地址的引用)和R_X86_64_32(重定位使用32位絕對地址的引用)。
在hello中,原來的.rela.text節已經沒有了,說明鏈接的過程已經完成了對.rela.text的重定位操作。Hello中出現了6個新的重定位條目。這些重定位條目都和共享庫中的函數有關,因為此時還沒有進行動態鏈接,共享庫中函數的確切地址仍是未知的,因此仍然需要重定位節,在動態鏈接后才能確定地址。
圖5-8 hello的重定位節
5.4 hello的虛擬地址空間
使用edb加載hello,可以看到進程的虛擬地址空間各段信息。可以看出,段的虛擬空間從0x400000開始,到0x400ff0結束。
圖5-9 edb查看hello的虛擬地址空間
?????? 由5.3中的節頭部表可以獲得各個節的偏移量信息,從而得知各節在虛擬地址空間中的地址。
?????? 例如,對于.interp節,節頭部表中給出了它的偏移量為0x1c8,大小為0x1c字節。
因此它的虛擬地址空間就從0x4001c8開始,在edb中查看該虛擬內存地址,可以看出,.interp節確實在這個位置。
圖5-10 edb查看.interp的虛擬地址
類似地,對于.rodata節,節頭部表中給出了它的偏移量為0x600,大小為0x2f字節。
因此它的虛擬地址空間就從0x400600開始,在edb中查看該虛擬內存地址,可以看出,.rodata節確實在這個位置,程序中的兩個字符串常量就存儲在這里。
圖5-11 edb查看.rodata的虛擬地址
對于.data節,節頭部表中給出了它的偏移量為0x900,大小為0x8字節。
因此它的虛擬地址空間就從0x400900開始,在edb中查看該虛擬內存地址,可以看出,.data節確實在這個位置,程序中的全局變量sleepsecs就存儲在這里,并且值為2。
圖5-12 edb查看.data的虛擬地址
對于.text節,節頭部表中給出了它的偏移量為0x4d0,大小為0x122字節。
因此它的虛擬地址空間就從0x4004d0開始,在edb中查看該虛擬內存地址,可以看出,.text節確實在這個位置,第一條指令的二進制機器碼的第一個字節為0x31。
圖5-13 edb查看.text的虛擬地址
?????? 對于其他的節同理,不再贅述。
5.5 鏈接的重定位過程分析
使用命令objdump -d -r hello > hellold.txt對hello進行反匯編,并將結果重定向到hellold.txt中便于查看分析。hello與hello.o的不同之處在于以下幾個方面:
(1) hello中的匯編代碼已經使用虛擬內存地址來標記了,從0x400000開始;而hello.o中的匯編代碼是從0開始的,還沒有涉及到虛擬內存地址。
(2)在hello.o中,只存在main函數的匯編指令;而在hello中,由于鏈接過程中發生重定位,引入了其他庫的各種數據和函數,以及一些必需的啟動/終止函數,因此hello中除了main函數的匯編指令外,還包括大量其他的指令。
(3)main函數中涉及重定位的指令的二進制代碼被修改。在之前匯編的過程中,匯編器遇到對最終位置未知的目標引用,會產生一個重定位條目,告訴鏈接器在將目標文件合并成可執行文件時如何修改這個引用。因此在鏈接的過程中,鏈接器會根據重定位條目以及已知的最終位置對修改指令的二進制碼,這個過程就是重定位的過程。下面以hello.o為例,說明hello如何進行重定位。
查看hello.o中的重定位條目,重定位條目給出了需要被修改的引用的節偏移、重定位類型、偏移調整等信息。
圖5-14 hello.o的重定位節
這里涉及到兩種不同的重定位類型,分別是R_X86_64_PC32(重定位使用32位PC相對地址的引用)和R_X86_64_32(重定位使用32位絕對地址的引用)。對于第一種重定位類型,以第一個條目為例,第一個條目的信息說明需要重定位的位置在.text中偏移量為0x1b的地方。在hello.o中找到相應的位置:
圖5-15 條目1的重定位位置
這條指令的目的是將某一個數傳送到%edi中,使其作為printf的參數。由源程序可知,這個指令對應與語句為 printf("Usage: Hello 學號 姓名!\n"); 因此參數應該是字符串常量"Usage: Hello 學號 姓名!\n"的地址。由于字符串常量的最終位置未知,因此產生了一個重定位條目。而重定位的目的就是修改這個數據,使得傳入%edi的是"Usage: Hello 學號 姓名!\n"的最終地址。同時,重定位類型為R_X86_64_32,因此地址為絕對地址。由5.4可知,該字符串常量的地址為0x400604,因此重定位會將這條指令的最后四個字節改為04 06 40 00(小端形式的地址)。查看hello的反匯編結果,確實是這樣的。
圖5-16 條目1的重定位結果
對于第二種重定位類型,以第二個條目為例,第二個條目的信息說明需要重定位的位置在.text中偏移量為0x1b的地方。在hello.o中找到相應的位置:
圖5-17 條目2的重定位位置
這條指令的目的是調用函數puts。由于函數puts的最終位置未知,因此產生了一個重定位條目。而重定位的目的就是修改這個數據,使得call指令的地址為puts函數的起始地址。同時,重定位類型為R_X86_64_PC32,因此地址為相對地址。從hello的反匯編結果可以獲得puts函數的地址為0x400460。
圖5-18 puts函數的位置
而這條call指令的地址為0x400514,它的下一條指令的地址為0x400519.
圖5-19 call指令的位置
因此相對地址為0x400460 – 0x400519 = 0xffffff47。因此重定位會將這條指令的最后四個字節改為47 ff ff ff(小端形式的地址)。查看hello的反匯編結果,確實是這樣的。
圖5-20 條目2的重定位結果
5.6 hello的執行流程
從加載hello到_start,到call main,以及程序終止的所有過程中調用的子程序名以及程序地址(調用順序為從上到下):
| 名稱 | 地址 |
| ld-2.23.so!_dl_start | 0x7f7c8a4619b0 |
| ld-2.23.so! dl_init | 0x7f7c8a470780 |
| hello!_start | 0x4004d0 |
| hello!__libc_start_main | 0x400480 |
| libc-2.23.so!__libc_start_main | 0x7f7c8a0b6750 |
| libc-2.23.so! cxa_atexit | 0x7f7c8a0d0290 |
| hello!__libc_csu_init | 0x400580 |
| hello!_init | 0x400430 |
| libc-2.23.so!_setjmp | 0x7f7c8a0cb260 |
| libc-2.23.so!_sigsetjmp | 0x7f7c8a0cb1c0 |
| hello!main | 0x4004fa |
| hello!puts@plt | 0x400460 |
| hello!exit@plt | 0x4004a0 |
| hello!printf@plt | 0x400470 |
| hello!sleep@plt | 0x4004b0 |
| hello!getchar@plt | 0x400490 |
| ld-2.23.so!_dl_runtime_resolve_avx | 0x7f7c8a477870 |
| libc-2.23.so!exit | 0x7f4ea0c8d5b0 |
5.7 Hello的動態鏈接分析
當程序調用一個由共享庫定義的函數時,編譯器沒有辦法預測這個函數的運行時地址,因為定義它的共享模塊在運行時可以加載到任意位置。正常的方法是為該引用生成一條重定位記錄,然后動態鏈接器在程序加載的時候再解析它。但這需要鏈接器修改調用模塊的代碼段,GNU編譯系統使用一種稱為延遲綁定的技術將過程地址的綁定推遲到第一次調用該過程時。
延遲綁定是通過兩個數據結構之間的交互來實現的,分別是GOT和PLT,GOT是數據段的一部分,而PLT是代碼段的一部分。PLT與GOT的協作可以在運行時解析函數的地址,實現函數的動態鏈接。
過程鏈接表PLT是一個數組,每個條目是16字節代碼。PLT[0]是一個特殊條目,跳轉到動態鏈接器中。每個被可執行程序調用的庫函數都有它自己的PLT條目。由5.3.2中的節頭部表知,存儲PLT的.plt節的開始地址為0x400450.
在hello的反匯編結果中可以查看到每個PLT條目。PLT[0]是一個特殊條目,跳轉到動態鏈接器中。接下來每個條目對應一個調用的庫函數,例如PLT[1]對應的是puts函數;PLT[2]對應的是printf函數……
圖5-21 hello的PLT條目
全局偏移量表GOT是一個數組,每個條目為8字節地址,和PLT聯合使用。GOT[0]和GOT[1]包含動態鏈接器在解析函數地址時會使用的信息,GOT[2]是動態鏈接器在1d-linux.so模塊中的入口點。其余的每個條目對應于一個被調用的函數,其地址需要在運行時被解析。每個條目都有一個相匹配的PLT條目。由5.3.2中的節頭部表知,存儲GOT的.got.plt節的開始地址為0x6008b8.
在edb中查看初始時的GOT條目(如圖5-22)。除了PLT[0]外,每個PLT對應的GOT條目初始時都指向這個PLT的第二條指令。例如:如圖5-21,PLT[1]對應地址0x6008d0處的GOT[3],而0x6008d0處的值為0x400466,恰好指向PLT[1]的第二條指令。在函數第一次被調用時,動態鏈接器會修改相應的GOT條目。
圖5-22 hello的初始GOT條目
同時也可以看到,GOT[1]和GOT[2]這兩個條目初始時均為0。而GOT[1]應該包含動態鏈接器在解析函數地址時會使用的信息,GOT[2]應該為動態鏈接器在1d-linux.so模塊中的入口點。使用edb調試,當dl_start函數返回后,發現這兩個條目被修改為正確的值。
圖5-23 dl_start后的GOT條目
在函數第一次被調用時,動態鏈接器會修改相應的GOT條目。以puts函數為例,puts函數對應的是PLT[1],PLT[1]對應地址0x6008d0處的GOT[3],而GOT[3]的初始值為0x400466,指向PLT[1]的第二條指令。當第一次調用puts時,動態鏈接器確定puts的運行時位置,用這個地址重寫GOT[3]。這時,puts函數才真正完成動態鏈接,后續對puts的調用就可以直接根據GOT[3]的值進行跳轉。
圖5-24 第一次調用puts后的GOT條目
5.8 本章小結
本章介紹了鏈接的概念與作用,簡要分析了可執行文件的ELF格式,hello的虛擬地址空間和執行流程,同時詳細地分析了靜態鏈接的重定位過程以及動態鏈接的過程。至此,一個完美的生命——hello誕生了。
(第5章1分)
第6章 hello進程管理
6.1 進程的概念與作用
進程的經典定義就是一個執行中程序的實例。系統中的每個程序都運行在某個進程的上下文中。通過進程的概念提供給我們一個假象,就好像我們的程序是系統中運行的唯一的程序;程序好像獨占地使用處理器和內存;處理器好像是無間斷地一條接一條地執行程序中的指令;程序的代碼和數據好像是系統內存中唯一的對象。
其中上下文是由程序正確運行所需的狀態組成的,包括存放在內存中的程序的代碼和數據,它的棧,通用目的寄存器的內容,程序計數器,環境變量以及打開文件描述符的集合。
6.2 簡述殼Shell-bash的作用與處理流程
shell是指為使用者提供操作界面的軟件,是一個交互型應用級程序,它接收用戶命令,然后調用相應的應用程序。shell是系統的用戶界面,提供了用戶與內核進行交互操作的接口。
shell的作用:shell最重要的功能是命令解釋,可以說shell是一個命令解釋器。Linux系統上的所有可執行文件都可以作為shell命令來執行,同時它也提供一些內置命令。此外,shell還包括通配符、命令補全、命令歷史、重定向、管道、命令替換等很多功能。
shell的處理流程:從終端讀入輸入的命令行->解析輸入的命令行,獲得命令行指定的參數->檢查命令是否是內置命令,如果是內置命令則立即執行,否則在搜索路徑里尋找相應的程序,找到該程序就執行它。
6.3 Hello的fork進程創建過程
當在shell中輸入命令“./hello 1190200817 劉小川”時,shell解析輸入的命令行,獲得命令行指定的參數。由于./hello不是shell內置的命令,因此shell將hello看作一個可執行目標文件,在相應路徑里尋找hello程序,找到該程序就執行它。shell會通過調用fork()函數創建一個子進程,新創建的子進程幾乎但不完全與父進程相同。子進程得到與父進程用戶級虛擬地址空間相同但獨立的一個副本,包括代碼段、數據段、堆、共享庫以及用戶棧。子進程還獲得與父進程任何打開文件描述符相同的副本,子進程可以讀寫父進程中打開的任何文件。父進程和子進程之間最大的區別在于它們的PID不同。hello程序之后就會運行在這個新創建的子進程的上下文中。
6.4 Hello的execve過程
shell創建一個子進程之后,這個子進程仍然是父進程的一個副本,因此需要在子進程中調用exceve()函數在當前進程的上下文中加載并運行我們需要的hello程序。execve函數加載并運行可執行文件filename,且帶參數列表argv和環境變量envp。只有當出現錯誤時,例如找不到filename,execve才會返回到調用程序。
execve函數用hello程序有效替代當前程序,需要以下幾個步驟:
(1)刪除已存在的用戶區域。刪除當前進程虛擬地址的用戶部分中的已存在的區域結構。
(2)映射私有區域。為新程序(即hello)的代碼、數據、bss和棧區域等創建新的區域結構。所有這些區域都是私有的、寫時復制的。代碼和數據區域被映射為hello文件中的.text和.data區。bss區域是請求二進制零的,映射到匿名文件,其大小包含在hello中。棧和堆區域也是請求二進制零的,初始長度為零。
(3)映射共享區域。如果hello程序與共享對象(或目標)鏈接,那么這些對象都是動態鏈接到這個程序的,然后再映射到用戶虛擬地址空間中的共享區域內。
(4)設置程序計數器。最后設置當前進程上下文中的程序計數器,使之指向代碼區域的入口點。
當內核調度這個進程時,它就將從這個入口點開始執行。Linux根據需要換入代碼和數據頁面。
6.5?Hello的進程執行
當子進程調用exceve()函數在上下文中加載并運行hello程序后,hello程序不會立即運行,需要內核調度它。進程調度是由內核中稱為調度器的代碼處理的。當內核選擇一個新的進程運行時,就說內核調度了這個進程。在內核調度了一個新的進程運行后,它就搶占當前進程,使用一種稱為上下文切換的機制來將控制轉移到新的進程。上下文切換包括:保存當前進程的上下文;恢復某個先前被搶占的進程被保存的上下文;將控制傳遞給這個新恢復的進程。其中上下文指的是內核重新啟動一個被搶占的進程所需的狀態。它由一些對象的值組成,包括通用目的寄存器、浮點寄存器、程序計數器、用戶棧、狀態寄存器、內核棧和各種內核數據結構(頁表、進程表、文件表等)。
處理器提供了一種機制,限制一個應用可以執行的指令以及它可以訪問的地址空間范圍。通常用某個控制寄存器的一個模式位來提供這種機制,該寄存器描述了進程當前享有的特權。當設置了模式位時,進程運行在內核模式中,進程可以執行指令集中的任何指令,并且可以訪問系統中的任何內存位置;沒有設置模式位時,進程運行在用戶模式中,進程不允許執行特權指令,也不允許直接引用地址空間中內核區內的代碼和數據,否則會導致保護故障。運行應用程序代碼的進程初始時在用戶模式中,進程需要通過中斷、故障或者陷入系統調用這樣的異常才能從用戶模式變為內核模式。
由于負責進程調度的是內核,因此內核調度需要運行在內核模式下。當內核代表用戶執行系統調用時,可能會發生上下文切換,中斷也可能引發上下文切換。同時,系統通過某種產生周期性定時器中斷的機制判斷當前進程已經運行了足夠長的時間,并切換到一個新的進程。
以hello的進程執行為例。當子進程調用exceve()函數在上下文中加載并運行hello程序后,hello進程等待內核調度它。當內核決定調度hello進程時,它就搶占當前進程,進行上下文切換,將控制轉移到hello進程,并從內核模式變為用戶模式,這時hello進程開始運行應用程序代碼。當hello進程調用sleep時,由于sleep是系統調用,進程陷入內核模式。這時hello進程被掛起,內核會選擇調度其他進程,通過上下文切換保存hello進程的上下文,將控制傳遞給新調度的進程。定時器的時間到了后會發送中斷信號,進入內核模式,將掛起的hello進程變成運行狀態,這時hello進程就可以等待內核調度它。當內核再次調度hello進程時,恢復保存的hello進程的上下文,就可以從剛才停止的地方繼續執行了。當hello調用getchar的時候同樣會陷入內核模式,由于getchar需要來自鍵盤的DMA傳輸,時間很長,因此內核不會等待DMA完成,而是去調度其他進程。當DMA完成后,會向處理器發送中斷信號,進入內核模式,內核知道DMA完成了,就可以再次調度hello進程了。
6.6 hello的異常與信號處理
6.6.1可能出現的異常及處理方法
hello執行過程中,四類異常都可能會出現,四類異常分別為:
| 類別 | 原因 | 異步/同步 | 返回行為 |
| 中斷 | 來自I/O設備的信號 | 異步 | 總是返回到下一條指令 |
| 陷阱 | 有意的異常 | 同步 | 總是返回到下一條指令 |
| 故障 | 潛在可恢復的錯誤 | 同步 | 可能返回到當前指令 |
| 終止 | 不可恢復的錯誤 | 同步 | 不會返回 |
hello執行過程中發生中斷:如果其他進程使用了外部I/O設備,那么在hello進程運行時可能會出現外部I/O設備引起的中斷。中斷的處理:將控制傳遞給適當的中斷處理程序,處理程序返回時,就將控制返回給下一條指令,程序繼續執行,好像沒有發生過中斷一樣。
hello執行過程中發生陷阱:hello中調用了系統調用sleep,產生陷阱。陷阱的處理:將控制傳遞給適當的異常處理程序,處理程序解析參數,調用適當的內核程序。處理程序返回時,將控制返回給下一條指令。
hello執行過程中發生故障:當hello進程剛從入口點開始執行時,會發生缺頁故障。hello進程運行的過程中,也可能發生缺頁故障。故障的處理:將控制傳遞給故障處理程序,如果處理程序能夠修正這個錯誤情況,就將控制返回到引起故障的指令并重新執行它;否則終止引起故障的應用程序。
hello執行過程中發生錯誤:hello執行過程中,DRAM或者SRAM可能發生位損壞,產生奇偶錯誤。發生錯誤時會將控制傳遞給終止處理程序,終止引起錯誤的應用程序。
6.6.2可能產生的信號及處理方法
hello執行過程中,可能產生的信號如:SIGINT,SIGTSTP,SIGCHLD,SIGKILL,SIGALRM等等。進程接受到信號時,會觸發控制傳遞到信號處理程序,信號處理程序運行,信號處理程序返回后,將控制返回給被中斷的程序。每個信號類型有相關聯的默認行為,使用signal函數可以修改和信號相關聯的行為。
下面以hello的運行過程為例,簡要說明異常與信號的處理。
(1)程序運行過程中不停亂按鍵盤,包括回車。如果亂按不包括回車,輸入的字符串會緩存到緩沖區;如果輸入的最后是回車,則getchar會讀進回車,把回車前的字符串作為輸入shell的命令,
圖6-1 程序運行過程中不停亂按鍵盤,包括回車
(2)程序運行過程中鍵入Ctrl-Z。鍵入Ctrl-Z會發送SIGTSTP信號給前臺進程組的每個進程,結果是停止前臺作業,也就是停止hello進程。
圖6-2 鍵入Ctrl-Z
使用jobs命令可以查看當前的作業,可以看出當前的作業是hello進程,且狀態是已停止
圖6-3 jobs命令
使用ps命令可以查看當前所有進程以及它們的PID,進程包括bash,hello以及ps。
圖6-4 ps命令
使用pstree命令將所有進程以樹狀圖形式顯示。
圖6-5 pstree命令
使用fg命令可以使停止的hello進程繼續在前臺運行。也可以再次鍵入Ctrl-Z停止hello的運行。
圖6-6 fg命令
使用kill命令可以給指定進程發送信號。比如 kill -9 8329 是指向PID為8329的進程(即hello)發送SIGKILL信號。這個命令會殺死hello進程,當再次使用ps時可以發現hello進程已經被殺死,使用jobs指令也看不到當前的作業了。
圖6-7 kill命令
?????? (3)程序運行過程中鍵入Ctrl-C。鍵入Ctrl-C會發送SIGINT信號給前臺進程組的每個進程,結果是終止前臺進程,即終止hello進程。
圖6-8 鍵入Ctrl-C
使用ps命令可以發現,hello進程已經終止并被回收,不再存在了。使用jobs指令也看不到當前的作業了。
圖6-9 ps,jobs命令
6.7本章小結
本章介紹了進程的概念和作用,簡述shell的工作過程,并分析了使用fork+execve加載運行hello,執行hello進程以及hello進程運行時的異常/信號處理過程。
(第6章1分)
第7章 hello的存儲管理
7.1 hello的存儲器地址空間
邏輯地址:邏輯地址是指由程序產生的與段相關的偏移地址部分。例如,在進行C語言指針編程中,可以使用&操作讀取指針變量的值,這個值就是邏輯地址,是相對于當前進程數據段的地址。一個邏輯地址由兩部份組成:段標識符和段內偏移量。
線性地址:線性地址是邏輯地址到物理地址變換之間的中間層。程序代碼會產生邏輯地址,或者說是段中的偏移地址,加上相應段的基地址生成了一個線性地址。如果啟用了頁式管理,那么線性地址可以再變換產生物理地址。若沒有啟用頁式管理,那么線性地址直接就是物理地址。
虛擬地址:因為虛擬內存空間的概念與邏輯地址類似,因此虛擬地址和邏輯地址實際上是一樣的,都與實際物理內存容量無關。
物理地址:存儲器中的每一個字節單元都給以一個唯一的存儲器地址,用來正確地存放或取得信息,這個存儲器地址稱為物理地址,又叫實際地址或絕對地址。
7.2 Intel邏輯地址到線性地址的變換-段式管理
邏輯地址由段標識符和段內偏移量兩部分組成。段標識符由一個16位長的字段組成,稱為段選擇符。其中前13位是一個索引號,是對段描述符表的索引,每個段描述符由8個字節組成,具體描述了一個段。后3位包含一些硬件細節,表示具體是代碼段寄存器還是棧段寄存器還是數據段寄存器等。通過段標識符的前13位,可以直接在段描述符表中索引到具體的段描述符。每個段描述符中包含一個Base字段,它描述了一個段的開始位置的線性地址。將Base字段和邏輯地址中的段內偏移量連接起來就得到轉換后的線性地址。
對于全局的段描述符,放在全局段描述符表中,局部的(每個進程自己的)段描述符,放在局部段描述符表中。全局段描述符表的地址和大小存放在gdtr控制寄存器中,而局部段描述符表存放在ldtr寄存器中。
給定邏輯地址,看段選擇符的最后一位是0還是1,用于判斷選擇全局段描述符表還是局部段描述符表。再根據相應寄存器,得到其地址和大小。通過段標識符的前13位,可以在相應段描述符表中索引到具體的段描述符,得到Base字段,和段內偏移量連接起來最終得到轉換后的線性地址。
7.3 Hello的線性地址到物理地址的變換-頁式管理
頁表是一個頁表條目(PTE)的數組,虛擬地址空間中的每個頁在頁表中一個固定偏移量處都有一個PTE。每個PTE由一個有效位和一個n位地址字段組成,有效位表明該虛擬頁是否被緩存在DRAM中。如果設置了有效位,那么地址字段表示相應的物理頁的起始位置;如果沒有設置有效位,那么空地址表示虛擬頁還未被分配,否則這個地址指向該虛擬頁在磁盤的起始位置。
MMU利用頁表實現從虛擬地址到物理地址的變換。CPU中的一個控制寄存器,頁表基址寄存器指向當前頁表。n位的虛擬地址包含一個p位的虛擬頁面偏移VPO和一個n-p位的虛擬頁號VPN。MMU利用VPN選擇適當的PTE,如果這個PTE設置了有效位,則頁命中,將頁表條目中的物理頁號和虛擬地址中的VPO連接起來就得到相應的物理地址。否則會觸發缺頁異常,控制傳遞給內核中的缺頁異常處理程序。缺頁處理程序確定物理內存中的犧牲頁,調入新的頁面,并更新內存中相應PTE。處理程序返回到原來的進程,再次執行導致缺頁的指令,MMU重新進行地址翻譯,此時和頁命中的情況一樣。同時,也可以利用TLB緩存PTE加速地址的翻譯。
圖7-1 線性地址到物理地址的變換
7.4 TLB與四級頁表支持下的VA到PA的變換
TLB的支持:在MMU中包括一個關于PTE的緩存,稱為翻譯后備緩沖器(TLB)。TLB是一個小的、虛擬尋址的緩存,每一行保存著一個由單個PTE組成的塊。由于VA到PA的轉換過程中,需要使用VPN確定相應的頁表條目,因此TLB需要通過VPN來尋找PTE。和其他緩存一樣,需要進行組索引和行匹配。如果TLB有2t個組,那么TLB的索引TLBI由VPN的t個最低位組成,TLB標記TLBT由VPN中剩余的位組成。
圖7-2 TLB
當MMU進行地址翻譯時,會先將VPN傳給TLB,看TLB中是否已經緩存了需要的PTE,如果TLB命中,可以直接從TLB中獲取PTE,將PTE中的物理頁號和虛擬地址中的VPO連接起來就得到相應的物理地址。這時所有的地址翻譯步驟都是在芯片上的MMU中執行的,因此非常快。如果TLB不命中,那和7.3中描述的過程類似,需要從cache或者內存中取出相應的PTE。
圖7-3 TLB支持下的線性地址到物理地址的變換
四級頁表的支持:多級頁表可以用來壓縮頁表,對于k級頁表層次結構,虛擬地址的VPN被分為k個,每個VPNi是一個到第i級頁表的索引。當1≤j≤k-1時,第j級頁表中的每個PTE指向某個第j+1級頁表的基址。第k級頁表中的每個PTE和未使用多級頁表時一樣,包含某個物理頁面的PPN或者一個磁盤塊的地址。對于Intel Core i7,使用了4級頁表,每個VPNi有9位。當TLB未命中時,36位的VPN被分為VPN1、VPN2、VPN3、VPN4,每個VPNi被用作到一個頁表的偏移量。CR3寄存器包含L1 頁表的物理地址,VPN1提供到一個L1 PTE的偏移量,這個PTE包含某個L2頁表的基址。VPN2提供到這個L2頁表中某個PTE的偏移量,以此類推。最后得到的L4 PTE包含了需要的物理頁號,和虛擬地址中的VPO連接起來就得到相應的物理地址。
圖7-4 四級頁表支持下的線性地址到物理地址的變換
7.5?三級Cache支持下的物理內存訪問
當MMU完成了從虛擬地址到物理地址的轉換后,就可以使用物理地址進行內存訪問了。Intel Core i7使用了三級cache來加速物理內存訪問,L1級cache作為L2級cache的緩存,L2級cache作為L3級cache的緩存,而L3級cache作為內存(DRAM)的緩存。
進行物理內存訪問時,會首先將物理地址發送給L1級cache,看L1級cache中是否緩存了需要的數據。L1級cache共64組,每組8行,塊大小64B。因此將物理地址分為三部分,塊偏移6位,組索引6位,剩下的為標記位40位。首先利用組索引位找到相應的組;然后在組中進行行匹配,對于組中的8個行,分別查看有效位并將行的標記位與物理地址的標記位匹配,當標記位匹配且有效位是1時,緩存命中,根據塊偏移位可以直接將cache中緩存的數據傳送給CPU。如果緩存不命中,需要繼續從存儲層次結構中的下一層中取出被請求的塊,將新塊存儲在相應組的某個行中,可能會替換某個緩存行。
L1級cache不命中時,會繼續向L2級cache發送數據請求。和L1級cache的過程一樣,需要進行組索引、行匹配和字選擇,將數據傳送給L1級cache。同樣L2級cache不命中時,會繼續向L3級cache發送數據請求。最后,L3級cache不命中時,只能從內存中請求數據了。
值得注意的是,三級cache不僅僅支持數據指令的訪問,也支持頁表條目的訪問,在MMU進行虛擬地址到物理地址的翻譯過程中,三級cache也會起作用。
圖7-5 三級Cache下的物理內存訪問
7.6 hello進程fork時的內存映射
當fork函數被當前進程調用時,內核為新進程創建各種數據結構,并分配給它一個唯一的PID。內核給新進程創建虛擬內存,創建當前進程的mm_struct、區域結構和頁表的原樣副本,將兩個進程中的每個頁面都標記為只讀,并將兩個進程中的每個區域結構都標記為私有的寫時復制。
當fork在新進程中返回時,新進程現在的虛擬內存剛好和調用fork 時存在的虛擬內存相同。當這兩個進程中的任一個后來進行寫操作時,寫時復制機制就會創建新頁面,為每個進程保持了私有地址空間的抽象概念。同時延遲私有對象中的副本直到最后可能的時刻,充分利用了稀有的物理內存。
7.7 hello進程execve時的內存映射
exceve()函數在當前進程的上下文中加載并運行我們需要的hello程序。execve函數加載并運行可執行文件filename,且帶參數列表argv和環境變量envp。只有當出現錯誤時,例如找不到filename,execve才會返回到調用程序。
execve函數用hello程序有效替代當前程序,需要以下幾個步驟:
(1)刪除已存在的用戶區域。刪除當前進程虛擬地址的用戶部分中的已存在的區域結構。
(2)映射私有區域。為新程序(即hello)的代碼、數據、bss和棧區域等創建新的區域結構。所有這些區域都是私有的、寫時復制的。代碼和數據區域被映射為hello文件中的.text和.data區。bss區域是請求二進制零的,映射到匿名文件,其大小包含在hello中。棧和堆區域也是請求二進制零的,初始長度為零。
(3)映射共享區域。如果hello程序與共享對象(或目標)鏈接,那么這些對象都是動態鏈接到這個程序的,然后再映射到用戶虛擬地址空間中的共享區域內。
(4)設置程序計數器。最后設置當前進程上下文中的程序計數器,使之指向代碼區域的入口點。
當內核調度這個進程時,它就將從這個入口點開始執行。Linux根據需要換入代碼和數據頁面。
圖7-6 execve時的內存映射
7.8 缺頁故障與缺頁中斷處理
缺頁故障的產生:CPU產生一個虛擬地址給MMU,MMU經過一系列步驟獲得了相應的PTE,當PTE的有效位未設置時,說明虛擬地址對應的內容還沒有緩存在內存中,這時MMU會觸發缺頁故障。
缺頁故障的處理:缺頁異常導致控制轉移到內核的缺頁處理程序。處理程序隨后執行以下步驟:(1)判斷虛擬地址是否合法。缺頁處理程序搜索區域結構的鏈表,把虛擬地址和每個區域結構中的vm_start和vm_end做比較。如果指令不合法,缺頁處理程序會觸發一個段錯誤,從而終止這個進程。(2)判斷內存訪問是否合法。比如缺頁是否由一條試圖對只讀頁面進行寫操作的指令造成的。如果訪問不合法,缺頁處理程序會觸發一個保護異常,從而終止這個進程。(3)這時,內核知道缺頁是由合法的操作造成的。內核會選擇一個犧牲頁面,如果這個犧牲頁面被修改過,那么就將它交換出去,換入新的頁面并更新頁表。處理程序返回時,CPU重新執行引起缺頁的指令,這條指令將再次發送給MMU。這次,MMU能正常地進行地址翻譯,不會再產生缺頁中斷了。
圖7-7 缺頁中斷處理
7.9動態存儲分配管理
動態內存分配器維護著一個進程的虛擬內存區域,稱為堆。系統之間的細節不同,但不失通用性,假設堆是一個請求二進制零的區域,緊接在未初始化數據區域后開始,向上生長。對每個進程,內核維護一個全局變量brk指向堆頂。分配器將堆視為一組不同大小的塊的集合來維護。每個塊是一個連續的虛擬內存片,要么是已分配的,要么是空閑的。已分配的塊顯式地保留,供應用程序使用;空閑塊可用來分配。空閑塊保持空閑,直到空閑塊顯式地被應用所分配。一個已分配的塊保持已分配狀態,直到它被釋放,這種釋放要么是應用程序顯式執行的(即顯式分配器),要么是內存分配器自身隱式執行的(即隱式分配器)。顯式分配器和隱式分配器是動態內存分配器的兩種基本風格。兩種風格都要求應用顯式地分配塊,不同之處在于由哪個實體來負責釋放已分配的塊。顯式分配器要求應用顯式地釋放任何已分配的塊。隱式分配器要求分配器檢測一個已分配塊何時不再被程序所使用,那么就釋放這個塊。隱式分配器也叫做垃圾收集器,而自動釋放未使用的已分配的塊的過程叫做垃圾收集。
圖7-8 動態內存分配的區域——堆
顯式分配器必須在一些約束條件下工作:處理任意請求序列;立即響應請求;只使用堆;對齊要求;不修改已分配的塊。在這些限制條件下,分配器試圖實現吞吐率最大化和內存使用率最大化,但這兩個性能目標通常是相互沖突的。
分配器的具體操作過程以及相應策略:
(1)放置已分配塊:當一個應用請求一個k字節的塊時,分配器搜索空閑鏈表。查找一個足夠大可以放置所請求的空閑塊。執行這種搜索的常見策略包括首次適配、下一次適配和最佳適配等。
(2)分割空閑塊:一旦分配器找到了匹配的空閑塊,需要決定分配這個空閑塊中多少空間。可以選擇用整個塊,但會造成額外的內部碎片;也可以選擇將空閑塊分割為兩部分,第一部分變成已分配塊,剩下的變成新的空閑塊。
(3)獲取額外的堆內存:如果分配器不能為請求塊找到空閑塊,分配器通過調用sbrk函數,向內核請求額外的堆內存。分配器將額外的內存轉化成一個大的空閑塊,將這個塊插到空閑鏈表中,然后被請求的塊放在這個新的空閑塊中。
(4)合并空閑塊:分配器釋放一個已分配塊時,要合并相鄰的空閑塊。分配器決定何時執行合并,可以選擇立即合并或者推遲合并。合并時需要合并當前塊和前面以及后面的空閑塊。
組織空閑塊的形式有很多,包括隱式空閑鏈表、顯式空閑鏈表、分離的空閑鏈表等等。
帶邊界標簽的隱式空閑鏈表分配器:一個塊由一個字的頭部、有效載荷、可能的一些額外的填充以及一個腳部。頭部位于塊的開始,編碼了這個塊的大小(包括頭部、腳部和所有的填充)以及這個塊是已分配的還是空閑的。由于對齊要求,頭部的高位可以編碼塊的大小,而剩余的幾位(取決于對齊要求)總是零,可以編碼其他信息。使用最低位作為已分配位,指明這個塊是已分配的還是空閑的。腳部位于每個塊的結尾,是頭部的一個副本,是為了方便釋放塊時的合并操作。頭部后面就是調用分配器時請求的有效載荷,有效載荷后面是一片不使用的填充塊,其大小可以是任意的。填充的原因取決于分配器的策略。如果塊的格式是如上所述,就可以將堆組織成一個連續的已分配塊和空閑塊的序列,這種結構為隱式空閑鏈表。空閑塊通過頭部的大小字段隱含地連接,可以通過遍歷堆中所有的塊間接遍歷整個空閑塊的集合。同時,需要一個特殊標記的結束塊(設置分配位而大小為零的頭部),這種設置簡化了空閑塊合并。
圖7-9 隱式鏈表的塊結構
顯式空間鏈表:已分配塊的塊結構和隱式鏈表的相同,由一個字的頭部、有效載荷、可能的一些額外的填充以及一個腳部組成。而在每個空閑塊中,增加了一個前驅指針和后繼指針。通過這些指針,可以將空閑塊組織成一個雙向鏈表。空閑鏈表中塊的排序策略包括后進先出順序、按照地址順序維護、按照塊的大小順序維護等。顯式空閑鏈表降低了放置已分配塊的時間,但空閑塊必須足夠大,以包含所需要的指針、頭部和腳部,這導致了更大的最小塊大小,潛在提高內部碎片程度。
圖7-10 顯式鏈表的塊結構
而malloc采用的是分離的空閑鏈表。分配器維護著一個空閑鏈表數組,每個大小類一個空閑鏈表,按照大小升序排列,當分配器需要一個大小為n的塊時,就搜索相應大小類對應的空閑鏈表。如果不能找到合適的塊,就搜索下一個鏈表,以此例推。
7.10本章小結
本章總結了hello運行過程中有關內存管理的內容。簡述了TLB、多級頁表支持下的地址翻譯、cache支持下的內存訪問、缺頁的處理、fork+execve過程的內存映射以及動態存儲分配的過程。
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO設備管理方法
一個Linux文件就是一個m個字節的序列:
B0,B1 ,B2……Bm-1
所有的 IO 設備(例如網絡、磁盤和終端)都被模型化為文件,所有的輸入和輸出都被當做對相應文件的讀和寫來執行。這種將設備優雅地映射為文件的方式,允許 Linux內核引出一個簡單、低級的應用接口,稱為 Unix I/O,使得所有的輸入和輸出都能以一種統一且一致的方式來執行。
設備的模型化:文件
設備管理:unix io接口
8.2 簡述Unix IO接口及其函數
Unix IO 接口,使得所有的輸入和輸出都能以一種統一且一致的方式來執行:
(1)打開文件。一個應用程序通過要求內核打開相應的文件,來宣告它想要訪問一個 I/O 設備。內核返回一個小的非負整數,即描述符,它在后續對此文件的所有操作中標識這個文件。內核記錄有關這個打開文件的所有信息。應用程序只需記住這個描述符。
(2)Linux shell創建的每個進程開始時都有三個打開的文件:標準輸入(描述符為0)、標準輸出(描述符為1)和標準錯誤(描述符為2)。頭文件中的常量可以代替顯式的描述符值。
(3)改變當前的文件位置:對于每個打開的文件,內核保持著一個文件位置 k,初始為0。這個文件位置是從文件開頭起始的字節偏移量。應用程序能夠通過執行seek操作,顯式地設置文件的當前位置為k。
(4)讀寫文件:一個讀操作就是從文件復制n>0個字節到內存,從當前文件位置k開始,然后將k增加到k+n。給定一個大小為m字節的文件,當k≥m時執行讀操作會觸發EOF條件,應用程序能檢測到這個條件。類似地,寫操作就是從內存復制n>0個字節到一個文件,從當前文件位置k開始,然后更新k。
(5)關閉文件:當應用完成了對文件的訪問之后,它就通知內核關閉這個文件。作為響應,內核釋放文件打開時創建的數據結構,并將這個描述符恢復到可用的描述符池中。無論一個進程因為何種原因終止時,內核都會關閉所有打開的文件并釋放它們的內存資源。
Unix I/O函數:
(1)進程通過調用open函數打開一個存在的文件或者創建一個新文件。
int open(char* filename,int flags,mode_t mode);
open函數將filename轉換為一個文件描述符,并且返回描述符數字。返回的描述符總是在進程中當前沒有打開的最小描述符。flags參數指明了進程打算如何訪問這個文件;mode參數指定了新文件的訪問權限位。
(2)進程通過調用close函數關閉一個打開的文件。
int closefd;
fd是需要關閉的文件描述符,成功返回0,錯誤返回-1。關閉一個已關閉的描述符會出錯。
(3)應用程序通過分別調用read和write函數來執行輸入和輸出。
ssize_t read(int fd,void *buf,size_t n);
ssize_t wirte(int fd,const void *buf,size_t n);
read函數從描述符為fd的當前文件位置復制最多n個字節到內存位置buf。返回值-1表示一個錯誤,而返回值0表示EOF。否則返回值表示的是實際傳送的字節數量。write函數從內存位置buf復制至多n個字節到描述符fd的當前文件位置。
8.3 printf的實現分析
printf的源代碼:
int?printf(const?char?*fmt,?...)
{
????int?i;
????char?buf[256];
????va_list?arg?=?(va_list)((char*)(&fmt)?+?4);
????i?=?vsprintf(buf,?fmt,?arg);
????write(buf,?i);
????
????return?i;
}
printf函數是格式化輸出函數, 一般用于向標準輸出設備按規定格式輸出信息。printf中調用了兩個函數,分別為vsprintf和write。
vsprintf函數根據格式串fmt,并結合args參數產生格式化之后的字符串結果保存在buf中,并返回結果字符串的長度。
write函數將buf中的i個字符寫到終端,由于i保存的是結果字符串的長度,因此write將格式化后的字符串結果寫到終端。
字符顯示驅動子程序:從ASCII到字模庫到顯示vram(存儲每一個點的RGB顏色信息)。
顯示芯片按照刷新頻率逐行讀取vram,并通過信號線向液晶顯示器傳輸每一個點(RGB分量)。
8.4 getchar的實現分析
getchar的源代碼:
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;
}
getchar函數會從stdin輸入流中讀入一個字符。調用getchar時,會等待用戶輸入,輸入回車后,輸入的字符會存放在緩沖區中。第一次調用getchar時,需要從鍵盤輸入,但如果輸入了多個字符,之后的getchar會直接從緩沖區中讀取字符。getchar的返回值是讀取字符的ASCII碼,若出錯則返回-1。
異步異常-鍵盤中斷的處理:鍵盤中斷處理子程序。接受按鍵掃描碼轉成ascii碼,保存到系統的鍵盤緩沖區。
getchar等調用read系統函數,通過系統調用讀取按鍵ascii碼,直到接受到回車鍵才返回。
8.5本章小結
本章介紹了Linux的IO設備管理方法和Unix IO接口及其函數,并分析了printf和getchar函數的實現。
(第8章1分)
結論
hello所經歷的過程:
源程序:在文本編輯器或IDE中編寫C語言代碼,得到最初的hello.c源程序。
預處理:預處理器解析宏定義、文件包含、條件編譯等,生成ASCII碼的中間文件hello.i。
編譯:編譯器將C語言代碼翻譯成匯編指令,生成一個ASCII匯編語言文件hello.s。
匯編:匯編器將匯編指令翻譯成機器語言,并生成重定位信息,生成可重定位目標文件hello.o。
鏈接:鏈接器進行符號解析、重定位、動態鏈接等創建一個可執行目標文件hello。此時,hello才真正地可以被執行。
fork創建進程:在shell中運行hello程序時,shell會調用fork函數創建子進程,供之后hello程序的運行。
execve加載程序:子進程中調用execve函數,加載hello程序,進入hello的程序入口點,hello終于要開始運行了。
運行階段:內核負責調度進程,并對可能產生的異常及信號進行處理。MMU、TLB、多級頁表、cache、DRAM內存、動態內存分配器相互協作,共同完成內存的管理。Unix I/O使得程序與文件進行交互。
終止:hello進程運行結束,shell負責回收終止的hello進程,內核刪除為hello進程創建的所有數據結構。hello的一生到此結束,沒有留下一絲痕跡。
對計算機系統的設計與實現的深切感悟:
hello從誕生到結束,經歷了千辛萬苦,在硬件、操作系統、軟件的相互協作配合下,終于完美地完成了它的使命。這讓我認識到,一個復雜的系統需要多方面的協作配合才能更好地實現功能。同時,計算機系統提供的一系列抽象使得實際應用與具體實現相互分離,可以很好地隱藏實現的復雜性,降低了程序員的負擔,使得程序更加容易地編寫、分析、運行。這讓我認識到抽象是十分重要的,是計算機科學中最為重要的概念之一。
(結論0分,缺失 -1分,根據內容酌情加分)
附件
hello.i:C預處理器產生的一個ASCII碼的中間文件,用于分析預處理過程。
hello.s:C編譯器產生的一個ASCII匯編語言文件,用于分析編譯的過程。
hello.o:匯編器產生的可重定位目標程序,用于分析匯編的過程。
hello:鏈接器產生的可執行目標文件,用于分析鏈接的過程。
hello.txt:hello.o的反匯編文件,用于分析可重定位目標文件hello.o。
hellold.txt:hello的反匯編文件,用于分析可執行目標文件hello。
helloelf.txt:hello.o的ELF格式,用于分析可重定位目標文件hello.o。
helloldelf.txt:hello的ELF格式,用于分析可執行目標文件hello。
(附件0分,缺失 -1分)
參考文獻
為完成本次大作業你翻閱的書籍與網站等
[1]? RANDALE.BRYANT, DAVIDR.O‘HALLARON. 深入理解計算機系統[M]. 機械工業出版社, 2011.
[2] ?https://www.cnblogs.com/clover-toeic/p/3851102.html
[3]? https://www.runoob.com/linux/linux-comm-pstree.html
[4] ?https://www.runoob.com/cprogramming/c-function-vsprintf.html
[5]? https://www.cnblogs.com/pianist/p/3315801.html
(參考文獻0分,缺失 -1分)
總結
以上是生活随笔為你收集整理的2021哈工大计算机系统大作业——程序人生-Hello’s P2P的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 网络带宽的分类
- 下一篇: 将 5G 应用于工业物联网