前面的程序中,只有一个代码段。现在的问题是,如果程序需要其他控件存放数据,放在哪里呢?

之前,我们学习到过0:200~0:2FF是相对安全的,可这段空间的容量只有256个字节,如果我们需要的空间超过256个字节应该怎么办呢?

在操作系统的环境中,合法地通过操作系统取得的空间都是安全的,因为操作系统不会让一个程序所用的空间和其他程序以及其系统自己的空间相冲突。在操作系统允许的情况下,程序可以取得任意容量的空间

程序取得所需空间的方法有两种

  1. 在加载程序的时候为程序分配
  2. 在程序执行的过程中向系统申请(这种方法这里不做讨论)

我们若要在一个程序被加载的时候取得所需要的空间,则必须要在源程序中做出说明,通过在源程序中定义段来进行内存空间的获取。

大多数有用的程序,都要处理数据,使用栈空间,当然也都必须有指令,为了程序设计上的清晰和方便,一般都定义不同的段来存放它们。

下面将以这样的顺序来深入地讨论多个段的问题:

  1. 在一个段中存放数据、代码、栈,体会一下不使用多个段时的情况
  2. 将数据、代码、栈放入不同的段中

一、在代码段中使用数据

考虑这样一个问题,编程计算以下8个数据的和,结果存放在ax寄存器中

  1. 0123h, 0456h, 0789h, 0abch, 0defh, 0fedh, 0cbah, 0987h

之前,我们都是对已经放在内存单元中的数据进行累加,现在只给了一组数据,需要将这组数据存储在一组地址连续的内存单元中。我们要使用指令一个一个地它们送入到地址连续的内存单元中,到哪里找这段内存空间呢

从规范的角度上来讲,我们是不能自己随便决定哪段空间是可以使用的,应该让系统为我们分配。我们在程序中,定义我们希望处理的数据,这些数据就会被编译、连接程序作为程序的一部分写到可执行文件中。当可执行文件中的程序被加载如内存时,这些数据也同时被加载到内存中。与此同时,我们的数据就获得了这段内存空间。

具体做法如下面程序所示

  1. assume cs:code
  2. code segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
  4. mov bx,0
  5. mov ax,0
  6. mov cx,8
  7. s: add ax,cs:[bx]
  8. add bx,2
  9. loop s
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. 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加载看一下

§ 第6章 包含多个段的程序 - 图1

从上图中可以到知道,程序从076A:0000 (CS:IP)开始存放,使用u命令查看程序却有一些看不懂的指令,这是因为这堆指令被打散重组了,所以就组成了一堆看不懂的代码。

1.1 数据部分

为什么没有看到程序中的指令呢?实际上用u命令从 076A:0000查看到的也是程序中的内容,只不过不是源程序中的汇编指令所对应的机器码。而是源程序中,在汇编指令前面,用dw定义的数据。

实际上,在程序中,有一个代码段,在代码段中,前面的16个字节是用dw定义的数据,从第16个字节开始才是汇编指令所对应的机器码,正好是存储的数据。

§ 第6章 包含多个段的程序 - 图2

1.2 指令部分

之后,使用u 076A:0010查看其他汇编指令的机器码

§ 第6章 包含多个段的程序 - 图3

怎么样执行程序中的指令呢?使用Debug加载后,可以将IP设置为10h,从而使CS:IP指向程序中的第一条指令。然后再用t命令、p命令或者是g命令执行。

可是这样一来,我们就必须用Debug来执行程序。程序编译、连接成可执行文件后,在系统中直接运行可能会出现问题,因为程序的入口处不是我们所希望执行的指令。如何让这个程序在编译、连接后可以在系统中直接运行呢?我们可以在源程序中知名程序的入口所在,如下程序所示

  1. assume cs:code
  2. code segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
  4. start: mov bx,0 ; 说明这句话是程序的第一条指令
  5. mov ax,0
  6. mov cx,8
  7. s: add ax,cs:[bx]
  8. add bx,2
  9. loop s
  10. mov ax,4c00h
  11. int 21h
  12. code ends
  13. end start ; 程序的入口在标号start

注意:在程序6.2中加入新的内容,在程序的第一条指令的前面加上了一个标号start,而这个标号出现在了伪指令end的后面。

这里,还要再次探讨end的作用,end除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。在程序中我们用end指令指明了程序的入口在标号start处。

在前面的课程中,我们已经知道在单任务系统中,可执行文件中的程序执行过程如下:

§ 第6章 包含多个段的程序 - 图4

现在的问题是,根据什么设置CPU的CS:IP指向程序的第一条要执行的指令?也就是说,如何知道哪一条指令是程序的第一条要执行的指令?这一点,是由可执行文件中的描述信息指明的。

§ 第6章 包含多个段的程序 - 图5

在上述程序中,用伪指令end描述了程序的结束和程序的入口。在编译、连接后,由end start指明的程序入口,被转换为一个入口地址,存储在可执行文件的描述信息中。

在上述程序的可执行文件中,这个入口地址的偏移地址部分为:10H。当程序被加载如内存之后 ,加载者从程序的可执行文件的描述信息中读到程序的入口地址,设置为CS:IP。这样CPU就从我们希望的地址处开始执行。

因此,一般程序的框架

  1. assume cs:code
  2. code segment
  3. ...
  4. ; 数据
  5. ...
  6. start:
  7. ...
  8. ; 代码
  9. ...
  10. code ends
  11. end start

二、在代码段中使用栈

完成下面的程序,利用栈,将程序中定义的数据逆序存放

  1. assume cs:codesg
  2. codesg segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0redh,0cbah,0987h
  4. ? ; 填补程序
  5. codesg ends
  6. end

该题解的思路为:

  1. 先将上述的8个字(16个字节)大小的数据存放到cs:0~cs:F单元中
  2. 依次将这些数据入栈
  3. 再依次出栈,根据先入后出的特性实现数据的逆序存放

所以,现在得有一段可当作栈的内存空间,这段空间应该由系统来分配,可以在程序中通过定义数据来取得一段空间,然后把这段空间当作栈空间来使用。

  1. assume cs:codesg
  2. codesg segment
  3. ; dw定义16个字型数据,在程序加载后,将取得16个字内存空间,存放这16个数据
  4. ; 在后面的程序中将这段空间当作栈来使用
  5. dw 0123h,0456h,0789h,0abch,0defh,0redh,0cbah,0987h
  6. dw, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
  7. start: mov ax,cs
  8. mov ss,ax
  9. mov sp,30h ; 将栈顶设置为ss:sp,指向cs:30
  10. mov bx,0
  11. mov cx,8
  12. s: push cs:[bx]
  13. add bx,2
  14. loop s ; 0~15单元中的8个字型数据依次入栈
  15. mov bx,0
  16. mov cx,8
  17. s0: pop cs:[bx]
  18. add bx,2
  19. loop s0 ; 以上依次出栈8个字型数据到代码段0~15单元中
  20. mov ax,4c00h
  21. int 21h
  22. codesg ends
  23. end start ; 指明程序的入口在start

首先,说明一下上面的程序中的sp为什么指向30h

  1. dw 0123h,0456h,0789h,0abch,0defh,0redh,0cbah,0987h定义了8个字型数据,所占用的字节大小为16
  2. dw, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0定义了16个字型数据,所占用的字节大小为32

§ 第6章 包含多个段的程序 - 图6

§ 第6章 包含多个段的程序 - 图7

在代码段中定义了16个字型数据,它们的数值都是0。这16个字型数据的值是多少,对程序来说没有意义。我们用dw定义了16个数据,即在程序中写入了16个字型数据,而在程序加载后,将会有32个字节大小的内存空间来存放它们,这段空间是我们所需要的,程序将他当作栈空间。

所以,dw的作用除了定义数据外,也可以变相的开辟内存空间

  1. ; 以下可以认为不仅定义了8个字型数据
  2. ; 还可以认为开辟了8个字大小的内存空间
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

三、将数据、代码、栈放入不同的段

在前面的内容中,我们在程序中用到了数据和栈,将数据、栈和代码都放到了一段里面。这样就造成了我们编程的时候需要注意何处是数据、何处是栈、何处是代码。但是这样做显然有两个问题

  1. 把它们放到一个段中使程序显得混乱
  2. 前面程序中处理的数据很少,用到的栈空间也小,加上没有多长的代码,放到一个段里面没有问题。但是如果数据、栈和代码所需要的空间超过64KB,就不能放在一个段中(一个段的容量不能大于64KB,是我们在学习中所用的8086模式的限制,并不是所有的处理器都这样

§ 第6章 包含多个段的程序 - 图8

所以,应该考虑多个段来存放数据、代码和栈,怎么做呢?

§ 第6章 包含多个段的程序 - 图9

用和定义代码段一样的方法来定义多个段,然后在这些段里定义需要的数据,或通过定义数据来取得栈空间。具体做法如下面的程序所示,这个程序实现了和上述程序一样的功能,不同之处在于它将数据、栈和代码放到了不同的段中。

  1. assume cs:code,ds:data,ss:stack
  2. data segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0redh,0cbah,0987h ; 定义数据段数据
  4. data ends
  5. stack segment
  6. dw, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ; 开辟16个字大小的内存空间作为栈
  7. stack ends
  8. codesg segment
  9. start: mov ax,stack
  10. mov ss,ax
  11. mov sp,20h ; 将栈顶设置为ss:sp,指向stack:20
  12. mov ax,data
  13. mov ds,ax ; ds指向data数据段
  14. mov bx,0
  15. mov cx,8
  16. s: push [bx] ; push默认使用ds段寄存器,将ds:bx字单元内容弹向栈中
  17. add bx,2
  18. loop s ; 0~15单元中的8个字型数据依次入栈
  19. mov bx,0
  20. mov cx,8
  21. s0: pop [bx] ; pop默认使用ds段寄存器,将ds:bx字单元内容弹出栈中
  22. add bx,2
  23. loop s0 ; 以上依次出栈8个字型数据到代码段0~15单元中
  24. mov ax,4c00h
  25. int 21h
  26. code ends
  27. end start ; 指明程序的入口在start

下面就对上面的程序进行说明

3.1 定义多个段的方法

我们可以从程序中明显的看出来,就是给每一段一个名字就可以了

  1. assume cs:code,ds:data,ss:stack
  2. data segment
  3. ; 1.数据段的定义
  4. data ends
  5. stack segment
  6. ; 2.栈段的定义
  7. stack ends
  8. codesg segment
  9. ; 3.代码段的定义
  10. code ends
  11. end

3.2 对段地址的引用

现在,程序中有多个段了,如何访问段中的数据呢?

访问数据的方法:段地址:偏移地址,如何指明要访问的段地址呢?在程序中,段名就相当于一个标号,它就代表了段地址,

  1. mov ax,data ; 将名称为data的段的段地址送入ax
  2. mov ax,stack ; 将名称为stack的段的段地址送入ax
  3. mov ax,code ; 将名称为code的段的段地址送入ax

现在,再添加上偏移地址来访问数据

  1. ; data:6处的数据给bx
  2. mov ax,data
  3. mov ds,ax
  4. mov bx,ds:[6]
  5. ; 注意,下面这种情况是错误的
  6. mov ds,data ; data是一个数值,不允许将一个数值送入段寄存器
  7. mov bx,ds:[6]

四、实验:编写、调试具有多个段的程序

4.1 实验一

将下面的程序编译、连接,用Debug加载、跟踪,然后回答问题

  1. assume cs:code,ds:data,ss:stack ; 这里只是分配了与data segment段大小相同的空间
  2. data segment
  3. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
  4. data ends
  5. stack segment
  6. dw 0,0,0,0,0,0,0,0
  7. stack ends
  8. code segment
  9. start: mov ax,stack
  10. mov ss,ax
  11. mov sp,16
  12. mov ax,data
  13. mov ds,ax ; 在这里才把data的段地址给DS
  14. push ds:[0]
  15. push ds:[2]
  16. pop ds:[2]
  17. pop ds:[0]
  18. mov ax,4c00h
  19. int 21h
  20. code ends
  21. end start

对上述程序进行调试,结果如下

§ 第6章 包含多个段的程序 - 图10

问题1

CPU执行程序,程序返回前,data段中的数据为多少?

§ 第6章 包含多个段的程序 - 图11

问题2

CPU执行程序,程序返回前,cs= 076C 、ss= 076B、ds= 076A

问题3

设程序加载后,code段的段地址为X,则data段的段地址为 X-2,stack的段地址为 X-1 。

答案参考:(20条消息) 王爽汇编语言第三版第5章实验5c_0934的博客-CSDN博客汇编语言实验5

4.2 实验二

将下面的程序编译、连接,用Debug加载、跟踪,然后回答问题

  1. assume cs:code,ds:data,ss:stack
  2. data segment
  3. dw 0123h,0456h
  4. data ends
  5. stack segment
  6. dw 0,0
  7. stack ends
  8. code segment
  9. start: mov ax,stack
  10. mov ss,ax
  11. mov sp,16
  12. mov ax,data
  13. mov ds,ax
  14. push ds:[0]
  15. push ds:[2]
  16. pop ds:[2]
  17. pop ds:[0]
  18. mov ax,4c00h
  19. int 21h
  20. code ends
  21. end start

问题1

CPU执行程序,程序返回前,data段中的数据为多少?

data段的数据0123h,0456h

问题2

CPU执行程序,程序返回前,cs= 076C 、ss= 076B 、ds= 076A 。

§ 第6章 包含多个段的程序 - 图12

问题3

设程序加载后,code段的段地址为X,则data段的段地址为X-2,stack段的段地址为 X-1 。

问题4

对于如下的段

  1. name segment
  2. ...
  3. name ends

如果段中的数据占N个字节,则程序加载后,该段实际占有的空间为 § 第6章 包含多个段的程序 - 图13*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加载、跟踪,然后回答问题

  1. assume cs:code,ds:data,ss:stack
  2. code segment
  3. start: mov ax,stack
  4. mov ss,ax
  5. mov sp,16 ; 指定栈
  6. mov ax,data
  7. mov ds,ax ; 指定数据寄存器
  8. push ds:[0]
  9. push ds:[2]
  10. pop ds:[2]
  11. pop ds:[0]
  12. mov ax,4c00h
  13. int 21h
  14. code ends
  15. data segment
  16. dw 0123h,0456h
  17. data ends
  18. stack segment
  19. dw 0,0
  20. stack ends
  21. end start

问题1

CPU执行程序,程序返回前,data段中的数据为多少?

数据为0123h,0456h

问题2

CPU执行程序,程序返回前,cs= 076A、ss= 076E、ds= 076D。

§ 第6章 包含多个段的程序 - 图14

问题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段中

  1. ;默认是一次记录到代码段中的
  2. assume cs:code
  3. a segment
  4. db 1,2,3,4,5,6,7,8
  5. a ends
  6. b segment
  7. db 1,2,3,4,5,6,7,8
  8. b ends
  9. cc segment
  10. db 0,0,0,0,0,0,0,0
  11. cc ends
  12. code segment
  13. start: mov ax,a
  14. mov ds,ax
  15. mov ax,b
  16. mov es,ax
  17. mov bx,0
  18. mov cx,8
  19. s: mov al,ds:[bx] ; 注意:因为使用db开辟空间,所以是字节空间,使用al
  20. add es:[bx],al
  21. inc bx
  22. loop s
  23. mov ax,4c00h
  24. int 21h
  25. code ends
  26. end start

4.6 实验六

程序如下,编写code段中的代码,用push指令将a段中的前8个字型数据,逆序存储到b段中。

  1. assume cs:code
  2. a segment
  3. dw 1,2,3,4,5,6,7,8,0ah,0bh,0ch,0dh,0eh,0fh,0ffh
  4. a ends
  5. b segment
  6. db 0,0,0,0,0,0,0,0
  7. b ends
  8. code segment
  9. start: mov ax,a
  10. mov ds,ax
  11. mov ax,b
  12. mov ss,ax
  13. mov sp,16
  14. mov cx,8
  15. mov bx,0
  16. s: push ds:[bx]
  17. add bx,2
  18. loop s
  19. mov ax,4c00h
  20. int 21h
  21. code ends
  22. end start

答案参考:《汇编语言(王爽)第三版》实验【未完待续】 - laolao - 博客园 (cnblogs.com)

五、检测点

  1. 下面的程序依次用内存0:0~0:15单元中的内容改写程序中的数据,完成程序:
    最一开始的数据:
    § 第6章 包含多个段的程序 - 图15
    经过不断的修改后的数据,最后和0:0~0:15的数据一致
    § 第6章 包含多个段的程序 - 图16 ```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

  1. mov cx,8
  2. s: mov ax,[bx]
  3. mov cs:[bx],ax
  4. add bx,2
  5. loop s
  6. mov ax,4c00h
  7. int 21h

codesg ends end start

  1. 2. 下面的程序实现依次用内存0:0~0:15单元中的内容改写程序中的数据,数据的传送用栈来进行,栈空间设置在程序内。完成程序:
  2. ```assembly
  3. assume cs:codesg
  4. codesg segment
  5. dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h ; 数据为8个字
  6. dw 0,0,0,0,0,0,0,0,0,0 ; 开辟10个字大小的内存当作栈空间
  7. start: mov ax,CS ; 填空
  8. mov ss,ax
  9. mov sp,37 ; 填空
  10. mov ax,0
  11. mov ds,ax
  12. mov bx,0
  13. mov cx,8
  14. s: push[bx]
  15. pop cs:[bx] ; 填空
  16. add bx,2
  17. loop s
  18. mov ax,4c00h
  19. int 21h
  20. codesg ends
  21. end start

这个答案有点问题,因为按理来说,一次pop或者push应该是要1个字,即2个字节,但是它一次只弹1个字节,就造成了每次只有1个字节的数据被改变