《汇编语言》第6章 包含多个段的程序

前面的程序中,只有一个代码段。现在有一个问题是,如果程序需要用其他空间来存放数据,使用哪里呢?第5章中,我们讲到要使用一段安全的空间,呆哪里安全呢?第5章中,我们说0:200~0:2FF是相对安全的,可这段空间的容量只有256个字节,如果我们需要的空间超过256个字节该怎么呢?
在操作系统的环境中,合法地通过操作系统取得的空间是安全的,因为操作系统不会让一个程序所用的空间和其他程序以及系统自己的空间相冲突。在操作系统允许的情况下,程序可以取得任意容量的空间。
程序取得所需空间的方法有两种,一是在加载程序的时候为程序分配,再就是程序在执行的过程中向系统申请。这里我们只讨论第一种方法。
加载程序的时候为程序为程序分配空间,我们在前面已经有所体验,比如我们的程序在加载的时候,取得了代码的存储空间。
我们若要一个程序在被加载的时候取得所需的空间,则必须要在源程序中做出说明。我们通过在源程序中定义段来进行内存空间的获取。
上面是从内存空间获取的角度上,谈定义段的问题。我们再从程序规划的角度来谈一下定义段的问题。大多数有用的程序,大多数有用的程序,都要处理数拓,使用栈空间,当然也都必须有指令,为了程序设计上的清晰和方便,我们一般也都定义不同的段来存放它们。
对于使用多个段的问题,我们先简单说到这里,下面我们将以这样的顺序来深入讨论多个段的问题:
(1)在一个段中存入数据、代码、栈,我们先来体会一下不使用多个段时的情况
(2)将数据、代码、栈放入不同的段中。

6.1 在代码段中使用数据
考虑这样一个问题,编程计算以下8个数据的和,结果存在ax寄存器中:
0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
在前面的课程中,我们都是累加某些内存单元中的数据,并不关心数据本身。可现在要累加的就是已经给定了数值的数据。我们可以将它们一个一个地加到ax寄存器中,但是,我们希望可以用循环的方法来进行累加,所以在累加前,要将这些数据存储在一组地址连接的内存单元中。如何将这些数据存储在一组地址连续的内存单元中呢?我们可以用指令一个一个地将它们送入地址连续的内存单元中,可是这样又有一个问题,到哪里去找这段内存空间呢?
从规范的角度来讲,我们是不能自己随便决定哪段空间可以使用的,应该让系统来为我们分配。我们可以在程序中,定义我们希望处理的数据,这些数据就会被编译,连接程序作为程序的一部分写到可执行文件中。当可执行文件中的程序被加载入内存时,这些数据也同时被加载入内存中。与此同时,我们要处理的数据也就自然而然地获得了存储空间。
个体的做法看下面的程序。
程序6.1

assume cs:codesg
codesg segment
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987
    mov bx,0
	mov ax,0
	mov cx,8
	
s:	add ax,cs:[bx]
	add bx,2
	loop s
	
	mov ax,4c00H
	int 21H
codesg ends
end

解释一下,程序第一行中的"dw"的含义是定义字型数据。dw即“define word”在这里,使用dw定义了8个字型数据(数据之间以逗号分隔),它们所占的内存空间的大小为16个字节。
程序中的指令就要对这8个数据进行累加,可这8个数据在哪里呢?由于它们在代码段中,程序在运行的时候CS中存放代码段的段地址,所以可以从CS中得到它们的段地址。它们的偏移地址是多少呢?因为用dw定义的数据处理于代码段的最开始,所以偏移地址为0.它们的偏移地址是多少呢?因为用dw定义的数据处理于代码段的 最开始,所以偏移地址为0,这8个数据就在代码段的偏移0,2,4,6,8,A,C,E处。程序运行时,它们的地址就是CS:0、CS:2、CS:0、CS:4、CS:6、CS:8、CS:0、CS:A、CS:C、CS:E。
程序中,用bx存放加2递增的偏移地址,用循环来进行累加。在循环开始前,设置(bx)=0,cs:bx指向第一个数据所在的字单元。每次循环中(bx)=(bx)+2,cs:bx指向下一个数据所在的字单元。
将程序6.1编译,连接为可执行文件t6_1.exe,用debug加载情况如下面所示:

 在上图中,通过"DS=075A",可知道程序从076A:0000开始存放。用u命令从076A:0000查看程序,却看到了一些让人读不懂的指令。
为什么没有看到程序中的指令?实际上用u命令从076A:0000查看到的也是程序中的内容,只不过不是源程序中的汇编指令所对应的机器码,而是源程序中,在汇编指令前面,用dw定义的数据。实际直,在程序中,有一个代码段,在代码段中,前面的16个字节是用"dw"定义的数据,从第16个字节开始才是汇编指令所对应的机器码。
可以用d命令更清楚地查看一下程序中前16个字节的内容,如下图所示。

可以从076A:0010查看程序中要执行的机器指令,如下图所示:

从上面的图中,我们可以看到程序加载到内存中后,所占内存空间的前16个单元存放在源程序中用“dw”定义的数据,后面的单元存放源程序中汇编指令所对应的机器指令。
怎样执行程序中的指令呢?用debug加载后,可以将IP设置为10h,从而使CS:IP指向程序中的第一条指令。然后再用t 命令、p命令,或者是g命令执行。
可是这样一来,我们就必须用debug来执行程序。程序6.1编译,连接成可执行文件后,在系统中直接运行可能会出现问题,因为程序的入口处不是我们所希望执行的指令。如何让这个程序在编译、连接后可以在系统中直接运行呢?我们可以在源程序中指明程序的入口所在,具体做法如下。
程序6.2

assume cs:codesg
codesg segment
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
start:	mov bx,0
		mov ax,0
	
		mov cx,8
	
	s:	add ax,cs:[bx]
		add bx,2
		loop s
	
		mov ax,4c00H
		int 21H
codesg ends
end start

注意在程序6.2中加入的新内容,在程序的第一条指令的前面加上了一个标号start,而这个标号在伪指令end的后面出现。这里,我们要再次探讨end的作用。end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。在程序6.2中我们用end指令指明了程序的入口在标号start处,也就是说,“mov bx,0”是程序的第一条指令。
在前面的课程中(参见4.8节),我们已经知道在单任务系统中,可执行文件中的程序执行过程如下。
(1)由其他的程序(debug, command或其他程序)将可执行文件中的程序加载入内存;
(2)设置CS:IP指向程序的第一条要执行的指令(即程序的入口),从而使程序得以运行;
(3)程序运行结束后,返回到加载者。
现在的问题是,根据什么设置CPU的CS:IP指向程序的第一条要执行的指令?也就是说,如何知道哪一条指令是程序的第一条要执行的指令?这一点,是由可执行文件中的描述信息指明的。 我们知道可执行文件由描述信息和程序组成,程序来自于源程序中的汇编指令和定义的数据;描述信息则主要是编译、连接程序对源程序中相关伪指令进行处理所得到的信息。我们在程序6.2中,用伪指令end描述了程序的结束和程序的入口。在编译、连接后,由“end start”指明的程序入口,被转化为一个入口地址,存储在可执行文件的描述信息中。

在程序6.2生成的可执行文件中,这个入口地址的偏移地址部分为:10H。当程序被加载入内存之后,加载者从程序的可执行文件的描述信息中读到程序的入口地址,设置CS:IP。这样CPU就从我们希望的地址处开始执行。
归根结底,我们若要CPU从何处开始执行程序,只要在源程序中用“end 标号”指明就可以了。
有了这种方法,就可以这样来安排程序的框架:

assume cs:code
code segment
        :
        :
        数据
        :
        :
start:
        :
        :
        代码
        :
        :
code ends
end start

6.2 在代码段中使用栈
完成下面的程序,利用栈,将程序中定义的数据逆离存栈。
assume cs:codesg
codesg segment
    dw 0123h,0456,0789h,0abch,0defh,0fedh,ocbah,0987h
    ?
codesg ends
end
程序的思路大致如下。
程序运行时,定义的数据存放在cs:0~cs:F单元中,共8个字单元。依次将这8个字单元中的数据入栈,然后再依次出栈到这8个字单元中,从而实现数据的逆序存放。
问题是,我们首先要有一段可当作栈的内存空间。如前所术,这段空间应该由系统来分配。可以在程序中通过定义数据来取得一段空间,然后将这段空间当作栈空间来用。程序如下。
程序6.3

assume cs:codesg
codesg segment
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
	;用dw定义16个字型数据,在程序加载后,将取得16个字的内存空间,
	;存放这16个数据。在后面的程序中将这段空间当作栈来使用
start:	mov ax,cs
		mov ss,ax
		mov sp,30h		;将设置栈顶ss:sp指向cs:30
	
		mov bx,0
		mov cx,8
	s:	push cs:[bx]
		add bx,2
		loop s			;以上将代码段0~15单元中的8个字型数据依次入栈
		
		mov bx,0
		mov cx,8
	
	s0:	pop cs:[bx]
		add bx,2
		loop s0			;以上依次出栈8个字型数据到代码段0~15单元中
	
		mov ax,4c00H
		int 21H
codesg ends
end start				;指明程序的入口在start处

注意程序6.3中的指令:
mov ax,cs
mov ss,ax
mov sp,30h
我们要将cs:10~cs:2F的内存空间当作栈来用,初始状态为空,所以ss:sp要指向栈底,则设置ss:sp指向cs:30。如果对这点还有疑惑,建议回头认真复习一下第3章。


在代码段中定义了16个字型数据,它们的数值都是0.这16个字型数据的值是多少,对程序来说没有意义。我们用dw定义16个数据,即在程序中写入了16个字型数据的值是多少,对程序来说没有意义。我们用dw定义16个数据,即在程序中写入了16个字型数据,而程序将它用作栈空间。可见,我们定义这些数据的最终目的是,通过它们取得一定容量的内存空间。所以我们在描述dw的作用时,可以说用它定义数据,也可以说用它开辟内存空间。比如对于:
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
可以说,定义了8个字型数据,也可以说,开辟了8个字的内存空间,这段空间中每个字单元中的数据依次是:0123h、0456h、0789h,0abch,0defh,0fedh,0cbah,0987h。因为它们最终的效果是一样的。

检测点6.1
(1)下面的程序实现依次用内存0:0~0:15单元的内容改写程序中的数据,完成程序:

assume cs:codesg
codesg segment
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
start:	mov ax,0
		mov ds,ax
		mov bx,0
		mov cx,8
	
	s:	mov ax,[bx]
		mov cs:[bx],ax	;把0:[bx]的内容复制到cs:[bx]的内容
		add bx,2
		loop s
	
		mov ax,4c00H
		int 21H
codesg ends
end

程序调试:

 

 

(2)下面的程序实现依次用内存0:0~0:15单元中的内容改写程序中的数据,数据的传送用栈来进行。栈空间设置在程序内。完成程序:

assume cs:codesg
codesg segment
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
	dw 0,0,0,0,0,0,0,0,0,0		;10个字单元用作栈空间
start:	mov ax,cs
		mov ss,ax		;设置栈的段地址
		mov sp,24h		;设置栈的偏移地址
		
		mov ax,0
		mov ds,ax
		mov bx,0
		mov cx,8
    
	s:	push [bx]		;把0~15个单元ds:[bx]的数据压入栈中
		pop cs:[bx]		;从栈中把数据取出放放cs:[bx]中
		add bx,2
		loop s
	
		mov ax,4c00H
		int 21H
codesg ends
end start

程序调试

 

 

 

 验证OK

6.3 将数据、代码、栈放入不同的段
在前面的内容中,我们在程序中用到了数据和栈,将数据、栈和代码都放到了一个段里面。我们在编程的时候要注意何处是数据,何处是栈,何处是代码。这样做显然有两个问题:
(1)把它们放到一个段中使程序显得混乱;
(2)前面程序中处理的数据很少,用到的栈空间也小,加上没有多长的代码,放到一个段里面没有问题,但如果数据、栈和代码需要的空间超过64KB,就不能放在一个段中(一个段的容量不能大于64KB,是我们在学习中所用的8086模式的限制,并不是所有的处理器都这样)。
所以,应该考虑用多个段来存放数据、代码和栈。
怎样做呢?我们用和定义代码段一样的方法来定义多个段,然后在这些段里面定义需要的数据,或通过定义数据来取得栈空间。具体做法如下面的程序所示,这个程序实现了和程序6.3一样的功能,不同之处在于它将数据、栈和代码放到了不同的段中。
程序6.4

assume cs:codesg,ds:data,ss:stack

data segment
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends

stack segment
	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends

codesg segment
start:	mov ax,stack
		mov ss,ax
		mov sp,20h		;设置栈顶ss:sp指向stack:20
		
		mov ax,data
		mov ds,ax		;ds指向data段
		
		mov bx,0		;ds:bx指向data段中的第一个单元
		mov cx,8
		
	s:	push [bx]
		add bx,2
		loop s			;以上将data段中的0~15单元中的8个字型数据依次入栈
	
		mov bx,0
		mov cx,8
		
	s0:	pop [bx]
		add bx,2
		loop s0			;以上依次出栈8个字型数据到data段的0~15单元中 
	
		mov ax,4c00H
		int 21H
codesg ends
end start

调试

 

 

 

 

 

 

下面对程序6.4做出说明。
(1)定义多个段的方法
这点,我们从程序中可明显地看出,定义一个段的方法和前面所讲的定义代码段的方法没有区别,只是对于不同的段,要有不同的段名。
(2)对段地址的引用
现在,程序中有多个段了,如何访问段中的数据呢?当然要通过地址,而地址是分为两部分的,即段地址和偏移地址。如何指明要访问的数据的段地址呢?在程序中,段名就相当于一个标号,它代表了段地址。所以指令“mov ax,data”的含义就是将名称为“data”的段的段地址送入ax。一个段中的数据的段地二可由段名代表,偏移地址就要看它在段中的位置了,程序中“data”段中的数据“0abch”的地址就是:data:6。要将它送入bx中,就要用如下的代码:
mov ax,data
mov ds,ax
mov bx,ds:[6]
我们不能用下面的指令:
mov ds,data
mov bx,ds:[6]
其中指令“mov ds,data”是错误的,因为8086CPU不允许将一个数值直接送入段寄存器中。程序中对段中的引用,如指令“mov ds,data”中的“data”,将被编译器处理为一个表示段地址的数值。
(3)“代码段”,“数据段”,“栈段”完全是我们的安排
现在,我们以一个具体的程序来再次讨论一下所谓的“代码段”、“数据段”、“栈段”。在汇编源程序中,可以定义许多的段,比如在程序6.4中,定义了3个段,“code”、“data”和“stack”。我们可以分别安排它们存放代码、数据和栈。那么我们如何让CPU按照我们的这种安排来执行这个程序呢?下面来看看源程序中对这3个段所做的处理。
⑴ 我们在源程序中为这3个段起了具有含义的名称,用来放数据的段我们将其命名为"data",用来放代码的段我们将其命名为“code”,用作栈空间的段命名为“stack”。
这样命名了之后,CPU是否就去执行“ocde”段中的内容,处理"data"段中的数据,将“stack”当做栈了呢?
当然不是,我们这样命名,仅仅是为了使程序便于阅读。这些名称同“start”、“s”、“s0”等标号一样,仅在源程序中存在,CPU并不知道它们。
⑵ 我们在源程序中用伪指令“assume cs:code,ds:data,ss:stack”将cs、ds和ss分别和 code、data、stack段相连。这样做了之后,CPU是否就会将cs指向code,ds指向data,ss指向stack,从而按照我们的意图来处理这些段呢
当然也不是,要知道assume是伪指令,是由编译器执行的,也是仅在源程序中存在的信息,CPU并不知道它们。我们不必深究assume的作用,只要知道需要用将你定义的具体一定用途的段和相关的寄存器联系起来就可以了。
⑶ 若要CPU按照我们的安排行事,就要用机器指令控制它,源程序中的汇编指令是CPU要执行的内容。CPU如何知道去执行它们?我们在源程序的最后用“end start”说明了程序的入口,这个入口将被写入可执行文件的描述信息,可执行文件中的程序被加载入内存后,CPU的CS:IP被设置指向这个入口,从而开始执行程序中的第一条指令。标号“start”在“code”段中,这样CPU就将code段中的内容当作指令来执行了。我们在code段中,使用指令:
mov ax,stack
mov ss,ax
mov sp,20h
设置ss指向stack,设置SS:SP指向stack:20,CPU执行这些指令后,将把stack段当做栈空间来用。CPU若要访问data段中的数据,则可用ds指向data段,用其他的寄存器(如bx)来存放data段中数据的偏移地址。
总之,CPU到底如何处理我们定义的段中的内容,是当作指令执行,当作数据访问,还是当作栈空间,完全是靠程序中具全的汇编指令,和汇编指令对CS:IP、SS:SP、DS等寄存器的设置来决定的。完全可以将程序6.4写成下面的样子,实现同样的功能。

assume cs:b,ds:a,ss:c

a segment
	dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
a ends

c segment
	dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
c ends

b segment
d:	mov ax,c
		mov ss,ax
		mov sp,20h		;设置栈顶ss:sp指向c:20
		
		mov ax,a
		mov ds,ax		;ds指向a段
		
		mov bx,0		;ds:bx指向a段中的第一个单元
		mov cx,8
		
	s:	push [bx]
		add bx,2
		loop s			;以上将a段中的0~15单元中的8个字型数据依次入栈
	
		mov bx,0
		mov cx,8
		
	s0:	pop [bx]
		add bx,2
		loop s0			;以上依次出栈8个字型数据到a段的0~15单元中 
	
		mov ax,4c00H
		int 21H
b ends
end d

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值