1. 程式人生 > >計算機系統大作業

計算機系統大作業

程式人生-Hello’s P2P

**

摘要

**

本文遍歷了hello.c在Linux下生命週期,藉助Linux下系列開發工具,通過對其預處理、編譯、彙編等過程的分步解讀及對比來學習各個過程在Linux下實現機制及原因。同時通過對hello在Shell中的動態連結、程序執行、記憶體管理、I/O管理等過程的探索來更深層次的理解Linux系統下的動態連結機制、儲存層次結構、異常控制流、虛擬記憶體及UnixI/O等相關內容。旨在將課本知識與例項結合學習,更加深入地理解計算機系統的課程內容。

**

關鍵詞

**
作業系統;編譯;連結;虛擬記憶體;異常控制流;

1.1Hello簡介

1.1.1 From Program to Process
首先hello.c通過I/O裝置如鍵盤等經過匯流排存入主存。然後GCC編譯器驅動程式讀取源程式檔案hello.c,通過前處理器cpp變成hello.i(修改了的源程式)然後通過編譯器ccl變成hello.s(彙編程式),然後通過彙編器as變成hello.o(可重定位目標程式),這時的hello.o就不是之前的文字了,而是對機器友好的二進位制程式碼了。最後再通過連結器ld與標準C庫進行連結,最終變成hello(可執行的二進位制目標程式)此時的hello就是一個Program了。然後在shell(Bash)裡面輸入字串“./hello”後,shell程式將字元逐一讀入暫存器,然後再放入到記憶體裡面去,然後shell呼叫fork函式建立一個新執行的子程序,這個子程序是父程序shell的一個複製,然後子程序通過execve系統呼叫啟動載入器。載入器刪除子程序現有的虛擬記憶體段,然後使用mmap函式建立新的記憶體區域,並建立一組新的程式碼、資料、堆和棧段。新的棧和堆段被初始化為零。通過將虛擬地址空間中的頁對映到可執行檔案的頁大小的片(chunk), 新的程式碼和資料段被初始化為可執行檔案的內容。最後,載入器跳轉到_start地址,它最終會呼叫應用程式的main 函式。然後程式從記憶體讀取指令位元組,然後再從暫存器讀入最多兩個數,然後在執行階段算術/邏輯單元要麼執行指令指明的操作,計算記憶體引用的有效地址要麼增加或者減少棧指標。然後在流水線化的系統中,待執行的程式被分解成幾個階段,每個階段完成指令執行的一部分。最後變成一個Process執行在記憶體中。
1.1.2 From Zero-0 to Zero-0
首先說明這裡的020應該指的是程式(Process)在記憶體中From Zero to Zero。一開始hello先是執行了上面所述的過程,然後在程式執行結束以後,該程序會保持在一種已終止的狀態中,直到該程序被其父程序也就是shell程序回收然後退出,shell會再次變成hello執行之前的狀態,也就是說又變成Zero了。

1.2 環境與工具

硬體環境:Intel Core i7-6700HQ x64CPU,16G RAM,256G SSD +1T HDD;
軟體環境:Ubuntu18.04.1 LTS;
開發與除錯工具:vim,gcc,as,ld,edb,readelf,HexEdit;

1.3 中間結果

hello.c:原始碼
hello.i:hello.c預處理生成的文字檔案。
hello.s:hello.i經過編譯器翻譯成的文字檔案hello.s,含組合語言程式。
hello.o:hello.s經彙編器翻譯成機器語言指令打包成的可重定位目標檔案
hello.elf:hello.o的ELF格式。
hello_o_asm.txt:hello.o反彙編生成的程式碼。
hello:經過hello.o連結生成的可執行目標檔案。
hello_out.elf:hello的ELF格式。
hello_out_asm.txt:hello反彙編生成的程式碼。

1.4 本章小結

本章主要是漫遊式地瞭解hello在系統中生命週期,對每個部分需要有系統地瞭解;同時本章列出本次實驗的基本資訊。

第2章 預處理

2.1 預處理的概念與作用

概念:前處理器cpp根據以字元#開頭的命令(巨集定義、條件編譯),修改原始的C程式,將引用的所有庫展開合併成為一個完整的文字檔案。
主要功能如下:
1、將原始檔中用#include形式宣告的檔案複製到新的程式中。比如hello.c第6-8行中的#include<stdio.h> 等命令告訴前處理器讀取系統標頭檔案stdio.h unistd.h stdlib.h 的內容,並把它直接插入到程式文字中。
2、用實際值替換用#define定義的字串。
3、根據#if後面的條件決定需要編譯的程式碼。

2.2在Ubuntu下預處理的命令

gcc -E hello.c -o hello.i
在這裡插入圖片描述

2.3 Hello的預處理結果解析

在這裡插入圖片描述
在這裡插入圖片描述

在這之前出現的是stdio.h unistd.h stdlib.h的依次展開,以stdio.h的展開為例,cpp到預設的環境變數下尋找stdio.h,開啟/usr/include/stdio.h 發現其中依然使用了#define語句,cpp對此遞迴展開,所以最終.i程式中是沒有#define的。而且發現其中使用了大量的#ifdef #ifndef的語句,cpp會對條件值進行判斷來決定是否執行包含其中的邏輯。其他類似。

2.4 本章小結

.c檔案中包含有標頭檔案也就是有外部檔案的,還有一些程式設計師需要但是對於程式執行沒有任何幫助的巨集定義以註釋,和一些程式設計師需要的條件編譯和完善程式文字檔案等操作都需要通過預處理來實現。預處理可以使得程式在後序的操作中不受阻礙,是非常重要的步驟。

第3章 編譯

3.1 編譯的概念與作用

概念: 編譯是利用編譯程式從預處理文字檔案產生彙編程式(文字)的過程。主要包含五個階段:詞法分析;語法分析;語義檢查、中間程式碼生成、目的碼生成。
作用:編譯作用主要是將文字檔案hello.i翻譯成文字檔案hello.s,並在出現語法錯誤時給出提示資訊,執行過程主要從其中三個階段進行分析:

  1. 詞法分析。詞法分析的任務是對由字元組成的單詞進行處理,從左至右逐個字元地對源程式進行掃描,產生一個個的單詞符號,把作為字串的源程式改造成為單詞符號串的中間程式;
  2. 語法分析。語法分析器以單詞符號作為輸入,分析單詞符號串是否形成符合語法規則的語法單位,如表示式、賦值、迴圈等,最後看是否構成一個符合要求的程式,按該語言使用的語法規則分析檢查每條語句是否有正確的邏輯結構,程式是最終的一個語法單位;
  3. 目的碼生成。目的碼生成器把語法分析後或優化後的中間程式碼經彙編程式彙編生成組合語言程式碼,成為可執行的機器語言程式碼。
    注意:這兒的編譯是指從 .i 到 .s 即預處理後的檔案到生成組合語言程式

3.2 在Ubuntu下編譯的命令

gcc -S -o hello.s hello.i
在這裡插入圖片描述
應截圖,展示編譯過程!

3.3 Hello的編譯結果解析

3.3.1 資料
有變數int sleepsecs,編譯器將其編譯成
.type sleepsecs, @object
.size sleepsecs, 4
3.3.2 賦值
賦值語句sleepsecs=2.5編譯器將其編譯成
sleepsecs:
.long 2
.section .rodata
賦值語句i=0編譯器將其編譯成
movl $0, -4(%rbp)
3.3.3 型別轉換(顯示或隱式)
由於sleepsecs是int型的而2.5是float型別的,這就有一個隱式的型別轉換,編譯器將2.5隱式地轉換成了2存入sleepsecs。
3.3.4 算術操作
編譯器將i++編譯成
addl $1, -4(%rbp)
3.3.5 關係操作
編譯器將i<10編譯成
cmpl $9, -4(%rbp)
jle .L4
將argc!=3編譯成
cmpl $3, -20(%rbp)
je .L2
3.3.6 陣列/指標/結構操作
printf函式裡面的一系列對指標和對陣列的操作編譯器編譯為:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
3.3.7 控制轉移
編譯器將if,for等控制轉移語句都使用了cmp來比較然後使用了條件跳轉指令來跳轉。編譯器將if(argc!=3)編譯成:
cmpl $3, -20(%rbp)
je .L2
將for迴圈裡面的比較和轉移編譯成:
cmpl $9, -4(%rbp)
jle .L4
3.3.8 函式操作
編譯器將printf(“Usage: Hello 學號 姓名!\n”);編譯為:
movl $.LC0, %edi
call puts
將printf(“Hello %s %s\n”,argv[1],argv[2]);編譯為:
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
將sleep(sleepsecs);編譯為:
movl sleepsecs(%rip), %eax
movl %eax, $edi
call sleep

此部分是重點,說明編譯器是怎麼處理C語言的各個資料型別以及各類操作的。應分3.3.1~ 3.3.x等按照型別和操作進行分析,只要hello.s中出現的屬於大作業PPT中P4給出的參考C資料與操作,都應解析。

3.4 本章小結

本章系統闡述了編譯器將預處理文字檔案hello.i翻譯為文字檔案hello.s的具體操作,主要就組合語言偽指令、資料型別、組合語言操作、控制轉移,函式操作、型別轉換六方面針對hello.s中各部分做出相應的解釋說明。

第4章 彙編

4.1 彙編的概念與作用

概念:把組合語言翻譯成機器語言的過程稱為彙編。
作用:彙編器(as)將hello.s翻譯成機器語言指令,並把這些指令打包成一種叫做可重定位目標程式的格式,並將結果儲存在二進位制目標檔案hello.o中。

注意:這兒的彙編是指從 .s 到 .o 即編譯後的檔案到生成機器語言二進位制程式的過程。

4.2 在Ubuntu下彙編的命令

as hello.s -o hello.o
在這裡插入圖片描述
在這裡插入圖片描述
應截圖,展示彙編過程!

4.3 可重定位目標elf格式

在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
下面以.L1的重定位為例闡述之後的重定位過程:連結器根據info資訊向.symtab節中查詢連結目標的符號,由info.symbol=0x05,可以發現重定位目標連結到.rodata的.L1,設重定位條目為r,根據圖4.5知r的構造為:
r.offset=0x18, r.symbol=.rodata, r.type=R_X86_64_PC32, r.addend=-4,
重定位一個使用32位PC相對地址的引用。計算重定位目標地址的演算法如下(設需要重定位的.text節中的位置為src,設重定位的目的位置dst):

   refptr = s +r.offset 

(1)refaddr = ADDR(s) + r.offset
(2)*refptr = (unsigned) (ADDR(r.symbol) + r.addend-refaddr)(3)
其中
(1)指向src的指標
(2)計算src的執行時地址,
(3)中,ADDR(r.symbol)計算dst的執行時地址,在本例中,ADDR(r.symbol)獲得的是dst的執行時地址,因為需要設定的是絕對地址,即dst與下一條指令之間的地址之差,所以需要加上r.addend=-4。
對於其他符號的重定位過程,情況類似。
.rela.eh_frame : eh_frame節的重定位資訊。
.symtab:符號表,用來存放程式中定義和引用的函式和全域性變數的資訊。重定位需要引用的符號都在其中宣告。
分析hello.o的ELF格式,用readelf等列出其各節的基本資訊,特別是重定位專案分析。

4.4 Hello.o的結果解析

機器語言指的是二進位制的機器指令集合,而機器指令是由操作碼和運算元構成的。組合語言的主體是彙編指令。彙編指令和機器指令的差別在於指令的表示方法上,彙編指令是機器指令便於記憶的書寫格式。
在對比兩個檔案後,彙編器在彙編hello.s時:
為每條語句加上了具體的地址,全域性變數和常量都被安排到了具體的地址裡面。
運算元在hello.s裡面都是十進位制,在到hello.o裡面的機器級程式時都是十六進位制。
跳轉語句jx&jxx原來對應的符號都變成了相對偏移地址。
函式呼叫時原來的函式名字也被替換成了函式的相對偏移地址。
在這裡插入圖片描述
objdump -d -r hello.o 分析hello.o的反彙編,並請與第3章的 hello.s進行對照分析。
說明機器語言的構成,與組合語言的對映關係。特別是機器語言中的運算元與組合語言不一致,特別是分支轉移函式呼叫等。

4.5 本章小結

彙編器將組合語言轉化成機器語言,機器語言是用二進位制程式碼表示的計算機能直接識別和執行的一種機器指令的集合。它是計算機的設計者通過計算機的硬體結構賦予計算機的操作功能。機器語言具有靈活、直接執行和速度快等特點。 不同型號的計算機其機器語言是不相通的,按著一種計算機的機器指令編制的程式,不能在另一種計算機上執行。
一條指令就是機器語言的一個語句,它是一組有意義的二進位制程式碼,指令的基本格式如,操作碼欄位和地址碼欄位,其中操作碼指明瞭指令的操作性質及功能,地址碼則給出了運算元或運算元的地址。
用機器語言編寫程式,程式設計人員要首先熟記所用計算機的全部指令程式碼和程式碼的涵義。手程式設計序時,程式設計師得自己處理每條指令和每一資料的儲存分配和輸入輸出,還得記住程式設計過程中每步所使用的工作單元處在何種狀態。這是一件十分繁瑣的工作。編寫程式花費的時間往往是實際執行時間的幾十倍或幾百倍。而且,編出的程式全是些0和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
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

使用ld的連結命令,應截圖,展示彙編過程! 注意不只連線hello.o檔案

5.3 可執行目標檔案hello的格式

使用readelf -a hello > hello.elf 命令生成hello程式的ELF格式檔案。

在ELF格式檔案中,Section Headers對hello中所有的節資訊進行了宣告,其中包括大小Size以及在程式中的偏移量Offset,因此根據Section Headers中的資訊我們就可以用HexEdit定位各個節所佔的區間(起始位置,大小)。其中Address是程式被載入到虛擬地址的起始地址。
在這裡插入圖片描述
在這裡插入圖片描述
分析hello的ELF格式,用readelf等列出其各段的基本資訊,包括各段的起始地址,大小等資訊。

5.4 hello的虛擬地址空間

檢視ELF格式檔案中的Program Headers,程式頭表在執行的時候被使用,它告訴連結器執行時載入的內容並提供動態連結的資訊。每一個表項提供了各段在虛擬地址空間和實體地址空間的大小、位置、標誌、訪問許可權和對齊方面的資訊。在下面可以看出,程式包含8個段:

1、PHDR儲存程式頭表。
2、INTERP指定在程式已經從可執行檔案對映到記憶體之後,必須呼叫的直譯器(如動態連結器)。
3、LOAD表示一個需要從二進位制檔案對映到虛擬地址空間的段。其中儲存了常量4、資料(如字串)、程式的目的碼等。
5、DYNAMIC儲存了由動態連結器使用的資訊。
6、NOTE儲存輔助資訊。
7、GNU_STACK:許可權標誌,標誌棧是否是可執行的。
8、GNU_RELRO:指定在重定位結束之後那些記憶體區域是需要設定只讀。
使用edb載入hello,檢視本程序的虛擬地址空間各段資訊,並與5.3對照分析說明。
5.5 連結的重定位過程分析
hello相對於hello.o有如下不同:
1、hello.o中的相對偏移地址到了hello中變成了虛擬記憶體地址。
2、hello中相對hello.o增加了許多的外部連結來的函式。
3、hello相對hello.o多了很多的節類似於.init,.plt等。
4、hello.o中跳轉以及函式呼叫的地址在hello中都被更換成了虛擬記憶體地址。
重定位:連結器在完成符號解析以後,就把程式碼中的每個符號引用和正好一個符號定義(即它的一個輸入目標模組中的一個符號表條目)關聯起來。此時,連結器就知道它的輸入目標模組中的程式碼節和資料節的確切大小。然後就可以開始重定位步驟了,在這個步驟中,將合併輸入模組,併為每個符號分配執行時的地址。在hello到hello.o中,首先是重定位節和符號定義,連結器將所有輸入到hello中相同型別的節合併為同一型別的新的聚合節。例如,來自所有的輸入模組的.data節被全部合併成一個節,這個節成為hello的.data節。然後,連結器將執行時記憶體地址賦給新的聚合節,賦給輸入模組定義的每個節,以及賦給輸入模組定義的每一個符號。當這一步完成時,程式中的每條指令和全域性變數都有唯一的執行時記憶體地址了。然後是重定位節中的符號引用,連結器會修改hello中的程式碼節和資料節中對每一個符號的引用,使得他們指向正確的執行地址。
objdump -d -r hello 分析hello與hello.o的不同,說明連結的過程。
結合hello.o的重定位專案,分析hello中對其怎麼重定位的。

5.6 hello的執行流程

載入程式 ld-2.23.so_dl_start
ld-2.23.so!_dl_init
LinkAddress!_start
ld-2.23.so!_libc_start_main
ld-2.23.so!_cxa_atexit
LinkAddress!_libc_csu.init
ld-2.23.so!_setjmp
執行 LinkAddress!main
程式終止 ld-2.23.so!exit

載入程式 ld-2.23.so_dl_start
ld-2.23.so!_dl_init
LinkAddress!_start
ld-2.23.so!_libc_start_main
ld-2.23.so!_cxa_atexit
LinkAddress!_libc_csu.init
ld-2.23.so!_setjmp
執行 LinkAddress!main
程式終止 ld-2.23.so!exit
使用edb執行hello,說明從載入hello到_start,到call main,以及程式終止的所有過程。請列出其呼叫與跳轉的各個子程式名或程式地址。

5.7 Hello的動態連結分析

在呼叫共享庫函式時,編譯器沒有辦法預測這個函式的執行時地址,因為定義它的共享模組在執行時可以載入到任意位置。正常的方法是為該引用生成一條重定位記錄,然後動態連結器在程式載入的時候再解析它。GNU編譯系統使用延遲繫結(lazybinding),將過程地址的繫結推遲到第一次呼叫該過程時。

延遲繫結是通過GOT和PLT實現的。GOT是資料段的一部分,而PLT是程式碼段的一部分。兩表內容分別為:

PLT:PLT是一個數組,其中每個條目是16位元組程式碼。PLT[0]是一個特殊條目,它跳轉到動態連結器中。每個被可執行程式呼叫的庫函式都有它自己的PLT條目。每個條目都負責呼叫一個具體的函式。

GOT:GOT是一個數組,其中每個條目是8位元組地址。和PLT聯合使用時,GOT[O]和GOT[1]包含動態連結器在解析函式地址時會使用的資訊。GOT[2]是動態連結器在1d-linux.so模組中的入口點。其餘的每個條目對應於一個被呼叫的函式,其地址需要在執行時被解析。每個條目都有一個相匹配的PLT條目。
分析hello程式的動態連結專案,通過edb除錯,分析在dl_init前後,這些專案的內容變化。要截圖示識說明。

5.8 本章小結

本章瞭解了連結的概念作用,分析可執行檔案hello的ELF格式及其虛擬地址空間,同時通過例項分析了重定位過程、載入以及執行時函式呼叫順序以及動態連結過程,深入理解連結和重定位的過程。

第6章 hello程序管理

6.1 程序的概念與作用

概念:程序的經典定義就是一個執行中的程式的例項。系統中的每個程式都執行在某個程序的上下文中。上下文是由程式正確執行所需的狀態組成的。這個狀態包括存放在記憶體中的程式的程式碼和資料,它的棧、通用目的暫存器的內容、程式計數器、環境變數、以及開啟檔案描述符的集合。
作用:程序給應用程式提供的關鍵抽象有兩種:
a) 一個獨立的邏輯控制流,提供一個假象,程式獨佔地使用處理器。
b) 一個私有的地址空間,提供一個假象,程式在獨佔地使用系統記憶體。

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

shell作為UNIX的一個重要組成部分,是它的外殼,也是使用者於UNIX系統互動作用介面。Shell是一個命令解釋程式,也是一種程式設計語言。
1.讀入命令列、註冊相應的訊號處理程式、初始化程序組。
2. 通過paraseline函式解釋命令列,如果是內建命令則直接執行,否則阻塞訊號後建立相應子程序,在子程序中解除阻塞,將子程序單獨設定為一個程序組,在新的程序組中執行子程序。父程序中增加作業後解除阻塞。如果是前臺作業則等待其變為非前臺程式,如果是後臺程式則列印作業資訊。

6.3 Hello的fork程序建立過程

首先了解程序的建立過程:父程序通過呼叫fork函式建立一個新的執行的子程序。新建立的子程序幾乎但不完全與父程序相同。子程序得到與父程序使用者級虛擬地址空間相同的(但是獨立的)一份副本,包括程式碼和資料段、堆、共享庫以及使用者棧。子程序還獲得與父程序任何開啟檔案描述符相同的副本,這就意味著當父程序呼叫fork時。子程序可以讀寫父程序中開啟的任何檔案。父程序和新建立的子程序最大的區別在於他們有不同的id。

fork後呼叫一次返回兩次,在父程序中fork會返回子程序的PID,在子程序中fork會返回0;父程序與子程序是併發執行的獨立程序。核心能夠以任何方式交替執行他們邏輯控制流中的指令。

6.4 Hello的execve過程

execve 函式載入並執行可執行目標檔案filename, 且帶引數列表argv 和環境變數列表envp 。只有當出現錯誤時,例如找不到filename, execve 才會返回到呼叫程式。所以,與fork 一次呼叫返回兩次不同, execve 呼叫一次並從不返回。

6.5 Hello的程序執行

Linux 系統中的每個程式都執行在一個程序上下文中,有自己的虛擬地址空間。當shell 執行一個程式時,父shell 程序生成一個子程序,它是父程序的一個複製。子程序通過execve 系統呼叫啟動載入器。載入器刪除子程序現有的虛擬記憶體段,並建立一組新的程式碼、資料、堆和棧段。新的棧和堆段被初始化為零。通過將虛擬地址空間中的頁對映到可執行檔案的頁大小的片(chunk), 新的程式碼和資料段袚初始化為可執行檔案的內容。最後,載入器跳轉到_start地址,它最終會呼叫應用程式的main 函式。
結合程序上下文資訊、程序時間片,闡述程序排程的過程,使用者態與核心態轉換等等。

6.6 hello的異常與訊號處理

6.6.1 異常種類
hello執行過程中會出現的異常種類有:
1、中斷:SIGSTP:掛起程式
2、終止:SIGINT:終止程式
6.6.2命令的執行
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
hello執行過程中會出現哪幾類異常,會產生哪些訊號,又怎麼處理的。
程式執行過程中可以按鍵盤,如不停亂按,包括回車,Ctrl-Z,Ctrl-C等,Ctrl-z後可以執行ps jobs pstree fg kill 等命令,請分別給出各命令及執行結截圖,說明異常與訊號的處理。

6.7本章小結

本章從程序的角度分別描述了hello子程序fork和execve過程,並針對execve過程中虛擬記憶體映像以及棧組織結構等作出說明。同時瞭解了邏輯控制流中核心的排程及上下文切換等機制。闡述了Shell和Bash執行的處理流程以及hello執行過程中可能引發的異常和訊號處理。

第7章 hello的儲存管理

7.1 hello的儲存器地址空間

邏輯地址空間是由段地址和偏移地址構成的。
例如:23:8048000 段暫存器(CS等16位):偏移地址(16/32/64);
真實模式下:邏輯地址CS:EA —>實體地址CS*16+EA;
保護模式下:以段描述符作為下標,到GDT/LDT表查表獲得段地址, 段地址+偏移地址=線性地址。
線性空間地址為非負整數地址的有序集合,例如{0,1,2,3…}。
虛擬地址空間為N = 2n 個虛擬地址的集合,例如{0,1,2,3,….,N-1}。
實體地址空間為M = 2m 個實體地址的集合,例如{0,1,2,3,….,M-1}。實體地址是真實的實體記憶體的地址。
Intel採用段頁式儲存管理(通過MMU)實現:
·段式管理:邏輯地址—>線性地址==虛擬地址;
·頁式管理:虛擬地址—>實體地址。
以hello中的puts呼叫為例:mov $0x400714,%edi callq 4004a0,$0x400714為puts輸出字串邏輯地址中的偏移地址,需要經過段地址到線性地址的轉換變為虛擬地址,然後通過MMU轉換為實體地址,才能找到對應實體記憶體。
結合hello說明邏輯地址、線性地址、虛擬地址、實體地址的概念。

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

最初8086處理器的暫存器是16位的,為了能夠訪問更多的地址空間但不改變暫存器和指令的位寬,所以引入段暫存器,8086共設計了20位寬的地址匯流排,通過將段暫存器左移4位加上偏移地址得到20位地址,這個地址就是邏輯地址。將記憶體分為不同的段,段有段暫存器對應,段暫存器有一個棧、一個程式碼、兩個資料暫存器。
分段功能在真實模式和保護模式下有所不同。
真實模式,即不設防,也就是說邏輯地址=線性地址=實際的實體地址。段暫存器存放真實段基址,同時給出32位地址偏移量,則可以訪問真實實體記憶體。
在保護模式下,線性地址還需要經過分頁機制才能夠得到實體地址,線性地址也需要邏輯地址通過段機制來得到。段暫存器無法放下32位段基址,所以它們被稱作選擇符,用於引用段描述符表中的表項來獲得描述符。描述符表中的一個條目描述一個段。
Base:基地址,32位線性地址指向段的開始。Limit:段界限,段的大小。 DPL:描述符的特權級0(核心模式)-3(使用者模式)。
所有的段描述符被儲存在兩個表中:全域性描述符表GDT和區域性描述符表LDT。gdtr暫存器指向GDT表基址。
在保護模式下,分段機制就可以描述為:通過解析段暫存器中的段選擇符在段描述符表中根據Index選擇目標描述符條目Segment Descriptor,從目標描述符中提取出目標段的基地址Base address,最後加上偏移量offset共同構成線性地址Linear Address。
當CPU位於32位模式時,記憶體4GB,暫存器和指令都可以定址整個線性地址空間,所以這時候不再需要使用基地址,將基地址設定為0,此時邏輯地址=描述符=線性地址,Intel的文件中將其稱為扁平模型(flat model),現代的x86系統核心使用的是基本扁平模型,等價於轉換地址時關閉了分段功能。在CPU 64位模式中強制使用扁平的線性空間。邏輯地址與線性地址就合二為一了。

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

線性地址被分為以固定長度為單位的組,稱為頁(page),例如一個32位的機器,線性地址最大可為4G,可以用4KB為一個頁來劃分,這頁,整個線性地址就被劃分為一個tatol_page[2 ^ 20]的大陣列,共有2的20個次方個頁。這個大陣列我們稱之為頁目錄。目錄中的每一個目錄項,就是一個地址——對應的頁的地址。另一類“頁”,我們稱之為物理頁,或者是頁框、頁楨的。是分頁單元把所有的實體記憶體也劃分為固定長度的管理單位,它的長度一般與記憶體頁是一一對應的。這裡注意到,這個total_page陣列有2^20個成員,每個成員是一個地址(32位機,一個地址也就是4位元組),那麼要單單要表示這麼一個數組,就要佔去4MB的記憶體空間。為了節省空間,引入了一個二級管理模式的機器來組織分頁單元。
1.分頁單元中,頁目錄是唯一的,它的地址放在CPU的cr3暫存器中,是進行地址轉換的開始點。
2.每一個活動的程序,因為都有其獨立的對應的虛似記憶體(頁目錄也是唯一的),那麼它也對應了一個獨立的頁目錄地址。——執行一個程序,需要將它的頁目錄地址放到cr3暫存器中
3.每一個32位的線性地址被劃分為三部份,面目錄索引(10位):頁表索引(10位):偏移(12位)
依據以下步驟進行轉換:
1.從cr3中取出程序的頁目錄地址(作業系統負責在排程程序的時候,把這個地址裝入對應暫存器)。
2.根據線性地址前十位,在陣列中,找到對應的索引項,因為引入了二級管理模式,頁目錄中的項,不再是頁的地址,而是一個頁表的地址。(又引入了一個數組),頁的地址被放到頁表中去了。
3.根據線性地址的中間十位,在頁表(也是陣列)中找到頁的起始地址。
4.將頁的起始地址與線性地址中最後12位相加,得到最終我們想要的實體地址。

7.4 TLB與四級頁表支援下的VA到PA的變換

36位VPN 被劃分成四個9 位的片,每個片被用作到一個頁表的偏移量。CR3 暫存器包含Ll頁表的實體地址。VPN 1 提供到一個Ll PET 的偏移量,這個PTE 包含L2 頁表的基地址。VPN 2 提供到一個L2 PTE 的偏移量,以此類推。

7.5 三級Cache支援下的實體記憶體訪問

前提:只討論L1 Cache的定址細節,L2與L3Cache原理相同。L1 Cache是8路64組相聯。塊大小為64B。
解析前提條件:因為共64組,所以需要6bit CI進行組定址,因為共有8路,因為塊大小為64B所以需要6bit CO表示資料偏移位置,因為VA共52bit,所以CT共40bit。
在上一步中我們已經獲得了實體地址VA,使用CI(後六位再後六位)進行組索引,每組8路,對8路的塊分別匹配CT(前40位)如果匹配成功且塊的valid標誌位為1,則命中(hit),根據資料偏移量CO(後六位)取出資料返回。
如果沒有匹配成功或者匹配成功但是標誌位是1,則不命中(miss),向下一級快取中查詢資料(L2 Cache->L3 Cache->主存)。查詢到資料之後,一種簡單的放置策略如下:如果對映到的組內有空閒塊,則直接放置,否則組內都是有效塊,產生衝突(evict),則採用最近最少使用策略LFU進行替換。

7.6 hello程序fork時的記憶體對映

當fork函式被shell程序呼叫時,核心為新程序建立各種資料結構,並分配給它一個唯一的PID,為了給這個新程序建立虛擬記憶體,它建立了當前程序的mm_struct、區域結構和頁表的原樣副本。它將這兩個程序的每個頁面都標記為只讀,並將兩個程序中的每個區域結構都標記為私有的寫時複製。

7.7 hello程序execve時的記憶體對映

execve函式呼叫駐留在核心區域的啟動載入器程式碼,在當前程序中載入並執行包含在可執行目標檔案hello中的程式,用hello程式有效地替代了當前程式。載入並執行hello需要以下幾個步驟:
刪除已存在的使用者區域,刪除當前程序虛擬地址的使用者部分中的已存在的區域結構。
對映私有區域,為新程式的程式碼、資料、bss和棧區域建立新的區域結構,所有這些新的區域都是私有的、寫時複製的。程式碼和資料區域被對映為hello檔案中的.text和.data區,bss區域是請求二進位制零的,對映到匿名檔案,其大小包含在hello中,棧和堆地址也是請求二進位制零的,初始長度為零。
對映共享區域, hello程式與共享物件libc.so連結,libc.so是動態連結到這個程式中的,然後再對映到使用者虛擬地址空間中的共享區域內。
設定程式計數器(PC),execve做的最後一件事情就是設定當前程序上下文的程式計數器,使之指向程式碼區域的入口點。

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

缺頁故障是一種常見的故障,當指令引用一個虛擬地址,在MMU中查詢頁表時發現與該地址相對應的實體地址不在記憶體中,因此必須從磁碟中取出的時候就會發生故障。
缺頁中斷處理:缺頁處理程式是系統核心中的程式碼,選擇一個犧牲頁面,如果這個犧牲頁面被修改過,那麼就將它交換出去,換入新的頁面並更新頁表。當缺頁處理程式返回時,CPU重新啟動引起缺頁的指令,這條指令再次傳送VA到MMU,這次MMU就能正常翻譯VA了。

7.9動態儲存分配管理

動態記憶體分配器維護著一個程序的虛擬記憶體區域,稱為堆(heap) 。系統之間細節不同,但是不失通用性,假設堆是一個請求二進位制零的區域,它緊接在未初始化的資料區域後開始,並向上生長(向更高的地址) 。對於每個程序,核心維護著一個變數brk, 它指向堆的頂部。

分配器將堆視為一組不同大小的塊(block) 的集合來維護。每個塊就是一個連續的虛擬記憶體片(chunk),要麼是已分配的,要麼是空閒的。已分配的塊顯式地保留為供應用程式使用。空閒塊可用來分配。空閒塊保持空閒,直到它顯式地被應用所分配。一個已分配的塊保持已分配狀態,直到它被釋放,這種釋放要麼是應用程式顯式執行的,要麼是記憶體分配器自身隱式執行的。
基本方法:這裡指的基本方法應該是在合併塊的時候使用到的方法,有最佳適配和第二次適配還有首次適配方法,首次適配就是指的是第一次遇到的就直接適配分配,第二次顧名思義就是第二次適配上的,最佳適配就是搜尋完以後最佳的方案,當然這種的會在搜尋速度上大有降低。
策略:這裡的策略指的就是顯式的連結串列的方式分配還是隱式的標籤引腳的方式分配還是分離適配,帶邊界標籤的隱式空閒連結串列分配器允許在常數時間內進行對前面塊的合併。這種思想是在每個塊的結尾處新增一個腳部,其中腳部就是頭部的一個副本。如果每個塊包括這樣一個腳部,那麼分配器就可以通過檢查它的腳部,判斷前面一個塊的起始位置和狀態,這個腳部總是在距當前塊開始位置一個字的距離。顯式空間連結串列就是將空閒塊組織為某種形式的顯式資料結構。因為根據定義,程式不需要一個空閒塊的主體,所以實現這個資料結構的指標可以存放在這些空閒塊的主體裡面。例如,堆可以組織成一個雙向空閒連結串列,在每個空閒塊中,都包含一個前驅和後繼指標,使首次適配的分配時間從塊總數的線性時間減少到了空閒塊數量的線性時間。為了分配一個塊,必須確定請求的大小類,並且對適當的空閒連結串列做首次適配,查詢一個合適的塊。如果找到了一個,那麼就(可選地)分割它,並將剩餘的部分插入到適當的空閒連結串列中。如果找不到合適的塊,那麼就搜尋下一個更大的大小類的空閒連結串列。如此重複,直到找到一個合適的塊。如果空閒連結串列中沒有合適的塊,那麼就向作業系統請求額外的堆記憶體,從這個新的堆記憶體中分配出一個塊,將剩餘部分放置在適當的大小類中。要釋放一個塊,我們執行合併,並將結果放置到相應的空閒連結串列中。
Printf會呼叫malloc,請簡述動態記憶體管理的基本方法與策略。

7.10本章小結

本章從Linux儲存器的地址空間起,闡述了Intel的段式管理和頁式管理機制,以及TLB與多級頁表支援下的VA到PA的轉換,同時對cache支援下的實體記憶體訪問做了說明。針對記憶體對映及管理,簡述了hello的fork和execve記憶體對映,瞭解了缺頁故障與缺頁中斷處理程式,對動態分配管理做了系統闡述。

第8章 hello的IO管理

8.1 Linux的IO裝置管理方法

本章從Linux儲存器的地址空間起,闡述了Intel的段式管理和頁式管理機制,以及TLB與多級頁表支援下的VA到PA的轉換,同時對cache支援下的實體記憶體訪問做了說明。針對記憶體對映及管理,簡述了hello的fork和execve記憶體對映,瞭解了缺頁故障與缺頁中斷處理程式,對動態分配管理做了系統闡述。
裝置的模型化:檔案
裝置管理:unix io介面

8.2 簡述Unix IO介面及其函式

Unix I/O介面統一操作:
開啟檔案。一個應用程式通過要求核心開啟相應的檔案,來宣告它想要訪問一個I/O裝置,核心返回一個小的非負整數,叫做描述符,它在後續對此檔案的所有操作中標識這個檔案,核心記錄有關這個開啟檔案的所有資訊。
Shell建立的每個程序都有三個開啟的檔案:標準輸入,標準輸出,標準錯誤。
改變當前的檔案位置:對於每個開啟的檔案,核心保持著一個檔案位置k,初始為0,這個檔案位置是從檔案開頭起始的位元組偏移量,應用程式能夠通過執行seek,顯式地將改變當前檔案位置k。
讀寫檔案:一個讀操作就是從檔案複製n>0個位元組到記憶體,從當前檔案位置k開始,然後將k增加到k+n,給定一個大小為m位元組的而檔案,當k>=m時,觸發EOF。類似一個寫操作就是從記憶體中複製n>0個位元組到一個檔案,從當前檔案位置k開始,然後更新k。
關閉檔案,核心釋放檔案開啟時建立的資料結構,並將這個描述符恢復到可用的描述符池中去。
Unix I/O函式:
int open(char* filename,int flags,mode_t mode) ,程序通過呼叫open函式來開啟一個存在的檔案或是建立一個新檔案的。open函式將filename轉換為一個檔案描述符,並且返回描述符數字,返回的描述符總是在程序中當前沒有開啟的最小描述符,flags引數指明瞭程序打算如何訪問這個檔案,mode引數指定了新檔案的訪問許可權位。
int close(fd),fd是需要關閉的檔案的描述符,close返回操作結果。
ssize_t read(int fd,void *buf,size_t n),read函式從描述符為fd的當前檔案位置賦值最多n個位元組到記憶體位置buf。返回值-1表示一個錯誤,0表示EOF,否則返回值表示的是實際傳送的位元組數量。
ssize_t wirte(int fd,const void *buf,size_t n),write函式從記憶體位置buf複製至多n個位元組到描述符為fd的當前檔案位置。

8.3 printf的實現分析

前提:printf和vsprintf程式碼是windows下的。
檢視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;
}

首先arg獲得第二個不定長引數,即輸出的時候格式化串對應的值。
檢視vsprintf程式碼:

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':     //只處理%x一種情況

         		itoa(tmp, *((int*)p_next_arg)); //將輸入引數值轉化為字串儲存在tmp

           strcpy(p, tmp);  //將tmp字串複製到p處
           p_next_arg += 4; //下一個引數值地址
           p += strlen(tmp); //放下一個引數值的地址
           break;
           case 's': break;
            default:  break;
        }
    }
    return (p - buf);   //返回最後生成的字串的長度
}

則知道vsprintf程式按照格式fmt結合引數args生成格式化之後的字串,並返回字串的長度。
在printf中呼叫系統函式write(buf,i)將長度為i的buf輸出。write函式如下:

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

在write函式中,將棧中引數放入暫存器,ecx是字元個數,ebx存放第一個字元地址,int INT_VECTOR_SYS_CALLA代表通過系統呼叫syscall,檢視syscall的實現:

sys_call:
     call save
     push dword [p_proc_ready]
     sti
     push ecx
     push ebx
     call [sys_call_table + eax * 4]
     add esp, 4 * 3
     mov [esi + EAXREG - P_STACKBASE], eax
     cli
     ret

syscall將字串中的位元組“Hello 1170300825 lidaxin”從暫存器中通過匯流排複製到顯示卡的視訊記憶體中,視訊記憶體中儲存的是字元的ASCII碼。
字元顯示驅動子程式將通過ASCII碼在字模庫中找到點陣資訊將點陣資訊儲存到vram中。
顯示晶片會按照一定的重新整理頻率逐行讀取vram,並通過訊號線向液晶顯示器傳輸每一個點(RGB分量)。
於是我們的列印字串“Hello 1170300825 lidaxin”就顯示在了螢幕上。
https://www.cnblogs.com/pianist/p/3315801.html
從vsprintf生成顯示資訊,到write系統函式,到陷阱-系統呼叫 int 0x80或syscall.
字元顯示驅動子程式:從ASCII到字模庫到顯示vram(儲存每一個點的RGB顏色資訊)。
顯示晶片按照重新整理頻率逐行讀取vram,並通過訊號線向液晶顯示器傳輸每一個點(RGB分量)。

8.4 getchar的實現分析

非同步異常-鍵盤中斷的處理:當用戶按鍵時,鍵盤介面會得到一個代表該按鍵的鍵盤掃描碼,同時產生一箇中斷請求,中斷請求搶佔當前程序執行鍵盤中斷子程式,鍵盤中斷子程式先從鍵盤介面取得該按鍵的掃描碼,然後將該按鍵掃描碼轉換成ASCII碼,儲存到系統的鍵盤緩衝區之中。
getchar函式落實到底層呼叫了系統函式read,通過系統呼叫read讀取儲存在鍵盤緩衝區中的ASCII碼直到讀到回車符然後返回整個字串,getchar進行封裝,大體邏輯是讀取字串的第一個字元然後返回。
非同步異常-鍵盤中斷的處理:鍵盤中斷處理子程式。接受按鍵掃描碼轉成ascii碼,儲存到系統的鍵盤緩衝區。
getchar等呼叫read系統函式,通過系統呼叫讀取按鍵ascii碼,直到接受到回車鍵才返回。

8.5本章小結

本章系統的瞭解了Unix I/O,通過LinuxI/O裝置管理方法以及Unix I/O介面及函數了解系統級I/O的底層實現機制。通過對printf和getchar函式的底層解析加深對Unix I/O以及異常中斷等的瞭解。

結論

hello程式 終於 完成了它 艱辛 的一生。hello的一生大事記如下:

編寫,通過editor將程式碼鍵入hello.c
預處理,將hello.c呼叫的所有外部的庫展開合併到一個hello.i檔案中
編譯,將hello.i編譯成為彙編檔案hello.s
彙編,將hello.s會變成為可重定位目標檔案hello.o
連結,將hello.o與可重定位目標檔案和動態連結庫連結成為可執行目標程式hello
執行:在shell中輸入./hello 1170300825 lidaxin
建立子程序:shell程序呼叫fork為其建立子程序
執行程式:shell呼叫execve,execve呼叫啟動載入器,加對映虛擬記憶體,進入程式入口後程序開始載入實體記憶體,然後進入 main函式。
執行指令:CPU為其分配時間片,在一個時間片中,hello享有CPU資源,順序執行自己的控制邏輯流
訪問記憶體:MMU將程式中使用的虛擬記憶體地址通過頁表對映成實體地址。
動態申請記憶體:printf會呼叫malloc向動態記憶體分配器申請堆中的記憶體。
訊號:如果執行途中鍵入ctr-c ctr-z則呼叫shell的訊號處理函式分別停止、掛起。
結束:shell父程序回收子程序,核心刪除為這個程序建立的所有資料結構。
用計算機系統的語言,逐條總結hello所經歷的過程。
你對計算機系統的設計與實現的深切感悟,你的創新理念,如新的設計與實現方法。

附件

檔名稱 檔案作用
hello.i 預處理之後文字檔案
hello.s 編譯之後的彙編檔案
hello.o 編譯之後的可重定位目標執行
hello 連結之後的可執行目標檔案
hello2.c 測試程式程式碼
hello2 測試程式
hello.objdmp Hello的反彙編程式碼
hello.elf Hellode ELF格式
hmp.txt 存放臨時資料

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

參考文獻

為完成本次大作業你翻閱的書籍與網站等
[1] 林來興. 空間控制技術[M]. 北京:中國宇航出版社,1992:25-42.
[2] 辛希孟. 資訊科技與資訊服務國際研討會論文集:A集[C]. 北京:中國科學出版社,1999.
[3] 趙耀東. 新時代的工業工程師[M/OL]. 臺北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).
[4] 諶穎. 空間交會控制理論與方法研究[D]. 哈爾濱:哈爾濱工業大學,1992:8-13.
[5] KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.
[6] CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.