1. 程式人生 > >2018-2019-1 20189215 《深入理解計算機系統》第三章學習總結

2018-2019-1 20189215 《深入理解計算機系統》第三章學習總結

《第3章 程式的機器級表示》


彙編程式碼是機器程式碼的文字表示,是與特定機器密切相關的。用高階語言編寫的程式可以在很多不同的機器上編譯和執行。

3.2 程式編碼

  • 彙編程式碼表示非常接近於機器程式碼。與機器程式碼的二進位制格式相比,彙編程式碼的主要特點是它用可讀性更好的文字格式表示,能夠理解彙編程式碼以及它與原始程式碼的聯絡,是理解九三級如何執行程式的關鍵一步。x96-64的機器程式碼和原始的C程式碼差別非常大。一些通常對C語言程式設計師隱藏的處理器狀態都是可見的:

    1.程式計數器(通常稱為“PC”,在x86-64中用%rip表示)給出將要執行的下一條指令在記憶體中的地址。
    2.整數暫存器檔案包含16個命名的位置,分別儲存64位的值。這些暫存器可以儲存地址(對應於C語言的指標)或整數資料。有的暫存器被用來記錄某些重要的程式狀態,而其他的暫存器用來儲存臨時資料,例如過程的引數和區域性變數,以及函式的返回值。
    3.條件碼暫存器儲存著最近執行的算術或邏輯指令的狀態資訊。它們用來實現控制或資料流中的條件變化,比如說用來實現if和while語旬。
    4.一組向量暫存器可以存放一個或多個整數或浮點數值。

  • 一些機器級程式碼和它的反彙編表示的特性值得注意:

    1.x86-64的指令長度從1~15位元組不等。
    2.設計指令格式的方式是,從某個給定位置開始,可以將位元組唯一地解碼成機器指令。
    3.反彙編器只是基於機器程式碼檔案中的位元組序列來確定彙編程式碼, 不需要訪問程式的原始碼或彙編程式碼。
    4.生成可執行的程式碼需要對一組目的碼檔案執行連結器,而這一組目的碼檔案中必須含有一個main函式。

  • 彙編程式碼中所有以.開頭的行都是指導彙編器和連結器工作的偽指令。
  • 我們的表述是ATT(根據“AT&T”命名的, AT&T是運營貝爾實驗室多年的公 司)格式的彙編程式碼,這是GCC、 OBJDUMP和其他一些我們使用的工具的預設格式。 其他一些程式設計工具,包括Microsoft的工具,以及來自Intel的文件,其彙編程式碼都是Intel格式的。這兩種格式在許多方面有所不同。比如下面的幾點:

    1.intel程式碼省略了指示大小的字尾。比如使用push和pop,而不是pushl和popl。
    2.intel程式碼省略了暫存器前面的“%”符號,用的是rbx而不是%rbx。
    3.intel程式碼用不同的方式來描述記憶體中的位置,例如是“QWORD PTR[rbx]”,而不是“(%rbx)”。
    4.在帶有多個運算元的指令情況下,列出運算元的順序相反。當在兩種格式之間進 行轉換的時候,這一點非常令人困惑。

3.3 資料格式

由於是從16位體系結構擴充套件成32位的,Intel用術語“字(word)”表示16位資料型別。因此,稱32位數為“雙字(double words)”,稱64位數為“四字(quad words)”。 在x86-64中,標準int值儲存為雙字(32位),指標(在此用char *表示)儲存為8位元組的四字。

3.4 訪問資訊

  • 一個x86-64的中央處理單元(CPU)包含一組16個儲存64位值的通用目的暫存器。 這些暫存器用來儲存整數資料和指標。
  • 運算元被分為三種類型:立即數、暫存器和記憶體引用。
  • 多種不同的定址模式
  • 簡單的資料傳送指令
  • 零擴充套件資料傳送指令
  • 符號擴充套件資料傳送指令
  • 壓入和彈出棧資料

    1.將一個四字值壓人棧中,首先要將棧指標減8,然後將值寫到新的棧頂地址。因此,指令pushq %rbp的行為等價於下面兩條指令:
    subq $8,%rsp
    movq %rbp, (%rsp)
    2.彈出一個四字的操作包括從棧頂位置讀出資料,然後將棧指標加8。 因此,指令popq %rax等價於下面兩條指令:
    movq (%rsp) ,%rax
    addq $8,%rsp

3.5 算術和邏輯操作

  • 可以分為四種::載入有效地址、一元操作、二元操作和移位。整數算術操作如下:
  • 載入有效地址(load effective address)指令leaq命令實際上是movq指令的變形。指令中第一個運算元是將有效地址寫入到目的運算元(並不是記憶體引用),同時目的運算元必須是一個暫存器。
  • 特殊的算術操作

3.6 控制

  • 條件碼
    除了整數暫存器, CPU還維護著一組單個位的條件碼(condition code)暫存器,它們描述了最近的算術或邏輯操作的屬性。可以檢測這些暫存器來執行條件分支指令。最常用的條件碼有:

    1.CF:進位標誌。最近的操作使最高位產生了進位。可用來檢查無符號操作的溢位。
    2.ZF:零標誌。最近的操作得出的結果為0。
    3.SF:符號標誌。最近的操作得到的結果為負數。
    4.OF:溢位標誌。最近的操作導致一個補碼溢位一正溢位或負溢位。
    對於邏輯操作,例如xoR,進位標誌和溢位標誌會設定成0。對於移位操作,進位標誌將設定為最後一個被移出的位,而溢位標誌設定為0。INC和DEC指令會設定溢位和零標誌,但是不會改變進位標誌。

  • 條件碼不會被直接讀取,一般常使用的方法有三種:

    1.可以根據條件碼的某種組合,將一個位元組設定為0或1

    2.可以跳轉到程式的某個其他部分
    3.可以有條件地傳送資料

  • 跳轉指令

    1.直接跳轉:跳轉目標是作為指令的一部分編碼的。
    2.間接跳轉:跳轉目標是從暫存器或記憶體位置讀出的,寫法是*後面跟一個運算元指示符。
    3.上圖中jmp是無條件跳轉指令,它可以是直接跳轉,也可以是間接跳轉。
    4.上圖中其他指令都是有條件的,條件跳轉只能是直接跳轉。

  • 條件分支的實現

    1.使用條件控制實現條件分支
    C語言中的if-else語句的通用形式模版如下:

    if (test-expr)>
        then-statement
    else
        else-statement

    彙編通常會使用下面的形式,為then-statementelse-statement產生各自的程式碼塊。它會插入條件和無條件分支,以保證能執行正確的程式碼塊。

        t = test-expr;
        if (!t)
            goto false;
        then-statement
        goto done;
    false:
        else-statement
    done:

    2.使用條件傳送實現條件分支
    條件傳送指令

  • C語言提供了多種迴圈結構,即do-WhileWhilefor。彙編中沒有相應的指令存在,可以用條件測試和跳轉組合起來實現迴圈的效果。
  • switch(開關)語句可以根據一個整數索引值進行多重分支,不僅提高了C程式碼的可讀性,而且通過跳轉表這種資料結構使得實現更加高效。跳轉表是一個數組,表項i是一個程式碼段的地址,這個程式碼段實現當索引值i時程式應該採取的動作。當開關的情況比較多,值的跨度範圍較小時,就會使用跳轉表。

3.7 過程

  • 執行時棧
    x86-64過程需要的儲存空間超出暫存器能夠存放的大小後,會在棧上分配空間。這個部分稱為過程的棧幀(stack fram)。
    以過程P呼叫Q,Q執行後返回P為例。P呼叫Q會將Q返回後下一條執行的P的程式碼作為P棧幀的一部分。Q的程式碼則會擴充套件當前棧的邊界,分配其棧幀所需要的空間。
  • 棧上的區域性儲存
    有些時候,區域性資料必須存放在記憶體中,常見的情況包括:

    1.暫存器不足夠存放所有的本地資料。
    2.對一個區域性變數使用地址運算子“&”,因此能夠為它產生一個地址。
    3.某些區域性變數是陣列或結構,因此必須能夠通過陣列或結構引用被訪問到。
    一般來說,過程通過減小棧指標在棧上分配空間。分配的結果作為棧幀的一部分,標 號為“區域性變數”。

  • 暫存器組是唯一被所有過程共享的資源
  • 遞迴呼叫一個函式本身與呼叫其他函式是一樣的。

3.8 陣列分配和訪問

  • C語言中的陣列是一種將標量資料聚整合更大資料型別的方式。C語言實現陣列的方式非常簡單,因此很容易翻譯成機器程式碼。C語言的一個不同尋常的特點是可以產生指向陣列中元素的指標,並對這些指標進行運算。在機器程式碼中,這些指標會被翻譯成地址計算。優化編譯器非常善於簡化陣列索引所使用的地址計算。
  • 指標計算
  • 變長陣列
    ISO C99引人了一種功能,允許陣列的維度是表示式,在陣列被分配的時候才計算出來。 在變長陣列的C版本中,我們可以將一個數組宣告如下:
    int A[exp1][exp2];
    它可以作為一個區域性變數,也可以作為一個函式的引數,然後在遇到這個宣告的時候,通過對錶達式exp1exp2求值來確定陣列的維度。因此,例如要訪問n×n的陣列的元素i,j,我們可以寫一個如下的函式:
int var_ele(long n, int A[n][n],long i, long j) {
    return A[i][j];
}

引數n必須寫在引數A[n][n]之前,這樣函式就可以在遇到這個陣列的時候計算出陣列的維度。
對應的彙編程式碼如下:

/* int var_ele(long n, int A[n][n],long i, long j)
    n in %rdi, A in %rsi, i in %rdx, j in %rcx */ 
var_ele :
    imulq     %rdx, %rdi
    leaq       (%rsi, %rdi, 4) , %rax
    movl      (%rax,%rcx,4) , %eax
    ret

3.9 異質的資料結構

  • 結構
    C語言的struct宣告建立一個數據型別,將可能不同型別的物件聚合到一個物件中,用名字來引用結構的各個組成部分。類似於陣列的實現,結構的所有組成部分都存放在記憶體中一段連續的區域內,而指向結構的指標就是結構第一個位元組的地址。編譯器維護關於每個結構型別的資訊,指示每個欄位(field)的位元組偏移。它以這些偏移作為記憶體引用指令中的位移,從而產生對結構元素的引用。
  • 聯合
    聯合提供了一種方式,能夠規避C語言的型別系統,允許以多種型別來引用一個物件。聯合宣告的語法與結構的語法一樣,只不過語義相差比較大。它們是用不同的欄位來引用相同的記憶體塊。
  • 資料對齊
    許多計算機系統對基本的資料型別的合法地址做出了一些限制,要求某種型別物件的地址必須是某個值K(通常是2,4,8)的倍數。這種對齊限制簡化了處理器和記憶體系統之間介面的硬體設計。無論資料是否對齊,x86-64的硬體都能正確工作。不過,intel還是建議要對齊資料以提高系統性能。
  • 資料對齊讓我想到了安裝記憶體或者系統的過程中的對齊,以前不懂為何要這樣做,現在通過這一節明白了資料對齊的重要性,可以提高系統性能,減少不必要的工作。

3.10 在機器級程式中將控制與資料結合起來

  • 理解指標:

    1.每個指標都對應一個型別.這個型別表明該指標指向的是哪一類物件。
    2.每個指標都有一個值,這個值是某個指定型別的物件的地址。
    3.指標用&運算子建立.這個運算子可以應用到任何lvalue類的C表示式上。lvalue意指可以出現在賦值語句左邊的表示式。
    4.*操作符用於間接引用指標,它的型別與該指標的型別一致。
    5.陣列與指標緊密聯絡,一個數組的名字可以像一個變數一樣引用(但是不能修改)。陣列引用與指標運算和間接引用有一樣的效果。
    6.將指標從一種型別強制轉換成另一種型別,只改變它的型別,而不改變它的值。
    7.指標也可以指向函式。

  • GDB偵錯程式常用命令
  • 記憶體越界引用和緩衝區溢位
    C對陣列不進行任何邊界檢查,因此當對越界的陣列元素進行寫操作會破壞儲存在棧中的資訊。一種特別常見的狀態破壞稱為緩衝區溢位。緩衝區溢位一個很致命的使用就是讓程式執行它本來不想執行函式,這是一種常見的通過計算機網路攻擊系統安全的方法。輸入給程式一個字串,這個字串包含一些可執行程式碼的位元組編碼,稱為攻擊程式碼,另外還有一些位元組會用一個指向攻擊程式碼的指標覆蓋返回區域。那麼執行ret指令的效果就是跳轉到攻擊程式碼。
  • 對抗緩衝區溢位可以採取以下方法:

    1.棧隨機化。使得棧的位置在程式每次執行時都有變化。
    2.棧破壞檢測。能夠檢測到棧何時已被破壞。最新的GCC版本加入了一種棧保護者機制來檢測緩衝區越界。其思想是在任何區域性緩衝區與棧狀態之間儲存一個特殊的金絲雀值。
    3.限制可執行程式碼區域。消除攻擊者向系統中插入可執行程式碼的能力。

3.11 浮點程式碼

  • 浮點傳送指令
  • 浮點轉換指令

    1.雙運算元浮點轉換指令

    2.三運算元浮點轉換指令

  • 過程中的浮點程式碼
    XMM暫存器用來向函式傳遞浮點引數,以及從函式返回浮點值。有如下規則:

    1.XMM暫存器%xmm0~%xmm7最多可以傳遞8個浮點引數。
    2.函式使用%xmm0來返回浮點值。
    3.所有XMM暫存器都是呼叫者儲存。被呼叫者可以不用儲存就覆蓋這些暫存器中任意一個。

  • 浮點數運算操作

學習進度條

章節數(新增/累積) 部落格量(新增/累積)
目標 共12章 共12篇
2018.10.21 1/1 1/1
2018.11.04 1/2 1/2
2018.12.08 1/3 1/3

第三章拖得時間有點久了,但是內容很多,還有很多內容沒有深化理解,本書需要多遍閱讀,細細體會。

參考資料