0
点赞
收藏
分享

微信扫一扫

王爽 汇编(上)

程序员伟杰 2022-04-21 阅读 52
  • 前置知识

关于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-2pop指令时,栈地址从低到高,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指令实现的循环程序:
在这里插入图片描述

  1. 将运算结果存放到dx寄存器中时,要考虑运算的结果是否会超出该寄存器的存储范围;
  2. 将一个字节的内存单元赋值给寄存器时,即使数据长度不一样,但是要保证数值是相等的;不能将一个字节的内存单元直接加到16位的寄存器中(将一个字节单元存放在低8位);
  3. 在汇编程序中,数字不能以字母开头,即大于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处理。
解决方法:

  1. 在汇编程序中,如果要用[number]来表示内存单元,则必须在"[]"前显示地给出段地址所在地段寄存器。mov ax,ds:[number]
  2. 如果"[]"里用寄存器,那么段地址则默认在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):
在这里插入图片描述
在这里插入图片描述

  1. 程序返回前data中的数据不变
  2. CS=076CH,SS=076BH,DS=076AH
  3. 假设code段的段地址为X,data段的段地址为X-2,stack段的段地址为X-1

(2):
在这里插入图片描述
注意,我们在数据段和栈段一共申请了4个字的空间。

在这里插入图片描述
前三问同(1)。但是,数据段四个字节后的地址并不是栈段,而是向上取整占满了16字节,0补充其余位。

(3):
在这里插入图片描述
可以看到,这次分段并没有按照数据段,栈段,代码段的顺序。
在这里插入图片描述

  1. 程序返回前data中的数据不变
  2. CS=076AH,SS=076EH,DS=076DH
  3. 假设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):
在这里插入图片描述在这里插入图片描述

举报

相关推荐

0 条评论