六星經典CSAPP-筆記(3)程式的機器級表示

來源:互聯網
上載者:User

標籤:

1.前言IA32機器碼以及彙編代碼都與原始的C代碼有很大不同,因為 一些狀態對於C程式員來說是隱藏的。例如包含下一條要執行代碼的記憶體位置的程式指標(program counter or PC)以及8個寄存器。還要注意的一點是:彙編代碼的 ATT格式Intel格式。ATT格式是GCC和objdump等工具的預設格式,在CSAPP中一律使用這種格式。而Intel格式則通常會在Intel的IA32架構文檔以及微軟的Windows技術文檔中碰到。兩者的主要區別有:
  • Intel格式忽略指令中暗示運算元長度的尾碼,例如mov而不是ATT格式的movl
  • Intel格式忽略寄存器名稱前的%,例如esp而不是ATT格式的%esp
  • Intel格式用不同的方式描述記憶體位置,例如DWORD PTR [ebp+8]而不是ATT格式的8(%ebp)
  • Intel格式指令的運算元順序與ATT格式的完全相反,ATT格式總是最後一個運算元是目標,例如movl %eax, (%edx)。
此外,作為16位處理器架構的遺留產物,如今的指令依舊用word指2個位元組16位,而用double word指4個位元組。所以指令中通常使用B、W、L表示運算元是1、2、4個位元組的指令,例如資料移動指令的三個版本movb、movw、movl。
這一章通過學習程式的機器級底層表示,學會閱讀底層代碼。為什麼逆向工程很難?因為原始碼與編譯後的代碼往往不是一一對應的。 編譯器會引入原始碼中不存在的新變數,同時為了節約寄存器的使用,編譯器也經常將多個值對應到一個寄存器。對於迴圈來說,通過觀察寄存器是如何在迴圈前初始化,在迴圈內的更新和條件檢測以及迴圈後的使用,能夠得到一些線索。

2.寄存器與定址第一章的筆記中我們看到,程式執行的很大一部分時間都是在將資料挪來挪去的。所以處理器支援只使用寄存器的1、2、4個位元組,同時並且支援多種定址方式。如右半邊的表格中所示,這樣我們就可以靈活地從記憶體中載入資料到寄存器,或者將寄存器中的值儲存到記憶體。


雖然看起來有些眼花繚亂,但實際上最基本的形式就是最後一種:Imm(Eb, Ei, s)=Imm+R[Eb]+R[Ei]*s (R[X]指寄存器X的值)。一共四個參數控制定址,看起來有些過於靈活,那就讓我們想象一下它的應用情境。先不考慮Imm,那麼最典型的應用就是訪問數組中某個資料項目。假如數組為int x[4],則此時Eb就是數組的首地址,相當於x,而Ei就是要訪問資料項目的下標,而s就是數組中資料類型的長度。例如我們要訪問x[3],那麼就相當於(x, 3, sizeof(int))=x+3*4。用C語言來寫就是*(x+3),因為C語言自動按照指標的類型長度進行移動(編譯器自動產生正確的代碼),所以我們並不用自己計算位移量乘以sizeof(int),但這都是後話了。那再加上Imm又能有何種應用情境,其實很簡單,就是 訪問struct中數組中某一項。如所示,直接一條指令就能訪問到結構中的數組中的某一項。



3.常用指令下面是一些最常見的彙編指令及其含義:
  • mov:資料移動。IA32強加了一條限制:一條移動指令的兩個運算元不能都是記憶體位址。所以從一個記憶體位置拷貝資料到另一個記憶體位置是需要兩條指令的。
  • leal:載入地址。效果就是mov Imm(%a, %b, s), %x會將%x賦值為Imm+%a+s*%b,而不是M[Imm+%a+s*%b],所以有兩個很有用的情境:1)拷貝地址。例如int *x=a彙編為mov (%eax), %edx,那麼int x=&a彙編為leal (%eax), %edx。所以leal不會真的將a的值(即(%eax))儲存到x(即%edx),而只是將a的地址(其實就是%eax)儲存到x。2)簡單算術運算。第二個很自然會想到的應用就是使用leal一條指令壓縮簡單的算術運算,例如leal 7(%edx, %edx, 4)=5x+7。
  • jmp:直接跳轉到標籤,或間接跳轉到寄存器中指定的地址。對於直接跳轉,在組合語言中通常就是符號化的標籤表示。但之後彙編器或連結器要對其進行編碼,最常見的編碼方式就是PC相對位址。即用1、2、4位元組的位移量表示跳轉目標地址與jmp指令緊接著的下一條指令的地址,如所示。但為什麼是緊接著jmp指令的下一條指令的地址而不是jmp這一條的?其實也是有曆史原因的,因為早期的處理器實現是先更新PC計數器作為第一步,然後再執行當前指令的。所以指令在執行的時候,其實PC已經指向下一條指令了,因此跳轉的位移量也就要相對下一條指令來說了。



4.類型轉換時發生了什麼有符號轉成不帶正負號的整數時,我們期望著編譯器能將負數變成0,正數保留不變,長過最大長度的正數賦值成TMax。然而實際上 相同長度的整數轉換其實只是簡單拷貝,什麼都不做。並且當同時需要長度轉換和類型轉換時,C語言首先進行長度轉換。長度轉換後兩個整數就都變成相同長度了,所以我們只需關注不同長度整數間的擴充和截斷是如何進行的:
  • 擴充:無符號進行零擴充,即用零填充高位。有符號進行符號擴充,即用最高位-符號位填充高位。
  • 截斷:簡單地扔掉高位位元組。對於小尾端來說,就是反過來,拷貝寄存器的高位如%al。


因為有符號整數在大部分機器上都是用反碼進行編碼的,對反碼進行有符號擴充是不會改變其值的,在第二章中有過證明。反碼就是這樣神奇!0有唯一表示,並且有符號擴充時值還不變!關鍵就在於:高位擴充出一個1後,-2w+2w-1=-2w-1,還是等於擴充前的原值。


5.邏輯運算為什麼要短路第二章筆記中曾說過位元運算和邏輯運算的兩個區別,一是邏輯運算的眼中只有TRUE和FALSE,非0的不管是幾都會被看做TRUE。而第二個區別就是邏輯運算的短路效果。那為什麼邏輯運算會短路?因為邏輯運算是用jmp實現的。在組合語言中,逐一判斷條件運算式中的各個部分的真假,當某一部分判斷出結果就直接跳轉了。正因為 邏輯運算是決定朝哪裡運行,而不像位元運算得出一個最終結果,所以組合語言可以用跳轉實現,所以就產生了進階語言中短路的性質



6.局部變數其實就在寄存器裡其實 局部變數是直接儲存在寄存器的,大部分情況下都會一直在寄存器中,而不會落地到記憶體。例如第7部分中的函數swap_add(),函數運行時棧幀(記憶體)實際上沒有儲存任何局部變數。整個函數的局部變數和邏輯都在寄存器和ALU中執行完成。
在以下情況,局部變數會被儲存在記憶體中(棧上):
  • 當沒有足夠的寄存器來儲存所有局部變數時。畢竟寄存器只有八個。
  • 一些局部變數是數組或struct,因此必須通過指標訪問。
  • 當對局部變數進行取地址&運算時,因此必須產生一個記憶體位址給它。

7.運行時的代碼與棧下面來看一個函數調用的例子,深入學習代碼底層是如何啟動並執行。

caller()代碼如下:

swap_add()代碼如下:

編譯器產生的程式碼會遵守一定的規則,這樣在執行各種跳轉、函數調用時才不會發生資料覆蓋等問題,從而使程式正確的運行。


8.指標的本質也許之前也曾聽過, 指標本質上就是一個記憶體位址。但之前沒有頓悟,現在通過研究底層知識來強化理解。從可以看出,指標取值實際上是一種很自然的操作,因為大多數時候我們 沒法在一個寄存器裡放下一個變數表示的全部資料,例如數組或結構。如果寄存器能夠放下整個數組和結構,那我們當然沒必要用指標了。所以很自然地,我們就會先載入資料的首地址的記憶體位址(就是指標!)到寄存器,然後再去訪問寄存器指向的記憶體位置。

六星經典CSAPP-筆記(3)程式的機器級表示

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.