0 寫在前面
為了更深入的了解程序的實現原理,近期我學習了IBM-PC相關原理,並手工編寫了一些x86匯編程序。
在2017年的計算機組成原理中,曾對MIPS體系結構及其匯編語言有過一定的了解,考慮到x86體系結構在目前的廣泛應用,我通過兩個月左右的時間對x86的相關內容進行了學習。
在《x86匯編語言實踐》系列中(包括本篇、x86匯編語言實踐(1)、x86匯編語言實踐(2)、x86匯編語言實踐(3)以及x86匯編語言實踐(4)),我通過幾個具體案例對x86匯編語言進行實踐操作,並記錄了自己再編寫匯編代碼中遇到的困難和心得體會,與各位學習x86匯編的朋友共同分享。
我將我編寫的一些匯編代碼放到了github上,感興趣的朋友可以點擊屏幕左上角的小貓咪進入我的github,或請點擊這里下載源代碼。
這是《x86匯編語言實踐》系列的最后一篇文章,明天就要迎來x86匯編的期末考試了,希望所有朋友們以及先先能夠考試順利!
1 基礎知識
1.Intel 8086/8088PC機的CPU字長為16位,16位的信息稱為1個字,內存的基本單元為1個字節,但任何相鄰兩個單元都可以組成1個字。Intel 8086/8088PC機共有20根地址線,其尋址范圍為00000H~FFFFFH。
2.用於間接尋址的寄存器有BX,SP,BP,SI,DI,其中,BX一般用於存放基址;在采用基址變址尋址時,采用SI或BX或DI寄存器,基址尋址默認的段是DS段(DS:[SI]);采用BP或SP寄存器,基址尋址默認的段是SS段(堆棧的位置和大小是由SP和SS共同決定的)。
3.串操作指令如MOVSB,STOSB,LODSB,SCASB,CMPSB,MOVSW,LODSW,STOSW,SCASW,CMPSW等,源操作數對應的地址是DS:[SI],目的操作數對應的地址是ES:[DI]。
4.Intel8086/8088CPU共有9個1為的標志寄存器(標志位),為了便於CPU的加工,他們被組合在一起形成一個16位的程序狀態字寄存器PSW中。幾個比較重要的標志位有:
- ZF:當運算結果為0時,ZF=1,否則ZF=0
- SF:運算結果為負時,SF=1,否則SF=0
- CF:算術運算最高位產生進位,CF=1.否則CF=0;還用於移位指令保存最高位左移或最低位右移移出的代碼。
- DF:DF=1時每次串操作SI和DI減1,DF=0時每次串操作SI和DI加1。使用CLD可以將DF清零,即規定為正向操作字符串。
- TF:TF=1時執行完一條產生單步中斷,中斷處理程序將TF置0。TF標志用於調試。
- PF,AF,IF,OF這里我斗膽預測一啵,不考(因為真的沒有使用過)。
5.STD是將DF置1的指令,與CLD將DF清零的效果相反。使用STD后,串指令對應的DI,SI寄存器每次操作后根據是SB還是SW操作自動減少1或2
6.邏輯地址向物理地址的轉化(書上P24)。地址轉化的動機:20位物理地址無法直接在16位字長的機器中直接運算,因此可以采用Intel的分段方法將其划分為16位段地址和16位段內地址(也稱為偏移地址)邏輯地址的基本形式為0000H:0000H,該邏輯地址表示物理地址的00000H。
那么邏輯地址向物理地址的轉換方式可以表述為以下公式:段地址x10H + 偏移地址 = 物理地址
舉例說明:邏輯地址1234H:5678H轉換為物理地址為:1234Hx10H + 5678H = 12340H + 5678H = 179B8H。再如:1234H:2001H = 12340H + 2001H = 14341H
7.幾個重要的數據傳送指令PUSH,POP,PUSHF,POPF
- PUSH SRC:先SP = SP - 2 再 SS:[SP] <- SRC
- PUSHF :先SP = SP - 2 再 SS:[SP] <- PSW
- POP SRC:先SRC <- SS:[SP] 再 SP = SP + 2
- POPF :先PSW <- SS:[SP] 再SP = SP + 2
總結而言,PUSH和POP是將操作數壓(彈)棧,POPF和PUSHF是將PSW標志寄存器壓(彈)棧
2 尋址方式
2-1 六種與數據有關的尋址方式
2-1-1 立即尋址
直接將立即數寫到指令中的尋址方式。注意不得超出寄存器的字節范圍:AL8位,AX16位。
【例】
- AND AX,0FFFEH
- MOV AL,100H
- MOV AL,00000101B
- MOV AX,512
【不能使用】
- MOV AL,100H
- MOV AX,10000H (超出了字節范圍)
2-1-2 寄存器尋址
使用寄存器的尋址方式。可以顯示使用,也可以隱式使用。也可以使用段寄存器CS,DS,SS,ES。
【例】
- MOV DS,AX
- PUSH DS
- PUSHF (隱式操作PSW)
- STD (隱式操作PSW)
- CMC (對CF取反操作,隱式操作PSW)
2-1-3 直接尋址
直接使用操作數的偏移地址進行尋址的方式,偏移地址用[立即數]的形式表示,或者直接用數據段中定義的變量名表示,或用數據段中定義的變量名+立即數的形式表示。
【例】
- AND AX,[0FFFEH]
- MOV AX,X ;其中X為數據段中定義好的數據
- MOV AX,STR+1 ;其中STR為數據段中定義好的數據,STR+1直接尋址到STR下一個字節單元的內容
2-1-4 寄存器間接尋址
使用寄存器中存儲的偏移地址進行尋址。注意只能使用尋址寄存器BX,BP,SI,DI進行尋址,而不能用DX等進行尋址。此外,尋址的地址必須為16位,即不能使用BL等進行尋址。
【例】
- MOV AX.[BX]
- MOV BH,[BP]
- MOV CX,[SI]
- MOV DL,[DI]
以上四條指令等價於
- MOV AX.DS:[BX]
- MOV BH,SS:[BP]
- MOV CX,DS:[SI]
- MOV DL,ES:[DI]
但是在每條指令前加上一個段超越的段名,既麻煩又沒必要,因此通常都默認缺省為上述隱含段規則。
【不能使用】
- MOV AX,[DX] (不能用DX)
- MOV DL,[BL] (必須為16位尋址)
2-1-5 寄存器相對尋址
在寄存器間接尋址的基礎上,再增加一個常偏移量。形式多變,大致有如下幾種
【例】
- MOV AX,[BX+100]
- MOV AX,[SI+10H] <==> MOV AX,10H[SI]
- MOV AX,ARRAY[SI]
- MOV TABLE[DI],AL
- MOV TABLE[DI+1],AL 3~5展示了立即數也可以是數據段中定義好的變量名
最終在debug下所有的尋址有效地址會被計算成[DI+XXXX]的形式,XXXX是一個十六進制數。
2-1-6 基址變址尋址
即基址加變址尋址方式,基址采用BX,BP尋址,變址采用DI,SI尋址,尋址規則相對固定。
【例】
- MOV AX,[BX][SI]
- MOV AX,[BX+SI]
- MOV ES:[BX+SI],AL
- MOV [BP+DI],AX
- MOV AX,[BX+SI+200]
- MOV ARRAY[BP+SI],AX
其中,段取決於基址寄存器,如BX的段就默認為DS;BP缺省為SS。當然,有指定段的情況除外。也可以在兩個寄存器加和的基礎上再增加一個立即數。
【不能使用】
- MOV [BX+CX],AX (CX不能做變址寄存器)
- MOV [BX+BP],AX (BP不能做變址寄存器)
- MOV [BX+DI],ARRAY (兩個全在內存中的操作數,不符合語法)
2-1 五種與轉移地址有關的尋址方式
2-2-1 標號與過程名
與轉移地址相關的指令主要是JMP和CALL指令,而要讓代碼能夠跳躍執行到指定的IP處則需要通過標號指示某行代碼,或是通過過程名定義進行CALL調用。
2-2-2 段內直接尋址
即直接使用標號與過程名進行跳轉。根據位移量的不同,可以加SHORT(8BITS)和NEAR PTR(16BITS)操作符。其中,條件跳轉只能是8位因此省略SHORT,而JMP則缺省為16位位移量。因此,在跳轉位移已知的前提下,使用JMP SHORT可以提高程序的執行效率。
【例】
- JMP L1
- CALL P1
- JMP SHORT L1
- JMP NEAR PTR L1 (L!與當前IP位移量為16位的數值)
2-2-3 段內間接尋址
即將轉移目的地址放入寄存器中進行存儲,調用的也是寄存器中的相應數值。
【例】
- MOV AX,OFFSET P1 CALL AX
- JMP BX
這里要特別注意段內間接尋址與數據尋址中寄存器間接尋址的區別,后者有[]進行尋址。
- MOV AX,OFFSET P1 MOV ADD1,AX CALL ADD1
- MOV BX,OFFSET ADD1 CALL [BX]
以上兩種也是段內間接尋址,注意這里ADD1不是過程名,而是數據段中的一個數據的地址,存放了子程序P1的位移量。BX則存放了ADD1的地址,因此調用CALL [BX]也屬於段內間接尋址。
2-2-4 段間直接尋址
具備FAR屬性的尋址。例如P2為一個有FAR屬性定義的過程:
- CALL FAR P2
2-2-5 段間間接尋址
形式如下:
- JMP DWORD PTR [BX+INTERS]
只要DWORD PTR后面是除了立即尋址和寄存器尋址之外的任何一種數據尋址方式即可。
3 語法知識
【判斷指令正誤】
- MOV [CX],AL 不正確。CX不能作為寄存器間接尋址的寄存器
- MOV BH,320 不正確。320超出了8位范圍(255)
- MOV DS,2000H 不正確。不存在從立即數到段寄存器的數據通路。此外,段寄存器作目的操作數時,不允許使用CS作為目的操作數。
- ADD SI,FDDH 不能確定。如果在數據段定義過一個名為FDDH的數據變量,且該數據在字節范圍內,則此指令正確。否則會認為FDDH是一個未定義的變量,改成0FFDH后正確。
- SHL AX,2 不正確。移位指令格式中,移位的數量count只能是1或CL。移動位數大於1(0和1也可以)必須放入CL寄存器中操作。
- CMP BYTE PTR [SI],X 不正確。源操作數和目的操作數不能同時為內存中的數。
- LEA BX,[SI] 正確。
- LDS BX,[DX] 不正確。DX不能用作寄存器間接尋址的寄存器
- JMP BYTE PTR AX 不正確。轉移只有NEAR/FAR PTR + 標號或SHORT+標號或只有標號/寄存器的形式。沒有JMP BYTE PTR的形式
- JMP AX 正確。
- JMP [AX] 不正確。AX不能用作間接尋址的寄存器。
- RET 5 不正確。后面的立即數必須為偶數。這是為了帶參數調用的子程序在返回時要彈出幾個參數的位置,進而維持堆棧的平衡。
- MOV [BX+SI+10],100 不正確。注意只有在寄存器相對尋址取數(作為源操作數)時直接尋址即可,若作為目的操作數則必須指定size。修改為MOV BYTE PTR[BX+SI+10],100即可。
- DIV AL 正確。執行結果為AX=0001即除以本身,商1余0。
4 簡答問題
4-1 解讀指令執行過程
1. RET EXP
IP ← [SP]
SP ← SP + 2
SP ← SP + EXP
2.RETF
注意如果是FAR屬性的過程,返回時是段間返回(即在匯編器中會被匯編成RETF指令),會執行以下過程
IP ← [SP]
SP ← SP + 2
CS ← [SP]
SP ← SP + 2
3.PUSH SRC
SP ← SP - 2
SS:[SP] ← SRC
4.PUSHF
SP ← SP - 2
SS:[SP] ← PSW
5.POP DST
DST ← SS:[SP]
SP ← SP + 2
6.POPF
PSW ← SS:[SP]
SP ← SP + 2
7.LEA REG,SRC
將SRC的偏移地址送入REG
8.LDS/LES REG,SRC
將SRC中的雙字內容分別送REG和DS/ES中。
這里的SRC中的雙字通常保存的是某個程序或變量的邏輯地址(SEG:OFFSET),前面的低字送入REG,后面的高字送入DS/ES。
9.CALL FAR PTR P1
SP ← SP - 2
SS:[SP] ← 返回地址段值
SP ← SP - 2
SS:[SP] ← 返回地址偏移值
IP ← 目的偏移地址
CS ← 目的段地址
10.CALL AX(假設為段內間接調用)
SP ← SP - 2
SS:[SP] ← 返回地址偏移值
IP ← AX中有效地址
11.JMP [BX]
從內存中根據BX間接尋址,取得的標號值送IP進行跳轉。
12.JMP DX
將DX中有效地址偏移值送入IP進行跳轉。
13.CALL DWORD PTR [BX]
段間間接調用,過程地址CS:IP(這是一個雙字,因此用DWORD PTR)位於數據段中通過BX間接尋址得到。
14.LOOP LP1
CX = CX - 1
若CX ≠ 0,則跳轉至LP1,否則順序執行之后代碼。
15.CMP BX,X1
分別通過寄存器尋址和直接尋址取得BX與X1的值,計算BX-X1並影響標志位。(如可用ZF判斷兩數是否相等、CF=1或SF=1則BX<X1等等,再配合JC,JS等指令即可進行條件跳轉)
16.INT 21H / IRET
Intel8086/8088指令系統中用於支持中斷調用的指令為INT n,返回中斷的指令時IRET。此外,CLI用於清除中斷標志,STI用於設置中斷標志。
----《書》P173
INT 21H的執行過程:
- SP ← SP - 2
- SS:[SP] ← PSW
- SP ← SP - 2
- SS:[SP] ← INT N 下一條指令的CS
- SP ← SP - 2
- SS:[SP] ← INT N 下一條指令的IP
- IP ← [0000 : N*4]
- CS ← [0000 : N*4+2]
IRET的執行過程:
- IP ← SS:[SP]
- SP ← SP + 2
- CS ← SS:[SP]
- SP ← SP + 2
- PSW ← SS:[SP]
- SP ← SP + 2
4-2 圖解移位指令

4-3 指出目的寄存器中的內容
已知:DS = 2100H,BX = 0100H,SI = 0002H;內存中:[21100H] = 12H,[21101H] = 34H,[21102H] = 56H,[21103H] = 78H。
- MOV AX,[101H] ;直接尋址,默認段DS。AX的結果為3456H ?在DOS下,匯編后變成了MOV AX,101這種立即尋址形式,使得AX最終的結果為0101H
- MOV AX,WORD PTR [BX+2] ;寄存器相對尋址。AX結果為7856H
- MOV AL,BYTE PTR [BX][SI+1] ;基址變址尋址。AL結果為78H
- MOV AX,100H [SI] ;寄存器相對尋址。AX結果為7856H(注意取出的為一個字!而且是小端存儲!低字節在高位!)
4-4 指出CS與IP的值
已知:DS = 2100H,BX = 0101H,CS = 1900H;內存中:[21101H] = 0C7H,[21102H] = 0FFH,[21103H] = 00H,[21104H] = 0F0H。
- JMP BX ;CS = 1900H,IP=0101H
- JMP [BX] ;CS = 1900H,IP=0FFC7H
- JMP WORD PTR [BX+1] ;CS = 1900H,IP=00FFH (注意小端存儲,低字節在低地址)
- JMP DWORD PTR [BX] ;CS = 0F000H,IP=0FFC7H (取雙字分別取得的是段地址與偏移值)
4-5 根據要求畫內存示意圖
1.定義MYSEG數據段,其中有S1,內容'ABCD'以00H結尾;S2是能用AH=9,INT 21H顯示的字符串;S3為10x10的二維字數組。L1為S1+S2+S3的長度。
則可以畫出該數據段內存示意圖如下:

其中一個英文字母占據1字節,即一個內存單元;顯示字符串必須以'$'結尾。常量定義形式應為 L1 EQU $-S1。注意,字數組一個字占兩個字節。故L1的值為210。
2.書P96第2題的數據段可以定義如下:
1 DATA SEGMENT PARA 2 X1 DB 'Display string',0DH,0AH,'$' 3 X2 DB 32 4 X3 DW 40H 5 X4 DD A000H,0120H 6 X5 DW 10 DUP(8 DUP(0)) 7 X6 EQU $-X1 8 DATA ENDS
4-6 綜合練習題
【題簽】有數據段定義如下:
1 DATA1 SEGMENT PARA 2 X1 DB 20H,?,'A' 3 X2 DW 2 DUP(1,2DUP(1,?)) 4 X3 DD 12345678H 5 LEN EQU $-X2 6 DATA1 ENDS
(1)畫出內存圖
(2)執行MOV AX,X3+1后,AX為?
(3)執行MOV CX,LEN后,CX為?
【解】
(1)內存圖如下:

說明:對於字和雙字的定義,低字節在低位,因此如對於DW 1234H來說,在內存中由低地址到高地址依次為34H、12H;對於DD 12345678H而言,在內存中由低地址到高地址依次為78H、56H、34H、12H。而對於數組的定義而言,如DUP,則是按照其定義先后順序在內存中由低到高排列的。必須注意的是:由於前面定義的是字DW,所以DUP中的每一個數值都占據兩個內存單元,即1個字的空間,這在畫內存圖時必須要注意!
(2)AX = 3456H這道題這里有點小bug,編譯后會報告1個warning,更好的改進是使用MOV AX,WORD PTR X3+1。
這道題可以改進成一個更有意思的考法:MOV AX,WORD PTR X3+2
這樣以來就要聯系(1)中畫的內存圖了。內存中高地址存放的是數據中的高字節。因此結果應該是AX=1234H
(3)CX = 18H(可以表示為16進制,一定注意數組定義DW DUP的問題!這會對LEN的計算產生影響)
5 編程題
5-1 加法
計算Z=X+Y。其中X,Y為16位數,Z為32位數。
1 XOR DX,DX 2 MOV AX,X 3 ADD AX,Y 4 ADC DX,0 5 MOV WORD PTR Z+2,DX 6 MOV WORD PTR Z,AX
這里引入一個技巧:為了操作32位數,我們需要借用DX:AX進行操作,我們一個一個地計算這兩個寄存器中的數值,低位產生的進位補到DX中去,使用ADC指令。最后為內存中的Z使用WORD PTR進行賦值即可。
5-2 右移
將32位X右移4位。
1 MOV AX,WORD PTR X 2 MOV DX,WORD PTR X+2 3 SHR DX,1 4 RCR AX,1 5 SHR DX,1 6 RCR AX,1 7 SHR DX,1 8 RCR AX,1 9 SHR DX,1 10 RCR AX,1
必須要注意的是這里必須使用SHR與RCR指令配合4次,每次移動1位進行使用,這是由於,CF只能存放1位數字!
5-3 乘法
用移位及加法指令,將32位數X計算X = X * 10。
1 MOV AX,WORD PTR X 2 MOV DX,WORD PTR X+2 3 SHL AX,1 4 RCL DX,1 5 MOV BX,AX 6 MOV CX,DX 7 SHL AX,1 8 RCL DX,1 9 SHL AX,1 10 RCL DX,1 11 ADD AX,BX 12 ADC DX,CX 13 MOV WORD PTR X,AX 14 MOV WORD PTR X+2,DX
這里用到的技巧是將X*10分解成X*2 + X*8來計算,也就是將X左移1位保存下來再左移2位加上剛才保存的值即可。
5-4 打印
將內存中16位X顯示為十六進制ASCII碼。
1 MOV BX,X 2 MOV CX,4 3 LP: 4 PUSH CX 5 MOV CL,4 6 ROL BX,CL 7 MOV AL,BL 8 AND AL,0FH 9 ADD AL,30H 10 CMP AL,39H 11 JBE DISP 12 ADD AL,7 13 14 DISP: 15 MOV DL,AL 16 MOV AH,2 17 INT 21H 18 POP CX 19 LOOP LP
注意以下幾點技巧:
- 16位數字X需要輸出4位數字,因此設置CX的值為4作為外層循環次數
- 輸出每次對X的值進行循環左移4位(不帶CF)的ROL指令,這樣每次BL的低4位即為當前要輸出的值
- 由於又用到了CL,因此外層循環的CX需要在第4行處壓棧處理
- 由於每次輸出只有4位,而最少取出AL為8位,因此需要使用第8行AND AL,0FH來屏蔽AL的高4位
- 需要注意的是,在16進制中超過9的數字變成了A,由於ASCII碼中‘9’與‘A’之間相差8,因此需要判斷是否需要給AL增加相應值,使用的是ADD AL,7實現
- 輸出單個字符的中斷調用為2號中斷調用
6 寫在最后
熊老師的《x86匯編語言》這門課程是本學期選的最成功的一門課程,熊老師對學生也十分認真負責。這也再次印證了那個真理,那就是只有實踐,才能真正把理論中的內容理解、消化。
從最開始的連課都聽不懂、程序寫不出、編程毫無頭緒,到后來經歷了幾次作業的歷練后,思路漸漸清晰,我不得不十分感謝熊老師的嚴格要求。
匯編是一種十分貼近計算機底層的語言,它深刻的揭示了程序運行的過程以及內存的使用和分配機制,在本科階段,有匯編編程的鍛煉經歷,我認為是十分有必要的。
最后,在編寫這篇筆記的過程中,還要特別感謝小馬哥和喬給我提出的寶貴的修改意見!
明天就要期末考試了。真心的希望先先能夠發揮高水平,取得好成績。與各位共勉!
