引言
我们在之前的汇编开发学习中,源程序只使用定义了一个段,那就是代码段CS。CS段是必须要定义的,毕竟程序的主体灵魂就是代码。可是只有代码还是不能够满足的,还需要有定义的变量数据,毕竟程序运行的基本逻辑就是操作处理数据,变量有时候也是不可或缺的一份子 。
那么,我们该怎么在汇编源程序中声明、使用变量呢?这就是本篇的学习目标:
1、在代码段中使用数据
2、在代码段中使用栈
目标一定,那么就让我们开始本篇的学习把!
使用数据
上面我们提到了程序中重要的成员:变量,我们这里说的变量,在高级语言中,指的是方法体之外的、事先声明好的字段,会在接下来的程序中进行使用,其数值可能会发生变化。
那么来到汇编语言,作为低级语言,“变量”则是一处内存空间,声明“变量”则是申请内存空间。在程序中会对该内存空间进行操作,写入或读取。
实际上高级语言中的声明一个变量,在底层执行时,也就是开辟了一处内存空间,变量其实就是一个标号,实际上是一个内存地址,在代码中使用变量,也就是访问该地址下的数据。
现在我们思考一个问题:就是声明变量,或者说申请内存空间,为什么对于程序开发来说很重要?我们之前学的源程序中也没申请啥空间,不是照样安安稳稳运行嘛!
引言中说过,程序运行的本质,就是操作处理数据,而数据都是放在内存中。所以,我们就必须要有一个地方,用来存放这些待处理或者已经处理好的数据,这就是申请内存空间的重要性体现。有的小伙伴可能会说,内存中不是有一段安全空间嘛 0:200~0:2FF,用不着申请。这样想是错误的,因为安全空间才256个字节,你能保证以后程序中操作数据大小不会超过这个数字?显然是不可保证的。
所以我们要学会向操作系统申请内存。在操作系统的环境中,合法的通过操作系统取得的空间都是安全的,因为操作系统不会让一个程序所用到的空间和其他程序以及系统自己的空间相冲突。在操作系统允许的情况下,程序可以取得任意容量的空间。
程序取得所需空间的方法有两种:
1、系统在加载程序的时候,为程序分配。这个空间主要用来存放程序中的代码数据。
2、程序在执行的过程中向系统申请。这个空间可以自由由程序本身支配。
针对第一种,我们已经深有体会了,比如程序在加载的时候,取得了代码段中的代码的存储空间。我们若要一个程序在被加载的时候取得所需的空间,则必须在源程序中做出说明。该说明通过在源程序中定义段的形式来体现。例如,我们在源程序中定义了代码段CS,则CS段在程序被加载的时候就取得了可供存放代码的内存空间。
那么第二种,就是我们接下来要学习到的。
在代码段中使用数据
我们通过一道题目来开始学习:编程计算,将以下八个数据相加:0123H、0456H、0789H、0abcH、0defH、0fedH、0cbaH、0987H,和放到AX寄存器中。
我们该如何实现?倘如按照之前的思路,我们会一连相加7次。这里我们不想手写7次相加,我们使用循环来做,那么就要首先找一个地方存放这八个数据才行,然后使用BX寻址累加即可。
代码如下:
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。
dw 是伪指令,仅编译器能够识别,全称:define word。
作用:定义内存空间
上述程序中,使用dw定义了8个字型数据,它们所占内存空间大小为16个字节。
那么上述源程序中,我们在程序开始处,即开辟了16个字节大小的内存空间,并用需要进行累加的八个字数据对该内存空间进行初始化。由于我们是在代码段中申请的内存,所以该处内存空间的段地址就是代码段的段地址,寻址就要写成是:CS:[BX]。
为什么每次循环偏移要加2?那是因为我们定义的是字型数据,一个字数据占用两个字节,所以偏移要加上2才行。
上述源程序的结构比较简单,这里博主就不对剩余的部分进行阐述了。那么接下来就让我们来编译运行以下:
我们在Debug内打开,R一下结果发现,此时CS:IP指向的第一行指令不是我们程序中的啊!我们使用U命令查看一下程序数据:
注意看内存中的数据,这时候我们才恍然大悟,原来是我们申请的那16个字节空间,初始化放的是那八个待累加的数据,由于这些数据是在代码段中,所以就被CPU错误的以为这是代码指令,这也就是为什么我们R查看当前指令不对的原因。
所以我们可以认为,现阶段,直接在代码段中申请开辟内存空间的做法是错误的,因为里面的数据会被CPU当作代码指令而错误执行,从而导致程序崩溃。
那么该如何规范且安全的申请开辟内存空间?
首先思考一下之所以导致上述错误指令的原因,是因为CS:IP指向了我们定义的内存空间,所以CPU才会把它当作代码指令来执行,而指令真正开始的地方是内存空间的后面(即16个字节后),所以我们如果把CS:IP执行我们指令真正开始的地方,那不就解决问题了么!
我们将上述错误源程序修改一下:
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。我们使用标号start来标记指令开始位置,并且在源程序最后一行,写上:end start。
end 伪指令
我们之前提到过,end的作用是告诉编译器编译结束,当编译器在编译过程碰到 end ,便会停止编译。
此外,end伪指令另外一个作用就是,描述程序的入口
end 标号:即告诉编译器,程序的入口在标号处。经过编译、连接后,标号便被转化为一个入口地址,储存在可执行文件的描述信息中。当程序被加载进内存之后,加载者从可执行文件的描述信息中读到程序的入口,设置CS:IP。这样CPU就从我们希望的地址处执行。
OK,现在我们已经明白了“end 标号”的作用,那么就让我们编译连接上述源程序,使用Debug再次加载看一下程序指令是否正常吧:
我们可以看到,显示的当前指令:mov BX,0000H,正是标号start所指向的指令。IP寄存器的值为:10H,为标号start所在的偏移。证明我们通过“end 标号”确实修改了程序的入口地址。
我们通过修改程序入口地址的形式,实现了在代码段中申请开辟内存空间,使用数据的目的。现在开始,我们需要调整一下我们当前的源程序结构,使得更加的规范化。接下来,我们源程序结构如下:
assume cs:code
code segment
...... ; 可申请内存空间
start:
......
......
...... ; 代码指令
mov ax,4c00H
int 21H
code ends
end start
请注意,标号:start,可以是任意字符串,但是为了代码的可读性,建议标号名字使用“start”或者与“开始”相关的英文单词。
在代码段中使用栈
我们还是通过一个题目来开始学习:利用栈,将程序中定义的数据逆序存放。如下程序:
程序思路很简单,首先我们将 CS:0~CS:F 这段内存空间的数据依次入栈,然后再使其依次出栈即可。那么问题就来了,我们该如何代码段中使用栈呢?其实也很简单,使用dw 继续申请一段空间当作栈空间来使用即可。
这里小伙伴们可能会有疑问,我们知道,如果源程序中没有定义栈,那么程序加载后将会是一个默认的栈空间,此处我们已经较为详细的分析过了,有遗忘的小伙伴可以翻看此前博文。
我们现在学习的是如何在代码段中使用栈,所以肯定是要自己申请一段内存空间当作栈来使用,并在代码中定义该栈。从现在开始,我们程序中涉及到栈操作的,都要使用自己定义的栈空间。
代码如下:
assume cs:code
code 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
start:
mov ax,cs
mov ss,ax
mov sp,30H
mov bx,0
mov cx,8
s:push cs:[bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0:pop cs:[bx]
add bx,2
loop s0
mov ax,4c00H
int 21H
code ends
end start
使用dw定义16个字型数据,并使用0来做填充。那么程序在被加载后,就将会取得这16个字的内存空间,来存放这16个数字0。
我们使用这16个字型数据大小的空间来当作栈空间。由于这段内存空间是位于代码段内,所以栈段的段地址也就是代码段的段地址。即:SS=CS。指令:mov ax,cs、mov ss,ax,将栈段段地址设置为代码段段地址。
栈空间大小为20H(16个字大小),栈底为:2FH,所以SP要设置为栈底的下一个字节:SP = 2F+1 = 30H。
上述程序中共使用了两个循环,第一个循环是将8个字数据依次入栈。按照栈的特殊访问方式,此时栈顶为最后一个字数据,即CS:F指向的字单元数据。第二个循环则是将栈中的数据依次出栈,BX偏移是从0开始,所以最后一个字数据便放到了CS:0处,这样就实现了一个倒叙。
下面就让我们编译连接,在Debug中加载运行一下吧:
我们首先查看一下申请的内存空间,使用D命令:
我们可以看到 0~F 空间存放着我们待操作的8个字数据,10~2F 空间则是我们定义的栈空间,当前初始化数据都为0。这里还要强调一下:栈空间是我们人为的定义,CPU并不关心这段空间是啥,它只关注SS:SP的指向是谁,哪里就是栈。
下面我们就使用T命令执行,查看执行结果是否符合要求:
我们可以看到,0~F空间内的字数据完成了倒序存放,证明我们的代码没问题。这时把目光转向我们的栈空间,可以看到里面的数据比较杂乱,而且并不是我们PUSH进去的那8个字型数据。至于为什么,这个问题我们在之前的博文中已经详细的探讨过了,原因就是DOS下共用一个栈导致的。有遗忘的小伙伴请翻阅此前的博文进行学习。
现在说第二点,小伙伴可以观察到,我们待操作的数据为16个字节大小,明明16个字节的栈空间就够存放了,但是我们却申请了32个字节大小的空间当作栈。这样做的原因可以从上图中得到直观的解释,图中我们可以看到,实际上栈中使用的数据大小为26个字节,即:16~2F。为什么会超过16个字节这么多,答案还是因为DOS下共用同一个栈。也就是说,栈不仅仅只是用了存放了我们程序中待操作的16个字节数据,它同时还被别的程序(这里的程序指:Debug)使用存放数据,如果我们栈空间只有16个字节,那么势必就会发生栈溢出。
这里我们得到的知识点就是:代码中申请栈空间,为了避免潜在的栈溢出问题,建议要充足,不要抠抠搜搜!当然也不能狮子大开口,一般建议栈空间为代码中要操作的数据大小的两倍即可。
本篇结束语
本篇我们主要学习了如何在代码段中通过 dw 申请内存空间用来使用,比如代码段中申请空间用来存放数据,和申请空间当作栈空间来使用。同时我们还学习了 end 伪指令的另一个作用,探讨了申请栈空间的大小问题,这些都保证了我们后续代码开发规范。
那么下篇博文,我们将学习到如何将数据、代码、栈放到它们该在的段里,这能够让我们的代码结构更加层次清晰。
感谢围观,转发分享请标明出处,谢谢!