- 前置知识
关于8086CPU的介绍:
CPU将CS、IP中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址。
8086CPU中的寄存器:(汇编指令一般都是对寄存器进行操作)
该CPU一共有14个寄存器:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW都是16位。
通用寄存器:用来存放一般性数据(总共16位,分为高8位和低8位,这也导致了在对寄存器进行操作时,两个寄存器的位数要相同,运算对象的类型要匹配)。
AX、BX、CX、DX
可以想想如果运算时,有两个问题:1.运算对象的类型不匹配;2.结果越界
怎么办?
段寄存器:提供内存单元的段地址。8086CPU有四个段地址寄存器,由于硬件设计,8086CPU不支持直接将数据送入段寄存器的操作。
对段地址有疑问的移步此:cpu访问内存为什么要段?分段的好处之一就是对同一段内存可以有多种表示方法。
CS DS SS ES
- debug中的指令
R命令查看或改变CPU寄存器的值:
// 查看
-r
//修改
-r [某寄存器]
-r ax
如图所示,AX寄存器的值已经被我们修改为2222 H。
D命令查看内存中的内容:
查看内存10000H处的内容
d 段地址:偏移地址
-d 1000:0
//指定d命令的查看范围 d 段地址:起始偏移地址 结尾偏移地址
//列出地址从10000H开始的共9个内存单元
-d 1000:0 9
该命令将列出从指定内存开始的128个内存单元的内容。左边是每行的起址,中间是内容,右边是对应的ASCII码字符,如果没有对应的ASCII字符,则用"."表示。
黄色线从右到左为汇编,红色线从左到右为反汇编。
E命令修改内存中的值:
-e 起始地址 数据 数据 数据...
写入字符:
写入字符串:
用E命令向内存中写入机器码,用U命令查看内存中机器码的含义,T命令执行内存中的机器码:
可以看到,内存中的指令和数据并没有差别。
还未修改时寄存器的值:
要用T命令执行我们写到1000:0的指令,必须使CS:IP指向1000:0。我们使用R命令分别修改CS和IP,使得CS:IP的值为1000:0。
执行T命令后,CPU执行CS:IP指向的指令,所以第一条指令被执行,即mov ax 0001。指令执行后,ax的值被修改,并且CS:IP指向下一条指令IP=IP+3(因为第一条指令的长度为3个字节)。我们还发现一条指令被完整的取出。
继续执行T命令,查看cpu中寄存器的变化。
执行最后一条指令:
使用Debug的A命令以汇编指令的形式写入指令:
Debug将汇编指令翻译为对应的机器指令,并将机器码写入内存。
- 第二章实验:
题目要求看书,这里给出答案。
(1):
修改CS:IP的的指向:
使用T命令执行指令:注意观察每条指令执行后cpu中寄存器的变化。
CS:IP的指向为当前要执行的指令,所以使用T命令执行命令前,要先将CS:IP的值设置为要执行指令的地址,IP值(单位字节)的变化与当前执行的指令长度有关,执行完当前指令后IP的值自动增加。
(2):
jmp指令可以使CS:IP跳转到我们想去的地方,循环语句的实现。
结果为100H,共循环8次。
(3)显卡的生产日期
1992年1月1日???wtf!!这不是老古董了吗?后来,经过博主查阅后,这个数据原来模拟出来的,感兴趣的小伙伴可以自行搜索一波。
修改值:
由于该区为rom区,所以写入的数据不会改变该地址里的内容。
(4)可知b8100h在显存地址空间,所以在里面写的数据会被读取并解析成相应的屏幕信息,所以就可以看到屏幕显示东西了。
DS和[address]:
[address]:内存的偏移地址。要访问内存,仅有偏移地址是不够的,指令执行时,8086CPU会自动取DS中的值作为段地址。
DS:保存内存单元的段地址(默认)。
指令执行前(已将CS:IP的指向修改为当前要执行指令的地址)
执行后:
关于栈:
push和pop指令:push指令时,栈地址从高到低,SP-2;pop指令时,栈地址从低到高,SP+2。因为8086CPU是16位结构,push时一次传输的数据只能是两个字节,所以SP固定减2。
CPU8086CPU并不知道栈的空间有多大,我们自己要保证操作时不会发生越界。
在执行push和pop指令的时候,8086CPU提供SS:SP寄存器来保存栈顶的段地址和偏移地址。任意时刻,SS:SP指向栈顶元素。
实验:(愚蠢的博主把代码段和栈段写在同一段空间了…)不过这也体现出分段的重要性。
我们还发现,用来当栈的这段空间被用来存放一些寄存器的数据了…我们暂时先不管,后面会揭晓。
正确版:
执行结果:
红线部分:在执行完mov ss,ax这条指令后,SS和SP的都一起改变了,但是mov sp,0010这条指令我们并没有执行过呀,在书中说到这涉及到中断机制,所以我们暂且不去了解,只要知道Debug的T命令在执行修改寄存器SS的指令时,下一条指令也紧接着被执行。
橙线部分:在push指令未执行前,SS:SP的指向为栈空间最高地址单元的下一个单元,即1000:10。在执行完push指令后,SP-2,SS:SP的值为1000:0C。经过两次push和两次pop后,SS:SP又重新指向栈底的前一个单元即1000:10了。
- 汇编程序
伪指令:
segment 和 ends是一对成对使用的伪指令,功能是定义一个段。使用格式为: 段名 segment … 段名 ends。
end:汇编程序的结束标记。
assume:请简单记住用于将代码段和CPU中的段寄存器CS联系。格式 assume cs:段名。
程序的编辑,编译与连接:
使用DOS下的edit编辑源程序:
编译:在DOS下输入masm,运行masm。由于博主的编写asm文件的与masm放在同一个文件夹,所以只需要输入文件名即可。
如果与masm不在同一个目录,且文件后缀是txt,则需要加上完整路径和文件的后缀。
由于当前已在masm目录下,所以我们只需写上接下来的路径,否则找不到。还有查询过后发现,在DOSBOX中只有mount过的盘符才存在。 如果你mount的是C盘,但你的源程序在D盘,那么你输入的路径以D盘开头是无效的,应切换为C盘。编译过后的文件都与masm程序在同一个目录(如果没指定的话)。
连接:
连接的好处:
快速的编译和连接:(忽略中间文件的生成)
exe文件的执行:
跟踪程序的执行过程:
使用debug调试程序
DS寄存器存放着程序所在内存区的段地址。
CX存放的是程序的长度。
CS=DS+10H
程序被装入内存的哪个位置?详细见书。
程序的物理地址:SA+10H:0
在执行int 21 指令时,使用p命令。
- 第三章实验:注意观察栈顶指针的变化
查看PSP内存单元中的内容:发现psp中也存放了程序的相关内容。
[BX] 和loop指令:
[BX]表示偏移地址在bx中,段地址由DS提供。例如:
// 将内存单元的内容存入ax,内存单元的长度为2字节,存放一个字,段地址在ds中,偏移地址在bx中
mov ax,[bx]
// mov ax,((ds)*16+(bx)),将物理地址为((ds)*16+(bx))的内存内容存放入ax。
loop指令格式:
mov cx [循环次数]
s: do sth...(循环执行的代码段)
loop s
利用cx来存放循环的次数;s标记了一处地址,该地址有一条指令;执行loop指令时,首先将(cx)-1,若(cx)不为0,则跳转至s标记的地址,执行该指令。
循环执行的程序段,要写在标号和loop指令的中间。
使用loop指令计算2^12:
用debug跟踪loop指令实现的循环程序:
- 将运算结果存放到dx寄存器中时,要考虑运算的结果是否会超出该寄存器的存储范围;
- 将一个字节的内存单元赋值给寄存器时,即使数据长度不一样,但是要保证数值是相等的;不能将一个字节的内存单元直接加到16位的寄存器中(将一个字节单元存放在低8位);
- 在汇编程序中,数字不能以字母开头,即大于9FFFh的数字,要在前面加0。
调试过程:
用u命令查看被Debug加载入内存的程序,可以看到,我们的程序在076A:0000~076A:001A的内存段中。
当指令是读取内存指令时,Debug会将要访问的内存单元中的内容显示,即(ffff6)=31h。
循环的过程:
循环前将cx-1,不为0,则执行loop指令,loop指令将ip设置为0012,即要循环指令的地址处。重新执行指令。如果我们要调试的程序中循环次数很多,我们不必每一条都去跟踪,这时可以使用p或者g命令来跳过。
g 0012命令执行后,CS:0012前的程序段被一一执行。
当下一条指令是loop指令时,使用p命令来跳过循环过程,它会重复执行指令,直到(cx)=0,也可以使用g命令直接跳过(如 g 0016):
Debug和汇编编译器masm对指令的不同处理:
mov ax,[number]
在Debug程序中,表示将ds:number处的内容送到ax;而在汇编程序中,将被当作mov ax,0处理。
解决方法:
- 在汇编程序中,如果要用[number]来表示内存单元,则必须在"[]"前显示地给出段地址所在地段寄存器。
mov ax,ds:[number]
- 如果"[]"里用寄存器,那么段地址则默认在ds中。
loop和[bx]的联合应用:
我们在最开始说到寄存器提到有两个问题,类型的不匹配和结果的越界。如何解决呢?现在的方法就是利用一个16位寄存器当中介。将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到bx上,从而使两个运算对象的类型匹配且不越界。
mov al,ds:[0]
mov ah,0
mov dx,ax
如果我们要将一段连续的内存单元(0~10)写入,那我们将写出以下代码
mov al,ds:[1]
mov ah,0
mov dx,ax
// 重复的8次该段指令
.
.
.
mov al,ds:[10]
mov ah,0
mov dx,ax
更好的办法?我们不能用常量来表示偏移地址,应该将偏移地址放到bx中,用[bx]的方式访问内存。每次循环后,X递增,指向下一个内存单元。
累加 ffff:0~ffff:b的内存单元:
inc [寄存器] :使寄存器的值+1
段前缀:
段前缀的使用:DS ES
将内存ffff:0 ~ ffff:b单元中的数据送到0020:0 ~ 0020:f
思路:首先bx设置偏移地址,初始值为0,然后在cx中设置循环次数。利用dl作为两个内存单元的中转站。
因为只用到一个段地址寄存器,所以在每次循环中我们需要修改ds的值,我们可以使用两个段地址寄存器,省略需要修改段地址的程序段。
ES段地址寄存器:
- 第四章实验:[bx]和loop的使用
(1):
注意点:在向内存写入数据时,先估计数据的大小,如果小于65535,则要选择低八位的寄存器,如果大于65535,那么分别将bl和bh写入 。 由于0:200 ~ 0:23F等同于0020:0 ~0023:f,所以总共有3f即(0 ~ 63)个内存单元。那么cx的值就设置为64。
(2):
由于内存单元的序号和存入的数据同步,所以我们无需再用另外一个寄存器来存放要存入内存的数据。
// 关键
mov ds:[bx],bl
(3):
- 包含多个段的程序
在代码段中使用数据:
如何用一段存储空间存储一些我们想进行运算的数据呢?我们不能自己随便决定。现在用的方法是在程序中定义数据,之后会被编译程序,连接程序作为作为程序的一部分写到可执行文件中,当可执行文件被加载入内存时,这些数据也同时被加载进内存。
// 定义字型数据,所占内存空间大小为16B
dw number
定义的字型数据存放在代码段。调试该程序:
前16个字节是我们定义的字型数据。由于CS:IP的指向是从数据开始的,所以我们必须用debug加载后,设置IP为10h,指向程序中的第一条指令,然后执行。但是这样无法在系统中直接运行,我们可以在程序中指明程序的入口处来解决。
这样程序就会从我们编写的第一条汇编指令开始执行。
格式:
assume cs:code
code segment
.
.
数据
.
.
start:
.
.
代码
.
.
code ends
end
在代码段中使用栈:
将数据,代码,栈放入不同的段:
一段内存中同时有数据段,代码段,栈段是非常混乱的,而且大小也有限制(<64KB,8086CPU)我们应该将它们进行分段,才能更好地管理和利用。
// 我们在栈段中定义了16个字=32B,所以sp的偏移地址=20h
// 使得ss:sp指向栈顶前一个单元
mov sp,20h
// 将名称为data的段的段地址送入ax(8086CPU不允许直接将数据送入段地址寄存器),
// 用ds寄存器来存放数据段的段地址
mov ax,data
mov ds,ax
// 将名称为stack段的段地址送入ax,
// 用ss寄存器来存放栈段的段地址
mov ax,stack
mov ss,ax
程序中对段名的引用,如“data”,“stack”,“code”将被编译器处理为一个表示段地址的数值。 这里可以看出对于数据段,代码段,栈段都是我们自己定义的,在运行程序的时候再由汇编中指令指定哪里是数据段,代码段,栈段。CPU对于这些段如何处理,取决于我们程序中对CS,DS,SS等寄存器的设置。
对程序进行调试:
运行该程序,cs指向该程序的第一条指令,076B就是栈段的段地址(stack标记栈段的段地址,运行时再具体给出值)
可以看到栈段段地址已经被修改为075B,并且SP也被修改为20H。那么同理,076A就是数据段的段地址(data标记数据段的段地址,运行时候给出)
DS已经被修改为076A。我们可以查看该段地址的内容,验证该段地址是否为我们刚才自己定义的数据段段地址。
很明显是的。
- 实验五:编写,调试具有多个段的程序
(1):
- 程序返回前data中的数据不变
- CS=076CH,SS=076BH,DS=076AH
- 假设code段的段地址为X,data段的段地址为X-2,stack段的段地址为X-1
(2):
注意,我们在数据段和栈段一共申请了4个字的空间。
前三问同(1)。但是,数据段四个字节后的地址并不是栈段,而是向上取整占满了16字节,0补充其余位。
(3):
可以看到,这次分段并没有按照数据段,栈段,代码段的顺序。
- 程序返回前data中的数据不变
- CS=076AH,SS=076EH,DS=076DH
- 假设code段的段地址为X,data段的段地址为X+3,stack段的段地址为X+4
(4):
不指明程序的入口[end start 去掉start]只有第三个程序可以运行。程序运行时,CS:IP指向程序的首地址,而代码段刚好在首地址,所以可以执行。如果是数据段的话,反汇编出来的是杂乱的指令,并不能执行。
(5):
博主的瞎搞版:
博主刚开始想利c段作为栈段,可是push指令一次必须压入一个字(SP-2),而我们前面定义数据是用db指令定义的(每个操作数占有1个字节),这也会导致我们相加后压入栈中,与前面数据段中的数据不会一一对应。并且为了压栈成功,SP的指向必须设置为16。
调试过程:
红线部分及以后是我们的数据段,黄线部分是我们的栈段,之后是我们的代码段。
运行一些指令后,栈段中出现了当前指令的地址076D:0016,前面有说过,想得起来吗?
循环的过程就不赘述了,查看内存我们可以看到,数据被逆序存放了,且al的数据被存放在低地址,ah存放在高地址。总的来说,不是很好的实现。
正确版:
运行结果如下,过程不赘述。
(6):