電腦系統大作業:Hello的一生

電腦系統大作業

題 目 程式人生-Hello』s P2P

專 業 電腦科學與技術

學 號   

班 級 

學 生 江水為竭   

指導教師 劉宏偉   

電腦科學與技術學院

2022年5月

摘 要

HelloWorld是幾乎全世界程式設計師編寫的第一個程式,我們緩慢地輸入HelloWorld 的幾行程式碼,點擊運行,驚喜地看到輸出欄中的「Hello,World!」。但是很快我們就不再理會這個程式,去學習其他的語法。然而,在這個簡單的hello中,囊括一個程式運行的所有過程,蘊含著無數電腦科學家的思想精華。

本文通過跟蹤hello的一生,介紹了hello從程式碼編輯器到運行最後結束的過程,對電腦底層進行了較深的分析。

關鍵詞:電腦系統;Linux;C語言;

 

目 錄

第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 –

結論 – 14 –

附件 – 15 –

參考文獻 – 16 –

第1章 概述

1.1 Hello簡介

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

P2P:在文本編輯器中將hello的程式碼輸入,並保存為.c格式的文件,形成hello.c文件。這就是程式(Program),這是hello程式的生命周期的開始。在OS(例如Linux)中,通過互動式應用程式Shell,輸入命令實現對hello.c從源文件到目標文件的轉化。源程式通過cpp(預處理器)預處理,ccl(編譯器)編譯,as(彙編器)彙編,最後通過ld(鏈接器)鏈接生成hello可執行目標程式並將其保存到磁碟中。

020:在Shell運行該hello程式時,Shell調用fork函數創建子進程,並通過execve函數將hello程式載入並創建運行環境,比如分配虛擬記憶體,運行完成後,Shell回收該進程,釋放記憶體空間。

1.2 環境與工具

硬體環境:X86-64 Intel i7 10th 16 GB RAM 512 GHD Disk

軟體環境:Windows 10 VMware 16 Ubuntu 20.04 LTS

開發調試工具:GDB,EDB,Visual Studio Code,Vim,gcc

1.3 中間結果

  1. hello.c:源程式碼文件。
  2. hello.i:預處理後的文本文件。
  3. hello.s:編譯後的彙編文件。
  4. hello.o:彙編後的可重定位文件。
  5. hello:鏈接後的可執行文件。
  6. disa_hello.s:反彙編hello.o的彙編程式碼。
  7. disa_hello_2.s:反彙編hello的彙編程式碼。

1.4 本章小結

本章對hello程式運行的P2P和020過程進行了簡單的介紹,列出了此次大作業所使用的相關工具和軟硬體環境,最後介紹了文中所用到的文件和作用。

第2章 預處理

2.1 預處理的概念與作用

概念:在預處理時,預處理器需要根據以字元#開頭的命令,修改原始的C程式。其實,就是在對源程式做文本替換的操作。比如hello.c第一行的「#include<stdio.h>」 預處理時預處理器會讀取stdio.h並把它插入到Hello程式中。

作用:在程式碼編寫的過程是為了方便程式設計師而設計這些#開頭的命令,而為了後續編譯器的方便,需要對程式碼進行替換。

2.2在Ubuntu下預處理的命令

預處理命令:gcc -E hello.c -o hello.i

圖1 預處理命令
圖2 文件列表

2.3 Hello的預處理結果解析

打開hello.i查看文件。

開頭時引入外部庫.h文件。

圖3 hello.i文本文件 1

接下來是typedef進行數據類型名稱替換。

圖4 hello.i文本文件 2

引入外部函數:

圖5 hello.i文本文件 3

最後,是我們編寫的main函數程式碼:

圖6 hello.i文本文件 4

2.4 本章小結

本章介紹了hello.c的預處理過程,大致分析了預處理後形成的hello.i文件。可以知道,僅23行的.c文件預處理後的文件竟有3000行。如果編寫一個hello程式需要3000行,這樣的效率是極其低下的。這也就是預處理的意義:能讓我們輕鬆寫出可讀性高,方便修改,利於調試的程式碼。

第3章 編譯

3.1 編譯的概念與作用

概念:編譯是將預處理後的文本文件.i翻譯為彙編語言的文本文件.s。

編譯程式把一個源程式翻譯成目標程式的工作過程分為五個階段:詞法分析,語法分析,語義檢查和中間程式碼生成,程式碼優化,目標程式碼生成。主要是進行詞法分析和語法分析,又稱為源程式分析,分析過程中發現有語法錯誤,給出提示資訊。

作用:將高級程式語言翻譯為統一的,接近機器語言,對機器友好的彙編語言。

3.2 在Ubuntu下編譯的命令

編譯命令:gcc -S hello.i -o hello.s

圖7 編譯命令
圖8 文件列表

3.3 Hello的編譯結果解析

編譯生成了hello.s文件,為彙編程式碼,該小節將針對hello程式具體介紹編譯器怎麼處理C語言中各個數據類型以及各類操作的。

常量

    字元串常量:

    ”用法: Hello 學號 姓名 秒數!\n”

    ”Hello %s %s\n”。

    彙編文件在開頭為.LC0和.LC1中存放這兩個字元串,其中一個漢字對應一個\xxx。這些常量存放在.rodata節,意為只讀數據。

圖9 hello.s文件 1

    立即數常量

    如for循環中9這個常量:

圖10 hello.s文件 2

變數

  局部變數

        「int i」

        局部變數存放在暫存器或棧中。

        查看彙編程式碼:

圖11 hello.s文件 3

        可以知道,i存放在棧中。

算術操作

  「i++」:

  如上圖,addl指令對i進行+1。

賦值:

  「i = 0」:

圖12 hello.s文件 4

  通過movl指令賦初值。

比較語句:

  比較通過cmp等指令來實現,根據兩個操作數的差來設置條件碼。

數組/指針操作

  「sleep(atoi(argv[3]))」:

圖13 hello.s文件 5

  可以知道%rax先保存了argv[3]的地址。

函數調用及返回:

  如上圖中調用的atoi函數,%rdi為參數,存放著字元串的首地址,%eax為返回值,為一個整數。

3.4 本章小結

本節主要介紹編譯器通過編譯由.i文件生成彙編語言的.s文件的過程,並分析了變數,賦值,循環等各類C語言的基本語句的彙編表示。彙編語言和高級語言很不同,即使是高級語言中一個簡單的條件語句或者循環語句在彙編語言中都需要涉及到更多步驟來實現。學習彙編語言與編譯,使我們能夠真正的理解電腦底層的一些執行方法,有利於我們以後對程式的優化或調試。

第4章 彙編

4.1 彙編的概念與作用

概念:把彙編語言翻譯成機器語言的過程稱為彙編,彙編器同時將彙編程式(.s文件)打包成可重定位目標程式(.o文件)。這裡的.o是二進位文件,而.s仍然是文本文件。

作用:通過彙編,彙編程式碼轉化為了電腦能夠完全理解的機器程式碼。

4.2 在Ubuntu下彙編的命令

彙編命令:gcc -c -m64 -no-pie -fno-PIC hello.s -o hello.o

圖14 彙編命令
圖15 文件列表

4.3 可重定位目標elf格式

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

分析:

  1. ELF頭

命令:readelf -h -hello.o

ELF頭首先以一個16位元組的序列開始,這個序列描述了生成該文件的系統的 字的大小和位元組順序。剩下部分就如下圖所示,列出了包含幫助鏈接器語法分析和解釋目標文件的資訊。其中包括ELF頭的大小(64位元組),目標文件的類型(REL可重定位文件),機器類型(AMD X86-64),節頭部表的文件偏移,以及節頭部表中條目的大小和數量。

圖16 ELF頭
  1. 節頭部表

命令:readelf -S hello.o

圖17 節頭部表

節頭部表描述了 hello.o 中文件中各個節的語義,包括節的類型、位置和大小等資訊

  1. 符號表(.symtab)

命令:readelf -s hello.o

圖18 符號表

可以看到.c文件中用到的main,atoi,exit,sleep函數。

  1. 重定位條目

命令:readelf -r hello.o

圖19 重定位條目

當彙編器生成 hello.o 後,它並不知道數據和程式碼最終將放在記憶體中的什麼位置。它也不知道這個模組引用的任何外部定義的函數或者全局變數的位置。所以,無論何時彙編器遇到對最終位置未知的目標引用,它就會生成一個重定位條目。重定位條目在鏈接時告訴鏈接器目標文件合併時如何修改應用。

重定位條目的類型這裡有兩種,R_X86_64_32意思是重定位時使用一個32位的絕對地址的引用,通過絕對定址,CPU直接使用在指令中編碼的32位值作為有效地址,不需要進一步修改。R_X86_64_PC32意思是重定位時使用一個32位PC相對地址的引用。一個PC相對地址就是據程式計數器的當前運行值的偏移量。

4.4 Hello.o的結果解析

命令:objdump -d -r hello.o > disa_hello.s

生成hello.o的反彙編文件。

分析hello.o的反彙編,並與第3章的 hello.s進行對照分析。

圖20 disa_hello.s文件內容

不同點:

  1. 跳轉命令:hello.s的跳轉目的是.L1,.L2這樣的符號來實現的,而disa_hello.s中,是直接跳轉到指令的地址。而這個地址在<>中註明了關於整個函數首地址的偏移量,這個偏移量是根據PC值和距跳轉目的的偏移量算出來的。
  2. 函數調用:hello.s函數調用後是函數名,而disa_hello.s中call後有函數名,也有關於這個函數地址的資訊,如重定位條目的類型。

4.5 本章小結

從彙編程式碼變為機器程式碼後,這個程式就可以真正被電腦理解。我們也可以利用反彙編工具查看一些二進位機器程式的彙編程式碼從而去破解或翻譯,雖然反彙編後的程式碼與原本的彙編程式碼有所不同。

第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

圖21 鏈接指令

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

  1. ELF頭

命令:readelf -h hello

圖22 ELF頭

節頭數量由14變為27。

  1. 節頭部表:

命令:readelf -S hello

圖23 節頭部表-1
圖24 節頭部表-2

發現了一些動態鏈接所增加的段。

5.4 hello的虛擬地址空間

hello的虛擬地址從0x401000開始。

圖25 EDB查看虛擬地址空間

比較readelf的段資訊和edb的虛擬記憶體空間:

查看偏移0x3048的.data段

圖26 EDB查看.data

5.5 鏈接的重定位過程分析

objdump -d -r hello > disa_hello_2.s分析與disa_hello.s的不同

圖27 查看disa_hello_2.s文件內容
  1. 新增加的函數:

新增加了很多函數和它的實現彙編程式碼,如我們源程式碼中使用的printf,atoi,exit等。還有_init初始化函數。

  1. 新增加的節(Section):

新增加了.init節,.plt節,.plt.sec。

  1. 調用函數:

調用函數不再有重定位類型和偏移量,而是直接為函數的絕對地址和它的函數名。

鏈接:鏈接器(ld)將各個目標文件(各種.o文件)組裝在一起,文件中的各個函數段按照一定規則累積在一起。從.o提供的重定位條目將函數調用和控制流跳轉的地址填寫為最終的地址。

5.6 hello的執行流程

圖28 hello的執行流程

5.7 Hello的動態鏈接分析

在進行動態鏈接前,首先進行靜態鏈接,生成部分鏈接的可執行目標文件 hello。此時共享庫中的程式碼和數據沒有被合併到 hello 中。只有在載入 hello 時,動態鏈接器才對共享目標文件中的相應模組內的程式碼和數據進行重定位,載入共享庫,生成完全鏈接的可執行目標文件。

例如:查看.plt段

圖29 EDB查看.plt段-1

運行後:

圖30 EDB查看.plt段-2

5.8 本章小結

本章主要介紹了鏈接器如何將hello.o可重定向文件與動態庫函數鏈接起來,分析了可重定位文件與可執行文件ELF的差異,並分析了重定位的過程。

第6章 hello進程管理

6.1 進程的概念與作用

概念:進程的經典定義是一個執行中程式的實例,系統的每個程式都運行在某個進程的上下文。上下文是由程式正確運行所需的狀態組成的,這個狀態包括存放在記憶體里的程式的程式碼和數據,它的棧,通用目的暫存器的內容,程式計數器,環境變數以及打開文件描述符的集合。進程是電腦科學中最深刻,最成功的概念。

作用:通過進程,我們會得到一種假象,好像我們的程式是當前唯一運行的程式,我們的程式獨佔處理器和記憶體,我們程式的程式碼和數據好像是系統記憶體中唯一的對象。而這些假象就是通過進程來實現的。

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

Shell 是一種交互型的應用級程式,用戶能夠通過 Shell 與作業系統內核進行交互。

bash,全稱為Bourne-Again Shell。它是一個為GNU項目編寫的Unix shell。bash腳本功能非常強大,尤其是在處理自動循環或大的任務方面可節省大量的時間。bash是許多Linux平台的內定Shell。

處理流程:

  1. Shell讀取用戶輸入的命令。
  2. Shell判斷是否為shell內置命令,如果不是則認為是一個可執行文件。、
  3. Shell構建參數和環境變數。
  4. Shell通過fork創建子進程,再通過execve函數載入可執行文件。
  5. Shell回收創建的子進程,循環。

6.3 Hello的fork進程創建過程

我們在Shell上輸入./hello,這個不是一個內置的Shell命令,所以Shell會認為hello是一個可執行目標文件,通過條用某個駐留在存儲器中被稱為載入器的作業系統程式碼來運行它。當Shell運行一個程式時,父進程通過fork函數生成這個程式的進程。這個子進程幾乎與父進程相同,子進程得到與父進程相同的虛擬地址空間(獨立)的一個副本,包括程式碼,數據段,堆,共享庫以及用戶棧,並且和父進程共享文件。它們之間最大的不同是PID不同。

Hello的execve過程

execve函數載入並運行可執行目標文件,且帶參數列表argv和環境變數列表envp。在exevce載入了後,它調用啟動程式碼,啟動程式碼會設置棧,並將控制傳遞給新程式的主函數。

  1. 刪除已存在的用戶區域
  2. 映射私有區:為 hello 的程式碼、數據、.bss 和棧區域創建新的區域結構,所有這些區域都是私有的、寫時才複製。
  3. 映射共享區:比如 hello 程式與共享庫 libc.so 鏈接。
  4. 設置 PC:exceve() 做的最後一件事就是設置當前進程的上下文中的程式計數器,使之指向程式碼區域的入口點

6.5 Hello的進程執行

邏輯控制流:一系列程式計數器PC的值的序列叫做邏輯控制流,進程是輪流使用處理器的,在同一個處理器核心中,每個進程執行它的流的一部分後被搶佔(暫時掛起),然後輪到其他進程。

時間片:一個進程執行它的控制流的一部分的每一時間段叫做時間片。

用戶模式和內核模式:處理器通常使用一個暫存器提供兩種模式的區分,該暫存器描述了進程當前享有的特權,當沒有設置模式位時,進程就處於用戶模式中,用戶模式的進程不允許執行特權指令,也不允許直接引用地址空間中內核區內的程式碼和數據;設置模式位時,進程處於內核模式,該進程可以執行指令集中的任何命令,並且可以訪問系統中的任何記憶體位置。

上下文資訊:上下文就是內核重新啟動一個被搶佔的進程所需要的狀態,它由通用暫存器、浮點暫存器、程式計數器、用戶棧、狀態暫存器、內核棧和各種內核數據結構等對象的值構成。

上下文切換:上下切換是由內核中調度器完成的,當內核調度新的進程運行後,它就會搶佔當前進程,並進行1)保存以前進程的上下文2)恢復新恢復進程被保存的上下文,3)將控制傳遞給這個新恢復的進程 ,來完成上下文切換。

Hello:當hello運行到sleep函數時,是在用戶模式,然後調用sleep函數,進入內核模式,內核處理休眠請求,並開始計時。當時間到了後,內核接受到恢復hello運行的訊號,再進行調度,切換上下文,返回用戶模式。

6.6 hello的異常與訊號處理

  1. 不停亂按,包括回車:
圖31 亂按的現象

Shell會將回車前輸出的字元串當作命令。

  1. Ctrl + C
圖32 Ctrl + C的現象

會立即終止進程,通過ps命令發現hello進程被回收。

  1. Ctrl + Z
圖33 Ctrl + Z的現象

會在後台停止,fg放到前台運行時,會輸出剩下的7個字元串。

6.7本章小結

本章了解了hello進程的執行過程。在hello運行過程中,內核對其調度,異常處理程式為其將處理各種異常。每種訊號都有不同的處理機制,對不同的shell命令,hello也有不同的響應結果。

結論

最終,我們在所學習的範圍內,回顧了這段hello的一生。

  1. 編寫hello.c源程式碼程式。
  2. hello.c預處理為hello.i。
  3. hello.i編譯為彙編語言hello.s。
  4. hello.s彙編翻譯為機器語言生成可重定位文件hello.o。
  5. hello.o文件與動態庫鏈接,生成可執行文件hello。
  6. 在Shell中,輸出./hello,創建進程。
  7. 在子進程中,載入hello的上下文,映射記憶體。
  8. 在hello運行的過程中,接收訊號,進行訊號處理。
  9. 程式運行完成後,回收進程,釋放記憶體。

會想起一開始的C語言學習,有時候報一些不是語法的錯很不理解,有時候數組越界了卻提示超時錯誤也很不理解。我們只是把從源程式碼到結果作為一個黑盒,而現在對於一個程式如何執行有了一個大體的認識,明白了之前的錯誤,但是還是有不清楚的地方,只學習到了第八章,還需要在以後的學習中繼續努力。

附件

  1. hello.c:源程式碼文件。
  2. hello.i:預處理後的文本文件。
  3. hello.s:編譯後的彙編文件。
  4. hello.o:彙編後的可重定位文件。
  5. hello:鏈接後的可執行文件。
  6. disa_hello.s:反彙編hello.o的彙編程式碼。
  7. disa_hello_2.s:反彙編hello的彙編程式碼。

參考文獻

[1] Randal E.Bryant . 深入理解電腦系統[M]. 北京:機械出版社,2016.7

[2] //blog.csdn.net/qq_36314864/article/details/121250743

[3] //blog.csdn.net/hzp020/article/details/83765267

[4] //www.runoob.com/w3cnote/gcc-parameter-detail.html

[5] //www.cnblogs.com/skywang12345/archive/2013/05/30/3106570.html

Tags: