目錄編程
我我的認爲雖然實踐很重要,可是書本上的一些重點也是須要記錄的,這樣在之後的複習中就能夠知道在當時學習的時候重點是什麼,因此我仍是要對第三章的重點作記錄。
個人實踐內容在總結的後面。數組
每一個後繼處理器的設計都是後向兼容的,能夠保證較早版本上編譯的代碼在較新的處理器上運行。安全
GCC將源代碼轉化爲可執行代碼的步驟:函數
連接器——產生可執行代碼文件佈局
命令gcc---GCC C、C++編譯器。是Linux上默認的編譯器。
gcc命令調用一系列程序,將源代碼轉化成可執行代碼:學習
2.一、機器級代碼測試
1.機器級編程的兩種抽象
(1)指令集結構ISA
是機器級程序的格式和行爲,定義了處理器狀態、指令的格式,以及每條指令對狀態的影響。
(2)機器級程序使用的存儲器地址是虛擬地址
看上去是一個很是大的字節數組,其實是將多個硬件存儲器和操做系統軟件組合起來。編碼
2.彙編代碼的特色:
用可讀性更好的文本格式來表示。操作系統
3.幾個處理器:命令行
一條機器指令只執行一個很是基本的操做。
書上107頁的代碼,須要用到反彙編器。在Linux系統中,帶‘d’命令行標誌的程序OBJDUMP能夠充當這個角色。
objdump -d xxx.xx
1.Intel中:
8 位:字節 16位:字 32位:雙字 64位:四字
2.c語言基本數據類型對應的IA32表示
char 字節 1字節 short 字 2字節 int 雙字 4字節 long int 雙字 4字節 long long int (不支持) 4字節 char * 雙字 4字節 float 單精度 4字節 double 雙精度 8字節 long double 擴展精度 10/12字節
3.數據傳送指令的三個變種:
movl 傳送雙字
一個IA32中央處理單元(CPU)包含8個存儲32位置的寄存器
存儲器
(1)當即數尋址方式
格式:$後加用標準c表示法表示的整數,如$0xAFF
(2)寄存器尋址方式
如%eax,與彙編中學過的AX寄存器類比。
(3)存儲器尋址方式
相對基址變址尋址方式
MOV
movl 傳送雙字
MOVS
MOVZ
1、加載有效地址
加載有效地址指令——leal,是movl指令的變形。
指令形式:從存儲器讀取數據到寄存器。
實際:將有效地址寫入到目的操做數,而目的操做數必須是寄存器;並不真實引用存儲器。
機器代碼:二進制格式
彙編代碼:用可讀性更好的文本格式來表示。
一條機器指令值執行一個很是基本的操做。
2.二、代碼示例
gcc -o1 –S code.c
gcc將.c文件編譯器產生可讀彙編代碼文件
gcc –o1 –C code.c
gcc將.c文件編譯並彙編該代碼,二進制格式。
objdump –d code.o
用於查看目標代碼文件的內容,將目標文件反彙編爲彙編代碼文件.s
gcc –o1 –o main.c prog
生成可執行文件
爲單個過程分配的那部分棧就叫作棧幀。棧幀的最頂端以兩個指針界定——幀指針(寄存器ebp)和棧指針(esp)。
不過我認爲對剛接觸的咱們來講,就是將其看作一個函數便可。既棧幀就是幀指針ebp和棧指針esp之間的內容,即函數主體,ebp指向函數頭,位置肯定,esp指向函數尾,隨函數內部變量的增長或減小而移動。
每個被調用的函數都有一個本身的棧幀結構,而且棧幀結構是由函數本身造成的。須要注意的是:CPU中的寄存器ebp和esp都只有一個。
個人疑問是:每個被調用的函數都有一個本身的棧幀結構,可是一段代碼都有一個函數,沒有界限,可是棧幀的寄存器ebp和esp都只有一個,因此如何來協調使用完成函數調用的呢?
首先學習了地址空間分配的問題,以下圖:
我上網查了一些資料並進行了學習和採納,如IA32程序用程序棧來支持過程調用。機器用棧來傳遞過程參數、存儲返回信息、保存寄存器用於之後恢復,以及本地存儲。而爲單個過程分配的那部分棧稱爲幀棧(stack frame)。
幀棧能夠認爲是程序棧的一段,它有兩個端點,一個標識着起始地址,一個標識着結束地址,而這兩個地址,則分別存儲在固定的寄存器當中,即起始地址存在%ebp寄存器當中,結束地址存在%esp寄存器當中。也就是說寄存器 %ebp 爲幀指針,寄存器 %esp 爲棧指針。
當程序執行時,棧指針能夠移動,所以大多數信息的訪問都是相對於幀指針的。
如圖:
上圖包含了程序棧的構成,它由一系列棧幀構成,這些棧幀每個都對應一個過程,並且每個幀指針+4的位置都存儲着函數的返回地址,每個幀指針指向的存儲器位置當中都備份着調用者的幀指針。各位須要知道的是,每個棧幀都創建在調用者的下方(也就是地址遞減的方向),當被調用者執行完畢時,這一段棧幀會被釋放。還有一點很重要的是,%ebp和%esp的值指示着棧幀的兩端,而棧指針會在運行時移動,因此大部分時候,在訪問存儲器的時候會基於幀指針訪問,由於在一直移動的棧指針沒法根據偏移量準確的定位一個存儲器位置。
從書中的學習和查資料對這一小塊的內容有了更多的瞭解,好比棧的空間分配和釋放。棧幀是向地址遞減的方向延伸,因此要想給棧分配空間,能夠將棧的指針減去一個空間的值,形象的來講,就是棧的指針上移和下移就是對棧空間的分配和釋放,也就是棧變長或者變短。
首先要明白什麼是過程的實現?個人理解就是數據在調用者和被調用者之間傳遞,以及在被調用者當中局部變量內存的分配以及釋放。
那麼這一過程是如何實現的?
我上網學習了一些資料和博客,參數傳遞以及局部變量內存的分配和釋放有下面的幾個操做,這些操做都是咱們常常產生彙編代碼所看到的東西。
1.如下彙編代碼的實現就是備份原來的幀指針,調整當前的幀指針到棧指針的位置。
pushl %ebp movl %esp, %ebp
2.如下代碼的實現就是建立的棧幀給被調用者準備,當被調用者使用棧幀時,須要給臨時變量分配預留內存
subl $16,%esp
3.如下代碼處理備份被調用者保存的寄存器當中的值果有值的話,備份的方式則壓入棧頂
pushl %ebx
4.使用創建好的棧幀,好比讀取和寫入,通常使用mov,push以及pop指令等等。
5.恢復被調用者寄存器當中的值,這一過程實際上是從棧幀中將備份的值再恢復到寄存器,不過此時這些值可能已經不在棧頂了。所以在恢復時,大多數會使用pop指令,但也並不是必定如此。
6.釋放被調用者的棧幀,釋放就意味着將棧指針加大,而具體的作法通常是直接將棧指針指向幀指針,所以會採用相似下面的彙編代碼處理(也多是addl)。
movl %ebp,%esp
7.恢復調用者的棧幀,恢復其實就是調整棧幀兩端,使得當前棧幀的區域又回到了原始的位置。由於棧指針已經在第六步調整好了,所以此時只須要將備份的原幀指針彈出到%ebp便可。相似的彙編代碼以下。
popl %ebp
8.彈出返回地址,跳出當前過程,繼續執行調用者的代碼。此時會將棧頂的返回地址彈出到PC,而後程序將按照彈出的返回地址繼續執行。這個過程通常使用ret指令完成。
學習了一些過程調用和返回的指令:
指令:
首先來學習什麼是call指令:
hello.c -> 預處理 hello.i -> 編譯階段 hello.s -> 彙編階段 hello.o -> 連接階段 ->hello
從上面過程來看,通過編譯階段生成彙編是必須的。
首先先寫一個hello.c程序
#include <stdio.h> int main() { printf("hello, world\n"); return 0; }
彙編產生的代碼以下;
在hello.c中只調用了一個庫函數就是printf因此代碼裏有callq和retq看看下面幾行
400520: e8 bb fe ff ff callq 4003e0 <puts@plt> 400525: b8 00 00 00 00 mov $0x0,%eax 40052a: c9 leaveq 40052b: c3 retq
最前面是內存地址(虛擬地址),接着是地址內存的機器指令,接下來是彙編代碼call指令調用printf函數時,先把下一條指令 mov %0x0, %eax 壓棧,而後跳轉到printf函數執行printf函數。執行完了執行ret指令,ret指令是從棧中彈出剛纔壓棧的指令,繼續執行此指令和後面的指令。棧段也就是起了一個臨時保存的做用。因此ret和call是配合着使用的。
call: 將當前的IP 或者 CS:IP 壓入棧中跳轉到指定位置
ret : 用棧中所保存的數據賦值給IP的, 跳轉回來。
程序寄存器組是惟一可以被全部過程共享的資源。雖然在給定時刻只能有一個過程是活動的,可是咱們必須保證當一個過程(調用者)調用另外一個過程(被調用者)時,被調用者不會覆蓋某個調用者稍後會使用的寄存器的值。爲此必須採用一組統一的寄存器使用慣例,全部的過程都必須遵照,包括程序庫的過程。
假如沒有這些規矩,好比在調用一個過程時,不管是調用者仍是被調用者,均可能更新寄存器的值。假設調用者在%edx中存了一個整數值100,而被調用者也使用這個寄存器,並更新成了1000,因而悲劇就發生了。當過程調用完畢返回後,調用者再使用%edx的時候,值已經從100變成了1000,這幾乎必將致使程序會錯誤的執行下去。因此便有以下規矩:
在 IA32 中,寄存器%eax,%edx和%ecx被劃分爲調用者保存寄存器。當過程 P 調用 Q 時,Q能夠覆蓋這些寄存器,而不會破壞 P 所需的數據。
寄存器%ebx,%esi和%edi被劃分爲被調用者保存寄存器。這裏 Q 必須在覆蓋這些寄存器的值以前,先把他們保存到棧中,並在返回前恢復它們,由於 P(或某個更高層次的過程)可能會在從此的計算中須要這些值。上面所說的過程實現的8個步驟中第三步即是如此。
考慮以下代碼:
int P(int x) { int y = x*x; int z = Q(y); return y+z; }
過程 P 在調用 Q以前會先計算 y 的值,並且它必須保證 y 的值在 Q返回後是可用的。這裏有兩種方法實現:
能夠在調用 Q 以前,將 y 的值保存在本身的幀棧中;當 Q 返回時,過程 P 就能夠從棧中取出y 的值。換句話說就是調用者 P 本身保存這個值。
能夠將 y 保存在被調用者保存寄存器中。若是 Q ,或者其它 Q 調用的程序想使用這個寄存器,它必須將這個寄存器的值保存在幀棧中,並在返回前恢復該值。換句話說就是被調用者保存這個值。當 Q 返回到 P 時,y 的值會在被調用者保存寄存器中,或者是由於寄存器根本就沒有改變,或者是由於它被保存並恢復了。
編寫一個代碼:function.c
#include <stdio.h> int add(int a,int b){ register int c = a + b; return c; } int main(){ int a = 100; int b = 101; int c = add(a,b); return c; }
在主函數過程當中調用add過程。
gcc -O0 -S function.c
爲了完整的展示那8個步驟,所以給變量c加了register關鍵字修飾,這將會將c送入寄存器,從而更改被調用者保存寄存器,就會致使步驟3的發生。如下是main函數以及add函數各自的棧幀狀況:
1.add函數會將返回結果存入%eax(前提是返回值可使用整數來表示),在main函數中,call指令以後,默認將%eax做爲返回結果來使用。
2.全部函數(包括main函數)都必須有第1步和第六、七、8步,這是必須的4步。咱們的棧指針和幀指針有固定的大小關係,即棧指針永遠小於等於幀指針,當兩者相等時,當前棧幀被認爲沒有分配內存空間。
一個過程也能調用本身自己的,就是遞歸調用。由於每一個調用在棧中都有它本身的私人空間,多個未完成調用的局部變量不會互相影響棧的原則也提供了適當的策略,當過程被調用時分佈局部存儲空間,當過程執行完畢返回時釋放存儲空間。
下面是一段求 n 的階乘的遞歸調用代碼:
int rfact(int n){ int result; if(n<=1){ result = 1; }else{ result = n * rfact(n-1); } return result; }
產生彙編代碼:
上面的彙編代碼,當用參數 n 來調用時,首先代碼 2~5 行會建立一個幀棧,其中包含 %ebp 的舊值、保存的被調用者保存的寄存器 %ebx 的值,以及當遞歸調用自身的時候保存參數的四個字節。
以下圖所示,它用寄存器 %ebx 來保存過程參數 n 的值(第 6 行代碼)。它將寄存器 %ebx 中的返回值設置爲 1,預期 n<=1 的狀況,它就會跳轉到完成代碼。
對於遞歸的狀況,計算 n-1,將這個值存儲在棧上,而後調用函數自身(第10~12行),在代碼的完成部分,咱們能夠假設:
①、寄存器%eax保存這(n-1)!的值
②、被調用保存寄存器%ebx保存着參數n
所以將這兩個值相乘(第 13 行)獲得該函數的返回值。對於終止條件和遞歸調用,代碼都會繼續到完成部分(第15~17行),恢復棧和被調用者保存寄存器,而後在返回。
因此咱們看到遞歸調用一個函數自己與調用其它函數是同樣的。棧規則提供了一種機制,每次函數調用都有它本身的私有狀態信息(保存的返回值、棧指針和被調用者保存寄存器的值)存儲。若是須要,它還能夠提供局部變量的存儲。分配和釋放的棧規則很天然的就與函數調用——返回的順序匹配
2、一元操做和二元操做
1.一元操做
只有一個操做數,既是源又是目的,能夠是一個寄存器,或者存儲器位置。
2.二元操做
第一個操做數能夠是當即數、寄存器或者存儲器位置
第二個操做數能夠是寄存器或者存儲器位置
可是不能同時是存儲器位置。
3、移位操做
SAL 算術左移 SHL 邏輯左移 SAR 算術右移(補符號位) SHR 邏輯右移(補0)
1、條件碼
CF:進位標誌 ZF:零標誌 SF:符號標誌 OF:溢出標誌
條件碼的改變:
數據傳送指令
MOV 不影響標誌位
PUSH POP 不影響標誌位
XCHG 交換指令 不影響標誌位
XLAT 換碼指令 不影響標誌位
LEA 有效地址送寄存器指令 不影響標誌位
PUSHF 標誌進棧指令 不影響標誌位
POPF 標誌出棧指令 標誌位由裝入值決定
2、訪問條件碼
這個指的是SET指令,經過set與不一樣的條件碼的組合,達到不一樣的跳轉條件。
某些底層的機器指令可能有多個名字,咱們稱之爲「同義名」。
3、跳轉指令及其編碼
jump分爲直接跳轉和間接跳轉:
直接跳轉:後面跟標號做爲跳轉目標
間接跳轉:*後面跟一個操做數指示符
4、循環
C語言提供了多種循環結構。即do-while,while,for。彙編中沒有相應的指令存在,能夠用條件測試和跳轉組合起來實現循環的效果。
循環的實踐我在下面寫出。
一個過程調用包括將數據和控制從代碼的一部分傳遞到另外一部分。另外,它還必須在進入時爲過程的局部變量分配空間,並在退出時釋放這些空間。
1、棧幀結構
棧用來傳遞參數、存儲返回信息、保存寄存器,以及本地存儲。
1.棧幀
爲單個過程分配的那部分棧稱爲棧幀,通用結構見149頁
因此本質上棧幀仍是棧。
2.兩個指針
最頂端的棧幀以兩個指針界定:
寄存器%ebp-幀指針
寄存器%esp-棧指針
棧指針可移動,因此信息訪問多相對於幀指針。
在開始實踐的時候,我在編譯時候按照書上的指令來執行可是缺沒辦法生成正確的.o文件
都賴我想起來,老師上課說過,由於版本不同,因此在敲指令的時候,要把-o去掉,這樣就能夠生成能夠檢驗的彙編語言代碼。
最後,我還掌握了另一種看彙編代碼的指令:
cat xxx.s
還有一種就是書上的代碼方式:
objdump -d xxx.o
實驗樓中的練習:
個人代碼中彙編是movq %rbq,由於是我沒使用-m32。
在64位下因此是movq %rbq。
書上還有幾個實踐,其中看代碼的二進制代碼: