前面说道,如果要使用安全的内存空间,0:200~0:2FF是相对安全得内存空间,可是这段空间只有256字节,如果需要的空间大于256字节该怎么办呢?
在操作系统允许的情况下,程序可以取得任意容量的空间。
取得空间的方法有两种。
若要一个程序在加载时取得所需的空间,则必须在源程序做出说明。
上面是从内存空间获取的角度上,谈定义段的问题。为了可读性、功能设计,一般一额定义不同的段来存放。
关于段的问题,我们将以这样的顺序讨论多个段的问题:
我们可以在程序中,定义我们希望处理的数据,数据作为程序的一部分一同被编译、链接写到可执行文件中。
考虑这样一个问题,编程计算8个数据的和,结果存放在AX寄存器中,下面是用我们前面知识写出的代码。
这里出现了一个新的指令dw
,dw
即“define word”,在这里,定义了8个字型数据,占用16字节的内存空间。
使用Debug调试程序,不运行,发现一个问题,程序所在的内存区为075C:0(DS=075C),前256字节存放着PSP,程序的存放位置应为076C:0,使用U指令查看确发现有点不对。
实际上,看到其实是有dw
定义的数据,从第16字节开始才是汇编指令对应的机器码。
怎样执行程序中的指令呢?在Debug中,可以手动修改IP寄存器的值,从而使CS:IP指向程序的另一条指令。
这样一来,在系统运行时就会出现问题,程序的入口不是我们希望执行的指令。
借助伪指令可以通知编译器程序的入口。
有了这个指令,可以仿照这个模板写出更多的程序,start
上面安排数据,start
和end start
之间安排代码。
这里的检测点没做出来,看视频才想通的,后来发现这个题目第一眼没看懂。
检测点考察dw
定义的数据在内存空间的位置,理解了这一点,题目就可以做出来了。
注释未知的指令,在debug模式中运行可以直观的感受到到这一点。
前面的内容中,我们将数据、栈和代码都放到了一个段里面,编程的时候需要注意何处是数据,何处是栈,何处是代码。显然这样有问题:
下面的程序用不同的段实现了上面的功能
cs:code
等代码将段名和寄存器联系起来end start
指明了程序的入口,CS:IP指向这个入口,从而执行程序的第一条指令这里是两个检测点,为了理解不同段在内存空间中的排列,一个段在内存空间中最小单位为16字节。
限于篇幅原因,我这里介绍比较最后的一个实验。
程序如下,编写code段的代码,用push
指令将a段中的前8个字型数据逆序存储在b段中。
写出程序很容易,不过这不是我要说的重点。
程序运行完之后,查看内存空间,注意我这里查看的DS。
从数据对应关系不难判断,76C:00~76C:1F是我们定义的数据段, 076C:20~76C:2E是我们定义的栈段,76C:30之后是代码段。
查看对应的寄存器,也验证了这一点,
$076C \times 10H+0=76C0H=76C \times 10H+0$
$076C \times 10H+20=76E0H=76E \times 10H+0$
$076C \times10H+30=76F0H=76F \times 10H+0$
这里主要涉及到了一些段编译的规则,编译的规则影响了内存的分配。我们在使用SSD格式化的时候,有一个选项为4K对齐,4K对齐是为了让操作系统的最小分配单元和闪存的一个页对应,提高读写效率,实际使用过程中,即使文件没有那么大,实际占用的均为4KB的倍数,在这一点上和编译规则有些相似。
前面,我们用[0]、[bx]的方法,定位内存单元的地址。本章介绍更为灵活的定位内存地址和相关的编程方法。
;and指令:逻辑与指令,按位进行与运算
mov al,00001111B
and al,11110000B
;执行后AL=00000000B 相应位设为0
;or指令:理解与指令,按位进行或运算
mov al,00001111B
or al,11110000B
;执行后 AL=11111111B 相应位设为1
计算机中所有的信息都是二进制,而人能理解的信息是具有约定意义的字符。将字符存储在计算机中,就要对其进行编码。计算机存储的信息展示给我们看时,就要对其进行解码。
ASCII是基于拉丁字母的一套编码系统。例如,文件编辑过程中,按一下按键的“a”键,计算机用ASII码规则编码为61H存储在内存中;文件编辑器从内存中取出61H,送入显卡上的显存中;显卡用ASII码的规则解释显存的内容,显卡驱动显示器,我们在显示器看到了字符“a”。
我们可以在汇编程序中,用‘……‘的方式指明数据是以字符的形式给出,编译器将转化为相对应的ASCII码。
在codesg中填写代码(我这里写好了),将datasg中的第一个字符串转化为大写,第二个字符转化为小写。
查看字母的ASCII表。
可以发现,大写字母到小写字母在于寄存器中第5个字符的不同(我没说错,从右往左数,从0到7),那么这道题的关键在于将第5个字符置0的转换了,写出上面的代码就很简单了。
前面使用[bx]的方式来指明一个内存单元,还可以使用[bx+idata]来表示内存单元,他的偏移地址为[bx]+idata。
有了这个特性,前面我们做过一道将a段和b段的内容相加到c段中的题目,可以将代码优化。
可以看出灵活的内存访问方式,减少了指令,加快的程序运行速度。
SI和DI是8086CPU中和BX功能相近的寄存器,SI和DI不能分成两个8为寄存器来使用
在前面,我们用[bx]和[bx+idata]的方式来指明一个内存单元,还可以使用更为灵活的方式:[bx+si]和[bx+di]
[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)(即bx中的数值加上si的数值)
[bx+si+idata]表示一个内存单元,偏移地址为(bx)+(si)+(idata)。
如下图,将datasg段中的每个单词改为大写字母
db
指令和dw
指令类似,不过他定义的是字节型数据
总共数据有4行,每行有3个字母需要更改,也就是$4\times3$此二重循环,有限的循环可以使用loop
指令,这里需要存储两个循环次数,经过艰苦的思考(并没有,我想不出来,看书上思路了),可以使用空寄存器DX暂存,循环完成后又拿回(下图左)。
程序中进场需要进行数据的暂存,寄存器的数量有限,如果不适用寄存器,只能使用内存了,我们开辟了新的一块内存,先存放在内存中,需要的时候在从内存单元中恢复(下图中)。
我们使用内存来暂存数据,这是比较聪明的选择,但是值得推敲的是,我们用怎样的结构来保存这些数据,从而使程序更为清晰。
一般来说,在需要暂存数据的时候,都应该使用栈。
我们使用栈暂存数据,采用相关的指令将数据入栈,需要时在出栈(下图右)。
为什么要用[bx+si+data]的形式来表示?
程序如何改进?
定义描述性的符号reg和sreg,reg表示一个寄存器,sreg表示一个段寄存器。
reg:x、bx、cx、dx、ah、al、bh、bl、cx、bl、dh、dl、sp、bp、si、di;
sreg:ds、ss、cs、es
[…]
中进行内存单元寻址[…]
可以单个出现,或只能以4中组合出现:bx和si、bx和di、bp和si、bp和di.机器指令不关心数据的值多少,而关心指令执行前一刻,它将要处理的数据所在的位置。指令执行前,处理的数据可以在3个地方:CPU内部、内存、端口(后面介绍)
数据存放在内存中的时候,可是用多种方式给定这个内存单元的偏移地址,这种定位内存单元的方法一般被称为寻址方式。
8086CPU中,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作。
mov ax,1
与mov al,1
X ptr
指明内存单元长度,X可以为word或byte顺便说一下,[bx].10h[si]=[bx+16+si]
。
div是触发指令
单纯看这段话容易看懵,在debug模式中试验下。
说明,这里演示的是$\frac{16}{3}=5\cdots\cdots1 $,其中16的部分在指令中我使用的是十六进制”10H“。
前面使用db
和dw
定义字节型数据和字型数据。dd用来定义dword(double word,双字)型数据
dup是一个操作符,同db、dw、dd等一样,也是由编译器识别处理的符号。配合db、dw、dd等数据定义伪指令使用,用来进行数据的重复。
使用的格式如下
db 重复的次数 dup (重复的字节型数据)
dw 重复的次数 dup (重复的字型数据)
dd 重复的次数 dup (重复的双字型数据)
这里基本将所有的知识都运用起来了,笔者自己做的时候感觉自己好渣,想不出来。还是需要进行分析,理解数据从哪里来,到哪里去,中间做了什么。
题目是将data中的年、收入、计算的人均收入写到table段中。直接给代码。
这个实验的段名仿佛在暗示什么,回顾我们做了什么,我们将零散的数据结构化,使数据阅读性提升,使用偏移地址访问非常遍历。换句话说,如果编写程序时,将数据结构化,程序效率也将提升。程序的效率与数据组织的合理不合理有关,有一门课程叫数据结构,讲的就是这么个问题。
本质上可以归纳为对数据的组织,而下一章转移指令的原理,本质上是对代码的组织。