在学习之前,有必要了解一下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位数据构成的。

汇编指令不区分大小写

在进行数据传送或运算时,要注意指令的两个操作对象的位数应当是一致的,例如

1
2
3
mov ax,bx
mov al,18H
add ax,bx

都是正确指令,而

1
2
3
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 + 偏移地址 的方法合成物理地址。例如,8086CPU要访问地址为123C8H的物理单元,过程为:

  1. 提供段地址1230和偏移地址00C8
  2. 送入加法器
  3. 段地址×16\times 16(左移4位),变为12300
  4. 段地址×16\times 16 + 偏移地址,得到123C8
  5. 输出123C8到地址总线

物理地址=段地址×16\times 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 + N单元开始,读取一条指令并执行。

读取指令的过程如下:

  1. 从CS:IP所指向的内存单元读取指令,读取的指令进入指令缓冲器;
  2. IP=IP+所读取指令的长度,从而指向吓一跳指令;
  3. 执行指令。转到步骤1,重复这个过程。

在 8086CPU 加电启动或复位后,CS和IP被设置为CS = FFFFH,IP = 0000H。因此,8086PC机刚启动是,CPU将从FFFF0H单元读取指令执行。FFFF0H单元中的指令是开机后执行的第一条指令。

mov指令被称为传送指令,不能用于设置CS、IP的值。能改变CS、IP的内容的指令被称为转移指令,例如:jmp。

若想同时修改CS、IP的内容,可用jmp 段地址:偏移地址指令完成

1
jmp 2AE3:3

执行后:CS=2AE3H, IP=00003H, CPU将从2AE33H处读取指令。

若修改IP的内容,可以用jmp 某一合法寄存器指令完成

1
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]

1
2
3
mov bx,1000H
mov ds,bx
mov al,[0]

三条指令将 10000H(1000:0)中的数据读到 al 中。

其中,[…] 表示一个内存单元,[0] 中的0表示内存单元的偏移地址。 内存单元的段地址默认放在 ds 中,指令执行时,8086CPU会自动从 ds 中取出。ds 是一个段寄存器,8086CPU 不支持将数据直接送入段寄存器的操作,所以要先用一个寄存器进行中转。

mov、add、sub 指令

mov 指令可以有以下几种形式

1
2
3
4
5
6
7
8
9
10
11
12
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 的执行,由以下两步完成:

  1. SP=SP-2,SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶。
  2. 将 ax 中的内容送入 SS:SP 指向的内存单元出, SS:SP 此时指向新栈顶。

8086CPU中,入栈时,栈顶从高地址向低地址方向增长。

栈为空的时候,SS:SP 指向栈空间最高地址单元的下一个单元。

pop ax 的执行,由以下两步完成:

  1. 将 SS:SP 指向的内存单元处的数 据送入 ax 中;
  2. SP=SP+2 ,SS:SP 指向当前栈顶下面的单元,以下面的单元为新的栈顶。

栈越界问题

8086CPU 不保证我们对栈的操作不会越界。我们在编程的时候要自己操心栈顶越界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多而导致的越界;执行出栈操作的时候也要注意,以防栈空的时候继续出栈而导致的越界。

push、pop 指令

1
2
push 寄存器 ;将寄存器中的数据入栈
pop 寄存器 ;用寄存器接收出栈的数据

也可以

1
2
3
4
5
push 段寄存器
pop 断寄存器

push 内存单元
pop 内存单元

一个字在栈中的存储,字的高8位比低8位在栈中的地址要高。

第一个程序

伪指令

1
2
3
4
5
6
7
8
9
10
11
12
13
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
  1. segment 和 ends 是一对成对使用的伪指令。其功能是定义一个段,使用格式为:
1
2
3
4
段名 segment


段名 ends

上述程序中定义了一个名为 codesg 的段。一个有意义的汇编程序至少要有一个段。

  1. end 是一个汇编程序的结束标记

  2. assume ,它假设某一段寄存器和程序中的某一个用 segment...ends 定义的段相关联。上述程序中,用 assume cs:codesg 将段 codesg 和段寄存器 cs 联系起来。

说明,程序中

1
2
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 单元的内容。

要完整地描述一个内存单元,需要两种信息:

  1. 内存单元的地址;
  2. 内存单元的长度

用 [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+NN \times 2 = N + N

例如,计算 2122^{12}

1
2
3
4
5
6
7
8
9
10
11
12
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 指令的时候,要进行两步操作

  1. (cx)=(cx)-1;
  2. 判断 cx 中的值,不为 0 则转至标号处执行程序,为 0 则向下执行。

使用 loop,可以简化为

1
2
3
4
5
6
7
8
9
10
11
12
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×236123 \times 236,结果存在 ax 中。

1
2
3
4
5
6
7
8
9
10
11
12
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

改进上题的计算速度

1
2
3
4
5
6
7
8
9
10
11
12
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。

(未完待续)