![fbd9970fdb0e40e3aff88244ff7761db.png](https://i-blog.csdnimg.cn/blog_migrate/af304f0ddb073390a37584943d0838ec.jpeg)
是时候关注我们一波了
包含多个段的程序
前面说道,如果要使用安全的内存空间,0:200~0:2FF是相对安全得内存空间,可是这段空间只有256字节,如果需要的空间大于256字节该怎么办呢?
在操作系统允许的情况下,程序可以取得任意容量的空间。
取得空间的方法有两种。
- 加载程序时为程序分配
- 执行过程中向系统申请(这里不讨论)
若要一个程序在加载时取得所需的空间,则必须在源程序做出说明。
上面是从内存空间获取的角度上,谈定义段的问题。为了可读性、功能设计,一般一额定义不同的段来存放。
关于段的问题,我们将以这样的顺序讨论多个段的问题:
- 在一个段中存放数据、代码、栈;
- 将数据、代码、栈放入不同的段中。
在代码段中使用数据
我们可以在程序中,定义我们希望处理的数据,数据作为程序的一部分一同被编译、链接写到可执行文件中。
考虑这样一个问题,编程计算8个数据的和,结果存放在AX寄存器中,下面是用我们前面知识写出的代码。
![7310e73c0e2dd05bbb0918a190086ed9.png](https://i-blog.csdnimg.cn/blog_migrate/c58caad76a60e87d0bc1e9c4a7bf83d3.jpeg)
这里出现了一个新的指令dw,dw即“define word”,在这里,定义了8个字型数据,占用16字节的内存空间。
使用Debug调试程序,不运行,发现一个问题,程序所在的内存区为075C:0(DS=075C),前256字节存放着PSP,程序的存放位置应为076C:0,使用U指令查看确发现有点不对。
实际上,看到其实是有dw定义的数据,从第16字节开始才是汇编指令对应的机器码。
![4e740025986e96f013eaddb2ca389286.png](https://i-blog.csdnimg.cn/blog_migrate/3a316ef47336e527d6dc59ca86bc4e4a.jpeg)
![3441e68637adac9369f95634cbe0cd46.png](https://i-blog.csdnimg.cn/blog_migrate/15b91f9e6e6795b0054160319d3ff16f.jpeg)
怎样执行程序中的指令呢?在Debug中,可以手动修改IP寄存器的值,从而使CS:IP指向程序的另一条指令。
这样一来,在系统运行时就会出现问题,程序的入口不是我们希望执行的指令。
借助伪指令可以通知编译器程序的入口。
![aadef20ca30980e8255c7d07f2317d17.png](https://i-blog.csdnimg.cn/blog_migrate/0ffdbd01106da9fff4c02361abfdad5d.jpeg)
有了这个指令,可以仿照这个模板写出更多的程序,start上面安排数据,start和end start之间安排代码。
在代码段中使用栈
![e3a8173aad97c1093ca8ef9ffff447cd.png](https://i-blog.csdnimg.cn/blog_migrate/69434c807e4480f0772e35a1bf776623.jpeg)
这里的检测点没做出来,看视频才想通的,后来发现这个题目第一眼没看懂。
检测点考察dw定义的数据在内存空间的位置,理解了这一点,题目就可以做出来了。
注释未知的指令,在debug模式中运行可以直观的感受到到这一点。
将数据、代码、栈放入不同的段
前面的内容中,我们将数据、栈和代码都放到了一个段里面,编程的时候需要注意何处是数据,何处是栈,何处是代码。显然这样有问题:
- 程序混乱
- 8086的段空间限制只有64KB
下面的程序用不同的段实现了上面的功能
![a3f46f9467443fa43fe7ca3154e3a70e.png](https://i-blog.csdnimg.cn/blog_migrate/93a8f7b48a3565d0743fddd246716fe0.jpeg)
- 对于不同的段,使用不同的段名联系不同的段寄存器。
- 注意红线的部分,段名相当于一个标号,代表了段地址,8086CPU不允许将一个数值送入段寄存器,因此使用其它寄存器中转
- “代码段”、“数据段”、“栈段”完全是我们的安排
- CPU如何处理定义的段的内容,取决与程序中具体的汇编指令。
- 段名只是为了阅读性
- cs:code等代码将段名和寄存器联系起来
- end start指明了程序的入口,CS:IP指向这个入口,从而执行程序的第一条指令
编写、调试具有多个段的程序
这里是两个检测点,为了理解不同段在内存空间中的排列,一个段在内存空间中最小单位为16字节。
限于篇幅原因,我这里介绍比较最后的一个实验。
程序如下,编写code段的代码,用push指令将a段中的前8个字型数据逆序存储在b段中。
![04f290def0185421ad16cebdd6275d08.png](https://i-blog.csdnimg.cn/blog_migrate/ca6dc14015263f2cb211cddfa353b74e.jpeg)
写出程序很容易,不过这不是我要说的重点。
![ddebafb51b62a8ee9b006d690c8a1c3f.png](https://i-blog.csdnimg.cn/blog_migrate/7797e692a2cba1d2f062a7a187e04da3.jpeg)
程序运行完之后,查看内存空间,注意我这里查看的DS。
![dc1456c427bd85b19538410e9e8b07b5.png](https://i-blog.csdnimg.cn/blog_migrate/cd5fa5a2362c44e535db077d78dda170.jpeg)
从数据对应关系不难判断,76C:00~76C:1F是我们定义的数据段, 076C:20~76C:2E是我们定义的栈段,76C:30之后是代码段。
![3d35060967313d2bcf03a933868a9d9d.png](https://i-blog.csdnimg.cn/blog_migrate/b8f416aceb8c3d0b480c3c24a6064957.jpeg)
查看对应的寄存器,也验证了这一点,
$076C imes 10H+0=76C0H=76C imes 10H+0$
$076C imes 10H+20=76E0H=76E imes 10H+0$
$076C imes10H+30=76F0H=76F imes 10H+0$
这里主要涉及到了一些段编译的规则,编译的规则影响了内存的分配。我们在使用SSD格式化的时候,有一个选项为4K对齐,4K对齐是为了让操作系统的最小分配单元和闪存的一个页对应,提高读写效率,实际使用过程中,即使文件没有那么大,实际占用的均为4KB的倍数,在这一点上和编译规则有些相似。
更灵活的定位内存地址的方法
前面,我们用[0]、[bx]的方法,定位内存单元的地址。本章介绍更为灵活的定位内存地址和相关的编程方法。
and和or指令
12345678;and指令:逻辑与指令,按位进行与运算mov al,00001111Band al,11110000B;执行后AL=00000000B 相应位设为0;or指令:理解与指令,按位进行或运算mov al,00001111Bor al,11110000B;执行后 AL=11111111B 相应位设为1
以字符形式给出的数据
计算机中所有的信息都是二进制,而人能理解的信息是具有约定意义的字符。将字符存储在计算机中,就要对其进行编码。计算机存储的信息展示给我们看时,就要对其进行解码。
ASCII是基于拉丁字母的一套编码系统。例如,文件编辑过程中,按一下按键的“a”键,计算机用ASII码规则编码为61H存储在内存中;文件编辑器从内存中取出61H,送入显卡上的显存中;显卡用ASII码的规则解释显存的内容,显卡驱动显示器,我们在显示器看到了字符“a”。
我们可以在汇编程序中,用‘……‘的方式指明数据是以字符的形式给出,编译器将转化为相对应的ASCII码。
大小写转换的问题
在codesg中填写代码(我这里写好了),将datasg中的第一个字符串转化为大写,第二个字符转化为小写。
![6308b4a8d825f2642c461cad8509811e.png](https://i-blog.csdnimg.cn/blog_migrate/32f03fa6d6e155930f8ed0b5c8bc98bb.jpeg)
查看字母的ASCII表。
![e8f9045cf2c946cbf7b4c2b53c4a8909.png](https://i-blog.csdnimg.cn/blog_migrate/5b663059d8099c65cfb968190c1f3a33.jpeg)
可以发现,大写字母到小写字母在于寄存器中第5个字符的不同(我没说错,从右往左数,从0到7),那么这道题的关键在于将第5个字符置0的转换了,写出上面的代码就很简单了。
[bx+idata]
前面使用[bx]的方式来指明一个内存单元,还可以使用[bx+idata]来表示内存单元,他的偏移地址为[bx]+idata。
有了这个特性,前面我们做过一道将a段和b段的内容相加到c段中的题目,可以将代码优化。
![32aa5d321da8444aa075c0f6e3a280dd.png](https://i-blog.csdnimg.cn/blog_migrate/c2d03f9226ac741f9c29bb5a1aeaf09d.jpeg)
可以看出灵活的内存访问方式,减少了指令,加快的程序运行速度。
SI+DI
SI和DI是8086CPU中和BX功能相近的寄存器,SI和DI不能分成两个8为寄存器来使用
[bx+si]和[bx+di]
在前面,我们用[bx]和[bx+idata]的方式来指明一个内存单元,还可以使用更为灵活的方式:[bx+si]和[bx+di]
[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)(即bx中的数值加上si的数值)
[bx+si+idata]和[bx+di+idata]
[bx+si+idata]表示一个内存单元,偏移地址为(bx)+(si)+(idata)。
不同的寻址方式的应用
- [idata]用一个常量表示内存地址,可直接定位一个内存单元
- [bx]用一个变量表示内存地址,可间接定位一个内存单元
- [bx+idata]用一个变量和常量表示内存地址,可在一个起始位置的基础上用变量间接定位一个内存单元
- [bx+si]用两个变量表示地址
- [bx+si+idata]用两个变量和一个常量便是地址。
如下图,将datasg段中的每个单词改为大写字母
![af0d9a8f7fe6bb7a9a72c71916c50f10.png](https://i-blog.csdnimg.cn/blog_migrate/d382e12bbae4c8713296df89b9eb70c8.jpeg)
db指令和dw指令类似,不过他定义的是字节型数据
总共数据有4行,每行有3个字母需要更改,也就是$4imes3$此二重循环,有限的循环可以使用loop指令,这里需要存储两个循环次数,经过艰苦的思考(并没有,我想不出来,看书上思路了),可以使用空寄存器DX暂存,循环完成后又拿回(下图左)。
程序中进场需要进行数据的暂存,寄存器的数量有限,如果不适用寄存器,只能使用内存了,我们开辟了新的一块内存,先存放在内存中,需要的时候在从内存单元中恢复(下图中)。
我们使用内存来暂存数据,这是比较聪明的选择,但是值得推敲的是,我们用怎样的结构来保存这些数据,从而使程序更为清晰。
一般来说,在需要暂存数据的时候,都应该使用栈。
我们使用栈暂存数据,采用相关的指令将数据入栈,需要时在出栈(下图右)。
![28f19b660e13c09eecd9e23f4863ca70.png](https://i-blog.csdnimg.cn/blog_migrate/d564fcf114a1174a61f3a2dd85521698.jpeg)
为什么要用[bx+si+data]的形式来表示?
- 为了可阅读性,理解数据的起始,体现了偏移的思想
程序如何改进?
- 更多的数据入栈,比如上面的BX进行入栈
数据处理的两个基本问题
- 处理的数据在什么地方
- 要处理的数据有多长
reg和sreg
定义描述性的符号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
bx、si、di和bp
- 8086CPU中,只有这4个寄存器可以用在[…]中进行内存单元寻址
- 在[…]可以单个出现,或只能以4中组合出现:bx和si、bx和di、bp和si、bp和di.
- 指令没有显性给出段地址,段地址就默认在SS中。
处理的数据在什么地方
机器指令不关心数据的值多少,而关心指令执行前一刻,它将要处理的数据所在的位置。指令执行前,处理的数据可以在3个地方:CPU内部、内存、端口(后面介绍)
汇编语言中数据的表达
- 立即数:直接在包含在机器指令中的数据(执行前在CPU的指令缓冲器中),称为立即数(idata)
- 寄存器:数据在寄存器中,给出寄存器名使用
- 段地址(SA)和偏移地址(EA):段地址默认在DS中,若BP做为有效地址的一部分,段地址默认在SS中,可以显性给出段寄存器
寻址方式
数据存放在内存中的时候,可是用多种方式给定这个内存单元的偏移地址,这种定位内存单元的方法一般被称为寻址方式。
![0883a9003aa4d59cbfee627eeb3b8161.png](https://i-blog.csdnimg.cn/blog_migrate/5ac62c5a47daaf6d0b85c3fc503c01fc.jpeg)
指令的数据有多长
8086CPU中,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作。
- 通过寄存器指明要处理的数据的尺寸,mov ax,1与mov al,1
- 没有寄存器时,用操作符指明:X ptr指明内存单元长度,X可以为word或byte
顺便说一下,[bx].10h[si]=[bx+16+si]。
div指令
div是触发指令
- 除数:有8位和16位两种,在一个reg或内存单元中
- 被除数:默认放在AX或DX和AX中
- 如果除数为8位,则被除数则为16位,默认在AX中存放
- 如果除数为16位,被除数为32位,在DX和AX中存放,DX存放高16位,AX存放低16位
- 结果
- 如果除数为8位,AL存储除法操作的商,AH存储除法操作的余数
- 如果除数为16位,AX存储除法操作的商,DX存储除法操作的余数
单纯看这段话容易看懵,在debug模式中试验下。
![5cd131dca193ef4a68248bcdd536bced.png](https://i-blog.csdnimg.cn/blog_migrate/71e87cdaf853b77b980ef9edee5e2144.jpeg)
![c32aa07a98d46b2bba38215a49722be3.png](https://i-blog.csdnimg.cn/blog_migrate/66ebf620e19c034eefc3f52125d6b455.jpeg)
说明,这里演示的是$frac{16}{3}=5cdotscdots1 $,其中16的部分在指令中我使用的是十六进制”10H“。
伪指令dd
前面使用db和dw定义字节型数据和字型数据。dd用来定义dword(double word,双字)型数据
伪指令dup
dup是一个操作符,同db、dw、dd等一样,也是由编译器识别处理的符号。配合db、dw、dd等数据定义伪指令使用,用来进行数据的重复。
使用的格式如下
123db 重复的次数 dup (重复的字节型数据)dw 重复的次数 dup (重复的字型数据)dd 重复的次数 dup (重复的双字型数据)
实验七
这里基本将所有的知识都运用起来了,笔者自己做的时候感觉自己好渣,想不出来。还是需要进行分析,理解数据从哪里来,到哪里去,中间做了什么。
题目是将data中的年、收入、计算的人均收入写到table段中。直接给代码。
![0ce9753e3f7fad2da144b44da424e16a.png](https://i-blog.csdnimg.cn/blog_migrate/99d122db61f806113ae804b5d1b31aed.jpeg)
- 年份的传递,可以使用寄存器,使用栈明显是更好的选择
- 使用相应的寄存器存储偏移量,可以使用[bx+idata]访问数据节省寄存器
- 除法运算中被除数为双字,使用AX、DX分别存储低16位和高16位,将AX中的商传递到Table段
实验的反思
这个实验的段名仿佛在暗示什么,回顾我们做了什么,我们将零散的数据结构化,使数据阅读性提升,使用偏移地址访问非常遍历。换句话说,如果编写程序时,将数据结构化,程序效率也将提升。程序的效率与数据组织的合理不合理有关,有一门课程叫数据结构,讲的就是这么个问题。
本质上可以归纳为对数据的组织,而下一章转移指令的原理,本质上是对代码的组织。
总结
前两章的介绍都是为了理解数据在内存中如何排列,后一章理解数据在处理过程中的细节。