瞭解彙編語言可以更加深刻的理解高級語言的本質,完全理解以前只是知道卻又不清楚爲何的知識,好比:程序員
使用 Visual C++ 6.0 建立一個 Win32 Console Application項目,建立一個數組:編程
#include "stdafx.h"
int main(int argc, char* argv[])
{
int array[] = {3, 4};
return 0;
}
複製代碼
輸入光標在 return 0 位置按 F9 插入斷點運行項目,而後鼠標右擊出下拉菜單點擊 Go To Disassembly 查看反彙編代碼:數組
基本能夠看出來彙編代碼的意義:將 3 存在內存地址爲 ebp-8 的位置上,將 4 存在內存地址爲 ebp-4 的位置上。sass
將高級語言代碼修改成:安全
int main(int argc, char* argv[])
{
// int array[] = {3, 4};
struct Person {
int no;
int id;
} p = {3, 4};
return 0;
}
複製代碼
反彙編查看對應的彙編代碼爲:bash
能夠發現,對應的彙編代碼一摸同樣,因此經過彙編沒法獲得對應的高級語言代碼,由於對於彙編來說,上面的兩段代碼都是開闢8個字節的內存空間,分別存3和4。經過彙編語言看逆向推導,是沒法知道高級語言是建立了數組仍是結構體的。這就是爲何彙編語言沒法還原成高級語言。數據結構
經過查看對應的彙編代碼能夠看到:架構
直接將4傳給對應的內存空間,並無調用任何函數,因此sizeof並非一個函數,而是一個編譯器特性,在編譯後直接轉成了對應的彙編代碼。函數
同理在Xcode中也是同樣的:工具
總線:即一根根導線的集合。
每個CPU新片都有許多管腳,這些管腳和總線相連,CPU經過總線和外部器件進行交互。
總線的分類:
地址總線決定了CPU的尋址能力,8086地址總線寬度是20,因此它的尋址能力是1M(2^20)。
尋址能力的計算:首先明白總線就是導線,導線可以傳遞的是電信號,電信號分爲兩種:高電平信號、低電平信號,高電平信號即 1,低電平信號即 0。假如總線總線的寬度是3,那麼3根導線高電平爲1,低電平爲0,它們最大可以傳遞的值只有2^3 種:000,001,010,011,100,101,110,111。
8086地址總線寬度是20,能夠表示2^20種不一樣的地址值,它的尋址能力就是2^20即1M。
數據總線決定了CPU單次數據的傳送量,也就是數據傳送的速度。8086的數據總線寬度是16,因此單次最大可以傳遞2個字節的數據。
單次數據傳送量的計算:數據總線的寬度是16,同地址線同樣,16根線表明16位0或1,即16位二進制數據,一次最多可以傳送16個二進制位。一個字節是8位,16位即2個字節。因此8086單次可以傳遞的最大數據量就是2個字節。
8088的數據總線寬度是8,8086的數據總線寬度是16,分別向內存中寫入89D8H時(89D8H即16進制的89D8,彙編語言中末尾加H代碼16進制)。一個16進製表明4個二進制位,兩個16進製表明8個二進制位即1個字節,四個16進制即2個字節。
由於8088數據線寬度是8,一次只能傳遞一個字節,因此8088傳遞89D8H須要傳2次,第一次傳D8,第二次傳89。而8086只須要一次就可以將89D8傳遞完成。
控制總線決定了CPU的控制能力,表明CPU有多少種控制能力。
1個CPU的尋址能力位8KB,那麼它的地址總線寬度爲 ( 13 )
2^10 = 1KB,2^10 * 8 = 8KB = 2^10 * 2^3 = 2^13。
複製代碼
8080、808八、8028六、80386 的地址總線寬度分別爲 16 根、20 根、24根、32根,則它們的尋址能力分別爲:( 64KB )、( 1MB )、( 16MB )、( 4GB )。
2^10 = 1KB,2^20 = 1MB,2^30 = 1GB
16根:2^16 = 2^10 * 2^6 = 64KB
20根:2^20 = 1MB
24根:2^24 = 2^20 * 2^4 = 16MB
32根:2^32 = 2*30 * 2^2 = 4GB
如今知道爲何32位的Windows最大隻能支持4G內存的把
複製代碼
8080、808八、808六、8028六、80386 的數據總線寬度分別爲 8根、8根、16根、16根、32根,則它們一次能夠傳送的數據爲:( 1B )、( 1B )、( 2B )、( 2B )、( 4B )。
8個二進制位(0000 0000):1B
16個二進制位(0000 0000 0000 0000):2B
32個二進制位(0000 0000 0000 0000 0000 0000 0000 0000):4B
複製代碼
從內存中讀取 1024 字節的數據,8086至少須要讀( 512 )次,80386至少須要讀( 256 )次。
讀取數據看數據總線的寬度:
8086:16,一次能夠讀2個字節,1024 / 2 = 512
80386:32,一次能夠讀4個字節,1024 / 4 = 256
複製代碼
全部的內存單元都有惟一的地址,這個地址叫作物理地址。
8086CPU的地址總線是20根,那麼它可以訪問的內存空間的地址值範圍即 0x00000 - 0xFFFFF(上面已經說明過,一個16進制位=4個二進制位),經過這個範圍能夠定位2^20個不一樣的內存單元,因此8006的內存空間大小爲1M。
下面是8086內存空間的示意圖:
上面提到8086的地址總線寬度爲20,尋址能力爲1M,可是實際上8086是一個16位架構的CPU,它內部可以一次性處理、傳輸、暫存的數據只有16位。這就意味這8086實際上只可以直接送出16的地址,可是它的地址總線寬度又是20位,意味這這樣就有4位是沒法使用的,它的實際尋址能力只可以是64KB。那麼它是如何作到實現1M的尋址能力呢,具體步驟以下:
段地址和偏移地址合成物理地址的計算規則:物理地址 = 段地址 * 10H + 偏移地址。
假如8086CPU須要訪問地址爲 0x136CC 的內存單元。 須要的拆分爲:段地址0x1360,偏移地址0x00CC。 物理地址 = 0x1360 * 0x10 + 0x00CC = 0x136CC
經過上面的計算能夠得出在16進制位表示下,合成段地址和偏移地址的規律:段地址 * 0x10 + 偏移地址
當段地址必定的時候,根據變化編譯地址最多可訪問的內存單元數量爲偏移地址的範圍0x0000 - 0xFFFF,即64KB。
注:段地址和偏移地址計算物理地址並非全部CPU通用的尋址方式,只是8086是比較特殊,它是一個16位架構的CPU,可是地址線寬度爲20。其它高級的CPU並無這種狀況,即它們沒有段地址,也不須要地址加法器,只須要一個偏移地址就可以訪問所有內存。
寄存器是CPU很是重要的部件,能夠經過改變寄存器的值來實現對程序的控制。不一樣CPU的寄存器個數和結構通常都不相同,下面是8086CPU寄存器的結構,8086CPU有14個寄存器,全部寄存器都是16位的。
CPU在對內存中的數據進行運算時,首先將內存中的數據存儲到寄存器中,而後再對寄存器的數據進行運算。
彙編語言沒有數據類型的概念,它是直接操做內存的,彙編語言的數據存儲單位有兩個:
好比數據4E20H,高字節是4EH(78),低字節是20H(32)。
0x4E20
0100 1110 0010 0000
|_______| |_______|
高位字節 低位字節
複製代碼
數據寄存器由AX、BX、CX、DX組成,雖然上圖裏邊每一個每個寄存器都分紅了兩塊,但它依然是一個寄存器。因爲8086以前的CPU是8位的架構,因此8086爲了兼容8位的程序,每一個16位數據寄存器均可以看成兩個單獨的8位寄存器來使用。
AX寄存器能夠分紅兩個獨立的8位寄存器,高8位爲AH,低8位爲AL,BX、CX、DX同理。除了四個數據寄存器以外,其它的寄存器均不能夠分爲兩個獨立的8位寄存器。獨立的意思是:當AH和AL作爲8位寄存器使用時,能夠看做它們是互不相關的,形式上能夠看做兩個徹底獨立的寄存器。既然數據寄存器能夠看成兩個獨立的寄存器,那麼它們的便可以用整個寄存器的16位存放一個數據,也能夠高8位和低8位分別存放一個數據共存放兩個數組。
前面關於8086的尋址方式裏邊提到,8086須要16位的段地址和偏移地址合成20位地址,其中的段地址就由段寄存器提供。段寄存器一共有四個,每一個段寄存器的做用都不相同。
CS和IP配合使用,它們指示了CPU當前要讀取指令的地址。任什麼時候候,8086CPU都會將CS:IP指向的指令作爲下一條須要取出執行的指令。
指令執行的過程:
在內存或者磁盤上中,指令和數據沒有任何區別,都是二進制信息。 CPU在工做時,有時候把信息看成指令,有時候看做數據,一樣的信息賦予不一樣的意義。
CPU根據什麼將內存中的數據信息看成指令? 經過CS:IP指向的內存單元內容看做指令。
DS是用來操做內存時提供段地址的,假如須要將內存中10000H 存入1122H,直接這樣寫是不能夠的:
mov 1000H:[0H],1122H
複製代碼
由於彙編語言又以下要求:
; 不能直接給DS賦值,須要經過寄存器中轉
mov ax, 1000H
mov ds, ax
; 不能直接給內存地址賦值,必須經過DS:[偏移地址]指向內存
; 內存中的10000H位置存入了1122H
mov [0H], 1122H
複製代碼
SS配合SP使用,SS:SP指向棧頂元素。後面棧章節中會有更詳細的介紹。
mov指令能夠修改大部分寄存器的值,好比AX、BX、CX、DX、SS、SP、DS,可是不能修改CS、IP的值,8086沒有提供這樣的功能。
; 彙編語言中的註釋用;
; 將1122H存入寄存器ax
mov ax,1122H
複製代碼
mov使用時最好和byte和word配合使用,明確操做的字節數量:
; 假設內存10000H原始值: 1122H
; 8086是小端模式,高字節放在高地址,低字節放在低地址
; 1000:0000 22
; 1000:0001 11
; 準備修改10000H位置的值
mov ax, 1000H
mov ds, ax
; 1000:0000 66
; 1000:0001 11
; 修改後10000H: 1166H
mov [0], 66h
; 1000:0000 66
; 1000:0001 11
; 修改後10000H: 1166H
mov byte ptr [0], 66h
; 1000:0000 66
; 1000:0001 00
; 修改後10000H: 0066H
mov word ptr [0], 66h
複製代碼
在高級語言中,不少狀況下都須要改變代碼的執行流程,好比if...else判斷,switch判斷等,這些改變代碼的執行流程本質上就是改變了CS、IP的指向。可是上面提到不可以直接CS、IP,8086提供了jmp指令:「 jmp 段地址:編譯地址 」 或 「 jmp 某個合法寄存器 」來完成。
; 修改CS:IP
jmp 23E4:3
; 執行後:CS=23E4H,IP=0003H
; CPU從23E43處讀取指令並送入指令緩衝區。
; 只修改IP
jmp ax
; 執行前:ax=1000H, CS=2000H, IP=0003H
; 執行後:ax=1000H, CS=2000H, IP=1000H
複製代碼
add是彙編語言中加法操做,add ax, 1111H 指令爲將寄存器ax中的值加上1111H再賦值給ax。
; ax=1122H
mov ax,1122H
; ax=2233H
add ax,1111H
複製代碼
sub是彙編語言中減法操做,sub ax,0011H 指令爲將寄存器ax中的值減去0011H再賦值給ax。
; ax=1122H
mov ax,1122H
; ax=1111H
sub ax,0011H
複製代碼
入棧,詳見後面棧章節。
出棧,詳見後面棧章節。
將0x1234存放在CPU內存中的0x4000位置,大小端的區別爲:
小端 大端
0x4000 0x34 0x12
0x4001 0x12 0x34
複製代碼
小端模式:808六、x86
大端模式:PowerPC、IBM、Sun
ARM既能夠工做在大端模式,也能夠工做在小端模式
須要運行和調試8086彙編會好的工具就是這個軟件 emu8086,這個軟件能夠很是方便和直觀編寫、調試、運行8086彙編,支持Windows平臺,軟件界面以下:
安裝完成後我先嚐試使用一下:
打開emu8086,打開後默認就有一個編輯界面,咱們嘗試在內存中10003H中寫入1234H,編寫以下指令後點擊emulate按鈕執行:
執行後會彈出一個調試窗口,點擊窗口頂部的菜單欄view-memory打開內存查看視圖:
在內存查看視圖修改默認的段地址和偏移地址,查看1000:0000的位置,能夠看到內存中1000:0003位置的值都是00H
如今觀察調試窗口的信息
左側是當前全部寄存器的值;中間藍色的是當前執行指令的位置,藍色的行數就是當前執行指令的長度;右側就是當前即將執行的指令。咱們能夠發現以下規律:
點擊single step執行mov ax, 1000H
點擊single step執行mov ds, ax
點擊single step執行mov bx, 1234H
點擊single step執行:mov [3H], bx
棧是一種具備特殊訪問方式的存儲空間(後進先出),在棧和隊列中有關於棧的數據結構和原理介紹。
; 將ax寄存器的數據入棧
push ax
; 將棧頂的數據送入ax寄存器
pop ax
; 注:8086 push和pop就是以word爲單位,沒有byte的操做,不須要指定單位
複製代碼
如今假設SS=1000H,SP=0004H,AX寄存器中存放着2266H,而且如今棧的內存空間都是存放00H。
下面就是棧的當前內存結構:
push ax 指令執行的步驟:
雖然棧頂相對內存是上移的,可是存入兩個字節時,仍是要從棧頂往高拿兩個字節的內存存放元素。
如上存入2266H,棧頂上移兩位後爲:10002H,那麼須要用10002H和10003H存放2266H。8086是小端模式,高字節22H放在高地址10003H,低字節66H放在低地址10002H。
接着上面的棧的狀態,咱們如今執行指令 pop bx。
注:先從棧頂指針指向的內存位置取兩個字節的數據,依然是往高取兩個字節:10002H和10003H。按照高字節高地址、第字節低地址的規則,10002H和10003H的存儲的值是2266H,將2266H放入bx。
注:觀察上圖第二步後棧的狀態,10002H的值依然是66H,10003H的值依然是22H,即pop操做後,內存中的值是不會清0的,它們還保持着原來的值。假以下次再進行將3399H入棧是,那麼33H就會覆蓋22H,99H就是覆蓋66H。
注意觀察上圖push操做的SS:SP位置,當棧是空的時候,SS:SP指向的是10004H的位置,push2266H後,最高存放22H的內存是10003H。
假如繼續將3399H入棧,那麼棧頂指針相對於棧空間的位置關係以下:
彙編語言中棧是不會自動判斷棧是否越界的,那麼就可能出現以下圖push和pop越界問題:
不管是push仍是pop越界都是很是危險的,由於棧外部的內存中可能存放其它任意數據,多是代碼、重要數據等,將它們覆蓋或者拿出來使用均可能發生不可預知的嚴重錯誤。
將10000H-1000FH看成棧空間,初始狀態棧是空的,假設AX=001A,BX=001BH,利用棧將AX、BX值交換。
mov ax, 1000H
mov ss, ax
mov sp, 0010H
mov ax, 001AH
mov bx, 001BH
push ax
push bx
pop ax
pop bx
複製代碼
下面的代碼包含彙編語言的基本指令:
; 將代碼段寄存器和咱們的代碼段關聯起來
assume cs:code
; 代碼段開始
code segment
mov ax, 1122h
mov bx, 3344h
add ax, bx
; 正常推出程序 至關於 return 0
mov ah, 4ch
int 21h
; 代碼段結束
code ends
; 程序的結束
end
複製代碼
彙編語言指令分爲2類:
上面的segment和ends的做用是定義一個段,segment表明段的開始,ends表明段的結束:
段名 segment
:
段名 ends
複製代碼
一個有意義的彙編程序中,至少要有一個段作爲代碼段存放代碼。
assume 的做用是將代碼段和mycode段和CPU中的CS寄存器關聯起來。
end 代碼程序的結束,編譯器遇到end就會結束編譯。
下面的代碼表明退出程序,int不是整形的意思,是interrupt的簡寫,表明中斷:
; 只要ah是4ch就能夠結束
; al是返回碼,相似於return 0的0,mov ax, 4c00h
mov ah, 4ch
int 21h
複製代碼
中斷是因爲軟件或硬件的信號,使CPU暫停執行當前的任務,轉而去執行另外一段子程序。
能夠經過 「 int 中斷碼 」 實現中斷,內存中有一張中斷向量表,用來存放中斷碼處理中斷程序的入口地址。CPU在接受到中斷信號後,暫停當前正在執行的程序,跳轉到中斷碼對應的向量表地址處去執行中斷。
常見中斷:
下面是int21對應AH寄存器部分功能對照表
AH | 功能 | 調用參數 | 返回參數 |
---|---|---|---|
09 | 顯示字符串 | DS:DX=串地址 | (DS:DX+1)=實際輸入的字符數 |
4c | 帶返回碼結束 | AL=返回碼 | 無返回參數 |
彙編語言中可使用db定義數據:
; 定義100個
// 定義一個字節的00H
db 0h
// 定義一個字的數據0000H
dw 0h
複製代碼
在數據段定義數據至關於建立全局變量
在棧段定義數據至關於指定棧的容量
在代碼段定義數據通常不會這樣使用
可使用dup批量的去聲明數據:
dw 3 dup(1234H)
複製代碼
聲明3個1234H:
代碼段用存放咱們須要執行的代碼。
數據段的指令用於建立數據,數據段的數據在程序開始運行的時候就已經建立好了,至關於全局變量。
棧段就是用來函數執行須要使用的臨時空間,通常用於存放臨時變量、函數返回後下一條指令的偏移地址。
建立一個包含完整的數據段、代碼段、棧段的彙編程序:
assume cs:code, ds:data, ss:stack
stack segment
; 自定義棧段容量
db 100 dup(0)
stack ends
data segment
db 100 dup(0)
data ends
code segment
start:
mov ax, stack
mov ss, ax
mov ax, data
mov ds, ax
mov ax, 1122h
push ax
pop bx
mov ax, 4c00h
int 21h
code ends
end start
複製代碼
上面咱們定義的棧段的容量是100,能夠看到程序運行後,SP=64H=100。
作爲iOS程序員若是瞭解過iOS內存管理的話必定知道下面的iOS內存佈局:
低地址
| 保留段
|
| 代碼段(__TEXT)
|
| 數據段(__DATA)
| 字符串常量
| 已初始化數據:已初始化的全景變量、靜態變量等
| 未初始化數據:未初始化的全局變量、靜態變量等
|
| 堆(heap)⬇️ 地址愈來愈高
|
| 棧(stack)⬆️地址愈來愈低
|
| 內核區
高地址
複製代碼
首先看一下是否是和咱們剛剛實現的彙編很類似,有代碼段和和數據段。
其中棧地址愈來愈低是否是和咱們剛剛彙編分析的棧push同樣,push的時候SP=SP-2,棧頂指針上移,棧頂指針變小,地址變低。
全局變量放在數據段中,即程序運行時就將數據放在數據段中跟剛剛彙編代碼中在數據段建立數據是同樣的,還有這也解釋了爲何全局變量的地址在編譯就已經肯定好不會再次改變,咱們彙編中在數據段建立的數據地址也是肯定而且不會變化的。
有了上面的基礎指令和分段以後,終於能夠實現經典程序HelloWorld的輸出了:
; 將代碼段寄存器和咱們的代碼段關聯起來
; 將數據段寄存器和咱們的數據段關聯起來
; 注:這裏的關聯並無任何實際操做,至關於給咱們本身的註釋而已
; 至關於即便不寫這一行也沒有關係
assume cs:code, ds:data
; 數據段開始
data segment
; 建立字符串
; 彙編打印字符串要在尾部用 $ 標記字符串的結束位置
; 將字符串用hello作一個標記,方便後面使用它
hello db 'Hello World, Whip!$'
; 數據段結束
data ends
; 代碼段開始
code segment
; 指令執行的起始,相似於C語言的main函數入口
start:
; 彙編語言不會自動把數據段寄存器指向咱們程序的數據段
; 將數據段寄存器指向咱們本身程序的數據段
mov ax, data
mov ds, ax
; 打印字符串的參數
; DS:DX=串地址,將字符串的偏移地址傳入dx寄存器
; 字符串是在數據段起始建立的,它的偏移地址是0h
; offset hello 即找到標記爲hello的數據段字符串的編譯地址
; 還能夠寫成 mov dx, 0h
mov dx, offset hello
; 打印字符串,ah=9h表明打印
mov ah, 9h
int 21h
; 正常退出程序,至關於高級語言的 return 0
mov ah, 4ch
int 21h
; 代碼段結束
code ends
; 程序的結束
end start
複製代碼
運行程序會顯示打印的窗口:
使用call和ret配合能夠調用和返回一段其它位置的指令,至關於面嚮對象語言的中的函數調用:
assume cs:code, ds:data
data segment
hello db 'Hello World, Whip!$'
data ends
code segment
start:
mov ax, data
mov ds, ax
call print
mov ah, 4ch
int 21h
print:
mov dx, offset hello
mov ah, 9h
int 21h
ret
code ends
end start
複製代碼
上面的彙編指令看起來很簡單,call調用,調用的指令完成ret返回,而後在執行call後面的指令。可是,它內部是如何實現的呢?ret以後,是如何知道繼續調用哪一條指令呢?下面就從調試工具來看看究竟是如何實現的,首先指令的調用是根據CS:IP的指向來決定的,咱們要關注CS、IP寄存器的變化,以及各個指令的內存地址,另外這裏既然涉及到相似復原的操做,首先想到的就是查看棧裏邊是否有變化。
首先先知道到即將調用call print的位置,能夠發現
下面執行call 0000Ch
下面一直執行到ret
經過上面的分析能夠知道call和ret的做用
將下一條指令的偏移地址入棧
執行函數
將棧頂的值出棧,賦值給IP
上面咱們經過call print實現了打印hello world,這裏咱們換成另外一種方式,讓call print返回須要打印的字符串的偏移地址,ret後打印出來。首先就是考慮如何將字符串的編譯地址返回出來。
下面就用數據段實現:
assume cs:code, ds:data
data segment
db 100 dup(0)
hello db 'Hello World, Whip!$'
data ends
code segment
start:
mov ax, data
mov ds, ax
call print
mov dx, [0]
mov ah, 9h
int 21h
mov ah, 4ch
int 21h
print:
mov [0], offset hello
ret
code ends
end start
複製代碼
assume cs:code, ds:data
data segment
db 100 dup(0)
hello db 'Hello World, Whip!$'
data ends
code segment
start:
mov ax, data
mov ds, ax
call print
mov dx, ax
mov ah, 9h
int 21h
mov ah, 4ch
int 21h
print:
mov ax, offset hello
ret
code ends
end start
複製代碼
先執行函數sum後,將eax寄存器中的值存入int c中,打印c的值,若是c=1+2=3,那麼就證實函數返回結構存放在eax中。
注:eax至關於8086的ax。
#include "stdafx.h"
int sum(int a, int b) {
return a + b;
}
int main(int argc, char* argv[])
{
sum(1, 2);
int c = 0;
__asm {
mov c, eax
}
printf("%d", c);
getchar();
return 0;
}
複製代碼
輸出結果:
高級語言的函數幾乎都是由 返回值-參數-函數名 構成的,好比:
int add(int a, int b) {
return a + b;
}
複製代碼
咱們以前已經在彙編函數實現了帶返回值的調用,這裏實現完整的帶參數-返回值的調用,來實現一個加法的功能。
彙編想要傳遞數據和上面實現返回值的思路是同樣的,能夠用不少種方式來考慮,好比使用數據段、使用棧、使用寄存器。
數據段通常不要用來作這種參數的傳值,由於參數數據是臨時,應該使用完成就釋放掉,不該該存到數據段中。
在iOS中,編譯器默認是優先使用寄存器傳值的,當寄存器不夠用時纔會用棧,這裏咱們先用寄存器實現一個加法:
assume cs:code, ds:data,
data segment
db 20H dup(0)
data ends
code segment
start:
mov ax, data
mov ds, ax
mov cx, 1111h,
mov dx, 2222h,
call sum1
mov ax, 4c00h
int 21h
sum1:
mov ax, cx
add ax, dx
ret
code ends
end start
複製代碼
先講1111h、2222h存入寄存器cx、dx中,再調用sum1將cx、dx相加返回到ax,上面已經提到在彙編中將值存入ax即返回值。
下面咱們再用棧來傳遞參數,假如調用call sum以前,先將1111h、2222h入棧,那麼在調用call的時候,又會將call的下一條指令的偏移地址入棧,那麼此時棧頂實際上是call的下一條指令的偏移地址。如何在sum中直接pop的話,會將call的下一條指令的偏移地址出棧,那麼ret後就沒法繼續執行call以後的代碼了。因此在sum中不可以進行出棧操做,而是要直接訪問棧內的元素。那麼可能會有疑問,棧不是隻能訪問棧頂嗎?彙編中不是的,彙編沒有編譯器語法和API的限制,只要是內存,咱們都可以訪問。
assume cs:code, ds:data, ss:stack
stack segment
db 20 dup(0)
stack ends
data segment
db 20 dup(0)
data ends
code segment
start:
mov ax, stack
mov ss, ax
mov ax, data
mov ds, ax
push 1111h
push 2222h
call sum
add sp, 4
mov ax, 4c00h
int 21h
sum:
mov bp, sp
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
ret
code ends
end start
複製代碼
代碼解析:
sum:
mov bp, sp
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
ret
複製代碼
push 1111h
push 2222h
call sum
add sp, 4
複製代碼
經過上面的操做,就能夠明白爲何說高級語言中方法的參數是臨時變量,由於仍是函數的操做都是在棧中,方法結束後棧又恢復原來棧頂位置。這個恢復棧的操做就是棧平衡。
回收內存空間的誤區
棧只是恢復了棧頂的位置,原來存入棧的值並無恢復成00,所謂的內存的回收並非將內存從新清零,只是再也不佔用這塊內存,之後須要使用內存的時候能夠用新的值覆蓋這塊內存。若是之前面對高級語言中的內存回收,認爲是將內存清空是不對的。
遞歸調用的問題
經過上面的彙編能夠知道,方法的調用須要將參數和下一條彙編指令的偏移地址入棧,在方法結束纔會對棧頂進行恢復。當遞歸調用出現時,每個方法都會在其內部進行調用另外一個方法,至關於在當前彙編方法的ret以前又調用call,這樣棧就會無限的push而不會pop,當棧溢出後,就會發生錯誤致使崩潰。
另外即使不是無限的遞歸,若是函數之間的調用層級深到必定程度,使得棧空間溢出的話,仍然會形成嚴重的錯誤乃至崩潰。並且函數之間的調用也會額外的佔用棧空間(內存),這也就是爲何高級語言大多數狀況下若是可以用循環解決問題的話,都儘可能不用遞歸的緣由之一。
彙編代碼至關於:
assume cs:code, ds:data, ss:stack
stack segment
db 20H dup(0)
stack ends
data segment
db 20H dup(0)
data ends
code segment
start:
mov ax, stack
mov ss, ax
mov ax, data
mov ds, ax
push 1111h
push 2222h
call sum
add sp, 4
mov ax, 4c00h
int 21h
sum:
mov bp, sp
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
push 1111h
push 2222h
call sum
add sp, 4
ret
code ends
end start
複製代碼
以下圖,遞歸調用棧內無限的push
咱們剛剛使用的棧平衡的方法就是外平棧,在函數調用後面對棧進行平衡。
push 1111h
push 2222h
call sum
add sp, 4
複製代碼
還有一種棧平衡的方法,在函數的內部進行棧平衡操做:
push 1111h
push 2222h
call sum
mov ax, 4c00h
int 21h
sum:
mov bp, sp
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
ret 4
複製代碼
C++的函數在調用時,能夠指定本身對應的彙編代碼使用哪一種方法傳遞參數和使用哪一種棧平衡方式:
int __cdecl sum(int a, int b) {
return a + b;
}
複製代碼
注:iOS開發中,在Xcode裏邊設置是無效的,規定就是使用第三種方式,並且會使用更多的寄存器傳遞參數,基本知足開發使用的函數所有使用寄存器傳參。
咱們還一直沒有在彙編函數中建立局部變量,對應的高級語言以下函數:
int sum(int a, int b) {
int c = 3;
int d = 4;
int e = c + d
return a + b + e;
}
int mian() {
sum(1, 2);
return 0;
}
複製代碼
根據局部變量的特性:只可以在函數內部訪問而且函數結束後要回收內存,這樣仍是使用棧來實現局部變量。下面就來用匯編實現上面的邏輯:
assume cs:code, ds:data, ss:stack
stack segment
db 100 dup(0)
stack ends
data segment
db 100 dup(0)
data ends
code segment
start:
; 將ds、ss指向程序的數據段和棧段
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
; 傳入參數
push 1h
push 2h
; 調用方法
; 至關於 sum(1, 2);
call sum
; 程序結束
mov ax, 4c00h
int 21h
sum:
; 當前sp的值複製給bp,用於從棧中取值
mov bp, sp
; 將sp=sp - 10,擴容棧,bp指向棧擴容前棧頂
; 用於函數存放函數中建立的臨時變量
sub sp, 10
; 在棧中存入臨時變量
; 在棧擴容前的棧頂位置加入,即bp-2的位置
; 對應 int c = 3;
mov word ptr ss:[bp-2], 3h
; 在棧中存入另外一個臨時變量
; 對應 int d = 4;
mov word ptr ss:[bp-4], 4h
; 將兩個臨時變量相加並壓入棧中
; 至關於 e = c + d;
mov ax, ss:[bp-2]
add ax, ss:[bp-4]
mov ss:[bp-6], ax
; 將函數的兩個參數、兩個臨時變量的和的臨時變量相加,並返回結果
; return a + b + e;
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
add ax, ss:[bp-6]
; 函數開始將sp上移了,而bp記錄着上移前的位置
; 由於ret的時候,須要從棧頂獲取下一條指令的偏移地址,如今棧頂ss:sp指向的是擴容後的位置
; 須要將sp恢復到上移以前的位置
mov sp, bp
; 將棧頂pop給IP,繼續執行call sum 的下一條指令
; 棧平衡(內平棧)
ret 4
code ends
end start
複製代碼
下面分析一下流程:
在調用push 01h以前,還有沒有傳遞參數,如今棧是空的,sp=64H,棧定爲:0710:0064。
執行call sum後,call下一條指令的偏移地址會入棧,當前IP=000E,call指令的長度爲3,那麼下一條指令的偏移地址爲000E + 3 = 11H。
能夠看到,執行call sum指令後,0011H入棧。當前SP=005E,BP=0000E。
接下來將SP=SP-10,能夠看到棧容量擴充了10byte,BP指向SP以前的值,即棧頂原來的位置。
將兩個臨時變量三、4入棧,三、4就在運來棧頂的位置繼續壓入,中間沒有空餘的內存浪費。
要將輸入存入棧最後一個可用位置,加到SS:[BP-2]的位置正好是SP下移以前的位置。第二次臨時變量繼續在原來BP-2的基礎再減2,就是SS:[BP-4],同理,當須要繼續加入臨時變量,就是SS:[BP-6]的位置。
將3和4的和7入棧,由於它對應的也是一個臨時變量 e ,須要入棧保存。
將兩個參數和兩個臨時變量的和相加,SS:[BP]指向的位置是0710:005E,以前兩個參數0001H、0002H存放的位置是:SS:[BP+2]、SS:[BP+4]的位置,臨時變量的和存放在SS:[BP-6]的位置。能夠發現一個規律,參數同BP+X獲取,臨時變量經過BP-X獲取。
將相加結果存入ax寄存器中,ax=0001H+0002H+0003H+0004H=000A,結果複合預期。
ret以前須要現將以前爲了存放臨時變量將SP=SP-10恢復,這裏只須要將BP的值給SP就能夠了,能夠看到臨時變量0003H、0004H、0007H都釋放了。
執行ret 4,將棧頂0011E給IP,CS:IP指向call sum的下一條指令繼續執行接下來的彙編代碼,而後內平棧,能夠發現棧頂恢復到函數函數以前的位置0710:0064,函數調用佔用的全部棧空間所有回收。
上面的代碼看上去好像沒有任何問題了,可是實際上還不夠,只是單個函數調用的時候看上去能夠的。下面假設以下的代碼:
int minus(int a, int b) {
int c = 1;
int d = 2;
int e = a - b;
return e + c - d;
}
int sum(int a, int b) {
int c = 3;
int d = 4;
// 0 + 1 + 2 + 3 + 4 = 10
int e = minus(8, 7);
// 0 +
return e + a + b + c + d;
}
int mian() {
// 000A;
sum(1, 2);
return 0;
}
複製代碼
按照以前單個函數調用的方式,在添加一個minus方法,並在sum函數內部調用它:
assume cs:code, ds:data, ss:stack
stack segment
db 100 dup(0)
stack ends
data segment
db 100 dup(0)
data ends
code segment
start:
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
push 2h
push 1h
call sum
mov ax, 4c00h
int 21h
sum:
mov bp, sp
sub sp, 10
mov word ptr ss:[bp-2], 3h
mov word ptr ss:[bp-4], 4h
push 7h
push 8h
call minus
mov word ptr ss:[bp-6], ax
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
add ax, ss:[bp-2]
add ax, ss:[bp-4]
mov sp, bp
ret 4
minus:
mov bp, sp
sub sp, 10
mov word ptr ss:[bp-2], 1h
mov word ptr ss:[bp-4], 2h
mov ax, ss:[bp+2]
sub ax, ss:[bp+4]
mov ss:[bp-6], ax
mov ax, ss:[bp-6]
add ax, ss:[bp-2]
sub ax, ss:[bp-4]
mov sp, bp
ret 4
code ends
end start
複製代碼
運行起來咱們會發現彙編指令無限的進行一個循環操做,根本沒法正常結束。如今來分析一下緣由:
調用call minus這裏都是咱們前面都已經理解的流程,下面開始是關鍵之處。
call minus以前首先將0007H和0008H入棧,如今SP由0054變成了0050,注意當前BP的值是sum棧擴容前SP的值,BP是關鍵,要特別關注。
這裏再額外注意一下call minus 的下一條指令和它的指令地址:
; 071E:0034
mov word ptr ss:[bp-6], ax
複製代碼
當執行call minus後,棧中又壓入call minus後面指令的偏移地址,上面能夠知道call minus指令的偏移地址是0031,長度是3,因此0031+3=0034H,入棧的值也恰好是0034H。如今BP的值依然是005E。
接下來要執行的代碼就是 mov bp, sp 。這個方法以前單獨執行sum方法的時候已經使用過,是爲了可以訪問當前方法使用的棧的元素,並且也是爲了釋放當前棧的局部變量空間。
如今BP已經從以前的005E指向了當前SP的值004E了,並且當前SS:BP存儲的值是0034,是sum函數中 call minus後面一條指令的偏移地址。
當minus執行完計算結果到 mov sp, bp這條指令時;ax=8-7+1-2=0H,結果複合預期。當前SP指向0044,BP指向004E,下面執行mov sp, bp,將bp的值送給sp。
接下來minus執行ret,將0034送給IP,並將恢復棧平衡。
雖然如今會直接執行call minus後面的代碼:
; 071E:0034
mov word ptr ss:[bp-6], ax
複製代碼
這裏已經發現了問題:BP仍是指向004E,這就致使sum內部在調用minus函數以後,經過bp取的值都是錯誤,並且sum在結算完結果後,恢復sp的時候,使用的bp也是錯誤的,它會將sp設置爲0034。
而ret指令又將ip設置爲ss:sp的值,即ip變成了0034,這就致使ret以後執行的不是call sum以後的指令,而是071E:0034地址對應的指令。
這個指令咱們以前已經特別記錄了,就是sum函數中調用call minus後面的指令,因此sum在調用call minus後,會在 mov word ptr ss:[bp-6], ax 和 ret 4 之間無限的循環執行。
既然問題出如今BP寄存器,咱們就須要想辦法解決彙編函數之間調用,BP寄存器的狀態恢復問題。
上面已經分析了函數之間調用是因爲BP的問題,這裏就來解決這個問題,依然是使用棧來對BP的原始值進行存儲,在須要恢復的BP的時候進行恢復,只須要添加和修改幾處代碼:
assume cs:code, ds:data, ss:stack
stack segment
db 100 dup(0)
stack ends
data segment
db 100 dup(0)
data ends
code segment
start:
mov ax, data
mov ds, ax
mov ax, stack
mov ss, ax
; 記得前面的函數調用約定吧,參數從右到左入棧
push 2h
push 1h
call sum
mov ax, 4c00h
int 21h
sum:
; 先將bp的值入棧存儲,函數結束將bp恢復
; 由於這裏有了一次push操做,因此當前棧頂相比以前多了bp的值
; 如今棧頂數據結構爲:
; -- bp值
; -- 函數調用完要執行的指令的偏移地址
; -- 函數的參數
push bp
; 將sp的值賦值給bp
; 由於棧相比以前多push了bp的原始值
; 因此此時bp的地址相比以前要小2
mov bp, sp
sub sp, 10
mov word ptr ss:[bp-2], 3h
mov word ptr ss:[bp-4], 4h
push 7h
push 8h
call minus
mov word ptr ss:[bp-6], ax
; 由於bp比以前小2,因此經過bp取參數須要在原來的基礎上+2
mov ax, ss:[bp+4]
add ax, ss:[bp+6]
add ax, ss:[bp-2]
add ax, ss:[bp-4]
; 將bp賦值給sp以後,這裏的sp相比以前+2
; 棧頂存放值bp以前的值
mov sp, bp
; 將bp原來的值從新送給bp
; 如今棧頂從新指向了下一條指令的偏移地址
pop bp
ret 4
minus:
push bp
mov bp, sp
sub sp, 10
mov word ptr ss:[bp-2], 1h
mov word ptr ss:[bp-4], 2h
mov ax, ss:[bp+4]
sub ax, ss:[bp+6]
mov ss:[bp-6], ax
mov ax, ss:[bp-6]
add ax, ss:[bp-2]
sub ax, ss:[bp-4]
mov sp, bp
pop bp
ret 4
code ends
end start
複製代碼
觀察ax寄存器中,已經獲得正確的結果000AH,BP按照上圖的流程正確的恢復爲0000E,棧空間也正確的回收。
經過上面的函數調用應該已經能夠發現彙編的規律:
將上面的C++代碼運行並查看其對應的彙編代碼:
13: int sum(int a, int b) {
// 對應咱們彙編代碼的 push bp
00401080 push ebp
// 將sp值賦給bp
00401081 mov ebp,esp
// 棧頂下移擴容棧空間,用於存放臨時變量
00401083 sub esp,4Ch
00401086 push ebx
00401087 push esi
00401088 push edi
// 填充棧空間
00401089 lea edi,[ebp-4Ch]
0040108C mov ecx,13h
00401091 mov eax,0CCCCCCCCh
00401096 rep stos dword ptr [edi]
// 局部變量 經過 bp-x 賦值
14: int c = 3;
00401098 mov dword ptr [ebp-4],3
15: int d = 4;
0040109F mov dword ptr [ebp-8],4
16: // 0 + 1 + 2 + 3 + 4 = 10
17: int e = minus(8, 7);
// 調用函數前經過棧push參數
// 從右到左入棧
004010A6 push 7
004010A8 push 8
004010AA call @ILT+10(minus) (0040100f)
004010AF add esp,8
004010B2 mov dword ptr [ebp-0Ch],eax
18: // 0 +
19: return e + a + b + c + d;
004010B5 mov eax,dword ptr [ebp-0Ch]
004010B8 add eax,dword ptr [ebp+8]
004010BB add eax,dword ptr [ebp+0Ch]
004010BE add eax,dword ptr [ebp-4]
004010C1 add eax,dword ptr [ebp-8]
20: }
004010C4 pop edi
004010C5 pop esi
004010C6 pop ebx
004010C7 add esp,4Ch
004010CA cmp ebp,esp
004010CC call __chkesp (00401140)
// 將bp值給sp
004010D1 mov esp,ebp
// 恢復bp
004010D3 pop ebp
004010D4 ret
複製代碼
雖然在函數內部穿插着不少如今還看不懂的代碼,可是仍是可以從中發現,函數調用的整體流程是一致的。下面就來一一去解釋和上面比咱們多出來的代碼指令。
這段代碼是在保護和恢復bp寄存器的基礎上,對以下寄存器也進行了保護和恢復:
// 棧頂下移擴容棧空間,用於存放臨時變量
00401083 sub esp,4Ch
00401086 push ebx
00401087 push esi
00401088 push edi
004010C4 pop edi
004010C5 pop esi
004010C6 pop ebx
複製代碼
下面這段代碼是將棧空內用於存放臨時變量的空間,所有用CC填充,當用程序異常,IP指向了臨時變量的值,而且這裏的值是CC的話,就會中斷,程序停在這裏,是一種安全機制。
00401089 lea edi,[ebp-4Ch]
0040108C mov ecx,13h
00401091 mov eax,0CCCCCCCCh
00401096 rep stos dword ptr [edi]
複製代碼
咱們也把其餘寄存器的保護代碼加上,實現一個完整的彙編程序:
; 代碼段、數據段、棧段聲明
assume cs:code, ds:data, ss:stack
; 棧段
stack segment
; 定義棧容量
db 100 dup(0)
stack ends
; 數據段
data segment
db 100 dup(0)
data ends
; 代碼段
code segment
; 程序入口
start:
; 數據段關聯
mov ax, data
mov ds, ax
; 棧段關聯
mov ax, stack
mov ss, ax
; 記得前面的函數調用約定吧,參數從右到左入棧
; 參數入棧
push 2h
push 1h
; 調用函數
call sum
; 程序正常退出
mov ax, 4c00h
int 21h
sum:
; 先將bp的值入棧存儲,函數結束將bp恢復
; 由於這裏有了一次push操做,因此當前棧頂相比以前多了bp的值
; 如今棧頂數據結構爲:
; -- bp值
; -- 函數調用完要執行的指令的偏移地址
; -- 函數的參數
push bp
; 將sp的值賦值給bp
; 由於棧相比以前多push了bp的原始值
; 因此此時bp的地址相比以前要小2
mov bp, sp
sub sp, 10
; 在後面保護寄存器
; 由於這樣方便bp訪問局部變量和參數,讓bp在局部變量和參數之間
push bx
push si
push di
; 函數執行邏輯
mov word ptr ss:[bp-2], 3h
mov word ptr ss:[bp-4], 4h
; 傳入參數
push 7h
push 8h
; 調用函數
call minus
; 函數執行邏輯
mov word ptr ss:[bp-6], ax
; 由於bp比以前小2,因此經過bp取參數須要在原來的基礎上+2
mov ax, ss:[bp+4]
add ax, ss:[bp+6]
add ax, ss:[bp-2]
add ax, ss:[bp-4]
; 恢復寄存器
pop di
pop si
pop bx
; 將bp賦值給sp以後,這裏的sp相比以前+2
; 棧頂存放值bp以前的值
mov sp, bp
; 將bp原來的值從新送給bp
; 如今棧頂從新指向了下一條指令的偏移地址
pop bp
; 結束函數並恢復棧平衡
ret 4
minus:
push bp
mov bp, sp
sub sp, 10
push bx
push si
push di
mov word ptr ss:[bp-2], 1h
mov word ptr ss:[bp-4], 2h
mov ax, ss:[bp+4]
sub ax, ss:[bp+6]
mov ss:[bp-6], ax
mov ax, ss:[bp-6]
add ax, ss:[bp-2]
sub ax, ss:[bp-4]
pop di
pop si
pop bx
mov sp, bp
pop bp
ret 4
code ends
end start
複製代碼
完整的彙編函數調用流程:
1,push 參數
傳遞參數給函數
2,push 函數下一條指令的偏移地址
用於函數執行完成後,可以正確執行後面的指令
3,push bp
保存bp以前的值用於恢復bp
4,mov bp, sp
保留sp以前的值,用於恢復sp;用於訪問棧空間的數據
5,sub sp x
分配棧空間給函數用於存儲局部變量,x爲自定義的大小
6,push bx\si\di
保護須要保護的寄存器
7,執行函數代碼
8,pop di\si\bx
恢復寄存器
9,mov sp,bp
恢復sp
10,pop bp
恢復bp
11,ret (x)
函數返回,棧平衡(內平棧或外平棧)
複製代碼
經過8086彙編瞭解彙編的基礎以後,就能夠很容易的理解和讀懂AT&T彙編了,這是iOS虛擬機中使用的彙編。