第六章 包含多个段的程序
前面我们说0:200~0:300这段内存空间是相对安全的,可这段空间容量只有256个字节。在操作系统环境中,合法地通过操作系统取得的空间都是安全的,因为操作系统部会让一个程序所用的空间和其他程序以及系统自己的空间相冲突。在操作系统允许的情况下,程序可以取得任意容量的空间。
程序取得所需空间的方法有两种,一是在加载程序的时候为程序分配,再就是程序在执行的过程中向系统申请。加载程序的时候为程序分配空间,前面所体验,比如我们的程序在加载的时候,取得了代码段中的代码的存储空间。
我们若要一个程序在被加载的时候取得所需的空间,则必须要在程序中做出说明。通过在源程序中定义段来进行内存空间的获取。大多数有用的程序,都要处理数据,使用栈空间,当然也都必须有指令,为了程序设计上的清晰和方便,我们一般也都定义不同的段来存放他们。
6.1 在代码段中使用数据
例: 计算以下8个数据的和,结果存放在ax寄存器中:
0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
通过之前的经验,我们可以讲它们一个一个地加到ax寄存器中,但是,我们希望用循环的方法来进行累加,所以在累加前,我们要将这些数据存储在一组地址连续的内存单元中。怎么样获得者连续的内存单元呢 ?
从规范的角度讲,我们是不能自己随便决定那段空间可以使用的,应该让系统为我们分配。我们可以在程序中,定义我们希望处理的数据,这些数据就会被编译、连接程序作为程序的一部分写到可执行文件中。当可执行文件中的程序被加载如内存时,这些数据也同时被加载如内存中。与此同时,我们要处理的数据也就自然而然地获得了存储空间。如下程序:
程序6.1
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”含义是定义字型数据,即define word。我们使用dw定义了8个字型数据(逗号隔开),它们所占内存为16个字节。
由于这8个数据在代码段中,程序在运行的时候CS中存放代码段的段地址。而dw定义的数据处于代码段最开始,所以偏移地址为0;
在4.8节,我们知道在单任务系统中,可执行文件中的程序执行过程如下:
(1) 由其他程序(Debug、command 或其他程序)将可执行文件中的程序加载入内存;
(2) 设置CS:IP 指向程序的第一条要执行的指令(即程序的入口),从而使程序得以运行;
(3) 程序运行结束后,返回到加载者。
我们之前所有使用Debug对程序的调试,实际上都是由Debug加载了我们的可执行文件到内存中;之后由Debug设置CS:IP到程序的入口;待程序结束再返回到Debug环境中。
但我们希望可执行文件自己可独立运行,无需其他程序将其加载入内存,并设置CS:IP的指向。所以我们在汇编源码中加入程序的入口标号: start
程序变为,程序6.2:
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在伪指令end的后面出现。我们要再次探讨 end 的作用。end 除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。在程序6.2中我们用end 伪指令指明了程序的入口在标号start 处,也就是说,“mov bx, 0”是程序的第一条指令。
现在的问题是,根据什么设置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:code
code segment
dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
??????????
code ends
end
我的代码:
assume cs:code
code segment
dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
;将数据复制到0:200处,将0:200~0:20f 这段内存当做栈,将数据压入栈中。
mov ax,0
mov ds,ax
mov bx,0
mov cx,8
mov ss,ax ;设置栈段寄存器
mov sp, 020fH+1 ;
s: mov ax,cs:[bx]
push ax
add bx,2
loop s
;弹出栈,进行逆序
mov ax,0
mov bx,0
mov cx,8
f: pop ax
mov cs:[bx],ax
add bx,2;
loop f
mov ax,4c00H
int 21H
code ends
end start
注意设置栈的偏移地址处,此处加1,是因为在把数据压入栈之前,sp先执行减法操作即sp = sp - 2;地址SS:SP在任意时刻都指向栈顶元素,栈的操作都是以字(2个字节)为单位进行的,当栈中没有元素时,则SP则应该指向栈底(亦即栈顶)的下一个内存单元,所以加1。若不加1,则整个栈空间则不是计划的安全内存空间0:200~0:20f ,即最后栈顶元素地址将不是0:200。
通过定义字型数据开辟内存空间:
例子代码:
assume cs:code
code segment
dw 0123H,0456H,0789H,0abcH,0defH,0fedH,0cbaH,0987H
dw 0,0,0,0, 0,0,0,0
start: mov ax,cs
mov ss,ax
mov sp,32
mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s
mov bx,0
mov cx,8
f: pop cs:[bx]
add bx,2
loop f
mov ax,4c00H
int 21H
code ends
end start
由此得,我们定义了8个字型数据,它们的数据都是0。所以在描述dw作用时,可以说用它定义数据,也可以说用它开辟内存空间。
6.3 将数据、代码、栈放入不同的段
在前面,我们在程序中用到了数据和栈,我们将数据、栈和代码都放到了一个段里面。我们在编程时要注意何处是数据,何处是栈,何处是代码。这样做显然有两个问题:
(1) 把它们放到一个段中使程序显得混乱;
(2) 若数据、栈和代码需要的空间超过64KB,就不能放在一个段中(一个段的容量不能大于64KB,是由于8086模式的限制,并不是所有的处理器都这样)。前面程序中处理的数据很少,用到得栈空间也小,代码长度也小,故放到一个段中没有问题。
所以我们应该考虑用多个段来存放数据、代码和栈。
我们用和定义代码段一样的方法来定义多个段,然后在这些段里面定义需要的数据,货通过定义数据来取得栈空间。如程序6.4和程序6.3实现了相同的功能:
程序6.4:
assume cs:code, 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,
stack ends
code segment
start: mov ax, stack
mov ss, ax
mov sp, 16 ; 设置栈顶ss:sp指向stack:16
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~16单元中的8个字型数据依次入栈
mov bx, 0
mov cx, 8
s0: pop [bx] ; 以上使8个字型数据依次出栈并送到data段的0~16单元中。
add bx, 2
loop s0
mov ax, 4c00H
int 21H
code ends
end start
(2) 对段地址的引用
我们通过段地址访问段中的数据,而段地址分为两部分,即段地址和偏移地址。在程序中,段名就相当于一个标号,它代表了段地址。所以“mov ax, data”的含义就是将名称为“data”的段的段地址送入ax。所以一个段中的数据的段地址可由段名代表,偏移地址就要看它在段中的位置了。程序中的“data”段中的数据“0abcH”的地址就是: data:6。我们用以下代码将它送入bx中:
mov ax, data
mov ds, ax
mov bx, ds:[6]
不能将段地址data直接送入ds中,“mov ds, data”是错误的,因为8086CPU不允许将一个数值直接送入段寄存器中。程序中对段名的引用,如指令“mov ds, data” 中的“data”,将被编译器处理为一个表示段地址的数据。
(3)“代码段”、“数据段”、“栈段”完全是我们的安排
我们在源程序中用伪指令“assume cs:code, ds:data, ss:stack”,将cs、ds、ss分别和code、data、stack段相连。这样做了之后,CPU并不会讲cs指向code,ds指向data,ss指向stack。标号assume是伪指令,并不由CPU直接执行,而是编译器需要用它将我们源程序中定义的具有一定用途的段和相关的寄存器联系起来。
若要CPU按照我们的安排行事,就要用机器指令控制它。我们在源程序的最后用“end start”说明了程序的入口,这个入口被写入可执行文件的描述信息,可执行文件中的程序被加载入内存后,CPU的CS:IP被设置指向这个入口,从而开始执行第一条指令。标号“start”在“code”段中,这样CPU就将code段中的内容当做指令来执行了。我们在code段中,使用指令:
mov ax, stack
mov ss, ax
mov sp, 16
设置ss指向stack,设置ss:sp指向stack:16,CPU执行这些指令后,将把stack段当做栈空间来用。CPU若要访问data段中的数据,则可用ds指向data段,用其他的寄存器来存放data段中数据的偏移地址。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/syf442/archive/2009/08/25/4481989.aspx