CSAPP

光風霽月發表於2024-07-31

深入理解計算機系統

0x ff 雜項

Instruction set Architecture:ISA,指令集體系架構

軟體和硬體之間的一層抽象層

馮諾依曼計算機,即程式儲存型計算機

重要思想:程式就是一系列被編碼了的位元組序列(看上去和資料一模一樣)


https://www.cnblogs.com/SovietPower/p/14877143.html

0x 00 參考資料 && lab

official:

官網

實驗


note:

影片詳解

筆記參考影片的原始碼


lab:

比較詳細的Attack,Data,Boom Lab參考

Boom,Attack,Shell Lab

全部實驗的詳細參考–知乎

全部實驗的詳細參考–CSDN

全部實驗的詳細參考–Github


video:

導讀 導讀筆記

小影片複習


book:

學生版重點知識

講師版重點知識


lab操作流程
# 1.datalab:
在原始檔 bits.c 中完善函式即可
./dlc bits.c 	 // 用於檢查程式是否合法,是否使用了程式規定的符號
make btest   	 // btest是評分(檢查對錯工具),每次執行btets前都要重新make一下
./btest bits.c   // 評分

# 2.bomblab
./bomb
輸入答案
導讀P3-52分鐘有第一關的實操

# 3.attacklab
./hex2raw < att1.txt > attraw1.txt // 將位元組序列at t1轉換為字串attraw1
./ctarget -q -i attraw1.txt     //測試答案
// (https://github.com/wuxueqian14/csapp-lab/tree/master/Attack%20Lab)

0x 01 二進位制

記憶體中儲存的是電壓,然後透過(不知道)某種方式抽象為數字01,然而計算機的記憶體太大了,以致於01的個數實在太多了,於是,我們把原有的0和1分塊,並再次抽象為0,1…。

![img](file:///C:\Users\24072\AppData\Roaming\Tencent\Users\2407217576\QQ\WinTemp\RichOle\7E[W6J9]YPX$8MS~3CCM[DG.png)

加入記憶體中有n bit,每m bit分為一塊,則最多可以分為2^m塊,因為m bit的排列組合數為2 ^ n個序列(sequence)

例如十進位制數字123,它應該表示為1*10^2 + 2*10^1 + 3*10^0,所以這裡的123準確來說應該是一個sequence,而不是一個數。

數是一個比較唯心的抽象的概念,你說一個數3,它可以是十進位制序列3,也可以是二進位制序列11…,3和11都是這個真正的(唯心的)3,這些序列之間是一一對應的,不僅如此,他們的運算也是一一對應的。十進位制的序列1+2,對應的二進位制下序列為1+01

取反對稱:對稱軸的兩側是相反數

對於1,2,3,4,他們分別取反對稱於-1,-2,-3,-4

對於二進位制000,001,010,011,他們分別取反對稱於111,110,101,100

IMAGE

0x 02 二進位制運算

位運算的迴圈圈:

IMG

​ (int型別有符號數)

img

​ (int型別無符號數)

透過這張圖,你可能會更好地理解補碼和無符號數運算是在mod 2^n 下計算的意義。

看一下樹狀陣列lowbit函式

int lowbit(int x) {
    return x & -x; // <==> x & (~x + 1);
}

這個函式為什麼能求得最後一個1所在位置的代表的權值呢?

首先 -x,其實就是x的補碼。關於補碼,我們有一個求補碼的方法:從右到左直到第一個1保持不變,後面的位取反,我們將x和x的補碼做與運算,最後得到的結果一定是這樣的形式:00..010..0,最後一個1左側全為0,右側也全為0。

#include <iostream>
using namespace std;

unsigned func1(unsigned x) {
    // 輸出一個無符號數x,判斷x在十六進位制下的的每一位是不是字母
    // 如果該位是字母就返回1,否則返回0
    // 並以一個16進位制數的形式返回
    unsigned x1 = (x & 0x22222222) >> 1;
    unsigned x2 = (x & 0x44444444) >> 2;
    unsigned x3 = (x & 0x88888888) >> 3;
    // printf("[1]:%04x\n[2]:%04x\n[3]:%04x\n", x1, x2, x3);
    return x3 & (x2 | x1);
}

unsigned func2(unsigned x) {
    // 輸出一個無符號數x,判斷x在十六進位制下的每一位是不是字母
    // 如果所有位都是字母返回1,否則返回0
    x = func1(x); //得到了每一位的結果
    x = x & (x >> 16); // 每次判斷一半
    x = x & (x >>  8);
    x = x & (x >>  4);
    return x;
}

unsigned func3(unsigned x) {
    // bigCount
    unsigned c;
    c = (x & 0x55555555) + ((x >>  1) & 0x55555555);
    c = (c & 0x33333333) + ((c >>  2) & 0x33333333);
    c = (c & 0x0f0f0f0f) + ((c >>  4) & 0x0f0f0f0f);
    c = (c & 0x00ff00ff) + ((c >>  8) & 0x00ff00ff);
    c = (c & 0x0000ffff) + ((c >> 16) & 0x0000ffff);
    return c;
}

int main()
{
    unsigned x = 0x1;
    // printf("0x%X = %X\n", x, func1(x));
    // printf("0x%X = %X\n", x, func2(x));
    printf("0x%X = %d\n", x, func3(x));
    
    return 0;
}

0x 03 浮點數

為什麼 IEEE 754浮點數Float型別的bias=127而不是128?

其實這也沒有一個官方的說法,不過為了讓自己接受這個設定,我們可以從兩個角度考慮:

  1. 首先,bias採用127時絕對值的範圍比較對稱
  2. 其次,bias採用127時最大的指數是127比bias=128時的126大,雖然只大1,但是我們直到指數的增長是“爆炸”的,因此其表示的範圍也大得多。

浮點的根據exp和frac分為三種情況:

  1. exp=111..1,指數全1。此時又分為兩種情況:(1)當frac全0時表示無窮大,根據符號位又分為正無窮和負無窮。(2)frac不全為0,表示NaN,一種未定義行為。(可以這樣區分無窮和NaN,由於未定義的行為有很多,因此需要根據frac進一步區分,所以frac不是固定的全0,(胡亂猜的),可以這樣記憶)。
  2. exp=000..0,指數全0。表示不規格化的浮點數。這裡的主要目的是為了擴充精度和範圍(往值小的方向)。
  3. else,規格化浮點數。

將一個無符號數轉換為一個浮點數的表示形式並儲存在一個無符號數字中

IEEE 754浮點數十六進位制相互轉換

關於浮點數舍入的討論

#include <iostream>
#include <cstring>
#include <stdint.h>
#include <algorithm>

using namespace std;

uint32_t uint2float(uint32_t u){ // 將一個服務號數u轉換成浮點數儲存的形式
    
    // 特判
    if (u == 0x00000000)
    {
        return 0x00000000;
    }
    
    // 找到最後一個1的後面的一個位置,求得該1後面還有多少個數
    int n = 31;
    while (n >= 0 && (((u >> n) & 0x1) == 0x0))
    {
        n = n - 1;
    }
    cout << "n: " << n << endl;
    
    uint32_t e, f; // exp, frac
    // <= 0000 0000 1.111 1111 1111 1111 1111 1111 : 32位
    // u的位數<=24,此時再隱藏一個1,就<=23位,於是frac就可以儲存所有位,不需要舍入
    if (u <= 0x00ffffff)
    {
        // no need rounding
        uint32_t mask = 0xffffffff >> (32 - n); // mask就是frac的掩碼
        f = (u & mask) << (23 - n);             // f = u & mask得到frac,但還需要左移移動到最右側[frac00..0],而不是[00..0frac]
        e = n + 127;
        printf("e: 0x%x, f: 0x%x\n", e, f);
        return (e << 23) | f;
    }
    // >= 0000 0001 0000 0000 0000 0000 0000 0000 
    // 總位數>=25,一位可以隱藏,還剩下至少24位,frac無法全部儲存,需要舍入(rounding)
    else
    {
        // expand to 64 bit for situations like 0xffffffff
        uint64_t a = 0;
        a += u;
        // compute g, r, s
        uint32_t g = (a >> (n - 23)) & 0x1;
        uint32_t r = (a >> (n - 23 - 1)) & 0x1;
        uint32_t s = 0x0;
        for (int j = 0; j < n - 23 - 1; ++ j)
        {
            s = s | ((u >> j) & 0x1);
        }
        // compute carry
        a = a >> (n - 23);
        // 0    1    ?    ... ?
        // [24] [23] [22] ... [0]
        if (r & (g | s) == 0x1)
        {
            a = a + 1;
        }
        // check carry
        if ((a >> 23) == 0x1) /
        {
            // 0    1    ?    ... ?
            // [24] [23] [22] ... [0]
            f = a & 0x007fffff; // 0x0000 0000 0111 1111 1111 1111 1111 1111只保留frac
            e = n + 127;
            return (e << 23) | f;
        }
        else if ((a >> 23) == 0x2) 
        {
            // 1    0    0    ... 0
            // [24] [23] [22] ... [0]
            e = n + 1 + 127;
            return (e << 23);
        }
    }
    // INF as default error
    return 0x7f800000; // 0 1111 1111 000 0000 0000 0000 0000 0000
}

int main()
{
    int x;  cin >> x;
    printf("%x", uint2float(0x10000000));
    
    return 0;
}

0x 04 時序電路和組合電路

原文連結:


數位電路根據邏輯功能的不同特點,可以分成兩大類,一類叫組合邏輯電路(簡稱組合電路),另一類叫做時序邏輯電路(簡稱時序電路)。組合邏輯電路在邏輯功能上的特點是任意時刻的輸出僅僅取決於該時刻的輸入,與電路原來的狀態無關。而時序邏輯電路在邏輯功能上的特點是任意時刻的輸出不僅取決於當時的輸入訊號,而且還取決於電路原來的狀態,或者說,還與以前的輸入有關。

時序電路,是由最基本的邏輯閘電路加上反饋邏輯迴路(輸出到輸入)或器件組合而成的電路,與組合電路最本質的區別在於時序電路具有記憶功能。

時序電路的特點是:輸出不僅取決於當時的輸入值,而且還與電路過去的狀態有關。它類似於含儲能元件的電感或電容的電路,如觸發器、鎖存器、計數器、移位暫存器、儲存器等電路都是時序電路的典型器件,時序邏輯電路的狀態是由儲存電路來記憶和表示的。

時序電路和組合電路的區別:
時序電路具有記憶功能。時序電路的特點是:輸出不僅取決於當時的輸入值,而且還與電路過去的狀態有關。組合邏輯電路在邏輯功能上的特點是任意時刻的輸出僅僅取決於該時刻的輸入,與電路原來的狀態無關

時序電路是 時序 邏輯 電路。時序,時間 順序,是在時鐘的推動下工作的,cpu就是一個複雜的時序電路。

組合邏輯電路和時序邏輯電路的最根本區別在於:組合邏輯電路的輸出在任一時刻只取決於當時的輸入訊號;而時序邏輯電路的輸出,不僅和當前的輸入有關,還和上時刻的輸出有關,它具有記憶元件(觸發器),可以記錄前一時刻的輸出狀態,它可以沒有輸入,僅在時鐘的驅動下,給出輸出。

時序電路的基本結構:

img

結構特徵:電路由組合電路和儲存電路組成,電路存在反饋

0x 05 緩衝區漏洞實驗

#include <stdio.h> //bomb.c

void echo()
{
	char buffer[4];
	gets(buffer); //緩衝區溢位的關鍵
	puts(buffer);
}

int main()
{
	puts("pls input: ");
	echo();
	return 0;
}
操作步驟:
1. gcc bomb.c -o main -fno-stack-protector -g
# -fno-stack-protector取消棧保護?
# -g除錯模式,因為後面還需要除錯

2. gdb main
2.1 在echo函式的gets函式加上一個斷點:b 6
# echo函式位於main.c的第六行
2.2 r
# run執行程式,此時會在斷點gets函式停下
2.3 info f 
# 顯示棧資訊,如下方圖-棧資訊所示
# 在這些資訊中,我們需要注意三個地址:
# (1)frame at 0x7ff.f3d0
# (2)rbp at   0x7ff.f3c0
# (3)bip at.  0x7ff.f3c8
# 其中frame at的地址是函式echo佔用棧的地址
# 此時,返回地址rip和舊的棧頂指標rbp已經入棧
# 由此可見,程式還沒執行,返回地址和舊的棧頂指標就會入棧
2.4 p/a &buffer[0]
# 列印陣列buffer的首地址
# 透過結構圖,我們可以發現,陣列與返回地址rip之間差了12(c8-bc)位元組,如果我們gets的陣列大於等於12位元組,那麼返回地址的資料就會被破壞,


![image-20220907100422585](/Users/epoch/Library/Application Support/typora-user-images/image-20220907100422585.png)

(圖-棧資訊)

![13C288AA-6A07-463D-A689-CC7FEF2DCB91](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/13C288AA-6A07-463D-A689-CC7FEF2DCB91.png)

(圖-陣列地址)

img

(圖-影片測試執行gets前的棧)

img

(圖-影片測試執行gets後的棧)

0x 06 Computer English


common:註釋

override:覆蓋

entry:入口,條目,輸入

Place holder:站位

ascending:升序

descending:降序

comma:逗號

brackets:括號

determine: 確定,決定,判定,下決心

deterministic: 確定行

finite: 有限的

infinite: 無限的

automaton: 自動機

positive: 正數

negative: 負數

decimal: 十進位制

hexadecimal:十六進位制

octal: 八進位制

optimazation:最佳化

pruning:剪枝

decode:譯碼

instance: 例子,例項

cpu和memory 就組成了一個狀態機

operand 運算元

opreator:運算子

memory:記憶體/儲存器

recursion:遞迴

reduce:歸約

iterate: 迭代

transistor:電晶體

complement:補充,補運算(~),輔

parse: 解析

simulator: 模擬器

simulate: 模擬,模擬,假裝

converter:轉換器

verbose: 冗長的,囉嗦

handler: 管理者,處理程式

illustrate: 說明

universal: 通用的

pecuilar: 特有,奇特,一場


0x 07 makefile

.1 規則

(1)make命令具有自動推導的功能,例如依賴中的.o檔案,即使不存在,make會使用內部預設的構造規則生成這些.o檔案。

(2)make後面不帶引數預設執行第一條命令

(3)mak的時間戳規則

make 命令執行的時候會根據檔案的時間戳判定是否執行 makefile 檔案中相關規則中的命令。

  1. 目標是透過依賴生成的,因此正常情況下:目標時間戳 > 所有依賴的時間戳 , 如果執行 make 命令的時候檢測到規則中的目標和依賴滿足這個條件,那麼規則中的命令就不會被執行。
  2. 當依賴檔案被更新了,檔案時間戳也會隨之被更新,這時候 目標時間戳 < 某些依賴的時間戳 , 在這種情況下目標檔案會透過規則中的命令被重新生成。
  3. 如果規則中的目標對應的檔案根本就不存在, 那麼規則中的命令肯定會被執行。

(4)對於不生成目標檔案的目標稱為偽目標,為了避免微偽目標的名字和真實的檔名重複,我們可以在偽目標的前面加上關鍵字:.PHONY(假) 例如:

.PHONY: clean
clean: 
	rm *.o

宣告位偽目標主要是避免這種情況:

如果目標不存在規則的命令肯定被執行, 如果目標檔案存在了就需要比較規則中目標檔案和依賴檔案的時間戳,滿足條件才執行規則的命令,否則不執行。

加入目標是clean,而恰好有一個真實的clean檔案,只要clean檔案不更新,那麼clean目標就無法執行。

(提醒)目錄連線到部落格中的例項6可以好好看看👀

.2 變數

make中的變數分為三種:

1.自定義變數:即使用者自己定義的變數,makefile中的變數是沒有型別的,直接建立變數然後給其賦值就可以了。透過$(obj) 可以取出自定義的obj變數。

obj = main.c
target = main
depend = main.o

$(target): $(depend)
	gcc $(obj) -o $(target)

# --------------
# 上面的命令等價於下面:

main: main.o
	gcc main.c -o main

2.預定義變數:在makefile中有一些已經定義好的變數,使用者可以直接使用這些變數,不用進行定義,預定義變數的名字一般是大寫的。

![96D31374-3040-4B27-8A65-B9DE685E3351](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/96D31374-3040-4B27-8A65-B9DE685E3351.png)

3.自動變數:makefile智慧鼓的規則語句經常會出現目標檔案和依賴檔案,自動變數用來代表這些規則中的目標檔案和依賴檔案,並且衙門只能在規則的命令總使用。

![DC05ED8E-B70B-44FB-A799-E6D0C938CF7F](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/DC05ED8E-B70B-44FB-A799-E6D0C938CF7F.png)

.3 模式匹配

模式匹配常常與自動變數結合使用,用來簡化makefile,減少冗餘和重複書寫。

.4 函式

1.wildcard:萬用字元,用來匹配製定目錄下的檔案

# 使用舉例: 分別搜尋三個不同目錄下的 .c 格式的原始檔
src = $(wildcard /home/robin/a/*.c /home/robin/b/*.c *.c)  # *.c == ./*.c
# 返回值: 得到一個大的字串, 裡邊有若干個滿足條件的檔名, 檔名之間使用空格間隔
/home/robin/a/a.c /home/robin/a/b.c /home/robin/b/c.c /home/robin/b/d.c e.c f.c

2.patsubst:pattern subsitude,匹配代替,用來替換檔名的字尾

src = a.cpp b.cpp c.cpp e.cpp
# 把變數 src 中的所有檔名的字尾從 .cpp 替換為 .o
obj = $(patsubst %.cpp, %.o, $(src)) 
# obj 的值為: a.o b.o c.o e.o

0x 08 gdb

.0 參考

![9523F5A0-416A-4635-99DB-47685282748F](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/9523F5A0-416A-4635-99DB-47685282748F.png)

本文件參考來源,功能基礎而簡單

設計多執行緒,多進城等高階功能,較為複雜

知乎

.1 新增命令列引數

set args … 啟動gdb後,在程式啟動之前設定引數

show args 檢視設定的命令列引數

.2 啟動程式

在整個gdb除錯過程中,啟動飲用程式的命令只能使用一次。

run 可以縮寫為 r,如果程式中設定了斷點會停在第一個斷點的位置,如果沒有設定斷點,程式就執行完了。

start 啟動程式,最終會阻塞在main函式的第一行,等待輸入後續其他 gdb 命令。

start 是要開始執行, run 是真的執行。

.3 退出 gdb

quit 縮寫為 q

.4 檢視程式碼

list 可以縮寫為 l ,透過這個命令可以檢視專案中任意一個檔案中的內容,並且還可以透過檔案行號,函式名等方式檢視。

(gdb) list
(gdb) list 行號
(gdb) list 函式名

一個專案通常由多個原始檔構成,預設情況下透過 list 檢視的是程式入口 main 函式對應的檔案。

(gdb) list 檔名:行號
(gdb) list 檔名:函式名

預設情況下 list 之顯示 10 行的內容。如果想顯示更多,可以透過 set listsize 設定,同時如果想檢視當前顯示的行數可以透過 show listsize 檢視。這裡的 listsize 可以縮寫為 list

(gdb) set listsize 行號
(gdb) show listsize

.5 斷點操作

如果想透過 gdb 掉時某一行或者得到某個變數在執行狀態下的實際值,就需要在這一行設定斷點,程式指定到斷點的位置就會阻塞。我們就可以透過 gdb 的除錯命令得到我們想要的資訊了。

設定斷點:break 縮寫為 b

斷點的設定方式由兩種:

  1. 常規斷點:程式只要執行到這個位置就會阻塞
  2. 條件斷點:只有指定的條件被滿足了程式才會在斷點處阻塞
# 設定普通斷點到當前檔案
(gdb) b 行號
(gdb) b 函式名 # 停在函式的第一行
# 設定普通斷點到某個非當前檔案
(gdb) b 檔名:行號
(gdb) b 問價名:函式名 # 停在函式的第一行
# 設定條件斷點
# 通常情況下,在迴圈中條件斷點用的比較多
(gdb)  b 行號 if 變數名 == 某個值

檢視斷點:info break ,其中 info 可以縮寫為 i , break 可以縮寫為 b

info break 檢視斷點資訊時的一些常用的屬性:Num: 斷點的編號,刪除斷點或者設定斷點狀態的時候都需要使用
Enb: 當前斷點的狀態,y 表示斷點可用,n 表示斷點不可用
What: 描述斷點被設定在了哪個檔案的哪一行或者哪個函式上


如果確定設定的某個斷點不再被使用了,可用將其刪除,刪除命令是 delete 斷點編號 , 這個 delete 可以簡寫為 del 也可以再簡寫為 d

刪除斷點的方式有兩種: 刪除(一個或者多個)指定斷點或者刪除一個連續的斷點區間,具體操作如下:

# delete == del == d
# 需要 info b 檢視斷點的資訊, 第一列就是編號
(gdb) d 斷點的編號1 [斷點編號2 ...]
# 舉例: 
(gdb) d 1          # 刪除第1個斷點
(gdb) d 2 4 6      # 刪除第2,4,6個斷點

# 刪除一個範圍, 斷點編號 num1 - numN 是一個連續區間
(gdb) d num1-numN
# 舉例, 刪除第1到第5個斷點
(gdb) d 1-5

如果某個斷點只是臨時不需要了,我們可以將其設定為不可用狀態,設定命令為 disable 斷點編號,當需要的時候再將其設定回可用狀態,設定命令為 enable 斷點編號。

# 讓斷點失效之後, gdb除錯過程中程式是不會停在這個位置的
# disable == dis
# 設定某一個或者某幾個斷點無效
(gdb) dis 斷點1的編號 [斷點2的編號 ...]

# 設定某個區間斷點無效
(gdb) dis 斷點1編號-斷點n編號
# 檢視斷點資訊
(gdb) i b
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x0000000000400cce in main() at test.cpp:14
4       breakpoint     keep y   0x0000000000400cdd in main() at test.cpp:16
5       breakpoint     keep y   0x0000000000400d46 in main() at test.cpp:23
6       breakpoint     keep y   0x0000000000400d4e in main() at test.cpp:25
7       breakpoint     keep y   0x0000000000400d6e in main() at test.cpp:28
8       breakpoint     keep y   0x0000000000400d7d in main() at test.cpp:30

# 設定第2, 第4 個斷點無效
(gdb) dis 2 4

# 檢視斷點資訊
(gdb) i b
Num     Type           Disp Enb Address            What
2       breakpoint     keep n   0x0000000000400cce in main() at test.cpp:14
4       breakpoint     keep n   0x0000000000400cdd in main() at test.cpp:16
5       breakpoint     keep y   0x0000000000400d46 in main() at test.cpp:23
6       breakpoint     keep y   0x0000000000400d4e in main() at test.cpp:25
7       breakpoint     keep y   0x0000000000400d6e in main() at test.cpp:28
8       breakpoint     keep y   0x0000000000400d7d in main() at test.cpp:30

# 設定 第5,6,7,8個 斷點無效
(gdb) dis 5-8

# 檢視斷點資訊
(gdb) i b
Num     Type           Disp Enb Address            What
2       breakpoint     keep n   0x0000000000400cce in main() at test.cpp:14
4       breakpoint     keep n   0x0000000000400cdd in main() at test.cpp:16
5       breakpoint     keep n   0x0000000000400d46 in main() at test.cpp:23
6       breakpoint     keep n   0x0000000000400d4e in main() at test.cpp:25
7       breakpoint     keep n   0x0000000000400d6e in main() at test.cpp:28
8       breakpoint     keep n   0x0000000000400d7d in main() at test.cpp:30

讓無效的斷點生效:

# enable == ena
# 設定某一個或者某幾個斷點有效
(gdb) ena 斷點1的編號 [斷點2的編號 ...]

# 設定某個區間斷點有效
(gdb) ena 斷點1編號-斷點n編號

.6 除錯命令

如果除錯的程式被斷點阻塞了又想讓程式繼續執行,這時候就可以使用 continue 命令。程式會繼續執行,直到遇到下一個有效的斷點。``continue可以縮寫為c`。

在 gdb 除錯的時候如果需要列印變數的值, 使用的命令是 print, 可縮寫為 p。如果列印的變數是整數還可以指定輸出的整數的格式,格式化輸出的整數對應的字元表如下:

![9BDD57D6-6D87-4080-B269-951C45DEC259](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/9BDD57D6-6D87-4080-B269-951C45DEC259.png)

printf 的語法格式如下:

# print == p
(gdb) p 變數名

# 如果變數是一個整形, 預設對應的值是以10進位制格式輸出, 其他格式請參考上表
(gdb) p/fmt 變數名

例如:

# 舉例
(gdb) p i       # 10進位制
$5 = 3
(gdb) p/x i     # 16進位制
$6 = 0x3
(gdb) p/o i     # 8進位制
$7 = 03

如果在除錯過程中需要檢視某個變數的型別,可以使用命令 ptype, 語法格式如下:

# 語法格式
(gdb) ptype 變數名

舉例:

# 列印變數型別
(gdb) ptype i
type = int
(gdb) ptype array[i]
type = int
(gdb) ptype array
type = int [12]

單步除錯

step 命令可以縮寫為 s, 命令被執行一次程式碼被向下執行一行,如果這一行是一個函式呼叫,那麼程式會進入到函式體內部。

如果透過 s 單步除錯進入到函式內部,想要跳出這個函式體, 可以執行 finish 命令。如果想要跳出函式體必須要保證函式體內不能有有效斷點,否則無法跳出。

next 命令和 step 命令功能是相似的,只是在使用 next 除錯程式的時候不會進入到函式體內部,next 可以縮寫為 n

透過 until 命令可以直接跳出某個迴圈體,這樣就能提高除錯效率了。如果想直接從迴圈體中跳出,必須要滿足以下的條件,否則命令不會生效:

0x e5 結構體位元組對齊規則

結構體的大小絕大部分情況下不會直接等於各個成員大小的總和,編譯器為了最佳化對結構體成員的訪問總會在結構體中插入一些空白位元組,有如下結構體:

struct align_basic
{
	char c;
	int i;
	double d;
};

那麼此時sizeof(align_basic)的值會是sizeof(char)+sizeof(int)+sizeof(double)的值麼?

img

如上圖經過測試我們發現其大小為16個位元組並不等於1+4+8=13個位元組,可知編譯器給align_basic結構體插入了另外3個位元組,接下來我們將分析編譯器對齊位元組的規則以及結構體在記憶體中的結構,首先感謝結構體在記憶體中的對齊規則 - 咕唧咕唧shubo.lk的專欄 - 部落格頻道 - CSDN.NET這篇文章的作者,在此之前我對記憶體對齊也是一知半解,很多時候也解釋不明白。

規則一:結構體中元素按照定義順序依次置於記憶體中,但並不是緊密排列。從結構體首地址開始依次將元素放入記憶體時,元素會被放置在其自身對齊大小的整數倍地址上。這裡說的地址是元素在結構體中的偏移量,結構體首地址偏移量為0。

在align_basic中元素c是第一個元素,那麼它的地址為0,第二個元素i不會被放在地址1處,int的對齊大小為4個位元組,此時雖然元素c只佔據一個位元組,但是由於i的地址必須在4位元組的整數倍上,所以地址必須再向後在移動三個位元組,故而需要放在地址4上,此時前兩個元素已經佔據了8個位元組的空間,第三個元素d會被直接放在地址8上,因為double的對齊大小為8個位元組,而前面兩個元素已經佔據了8個位元組,正好是double對齊大小的整數倍,所以元素d不需要再往後移動。說了這麼多也不如讓機器給我們驗證下有說服力:

printf("%d %d %d %d\n", sizeof(align_basic), &align_basic::c, &align_basic::i, &align_basic::d);

img

img

那麼這樣就夠了嗎,會不會太簡單?我們把元素i和d的位置交換下,此時結構體的大小會是20嗎,我們仍然先讓機器說話,(⊙o⊙)…畢竟後面打臉有證據:

struct align_basic
{
	char c;
	double d;
	int i;
};
printf("%d\n", sizeof(align_basic));

img

我們發現此時結構體的大小並不是20而是24,那麼多出來的這4個位元組如何解釋?我們引出第二條規則。

規則二:如果結構體大小不是所有元素中最大對齊大小的整數倍,則結構體對齊到最大元素對齊大小的整數倍,填充空間放置到結構體末尾。

運用規則一,此時c仍然是第一個元素,其地址為0,第二個元素地址為8, 第三個元素地址為16,然後運用規則二,c,d,i中d的對齊大小為8最大所以整個結構必須對齊到8的整數倍,前面是三個元素已經佔據了20個位元組的空間,只需要在結構體的尾部填充4個位元組的空間就是8的倍數了,所以此時整個結構體的大小為24個位元組。

printf("%d %d %d %d\n", sizeof(align_basic), &align_basic::c, &align_basic::d, &align_basic::i);

img

img

規則三:基本資料型別的對齊大小為其自身的大小,結構體資料型別的對齊大小為其元素中最大對齊大小元素的對齊大小。 規則三可以由規則二推匯出來。

char型別的對齊大小為1位元組,short型別的對齊大小為2位元組,int型別的大小為4位元組,double的對齊大小為8位元組,align_basic結構體中最大對齊大小元素為d是double型別,所以align_basic的對齊大小是8。有人會問如果結構體中有陣列呢?很簡單將陣列看做是連續數個相同型別的元素即可。

0x e6 第一章小結

深入理解計算機系統的“系統”,並不是作業系統,這個系統包括了硬體,作業系統,網路,編譯等等

學習計算機系統應該具備的三個抽象能力:問題抽象,系統抽象(csapp),資料抽象

計算機系統是由硬體和系統軟體組成的。

數字的機器表示方法是對真值的有限近似值

指令的執行:

  1. 從磁碟讀取指令和資料到記憶體
  2. 從記憶體送到cpu中去執行
  3. 將返回的資料送到螢幕

0x e7 bomb lab

.1 phase1

disas main,可以發現我們輸入的字串賦值給了 $rdi
並且之後呼叫了函式<phase_1>
disas phase_1
發現沒有修改暫存器 $rdi 的值
然後把一個立即數 0x402400 傳給了暫存器 $esi
之後呼叫函式 <strings_not_euqal>
在之後test $eax $eax
如果 je,即 $eax = 0
呼叫函式 <eoplode_bomb>,炸彈爆炸
否則正常返回

進入函式 <strings_not_equal>
該函式又會呼叫 <string_length> 函式
這個函式會計算 $rdi 內字串的長度

p/x $rdx :以x(16進位制)方式列印暫存器$rdx的值
x $rdx 檢查(examine) $rdx記憶體中的值

watch = sepcial break

.2 phase2

.3 phase3

![78E9B95E-D7EC-4E49-8A30-94EF4B0A4D48](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/78E9B95E-D7EC-4E49-8A30-94EF4B0A4D48.png)

# phase_3
if(eax > 1)
{
    if(7 < rsp + 8)
    {
        eax = rsp + 0x8; // first input
        switch(eax)
        {
            case 0: 
                eax = 0xcf;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 1:
                eax = 0x137;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 2:
                eax = 0x2c3;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 3:
                eax = 0x100;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 4:
                eax = 0x185;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 5:
                eax = 0xce;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
            case 6:
                eax = 0x2aa;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;
        }
        	case 7:
        		eax = 0x147;
                if(rsp + 0xc == eax)    return Accept;
                else    return BOOM!!!;       		
    }
    else
    {
        return BOOM!!!
    }
}
else
{
    return BOOM!!!;
}

有多組答案:注意第二個引數不能輸入十六進位制數,只能輸入10進位制數,因為這裡的資料的讀如是採用sscanf,把我們的輸入作為str,如果我們的第二個引數是個十六進位制數,那麼一定以0x開頭,結果0會被讀取到第二個引數,讀到x不合法就結束了。

第一個引數 第二個引數
0 207
1 311
2 707
3 256
4 389
5 206
6 682
7 327

.4 phase4

第一個引數

.func4:
eax = edx
eax -= edx
ecx = eax
ecx >>= 0x1f // unsigned
eax += ecx
eax >>= 1
ecx = &(rax+rso+1)
if(ecx <= edi)  
{
    eax = 0;
    if(exc >= edi)  return 0; // 只有當 ecx<=edi<=ecx,即edi=ecx=7時可以正常退出並返回0
    else
    {
        。。。
    }
}
else
{
    edx = &(rcx - 1)
    call func4
}
// goal: make eax = 0

第二個引數看phrase4的彙編很容易得出為0

.5 phase5

reference

.6 phase6

不想做了

.7 phase7

no

.8 answer(2016)

Border relations with Canada have never been better.
1 2 4 8 16 32
7 327
7 0
)/.%&'

0x 09 Assembly實驗

BE9A5FC6EBB55797FF78C5D5105D31DF

如上圖,我們用(gdb) x mingling列印 0x7fffffffe3b0附近的值,這個地址是個虛擬地址,它在記憶體中的值為0x0

棧指標是會浮動的!但是rsp和rbp的差值應該是不變的。

gdb(ni) :會跳出函式執行

gdb(si):會進入函式執行

![A44D5C36-2816-49B3-9E01-23E15BC5DA72](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/A44D5C36-2816-49B3-9E01-23E15BC5DA72.png)

小端儲存的又一個例子啊,我們把暫存器 %rbp(0x7fffffffe3d0) 放入 %rsp,觀察可以發現,0x00007ffff倍放在了後面的地址,而0xffffe3d0被放在了前面的地址。x命令列印的地址從左到右,從上到下是以4為單位遞增的,

0x 0a ld_preload環境變數劫持函式

首先在目錄下建立兩個檔案 main.c 和 txt

#include <stdio.h>
int main() // main.c
{
	FILE *fd = fopen("txt", "r");
	if(fd == NULL)
	{
		printf("*** open file error!\n");
		return 1;
	}
	printf("open file success!\n");
	return 0;
}

正常來說最後程式會正確執行

但如果我們更改動態連結庫

先建立一個trik動態連結庫

#include <stdio.h> // trik.c
FILE *fopen(const char *path, const char *mode)
{
	printf("*** Always open error!");
	return NULL;
}
gcc -shared -fPIC trik.c -o trik.so
LD_PRELOAD=$PWD/trik.so ./a.out

最後檔案會開啟失敗

![53DE4182-359A-4F3E-80BB-4B97508E7F9B](/Users/epoch/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/Users/2407217576/QQ/Temp.db/53DE4182-359A-4F3E-80BB-4B97508E7F9B.png)

原理就是透過自己寫的庫函式劫持系統的庫函式,使得程式執行我們的庫函式。

0x 0b attack lab

0x0c 連結 points

1.引入啞節點dummy

2.引入資料結構–elf

3.靜態連結的過程:elf定位到符號->符號解析->重定位

4.*.o, elf 都是二進位制檔案

5.unix下大部分工具都在/usr/bin或者/bin目錄下的。使用hexdump可以檢視二進位制檔案

6.第一個section的name為空(其實叫做 undefine section),且資料全為0,裡面存放的內容是undefine的資料。

7.將函式定義為一個弱符號:attribute__((weak)) int add*() {} ,這裡的 add 函式被定義為一個弱符號,它可以被強符號函式 add 覆蓋。

8.對於 C Language 來說,出現 Warning 說明你的語句有歧義 ,但是 C 語言為你選擇了一種結果,注意這種結果可能與你的本意不同!

9.對於初始化為 0 的全域性變數和靜態變數,也被劃分到 .bss,這是因為全域性變數和靜態變數預設初始化就是 0

10.為什麼在可重定位目標檔案中有 COMMON,在可執行目標檔案中就沒有 COMMON 了呢。

回想一下COMMON的定義,對於未初始化的全域性變數, 屬於COMMON

對於未初始化的全域性變數, 在連結之後它有三種可能的情況(假設這裡有兩個檔案 s1.c, s2.c,在 s1.c 中定義有未初始化的全域性變數 g

  1. 如果在 s2.c 中也定義了一個全域性變數 g 並且初始化為 0,則 g 屬於 .bss

  2. 如果初始化不是 0,就屬於 .data

  3. 如果 s2.c 沒有定義 g ,那麼 s2 就屬於 .bss

    因為有如上三種(合法)情況,所以把它劃分到 COMMON,而之所以在可執行目標檔案中沒有了 COMMON ,是因為此時已經連結完了,g 屬於那個節已經很明確了,因此也就不需要了。

0x0d 修改 ROF 資訊的實驗

首先編譯原始檔 add.c 生成可重定位目標檔案 add.o

int addcnt = 0;

int add(int a, int b)
{
	addcnt ++ ;
	return a + b;
}

使用 hexdump -S add.o 檢視 Section Headers

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000003c  0000000000000000  AX       0     0     4
  [ 2] .rela.text        RELA             0000000000000000  00000228
       0000000000000060  0000000000000018   I       9     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000007c
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  0000007c
       0000000000000004  0000000000000000  WA       0     0     4
  [ 5] .comment          PROGBITS         0000000000000000  0000007c
       0000000000000027  0000000000000001  MS       0     0     1
  [ 6] .note.GNU-stack   PROGBITS         0000000000000000  000000a3
       0000000000000000  0000000000000000           0     0     1
  [ 7] .eh_frame         PROGBITS         0000000000000000  000000a8
       0000000000000030  0000000000000000   A       0     0     8
  [ 8] .rela.eh_frame    RELA             0000000000000000  00000288
       0000000000000018  0000000000000018   I       9     7     8
  [ 9] .symtab           SYMTAB           0000000000000000  000000d8
       0000000000000138  0000000000000018          10    11     8
  [10] .strtab           STRTAB           0000000000000000  00000210
       0000000000000018  0000000000000000           0     0     1
  [11] .shstrtab         STRTAB           0000000000000000  000002a0
       0000000000000059  0000000000000000           0     0     1

可以發現下標為 1 的節是 .text

我們現在要修改 add.o 使其顯示為 .ext

首先需要下載 hexedit

然後複製一份 add.o 的副本 badadd.o

(不在原始檔上直接修改是個好習慣)

然後執行命令hexdump -c badadd.o 找到 .text 的位置。

透過 elf header 中的資訊可以得到 Section header tableoffset0x300,其中每個條目(entry) 的 size0x40 ,由此可以得到第二個條目(下標為1)的 .text 節的位置為 0x340,並透過 struct elf64_shdr 得到前 4 個位元組為 name

00000340 20 00 00 00 01 00 00 00 06 00 00 00 00 00 00 00 | ...............|

name = 0x00000020 ,我們只需要修改其為 0x00000022,就可以實現 name 往後偏移兩個位元組

這樣 name 就從 ``.text變成了ext`

執行命令:hexedit badadd.o 找到位置並修改即可。

F10 退出

最後結果如下:

readelf -S badadd.o

[Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] ext               PROGBITS         0000000000000000  00000040
       000000000000003c  0000000000000000  AX       0     0     4
  [ 2] .rela.text        RELA             0000000000000000  00000228
       0000000000000060  0000000000000018   I       9     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000007c
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  0000007c
       0000000000000004  0000000000000000  WA       0     0     4
  [ 5] .comment          PROGBITS         0000000000000000  0000007c
       0000000000000027  0000000000000001  MS       0     0     1
  [ 6] .note.GNU-stack   PROGBITS         0000000000000000  000000a3
       0000000000000000  0000000000000000           0     0     1
  [ 7] .eh_frame         PROGBITS         0000000000000000  000000a8
       0000000000000030  0000000000000000   A       0     0     8
  [ 8] .rela.eh_frame    RELA             0000000000000000  00000288
       0000000000000018  0000000000000018   I       9     7     8
  [ 9] .symtab           SYMTAB           0000000000000000  000000d8
       0000000000000138  0000000000000018          10    11     8
  [10] .strtab           STRTAB           0000000000000000  00000210
       0000000000000018  0000000000000000           0     0     1
  [11] .shstrtab         STRTAB           0000000000000000  000002a0
       0000000000000059  0000000000000000           0     0     1

0x0e vim tabe

vim中的分頁命令,多視窗vim

透過help tab-page-intro命令,可以獲得關於標籤頁使用的更多資訊。

:tabnew 新建標籤頁
:tabs 顯示已開啟標籤頁的列表
:tabc 關閉當前標籤頁
:tabe <filename> 開啟新檔案(tabedit)
:tabp 移動到上一個標籤頁
:tabn 移動到下一個標籤頁(tabnext)
:gt 移動到下一個標籤頁
:tabr 移動到第一個標籤頁(tabrewind,tabfirst)
:tabl 移動到最後一個標籤頁(tablast)
$vim -p <f1> <f2> <f3> vim開啟多個標籤頁

0x0f bilbili 連結

連結步驟:

  1. parse text
  2. symbol parse
  3. Relocation

2 和 3 都依賴於 1 的 text

Csapp Link

::English

separate compliation:分離編譯

mangling:重整

:: Tool

GNU READELF:檢視目標檔案內容的很方便的工具。

0x00 introduce

1. 連結的執行階段

  1. compile time
  2. load time
  3. run time
  1. 理解連結器將幫助你構造大型程式
  2. 理解連結器將幫助你避免一些危險的程式設計錯誤。
  3. 理解連結器將幫助你理解語言的作用域規則是如何實現的。
  4. 理解連結將幫助你理解其他重要的系統概念。(載入和執行程式,虛擬記憶體,分頁,記憶體對映)
  5. 理解連結將使你能夠利用共享庫。

0x01 compiler driver

compiler dirver:編譯器驅動程式

它代表使用者在需要的時候呼叫:

  1. cpp
  2. cc1
  3. as
  4. ld

可以使用 -v 選項檢視這個過程

當我們在 Linux 命令列輸入:./proc

shell 呼叫作業系統中一個叫做載入器的函式,它將可執行檔案 proc 中的程式碼和資料複製到記憶體,然後將控制轉移到這個程式的開頭。

Relocaable object file: 由各種不同的程式碼和資料節(section)組成,每一節都是一個連續的位元組序列。

為了構造 executable file,linker 必須完成兩個主要任務:

  1. Symbol resolution(符號解析):符號解析的目的是將每個符號引用正好和一個符號定義關聯起來。
  2. relocation(重定位)。

Symbol(符號):目標檔案定義和引用符號,每個符號對應於一個函式,一個區域性變數或一個靜態變數(即C語言任何非 static 屬性宣告的變數)。

Compiler and Assembly generate code and data section start at address 0, linker connect every symbol define with one memory address, so can relocate those sections, and then modify all the symbol define, make them point the address. Linker use the detailed instructions of relocation entry(重定位條目) which generated by assembly to execute those relocation with no check.

0x03 object file

object file(目標檔案) types:

  1. relocatable object file:在編譯時與其他可重定位目標檔案合併起來,建立一個可執行目標檔案。
  2. executable object file
  3. Share object file(共享目標檔案): 一種特殊型別的可重定位目標檔案,可以在載入或者執行時被動態地載入進記憶體並連結。

Compiler and Assembly generate relocatable object file. Linker generate executable object file.

Technically talking, a object module(目標模組) is a byte sequence, and a object file is a object module which storage in disk as a type of file.

目標檔案是按照特定的目標檔案格式來組織的,各個系統的目標檔案格式都不相同。

  1. Unix: a.out
  2. Windows: PE(Portable Executable)(可移植可執行)
  3. MacOS-X: Mach-O
  4. Modern x86-64 and Unix: ELF(Executable and Linkable Format)(可執行可連結格式)

0x04 relocatable object file

典型的ELF可重定位目標檔案

IMG

ELF contains: ELF header,Sections,Section header table(節頭部表)。

(1) ELF header:

  1. 以一個 16 位元組的序列開始,這個序列描述了生成該檔案的系統的字的大小位元組順序。
  2. 剩下的部分包含幫助連結器語法分析和解釋目標檔案的資訊。其中包含:
    • ELF 頭的大小
    • 目標檔案的型別(可重定位、可執行或者共享)
    • 機器型別(x86-64)
    • 節頭部表的檔案偏移
    • 節頭部表中條目的大小和數量

(2) Section headere table: 不同 Section 的位置和大小是由節頭部表描述的,其中目標檔案中的每個節都有一個固定大小的條目(entry)。

(3) Section:

  1. .text:已編譯程式的機器程式碼。
  2. .rodata:只讀資料。
  3. .data:已初始化的全域性和靜態 C 變數。(區域性變數在棧中,既不出現在 .data中,也不出現在 .bss彙總)
  4. .bss:未初始化的全域性和靜態 C 變數,以及所有被初始化為 0 的全域性或靜態變數(預設初始化)。在目標檔案中這個節不佔用實際的空間,它僅僅是一個佔位符。 目標檔案中區分 .bss 和 .data 是為了空間效率:在目標檔案中,未初始化變數不需要佔用任何實際的磁碟空間。執行時,在記憶體中分配這些變數,初始化為0。
  5. .symtab;符號表。存放在程式中引用定義的函式和全域性變數的資訊。(不包含區域性變數的條目)。
  6. .rel.text:relocation。一個 .text 節總位置的列表。當 Linker 把這個目標檔案和其他檔案組合時,需要修改這些位置。一般而言,任何呼叫外部函式或者引用全域性變數的指令都需要修改。
  7. .rel.data:被模組定義或引用的所有全域性變數的重定位資訊。一般而言,任何已初始化的全域性變數,如果它的值是一個全域性變數地址或者外部定義的函式的地址,都需要被修改。
  8. .debug:除錯符號表。只有使用 -g 選項時才會得到這張表。
  9. .line:原始 C 源程式中的行好和 .text 節 中機器指令之間的對映。只有使用 -g 選項時才會得到這張表。
  10. .strtab:字串表。其內容包含 .symbol 和 .debug節中的符號表,已經節頭部中的節名字。字串表就是以 null 結尾的字串的序列。

為什麼未初始化的資料成為 .bss

起始於 IMB 704 組合語言(大約在1957年) Block Storage Start(塊儲存開始)指令的首字母縮寫。並沿用至今。

你可以這樣理解並區分於 .data:Better Save Space(更好的節省空間)的縮寫。

0x05 symbol and symbol table

每個 relocatable object module m 都有一個符號表,它包含 m 定義和引用的符號的資訊。在 Linker 的上下文中,有三種不同的符號:

  1. m 定義的並且能被其他 module 引用的全域性符號。
  2. 其他 module 定義並被模組 m 引用的全域性符號,
  3. 只被 m 定義和引用的區域性符號。

符號表是由 Assembly 構造的,使用 Compiler 輸出到組合語言 .s 檔案中的符號。

.symtab 節的內容是一個陣列,陣列的元素是一個符號條目:

typedef struct {
    int name;
    char type: 4,
    	binding: 4;
    char reserved;
    short section;
    long value;
    long size;
} Elf_64_Symbol;

name:是字串表中的位元組串,指向符號的以 null 結尾的字串名字。

section(base_address):到節頭部表的索引,指明被分配到那個節。

value(offset_address):是符號的地址。對於可重定位的 module 來說,value 是距定義目標的節的其實地址的 offset。

size:是目標的大小(byte)。

type:data or function。

binding:static or global

有三個特殊的偽節,它們在節頭部表中是沒有條目的(只有可重定位目標模組才有):

  1. ABS:不應該被重定位的符號。
  2. UNDEF:未定義的符號,也就是在本目標模組中引用,但是在其它地方定義的符號。
  3. COMMON:還未被分配位置的未初始化的資料目標。對於 common u符號,value 欄位給出對其要求

common 和 .bss 的區別很細微,現代的 GCC 根據以下規則分配符號:

  1. Common: 未初始化的全域性變數
  2. .bss:未初始化的靜態變數,及其初始化為0的全域性變數和靜態變數

0x06 symbol parse

1.連結器解析符號引用的方法

連結器解析符號引用的方法是將每個引用於它輸入的可重定位目標檔案的符號表的一個確定的符號定義關聯起來。

對那些和引用定義在相同模組中的區域性符號的引用,符號解析是非常簡單明瞭的。編譯器只允許每個模組中每個區域性符號有一個定義。靜態區域性變數也會有本地連結器符號,編譯器還要確保他們擁有唯一的名字。

不過,對全域性符號的引用解析就棘手的多。當編譯器遇到一個不是在當前模組中定義的符號(變數或者函式名)時,會假設該符號是在其它某個模組中定義的,升成一個連結器符號表條目,並把它交給連結器處理。如果連結器在它的任何輸入模組中都找不到這個被引用符號的定義,就輸出一條(通常很難閱讀的)錯誤資訊並終止。

2.c++ 和 java 中的重整恢復

C++ 和 Java 都允許過載方法,這些方法在原始碼中有相同名字,卻有著不同的引數列表。那麼連結器是如何區別這些不同的過載函式之間的差異呢?

因此編譯器將每個唯一的方法和引數列表組合編碼成一個對連結器來說唯一的名字。這種編碼過程叫做重整(mangling),而相反的過程叫做恢復(demangling)。

幸運的事,C++ 和 Java使用相容的重整策略。一個被重整的類名字是由名字中字元的整數數量,後面跟上原始名字組成的。例如:類 Foo 被編碼成 3Foo。方法被編碼為原始方法名,後面加上‘__’(下劃線),加上被重整的雷鳴,再加上每個引數的單字母編碼。比如:Foo::bar(int, long) 被編碼為 bar_3fooil。

重整全域性變數和模版名字的策略是相似的。

例如 C++程式 :

#include <iostream>

using namespace std;

int get(int a, int b)
{
	return a + b;
}

int get(int a, int b, int c)
{
	return a + b + c;
}

int main()
{
	int a = 1, b = 2, c = 3;
	int sum1, sum2;
	sum1 = get(a, b, c);
	sum2 = get(a, b);
	cout << "sum1: " << sum1 << endl;
	cout << "sum2: " << sum2 << endl;
	return 0;
}

執行命令:

readelf mangling.o --syms

得到如下符號表:

Symbol table '.symtab' contains 30 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS mangling.cpp
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 .data
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 .bss
     5: 0000000000000000     1 OBJECT  LOCAL  DEFAULT    4 _ZStL8__ioinit
     6: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    4 $d
     7: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    1 $x
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .rodata
     9: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    5 $d
    10: 00000000000000fc    96 FUNC    LOCAL  DEFAULT    1 _Z41__static_ini[...]
    11: 000000000000015c    28 FUNC    LOCAL  DEFAULT    1 _GLOBAL__sub_I__[...]
    12: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 .init_array
    13: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    6 $d
    14: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 .note.GNU-stack
    15: 0000000000000014     0 NOTYPE  LOCAL  DEFAULT   10 $d
    16: 0000000000000000     0 SECTION LOCAL  DEFAULT   10 .eh_frame
    17: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 .comment
    18: 0000000000000000    32 FUNC    GLOBAL DEFAULT    1 _Z3getii
    19: 0000000000000020    44 FUNC    GLOBAL DEFAULT    1 _Z3getiii
    20: 000000000000004c   176 FUNC    GLOBAL DEFAULT    1 main
    21: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZSt4cout
    22: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZStlsISt11char_[...]
    23: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZNSolsEi
    24: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZSt4endlIcSt11c[...]
    25: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZNSolsEPFRSoS_E
    26: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZNSt8ios_base4I[...]
    27: 0000000000000000     0 NOTYPE  GLOBAL HIDDEN   UND __dso_handle
    28: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _ZNSt8ios_base4I[...]
    29: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __cxa_atexit

可以觀察到,兩個 get 函式分別被標識為:_Z3getii_Z3getiii

3. Linux 處理多重定義的符號

強符號:函式和已初始化的全域性變數

弱符號:未初始化的全域性變數

Linux 處理多重定義的符號名的三個規則:

  1. 不允許多個同名的強符號。
  2. 如果有一個強符號和多個弱符號同名,選擇強符號。
  3. 如果有多個弱符號同名,任意選擇一個。

4. 靜態庫

4.1 為什麼要引入靜態庫?

如果不引入靜態庫的話,試想一下編譯器開發人員會使用什麼方法來向使用者提供這些函式。

編譯器代勞!

一種方法是讓編譯器辨認出對標準函式的呼叫,並直接生成相應的程式碼。對於那些提供了一小部分標準函式的語言(例如 Pascal)是可以的,但是對於 C 這種標準定義了大量的標準函式是不可以的。因為每次新增、修改或刪除一個標準庫函式時,就需要一個新的編譯器版本。然而,對於應用程式猿而言,這種方法是非常方便的,因為標準函式將總是可用(只需要你編譯器開發人員搞定就行了,管我什麼事 - -

所有函式對應一個可重定位目標模組!

另一種方法是將所有的 C 函式都放在一個單獨的可重定位目標模組中(比如說 libc.a),應用程式猿可以把這個模組連線到他們的可執行檔案中:

gcc main.c /usr/lib/libc.o

IOS C99 定義的 C庫:libc.a; 數學函式庫:libm.a

透過把函式放在目標模組中,可以把編譯器的實現與標準函式的實現分離開來。但是,現在每個可執行檔案都包含著一份標準函式集合的副本(除非你不連結它,但這怎麼可能呢?),這是對磁碟的極度浪費!在一個典型的系統中,libc.a 大約是 5MB,llib.a 大約是 2MB)。另外,每個執行的程式都將它的這些函式的副本放在記憶體中,這是對記憶體的極大浪費。此外,只要標準庫修改了一個小小的地方,無論多麼小,你都要重新編譯整個原始檔,非常耗時

每個函式對應一個可重定位目標模組!

我們可以透過為每個庫函式建立一個獨立的可重定位模組,把他們放在一個為大家都知道的目錄中來解決其中的一些問題。然而,問題也是相當明顯的:

  1. 那你要手寫多少模組啊?
  2. 太多了不小心寫錯名字了怎麼辦?從頭再檢查一遍吧!
  3. 太多了,你得寫到什麼時候?
  4. 。。。
  5. 真是一個麻煩又耗時又糟心的過程!

gcc main.c /usr/lib/printf.o /usr/lib/scanf.o ........

靜態庫!

於是,為了解決這些問題,靜態庫誕生了!!!

我們可以結合上面的方法,既不把所有函式劃分到一個模組,也不每個函式對應一個模組,而是把一些相關的函式劃分到一個模組(例如 C 標準庫和數學庫等),然後封裝成一個單獨的靜態庫檔案。而不是每個函式對應一個模組。

gcc main.c /usr/lib/libc.a /usr/lib/libm.a ..

你可能會問:這個靜態庫和前面把所有函式放在一個可重定位目標模組有什麼區別嗎?不就是一個叫(模組 .o),一個叫靜態庫(.a)罷了!

那我可就得給你好好講講了:當所有函式封裝在一個模組中,那我們連結的時候,就不得不連結所有庫函式了。

但是!接下來好好聽了!

如果說模組是函式的集合,那麼靜態庫就是模組的集合!所以,你可能想到了,雖然我們連結到了靜態庫,但並不連結靜態庫中的所有模組,而是隻連結需要用到的模組,這樣既避免了類似於一個函式一個模組那樣連結模組太多的問題,又避免了連結所有模組的問題。

你可能會問:這怎麼實現呢?

答案是:暴力出奇跡,迴圈判斷是否用到就好了。用不到的模組就捨棄掉。

妙不妙!再看一看靜態庫的定義吧。

在 Linux 中,靜態庫是以一種稱為 存檔(archive) 的特殊檔案形式存放在磁碟中的。存檔是一組連線起來的可重定位目標檔案的集合,有一個頭部用來描述每個成員目標檔案的大小和位置。存檔檔案由字尾(.a)標識。

4.2 建立靜態庫

靜態庫和動態庫建立參考

(1) 首先,我們需要原始檔(.c)

這裡為 mul.c 和 add.c

int mulcnt = 0;

int mul(int a, int b)
{
	mulcnt ++ ;
	return a * b;
}// mul.c
int addcnt = 0;

int add(int a, int b)
{
	addcnt ++ ;
	return a + b;
}// add.c

(2) 然後,我們需要將原始檔處理成可重定位目標檔案

gcc -c add.c mul.c

(3) 最後,將需要的可重定位目標檔案封裝到靜態庫中。

例如: ar rcs mylib.a a.o b.o...

r: replace and insert

c : create

s: add index

ar rcs mylib.a add.o mul.o

(4) 別以為就這樣結束了,編寫個 main 程式測試你下你的庫吧!

#include <stdio.h>

int main()
{
	int x = 1, y = 2;
	int s1 = add(x, y);
	int s2 = mul(x, y);
	printf("x = %d, y = %d\nsum = %d, mul = %d\n", x, y, s1, s2);

	return 0;
}
gcc -c testar.c # 先編譯生成可執行檔案
gcc --static -o main testar.o -L. mylib.a # 與靜態庫連結

–static 引數告訴編譯器驅動程式,連結器應該構建一個完全連結的可執行目標檔案,它可以載入到記憶體並執行,在載入時無需更進一步的連結。所以說不加也是可以的。

-Ldir 指明瞭連結器在那個目錄下查詢 mylib.a,dot就表示當前目錄。

0x07 relocation

1. 重定位的任務:

重定位合併輸入模組,併為每個符號分配執行時地址。

由兩步組成:

  1. 重定位節和符號定義:

    1. 將所有相同型別的節合併為一個節
    2. 將執行時記憶體地址賦給新的聚合節
    3. 賦給輸入模組定義的每個符號

    完成後,程式中的每條指令和全域性變數都有唯一的執行時記憶體地址了。

  2. 重定位節中的符號引用:連結器修改程式碼節和資料節中對每個符號的引用,使得它們指向正確的執行時地址。要執行這一步,連結器依賴於可重定位目標模組中稱為重定位條目的資料結構。

2. 重定位條目

為什麼需要重定位條目?

當彙編器生成一個目標模組時,它並不知道資料和程式碼最終將放在記憶體中的什麼位置。

它也不知道這個模組引用的任何外部定義的函式或者全域性變數的位置。

所以,無論何時彙編器遇到對最終位置未知的目標引用,它就會生成一個 “重定位條目”,告訴連結器在將目標檔案合併成可執行檔案時許和修改這個引用。

程式碼的重定位條目放在 .rel.text 中,已初始化資料的重定位條目放在 .rel.data 中。

ELF 重定位條目的格式:

typedef struct {
    long offset;	// 我在那
    long type: 32;	// 怎麼引用
    	smybol: 32; // 我引用了誰
    long addend;	// 我的偏移量
} Elf64_Rela;

offset 是需要被修改的引用的在節內的偏移。(一般是一個地址)

symbol 標識被修改引用應該指向的符號。

type 告知連結器如何修改新的引用。

addend 是一個有符號常數,一些型別的重定位要使用它對被修改引用的值做偏移調整。(addend的值一般是當前引用的地址距離下一條指令的偏移)(講的標準一點就是對 rip 的修正,因為重定位所在的地址並不是下一條指令的 rip 地址)

兩種最基本的重定位型別(type):

  1. R_X86_64_PC32:重定位一個使用 32 位 PC 相對地址的引用。(一個 PC 相對地址就是距程式計數器(PC)的當前執行時值的偏移量。當 CPU 執行一條使用 PC 相對定址的指令時,它就將在指令中編碼的 32 位值加上 PC 的當前執行時值,得到有效地址, PC 值通常是下一條指令在記憶體中的地址)。

    簡而言之,相對的意思就是,相對於下一條指令的偏移量。

  2. R_X86_64_32:重定位一個 32 位絕對地址的引用。透過絕對定址,CPU 直接使用在指令中編碼的 32 位值作為有效地址。

3. 重定位符號引用

相對引用

call addr 
	sym.offset: R_X86_64_PC32 sym

首先,要清楚我們的目標:透過 addr 的相對偏移得到該符號的執行時地址,這個地址我們是已知的。(我們用ADDR(x)表示符號 x 的執行時地址)

當前引用的地址 + 距離下一條指令的偏移量 + addr = 目標符號的執行時地址

addr = ADDR(sym) - (當前引用的地址 + 距離下一條指令的偏移量)

不過,距離下一條指令的偏移量通常以 sym.addend 的形式存在,於是,上式變成了:

addr = ADDR(sym) - 當前引用的地址 + sym.addend

我們發現,公式在經過轉換後,由 “距離” 下一條指令的偏移量變成了 “加上” sym.addend。

而偏移量肯定是一個正數(不然怎麼偏移到下一條指令),所以說 sym.addend 肯定是個負數。

自己推導的,不一定對??

而當前引用的地址 = 引用所在節的執行時地址 + 引用的偏移(sym.offset)

所以,上式最終等於如下:

addr = ADDR(sym) - (ADDR(Section) + sym.offset)+ sym.addend


絕對引用

call addr
	sym.offset: R-X86_64_32 sym

addr = ADDR(sym) + sym.addend

在絕對引用中,我們依然需要加上偏移量addend,只不過 sym.addend=0。

可以發現,相較於絕對引用,相對引用只需要減去當前引用的地址即可,距離下一條指令的偏移儲存在了 addend 中。

0x08 executable object file

典型的 ELF 可執行目標檔案(EOF,段和節):

img

ELF頭還包括了程式的入口點?也就是程式的第一條指令的地址。

透過圖可以發現,EOF 檔案中還多了 .init 節。.init節定義了一個小函式,叫做 _init_,程式的初始化程式碼會呼叫它。

.text,.data,.rodata 與可重定位目標檔案的節是相似的,除了這些節已經被重定位到它們最終的執行時記憶體地址以外。

因為 EOF 檔案是完全連結的(已被重定位),所以它不再需要 .rel 節。

EOF 檔案還有對其要求。這主要與虛擬記憶體有關

0x09 load EOF

我們通常在 Linux Shell 命令列輸入可執行目標檔案的名字 (例如prog) 來執行它:

Linux> ./prog

因為 prog 不是一個內建的 shell 命令,所以 shell 會認為 prog 是一個可執行目標檔案。

透過呼叫某個駐留在記憶體中稱為載入器(loader)的作業系統程式碼來執行它。

任何 Linux 程式都可以透過呼叫 execve() 呼叫載入器。

載入器將 EOF 檔案的程式碼和資料從磁碟複製到記憶體中,然後透過跳轉到程式的第一條指令或入口點來執行該程式。這個將程式從磁碟複製到記憶體並執行的過程叫做 “載入”。

img

1. 為什麼引入動態庫

當然是因為靜態庫有一些缺點了。

第一個問題,靜態庫不方便後續的更新和維護。

靜態庫和所有軟體一樣,需要定期維護和更新。

如果應用程式設計師想要使用一個庫的最新版本,他們必須以某種方式瞭解到該庫的更新情況,然後顯式的將他們的程式與更新了的庫重新連結。

第二個問題,靜態戶仍然會造成對記憶體資源的極大浪費。

雖然在上面 “引入靜態庫” 一節中我們已經說明了,靜態庫已經是一種比較節約記憶體資源的方式。

但那僅僅是在只針對一個檔案的情況下,我們儘可能只引用必須用到的模組而避免引用了許多不會用到的模組造成記憶體浪費。

但試想一下,如果我們存在許多檔案呢,幾乎每個檔案都會用到 printf() 函式等標準 IO 函式。在執行時,這些函式的程式碼會被複制到每個執行程序的文字段中(試想一下如果我們 printf() 了幾百次,難道每一次呼叫都要複製一份 printf() 的程式碼嗎?那也太浪費記憶體了!)。

特別是在一個執行上百個金層的典型系統上,這將是對稀缺的記憶體資源的極大浪費。

(記憶體的一個有趣屬性就是無論系統的記憶體多大,他總是一種奇缺資源。磁碟空間和廚房的垃圾桶具有同樣的屬性)。

於是,為了致力解決靜態庫的缺憾,共享庫誕生了。

共享庫是一個目標模組,在執行或載入時,可以載入到任意的記憶體空間,並和一個在記憶體中的程式連結起來。這個連結的過程就叫做 “動態連結”,是由一個叫做動態連結器的程式來執行的。

共享庫也稱為 “共享目標”(shared object)。在 Linux 系統中用 .so 字尾來標識。微軟的作業系統大量的使用了共享庫,它們稱為 DLL(動態連結庫)

2. 共享庫的工作方式

共享庫是以兩種不同的方式來實現 “共享”的。

首先,在任何給定的檔案系統中,對於一個酷只有一個 .so 檔案,所有引用該庫的可執行目標檔案分享這個 .so 檔案中的程式碼和資料,而不是像靜態庫的內容那樣被複制和嵌入到引用它們的可執行目標檔案中。(解決了靜態庫記憶體浪費的問題)

其次,再記憶體中,一個共享庫的 .text 節的一個副本可以被不同的正在執行的程序共享(與虛擬記憶體有關)。

img

如何構造一個共享庫:

gcc -shared -fpic -o libname.so module1.o module2.o ....

-fpic 選項指示編譯器生成與位置無關的程式碼。

-shared 選項指示編譯器建立一個共享的目標檔案。

下面將將這個共享庫連結到程式當中:

gcc -o prog main.c ./libname.so

根據上圖(7-16)我們可以發現,可執行目標檔案 prog21 在載入之後,也就是執行時可以和動態庫 livvector.so 連結。基本的思路就是當建立可執行檔案時,靜態執行一些連結,然後在程式載入時,動態完成連結過程

by xjy:

注意上面的話並不矛盾,前一句話說程式執行時和動態庫連結,下一句又說在程式載入時動態完成連結。一個是在執行時,一個是在載入時。

這可能是因為程式並不是直接全部載入到記憶體的(作業系統),它用到一點就載入一點,所以說,載入和執行是交叉的。

注意,再整個連結的過程當沒有任何動態庫的程式碼和資料真的被複制到可執行檔案 prog21 當中。反之,連結器複製了一些重定位和符號表資訊,它們使得執行時可以解析對動態庫中程式碼和資料的引用。

3. 小實驗

下面是一個小實驗,1.c,2.c 用來構建動態庫和靜態庫,main.c 是測試函式。

app 是連結靜態庫生成的可執行檔案。

prog 是連結動態庫生成的可執行檔案。

可以發現,prog 的大小比 app 小的多(小了50多倍)。

IMG

csapp memory

一、cache

0x01 一種初始化方式

#include <stdio.h>

typedef struct Node
{
    int l, r;
    char s[100];
} node_t;

int main()
{
    node_t p = {
        .l = 100,
        .r = 200,
        .s = "hello,world!",
    };
    
    printf("node: %d %d %s\n", p.l, p.r, p.s);
    
    return 0;
}

0x02 note

我們知道記憶體是分頁的,cache的 line 只會存在於某一個頁,它不會跨頁存在。

0x03 true/fake sharing

罪魁禍首:MESI 協議

false sharing有一個問題,就是對於sum求和這個例子,雖然我們設定sum1和sum2分別求和,但是sum1和sum2都是分配在棧上的,並且地址十分接近,所以它們可能在同一個cache當中,這樣不管是sum1修改還是sum2修改,都會觸法 MESI 的同步協議,這樣 false sharing的速度和true sharing相差幾乎無幾。

0x04 MESI protocol

exclusive:獨有的

exclusive 和 shared 不能共存

四種狀態:(由於讀資料不會產生資料一致性問題,因此這裡只考慮寫資料操作)

M: (exclusive) modify, like dirty. 實體地址被快取到某一個 cache,並且資料已經被修改

E: exclusive (clean).實體地址被快取到某一個 cache,並且資料沒有被修改

S: (exclusive) shared clean.實體地址被快取到 cache,並且多個 cache 共享。

如果修改一個狀態為 s 的 cache,它會傳送一個廣播,將所有其他狀態為 s 的 cache 的狀態修改為 invalid(具體方法是將其擁有資料寫入到 dram,然後修改狀態為 invalid),然後將自己的狀態修改為 M,這樣就可以保證全域性狀態下只有一個 M,也就是 exclusive的。

I: invalid.實體地址並沒有快取到 cache。

此時如果發生 cache write

  1. 如果其他 cache 的狀態都是 invalid,從記憶體 load 資料,修改器狀態為 M。
  2. 如果存在 (shared)S狀態 的 cache,將它們的資料寫入到 dram,然後修改狀態為 invalid。

每個處理器的cache line都是 dram 的 cache line 的複製

二、page table

0x01 tips

地址翻譯由硬體實現,作業系統為應用提供這個功能。

TLB 也是一個 cache。

現在 64 位的處理器(cpu)的虛擬地址一般其實只有 48 位,剩下的 16 位屬於核心。

虛擬地址空間呈現區域性密集,整體稀疏的特徵。

多級頁表在最壞的情況下(滿對映,每頁都必須有有效資料)是一棵完全二叉樹,此時頁表條目會比樸素頁表多出來一倍。但這種情況幾乎不可能出現(虛擬地址空間的稀疏性和程式的區域性性)。

頁表分配在作業系統的核心態。

在windows下,資源管理器的記憶體中可以看到:分頁緩衝池和非分頁緩衝池。分頁緩衝池指的是可以和磁碟進行換入(page in)和換出(page out)的頁,而非分頁並不是指不分頁,而是不能喝磁碟進行 swap。

0x02 how to reflect va2pa

在我們編寫的地址轉換函式中,我們簡單的透過去模數將實體地址轉換為虛擬地址,然而,這是極為不合理的,例如:

  1. 產生不合法的地址(地址越界)。例如:0x200(1024)%0x200=0x000,它產生了一個地址為 0 的地址,這顯然是錯誤的。
  2. 不同程序間地址衝突的問題。因為每個程序的地址都是從 0x00400000 開始的,而相同地址取模之後的值是相同的,這就會導致地址衝突。

一種可行的方法是使用 hashmap 完成實體地址到虛擬地址地址對映。它解決了使用取模方法產生的衝突和越界問題,但是,它又會產生以下兩個問題:

  1. 記憶體浪費嚴重。在 hashmap 中,我們需要額外的兩份空間來分別儲存實體地址和虛擬地址以記錄他們的對映關係,並且,由於 hashmap 並不是全部使用的,它的內部會有空閒,因此我們還需要乘上一個空閒率 k(k>=1),因此 hashmap 就需要額外的 2k 倍的額外記憶體空間要儲存對映資訊。
  2. 破壞程式的區域性性。由於 hashmap 的對映是離散的,這就會導致程式會被離散化,破壞程式的區域性性。

但是,hashmap 產生的這兩個問題屬於 效能 問題,它只是導致程式執行效率不好,並不會導致程式執行錯誤。而取模方法則會導致程式執行出錯。

現在我們再來想,hashmap 中記錄如此之多的對映資訊是否有必要?

肯定是有必要的,不然我們就無法找到實體地址了。但是!如果我們透過虛擬地址對映到實體地址不是離散的,例如:

虛擬地址 0x1,0x2,透過 hashmap 地址對映為實體地址:0xa, 0xabcd。如果我們想找到這兩個實體地址,我們必須儲存對映資訊,因為 0xa, 0xabcd 之間毫無關聯。但是這種離散性是毫無必要的,如果我們將地址對映為 0xa, 0xb 這種連續的地址的話,它不僅可以避免破壞程式的區域性性,還能減少地址對映需要儲存的資訊。

比如虛擬地址 [0x0, 0xffff] 這一塊區域,如果我們採用 hashmap,它需要 0xffff 份對映資訊,這也太多了。但是,如果 hashmap 對映的地址是連續的,我們就可以透過三元組(va0, pa0, offset) (offset表示偏移量)來找到這個區域內任意一個地址的對映,並且僅僅只需要一份對映資訊,對於任意 va,pa=pa0+va-va0(va >= va0 && va <= va0 + offset)。

現在, 完成地址對映需要的額外資訊由 2k 變成了 3M,M 就是上述三元組的數量,這個 M 遠小於地址的數量。

這就是分段思想。

當然,分段也是有問題的,例如:

  1. 碎片。內部碎片和外部碎片。
  2. 每次計算都需要比較 va 是否越界。(va >= va0 && va <= va0 + offset)
  3. 不方便擴充。當我們的段太大或者或許頻繁擴充的時候,尋找一個合適的空間比較麻煩。

所以說,我們需要把 offset 變成一個較小且固定的數值,這就是分頁思想。

0x03 address transfer

ARM64架構下地址翻譯相關的宏定義

else

else

0x04 page falult

MM: main memory,主存

page table is the cache from disk to main memory

交換空間:當我們頁表快取的頁滿了之後,我們想再往記憶體對映一頁,此時需要將該頁 page out,但是如果該頁的資料被修改了 dirty,我們該怎麼辦?

  1. 不管它,這肯定不行
  2. 將該頁寫回檔案 program file,這也肯定不行,我們不應該修改原始檔。
  3. 放到別的地方 – swap space。

將一頁從 mm 放到 swap space 的過程就叫做 swap out

相反的,將頁從 swap space 再放到 mm 的過程叫做 swap in

所以說,一個檔案佔用的空間包括了 mm 和 swap

swap space 也在磁碟

demand paging: waiting until the miss to copy the page to DRAM is konwn as deman paging

程式的程式碼檔案,例如 .data 段它是儲存在磁碟當中的,所以它與記憶體之間可以存在對映關係,但是 .data 段,stack, heap 不是儲存在磁碟當中的,當我們需要把這些短存放在磁碟當中時,我們需要放入 swap space 中。它們又稱為“匿名頁”(在磁碟中沒有檔案與它對應)。

三、virtual memory overview

​ virtual memory 主要是為了解決實體記憶體和程序所看到的虛擬記憶體不匹配的問題,所以說每個 virtual memory 肯定是提供給每一個程序的。

每個程序就是一段 active 的記憶體,例如:

  1. .text 是死的
  2. .data 是活的,因為它需要寫入操作等

如果區分 user 的虛擬地址空間和 kernel 的虛擬地址空間:kernel 的64位虛擬地址的最高位是1,user 的64位虛擬地址的最高位是 0。

我們通常看到的程式的虛擬地址空間圖中, user 的虛擬地址空間地址的高部分都被 stack 佔用了,但是這通常是作者的簡化,實際上地址的最高部分被 kernel 部分佔用了,只不過一半不標識出來。

只有第一級頁表可以區分user mode or kernel mode,因為只有第一級頁表可以得到地址的最高位。

使用者的虛擬地址空間中的 user 部分對映到程式的虛擬地址空間的user 部分,對映方法為:0x0 + addr,kernel mode 部分的對映方法為:0xffff + addr,user的虛擬地址空間的地址最高為2^48。0xffff正好是16位。

pgb 在 kernel 中只有唯一一份。

kernel 的虛擬地址從 2^47?

核心的地址翻譯全域性一致。

四、TLB

hardware acceleration:硬體加速

TLB is the cache of va2pa

我們可以把 cache 看作一個 key-value 庫

相關文章