x86 16位实模式 01——Debug、寄存器、一些指令

时间:2024-03-05 17:22:03

镇楼图

Pixiv:_LM7_

==================

〇、主板&接口卡

我们知道CPU若想对存储器进行读写操作,需要有三种总线来交互。

CPU有很多管脚,这些管脚所要链接的部分即是总线,三种总线的宽度标志了CPU在三个方面不同的性能。

也存在着一种叫接口卡(端口)的东西去联系外部设备,比如网卡、USB、声卡、显卡等。

==================

一、存储器芯片

在一台计算机中,有很多存储器,所实现的存储功能也略有不同。

大体上来分就只有随机存储器RAM和只读存储器ROM之分

RAM可读可写,但需要带电存储(关机后即会丢失内容)

ROM只能读取,但关机后并不会丢失内容

·····

①BIOS上的ROM

BIOS即Basic Input/Output System基本输入输出系统。它由厂商提供的一个最基本的软件系统,支持你对硬件设备进行最基本的读写操作。BIOS上的信息存储在ROM上,无法修改只能读取(操作)

②随机存储器RAM

这上面存储了大部分程序和数据,直观点来说就是C槽、D槽。

③接口上的RAM

接口上的RAM可以暂时存储需要大量存储的数据,比如显卡上的RAM我们就称为显存

内存地址空间

在下面这张图上清晰地显示了CPU和存储器的关系,我们可以通过e指令来修改一个指定内存里的数据。

CPU都通过总线与这些存储器相连,且要操作时都通过控制线来发出指令对内存进行操作。

从上到下我们知道这些地址空间被分为了主存储器、显存、显卡BIOS ROM、网卡BIOS ROM、系统BIOS ROM,这些所分配的内存地址关系大体上如下表格所示

存储器 DOS下的地址空间(1MB)
主存储区
(中断向量表
BIOS数据区
程序可用内存区)
00000H~9FFFFH
显存区 A0000H~BFFFFH
各类ROM的地址空间 C0000H~FFFFH

在最后ROM地址空间中可以用f000:fff0找到日期信息

e指令

如果想修改一个内存里的内容,我们可以使用e指令

比如

e B800:000

但注意ROM的数据是无法修改的,即使打了e指令也不会有任何效果

也许你注意到了我们所用到的地址和上面表格所说的地址根本不一样

这其实是段地址:偏移地址的形式来去表示物理地址

具体怎么将这两者转换我们会在后续学到

==================

二、寄存器简介

通常来说,一个CPU是由运算器、控制器、寄存器、(三种)总线等器件构成。

■运算器用来处理数据

■寄存器用来存储数据

■总线用来传输数据

■控制器用来控制其他部件

这样看来CPU内的部件不仅分工明确还很有效。

我们是来学习汇编语言的,没必要了解运算器、控制器。

我们的侧重点是如何存储、读写内存的,所以这里只介绍寄存器。

就我们所用的8086CPU来说有14个寄存器且都是占2个字节的:

AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW

r指令

我们可以通过r指令来查看各个寄存器

==================

三、通用寄存器、16位与8位寄存器

在8086CPU中我们把AX、BX、CX、DX称为通用寄存器。

通用寄存器功能:存放数据

简单来说,寄存器相当于C语言中的一个"void类型16位的变量"

假如有一个AX是这样的

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
1 0 1 1 1 1 1 0 0 0 0 0 0 0 1 0

那么我们知道它所存储的信息是1011111000000010B

当然这是1个两字节的寄存器,我们还可以把它拆解成2个一字节的寄存器

低位的也就是0~7的我们用L(low)表示,也就是AL

低位的也就是8~15的我们用H(high)表示,也就是AH

其它的通用寄存器也一样,可以用CH、DL等去拆解1个两位的寄存器

上述例子的AL就是00000010B,AH就是10111110B

0 0 0 0 0 0 1 0 AL
1 0 1 1 1 1 1 0 AH

我们也把一个完整的寄存器叫作16位寄存器

把拆解开的寄存器叫作8位寄存器

解决兼容问题

引入8位寄存器最主要是为了解决一个兼容问题,因为我们知道一个存储单元最小的是占8字节。比如在8086之前的8080CPU的数据总线是8根,也就是8位数据;如果不存在8位寄存器,直接把8080CPU上的程序copy到8086CPU上肯定要出问题的。因此8位寄存器可以很好地兼容以前的CPU的程序,同时也可以提高一定的灵活性。

具体1个寄存器最大能表示什么数,这相信你已经在C语言的数据类型里有体会过了,这里就不再说明。

==================

四、尝试编程

我们之前输入的Debug是DOS、Windows提供的一种实模式(8086方式)程序的调式工具,它提供了二十多种指令

之前我们学习了u、d、e、r指令,这里我们将会学到新的指令?、t、p、a

这里我把之前的和我们即将要学的进行一次汇总

debug模式下的指令 功能
u 将机器指令转化为汇编指令
若指定地址则可查看指定内存地址
d 查看单元地址的内容
若指定地址则可查看指定内存地址的内容
e 修改指定地址里的内容
(注:修改显存的内容更直观,e B800:60)
r 查看当前状态下的寄存器
也可以在后面跟一个寄存器的名字,这样就可以直接修改这个寄存器的值
? 查看帮助
t 单步执行一条汇编指令
p 直接执行所有汇编指令
a 编写汇编指令
注:这里的地址指的不是物理地址而是段地址+偏移地址

现在我们学习两条汇编指令:

mov

​ mov指令可以将数据写入到寄存器里

add

​ add指令可以将指定的寄存器在其原本数据基础上加上一个数据


这里我们使用a和t指令去编写和执行

当然你也可以用p指令去执行


现在来点复杂的

然后看以下结果

由此我们可以得出以下结论

①从第一条指令我们得出16位寄存器和8位寄存器无法一起使用,也就是操作时位数要对齐

②就结果来看显示寄存器的值它是16进制的,而且你mov、add的值都是16进制的。

比如尝试使用add al,500就会因为500太大而error,因为500是16进制的500.

(注:部分的汇编代码可能会把500看作十进制)

③AL则是AX的后两位,AH则是AX的前两位

③从第三条指令我们尝试给ah add -5,很明显会超出结果。但最终它变成了补码的形式

哪怕ax是FFFF我们去+5,它也会变成0004

④你应该注意到这些通用寄存器和高级语言的变量很类似(但个数很少),mov似乎就是赋值,add似乎就是加法

⑤寄存器默认值都是0

⑥因为都是16进制的缘故,我们不必做很繁琐的换算,可以直接得到

\[AX=AH拼接AL \\ AX=BH拼接BL \\ CX=CH拼接CL \\ DX=DH拼接DL \]


猜测以下汇编代码的结果

mov ax,18
mov ah,78
add ax,8
mov ax,bx
add ax,bx

以下这段代码的结果能告诉你AL、AH是互不影响的,即使AL在FF去+1也不会去进位到AH上

mov al,ff
add al,1

==================

五、段地址:偏移地址

我们现在知道如果要想访问寄存器需要一个物理地址也就是地址总线去表示一个内存。但很坑爹的一点是8086CPU的地址总线有20根,而要存储地址的数据总线却只有16根,这就表明我们无法直接地用一个物理地表示但我们依然用了一种方法去解决这个问题,我们用段地址+偏移地址的方式去描述这个物理地址

\[物理地址=段地址×16+偏移地址 \]

首先我们要学会给出一个段地址:偏移地址去知道它所代表的物理地址是什么?

当我们输入a的时候,在编写指令时它会在左边显示这个指令的地址

比如我们要计算上图mov ax,0的物理地址。

段地址=073F,偏移地址=0100

我们通过公式可以得到其物理地址为074F0H

这样我们就知道我们所书写的代码它的地址是从074F0H开始的。

用段地址+偏移地址是为了解决地址总线不足的问题,并不适合我们去描述,而物理地址适合我们去描述。

当然有的时候也会用段地址去描述一块内存区域。

段地址、偏移地址为什么能描述物理地址?它们有什么含义?

这一点我们需要知道一些进制的知识(这里我不会说明)

物理地址有5位,段地址、偏移地址只有4位。

就16进制而言,我们乘上16相当于左移了一位,比如073FH乘16就会变成073F0H。

这样我们的段地址可以用来描述5位中的前4位,而偏移地址可以描述5位中的后4位。

就大小而言,段地址更大,偏移地址更小。

因此我们可以把这两个地址看成一个大的地址值加上一个较小的偏移量。

这个较大的地址我们可以在逻辑上描述为一段段大的内存空间,较小的地址我们则称为一个小的偏移量。

如何去描述一个物理地址?

我们能通过段地址+偏移地址去描述物理地址,在我们想要访问的一个物理地址中自然也需要会这种逆向运算。

假如我们想尝试用e指令修改B8765H(A0000H~BFFFFH)的内容,该怎样计算?

一个较为简单的方法就是前四位看作是它的段地址,后一位则当作其偏移地址

这里段地址SA=B876H,偏移地址EA=0005H

下图为e指令后的结果(因为是在显存区,所以修改后会在屏幕上显示颜色各异的字符)

我们再来输入d指令看看数据是不是真的被我们修改了

可以看到的是我们确实修改了显存区的一些数据

去描述物理地址的一个弊端

首先给出以下地址

2000:1F60
2100:0F60
21F0:0060
21F6:0000

我们通过计算可以发现它所表示的物理地址全是21F60H

因此我们能得出一个结论

物理地址可以用多种SA:EA表示

==================

六、寄存器CS、IP

代码段寄存器CS,指令指针寄存器IP

我们运用了段地址+偏移地址去描述物理地址,但其运作机制我们尚不了解,现在需要去了解其机制。

首先CS寄存器是用来存段地址的,IP寄存器是用来存偏移地址。

CS:IP所指向的内存会被转换成指令

下图解释了指令如何在CPU里执行

(1)将CS、IP地址传输到地址加法器中,去输出一个物理地址

(2)将转换好的物理地址传输至控制电路中

(3)将这个地址作为地址总线的值在内存中去索引

(4)索引所得到的数据通过数据总线传回控制电路

(5)将这个数据传输至指令缓冲器

(6)会先让CS:IP自动增加一个值,所得到新的CS:IP指向了下一次要执行的指令

(7)执行指令

这也是为什么你输出一段汇编代码后用r查看当前CS:IP的指令显示的却是下一个指令的原因

【图中红框部分有当前CS:IP的信息,也有当前CS:IP所指向的指令的信息】

用jmp指令修改CS、IP的值

因为CS、IP它们决定当前CPU要读取什么指令,如果我们尝试去修改CS、IP那么它所要读取的指令也会改变,这样修改CS、IP就如同使用C语言的goto一样跳转语句。

8086不支持直接用mov指令修改CS,IP的值,但却提供了另外一种更加直观的方法:jmp

jmp如其名字一样揭示了修改CS、IP就如同jump。

jmp的作用就是修改CS:IP的值

jmp 0100

上面代码表示将IP修改为0100

jmp A:BC

上面的代码表示将CS:IP修改为000A:00BC

a 2000:0
mov ax,0
add bx,1
jmp ax

上面的代码因为编写代码的地方被指定在了2000:0000,所以第一条指令mov ax,0的地址为2000:0。jmp却会将IP设置为0,因此这是一个死循环,不断执行这三条指令。

这是一个不断乘2的小程序
a 073F:0100
mov ax,1
add ax,ax
jmp 0103

代码段

不管是C语言还是汇编,都会有一块内存专门用来存放代码,我们把这一块内存称为代码段。在汇编语言中我们可以用a 地址来指定编写哪一块内存的代码

CPU如何区分指令、数据?

之前我们有个问题,同一个二进制数据既可以表示成指令,也可以成数据那么该如何区分?

现在我们有了答案。

数据就是存放在AX、BX等这些通用寄存器,指令则是存放在CP、IP这些寄存器中。

当我们用通用寄存器时这些二进制数据就被识别成了数据。

当我们用了CS、IP时则会被识别成指令。

==================

总结

这里你可以学会基本的debug指令a、e、u、d、t、p、?

能够简单地了解一些汇编指令、寄存器

能够转换物理地址和SA:EA

能够使用jmp指令修改CS:IP

==================

参考书籍

《汇编语言 第四版》——王爽

参考网站

https://fishc.com.cn/forum-39-1.html

https://b23.tv/qUfphX