写这个系列文章的主要目的是记录书中重要的知识点,并和大家分享一些个人理解与实践。由于笔记中的知识点比较零散,而书中系统的介绍了一个 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 + si
bx + di
bp + si
bp + 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。