前面的程序中,只有一个代码段。现在的问题是,如果程序需要其他控件存放数据,放在哪里呢?
之前,我们学习到过0:200~0:2FF是相对安全的,可这段空间的容量只有256个字节,如果我们需要的空间超过256个字节应该怎么办呢?
在操作系统的环境中,合法地通过操作系统取得的空间都是安全的,因为操作系统不会让一个程序所用的空间和其他程序以及其系统自己的空间相冲突。在操作系统允许的情况下,程序可以取得任意容量的空间。
程序取得所需空间的方法有两种
- 在加载程序的时候为程序分配
- 在程序执行的过程中向系统申请(这种方法这里不做讨论)
我们若要在一个程序被加载的时候取得所需要的空间,则必须要在源程序中做出说明,通过在源程序中定义段来进行内存空间的获取。
大多数有用的程序,都要处理数据,使用栈空间,当然也都必须有指令,为了程序设计上的清晰和方便,一般都定义不同的段来存放它们。
下面将以这样的顺序来深入地讨论多个段的问题:
- 在一个段中存放数据、代码、栈,体会一下不使用多个段时的情况
- 将数据、代码、栈放入不同的段中
一、在代码段中使用数据
考虑这样一个问题,编程计算以下8个数据的和,结果存放在ax寄存器中
0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h
之前,我们都是对已经放在内存单元中的数据进行累加,现在只给了一组数据,需要将这组数据存储在一组地址连续的内存单元中。我们要使用指令一个一个地它们送入到地址连续的内存单元中,到哪里找这段内存空间呢?
从规范的角度上来讲,我们是不能自己随便决定哪段空间是可以使用的,应该让系统为我们分配。我们在程序中,定义我们希望处理的数据,这些数据就会被编译、连接程序作为程序的一部分写到可执行文件中。当可执行文件中的程序被加载如内存时,这些数据也同时被加载到内存中。与此同时,我们的数据就获得了这段内存空间。
具体做法如下面程序所示
assume cs:code
code segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
mov bx,0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end
上面程序中的第一行dw 0123h, ... 0987h
中的dw
的含义是定义字型数据。dw
即”define word”,上面的第一句话定义了8个字型数据,占用的内存空间大小为16个字节。
程序中的指令就要对这8个数据进行累加,可这8个数据在哪里呢?
程序在运行的时候CS中存放代码段的段地址,所以可以从CS中得到它们的段地址,那么它们的偏移地址呢?
因为用**dw**
定义的数据处于代码段的最开始,所以偏移地址为0,这8个数据在代码段的偏移0、2、3、6、8、A、C、E处。程序运行时,它们的地址就是CS:0、CS:2、CS:4、CS:6、CS:2、CS:8、CS:A、CS:C、CS:E
程序中,用bx
存放加2递增的偏移地址,用循环来进行累加。在循环开始前,设置(bx)=0
,cs:bx指向第一个数据所在的字单元。每次循环中(bx)=(bx)+2
,cs:bx指向下一个数据所在的字单元。
将上述程序编译、连接为可执行文件,使用Debug加载看一下
从上图中可以到知道,程序从076A:0000 (CS:IP)开始存放,使用u
命令查看程序却有一些看不懂的指令,这是因为这堆指令被打散重组了,所以就组成了一堆看不懂的代码。
1.1 数据部分
为什么没有看到程序中的指令呢?实际上用u
命令从 076A:0000查看到的也是程序中的内容,只不过不是源程序中的汇编指令所对应的机器码。而是源程序中,在汇编指令前面,用dw
定义的数据。
实际上,在程序中,有一个代码段,在代码段中,前面的16个字节是用dw
定义的数据,从第16个字节开始才是汇编指令所对应的机器码,正好是存储的数据。
1.2 指令部分
之后,使用u 076A:0010
查看其他汇编指令的机器码
怎么样执行程序中的指令呢?使用Debug加载后,可以将IP设置为10h,从而使CS:IP指向程序中的第一条指令。然后再用t
命令、p
命令或者是g
命令执行。
可是这样一来,我们就必须用Debug来执行程序。程序编译、连接成可执行文件后,在系统中直接运行可能会出现问题,因为程序的入口处不是我们所希望执行的指令。如何让这个程序在编译、连接后可以在系统中直接运行呢?我们可以在源程序中知名程序的入口所在,如下程序所示
assume cs:code
code 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
code ends
end start ; 程序的入口在标号start处
注意:在程序6.2中加入新的内容,在程序的第一条指令的前面加上了一个标号start
,而这个标号出现在了伪指令end
的后面。
这里,还要再次探讨end
的作用,end
除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。在程序中我们用end
指令指明了程序的入口在标号start
处。
在前面的课程中,我们已经知道在单任务系统中,可执行文件中的程序执行过程如下:
现在的问题是,根据什么设置CPU的CS:IP指向程序的第一条要执行的指令?也就是说,如何知道哪一条指令是程序的第一条要执行的指令?这一点,是由可执行文件中的描述信息指明的。
在上述程序中,用伪指令end
描述了程序的结束和程序的入口。在编译、连接后,由end start
指明的程序入口,被转换为一个入口地址,存储在可执行文件的描述信息中。
在上述程序的可执行文件中,这个入口地址的偏移地址部分为:10H
。当程序被加载如内存之后 ,加载者从程序的可执行文件的描述信息中读到程序的入口地址,设置为CS:IP。这样CPU就从我们希望的地址处开始执行。
因此,一般程序的框架
assume cs:code
code segment
...
; 数据
...
start:
...
; 代码
...
code ends
end start
二、在代码段中使用栈
完成下面的程序,利用栈,将程序中定义的数据逆序存放
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0redh,0cbah,0987h
? ; 填补程序
codesg ends
end
该题解的思路为:
- 先将上述的8个字(16个字节)大小的数据存放到cs:0~cs:F单元中
- 依次将这些数据入栈
- 再依次出栈,根据先入后出的特性实现数据的逆序存放
所以,现在得有一段可当作栈的内存空间,这段空间应该由系统来分配,可以在程序中通过定义数据来取得一段空间,然后把这段空间当作栈空间来使用。
assume cs:codesg
codesg segment
; 用dw定义16个字型数据,在程序加载后,将取得16个字内存空间,存放这16个数据
; 在后面的程序中将这段空间当作栈来使用
dw 0123h,0456h,0789h,0abch,0defh,0redh,0cbah,0987h
dw, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
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处
首先,说明一下上面的程序中的sp
为什么指向30h
dw 0123h,0456h,0789h,0abch,0defh,0redh,0cbah,0987h
定义了8个字型数据,所占用的字节大小为16dw, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
定义了16个字型数据,所占用的字节大小为32
在代码段中定义了16个字型数据,它们的数值都是0。这16个字型数据的值是多少,对程序来说没有意义。我们用dw
定义了16个数据,即在程序中写入了16个字型数据,而在程序加载后,将会有32个字节大小的内存空间来存放它们,这段空间是我们所需要的,程序将他当作栈空间。
所以,dw
的作用除了定义数据外,也可以变相的开辟内存空间
; 以下可以认为不仅定义了8个字型数据
; 还可以认为开辟了8个字大小的内存空间
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
三、将数据、代码、栈放入不同的段
在前面的内容中,我们在程序中用到了数据和栈,将数据、栈和代码都放到了一段里面。这样就造成了我们编程的时候需要注意何处是数据、何处是栈、何处是代码。但是这样做显然有两个问题
- 把它们放到一个段中使程序显得混乱
- 前面程序中处理的数据很少,用到的栈空间也小,加上没有多长的代码,放到一个段里面没有问题。但是如果数据、栈和代码所需要的空间超过64KB,就不能放在一个段中(一个段的容量不能大于64KB,是我们在学习中所用的8086模式的限制,并不是所有的处理器都这样)
所以,应该考虑多个段来存放数据、代码和栈,怎么做呢?
用和定义代码段一样的方法来定义多个段,然后在这些段里定义需要的数据,或通过定义数据来取得栈空间。具体做法如下面的程序所示,这个程序实现了和上述程序一样的功能,不同之处在于它将数据、栈和代码放到了不同的段中。
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h,0789h,0abch,0defh,0redh,0cbah,0987h ; 定义数据段数据
data ends
stack segment
dw, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ; 开辟16个字大小的内存空间作为栈
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
mov cx,8
s: push [bx] ; push默认使用ds段寄存器,将ds:bx字单元内容弹向栈中
add bx,2
loop s ; 将0~15单元中的8个字型数据依次入栈
mov bx,0
mov cx,8
s0: pop [bx] ; pop默认使用ds段寄存器,将ds:bx字单元内容弹出栈中
add bx,2
loop s0 ; 以上依次出栈8个字型数据到代码段0~15单元中
mov ax,4c00h
int 21h
code ends
end start ; 指明程序的入口在start处
下面就对上面的程序进行说明
3.1 定义多个段的方法
我们可以从程序中明显的看出来,就是给每一段一个名字就可以了
assume cs:code,ds:data,ss:stack
data segment
; 1.数据段的定义
data ends
stack segment
; 2.栈段的定义
stack ends
codesg segment
; 3.代码段的定义
code ends
end
3.2 对段地址的引用
现在,程序中有多个段了,如何访问段中的数据呢?
访问数据的方法:段地址:偏移地址,如何指明要访问的段地址呢?在程序中,段名就相当于一个标号,它就代表了段地址,
mov ax,data ; 将名称为data的段的段地址送入ax
mov ax,stack ; 将名称为stack的段的段地址送入ax
mov ax,code ; 将名称为code的段的段地址送入ax
现在,再添加上偏移地址来访问数据
; 将data:6处的数据给bx
mov ax,data
mov ds,ax
mov bx,ds:[6]
; 注意,下面这种情况是错误的
mov ds,data ; data是一个数值,不允许将一个数值送入段寄存器
mov bx,ds:[6]
四、实验:编写、调试具有多个段的程序
4.1 实验一
将下面的程序编译、连接,用Debug加载、跟踪,然后回答问题
assume cs:code,ds:data,ss:stack ; 这里只是分配了与data segment段大小相同的空间
data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,data
mov ds,ax ; 在这里才把data的段地址给DS
push ds:[0]
push ds:[2]
pop ds:[2]
pop ds:[0]
mov ax,4c00h
int 21h
code ends
end start
对上述程序进行调试,结果如下
问题1:
CPU执行程序,程序返回前,data段中的数据为多少?
问题2:
CPU执行程序,程序返回前,cs= 076C 、ss= 076B、ds= 076A
问题3:
设程序加载后,code段的段地址为X,则data段的段地址为 X-2,stack的段地址为 X-1 。
4.2 实验二
将下面的程序编译、连接,用Debug加载、跟踪,然后回答问题
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h
data ends
stack segment
dw 0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,16
mov ax,data
mov ds,ax
push ds:[0]
push ds:[2]
pop ds:[2]
pop ds:[0]
mov ax,4c00h
int 21h
code ends
end start
问题1:
CPU执行程序,程序返回前,data段中的数据为多少?
data段的数据0123h,0456h
问题2:
CPU执行程序,程序返回前,cs= 076C 、ss= 076B 、ds= 076A 。
问题3:
设程序加载后,code段的段地址为X,则data段的段地址为X-2,stack段的段地址为 X-1 。
问题4:
对于如下的段
name segment
...
name ends
如果段中的数据占N个字节,则程序加载后,该段实际占有的空间为 *16%7D#card=math&code=%5Ctext%7BN%2516%20%3D%3D%20N%20%3F%20%3A%20%28N%2F16%2B1%29%2A16%7D&id=Ex7LR)(即16的整数倍,不够16的按16处理) 。
4.3 实验三
将下面的程序编译、连接,用Debug加载、跟踪,然后回答问题
assume cs:code,ds:data,ss:stack
code segment
start: mov ax,stack
mov ss,ax
mov sp,16 ; 指定栈
mov ax,data
mov ds,ax ; 指定数据寄存器
push ds:[0]
push ds:[2]
pop ds:[2]
pop ds:[0]
mov ax,4c00h
int 21h
code ends
data segment
dw 0123h,0456h
data ends
stack segment
dw 0,0
stack ends
end start
问题1:
CPU执行程序,程序返回前,data段中的数据为多少?
数据为0123h,0456h
问题2:
CPU执行程序,程序返回前,cs= 076A、ss= 076E、ds= 076D。
问题3:
设程序加载后,code段的段地址为X,则data段的段地址为 X+3 ,stack段的段地址为X+4 。
4.4 实验四
如果将123题中的最后一条伪指令end start
改为end
(即不指明程序的入口),则哪个程序仍然可以正确执行?请说明原因
第三个程序可以执行。
如果不指名
start
入口,并且使用end
替换end start
,程序仍然可以执行。因为如果不指名入口,程序则从加载进内存的第一个单元起开始执行,但因为程序中有部分是作为数据使用的,如果不指明入口,CPU会把这些数值数据当成汇编指令执行,因此有必要通过start
来指明入口
4.5 实验五
程序如下,编写code段的代码,将a段和b段中的数据依次相加,将结果保存到c段中
;默认是一次记录到代码段中的
assume cs:code
a segment
db 1,2,3,4,5,6,7,8
a ends
b segment
db 1,2,3,4,5,6,7,8
b ends
cc segment
db 0,0,0,0,0,0,0,0
cc ends
code segment
start: mov ax,a
mov ds,ax
mov ax,b
mov es,ax
mov bx,0
mov cx,8
s: mov al,ds:[bx] ; 注意:因为使用db开辟空间,所以是字节空间,使用al
add es:[bx],al
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end start
4.6 实验六
程序如下,编写code段中的代码,用push
指令将a段中的前8个字型数据,逆序存储到b段中。
assume cs:code
a segment
dw 1,2,3,4,5,6,7,8,0ah,0bh,0ch,0dh,0eh,0fh,0ffh
a ends
b segment
db 0,0,0,0,0,0,0,0
b ends
code segment
start: mov ax,a
mov ds,ax
mov ax,b
mov ss,ax
mov sp,16
mov cx,8
mov bx,0
s: push ds:[bx]
add bx,2
loop s
mov ax,4c00h
int 21h
code ends
end start
答案参考:《汇编语言(王爽)第三版》实验【未完待续】 - laolao - 博客园 (cnblogs.com)
五、检测点
- 下面的程序依次用内存0:0~0:15单元中的内容改写程序中的数据,完成程序:
最一开始的数据:
经过不断的修改后的数据,最后和0:0~0:15的数据一致
```assembly 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
add bx,2
loop s
mov ax,4c00h
int 21h
codesg ends end start
2. 下面的程序实现依次用内存0:0~0:15单元中的内容改写程序中的数据,数据的传送用栈来进行,栈空间设置在程序内。完成程序:
```assembly
assume cs:codesg
codesg segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ; 数据为8个字
dw 0,0,0,0,0,0,0,0,0,0 ; 开辟10个字大小的内存当作栈空间
start: mov ax,CS ; 填空
mov ss,ax
mov sp,37 ; 填空
mov ax,0
mov ds,ax
mov bx,0
mov cx,8
s: push[bx]
pop cs:[bx] ; 填空
add bx,2
loop s
mov ax,4c00h
int 21h
codesg ends
end start
这个答案有点问题,因为按理来说,一次pop
或者push
应该是要1个字,即2个字节,但是它一次只弹1个字节,就造成了每次只有1个字节的数据被改变