寫這個系列文章的主要目的是記錄書中重要的知識點,並和大家分享一些個人理解與實踐。由於筆記中的知識點比較零散,而書中系統的介紹了一個 x86-16 處理器在實模式下的工作原理以及如何使用匯編語言與其進行“溝通”,所以推薦想要系統學習的朋友們去學習這本書。當我們掌握了實模式的工作原理之後,就可以進一步研究後來出現的其他運行模式(如保護模式)。除此之外,熟悉彙編語言有助於我們掌握上層語言(如 C)的執行原理,因為它們都要對彙編(機器碼)進行抽象,而彙編程序就是基於 CPU 的執行機理寫出來的。
第四章
一個有意義的彙編程序中至少要有一個段,即:代碼段。在 masm 中可以用 assume 將有特定用途的段和特定的段寄存器關聯起來。
彙編程序從寫出到執行的過程
編程 -> 1.asm -> 編譯 -> 1.obj -> 連接 -> 1.exe -> 加載 -> 內存中的程序 -> 運行
所有語言編寫的代碼,都要經過類似方式最終被轉換成計算機可以識別的機器碼。比如:C 語言在編譯系統(gcc,as,ld)中的處理過程是這樣的:
編程 -> 1.c -> 預處理 -> 1.i -> 編譯 -> 1.s -> 彙編 -> 1.o -> 鏈接 -> a.out
EXE 文件中程序的加載過程
參見:圖 4.20(與 Linux 加載執行可執行文件(ELF)的過程類似)
實驗三:編程、編譯、連接、跟蹤
- 下載 edit 編輯器(
edit.com),將其拷貝至可執行程序目錄~/DOS/bin -
在
~/DOS/mytest/目錄中使用 edit 編輯以下內容,然後保存到t1.asm文件中assume cs:codesg codesg segment mov ax, 2000H mov ss, ax mov sp, 0 add sp, 10 pop ax pop bx push ax push bx pop ax pop bx ; 這 4 行實現了交換 ax、bx 中的數據 ; 程序返回 mov ax, 4c00H int 21H codesg ends end -
編譯、連接
- 下載 masm5.0 彙編器(
masm.exe) 和 Overlay Linker3.60 連接器(link.exe),將其拷貝至~/DOS/bin目錄下 -
啓動 dosbox, 使用 編譯器 和 連接器 進行編譯和連接
cd mytest ###### 編譯 (參見4.4節) ###### masm # 根據提示輸入 Source filename [.ASM]: c:\mytest\t1 Object filename [ti.OBJ]: c:\mytest\t1 Source listing [NUL.LST]: [按下回車] Cross-reference [NUL.CRF]: [按下回車] ###### 連接 (參見4.5節) ###### link # 根據提示輸入 Object Modules [.OBJ]: t1 Run File [T1.EXE]: [按下回車] List File [NUL.MAP]: [按下回車] Libraries [.LIB]: [按下回車] -
編譯、連接的簡化方式:
masm c:\mytest\t1 link t1
- 下載 masm5.0 彙編器(
-
執行和跟蹤
# 執行(加不加 .exe 後綴都可以) t1 t1.exe # 跟蹤(注:必須要加 .exe 後綴) debug t1.exe # 注意:要使用 -p 調試 "int 21H" 指令!!!
第五章
要完整的描述一個內存數據,需要的信息
- 地址(即首地址、起始地址)
- 類型(即上下文,可以確定該信息由多少個連續的存儲單元組成,以及該內存區域的佈局情況)
用 cx 寄存器和 loop 指令實現循環
- 在
cx中存放循環次數 loop指令中的標號所標識的地址要在前面- 循環執行的程序段寫在標號和
loop指令之間
loop 指令的內部執行過程
-
先進行
cx=cx-1, 然後判斷cx的值是否等於0- 是:執行下一條指令(在取址階段之後~執行
loop之前,會將IP更新為IP+loop指令長度) - 否:繼續執行循環體(在取址階段之後~執行
loop之前,會將IP更新為 標號代表的地址)
- 是:執行下一條指令(在取址階段之後~執行
其他內容
5.4節 説明了 masm 編譯器將 mov ax,[0] 識別為 mov ax,0 的解決辦法——顯示指定段前綴
注意,我們在純 DOS 方式(實模式)下,可以不理會 DOS, 直接用匯編語言去操作真實的硬件,因為運行在 CPU 實模式下的 DOS, 沒有能力對硬件系統進行全面、嚴格的管理。但在 Windows 2000、Unix 這些運行於 CPU 保護模式下的操作系統中,不理會操作系統,用匯編語言去操作真實的硬件,是根本不可能的。硬件已被這些操作系統利用 CPU 保護模式所提供的功能全面而嚴格的管理了。
DOS 中
0:200~0:2ff這 256 個字節的空間一般是安全的。
實驗四:[bx]和loop的使用
練習(2): 向 0:200~0:23F 內存單元內,依次寫入十進制數字:0~63
; file: exp4_2.asm
assume cs:codesg
codesg segment
mov ax, 0020H ; 0020:0~0020:3F 與 0:200~0:23F 物理地址重合,只是表示不同的段
mov ds, ax ; 使用 0020 作為段基址可以讓【偏移量】和【要寫入的數字】保持一致
mov bx, 0
mov cx, 64
s: mov [bx], bl ; 注意:1.每次寫入一字節 2.`mov [dx], bl` 報錯(見寄存器尋址規則)
inc bx
loop s
mov ax, 4c00H
int 21H
codesg ends
end
練習(3): 將 mov ax, 4c00H 指令前的指令複製到 0020:0000 開始的內存單元中
; file: exp4_3.asm
assume cs:codesg
codesg segment
mov ax, cs
mov ds, ax
mov ax, 0020H
mov es, ax
mov bx, 0
mov cx, 17H ; 如何知道要複製的指令字節長度是 "17H"? 是使用 debug 調試得到的
s: mov al, [bx]
mov es:[bx], al
inc bx
loop s
mov ax, 4c00H
int 21H
codesg ends
end
第六章
可執行文件的組成
- 描述信息(彙編器、連接器對源程序中相關偽指令進行處理所得到的),如程序的入口地址
- 程序(數據、指令)
6.1節 在代碼段中使用數據,程序框架:
assume cs:codesg
codesg segement
數據
…………
start: 代碼
…………
codesg ends
end start
將數據、代碼、棧放入不同段的意義在於:
- 讓程序的結構清晰明瞭,提高程序的可讀性
- 讓每個段獨佔
64KB大小的地址空間(實現段隔離) - 內存對齊,方便尋址,有利於代碼優化:對於每個段,起始地址的偏移地址都是
0000, 而將數據全部定義在代碼段中時,數據會依次緊湊排列(不會對齊)
實驗五:編寫、調試具有多個段的程序
練習(5): 將 a 段和 b 段中的數據同一列相加,結果存入 c 段對應列中(實現方法一)
; file: exp5_5.asm
assume cs:code
a segment
dw 1,2,3,4,5,6,7,8
a ends
b segment
dw 1,2,3,4,5,6,7,8
b ends
c segment
dw 0,0,0,0,0,0,0,0
c ends
; 程序退出前,通過 "-d 076c:0 2f" 能夠看到以上3個段實際的內存數據(雖然是3個段,但它們佔用的內存是連續的)
code segment
start: mov bx, 0
mov cx, 8
s: mov ax, a
mov ds, ax
mov dx, ds:[bx] ; 獲取 a 段的值,存入 dx
mov ax, b
mov ds, ax
add dx, ds:[bx] ; 獲取 b 段的值,計算加法,存入 dx
mov ax, c
mov ds, ax
mov ds:[bx], dx ; dx 寫入 c 段中
;add bx, 2
inc bx
inc bx
loop s
mov ax, 4c00H
int 21H
code ends
end start
練習(5): 實現方法二——使用寄存器+偏移量進行尋址
; file: exp5_5_1.asm
assume cs:code
a segment
db 1,2,3,4,5,6,7,8
a ends
b segment
db 8,7,6,5,4,3,2,1
b ends
c segment
db 0,0,0,0,0,0,0,0
c ends
; 程序退出前,通過 "-d ds:0 2f" 能夠看到以上3個段實際的內存數據(雖然是3個段,但它們佔用的內存是連續的)
code segment
start: mov ax, a
mov ds, ax
mov bx, 0
mov cx, 8
s: mov dx, ds:[bx]
add dx, ds:[bx+16]
mov ds:[bx+32], dx
inc bx
loop s
mov ax, 4c00H
int 21H
code ends
end start
練習(6): 用 push 指令將 a 段中前 8 個字型數據,逆序存儲到 b 段中
; file: exp5_6.asm
assume cs:code
a segment
dw 1,2,3,4,5,6,7,8,9,0aH,0bH,0cH,0dH,0eH,0fH,0ffH
a ends
b segment
dw 0,0,0,0,0,0,0,0
b ends
code segment
start: mov ax, a
mov ds, ax
mov ax, b
mov ss, ax
mov sp, 10H
mov bx, 0
mov cx, 8
s: push ds:[bx]
;add bx, 2
inc bx
inc bx
loop s
mov ax, 4c00H
int 21H
code ends
end start
第七章
在彙編程序中,可以用單引號引用字符,編譯器將把它們轉換為相應的 ASCII 碼。如:
db 'unIX'
; 相當於
db 75H, 6EH, 49H, 58H
mov al, 'a'
; 相當於
mov al, 61H
大小寫字母轉換問題
對於字母的 ASCII 碼,有以下特徵:
- 所有字母
[a-zA-Z]的 ASCII 碼值,二進制的最高位1都在第7位,即可表示為:01** **** - 同一字母,大寫碼值+20H=小寫碼值,即相差
32 (2^5)
基於以上特徵得出結論:對於同一字母的大小寫碼值,它們的 8 位二進制中,只有第6位有差異,其它位是相同的,即:
-
大寫字母的特徵:ASCII 碼值(十進制)為 65~90, 第 6 位一定是:
0-
小寫字母轉大寫:將
第6位變成 0 即可,如:mov al, 'a' and al, dfH ; 二進制:11011111B
-
-
小寫字母的特徵:ASCII 碼值(十進制)為 97~122, 第 6 位一定是:
1-
大寫字母轉小寫:將
第6位變成 1 即可,如:mov al, 'A' or al, 00100000B
-
以下幾種尋址方式,作用相同
; case1——[bx+idata]
mov ax, [bx+200]
mov ax, [200+bx]
mov ax, 200[bx]
mov ax, [bx].200
; case2——[bx+si]或[bx+di]
mov ax, [bx+si]
mov ax, [bx][si]
; case3——[bx+si+idata]或[bx+di+idata]
mov ax, [bx+si+200]
mov ax, [bx+200+si]
mov ax, [200+bx+si]
mov ax, 200[bx][si]
mov ax, [bx].200[si]
mov ax, [bx][si].200
問題 7.6(對 [bx(或 si、di)+idata] 的應用)
; 將 datasg 段中每個單詞的頭一個字母改為大寫字母
assume cs:codesg,ds:datasg
datasg segment
db '1. file ' ; 16 bytes
db '2. edit '
db '3. search '
db '4. view '
db '5. options '
db '6. help '
datasg ends
codesg segment
start: mov ax, datasg
mov ds, ax
mov bx, 0
mov cx, 6
s: mov al, [bx+3]
and al, 11011111B ; 小寫轉大寫,使用與運算將第 6 位設置為 0, 其他位不變
mov [bx+3], al
add bx, 16
loop s
mov ax, 4c00H
int 21H
codesg ends
end start
問題 7.7(對 [bx+si(或 di)] 的應用)
; 將 datasg 段中每個單詞改為大寫字母
assume cs:codesg,ds:datasg
datasg segment
db 'ibm ' ; 16 bytes
db 'dec '
db 'dos '
db 'vax '
datasg ends
codesg segment
start: mov ax, datasg
mov ds, ax
mov bx, 0
mov cx, 4
s0: mov dx, cx ; 寄存器數量是有限的,當寄存器都被使用時,需要使用內存(通常是使用"棧",避免程序中定義額外的結構)來做臨時存儲區域
mov si, 0
mov cx, 3
s: mov al, [bx+si]
and al, 11011111B
mov [bx+si], al
INC si
loop s
add bx, 16
mov cx, dx
loop s0
mov ax, 4c00H
int 21H
codesg ends
end start
實驗六——對問題 7.9 進行編程:將 datasg 段中每個單詞的前4個字母改為大寫字母
; file: exp6.asm
assume cs:codesg,ds:datasg,ss:stacksg
stacksg segment
dw 0, 0, 0, 0, 0, 0, 0, 0
stacksg ends
datasg segment
db '1. display '
db '2. brows '
db '3. replace '
db '4. modify '
datasg ends
codesg segment
start: mov ax, stacksg
mov ss, ax
mov sp, 16
mov ax, datasg
mov ds, ax
mov bx, 0
mov cx, 4
s0: push cx
mov si, 0
mov cx, 4
s: mov al, [bx+3+si]
and al, 11011111B
mov [bx+3+si], al
INC si
loop s
add bx, 16
pop cx
loop s0
mov ax, 4c00H
int 21H
codesg ends
end start
第八章——之前章節的總結
8086 中寄存器尋址的規則
偏移地址(寄存器的有效組合)
- 在 8086 中只有
bx, si, bi, bp這4個寄存器可以用在 "[ ]" 中來進行內存單元的尋址 -
這4個寄存器可以單個出現,或只能以如下組合出現(
bx, bp不能配合;si, di不能配合):bx + sibx + dibp + sibp + di
- 以上所有的出現方式,都可以再和立即數
idata進行加減
段地址
- 只要在 "[ ]" 中使用了寄存器
bp, 如果沒有顯示指定段地址,那麼段地址就在ss中 - 如果在 "[ ]" 中沒使用寄存器
bp, 並且沒有顯示指定段地址,那麼段地址就在ds中
尋址方式總結
- 直接尋址——
[立即數] - 寄存器間接尋址——
[bx|si|bi|bp] - 寄存器相對尋址——
[bx|si|bi|bp+立即數] - 基址變址尋址——
[bx|bp+si|bi] - 相對基址變址尋址——
[bx|bp+si|bi+立即數]
8086 機器指令中要處理的數據的位置及如何表達這個位置
| 位置 | 表達方式 |
|---|---|
| CPU 內部——寄存器 | 使用寄存器名,如:ax |
| CPU 內部——指令緩衝器 | 使用立即數來表達 |
| 內存 | 指令中給出偏移地址,段地址在相應的段寄存器中 |
8.6節 C語言結構體與彙編的【尋址語法】類比
8.7節 8086 中除法的計算規則
彙編指令:div 除數(在內存或某個寄存器中),除數要麼是 8 位,要麼是 16 位
| 8位除數 | 16位除數 | |
|---|---|---|
| 被除數 | 16位——在 ax 中 |
32位——dx 存高16位、ax 存低16位 |
| 商 | al 中 |
ax 中 |
| 餘數 | ah 中 |
dx 中 |
實驗七:尋址方式在結構化數據訪問中的應用
將 data 段中的數據按照書中指定的格式寫入到 table 段中,並計算 21 年中的人均收入(取整),結果也按照格式保存在 table 段中
; file: exp7.asm
assume cs:codesg
data segment
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
; 以上是表示這21個【年份】的21個字符串(84字節)
dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
; 以上是表示這21年【每年總收入】的21個 dword 型數據(84字節)
dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
dw 11542,14430,15257,17800
; 以上是表示這21年【每年僱員人數】的21個 word 型數據(42字節)
data ends
table segment
db 21 dup ('year summ ne ?? ') ; 16 bytes
; 相當於聲明 21 個:db 'year summ ne ?? '
table ends
codesg segment
start: mov ax, data
mov ds, ax
mov ax, table
mov es, ax
mov si, 0 ;【每年僱員人數】數組中,元素的偏移地址(2字節遞增)
;【年份】和【每年總收入】這兩個數組中,元素的偏移地址(4字節遞增,等於 si*2)
mov di, 0 ; table 段數據(結構體數組),每行開始的地址(每行表示一個結構體元素)
mov cx, 21
s: push si
add si, si ; si*2
;【年份】數組的起始地址(相對於 data 段的偏移量)是 0
mov ax, [si]
mov es:[di], ax
mov ax, [si+2]
mov es:[di+2], ax
mov byte ptr es:[di+4], 20H ; 空格(ASCII值)
;【總收入】數組的起始地址(相對於 data 段的偏移量)是 84
mov ax, [84+si] ; 低位字節寫入 ax (注意:字節序!!)
mov es:[di+5], ax
mov dx, [84+si+2] ; 高位字節寫入 dx
mov es:[di+7], dx
mov byte ptr es:[di+9], 20H
;【僱員數】數組的起始地址(相對於 data 段的偏移量)是 168
pop si
div word ptr [168+si] ; 先計算【人均收入】,商存入 ax
mov dx, [168+si] ; 再獲取僱員數
mov es:[di+10], dx
mov byte ptr es:[di+12], 20H
mov es:[di+13], ax ; 人均收入
mov byte ptr es:[di+15], 20H
add si, 2
add di, 16
loop s
mov ax, 4c00H
int 21H
codesg ends
end start