CSAPP第三章——程式的機器級表示:學習筆記總結

Eternitykc發表於2020-11-29

花了半個多月,補完王爽老師的組合語言後,跟著CMU的視訊課+課本,學完了第三章的知識,最深的感觸就是CSAPP無論是視訊還是書的質量都非常的硬,不愧它的盛名。(lab6和課後的家庭作業還沒做,之後再補)
現在來對前面所學做一個總結。(大致按照CMU視訊的順序進行)

一、Basics 基礎

1.使用指令新建、編輯、彙編、連結組合語言程式
①新建並編輯原始碼
命令:getdit sum.c
說明:gedit是一個GNOME桌面環境下相容UTF-8的文字編輯器。使用vi或者vim同樣可以實現新建與編輯。
②預處理【sum.c -> sum.i】
命令:gcc -E sum.c -o sum.i 【sum.c -> sum.i】
說明:預處理時,編譯器會將C原始碼中包含的的標頭檔案編譯進來
③編譯 【sum.i -> sum.s】
命令:gcc -S sum.i -o sum.s
說明:gcc首先檢查程式碼的規範性,是否有語法錯誤,確定程式碼實際要做的工作,讓後將程式碼翻譯成組合語言
④彙編【sum.s -> sum.o】
命令:gcc -c sum.s -o sum.o
說明:gcc進行彙編階段,將編譯階段生成的”.s”檔案轉成二進位制目的碼(可重定位目標檔案)
⑤連結【sum.o -> sum】
命令:gcc sum.o -o sum
說明:連結過程將有關的目標檔案彼此連線起來,使得所有目標檔案成為一個能夠執行的統一整體。
⑥執行
命令:./sum
說明:執行可執行檔案,輸出結果

2.資料格式
b(byte):位元組
w(word):1字=2位元組
l(double words):雙字=4位元組
q(quad words):四字=8位元組
對應的mov指令為:
movb movw movl movq

al:1位元組
ax:2位元組
eax:4位元組
rax:8位元組

3.資料傳送
傳記憶體資料的格式:在這裡插入圖片描述
傳地址的格式:
在這裡插入圖片描述
擴充套件:
擴充套件分為零擴充套件和符號擴充套件,若要擴充套件後保持相同的數,對於有符號數,符號擴充套件(即高位補原來的最高位)可以保證擴充套件前後相同,對於無符號數,零擴充套件可以保證擴充套件前後相同。指令,以位元組->字為例,其他同樣格式。
零擴充套件:movzbw dl,ax
符號擴充套件:movsbw dl,ax
另,符號擴充套件多一個cltq指令,表示將%(eax)符號擴充套件->rax
另,當給32位暫存器賦值時,總會將高位自動改為0

4.暫存器作用

0-630-310-158-150-7使用慣例
%rax%eax%ax%ah%al儲存返回值
%rbx%ebx%bx%bh%bl被呼叫者儲存
%rcx%ecx%cx%ch%cl第4個引數
%rdx%edx%dx%dh%dl第3個引數
%rsi%esi%si%sil第2個引數
%rdi%edi%di%dil第1個引數
%rbp%ebp%bp%bpl被呼叫者儲存
%rsp%esp%sp%spl棧指標
%r8%r8d%r8w%r8b第5個引數
%r9%r9d%r9w%r9b第6個引數
%r10%r10d%r10w%r10b呼叫者儲存
%r11%r11d%r11w%r11b呼叫者儲存
%r12%r12d%r12w%r12b被呼叫者儲存
%r13%r13d%r13w%r13b被呼叫者儲存
%r14%r14d%r14w%r14b被呼叫者儲存
%r15%r15d%r15w%r15b被呼叫者儲存

前六個引數分別存在rdi rsi rdx rcx r8 r9中,若有更多的引數,則放在棧上,且棧頂為第7個引數,其次第8個引數…
rbx rdx r12~r15是被呼叫者儲存,在函式體中如果要呼叫別的函式,可以把有用的資料放在這些被呼叫者儲存的暫存器中,這樣在被呼叫的函式中,如果要改變這些暫存器的值,總是會事先入棧儲存並在return前彈出。
而呼叫者儲存的暫存器,在呼叫函式前,不希望被修改的有用資料總是要先儲存在棧中,在呼叫函式後再彈出使用。

5.算術和邏輯操作
在這裡插入圖片描述
注意算術右移(SAR)和邏輯右移(SHR),算術右移是有符號數的右移,高位補1,邏輯右移是無符號數的右移,高位補0.

imulq:有符號全乘法
mulq:無符號全乘法
【上面兩條指令都需要一個引數在%rax中】
idivq:有符號除法
divq:無符號除法
乘積存在%rdx(高64位)和%rax(低64位)
商存在%rax中,餘數存在%rdx中。

question:為什麼家庭作業1中的imulq指令乘積存在rdx中...??

二、Control 控制

1.Condition code 條件碼

條件碼英文含義
CFCarry Flag (for unsigned)無符號數相加時的進位標記
ZFZero Flag結果是0
SFSign Flag (for signed)符號標記,運算結果最高有效位為1(負數),則SF置為1
OFOverflow Flag (for signed)溢位標記,表示有符號數的溢位(兩個同符號數相加結果為不同符號,就會有這個溢位)

在這裡插入圖片描述
lea不會設定這四個標誌位。
cmpq用於比較大小,testq用於將某個數和0比較

CF和OF的區別:(參考了大佬的blog)
①首先需要知道,計算機對數值的儲存採用補碼形式儲存,一來避免了+0和-0的尷尬,二來數值的加法和減法可以統一為補碼的加法。在組合語言層面,定義變數的時候,沒有 signed和unsignde 之分,彙編器統統將你輸入的整數字面量當作有符號數(最高位的符號位根據輸入的數值符號決定)處理成二進位制補碼存入到計算機中,只有這一個標準!彙編器不會區分有符號還是無符號然後用兩個標準來處理,它統統當作有符號的!並且統統彙編成補碼!


②那麼,有符號和無符號數在計算機中是怎麼區分並對他們的運算採用不同的策略呢?有一個重要的點是,補碼是一個強大的設計,其統一了無符號數和有符號數的加法運算是相同的,即從0位到高位一個個相加,且相加的時候再加上從前面的進位。【所以用同一個加法器即可】那麼,無符號數和有符號數在加法運算的時候並不區分,用同一個加法器即可,只是對結果擁有不同的解釋權罷了【但是乘法運算用不了同一套了,能力有限】


③ OF、CF、SF標誌。
先看CF標誌位,書上說CF標誌位只對無符號數有意義,首先明白一點,即使是兩個有符號數相加,也會導致CF的變動,並不是說有符號數,編譯器不設定CF位。因為CF的標誌位的變動是由於最高有效位(如果對於8位數,就是第8位)向更高位(第9位)產生了進位或者借位而產生,而對於有符號數來說,最高位是符號位,它的變動和數值位的變動意義不一樣。所以對於有符號數,CF也可能發生變動,但是它的變動是沒意義的。而如果是無符號數,它的變動就意味中8位的記憶體或暫存器不足以儲存資料,因為資料產生了進位或借位。
再看OF標誌位,它只對有符號數有意義,因為兩個標準的8位有符號資料(標準指的是賦值的時候不要賦超過有符號數範圍的數字,由於截斷,即是8位能儲存,儲存進來的資料數值大小早就產生了變化),這2個資料只有同號(都為正或為負)相加才會溢位,也就是結果超過有符號數的範圍。例如2個正數,符號位(第8位)都為0,相加後發生溢位,符號位由於第7位的進位變成了1,兩個正數相加變為了負數?由此對OF產生了作用,如此來說OF的作用是由於符號位發生變化,如果是兩個無符號數,最高位代表的並不是符號意義,產生了變動也是無意義的,所以說OF只對有符號數有意義。
最後SF標誌,有了上面的介紹,就能理解SF看的是最高位的符號位意義,對於無符號數來說,最高位代表的是數值意義,並不是符號意義。


④可愛又可怕的c語言。
為什麼又扯到 c 了?因為大多數遇到有符號還是無符號問題的朋友,都是c裡面的 signed 和 unsigned 宣告引起的,那為什麼開頭是從彙編講起呢?因為我們現在用的c編譯器,都是將c語言程式碼編譯成組合語言程式碼,然後再用匯編器彙編成機器碼的。搞清楚了彙編,就相當於從根本上明白了c,而且,用機器的思維去考慮問題,必須用匯編。(我一般遇到什麼奇怪的c語言的問題都是把它編譯成彙編來看。)
C 是可愛的,因為c符合kiss 原則,對機器的抽象程度剛剛好,讓我們即提高了思維層面(比彙編的機器層面人性化多了),又不至於離機器太遠 (像c# ,Java之類就太遠了)。當初K&R 版的c就是高階一點的彙編……?
C又是可怕的,因為它把機器層面的所有的東西都反應了出來,像這個有沒有符號的問題就是一例(java就不存在這個問題,因為它被設計成所有的整數都是有符號的)。為了說明c的可怕特舉一例:

#include <stdio.h> 
#include <string.h> 
int main()
{
int x = 2; 
char * str = "abcd"; 
int y = (x - strlen(str) ) / 2;
//注:原作者這樣寫,編譯器可能會對其優化,直接使用右移移位指令而不是採用除法指令,改成3即可看到
printf("%d\n",y);
}

結果應該是 -1 但是卻得到:2147483647 。為什麼?因為strlen的返回值,型別是size_t,也就是unsigned int ,與 int 混合計算時,int型別被自動轉換為unsigned int了,結果自然出乎意料。。。
觀察編譯後的程式碼,除法指令為 div ,意味無符號除法。解決辦法就是強制轉換,變成 int y = (int)(x - strlen(str) ) / 2; 強制向有符號方向轉換(編譯器預設正好相反),這樣一來,除法指令編譯成 idiv 了。我們知道,就是同樣狀態的兩個記憶體單位,用有符號處理指令 imul ,idiv 等得到的結果,與用 無符號處理指令mul,div等得到的結果,是截然不同的!所以牽扯到有符號無符號計算的問題,特別是存在討厭的自動轉換時,要倍加小心!(這裡自動轉換時,無論gcc還是cl都不提示!!!)
為了避免這些錯誤,建議,凡是在運算的時候,確保你的變數都是 signed 的。


2.Conditional branches 條件分支
在這裡插入圖片描述注:greater和less是有符號的,above和below是無符號的。
在這裡插入圖片描述
注:原則:總是先判斷!test然後決定是否轉向else(上面這個是條件控制轉移)


Conditional move條件傳送
在這裡插入圖片描述

注:這是一種分支預測優化技術,基本思想是把then程式碼和else都執行得到兩個結果,然後才會選擇使用哪一個結果,看起來似乎浪費時間但事實是如果是簡單的計算,會更有效率,學到效能優化時會明白原因。【這是一種流水線技術,當程式碼執行時到達一個分支,他們會試著猜測分支結果,這被稱為分支預測技術,並且他們非常擅於預測,98%的時候他們都能猜對,所以他們可以在路上預測suta曲線,並開始朝這個方向前進,只要猜測正確,就會非常有效率,但是如果分支猜錯了,必須要阻止它並轉向另一個方向重新開始】

3.Loops 迴圈
do while:
在這裡插入圖片描述


while:
在這裡插入圖片描述


for:
在這裡插入圖片描述

4.switch語句
Switch語句利用了Jump Table跳轉表機制,跳轉表機制避免了需要順序地判斷各個case(O[n])【或者二分演算法O(logn)】,而可以根據偏移直接跳轉到那個程式碼塊case(O(1))
在這裡插入圖片描述在這裡插入圖片描述
注:switch語句總是用ja (max of x) 先判斷是否為預設,若是預設的話直接跳轉預設,不是預設的話才利用跳轉表機制,以x的值為索引去查跳轉表,然後跳到那個跳轉表中存著的分支程式碼的地址。
如果不是從0開始的,那麼會給一個偏移量【所以總變成從0開始所以】
如果case值很稀疏,那麼編譯器會優化成if-else語句
在這裡插入圖片描述

今天先寫到這,明天接著寫完。

三、Procedures 過程

內容:

四、Data 資料

內容:

五、Advanced topics

內容:

相關文章