汇编语言
第二章
通用寄存器AX、BX、CX、DX,为了兼容可分为两个八位XH和XL
SI、DI、SP、BP、
PSW
| 汇编指令 | 描述 | |
|---|---|---|
mov ax/ah, 18 |
AX = 18 |
mov指令不能用于修改CS和IP的值 |
add ax, 8 |
AX = AX + 8 |
|
jmp 段地址:偏移地址 |
同时修改CS和IP的值 |
|
jmp 某一合法寄存器ax |
IP = ax |
仅修改IP的值 |
mov ax,[offset] |
AX = 地址为DS+offset内存单元中的数据 |
段寄存器
一个段的的最大长度为64KB:段地址1000H左移一位+偏移地址1111H 偏移最大为16位
四个段寄存器SS、 DS、ES、CS
指令指针寄存器IP
即CPU任意时刻将 CS:IP 指向的内容当做指令来执行
8086CPU给出物理地址的方法: 段地址 * 16 + 偏移地址
执行过程:
从CS:IP指向的内存读取指令,取到指令缓冲器
IP=IP+所读取指令的长度,从而指向下一条指令
执行指令跳转到(1)
8086CPU在加电启动或复位后
CS–>FFFFH
IP–>0000H
实验
1 | -r : 查询寄存器状态 |
各类存储芯片
随机存储器
装有BIOS是ROM(只读)
接口卡上的RAM
| 00000~9FFFF | 主储存器地址空间(RAM) |
|---|---|
| A000~BFFFF | 显存地址空间 |
| C0000~FFFFF | 各类ROM地址空间 |
| [^8086PC机内存地址空间分配1] |
- 从地址0~9FFFF读取数据实际就是在读取主随机存储器上的数据
- 向地址A0000~BFFFF的内存单元写入数据就是向显存写入数据
1 | -e b810:0000 01 01 02 02 03 03 04 04 |
- 向C0000~FFFFF的内存单元写入数据是无效的(ROM是只读的)
第三章 寄存器及内存访问
字单元: 存放一个字型数据的内存单元(16位)
N地址字单元: 起始地址为N的字单元
DS寄存器: 存放要访问数据的段地址
1 | mov bx,1000H |
栈、栈段
8086CPU提供出站入站的指令且出站入栈操作都是以字为单位,SS:SP选择一段以栈的方式访问的内存空间
段寄存器SS和寄存器SP,任意时刻SS:SP指向栈顶元素 SP递减栈由高地址向低地址
1 | 设置栈 |
Debug的T命令在修改寄存器SS的指令时,下一条指令会紧接着执行
1 | mov ax,1000H |
实验任务
单步调试(t指令)导致新建栈区内存发生改变
你在设置好 SS(栈段)和 SP(栈顶指针)后,就算还没有写任何 push 指令,这块内存也被修改了。
根本原因在于: Debug 的 t 指令(单步跟踪)是依赖 CPU 的单步中断(Interrupt 1) 来实现的。CPU 在处理任何中断时,都必须强制把当前的“运行现场”保存到当前有效的栈中,以便中断处理完后能找回原来的执行位置。这个“强制保存”的动作,悄无声息地向你的栈里压入了 6 个字节的数据。
执行目标指令: CPU 首先老老实实地执行完你代码里的一条指令(例如
mov bx, 1111)。触发单步中断: 指令执行完毕的瞬间,CPU 硬件检测到标志寄存器(FLAGS)中的 TF(陷阱标志)位为 1,立即触发单步中断。
强制压栈保存现场(内存被修改的发生地): 为了能去执行 Debug 程序的代码并在屏幕上显示寄存器状态,CPU 自动将 3 个关键寄存器压入你刚刚设置好的栈 (
SS:SP) 中。顺序严格如下:SP = SP - 2,压入 FLAGS (保存各种状态标志位)SP = SP - 2,压入 CS (保存中断发生时的代码段地址)SP = SP - 2,压入 IP (保存中断发生时,下一条将要执行的指令的偏移地址)
(此时,你的栈空间里就多出了这 6 个字节的数据,内存发生了你所观察到的改变。)
移交控制权: CPU 修改 CS 和 IP 的值,跳转到 Debug 的核心代码去执行,把刚才的寄存器状态打印到你的屏幕上。
恢复现场并返回: Debug 程序执行到最后一条指令
IRET(中断返回)。CPU 再次接管,按照刚才压栈的逆序,从你的栈中把数据弹出来:弹出数据到 IP,
SP = SP + 2弹出数据到 CS,
SP = SP + 2弹出数据到 FLAGS,
SP = SP + 2(此时,SP 指针完美复位,CPU 回到你的代码中继续待命,等待你输入下一个命令。)
第四章
汇编 -> 编译 -> 链接
源程序
1 | assume cs:codesg /* 表示codesg段 与cs寄存器(代码段) 有关系 即为一个代码段 */ |
| 伪指令 | 用法 |
|---|---|
| end | 汇编结束标记符 |
| codesg segment … codesg ends | 定义一个段 |
| assume | 某寄存器和某段有关 |
程序执行的过程跟踪
编程 -> 1.asm -> 编译 -> 1.obj -> 连接 -> 1.exe -> 加载(command)
加载过程:
- 先找到一段初始SA 地址作为起始地址有足够空间
- 在内存区的钱256个字节中创建一个程序前缀(PSP)的数据区
- PSP区:SA:0
程序区:SA:SA + 10H:0
一般将两个区分成不同段 - DA = SA 初始化后 CS:IP = SA + 10H:0
(程序执行完后可通过DS来查看代码所在地址)
第五章 [ BX ] 和 loop 指令
- BX -> EA 存放的数据作为一个偏移地址
- cx(count计数寄存器 用于loop循环的次数)先减一在判 0
- 在汇编源程序中,数据不能以字母开头 A000H -> 0A000H
Debug 和 汇编编译器 masm 对 [num] 的处理不同:
- debug会认为是ds:num处的数据,而masm会认为是常数num
解决方式:
- 在[ num ]前面显式给出段地址所在的段寄存器 例如 ds : [num]
**此处ds称作段前缀 **
DOS和其他合法程序一般不会使用0:2000:2ff(00200h002ffh)的256个字节空间所以这段空间是安全的(在0:0026处写入数据会造成死机)
实验:向内存0:200 ~ 0:23F 依次传入数据 0 ~ 63(3FH)
1 | assume cs:code |
第六章包含多个段的程序
- dw(define word)用于定义字形数据,若在段开头定义则数据地址在该段的起始地址(cs:[0])
1 | assume cs:code |
可执行文件由描述信息和程序组成,程序来源于源程序中的汇编指令和定义的数据,描述信息主要是编译和连接程序对源程序中相关伪指令进行的处理
1 | ;6.3 |
定义多个不同段可以直接用段的名称找到该段的地址
例如定义了 stack段,可以使用 mov ax, stack 可以将栈段的地址传入ax
实验五:
1 | assume cs:code, ds:data, ss:stack |
- CPU执行程序,返回前,data段中的数据没有变化
- cs = 076C 、ss = 076B、 ds = 076A
- X - 2, X - 1
- 实际占有空间 [ N / 16] * 16 个字节
- 写在后面需要提前计算代码段的长度计算偏移(32)
| 指令 | 作用 | 结果 |
|---|---|---|
end start |
告诉编译器:程序结束了,并且程序的启动入口在 start 标号处。 |
编译出的 EXE 文件头里会记录入口地址。 |
end |
仅仅告诉编译器:源文件到此结束,不再编译后面的文字。 | 不指定入口地址,系统默认从程序的第一行代码开始执行。 |
内存寻址方法和指令长度
我们将寻址方式按“从简到繁”排列,看看字节数是如何增加的:
- 寄存器寻址(最快、最短)
- 示例:
mov ax, bx - 长度: 2 字节
- 原理: 1 字节操作码 + 1 字节 ModR/M(告诉 CPU 是哪两个寄存器)。不需要任何额外的内存地址信息。
- 寄存器间接寻址(无位移)
- 示例:
mov ax, [bx] - 长度: 2 字节
- 原理: 地址已经存在寄存器里了,CPU 只需要 ModR/M 字节就能确定“去
bx指向的地方提货”。
- 直接寻址(带 16 位偏移量)
- 示例:
mov ax, [1234h]或push ds:[0] - 长度: 3 ~ 4 字节
- 原理: 除了操作码,指令里必须硬编码进
1234h这两个字节的地址。mov ax, [addr]是特化指令,3 字节。push ds:[0]是通用格式,4 字节(操作码 + ModR/M + 2字节偏移量0000h)。
- 基址变址寻址(最复杂)
- 示例:
mov ax, [bx + si + 1234h] - 长度: 4 字节
- 原理: 这是最复杂的组合。
- ModR/M 字节记录了
bx和si的组合关系。 - 指令结尾必须带上
1234h这 2 字节 的位移量。
- ModR/M 字节记录了
例5:编写程序将a段和b段中的数据相加存储到c段中
1 | assume cs:code |
例6:用push指令将a段中的前八个字形数据,逆序存储到b段中
1 | assmue cs:code |
内存地址方法
与或指令 and和or
大小写转换问题使用and方式就可以完美解决:
大写字母的ASCII码的二进制第五位(从零位开始)置零就一定是大写
基址变址寻址 [bx + idata]