汇编学习教程:一些零碎知识补充

引言

学习到现在,我们已经进入到汇编开发的10%了,回顾我们之前讲述的知识,还有一些比较零散的知识点需要给大家补充一下。这些知识点严格上来说,学与不学并不会影响到我们后续的汇编开发学习,但是了解了会增加我们对底层开发的理解和认知,总体来说还是有些许帮助的。

那么,本片博文中,我们将主要讲解以下知识点:

1、程序的装载

2、内存中的安全空间

现在就让我们开始学习吧~

装载程序

还记得我们学习的第一个汇编源程序 s1.exe吗?我们是如何执行它的呢?

我们是在DOSBox虚拟的DOS系统一个黑窗口里,直接输入 s1.exe 进行了执行:如下图:

那么我们就可以说,是这个“黑窗口”完成了 s1.exe 可执行程序的装载、运行工作。那么,这个黑窗口的真身是什么呢?

操作系统的外壳

众所周知,一个操作系统是由多个功能模块组成的庞大复杂的软件系统,本质上来说是由一个个子程序组成的复合程序。任何一个操作系统都需要对外界开放一个可操作可处理的接口,接口由一个单独的程序进行管理,这程序被称为shell(外壳),用户通过这个程序便可以操作计算机系统进行工作。我们现在的Windows10系统,大家都熟悉的命令提示符就是它的shell程序。

DOS系统内也有对应的shell程序:command.com,这个程序在DOS中称为命令解释器,也就是我们上面寻找的“黑窗口”真身。

DOS系统启动时,先完成一些重要的初始化工作,然后便会启动command.com,command.com运行后便会开启一个背景黑色的窗口,窗口内显示出由当前盘符和当前路径组成的提示符,比如上面图片中的“C:\>”,然后等待用户的输入。

用户可以输入所要执行的命令,比如 cd、dir等,这些命令会由command去执行,command执行完成后,会显示出执行结果,并再次显示出当前盘符和当前路径组成的提示符,等待用户的再次输入。例如我们执行 dir,如下图所示:

 现在我们说执行程序的过程。如果用户想执行一个程序,则需要输入该程序的可执行文件名,command首先根据文件名找到可执行文件,然后将这个可执行文件中的程序加载进内存中,并设置 CS:IP 指向程序的入口。由于 CS:IP 已经被修改为程序的入口,所以command程序便会暂停运行,CPU便开始运行刚才被加载进内存中的程序。程序运行结束后,返回到command程序,command便再次显示出当前盘符和当前路径组成的提示符,等待用户的再次输入。

整体流程如下:

图中的流程相信小伙伴们都会看的很清楚了,那么现在我们可以准确的说,是又command.com完成了 s1.exe 可执行程序的装载、运行工作。

程序加载过程

下面我们就来看看DOS系统中 .exe文件中的程序的加载过程。在说这个之前,我们先聊一个有趣的现象。

我们已经学过了使用Debug加载程序,并对其进行调试和追踪,就是不知道大家有没有注意到,Debug在没有加载程序的时候,相比较Debug加载了程序的时候,此时的寄存器会发生了什么变化呢?

如下图所示,这是Debug没有加载程序的时候:

 这个则是Debug加载了程序的时候:

现在我们一起找不同,来比较两张图中都是什么地方发生了变化。

从两张图中的对比,我们可以发现以下不同:

AX:0000H  ->  FFFFH

CX:0000H  ->  000FH

SP:00FDH  ->  0000H

DS:073FH  ->  075AH

ES:073FH  ->  075AH

SS:073FH  ->  0769H

CS:073FH  ->  076AH

IP:0100H  ->  0000H

我们思考为啥那么这些寄存器的值会发生如此变化呢?

首先,CX变化为000FH,这个有什么意义呢?仔细思考会发现,FH是我们 s1.exe中程序的长度,那么我们也就明白了,Debug在加载 s1.exe可执行程序时,要使用到循环来处理程序代码(比如复制代码到指定内存空间),CX设置为需要操作的代码字节长度,所以我们就可以看到CX中值对应了程序长度。

AX中数值的变化则证明Debug在加载程序时使用到了AX寄存器来做某样操作,我们现阶段还不清楚是做了什么样的操作,但是我们可以从数值 FFFFH进行一些合理猜想。我们知道,在汇编开发中我们一般使用AX寄存器来给各种段寄存器赋值,比如DS段寄存器等。假设这里它是给DS赋值,那么我们可以得知寻址的起始是在FFFFH:BX下,之前我们已经讲过,在8086PC机中的内存地址空间分配,C0000H~FFFFFH,这段内存空间是属于各类ROM地址空间。我们需要加载的 s1.exe文件,是放在硬盘中,所以这里应该是Debug读取硬盘中s1.exe文件,AX值设置为硬盘的ROM地址FFFFH。

下面我们来看四个段寄存器的变化,他们四个的变化是我们需要重点关注的地方。

首先,在未加载程序的时候,它们四个都是 073FH,在加载了s1.exe程序后,DS、ES变成了:075AH,而CS变成了:076AH,SS变成了:0769H。

要想明白为啥那么会发生如此变化,我们需要先了解另外一个东西:PSP。

PSP

什么是PSP?PSP是一个被称为程序段前缀的数据区,这个数据区是一个固定的大小为:100H。它的作用就是:DOS系统要利用PSP来和被加载的程序进行通信。你可以理解为是程序与操作系统之间沟通的桥梁。

那么,.exe文件中的程序加载过程如下:

1、首先,系统找到一段空闲的内存空间,并且该控件容量足够大,可以完整放进待加载的程序。我们假设这段空闲的内存空间起始地址为:SA:0,如下图:

2、在这段内存空间的前100H个字节中,创建PSP。如下图:

3、从这段内存空间的100H字节处开始,将程序装入,程序的地址为:SA+10H : 0H。如下图:

为了更好的区分PSP与程序,DOS一般将他们划分为不同的段,所以程序起始地址只能使用 SA+10H : 0H 来表示,而不能使用 SA:100H。那么就有了这样的地址安排:SA:0 是PSP区,SA+10H:0 是程序区。

4、将该内存的段地址放入DS段寄存器中,初始化其他相关寄存器后,设置CS:IP指向程序的入口。如下图:

至此加载过程结束。

那么回到我们之前探讨的问题:段寄存器的变化。了解完上述的加载过程,那么现在我们可以给出答案了。

Debug工具实际上也是一个程序,那么它被系统加载时也是符合上述加载过程的。Debug未加载程序时,我们发现四个段寄存器值都为:073FH,在上述过程4中,段地址放入DS寄存器中,所以我们可以作为对应,SA=073FH。这里有问题的是,为什么CS值也会是073FH呢?按照上述过程,CS值应该为SA+10H,也就是 074FH才对呀!为什么和DS保持了一致?!为什么不是 074FH,是因为此时IP的值为:100H。地址073FH:100H 和地址 074FH:0H 是同一地址。

这个当然是Debug做的,Debug的程序代码是在 074FH:0开始加载进内存中,Debug运行结束后,由于此时并没有调试程序,所以Debug会重新把CS:IP指向本身代码开始处,CS和DS保持了一致,自然IP值就要为 100H。

有的小伙伴可能说,多说无益证据为重,那么怎么证明我们说的是对的呢?有证明。已知PSP的头两个字节是:CD、20,上述中PSP开头地址为:073FH:0H,我们查看一下这个地址的数据不就可以证明了么。如下图:

 

可以看到 073FH:0H地址确实是PSP的起始地址,证明我们说的是准确的。 

当我们使用Debug加载了可执行文件s1.exe,则贴合了上述加载过程。起始地址为075AH,则CS值就要为075AH+10H=076AH,而此时IP值为0H。我们查看下地址:075AH:0H 下的数据:

 

开头是PSP的头两个字节,证明 075AH:0H 是PSP的起始地址。有眼尖的小伙伴会发现,这两个PSP区0~F间的数据,除了偏移2、3下的字节数据不同外,其余相同。那是因为加载的程序不相同,一个是command.com加载的Debug,一个是Debug加载的s1.exe。

现在还剩下SS段寄存器的变化,加载s1.exe后,我们看到此时栈顶指向:0769H:0H。要知道,我们在s1.exe中并未设置一个栈,所以加载后则由系统默认分配栈空间。我们知道,076AH:0H为程序开始, 程序总长度为FH,程序空间为:076AH:0H~FH。由于SP起始为0H,那么实际上栈底地址为:0769H:FFFFH。为什么栈底是 076AH:FFFFH,有不理解的小伙伴可以参考之前我们讲述栈方面的博文,具体原因这里不再表述。所以我们可以得出栈空间大小:0769H:FFFH-076AH:FH=FFE0H。

安全空间

我们现在已经知道,对于8086CPU来说,内存就好比是一个长度为FFFFFH的数组,每一个下标对应一个字节的数据。CPU对这条“数组”的不同区域做出了划分,参考我们之前说过的内存空间分配。

既然内存中不同的区域都有严格的划分,那么也就是说,我们在开发过程中,是绝对不能对内存中的数据进行随意的读写,因为这块内存中可能存放着重要的系统数据或者代码。一旦改写,可能就会发生灾难性的后果。

之前我们讲述的一些代码中,比如向2000H:0H下写入数据,如果2000H:0H下存放的有系统数据,那么该数据就会被改写掉,导致系统崩溃。

下面我们举个例子来感受一下随便向内存中写入数据,将会带来什么样的后果。代码如下:

assume cs:code  

code segment
	mov ax,0
	mov ds,ax
	mov ds:[26H],ax
	
	mov ax,4c00H
	int 21H
code ends
end

代码中,我们将修改内存地址 0:26 下的数据为0,那么该段代码运行后将会发生什么呢?我们来编译连接运行一下:

看样子是正常运行了,也没出现页面崩溃什么的。正当你怀疑到底行不行的时候,你抬手按键想输入命令,你就会惊诧的发现,你已经没办法输入什么数据了!

这是怎么回事?!只不过是修改了 0:26 下的一个字节数据而已,竟然导致我们的键盘失灵,无法向窗口中输入数据!

导致这个问题的原因正是因为我们改了区区一个字节导致的。因为0:26 下存放的是中断向量表,系统依靠这个中断向量表才能正确的处理各种中断。我们按下键盘,就是一个中断的产生和处理,现在由于我们修改了中断向量表,导致系统无法正确的处理按键中断,自然也就键盘失灵了!

要想恢复,那么我们就需要重新启动系统了,让系统重新写入中断向量表。我们现在使用的是DOSBox,也就是重启DOSBox即可恢复。

小伙伴可自行进行实际操作感受,确切明白向内存中随意写入数据的危害。

那么什么是安全空间呢?安全空间就是说,在这个空间内,你可以随便的进行写入读取,而不用担心会导致系统崩溃或者其他灾难问题

为什么需要安全空间

我们的电脑是由操作系统进行管理,包括内存资源也是由操作系统统一分配。如果我们想要安全的编程开发,那么就需要在操作系统指定的方式下才能做到合规编程。

但是不要忘记,我们学习的是汇编语言,就是要通过它获得底层的编程体验,理解计算机底层的基本原理。所以我们需要直接对硬件编程,而不需要理会操作系统。我们是想在操作系统内安全、规矩的编程,还是使用汇编语言自由自在的直接操作硬件,了解那早已被层层系统软件掩盖的真相呢?答案当然是后者。你可以理解为我们现在编写的汇编程序就是一个病毒,不受操作系统的节制,直接对硬件层作用。

所以,我们就需要一个安全空间,可以让我们自由操作内存,避免与操作系统发生矛盾。那么这个安全空间就是:0:200~02FF 共256个字节空间。

一般的PC机,在DOS方式下,DOS和其他合法程序一般都不会使用 0:200~0:2FF 这段内存空间,所以我们使用这段空间是安全的。那么为什么是这段空间?其实是因为这段空间是系统预留出来的用来放中断向量表的空间。为谁预留的?是考虑到电脑可能有新增加的外接设备存在,有新增的外接设备中断向量就放在这些预留的空间内,没有新增的外接设备那么就不会使用。

这就是为什么说我们在强调“一般情况”,这段空间可能还是会可能会被占用的。所以建议在使用之间,查看一下 0:200~0:2FF 内存空间是否有被占用的情况,这样安全一些。

在接下来我们也会学习如何在汇编开发中向操作系统申请一段内存来使用,当然这样做不是向操作系统妥协,而是和操作系统和平相处啦。

实验4

现在我们讲解实验4中的一道编程题目:向内存 0:200~0:2FF 依次传送数据 0~63,程序中只能使用9条指令,9条指令中包括“mov ax,4c00H”和“int 21H”。

也就是说,我们需要使用7条指令来完成操作。该如何实现逻辑?

首先我们先确定段地址为:20H;然后需要循环64次;在每一次循环内,将 [bx] 赋值为bx的值,bx循环内累加。那么我们来编码实现:

assume cs:code  

code segment
    mov ax,20H
	mov ds,ax       ; 设置段地址为 20H
	
    mov cx,64       ; 0~63 共64个数字,要循环64次
    mov bx,0        ; 偏移从0开始

  s:	
	mov ds:[bx],bl  ; 传输的是字节,所以要使用 BL寄存器
	add bx,1        ; BX值加1
	loop s          ; 判断循环是否结束
	
	mov ax,4c00H
	int 21H
code ends
end

代码整体并没有太大的难度,唯一要注意的一点是:MOV ds:[bx],bl。因为要求的是给字节赋值,所以我们一定要使用BL寄存器来传输。另外,由于最大值 3F<FF,所以我们 add bx,1,也就相当于是 add bl,1。

我们编译连接运行,查看内存中值:

实现了题中要求。那么接下来我们思考,可不可以换一种方式来实现呢?我们不对BX进行累加,只用他进行寻址,那么该怎么实现?我们直接看代码:

assume cs:code  

code segment
    mov ax,20H
	mov ds,ax      
	
    mov cx,63      

  s:	
	mov bx,cx
	mov ds:[bx],cl
	loop s
	
	mov ax,4c00H
	int 21H
code ends
end

这里博主没有加注释,那么我们直接来分析上述代码实现。与第一段代码相比,这段代码主要将目光投向了CX,利用Loop指令中CX自减,来倒序赋值指定空间段。

1、CX设置为63,因为我们是倒序赋值,也就是从 0:23F开始赋值,所以CX只能赋值为63次。循环63次也就意味着最后 0:200 该处字节不会在循环内赋值,不过无所谓,我们知道 0:200~0:2FF 这段空间都是0,0:200 该处字节就已经是0了,也就不需要给它赋值。

2、由于 [...] 只能由BX承担,所以为了寻址,每次循环我们需要将此时的CX赋值给BX。而CX会在每次的Loop指令中减1,这也就实现了倒序的寻址

我们对比两个代码,第一个是正序赋值,第二个是倒序赋值。相比较指令数量,第二个代码指令还要更少一些,而且循环内少做了一条逻辑运算,相当于少做了63次加法,在运行效率上,自然要比第一个代码要更好。

博主这里只想告诉大家的是,在提高运行效率这个亘古不变的话题上,往往只有打破思维定势,打破常规,才能得到更好的代码!

第3题

这道题是要求我们补全代码,然后运行调试。题目要求:将“mov ax,4c00H” 之前的指令复制到 0:200处,代码如下:

assume cs:code  

code segment
    mov ax,___
	mov ds,ax
	mov ax,0020H
	mov es,ax
	
	mov bx,0
	mov cx,___

  s:	
	mov al,[bx]
	mov es:[bx],al
	inc bx
	loop s
	
	mov ax,4c00H
	int 21H
code ends
end

我们可以看到,我们需要填写两处代码,一个是AX的赋值,一个是CX的赋值。我们先来看AX处的赋值,可以看到下面两条指令是设置复制的目的段地址:20H,那么此处要我们填写的就是复制源的段地址。这个地址是谁?看题目,复制的是代码数据,所以毫无疑问,复制源的段地址就是代码段的段地址,也就是CS啦!所以这处我们填写:CS。

第二处是给CX赋值,结合下面的循环我们知道此处要填写循环次数。那么要循环多少次?我们看循环体中,是字节数据的传输,那也就是说,循环的次数要等于待复制的代码长度。好,问题来了我们该怎么知道代码长度是多少?

就目前来说,我们需要借助Debug加载程序,通过查看内存中代码才能知道代码的长度是多少~所以我们先要将上述程序加载进内存。为了通过编译,我们需要先将空缺补上:mov ax,cs,CX那里我们先填上0,即:mov cx,0。

编译连接,使用Debug加载后,用U命令查看当前内存中的代码:

从图中我们就可以一眼看出,指令“mov ax,4c00H”之前的代码长度是:17H。有的小伙伴可能认为长度是:15H,因为 076A:17处就是最后一句代码了,所以是15H。这是错误的,因为076A:15是语句Loop开始的地址,不是结束的地址。我们要复制“mov ax,4c00H”前的代码,自然是偏移截止到语句“mov ax,4c00H”开始的位置,即:17H

现在我们已经知道了CX需要设置的值是 17H,那么代码补全如下:

assume cs:code  

code segment
    mov ax,cs
	mov ds,ax
	mov ax,0020H
	mov es,ax
	
	mov bx,0
	mov cx,17H

  s:	
	mov al,[bx]
	mov es:[bx],al
	inc bx
	loop s
	
	mov ax,4c00H
	int 21H
code ends
end

我们编译连接,使用Debug加载执行,执行完毕后使用U命令查看0:200处内存:

 ok,我们确实将代码复制到了0:200,证明我们填补的代码没有问题,是正确的。

本篇结束语

本篇是目前为止码字最多的,,,本本片中我们讲述了程序加载流程,探讨了Debug加载程序与不加载程序的寄存器变化。了解了安全空间相关的知识,并对实验4中的题目进行了讲解。

下篇我们将开始学习编程中使用多个段,比如使用DS段、SS段,期待我们的学习~

感谢围观,转发分享请标明出处,谢谢!

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值