第 1 章 基础知识
在汇编层面,指令和数据别无二致,都是一堆二进制数据。
那么,CPU 是如何分辨此时的二进制代码应当识别为数据还是指令呢?
答案是 IP 寄存器。IP 寄存器指向的数据即为指令。
CPU 和内存共同组成了计算机的“大脑”,二者之间通过总线进行连接。总线根据用途,可以分为:
- 用于 CPU 告诉内存要取的数据地址的地址线;
- 用于内存向 CPU 传递数据的数据线;
- 用于 CPU 向内存传递读写指令的控制线。
CPU 的所有指令以及数据,都是通过总线在内存中进行访问的,但物理上各种数据的存储又是分开的,比如显卡的显存,主存储器的 RAM 等(这些都是运行时的内存)。因此,很容易联想到,这些物理上的内存一定通过某种方式联系了起来,供 CPU 使用。具体表现为,CPU 使用的是虚拟内存地址,将逻辑地址映射到物理地址上。
其中,虚拟内存地址的映射是通过内存管理单元(MMU, Memory Management Unit)来实现的。MMU 负责将 CPU 的逻辑地址(虚拟地址)映射到物理地址,并管理虚拟内存的分页、段等操作。这一过程涉及到页表(Page Table)的使用,CPU 通过查找页表,将虚拟地址转换为物理地址,最终访问到具体的物理内存。
第 2 章 寄存器
8086 cpu 有 14 个寄存器,其中通用寄存器有 4 个:ax、bx、cx、dx,这四个都可以分为上下进行使用,例如 ax 可以分为 al、ah。
8086 cpu (16 位)可以处理两种类型的数据:
- 字节,1 字节 = 8 bit
- 字,1 字 = 2 字节 = 16 bit
cs 和 ip 是最重要的寄存器。CPU 通过 cs:ip 访问所有的指令,指令的位置为基址 + 偏移
,即 cs * 16 + ip
。
常用指令:
jmp 2AE3:3
,令 cs = 2AE3,ip = 3。mov ax, bx
,将 bx 的值赋给 ax。jmp 4
,令 ip = 4,cs 不变。
第 3 章 寄存器(内存访问)
对于这张图,1 地址处的字型数据为
124EH
,字节型数据为 4EH
。
mov al, [0]
:将 [0] 单元中的数据送入 al 寄存器,该数据的默认段地址为 ds 寄存器中的内容。[0]
表示一个偏移地址为 0 的内存单元。
mov ds, 1000H
是不合法的命令,mov ds, ax
则合法。
栈由 ss
寄存器 和 sp
指针决定, ss:sp
指向当前栈的栈顶。如果此时栈为空,则 sp
为栈最高内存单元地址 + 2。
push ax
/ pop ax
的作用为先变化 sp
指针,然后将 ax 中的内容压入栈中/从栈中内容弹出到 ax 中。sp
指针从高地址向低地址变化。
8086 CPU 根据不同的寄存器/指针判断当前内存区域是代码、栈还是数据:
- cs:ip 指向的内容为代码段的首位。
- ds 中存放的内容就作为数据段的首位地址。
- ss:sp 指向的内容为栈段的栈顶。
第 4 章 第一个程序
一个程序编写好了之后,需要经过编译、连接,才能成为可执行文件。每个步骤执行结束之后的文件后缀分别为:
- 编写完成:
.asm
- 编译完成:
.obj
- 连接之后:
.exe
连接的作用:
- 程序很大时,将多个编译后的文件连接在一起。
- 程序调用库文件中的程序时,将库文件和该文件生成的目标文件连接在一起。
- 单个文件编译之后有些内容无法直接生成机器码,这部分内容就由连接来生成机器码。
第 5 章 [BX]和loop指令
设 bx 中的数据为 EA,ds 中的数据为 SA,则:
mov ax, [bx]
表示将 EA:SA 的数据赋给 ax。mov [bx], ax
表示将 ax 的数据送入内存 EA:SA 处。
loop s
表示循环标号 s 所在的代码,循环次数由 cx 寄存器决定。
指令中不止可以使用默认 ds 表示段地址,也可以使用段前缀,例如:mov ds:[0] al
,mov ax, es:[bx]
等。
第 6 章 包含多个段的程序
db 用于定义字节型数据,dw 用于定义字型数据。
dw 0123h, 0456h
定义两个 16 进制数据 0123,0456。
如果先定义数据,再编写程序,则 CPU 指执行时最先接触的就是数据而非程序。因此,我们通过 end
指明程序的入口,例如 end start
指明入口程序的标号为 start
,程序执行的时候,CPU 就会自动将标号 start 程序所在的地址作为程序的起始地址。
dw 命令既可以说它定义了数据,也可以说它开辟了空间。
mov ds data
,data 为段标号。这句是将 data 所在的段地址赋给了 ds。
db 'a', 'b'
定义两个字符数据 a, b。当然实际只是 ASCII 码 65h、66h。
若是需要让 cpu 将代码段、栈段、数据段正确识别,就需要在程序运行时将段地址(cs、ss、ds)赋好、指针(ip、sp)的位置指好,例如:
|
|
第 7 章 更灵活的定位内存地址的方法
and al, 10111111B
和 or al, 10111111B
进行与和或的操作。
某内存的数据如下所示:
2000:1000 BE 00 06 00 6A 22 ......
(ds = 2000, bx = 1000)则
- 第一个字的数据地址为
2000:1000
,值为00BEh
。 - 第二个字的数据地址为
2000:1002
,值为0006h
。 - 而
ds:[bx + 1]
加的是字节,不是字,因此其值为0600h
。
大写字母和小写字母在二进制层面的区别为:小写字母的第 5 位为 1,而相应的大写字母的第 5 位为 0,且二者的二进制只有这里有区别。(位数从 0 开始算)
si,di 为功能和 bx 相近的寄存器。
常量、变量或者他们的结合都可以用来表示一个内存地址,例如 [5],[bx + 4],[bx + si],[bx + di + 4] 等。
第 8 章 数据处理的两个基本问题
指令执行前,数据可能在的位置有 3 个:CPU 内部、内存、端口。
汇编中的数据位置有 3 种表达方式:
- 立即数,执行前在 CPU 的指令缓冲器中,例如
mov ax 1
。 - 寄存器,执行前在 CPU 的寄存器中,例如
mov ax,bx
。 - 通过寄存器给出的偏移地址指向内存中的地址,执行前在内存中,例如
mov ax,[bx + 8]
8086 cpu 中只有 4 个寄存器可以放在中括号 []
中表示内存地址,分别是 bx、di、si、bp。其中:
- 当寄存器组合出现的时候,只有以下四种组合合法:
bx 和 di
、bx 和 si
、bp 和 di
、bp 和 si
。 [bp]
的段地址默认在 ss 中,而非 ds 中。
常用寻址方式小结:
如何确定当前 CPU 要操作的是字型数据还是字节型数据?有如下 3 种方式:
- 通过寄存器名指明,例如 ax 是字的,al 是字节的。
- 通过关键字指明,例如
mov word ptr [bx]
指明是字型数据,mov byte ptr [bx]
则指明是字节型数据。
设被除数为 x,除数为 y,则要表达 x / y,其汇编指令为 div y
,除数存储在内存单元或者一个寄存器中,而被除数则存储在 ax
或者 ax
和 dx
中。
- 若 x 为 8 位,则 y 为 16 位,默认存放在 ax 中。
- 若 x 为 16 位,则 y 为 32 位,默认存放在 ax 和 dx 中,dx 放高 16 位,ax 放低 16 位。
dd 1
定义一个双字型的数据 00000001H
。
dup
配合 db
,dw
,dd
等伪指令使用,用于重复创建相同的数据。例如 db dup 3 (0)
创建 3 个字节类型的 0。
第 9 章 转移指令的原理
可以修改 ip 或 cs & ip 的指令称为转移指令。主要分为:
- 无条件转移指令(如:jmp)
- 条件转移指令
- 循环指令(如:loop)
- 过程
- 中断
转移行为分为:
- 段内转移(只修改 ip),如
jmp ax
。根据修改的 ip 大小,段内转移又分为:- 短转移(-128 ~ 127)。
- 近转移(-32768 ~ 32767)。
- 段间转移(同时修改 cs & ip)如
jmp 1000:0
。
mov ax, offset start
,offset 的作用是获取 start 标号所在的偏移地址。
jmp short s
,short 表明此次 jmp 进行的是短转移,用 s 的偏移地址修改 ip 指针。
jmp 命令并不是通过目的地址进行跳转,而是通过相对量进行位移(相对量通过 标号所在地址
与 jmp 后一条命令所在地址
的地址之差获取)。使用相对地址跳转而不是直接使用绝对地址的好处在于,如果程序的在内存中的位置更改了,不用把所有的地址都一并更改。
jmp far ptr s
,far ptr 表明此次 jmp 进行的是段间转移(远转移),用 s 的段地址和偏移地址修改 cs 和 ip。
jmp word ptr ds:[0]
,word 表明在 ds:[0] 处存放了一个字的内容,用这个内容修改 ip 的值。
jcxz s
,jcxz 是一个条件转移,只有当 cx == 0 的时候才跳转到标号 s 所在位置。
loop s
,所有循环都是短转移,只有当 cx == 0 的时候才跳转到标号 s 所在位置。
第 10 章 call 和 ret 指令
ret
相当于 pop IP
,即把栈中的数据弹到 IP 指针中。
retf
相当于 pop IP
再 pop CS
,即把栈中的数据弹到 IP 指针之后再弹一次数据到 CS 中。
call 标号
的就是调用函数, 作用是先将当前的 IP 或者 CS & IP 压栈,然后再跳到标号所在的位置执行程序。根据是否跨段决定是否从栈中弹出 CS 的值,例如:
call 标号
,相当于push IP
,jmp near ptr 标号
。call far ptr 标号
,相当于push CS
,push IP
,jmp far ptr 标号
。call al
,相当于push IP
,jmp al
。call word ptr [4]
,相当于push IP
,jmp word ptr [4]
。
call
和 ret
一般配合使用,call 调用函数,在这之前将当前执行位置压栈,等执行完毕之后使用 ret 把栈中的内容返回。
mul bl
或者 mul bx
,mul 表示乘法,两个乘数要么都是 16 位,要么都是 8 位。除了 mul 后的操作数,另一个乘数默认在 ax 中或者 al 中(跟位数有关)。8 位乘法的结果默认放在 ax 中,16 位默认高位在 dx 中,低位在 ax 中。
父子程序可能使用同一个寄存器,因此子程序的最开头需要将这些信息入栈保存起来,结尾时候出栈返回。
第 11 章 标志寄存器
即 flag,每一位都有作用。
- 第 6 位,zf,0 标志位。zf = 1,则结果为 0。
- 第 2 位,pf,奇偶标志位。pf = 1,则结果里位为 1 的个数为偶数。
- 第 7 位,sf,符号标志位。sf = 1,则结果为负。
- 第 0 位,cf,进位标志位。cf = 1,则向高位借位或者进位了。
- 第 11 位,of,溢出标志位。of = 1,则发生了溢出。
- 第 10 位,df,方向标志位。df = 1,则每次操作之后 si、di 递减。
adc 比 add 多加了一个 cf,sbb 比 sub 多减了一个 cf。
cmp 只做减法,仅影响标志位,不保存结果。
常用条件转移及相关标志位:
- je/jne, zf = 1 / zf = 0。
- jb/jnb, cf = 1 / cf = 0。
- ja/jna, (cf = 0 && zf = 0) / (cf = 1 || zf = 1)。 (not,equal, below, above)
movsb
等价于将 ds:si 地址上的值赋给 es:di 地址。
cld
将 df 置 0,std 将 df 置 1 。
pushf 将 flag 的值压栈,popf 则出栈到 flag 中。
debug 中的标志位表示(标志 - 值为 1 时- 值为 0 时):
- of-OV-NV
- sf-NG-PL
- zf-ZR-NZ
- pf-PE-PO
- cf-CY-NC
- df-DN-UP
第 12 章 内中断的产生
4 种产生中断的情况(形式 - 类型码):
- 除法错误 - 0。
- 单步执行 - 1。
- 执行 into 指令 - 4。
- 执行 int 指令 - n。
中断向量表中存放中断类型码以及相关的中断程序地址,每个表项占用两个字,高地址字放段地址,低地址字放偏移地址。
中断的过程:
- 取得中断类型码 N
- pushf
- TF = 0,IF = 0
- push CS
- push IP
- (IP) = (N * 4), (CS) = (N * 4 + 2)
offset do0end - offset do0
表达式可以计算出 do0 标号程序的长度。
tf = 1 时,会执行单步中断。
进行栈相关的操作之后,ss
和 sp
指针应该连续设置(这期间连中断也不会响应)。
第 13 章 int 指令
int
和 iret
是中断中的 call
和 ret
。
系统板 ROM 中存放了一些程序,叫 BIOS,其存放了这些程序(中断例程):
- 检测硬件、初始化程序;
- 外部、内部中断;
- 对硬件进行 I/O;
- 其它与硬件相关的。
B8000H ~ BFFFFH 共 32 kb 的空间为显示缓冲区。一屏可显示的内容占用 4000 个字节。
要在显示器上显示字符,其实就是把屏幕显示相关的寄存器设置好,再将要显示的字符送入指定的内存地址(即显示缓冲区),然后调用屏幕显示相关的中断让操作系统将其显示在屏幕上。寄存器中的设置的值可以用于指示包括显示光标位置、字符的属性、调用哪个中断的子程序、显示缓冲区中的第几页等。
第 14 章 端口
CPU 外部各种网卡、外设等芯片中的寄存器称为端口。
总结:CPU 可以读取数据的位置有 3 个:CPU 内部的寄存器、内存单元、端口。
in al, 60h
, 从 60 号端口中读入一个字节。出为 out。
in/out 指令只能用 ax(16 位)/al (8 位) 存放要读写的数据。
CMOS RAM 芯片,简称 CMOS,128 字节的 RAM 存储器中 0 ~ 0 dh 字节存放时间信息,其余大部分用于保存系统配置信息。
其存储的时间以 BCD 码的形式存放,1 byte 可表示两个 BCD 码。
shl/shr 是逻辑左移、右移的指令,移出的最后一位放入 CF 中,低位补 0 。
第 15 章 外中断
I/O相关的中断称为外中断,其分为可屏蔽中断和不可屏蔽中断。
可屏蔽中断和内中断的过程相同。不可屏蔽中断的类型码固定为 2,所以没有那些取址的操作。
if 用于控制 cpu 是否需要响应可屏蔽中断,if = 0 则不响应。
sti
设置 if = 1;cli
设置 if = 0 。
键盘输入,按下和松开一个键,会分别产生一个通码和断码(都叫扫描码)。
它们都会被送到 60h 号端口,然后相关芯片就会向 CPU 发送 int 9 中断可屏蔽中断,如果 if =1,则 CPU 响应中断。
通码的第 7 位为 0,断码的第 7 位为 1 ,即断码 = 通码 + 80h。
以下为部分键的通码。
第 16 章 直接定址表
数据标号:
- 若用
a:db 1,2,3,4,5,6,7,8
定义了一组数据,则可以使用mov al, a[si]
替代mov al, cs:[si]
,并且表示 a 起始地址及之后的内存单元都是字节单元。 - 类似地,如果用
b dw 0
定义了一个字单元,则可以使用add b, ax
替代add cs:[bx], ax
,并且 b 起始地址及之后的内存单元都是字单元。
mov al, a[si]
编译为 mov al, [si + 0]
; add b, ax
编译为 add [8], ax
。
a[si]
默认的段地址放在 ds
中,要想正确地访问的是 data
段的地址,需要首先将 a
所在的段 data
的段地址放在 ds
中,即需要如下语句:
assume ds:data
.mov ax, data
,mov ds, ax
.
标号可以直接当数据定义,编译器会将标号所在的地址当作数据的值。例如(在段中定义的):
c dw a, b
相当于c dw offset a, offset b
.c dd a, b
相当于c dw offset a, seg a, offset b, seg b
.seg a
用于取得 a 所在的段地址。
将不同子程序,用空间换时间。
第 17 章 使用 BISO 进行键盘输入和磁盘读写
键盘输入的过程(以 Shift_A 为例):
- 按下左 Shift 键,引发键盘中断。int 9 中断例程接收左 Shift 键的通码,设置 0040:17 处的状态字节的第 1 位为 1,表示左 Shift 键按下。
- 按下 A 键,引发键盘中断,CPU 执行 int 9 中断例程,从 60h 端口读出 A 键的通码。
- 检测状态码,发现左 Shift 键被按下,则将 A 键的扫描码 1Eh 和 A(大写) 对应的 ASCII 码写入键盘缓冲区。
int 16h
从缓冲区读入并删除一个键盘输入。
int 13h
对磁盘进行读写。