深入理解計(jì)算機(jī)系統(tǒng) 第三章 程序的機(jī)器級(jí)表示(上)

[toc]

概 述

整個(gè)第三章就是在講匯編語(yǔ)言?,F(xiàn)在的程序員完全不需要去自己寫匯編語(yǔ)言,但是如果你可以看得懂,那么對(duì)分析代碼會(huì)有很大的幫助。這一篇就總結(jié)下上半部分的知識(shí)。我寫的東西是自己的總結(jié)點(diǎn),不是重寫書上的東西,所以沒看過(guò)書請(qǐng)去看書。

關(guān)鍵詞是:

  1. 歷史觀點(diǎn)和匯編語(yǔ)言概述
  2. 訪問信息中的各種指令與指示符
  3. 算術(shù)和邏輯操作
  4. 控制,也就是條件,循環(huán)等

歷史觀點(diǎn)和匯編語(yǔ)言概述

程序員寫出的高級(jí)語(yǔ)言會(huì)被編譯器轉(zhuǎn)化成機(jī)器代碼去實(shí)現(xiàn),機(jī)器代碼的文本表示,就是匯編語(yǔ)言。最后編譯器用匯編器和鏈接器生成二進(jìn)制的代碼,計(jì)算機(jī)就讀懂了。具體的命令是: gcc -og -s name.c

閱讀匯編代碼可以理解編譯器的優(yōu)化能力,并且發(fā)現(xiàn)低效率的部分,發(fā)現(xiàn)各種漏洞。

編譯器優(yōu)化代碼的時(shí)候會(huì)有不同的優(yōu)化程度,這個(gè)規(guī)律和我們寫的代碼很相似。當(dāng)追求代碼的效率到一定極致的時(shí)候,這個(gè)代碼會(huì)變得特別難讀懂,特別難改動(dòng)。如果寫一個(gè)思路特別清晰,大家一看就明白的代碼,那么它的效率肯定不是最高的。編譯器在優(yōu)化的時(shí)候有很多優(yōu)化選項(xiàng),-og是書上比較推薦的一種,因?yàn)檫@種方式能讓大家看懂匯編代碼,如果是-o1 或者 -o2的匯編代碼,我們很可能是理解不了的。

機(jī)器及代碼有兩種重要的抽象,分別是:

  • 指令集架構(gòu)
    定義了處理器的狀態(tài),指令的格式,還有每條指令對(duì)狀態(tài)的影響。x86-64讓程序看起來(lái)是每條指令都是按順序執(zhí)行,做完一個(gè)再做下一個(gè),但是其實(shí)硬件層面上是并發(fā)的執(zhí)行很多命令,但是最終能保證行為上和一條一條執(zhí)行的結(jié)果完全一致。
  • 虛擬內(nèi)存地址
    整個(gè)內(nèi)存模型被計(jì)算機(jī)認(rèn)為是一個(gè)非常大的字節(jié)數(shù)組,而且操作系統(tǒng)讓64位計(jì)算機(jī)總是認(rèn)為自己有填滿64位那么多的內(nèi)存,顯然這不現(xiàn)實(shí),很多內(nèi)存被標(biāo)注為不可用。而且操作系統(tǒng)會(huì)把虛擬地址轉(zhuǎn)化成實(shí)際的物理地址。

寄存器在匯編中是出現(xiàn)最頻繁的東西,你可以理解成cpu的每個(gè)核都有一組配套寄存器。寄存器不是cache,不是內(nèi)存。對(duì)于它你或許可以這樣子理解,辦公桌上面可以放書,旁邊的書架也可以放書,如果書再多了可以放在地下室里面,對(duì)于cpu來(lái)說(shuō),寄存器它隨手就取就存,所以是辦公桌上面的書,cache就是書架上的書,RAM啥的就是地下室的書。

在C語(yǔ)言中,我們是看不到寄存器的,沒辦法直接操作。但是匯編語(yǔ)言顯示的就是各類寄存器的操作:

  1. 程序計(jì)數(shù)器(PC),x86-64中等于%rip,它指向下一次要執(zhí)行的指令在內(nèi)存中的地址。所以cpu其實(shí)傻乎乎的,%rip指向一個(gè)指令,它就操作這個(gè)指令,然后指向下一個(gè),它做下一個(gè),while(true)循環(huán)永不停歇,打工人實(shí)錘了。

  2. 整數(shù)寄存器,有16個(gè)。這些寄存器可以儲(chǔ)存地址或者整數(shù)數(shù)據(jù)。比如一個(gè)函數(shù),傳入兩個(gè)參數(shù),那么會(huì)有兩個(gè)固定的寄存器去儲(chǔ)存這兩個(gè)參數(shù)的值。假設(shè)我們?cè)谶@個(gè)函數(shù)中創(chuàng)建局部變量,那么又會(huì)有一個(gè)寄存器去儲(chǔ)存這個(gè)局部變量的值。

  3. 條件寄存器,儲(chǔ)存條件狀態(tài)的,比如上一條指令執(zhí)行了一個(gè)減法,那么這個(gè)條件寄存器里面會(huì)被保存一組狀態(tài),之后通過(guò)這個(gè)狀態(tài)你就可以判斷了。你可以理解為if語(yǔ)句是通過(guò)這個(gè)條件寄存器來(lái)判斷真假的。


自學(xué)疑問和注意點(diǎn)--算術(shù)邏輯指令

這里面記錄的是我在看書的時(shí)候遇到的問題,還有我認(rèn)為應(yīng)該留意的知識(shí)點(diǎn)

  1. 一個(gè)就6個(gè)寄存器來(lái)保存參數(shù),如果參數(shù)過(guò)多會(huì)怎么樣?
    我猜會(huì)把這些參數(shù)暫時(shí)保存在內(nèi)存中,之后用的時(shí)候再取回來(lái)放在寄存器里面。暫時(shí)還沒見到比6個(gè)參數(shù)更多的例子。

  2. subq $8, %rdi,請(qǐng)問%rdi - 8,這個(gè)8是什么意思?單位是什么?
    應(yīng)該是-8字節(jié),因?yàn)閟ubq的單位是q(quad word,64位)

  3. movl $0x1, %eax請(qǐng)問這個(gè)操作有何特殊之處?
    當(dāng)movl指令以寄存器作為目的時(shí),雖然它只是復(fù)制了32位,但是這個(gè)操作會(huì)同時(shí)把高32位置零movz會(huì)置零,movs會(huì)符號(hào)位擴(kuò)展。

  4. 數(shù)據(jù)不允許被在內(nèi)存中直接mov到內(nèi)存上,其他操作例如內(nèi)存到寄存器,立即數(shù)到內(nèi)存均可(好像是為了硬件設(shè)計(jì)方便)。

  5. %rdi 和 %rsi 記死背硬,第一個(gè)和第二個(gè)參數(shù)。%rsp,棧指針,永遠(yuǎn)指向棧頂,我對(duì)這個(gè)東西的理解是,要執(zhí)行的程序地址被一個(gè)一個(gè)壓進(jìn)來(lái),然后執(zhí)行完了就被彈出去,比如遇到一個(gè)函數(shù),入棧,那么就跳轉(zhuǎn)到了這個(gè)函數(shù)上面,執(zhí)行完,出棧再來(lái)下一個(gè)函數(shù)。如果是遞歸就復(fù)雜一點(diǎn),遞歸的話函數(shù)棧會(huì)越堆越多,所以可能溢出。

  6. leaq和mov的區(qū)別是,leaq不解引用,直接把括號(hào)里面的東西賦值過(guò)去就行了

  7. 移位量要么是立即數(shù),要么保存在單字節(jié)寄存器里面。假設(shè)單字節(jié)寄存器里面保存了0xFF,那么不同大小的寄存器在讀取移位的時(shí)候讀取的位是不同的,如果只有一個(gè)字節(jié),就讀3位,兩個(gè)字節(jié)讀4位,以此類推,log2(總位數(shù))所以移位量是不可能大于總位數(shù)的(會(huì)被自動(dòng)余模)。

  8. (%rdi, %rsi, 4)為什么x86要搞出這么一種操作指令呢,其實(shí)就是為了計(jì)算數(shù)組或者結(jié)構(gòu)體很方便,%rdi 是初始地址,%rsi 是index,4是4個(gè)字節(jié),%rdi + %rsi * 4不就剛好是數(shù)組指針的操作


自學(xué)疑問和注意點(diǎn)--控制,條件

  1. 有一組條件寄存器,一直保存4種狀態(tài),你不需要知道這些東西是什么,只要知道cmp這句代碼之后,條件寄存器保存了這次比較的值,一般會(huì)立馬接一個(gè)setge jge等等jmp操作,就是我們寫的 if/else。jmp操作要記glgreater / less 代表有符號(hào), ababove / below 代表無(wú)符號(hào)。當(dāng)然e就是相等。

  2. 跳轉(zhuǎn)的指令有點(diǎn)奇怪,舉個(gè)栗子(知道這個(gè)是為了后面第七章)

4003fa:  74  02        je  XXXXX
4003fc:  ff    d0        callq *%rax

求這個(gè)XXXXX的地址是什么(程序想跳到哪里),第一行那個(gè)74你不用看,這個(gè)應(yīng)該代表je,之后02,代表+2,什么加2呢,是緊接著je的下一行代碼地址 + 2,也就是 4003fc + 2 = 4003fe

  1. 說(shuō)到控制就要說(shuō)到兩種傳送方法:

第一種叫 數(shù)據(jù)條件傳送(現(xiàn)代處理器使用), 第二種叫 控制條件分支(jmp .L1這種分支)。
正常人在讀if else 的邏輯當(dāng)然是,哪個(gè)true進(jìn)入哪個(gè)代碼塊,另一個(gè)在這里就先不看了。但是計(jì)算機(jī)不一樣,首先計(jì)算機(jī)使用流水線原理處理多條命令,這要求計(jì)算機(jī)必須在這個(gè)判斷指令沒出結(jié)果之前就選擇好決定進(jìn)哪個(gè)分支(啊哈哈哈哈哈是不是很懵逼)。

所以計(jì)算機(jī)只能去猜哪個(gè)正確與否,猜對(duì)了的話自然沒事,但是猜錯(cuò)了后果非常嚴(yán)重,需要多花15-30個(gè)時(shí)鐘來(lái)返回來(lái)重新計(jì)算。公開課上教授舉例:計(jì)算機(jī)像一條大海上的重量級(jí)郵輪,很難轉(zhuǎn)向。就像計(jì)算機(jī)很難把已經(jīng)運(yùn)行過(guò)的地方再跳回去運(yùn)行一下。

這個(gè)計(jì)算機(jī)猜哪個(gè)對(duì)的方法就是,控制條件轉(zhuǎn)移。

而現(xiàn)在我們常用的 數(shù)據(jù)條件傳送就不一樣了,我兩個(gè)全都給算了,之后哪個(gè)對(duì)我進(jìn)入哪個(gè)。

if (a < 0){
    a = b;
} else {
    a = c;
}

比如說(shuō)上面這個(gè)代碼,編譯器可能會(huì)把b和c都先放在寄存器里面?zhèn)溆?/strong>,在判斷之前先把 b 賦值給 a,之后用cmov來(lái)判斷,是否小于零,如果小于零就不做任何操作,如果false就把準(zhǔn)備好的c賦值給a。

  1. 不是所有的條件表達(dá)式都能用條件傳送編譯,有一些必須用分支(L1 L2 L3等等)來(lái)編譯,反例1:如果某指針可能是空指針,那就不能隨便解引用,只能判斷后在分支內(nèi)解引用。反例2:有可能if else 的計(jì)算相當(dāng)復(fù)雜,計(jì)算兩次得不償失,所以還得猜。

  2. 循環(huán),感覺循環(huán)沒啥太多可說(shuō)的,就是goto跳來(lái)跳去,有兩種跳躍方法。
    有個(gè)點(diǎn)是,編譯器很多時(shí)候會(huì)直接“看出來(lái)”循環(huán)的第一次是否能運(yùn)行,也就是說(shuō),匯編代碼里,很可能不會(huì)去做第一個(gè)判斷,而是直接就開始jmp到代碼塊進(jìn)行運(yùn)行了。

  3. switch,編譯器在寫switch的時(shí)候用了個(gè)小技巧。比如我的case是 100--103,那么他會(huì)先把我的case減100,讓case的最小值為0,有點(diǎn)像歸一化,然后會(huì)出現(xiàn)已下代碼

cmpq        $3, %rsi
ja              .L8

上面的這個(gè)ja大有門道,別忘了這個(gè)是無(wú)符號(hào)比較,也就是說(shuō),如果%rsi 大于3或者小于0,那么這個(gè)ja都成立。你品你細(xì)品。


指令與指示符

說(shuō)了這么多,其實(shí)直接開擼代碼,大家就一下子明白了,但是之前不說(shuō)這么多鋪墊,一下子過(guò)來(lái)還是太突兀了。這個(gè)代碼我是直接敲的書上的,畢竟要靠譜一點(diǎn)。

long mult2(long, long);

void multstore(long x, long y, long *dest) {
    long t = mult2(x, y);
    *dest = t;
}

下面就是匯編代碼了,你看到的前面帶個(gè)百分號(hào)%的東西,那就是寄存器,最常用的那些寄存器有16個(gè),很多都有自己的獨(dú)特功能,哪天我自己畫個(gè)圖再上傳上來(lái)吧。

multstore:
    pushq %rbx
    movq  %rdx, %rbx
    call  mult2
    movq  %rax, (%rax)
    popq  %rbx
    ret

pushq 的意思是壓棧,將寄存器rbx的內(nèi)容壓入程序棧。壓棧的話就是push,這個(gè)q是quad word的意思,也就是4字 == 64字節(jié)(一個(gè)字 == 兩個(gè)字節(jié))。說(shuō)到這里就要把后綴說(shuō)一下。匯編里面我現(xiàn)在見到的后綴,從小到大,依次是 b w l q。其實(shí)這個(gè)不注意的話有些懵,因?yàn)闀锩娼?jīng)常說(shuō) “字”,這個(gè) “字” 代表兩字節(jié) 16位,縮寫是 w,b代表一個(gè)字節(jié),l代表4個(gè)字節(jié),q就是8個(gè)字節(jié)了,也就是4個(gè)字。

mov的功能就是 mov src, dest,我們可以理解成這是一個(gè)賦值,把一個(gè)寄存器的值放在另一個(gè)寄存器里面。但是要注意,一個(gè)寄存器64位,有時(shí)候可以只改變低8或者16位,而不影響前面的位(但是32位不行,movl會(huì)直接導(dǎo)致高32位被清0)。

call就是調(diào)用另一個(gè)函數(shù),這個(gè)時(shí)候如果mult2這個(gè)函數(shù)要運(yùn)行的話,那么我們要跳到另一個(gè)函數(shù)的。

倒數(shù)第三行這個(gè)movq,括號(hào)什么意思呢?你可以理解成 指針解引用(*) 的意思,因?yàn)樗械募拇嫫鞅4娴亩际?101010的機(jī)器碼,假設(shè)%rax里面存著0001,那么計(jì)算機(jī)也不知道,這個(gè)0001,到底是地址0001,還是整數(shù)0001,但是一旦出現(xiàn)了(0001),就要去內(nèi)存0001處找值。

最后程序出棧,結(jié)束。

之后還有個(gè)操作數(shù)指示符,,臥槽實(shí)在不想寫了,感覺再寫真的就是造輪子了,我還是寫點(diǎn)自己的易錯(cuò)點(diǎn)吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容