1. 程式人生 > >CSAPP大作業 2018 Hello's P2P

CSAPP大作業 2018 Hello's P2P

電腦科學與技術學院
2018年12月
摘 要
在電腦科學的發展中,大部分程式猿都是通過hello.c這一簡單的程式來接觸程式設計。然而正是因為hello的單純與淺顯沒有讓程式猿感到“至少40%”的神祕,它便遭遇冷落甚至無視。難道它真的如同它的表象,簡單得不像是實力派嗎?還真不是:僅僅這樣一個簡單的程式,就毫無漏洞地向我們展示了整個計算機系統的工作歷程以及一個程式完整的生命週期,並形象地解釋了計算機系統許多內在的概念。在本文中,我們以第三人稱的口吻,通過跟蹤hello程式的生命週期來漫遊該程式的“一生”——從它出生(被建立)開始,到它艱苦奮鬥(在系統上執行),達到人生巔峰(輸出簡單的訊息),最後實現中國夢,歸隱山林(終止)。

關鍵詞:程式;系統;生命週期;建立;執行;終止

(摘要0分,缺失-1分,根據內容精彩稱都酌情加分0-1分)

目 錄

第1章 概述 - 4 -
1.1 HELLO簡介 - 4 -
1.2 環境與工具 - 4 -
1.3 中間結果 - 4 -
1.4 本章小結 - 4 -
第2章 預處理 - 5 -
2.1 預處理的概念與作用 - 5 -
2.2在UBUNTU下預處理的命令 - 5 -
2.3 HELLO的預處理結果解析 - 5 -
2.4 本章小結 - 5 -
第3章 編譯 - 6 -
3.1 編譯的概念與作用 - 6 -
3.2 在UBUNTU下編譯的命令 - 6 -
3.3 HELLO的編譯結果解析 - 6 -
3.4 本章小結 - 6 -
第4章 彙編 - 7 -
4.1 彙編的概念與作用 - 7 -
4.2 在UBUNTU下彙編的命令 - 7 -
4.3 可重定位目標ELF格式 - 7 -
4.4 HELLO.O的結果解析 - 7 -
4.5 本章小結 - 7 -
第5章 連結 - 8 -
5.1 連結的概念與作用 - 8 -
5.2 在UBUNTU下連結的命令 - 8 -
5.3 可執行目標檔案HELLO的格式 - 8 -
5.4 HELLO的虛擬地址空間 - 8 -
5.5 連結的重定位過程分析 - 8 -
5.6 HELLO的執行流程 - 8 -
5.7 HELLO的動態連結分析 - 8 -
5.8 本章小結 - 9 -
第6章 HELLO程序管理 - 10 -
6.1 程序的概念與作用 - 10 -
6.2 簡述殼SHELL-BASH的作用與處理流程 - 10 -
6.3 HELLO的FORK程序建立過程 - 10 -
6.4 HELLO的EXECVE過程 - 10 -
6.5 HELLO的程序執行 - 10 -
6.6 HELLO的異常與訊號處理 - 10 -
6.7本章小結 - 10 -
第7章 HELLO的儲存管理 - 11 -
7.1 HELLO的儲存器地址空間 - 11 -
7.2 INTEL邏輯地址到線性地址的變換-段式管理 - 11 -
7.3 HELLO的線性地址到實體地址的變換-頁式管理 - 11 -
7.4 TLB與四級頁表支援下的VA到PA的變換 - 11 -
7.5 三級CACHE支援下的實體記憶體訪問 - 11 -
7.6 HELLO程序FORK時的記憶體對映 - 11 -
7.7 HELLO程序EXECVE時的記憶體對映 - 11 -
7.8 缺頁故障與缺頁中斷處理 - 11 -
7.9動態儲存分配管理 - 11 -
7.10本章小結 - 12 -
第8章 HELLO的IO管理 - 13 -
8.1 LINUX的IO裝置管理方法 - 13 -
8.2 簡述UNIX IO介面及其函式 - 13 -
8.3 PRINTF的實現分析 - 13 -
8.4 GETCHAR的實現分析 - 13 -
8.5本章小結 - 13 -
結論 - 14 -
附件 - 15 -
參考文獻 - 16 -

第1章 概述
1.1 Hello簡介
1.1.1 hello的P2P
P2P: From Program to Process
hello的program是從#include <stdio.h>……到……return 0; }的程式碼集合;
hello.c依次經過:
(1) 前處理器cpp(將hello.c變為hello.i➡ascii碼文字檔案)
(2) 編譯器gcc(將hello.i變為hello.s➡含有彙編程式碼的組合語言程式)
(3) 彙編器as(將hello.s變為hello.o➡可重定位目標檔案)
(4) 連結器ld(生成Linux下的a.out格式的可執行檔案)
最後,在shell中,程序管理為這個可執行檔案fork一個子程序(process)。

1.1.2 hello的020
020: From Zero-0 to Zero-0
建立子程序之後,系統呼叫execve函式啟動新的程式;
(1) 核心為該程序“建立”了虛擬記憶體的佈局,建立記憶體和磁碟檔案之間的對映;
(2) 執行開始時,程式通過缺頁異常,將資料拷貝到實體記憶體;
(3) 從main函式開始,執行目的碼;
(4) 執行結束後,父程序回收該子程序,核心釋放子程序的記憶體。

1.2 環境與工具
1.2.1 硬體環境
x64 CPU; 2.7GHz; 8G RAM; 128GHD Disk
1.2.2 軟體環境
macOS Mojave; Ubuntu 16.04
1.2.3 開發工具
Xcode; vim; gcc; edb; objdump; readelf

1.3 中間結果
hello.i:由前處理器翻譯成的文字檔案
hello.s:由編譯器編譯成的組合語言檔案
hello.o:由彙編器翻譯成的可重定位目標檔案
helloo.txt:hello.o檔案由objdump形成的反彙編程式碼檔案的輸出重定向
helloaout.txt:hello檔案由objdump形成的反彙編程式碼檔案的輸出重定向
hellooelf.txt:hello.o檔案的elf全部資訊的輸出重定向
helloaoutelf.txt:hello檔案的elf 全部資訊的輸出重定向。

1.4 本章小結
本章描述了hello程式的執行框架:預處理➡編譯➡彙編➡連結➡執行➡回收,高度概括了hello的一生“做了什麼”,“被做了什麼”。

(第1章0.5分)

第2章 預處理
2.1 預處理的概念與作用
2.1.1 預處理的概念
預處理:在編譯之前進行的處理。C語言的預處理主要有三個方面的內容:
(1) 巨集定義
(2) 檔案包含
(3) 條件編譯
預處理命令以符號“#”開頭。[1]

2.1.2 預處理的作用
預處理過程讀入原始碼,檢查包含預處理指令的語句和巨集定義,並對原始碼進行相應的轉換。預處理過程還會刪除程式中的註釋和多餘的空白字元。[2]

2.2在Ubuntu下預處理的命令
命令:cpp hello.c -o hello.i
在這裡插入圖片描述
圖2.2-1 預處理命令及生成的hello.i

2.3 Hello的預處理結果解析
2.3.1 預編譯程式讀出原始碼,對其中內嵌的指示字進行響應,產生原始碼的修改版本,修改後的版本會被編譯程式讀入。(即產生的hello.i程式,總共3126行!)
在這裡插入圖片描述
圖2.3-1 預處理結果檔案hello.i

預編譯過程主要處理那些原始碼檔案中以“#”開始的預編譯命令。比如“#include”、“#define”等,主要處理規則如下:
(1) 將所有的“#define”刪除,並且展開所有的巨集定義;
(2) 處理所有條件預編譯指令,比如“#if”、“#ifdef”、“#elif”、“#else”、“#endif”;
(3) 處理“#include”預編譯指令,將被包含的檔案插入到該預編譯指令的位置。注意,這個過程是遞迴執行的,也就是說被包含檔案可能還包含其他檔案;
(4) 刪除所有的註釋“//”和“/* */”;
(5) 新增行號和檔名標識,如下圖,以便於編譯時編譯器產生除錯用的行號資訊及用於編譯時產生編譯錯誤或警告時能夠顯示行號;
在這裡插入圖片描述
圖2.3-2 新增行號和檔名標識

(6) 保留所有的#program編譯器指令,因為編譯器需要使用它們。

注:經過預編譯後的.i檔案不包含任何巨集定義,因為所有的巨集已經被展開,並且包含的檔案也已經被插入到.i檔案中。所以當我們無法判斷巨集定義是否正確或標頭檔案包含是否正確的時候,可以檢視預編譯後的檔案來確定問題。[3]

2.4 本章小結
任何一個看似簡單的程式的預處理都需要大量的工作!在完成本章的過程中,我對我們計算機初學者在程式設計中常用的預處理命令有了全新的理解。例如,#include後面標頭檔案的檔名的表示形式:可以是尖括號或雙引號。尖括號是到系統規定的路徑去尋找該檔案,雙引號則是預處理使用者的第三方檔案。另外,我還認識到,還可以使用巨集定義#define來定義一些簡單函式(也就是說巨集定義中可以包含變數),但是要注意運算子的優先順序。比如,如果要定義一個函式func(a, b) = ab,#define func(a, b) ab和#define func(a, b) (a)(b)是不同的。巨集定義只是簡單地進行字串替換。如果要計算func(1+2, 3),前者計算的是1+23,後者計算的是(1+2)*3!

(第2章0.5分)

第3章 編譯
3.1 編譯的概念與作用
3.1.1 編譯的概念
把C語言轉換成組合語言的過程。

3.1.2 編譯的作用
把.i檔案編譯成.s檔案(.s檔案是組合語言源程式)。

3.2 在Ubuntu下編譯的命令
命令:gcc -S hello.i -o hello.s
在這裡插入圖片描述
圖3.2-1 編譯命令及生成的hello.s

3.3 Hello的編譯結果解析
3.3.1 常量
本程式唯一的常量只有第18行的字串:
在這裡插入圖片描述
圖3.3-1 唯一的字串常量

它對應於hello.s中的下列程式碼:
在這裡插入圖片描述
圖3.3-2 hello.s中的字串常量

(1) 斜槓“\”加三個數字表明:字串被編碼成UTF-8的格式,每個漢字佔3個位元組,每個“\”加3個數字表示1個位元組,後面的3個數字均採用8進位制數表示;
(2) 容易發現,這裡總共有12個位元組。因為在原始碼中,三個漢字後的“!”為中文的“!”而不是英文的”!”,所以“!”也要佔3個位元組。

3.3.2 變數
(1) 全域性變數
該程式唯一的全域性變數定義如下:
在這裡插入圖片描述
圖3.3-3 唯一的全域性變數

它對應於hello.s中的下列程式碼:
在這裡插入圖片描述
圖3.3-4 hello.s中的全域性變數

a. 第2行,.globl宣告sleepsecs為全域性型別的變數;
b. 第3行,由於sleepsecs已經被賦初值,所以會在.data節中宣告該變數;
c. 第4行,在.data節中,設定對齊方式為4;
d. 第5行,設定該變數型別為object(物件);
e. 第6行,設定該型別變數大小為4位元組;
f. 第8行,本是int型別的sleepsecs被宣告為long型別(在32位機器上,long與int同義),且值為2;(不是2.5是因為sleepsecs被宣告為整型,卻被賦值為小數,此時編譯器會自動忽略掉該初值的小數部分,僅把整數部分的2賦值給sleepsecs)

(2) 區域性變數
a. 變數i
變數i所在的for迴圈採用“跳轉到中間”的跳轉方式,如圖所示:
在這裡插入圖片描述
圖3.3-5 hello.s中的變數i

由圖可知,變數i存放在主函式的棧幀%rbp-4的位置處。jle指令表明,當i<=9即i<10時,跳轉到.L4繼續執行迴圈,否則迴圈結束。

b. 變數argc
argc是由外部輸入決定的變數,它代表命令列中字串的個數,這裡的所說的字串中間以空白字元分隔。根據下圖可知,argc也被存在main函式的棧幀中:
在這裡插入圖片描述
圖3.3-6 hello.s中的變數argc

該彙編語句對應原C程式碼中對argc和3的比較。程式開始時,argc被儲存在暫存器%edi中。

c. 變數argv[]
argv[]也是由外部輸入決定的變數,它是一個二級指標變數。它對應如下彙編程式碼:
在這裡插入圖片描述
圖3.3-7 hello.s中的argv

如圖,列印字串“Hello %s %s\n”, argv[1], argv[2]時,對應上面的語句。這裡呼叫printf而不呼叫puts是因為該字串含有兩個%s控制符,而且它不是一個字串常量。上圖第41行和第44行分別表示把%rax中儲存的記憶體地址處的值傳遞給%rdx和%rax,即argv[1]和argv[2]。第47行把0傳遞給%eax,這是main函式的返回值。

(3) 操作
a. 賦值運算
本程式中使用的賦值運算有:對sleepsecs賦值為2.5(實際得到的值為2)、對迴圈索引i進行從0到9的賦值、隱含地對argc和argv[]賦值(由外部輸入決定)。
sleepsecs和i都屬於整形變數,賦值操作均用mov指令來完成:
在這裡插入圖片描述
圖3.3-8 簡單的資料傳送指令[4]

注:圖中1個字代表2個位元組

b. 關係運算
本程式中使用的關係運算有argc和3的“!=”判斷、i和10的“<”判斷。
關係運算表示式的值要麼為真,要麼為假。表示式為真時,這個表示式也對應一個整數值1,為假時這個表示式對應一個整數值0。在程式設計中使用條件語句和迴圈語句時,有時可以省略關係說明,直接利用表示式的真值作為迴圈終止條件。
以上C程式碼中的關係運算得到結果後,在組合語言中都會設定相應的條件碼,程式再根據“最近”設定的條件碼進行跳轉(通常跳轉語句都緊貼在比較和測試語句的後一條語句):

在這裡插入圖片描述
圖3.3-9 彙編中的比較和測試指令[4]

在這裡插入圖片描述
圖3.3-10 彙編中的跳轉指令[4]

c. 算術運算
本程式中使用的(整數)算術運算有迴圈中對迴圈索引i的增一操作。算數運算對應的組合語言指令如下:
在這裡插入圖片描述
圖3.3-11 彙編中的整數算術運算[4]

d. 型別轉換
本程式中使用的型別轉換隻有將sleepsecs的2.5轉換為2,為隱式型別轉換(編譯器自動忽略小數部分)。該操作在一開始宣告全域性變數的時候就完成了:
在這裡插入圖片描述
圖3.3-12 hello.s中的型別轉換

第8行直接將其賦值為2,而不是2.5。

e.陣列和指標操作
本程式中使用的陣列/指標操作只有二級指標(指標陣列)argv[]。圖3.3-7已經分析過,兩次將%rax儲存的記憶體地址處的值傳到暫存器,作為字串的一部分(%s格式控制的部分輸出)。另外,正因為它是二級指標(指標陣列),該陣列中存的是相應字串的“地址”,所以還要再進行一次定址(可能是基址定址,也可能是偏移量定址。本程式中採用基址定址(%rax))。

f. 控制轉移
本程式中有兩處控制轉移:if語句和for語句,對應如下彙編程式碼:


在這裡插入圖片描述
圖3.3-13 hello.s中的控制轉移

if語句和for語句對應的控制轉移語句均為條件跳轉。

g. 函式操作
本程式中使用的函式有:main、printf、exit、sleep、getchar
函式引數均在相應函式的棧幀中儲存。每次轉移到相應的子函式棧幀時,都需要讓舊的%rbp入棧,再讓此時的%rbp去尋找此時的%rsp,這樣就轉移到了子函式的棧幀。以main函式為例:
在這裡插入圖片描述
圖3.3-14 main函式棧幀起始操作

  1. 引數傳遞
    main函式的引數:argc和argv[]。由圖3.3-7知,這兩個引數儲存在主函式的棧幀中。當呼叫main函式時,棧頂指標%rsp為main函式開闢一段新的棧空間,直到程式結束再釋放這段棧空間。子函式類似。
  2. 函式呼叫
    函式呼叫指令為call+函式名,如下圖所示:
    在這裡插入圖片描述
    圖3.3-15 hello.s中的call指令

上圖便是呼叫puts函式和exit函式的指令。當執行call指令時,原指令的下一條指令地址IP會壓入棧,此時控制轉移到相應的子函式。與call指令相對應的是ret指令。當執行ret指令時,會把原call指令的下一條指令的地址彈回IP,開始執行原call指令的下一條指令。

  1. 函式返回
    本程式中的函式返回只有主函式的“return 0”。
    大多數情況下,函式的返回值都儲存在特定的暫存器%eax(%rax)中,本程式將返回值0傳遞給%eax作為返回值,如下圖:
    在這裡插入圖片描述
    圖3.3-16 hello.s中的函式返回

函式返回後,已執行完的函式棧幀會被釋放,恢復棧幀為呼叫前的狀態。上圖leave指令執行該操作,leave與每個函式彙編指令的前兩條恰好相反,它讓%rsp去尋找%rbp,然後把棧頂值彈回%rbp(即之前壓入棧的舊的%rbp)。

3.4 本章小結
本章我們深入到低階的組合語言來漫遊hello.c“編譯”的過程。我們分別從hello程式的不同變數型別和操作型別,觀察了組合語言的執行過程。我們可以發現,各種各樣的C程式碼寫出的程式是有很大共性的:邏輯上近似相同的C程式碼,即使具體實現方式不同,編譯成彙編程式碼的時候,它們的指令也會有很大一部分重疊。本章使我們深入探索“程式的機器級表示”,深入到暫存器,深入到系統核心,深入到記憶體。

(第3章2分)

第4章 彙編
4.1 彙編的概念與作用
4.1.1 彙編的概念
彙編是把編譯階段生成的“.s”檔案轉成“.o”格式的目標檔案。

4.1.2 彙編的作用
輸入組合語言源程式,檢查語法的正確性。如果正確,則將源程式翻譯成等價的二進位制或浮動二進位制的機器語言程式,並根據使用者的需要輸出源程式和目標程式的對照清單;如果語法有錯,則輸出錯誤資訊,指明錯誤的部位、型別和編號。最後,對已彙編出的目標程式進行善後處理。[5]

4.2 在Ubuntu下彙編的命令
命令:as hello.s -o hello.o
在這裡插入圖片描述
圖4.2-1 彙編命令及生成的hello.o檔案

4.3 可重定位目標elf格式
(1) 理論上典型的elf格式檔案包含的內容:
在這裡插入圖片描述
圖4.3-1 典型的ELF可重定位目標檔案[4]

上圖展示了一個典型的ELF可重定位目標檔案的格式。ELF頭(ELF header)以一個16位元組的序列開始,這個序列描述了生成該檔案的系統的字的大小和位元組順序。ELF頭剩下的部分包含幫助連結器語法分析和解釋目標檔案的資訊。其中包括ELF頭的大小、目標檔案的型別(如可重定位、可執行或者共享的)、機器型別(如x86-64)、節頭部表(section header table)的檔案偏移,以及節頭部表中條目的大小和數量。不同節的位置和大小是由節頭部表描述的,其中目標檔案中每個節都有一個固定大小的條目(entry)。
夾在ELF頭和節頭部表之間的都是節。一個典型的ELF可重定位目標檔案包含下面幾個節:
.text:已編譯程式的機器程式碼。
.rodata:只讀資料,比如printf語句中的格式串和開關語句的跳轉表。
.data:已初始化的全域性和靜態C變數。區域性C變數在執行時被儲存在棧中,既不出現在.data節中,也不出現在.bss節中。
.bss:未初始化的全域性和靜態C變數,以及所有被初始化為0的全域性或靜態變數。在目標檔案中這個節不佔據實際的空間,它僅僅是一個佔位符。目標檔案格式區分已初始化和未初始化變數是為了空間效率:在目標檔案中,未初始化變數不需要佔據任何實際的磁碟空間。執行時,在記憶體中分配這些變數,初始值為0。
.symtab:一個符號表,它存放在程式中定義和引用的函式和全域性變數的資訊。一些程式設計師錯誤地認為必須通過-g選項來編譯一個程式,才能得到符號表資訊。實際上,每個可重定位目標檔案在.symtab中都有一張符號表(除非程式設計師特意用STRIP命令去掉它)。然而,和編譯器中的符號表不同,.symtab符號表不包含區域性變數的條目。
.rel.text:一個.text節中位置的列表,當連結器把這個目標檔案和其他檔案組合時,需要修改這些位置。一般而言,任何呼叫外部函式或者引用全域性變數的指令都需要修改。另一方面,呼叫本地函式的指令則不需要修改。注意,可執行目標檔案中並不需要重定位資訊,因此通常省略,除非使用者顯式地指示連結器包含這些資訊。
.rel.data:被模組引用或定義的所有全域性變數的重定位資訊。一般而言,任何已初始化的全域性變數,如果它的初始值是一個全域性變數地址或者外部定義函式的地址,都需要被修改。
.debug:一個除錯符號表,其條目是程式中定義的區域性變數和型別定義,程式中定義和引用的全域性變數,以及原始的C原始檔。只有以-g選項呼叫編譯器驅動程式時,才會得到這張表。
.line:原始C源程式中的行號和.text節中機器指令之間的對映。只有以-g選項呼叫編譯器驅動程式時,才會得到這張表。
.strtab:一個字串表,其內容包括.symtab和.debug節中的符號表,以及節頭部中的節名字。字串表就是以null結尾的字串的序列。[4]

(2) hello.o的ELF格式
readelf命令:readelf -a readelf.o(-a表示檢視所有資訊)
a. ELF header
在這裡插入圖片描述
圖4.3-2 hello.o的ELF頭

該表的作用與上述(1)典型的ELF可重定位目標檔案完全相同。

b. header sections
在這裡插入圖片描述
圖4.3-3 hello.o的節頭部表

hello.o的節頭部表描述了不同節的型別、地址、偏移量等資訊。

c. .rela.text
在這裡插入圖片描述
圖4.3-4 hello.o的.rela.text節

offset是指相對可重定位text或data段的偏移量,當彙編器生成一個目標模組時,它並不知道資料和程式碼最終將存放在儲存器中的什麼位置,它也不知道這個模組引用其他模組的函式或者全域性變數的位置。所以就生成了一個重定位表,告訴聯結器在將目標檔案合併成可執行檔案時如何修改這個引用,這個重定位表就告訴連結器需要重定位的地方。 [6]
在這裡插入圖片描述
圖4.3-5 生成重定位表的過程[6]

觀察圖4.3-4最右面一列,這一列出現了hello程式中的函式名及全域性變數的名字。最後一列是hello中所有需要重定位的函式和變數的宣告。其中兩個.rodata表示兩個字串常量(因為是隻讀的);

d. .rela.eh_frame
在這裡插入圖片描述
圖4.3-6 hello.o的.rela.eh_frame節

e. .symtab
在這裡插入圖片描述
圖4.3-7 hello.o的.symtab節

該符號表的作用與上述(1)典型的可重定位目標檔案ELF相同。

4.4 Hello.o的結果解析
命令:objdump -d -r hello.o
在這裡插入圖片描述
圖4.4-1 用objdump檢視hello.o的反彙編

在這裡插入圖片描述
圖4.4-2 hello的彙編與反彙編的main函式比較

上圖中左側是hello.s組合語言檔案,右側是objdump得到的hello.o的反彙編檔案。比較以上兩者可以看出它們的區別有如下幾點:
a. (機器語言的運算元與組合語言不一致).s檔案中,立即數通常是用10進製表示的,而反彙編檔案中,所有的立即數及機器碼都是用十六進位制表示的;
b. 觀察劃黑線的第一行,.s檔案中$.LC0對應反彙編中的 0 x 0 c . 調 . s 調 c a l l + . s . L 1 , . L 2 使 c a l l + 調 4.3 4 T y p e 4.3 4 S y m . N a m e + A d d e n d d . . s 0x0; c. (分支轉移函式呼叫)觀察劃黑線的第二行,.s檔案中呼叫函式的命令是call+函式名,在.s檔案的各個部分設定瞭如.L1, .L2等分支轉移的標誌。而反彙編中使用的是call+相對於該函式起始位置的偏移量來轉移控制,同時,緊接著,在下面一條指令中宣告即將呼叫的函式的型別(圖4.3-4的Type)和函式的宣告(圖4.3-4的Sym.Name + Addend); d. 最後一處劃黑線的地方,.s檔案中 .LC1對應反彙編中的$0x0;
e. .s檔案中不包含機器碼,反彙編中彙編指令左面是每一條指令的機器碼,且同樣用十六進位制表示。
f. 當用上述指令(objdump -d -r hello.o)反彙編hello.o得到的只有該檔案中的.text節,且main函式的起始地址預設設為全0。

4.5 本章小結
本章描述了彙編器將hello.s轉換成hello.o的過程,即彙編。彙編器向我們展示了它是如何將C程式中不同型別的變數及變數名字進行分類儲存的,具體說就是分散儲存在各個節中。同時,本章還看到了objdump工具得到的反彙編程式碼與彙編程式碼的區別:在我看來,彙編就是把.i檔案(ascii碼語言)翻譯成組合語言,再由as(彙編器)把組合語言檔案轉換成.o檔案(機器碼)的過程。而反彙編,仍可以“望文生義”,“反”在將.o檔案逆向轉換成組合語言檔案的過程。總之,彙編和反彙編都是向著組合語言轉換。

(第4章1分)

第5章 連結
5.1 連結的概念與作用
5.1.1 連結的概念
連結(linking)是將各種程式碼和資料片段收集並組合成為一個單一檔案的過程,這個檔案可被夾在(複製)到記憶體並執行。[4]

5.1.2 連結的作用
5.1.1連結的概念已經給出

5.2 在Ubuntu下連結的命令
連結多個.o檔案的命令如下:
ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o hello.o -lc /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/x86_64-linux-gnu/crtn.o -z relro -o a.out
在這裡插入圖片描述
圖5.2-1 Linux下連結生成可執行檔案hello

5.3 可執行目標檔案hello的格式
(1) 理論上典型的elf格式檔案包含的內容:
在這裡插入圖片描述
圖5.3-1 典型的ELF可執行目標檔案

(2) hello程式的elf格式檔案包含的內容:
使用命令:readelf -a hello

在這裡插入圖片描述
在這裡插入圖片描述
圖5.3-2 hello的elf格式中各個節的基本資訊

上圖第一行的Size, Type, Address, Offset分別表示各個節的大小、型別、地址和偏移量。

5.4 hello的虛擬地址空間
在這裡插入圖片描述
圖5.4-1 使用edb載入hello

在這裡插入圖片描述
圖5.4-2 ELF檔案中的Program Headers Table

(1) 通過左下角Data Dump可以檢視hello的虛擬地址空間;
(2) Data Dump視窗處,虛擬地址的變化範圍是0x400000到0x401000,說明該程式被對映到這段虛擬記憶體空間;
(3) 檢視該hello程式的程式頭表,如圖5.4-2所示,該表列出了7個段,這些段組成了最終在記憶體中執行的程式。
PHDR儲存程式頭表;
INTERP指定在程式已經從可執行對映到記憶體之後,必須呼叫直譯器。在這裡直譯器並不意味著二進位制檔案的記憶體必須由另一個程式解釋。它指的是這樣的一個程式:通過連結其他庫,來滿足未解決的引用;
LOAD表示一個從二進位制檔案對映到虛擬地址空間的段。其中儲存了常量資料(如字串),程式的目的碼等等;
DYNAMIC段儲存了其他動態連結器(即,INTERP中指定的直譯器)使用的資訊;
NOTE儲存了專有資訊;
虛擬地址空間的各個段,填充了來自ELF檔案中特定段的資料;
readelf輸出的第二部分制定了哪些節載入到哪些段。[7]

5.5 連結的重定位過程分析
在這裡插入圖片描述
圖5.5-1 可執行檔案hello的反彙編
在這裡插入圖片描述
圖5.5-2 hello的反彙編對比圖

(1) hello與hello.o反彙編的不同
a. hello的反彙編程式碼內容比hello.o的多出如下4個節:
.init:程式初始化需要執行的程式碼
.plt:動態連結-過程連結表
.got.plt:動態連結-全域性偏移量表-存放函式
.fini:程式正常終止時需要執行的程式碼
b. hello中不再使用相對函式入口偏移的地址,而是使用虛擬記憶體地址.

(2) 連結的過程
a. 單獨的hello.o檔案無法獨自變成可執行檔案a.out,它需要和其他.o檔案連結,其中包括crt1.o、crti.o、crtn.o。這三個檔案中主要定義了程式入口_start和初始化函式_init。_start呼叫hello中的main函式,libc.so是動態連結共享庫,其中定義了printf、sleep、getchar、exit函式和_start中呼叫的__libc_csu_init、__libc.csu_fini、__libc_start_main;
b. 在hello的反彙編程式中,call指令呼叫函式時不再使用相對於函式起始位置偏移量的方式,而是使用虛擬地址記憶體的方式。在反彙編左側機器碼中,call後面函式前的地址為call的下一條指令的地址與call指令機器碼後面值的補碼運算和,即call的機器碼(e8)後面的值為目標函式的地址與call的下一條語句指令地址的差值:
在這裡插入圖片描述
圖5.5-3 hello反彙編中函式的呼叫變化

0x400445 + 0x7b == 0x4004c0,同理,jmp指令也如此。

(3) 重定位
重定位由兩步組成:
a. 重定位節和符號定義。在這一步中,連結器將.o檔案中所有型別相同的節合併為同一型別的新的hello可執行檔案的聚合節;
b. 重定位節中的符號引用。在這一步中,連結器修改.o檔案中程式碼節和資料節對每個符號的引用,使得他們指向正確的執行時地址。
當彙編器生成一個目標模組時,它並不知道資料和程式碼最終將存放在儲存器中的什麼位置。它也不知道這個模組引用的任何外部定義的函式或者全域性變數的位置。程式碼的重定位條目放在.rel.text中,已初始化資料的重定位條目放在.rel.data中。

5.6 hello的執行流程
(左側為子程式名稱,右側為地址)
ld-2.23.so!_dl_start 00007f49:1359ac33
ld-2.23.so!_dl_init 00007f49:1359ac65
ld-2.23.so!_dl_lookup_symbol_x 00007f49:1359ace6
ld-2.23.so!_dl_open 00007f49:1359ad55
ld-2.23.so!_dl_catch_error 00007f49:1359ae3a
ld-2.23.so!_dl_dprintf 00007f49:1359ae74
ld-2.23.so!_dl_map_object 00007f49:1359aeb0
………………(以上是載入hello前的子程式,太多了,沒有完全列出,題目要求列出hello!__libc_start_mai[email protected]之後的)
[email protected] 00000000:004004f4
[email protected] 00000000:00400514
[email protected] 00000000:0040051e
[email protected] 00000000:0040054f
[email protected] 00000000:0040055c
[email protected] 00000000:0040056b
hello!_init 00000000:004005ac
qword [r12+rbx*8] 00000000:004005c9

過程說明:hello程式首先進入main函式,判斷argc是否等於3,然後呼叫puts列印字串常量(沒有%s格式控制符,可不用printf),此時呼叫exit,程式以1為返回值退出。否則呼叫printf函式列印for迴圈中的字串,再呼叫sleep函式。迴圈結束後,呼叫getchar函式,最終返回0值。

5.7 Hello的動態連結分析
在這裡插入圖片描述
圖5.7-1 呼叫dl_ini前

在這裡插入圖片描述
圖5.7-2 呼叫dl_init後

如圖,呼叫dl_init函式前後,發生改變的為.got.plt節的內容。該地址由圖5.3-2確定,地址為0x6008c8。

5.8 本章小結
連結聽起來簡單,實際上,過程邏輯還是好理解的,但是需要的準備工作太多。簡單地說,本章連結的內容就是把.o檔案變成最終a.out的可執行檔案。在這過程中,連結器需要在其他模組中尋找變數、把相同型別的段合併、把不同變數重定位。當程式執行起來的時候,連結還會起作用。這時連結器去定位動態連結庫,並把這個庫連結到程序的虛擬地址空間

(第5章1分)

第6章 hello程序管理
6.1 程序的概念與作用
6.1.1 程序的概念
程序(Process)是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。程序是執行緒的容器,是程式的實體。

6.1.2 程序的作用
程序為使用者提供以下假象:我們的程式好像是系統中當前執行的唯一的程式一樣,我們的程式好像是獨佔地使用處理器和記憶體,處理器好像是無間斷地執行我們程式中的指令,我們程式中的程式碼和資料好像是系統記憶體中唯一的物件。

6.2 簡述殼Shell-bash的作用與處理流程
6.2.1 Shell-bash的作用
shell的一項主要功能是在互動方式下解釋從命令列輸入的命令。shell解析命令列,將其分解為詞(也稱為token),詞之間由空白分隔,空白由製表符、空格鍵或換行組成。如果詞中有特別的元字元,shell會對其進行替換。shell處理檔案I/O和後臺程序。對命令列的處理結束後,shell搜尋命令並開始執行他。
shell的另一項重要功能是制定使用者環境,這通常在shell的初始化檔案中完成。初始化檔案中有非常多定義,包括設定終端鍵和視窗屬性,設定用來定義搜尋路徑、許可權、提示符和終端型別的變數,設定特定應用程式所需的變數,如視窗、字處理程式和程式語言的庫等。Korn/Bash shell和C/TC shell還提供了更多的制定功能:歷史新增、別名、設定內建變數防止使用者破壞檔案或無意中退出,通知使用者作業完成。
shell還能用作解釋性的程式語言。shell程式(也稱為shell指令碼)由檔案中的一列命令組成。shell程式用編輯器生成(也能在命令列上直接輸入指令碼)。他們由UNIX命令組成,命令之間插入了一些基本的程式結構,如變數賦值、條件測試和迴圈。shell指令碼不必編譯。shell會逐行解釋指令碼,就似乎他是從鍵盤輸入相同。shell負責解釋命令,因此,使用者需要了解可用的命令有哪些。附錄A中列出了一些有用的命令。
6.2.2 Shell-bash的處理流程
(1) 讀取輸入並解析命令列;
(2) 替換特別字元,比如萬用字元和歷史命令符;
(3) 設定管道、重定向和後臺處理;
(4) 處理訊號;
(5) 程式執行相關設定。
6.3 Hello的fork程序建立過程
(1) 在Linux終端輸入./hello 1171910407 郭奕含,shell對輸入的命令列進行解析:如果字串不是系統內建命令,那麼shell會把它自動當成檔名來處理;
(2) 系統呼叫fork函式,為hello程式建立一個子程序;

注:新建立的子程序與父程序的使用者級虛擬地址空間相同,子程序得到的是父程序虛擬地址空間的一個副本,但父程序與子程序有著不同的pid。
至此,建立完畢。
6.4 Hello的execve過程
execve函式在當前程序的上下文中載入並執行一個新程式。
execve函式載入並執行可執行目標檔案hello,且帶引數列表argv和環境變數列表envp。只有當出現錯誤時,例如找不到hello,execve才會返回到呼叫程式。所以,與fork一次呼叫返回兩次不同,execve呼叫一次並從不返回。
在終端輸入./hello時,由於hello不是一個內建的命令,所以系統會認為hello是一個可執行目標檔案,然後呼叫某個駐留在儲存器中稱為載入器(loader)的作業系統程式碼來執行它。載入器將可執行目標檔案中的程式碼和資料從磁碟拷貝到儲存器中,然後通過跳轉