引言
此前我们讲述强指定形式,比如在源程序中我们写:ds:[idata],这样就是告诉编译器偏移地址idata对应的是DS数据段的偏移,以免编译器发生编译错误,也就是说这是一种强指定(显式指定)。
那么我们在本篇博文中,将学到另一个概念:段前缀。
段前缀
什么是段前缀?它和我们之前提到的强指定有什么不同?
源程序内,在访问内存单元的指令中,用于显式地指明内存单元的段地址的“ds:,cs:,ss:,es:”,在汇编语言中称为段前缀。
OK,还是以ds:[idata]为例,其中“ds:”就是段前缀,因为这里是显式地指明要访问的内存段为ds数据段。
带有了段前缀的访问内存语句,CPU将会访问显式指定的段空间,而不会是使用默认的段。例如,语句:mov ax,[0],在默认情况下,CPU将会访问数据段下偏移为0的字单元数据,如果使用段前缀,写成:mov ax,es:[0],那么CPU将会访问补充段下偏移为0的字单元数据。
我们可以看出,段前缀实际上就是一种强指定,明确的指明了需要访问的是哪个段下的偏移,CPU就按照这种指定进行访问。
在Debug中,是无法使用段前缀的,如图:
会提示你格式错误。那也就是说,段前缀只能在源程序中使用,也就是给MASM编译器看的。编译器会将这种显式指定翻译成CPU能够认识的机器码,从而使CPU按照我们给定的段进行访问。
编译后的段前缀
段前缀的机器码是什么样子呢?那么现在就让我们开始编写代码来一探究竟吧。
现在新建 .asm文件,在Nodepad++中打开,编写以下代码:
assume cs:code
code segment
mov ax,2000H
mov ds,ax
mov ax,ds:[0]
mov ax,4c00H
int 21H
code ends
end
然后编译连接后,使用Debug加载程序,使用U命令查看内存中的机器码:
我们可以看到,段前缀ds: 经过编译后,变成了 [0] 形式。这里我们之前也有提到过,如果在源程序中不使用段前缀,而是mov ax,[0],那么编译后将变成 mov ax,0。具体原因我们已经在此前的博文中讲述很清楚了,有遗忘的小伙伴可以翻看进行学习。
我们已经知道了段前缀有四种,分别是:ds:、cs:、ss:、es:。现在我们换一种段前缀,将原程序中的语句:mov ax,ds:[0],修改为:mov ax,es:[0]。具体代码如下:
assume cs:code
code segment
mov ax,2000H
mov es,ax
mov ax,es:[0]
mov ax,4c00H
int 21H
code ends
end
然后我们再次编译连接加载,看下使用 es: 段前缀,会有什么不同呢:
我们可以看到,相比语句:mov ax,ds:[0],语句:mov ax,es:[0] 经过编译后,变成了两部分。其中一部分:es: 段前缀经过编译后,变成了一个单独占用一个字节的机器码:26H;剩下的则是正常语句:mov ax,[0]。即:
我们从上图中可以看出,语句:mov ax,es:[0] 是分成了两部分进行编译,段前缀 es: 和MOV语句分别单独编译。机器码 26H 就相当于是补充段ES的标记,当CPU读到机器码 26H,它就知道接下来的语句 mov ax,[0],是要访问补充段ES下的内存空间。
那么我们来思考,为什么段前缀ds: 和段前缀 es: 编译后会出现如此不同呢?
答案就是因为CPU设定的默认机制。我们都知道,如果是语句:mov ax,[0],没有指定段前缀的话,那么CPU就会默认需要访问的段是数据段DS,会取DS寄存器中的值当作段地址进行寻址。我们加上段前缀 ds:,因为CPU默认就是访问数据段DS,那么在编译时,考虑到编译效率,MASM编译器就会把段前缀 ds: 给忽视掉,不需要对它进行编译,编译结果就变成了:mov ax,[0]。
如果我们给出的段前缀不是数据段 ds:,而是其他的段前缀例如 es:,那么MASM编译器在编译时,就需要特别的将段前缀 es: 编译成对应的机器码,这样CPU才能知道要要访问的是补充段ES,而不是又按照默认的DS段进行访问了!
段前缀的应用
段前缀存在有什么意义呢?
之前我们说到它的作用是:1、保证源程序编译的准确性;2、提高了源程序可阅读性。那么,接下来我们将通过一个编程实例,来实际感受它最重要的作用。
现在给出编程题目:将内存 ffff:0~ffff:b 单元中的数据复制到内存 0:200~0:20b单元中。
现在我们来分析这个编程示例在不使用到段前缀的情况下该如何实现。首先我们想到的就是需要应用循环,需要复制12个字节,那么要循环12次。使用BX配合DS完成寻址,此外,由于是两个段地址,分别是:fffffH、0000H,那么就需要在循环中修改段地址。程序如下:
assume cs:code ; 声明代码段
code segment ; 代码段开始
mov bx,0 ; bx用来偏移,初始值为0
mov cx,12 ; 需要循环12次
s: ; 开始循环
mov ax,0ffffH ; 赋值ax为ffffH
mov ds,ax ; 设置数据段为ffffH,因为要先读取段地址ffffH下的数据
mov dl,[bx] ; 读取段ffffH下的一个字节到dl中,这里使用dl寄存器作为数据载体
mov ax,20H ; 赋值ax为0020H
mov ds,ax ; 将数据段修改为0020H,因为要写到内存空间起始地址0200H下
mov [bx],dl ; 将dl中的数据写到内存空间起始地址0200H中
add bx,1 ; 偏移加1,访问下一个字节数据
loop s ; 判断循环是否结束
mov ax,4c00H ; 程序结束
int 21H ; 程序结束
code ends ; 代码段结束
end ; 源程序结束
博主对上述每一行语句都加了注释,相信小伙伴们都能看得明白。这里有一点需要解释一下,那就是针对目标内存地址空间 0:200~0:20b,我们寻址是给DS寄存器赋值的是:20H。为什么会是20H,而不是0H呢?
这是因为我们要使用的是同一个偏移地址BX。如果按照地址 0:200 来设置,那么DS寄存器将设置为0,BX寄存器需要为200H,而读取源地址为 ffffH:0,BX寄存器需要为0H。按照我们源程序中,因为BX需要在循环中进行累计来移动偏移位置,所以就要求偏移保持一致,那么对于目的地址 0:200H来说,访问它的偏移也要从0开始才行,而不是200H。
我们之前的博文中就说过,同一个目的地址可以由多个地址进行表示。0:200H 表示的目的地址为 0X16+200H=200H,200H也可以使用20HX16+0H来表示。所以,当我们将段地址设置为20H的时候,这时它的偏移位置就是0H,就和读取源地址的偏移保持了一致。
这段程序小伙伴们可以自行编译连接在Debug中调试,这里我们思考这段源程序的执行效率问题。我们观察这段程序可以看到,为了实现数据从源地址复制到目的地址,在单次循环中我们需要两次给DS数据段赋值,12次循环我们将会赋值24次。当然,这样做是正确的,可以实现我们的目的,但是在开发中我们经常强调的就是代码优化,以达到更高的执行效率。显然,上述源程序并不是一个效率很高的设计。
那么,我们该怎么办才能实现高效率复制数据呢?
答案就是使用段前缀!
我们通过分析可以得出,影响执行效率的主要原因是因为我们不得不要在循环内不停的对DS寄存器赋值,如果我们不需要在循环内修改源地址和目的地址,那么效率不就上去了么!
首先,有两处内存地址,一个是读取的源地址:ffffH:0H,一个是写入的目的地址:20H:0H。我们使用DS寄存器来存放源地址的段地址 ffffH,然后使用ES寄存器来存放目的地址的段地址 20H,在循环中,通过段前缀,来指定当前需要访问的是源地址(数据段DS),还是目的地址(补充段ES)。
确定好了优化方案,那么我们就来修改以下源程序,修改后如下:
assume cs:code
code segment
mov ax,0ffffH
mov ds,ax ; 使用DS保存源地址的段地址
mov ax,20H
mov es,ax ; 使用ES保存目的地址的段地址
mov bx,0 ; 偏移从0开始
mov cx,12
s:
mov dl,ds:[bx] ; 指定读取源地址下的一个字节数据放入到dl寄存器中
mov es:[bx],dl ; 将dl寄存器中的数据写入到指定的目的地址下一个字节
add bx,1
loop s
mov ax,4c00H
int 21H
code ends
end
从上面的代码中我们可以看到,将两处地址的段地址赋值放在了循环外,循环内直接使用段前缀进行访问,这样就避免了循环内重复设置段地址导致的效率低下问题。
我们将上述源程序编译连接,使用Debug加载,进行调试,来看下程序是否达到了我们的目的要求:
我们直接使用P命令执行完循环后,使用D命令查看两处的内存空间数据,可以看到,程序成功的将内存 ffff:0H~ffff:bH 之间的数据复制到了 0:200H~0:20bH下,证明使用段前缀在提高程序运行效率的同时也达到了相应的目的。
这就是段前缀一个最重要的作用,在后面的学习中我们还会遇到更多使用段前缀的编程实例。这里还要说明另外一点,那就是ES段寄存器。
我们现在对DS段寄存器、CS段寄存器、SS段寄存器都比较熟悉了,也知道他们三个的作用和使用方法。对于ES段寄存器,我们实际并未说太多,上面的编程实例中算的是ES首次使用。
ES段寄存器,它存放的是补充段的段地址,什么是补充?当某样东西不够用了,有新的东西可以补充进来当作替代,所以ES存在的作用就是应对段寄存器不够使用的情况。例如上面的编程实例中,有两个数据段,显然对于DS段寄存器来说,是不够使用的。我们当然可以通过降低运行效率的方式来达到使用一个DS来同时访问两个数据段的目的,但是并不是可取的。
那么作为候补选手,ES段寄存器自然就派上了用场。在上面的编程实例中,可以将ES段寄存器视为和DS段寄存器有相同功能,当然也可以配合BX完成内存寻址。这样算下来,BX的合作对象其实不止DS一个,还有ES。
本篇结束语
在本篇博文中,我们学习了段前缀的意义和它的作用,通过编程示例来感受到了段前缀对于提高程序运行效率上的作用。此外,我们学习了解了ES段寄存器,作为替补选手,虽然不如DS、CS它们那么的光芒耀眼,但是它在某些场合下承担着救火员角色,发挥着不可缺少的作用!
那么在下篇博文中,我们将开始学习在源程序中如何使用多个段。
感谢围观,转发分享请标明出处,谢谢!