因写操作系统的需要打造自己的操作系统(一),在此单独开一篇记录汇编语言学习的历程。
在学习之前,有必要了解一下x86 和 x64 在计算机中到底表示什么。参考 32、64、x86、x64 有什么区别?
寄存器
8086CPU有14个寄存器,分别是
AX, BX, CX, DX, SI, DI, SP, BP, IP, CS, SS, DS, ES, PSW
所有寄存器都是16位
通用寄存器,通常用来存放一般性的数据:
AX, BX, CX, DX
为了和之前的寄存器兼容,16位寄存器低8位(0-7)位构成AL寄存器,高8位(8-15)位构成AH寄存器。AH和AL是可以独立使用的8位寄存器。
用十六进制来表示数据可以直观地看出这个数据是由哪些8位数据构成的。比如2000写成4E20就可以直观地看出,这个数据是由4E和20两个8位数据构成的。
汇编指令不区分大小写
在进行数据传送或运算时,要注意指令的两个操作对象的位数应当是一致的,例如
mov ax,bx
mov al,18H
add ax,bx
都是正确指令,而
mov ax,bl
mov bh,ax
mov al,20000 (8位寄存在最大可存放值为255的数据)
都是错误指令
另外急需要注意的是,将数据送入寄存器的时候,高8位和低8位是如何存储的。例如,将数据 mov ax,BE00H
,执行完成后,al 中存储的是 BE,ah 中存储的是 00。
物理地址
8086CPU是16位机,有20位地址总线,可以传送20位地址,达到1MB的寻址能力。但它又是16位结构,表现出的寻址能力只有64KB。因此,8086CPU采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址。
地址加法器采用 物理地址=段地址 × 16 \times 16 ×16 + 偏移地址 的方法合成物理地址。例如,8086CPU要访问地址为123C8H的物理单元,过程为:
- 提供段地址1230和偏移地址00C8
- 送入加法器
- 段地址 × 16 \times 16 ×16(左移4位),变为12300
- 段地址 × 16 \times 16 ×16 + 偏移地址,得到123C8
- 输出123C8到地址总线
物理地址=段地址 × 16 \times 16 ×16 + 偏移地址 的本质含义:CPU 在访问内存时,用一个基础地址和一个相对于基础地址的便宜地址相加,给出内存单元的物理地址。
段的概念
段:若干地址连续的内存单元。偏移地址为16位,16位的寻址能力为64KB,所以一个段的长度最大为64KB。段的起始地址一定是16的倍数。
段地址称SA,偏移地址称EA
结论:CPU 可以用不同的段地址和偏移地址形成同一个物理地址。
段寄存器
8086CPU的4个段寄存器:CS, DS, SS, ES
CS 和 IP
CS和IP指示了CPU当前要读取指令的地址。CS为代码段寄存器,IP为指令指针寄存器。
设CS中的内容为M,IP中的内容为N,则8086CPU将从内存M × 16 \times 16 ×16 + N单元开始,读取一条指令并执行。
读取指令的过程如下:
- 从CS:IP所指向的内存单元读取指令,读取的指令进入指令缓冲器;
- IP=IP+所读取指令的长度,从而指向吓一跳指令;
- 执行指令。转到步骤1,重复这个过程。
在 8086CPU 加电启动或复位后,CS和IP被设置为CS = FFFFH,IP = 0000H。因此,8086PC机刚启动是,CPU将从FFFF0H单元读取指令执行。FFFF0H单元中的指令是开机后执行的第一条指令。
mov
指令被称为传送指令,不能用于设置CS、IP的值。能改变CS、IP的内容的指令被称为转移指令,例如:jmp。
若想同时修改CS、IP的内容,可用jmp 段地址:偏移地址
指令完成
jmp 2AE3:3
执行后:CS=2AE3H, IP=00003H, CPU将从2AE33H处读取指令。
若修改IP的内容,可以用jmp 某一合法寄存器
指令完成
jmp ax
debug
在windows xp的命令行中,输入debug
即可进入debug模式
r用来查看、改变CPU寄存器的内容
r ax
修改ax寄存器内容
r cs
修改cs寄存器内容
d用来查看内存中的内容,格式为:d 段地址:偏移地址
。如果想知道内存 10000H 处内容,用d 1000:0
。
e 用来改写内存中的内容,比如要将内存 1000:0~1000:9 单元中的内容分别写为0、1、2、3、4、5、6、7、8、9,可以用e 1000:0 0 1 2 3 4 5 6 7 8 9
,也可以e 1000:0
按回车,逐个修改。
练习:
a 在一个预设的地址,用汇编的形式写入指令,再用 t 执行,可以看到执行的结果。
寄存器(内存访问)
字单元,存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成。将起始地址为 N 的字单元简称为 N 地址字单元。
DS 和 [address]
mov bx,1000H
mov ds,bx
mov al,[0]
三条指令将 10000H(1000:0)中的数据读到 al 中。
其中,[…] 表示一个内存单元,[0] 中的0表示内存单元的偏移地址。 内存单元的段地址默认放在 ds 中,指令执行时,8086CPU会自动从 ds 中取出。ds 是一个段寄存器,8086CPU 不支持将数据直接送入段寄存器的操作,所以要先用一个寄存器进行中转。
mov、add、sub 指令
mov 指令可以有以下几种形式
mov 寄存器,数据
mov 寄存器,寄存器
mov 寄存器,内存单元
mov 内存单元,寄存器
mov 段寄存器,寄存器
mov 寄存器,段寄存器
mov 内存单元,段寄存器
mov 段寄存器,内存单元
add 和 sub 同mov一样,但是不能操作段寄存器
数据和程序有区别吗?如何确定内存中的信息哪些是数据,哪些是程序?
CPU 提供的栈机制
入栈和出站指令:push 和 pop
push ax
表示将寄存器 ax 中的数据送入栈中,pop ax
表示将栈顶取出的数据送入 ax。入栈和出栈操作都是以字为单位进行的。
栈顶的段地址存放在 SS 中,偏移地址存放在 SP 中。任意时刻,SS:SP 指向栈顶元素。
push ax 的执行,由以下两步完成:
- SP=SP-2,SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶。
- 将 ax 中的内容送入 SS:SP 指向的内存单元出, SS:SP 此时指向新栈顶。
8086CPU中,入栈时,栈顶从高地址向低地址方向增长。
栈为空的时候,SS:SP 指向栈空间最高地址单元的下一个单元。
pop ax 的执行,由以下两步完成:
- 将 SS:SP 指向的内存单元处的数据送入 ax 中;
- SP=SP+2 ,SS:SP 指向当前栈顶下面的单元,以下面的单元为新的栈顶。
栈越界问题
8086CPU 不保证我们对栈的操作不会越界。我们在编程的时候要自己操心栈顶越界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的越界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的越界。
push、pop 指令
push 寄存器 ;将寄存器中的数据入栈
pop 寄存器 ;用寄存器接收出栈的数据
也可以
push 段寄存器
pop 断寄存器
push 内存单元
pop 内存单元
一个字在栈中的存储,字的高8位比低8位在栈中的地址要高。
第一个程序
伪指令
assume cs:codesg
codesg segment
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00H
int 21H
codesg ends
end
- segment 和 ends 是一对成对使用的伪指令。其功能是定义一个段,使用格式为:
段名 segment
段名 ends
上述程序中定义了一个名为 codesg 的段。一个有意义的汇编程序至少要有一个段。
-
end 是一个汇编程序的结束标记
-
assume ,它假设某一段寄存器和程序中的某一个用
segment...ends
定义的段相关联。上述程序中,用 assume cs:codesg 将段 codesg 和段寄存器 cs 联系起来。
说明,程序中
mov ax,4c00H
int 21H
所实现的功能就是程序返回(现在不需要理解为什么)。
程序编译连接
编译环境按照MASM5.0汇编环境安装所述的搭建,编译连接的过程按照书中所述进行。
可执行文件中的程序 P1 若要运行,必须有一个正在运行的程序 P2,将 P1 从可执行文件中加载入内存,将 CPU 的控制权交给它;P1 运行完毕后,应该将 CPU 的控制权交还给 P2。
问题:有一个正在运行的程序将 1.exe 中的程序加载入内存,这个正在运行的程序时什么?它将程序家载入内存后,如何使程序得以运行?程序运行结束后,返回到哪里?
答:DOC 系统的 shell 叫command.com
(1) 在 DOC 系统中直接执行 1.exe 时,是正在运行的 command,将 1.exe 加载入内存;
(2) command 设置 CPU 的 CS:IP 指向程序的第一条指令,从而使从徐得以运行。
(3) 程序运行结束后,返回到 command 中,CPU 继续运行 command。
跟踪程序的运行过程
可以使用 debug 1.exe
观察程序的单步执行。
程序的加载过程:
(1) 程序加载后,ds 中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序所在的内存区的地址为 ds:0
(2) 这个内存区的前 256 个字节中存放的是 PSP,DOS用来和程序进行通信。从 256 字节处向后的空间存放的是程序。
所以,ds 中可以得到 PSP 的段地址 SA,PSP 的偏移地址为 0,则物理地址为 SA X 16 + 0
因为 PSP 占 256(100H) 字节,所以程序的物理地址是:
SA X 16 + 0 + 256 = SA X 16 + 16 X 16 + 0 = (SA + 16) X 16 + 0
即 (SA + 10H) : 0
可以看到,DS = 075C,则 PSP 的地址为:075C:0,程序的地址为:(075C+10H):0,即 076C:0。图中,CS=076C,IP=0000,CS:IP指向程序的第一条指令。
[bx] 和 loop 指令
定义描述性符号:"()",用来表示一个寄存器或一个内存单元的内容。比如 (ax) 表示 ax 中的内容,(2000H) 表示内存 2000H 单元的内容。
要完整地描述一个内存单元,需要两种信息:
- 内存单元的地址;
- 内存单元的长度
用 [0] 表示一个内存单元时,0 表示单元的偏移地址,段地址默认在 ds 中,单元的长度(类型)可以由具体指令中的其他操作对象(比如说寄存器)指出。
[bx] 同样也表示一个内存单元,它的偏移地址在 bx 寄存器中。
[bx]
mov ax,[bx]
表示 (ax) = ((ds) x 16 + (bx))
mov [bx],ax
表示 ((ds) x 16 + (bx)) = (ax)
问题:写出程序执行后,21000H ~ 21007H 单元中的内容。
自行思考,答案:
ps: inc 是自增指令,dec 是自减指令
loop 指令
N × 2 = N + N N \times 2 = N + N N×2=N+N
例如,计算 2 12 2^{12} 212
assume cs:code
code segment
mov ax,2
add ax,ax
add ax,ax
;一共做11次 add ax,ax
mov ax,4c00h
int 21h
code ends
end
loop 指令的格式是:loop 标号
,CPU 执行 loop 指令的时候,要进行两步操作
- (cx)=(cx)-1;
- 判断 cx 中的值,不为 0 则转至标号处执行程序,为 0 则向下执行。
使用 loop,可以简化为
assume cs:code
code segment
mov ax,2
add cx,11
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end
问题:用加法计算 123 × 236 123 \times 236 123×236,结果存在 ax 中。
assume cs:code
code segment
mov ax,0
mov cx,236
s: add ax,123
loop s
mov ax,4c00h
int 21h
code ends
end
改进上题的计算速度
assume cs:code
code segment
mov ax,0
mov cx,123
s: add ax,236
loop s
mov ax,4c00h
int 21h
code ends
end
有一点需要注意,汇编源程序中,数据不能以字母开头,这意味着,大于 9FFFh 的十六进制数据,例如 A000h、C001h、FFFEh,都要在前面加 0,写成 0A000h、0C001h、0FFFEh。
(未完待续)