【笔记】8086汇编-CALL和RET指令

前言:本篇仅仅是笔记,整理于2017/8/18,结构可能会略有混乱,见谅。

-call和ret指令都是转移指令,它们都修改IP,或同时修改CS和IP。它们经常被共同用来实现子程序的设计。

-【10.1 ret和retf】

–ret使用栈中的数据,修改IP内容,实现近转移

–retf指令使用栈中的数据,修改CS和IP的内容,实现远转移

–ret指令的操作

—1)读取当前栈中的数据

—2)(sp)=(sp)+2

–retf指令的操作:相当于连续出栈两次,第一次出栈的是IP,第二次出栈的是CS。【也就是说:后入栈的将会被作为IP的值,先入栈的会被作为CS的值】

-【10.2 call指令】

–操作:

—1)将当前的IP或CS和IP入栈【注意,这里指的是call指令后的第一个字节的地址】【注意入栈顺序:CS先入栈占据高位,IP后入栈占据低位】

—2)转移

–call指令不能实现短转移,只能实现近转移和远转移

-【10.3 依据位移进行转移的call指令】

–和原来的jmp一样,16位位移=标号处的地址-call指令后第一个字节的地址,位移使用补码来表示,位移在编译时算出。

–语法:call 标号

—使用相当于先进行push ip,在执行jmp short ptr 标号

-【转移的目的地址在指令中的call指令】

–call far ptr 标号  实现的是段间转移

–执行步骤相当于:

—push CS

—push IP

—jmp far ptr 标号

–【注意:这一情况下,目的地址会出现在机器码中,低地址是IP,高地址是CS】

-【转移地址在寄存器中的call指令】

–语法: call 16位reg

–等价于:

—push IP

—jmp 16位reg

–这个实现的是段内近转移,转移的IP地址存储在ax中。

-【10.6  转移地址在内存中的call指令】

–语法:

—1)call word ptr 内存地址【段内转移】

—2)call dword ptr 内存地址 【段间转移】

–解释:

—读取word(16位)的指令只改变IP,而读取dword的指令依次改变IP、CS【CS放在高地址位】

-【10.7  call与ret配合使用】

–使用意义:我们使用call与ret配合,可以实现主程序与子程序的跳转,当我们想要跳转到子程序中,我们可以使用“call 子程序标号”,在子程序中设置最后一个指令为ret,这样在执行完毕子程序后会自动跳转回主程序。

–根据习惯,我们不妨将主程序标号名称设置为main,子程序标号可以自定义名称,或简单地设定为“sub1”等。

–→获得:子程序框架←

-【10.8  mul指令】

–作用:乘法

–两个相乘的数:要么都是8位,要么都是16位。如果是8位,一个默认放在AL中,另一个放在8位reg或内存字节单元中;如果都是16位,则一个默认AX,另一个放在16位reg或内存单元字型数据。

–结果:8位相乘在AX;16位相乘高位在DX,低位在AX。

–注意:可以将8位数通过补0来变成16位。

-【10.9  模块化程序设计】

–下面来看相关解决方法:

-【10.10  参数和结果传递的问题】

–关键解决:传递参数和返回值。

–【对于子程序,建议做好备注,这样方便日后理解,并且也可以便于他人使用】

–解决方法:定义两个寄存器,一个当做参数寄存器,另一个当做结果寄存器。

-【10.11批量数据的传递】

–在特殊的情况下,我们可以这样设计:将要批量处理的数据的首地址放在si中,然后将cx也在主程序中设置好(可以理解为这是一个需要两个参数数值的函数,这两个参数分别是首地址和数据长度),在子程序中设计循环。【比如我们要将全部的小写字母设置为大写字母】

–在大多情况下,我们使用栈来传递参数,C语言中往往也是这样,在进入函数时,我们可以将现有数据全部入栈,子程序返回时会全部出栈。

-【10.12  寄存器冲突的问题】

–我们可以将一个字符串最后定义一个0作为结尾,所以我们可以使用jcxz来跳转(在ret【return?】前加一个标号,跳转到这里),而不用实现知道字符串有多长。【高级语言中以\0结尾(只是C与C++?)】

–【关于此处的一个备注:由于要操作字符,请一定要将al存上字符的值,ah设置为0,也就是说要分两步操作,而不可以直接mov ax,byte ptr [bx],这样会得到一个“类型不匹配”的warning,并且,程序将无视我们限定的“一个字节”这个要求,而是直接读取了两个字节】

–对于连续的多个字符串,我们可以加上循环,循环次数就是字符串总数。【注意不要忘了将cx记录的循环次数提前入栈】

–我们发现了一个一般性的问题

—很多情况下,我们在子程序中使用的寄存器在主程序中也会被用到,这样很容易发生冲突现象,造成运行时的错误,我们需要寻找一个万用方案,或者说“通解”。

—解答:我们不想麻烦地分析哪个寄存器被使用,哪个没有,在大型程序中,这样的分析也不现实,所以我们使用了栈来临时存放这些数据,为此我们提出了一个常用框架。

—框架:

—-子程序开始:子程序中使用的寄存器入栈

—-                    子程序内容

—-                    子程序中使用的寄存器出栈

—-                    程序返回

—注意栈的特点LIFO

-【实验10  编写子程序】

–【子程序1  通用的显示字符串子程序】

—要求:提供灵活的调用接口,使调用者可以决定现实的位置、内容以及颜色。

—名称:print_str

—详细功能:在指定的位置,用指定的颜色,显示一个用0结束的字符串

—参数:(dh)=行号(取值范围0~24),(dl)=列号(取值范围0~79),(cl)=颜色,ds:si指向字符串的首地址。

—返回值:无

—开始设计:首先确定需要的功能:1)正确读取并存放参数,2)正确读取字符串,3)正确显示(存放数据)

—-不妨假设,现在dh、dl、cl、ds、si已经存放好了需要的数据。那么我们知道,彩色字符显示模式显示区域在B8000H~BFFFFH,总共64KB,由于要给定行号和列号,所以我们想先将这个存放在第0页,由于每个字符的属性的存放地址都比字符本身存放地址大1,所以这个1可以通过idata来表示,我们可以使用di确定起始位置,es存放段地址B8000H,那么如何知道具体偏移地址呢,这就要通过dh和dl中的数值进行计算了,由于每行是80列,我们不妨把除最后一列后的行数乘80*2,再加上列的个数数,就可以进行定位,这其中我们应当注意几个细节,首先行数与列数是从0开始的,所以行号=实际行的个数-1,比如在第三行,它的行号是2,所以我们应当直接使用通过dh传递来的行号来乘80*2,而列号应当怎么使用呢?不妨假设现在是在0行,要从第三个(列号为2)列来输出,它的地址应当是列号乘2(因为列号实际上就是表示了前面一共有几个字符存在了,所以只要将行首的第一个字符的起始地址加上中间的内存单元个数,就可以得到现在的内存单元地址了),所以我们知道了(di) = (dh)*80*2 + (dl)*2,由于这是典型的8位乘法,所以我们需要使用ax寄存器,为了减少操作步骤,所以我们先将ax的值设为160,之后使用mul dh,这样就可以在不动dh、dl的情况下进行计算,但是,我们发现一个问题,也就是说,我们计算好了dh*160后,我们还要再算dl*2,但是ax现在已经被占用了,为了方便,我们不妨改进一下流程(虽然临时存储到内存单元中也是可以的,但这里想要设计地尽量不再访问内存单元),我们不妨做出如下改进:1)将al设置为80;2)使用mul dh指令;3)将dl的值加到ax中;4)对dx进行赋值为2(之所以使用dx,而不是dl,是因为我们这里要使用16位与16位相乘,ax是16位的);5)使用mul dx,我们可以估算到,这个乘积最大是40FEh,所以乘积不需要使用dx来存放,我们可以放心地将ax的值放到di中。以上我们关于寻址的设计已经完成,之后我们需要设计一下字符与属性读取及存放的部分程序:

—-我们需要使用循环来读取字符串,这个循环应当包括跳转部分(用来识别0),转存部分,所以我们分开来设计:

—–跳转部分:我们这时有一个想法,那就是,循环的控制应当在这个子程序之外,而跳转程序只需要做一件事——判断是不是0,是0则跳出,不是0则进入转存,所以我们自然要用到jcxz来判断,因此我们会首先将字符传送到cx中(所以我们需要提前将存储着循环次数的cx的值入栈),然后,我们便要紧跟上jcxz指令,跳转到一个标号处,这个标号指向了ret或retf指令(具体是哪一个,要再后边讨论,如果段内转移能够满足要求,那就使用ret即可),而在jcxz指令后边,则应当是访问转存指令,然后这条指令的下一个指令就是刚才的ret或retf。以上,跳转部分设计完毕,现在重新来观察,我们发现,这个跳转部分实际上是实现了判断一个字符是否为0的功能。

—–转存部分:当进入转存部分时,cx中一定存放着字符数据,所以我们首先将cx值存放到之前准备好的es:di中,然后我们再将属性存到es:[di+1]中,可是这时我们发现,原先的颜色属性是在cl中存放的,而刚才跳转部分的设计已经将这个覆盖了(因为jcxz必须要判断完整的cx的值),我们不妨对跳转部分的设计进行修改,让跳转部分在最开始时,先push cx,然后再jcxz指令之后,再使用pop cx,之后再进入转存部分,可是这样的话,我们又会发现,刚刚字符数据不见了,并且这个设计再每次循环中都要多次访问内存,这会使计算机处理所需时间增长,这显然不是我们想要的;我们不妨在进入跳转部分前就将cl的值给另外一个寄存器来存放,比如在循环存放步骤中没有被使用到的al,至此问题解决了,我们先在跳转中将cl设置为字符数据,在转存部分先存cl,再存al,之后离开转存部分,回到跳转部分的开头处。(因为既然这个字符进入了转存部分,那么这个字符一定不是最后一个字符)

—-我们总结一下这个子程序:

—–栈的定位→入栈部分→存储地址计算部分→循环部分→出栈部分→返回部分

—–这个子程序中,我们使用到了ax,bx,cx,dx,si,di,ds,ss,es。其中,ds是没有被改变的,我们不需要将它入栈,其它的部分都进行了一定程度的改变,所以我们在子程序最开始部分,要执行一个入栈部分,这个栈应当足够大,根据我们要存储的数据,发现需要18个字节,所以我们将这个栈的大小声明为32个字节。

—–存储地址计算部分已经如上所述,返回结果是,di中存放了正确的偏移地址。

—–循环部分:

——循环部分最开始应当是循环初始化操作,我们将cl存储的属性值传给al,然后用cx设定循环次数【这里出现了错误,由于我们要使用跳转来判断,所以不需要循环次数】,然后访问跳转部分(转存部分由跳转部分内部进行跳转),之后就是参数变化部分,这一部分,我们要让si自增1,di自增2。

——之后就是循环部分的返回部分

—–子程序的出栈部分

—–最后就是这个子程序的返回部分

–子程序3:数值转字符:

—ASCII有这样的规律:数值+30H = 数字的ASCII码

—我们可以先按照倒过来的顺序存放,之后再进行一次倒序,这样就可以得到正确顺序的字符串。

【笔记】8086汇编-更灵活的内存控制

前言:本篇仅仅是笔记,整理于2017/8/4,结构可能会略有混乱,见谅。

-本章核心:灵活定位

-关键知识:

–二重循环

–栈的应用

–大小互换

–and和or

-【and和or指令】

–and指令:逻辑“与”指令,按位进行“与”运算

—都为1时结果才为1【同真为真】

—常用技巧:可将特定位设置为0,方法是:

—-and al,10111111B,这样可以将第6位设置为0,其它位不变

–or指令:逻辑“或”指令,按位进行“或”运算

—常用技巧:可将特定位设置为1,方法是:

—or al,01000000B,这样可以将第6位设置为1,其它位不变

-【关于ASCII码】

–我们按下a键,屏幕是如何显示出a呢?

—按下的同时,会将61H(a的ASCII码)传送到内存空间,文本编辑器取出这个61H再放到显卡的显存中;工作在文本模式下的显卡,用ASCII码的规则解释内容,显卡驱动显示器,将字符呈现在屏幕上。

-【用字符形式给出的数据】

–示例

—db ‘test’

—1)注意要使用单引号

—2)注意要使用db

–备注:一个英文字符对应1个字节

-【大小写转换问题】

–关键在于寻找到规律:大写比小写字母的二进制小10000B(也就是20H)

—但是,我们并不会使用条件判断,这样我们就无法判断一个字母是大写还是小写,呢么我们应当换一个思路:我们可以用or与and,因为大小写字母的区别仅仅是:大写字母第5位是0,小写字母第5位是1。

-【[bx+idata]】

–指令mov ax,[bx+200]可以写成如下格式

—1)mov ax,[200+bx]

—2)mov ax,200[bx]

—3)mov ax,[bx].200

–使用这个指令后,偏移地址为(bx)+idata

-【使用[bx+idata]实现对数组的处理】

-【SI与DI】

–在8086CPU中,SI与DI与BX作用类似,但是SI、DI不可以分成两个八位寄存器来使用。所以可以使用[si]、[di],并且也可以增加idata。

–习题:使用寄存器SI和DI实现字符串复制

—注意mov [si],[di]是非法指令,也就是说无法直接将内存单元的数据传给另一个内存单元。

—习惯上,我们习惯用si指向原始地址,用di指向目标地址。

—实际上,这里我们可以使用数组(统一)的思维来处理,直接用bx指向原始地址,bx+idata指向目标地址。

-【[bx+si]、[bx+di]】【下面使用bx与si进行解释】

–偏移地址将会是(bx)+(si)

–mov ax,[bx+si]指令也可以写为

—mov ax,[bx][si]

-【[bx+si+idata]】

–也可以写作:

—mov ax,200[bx][si]

—mov ax,[bx].200[si]

—mov ax,[bx][si].200

—mov ax,[200+bx+si]

—mov ax,[200+bx][si]

—【需要注意:如果数据在后面,则一定要加”.”】

-【不同寻址方式的灵活应用】

–灵活使用实际上是让我们能够以更加结构化的方式来看待数据。

–【编程错误记录,备注:这里在调试程序时,不小心把结束指令中mov ax,4c00h写成了mov cx,4c00h,最终运行到int 21h时,使用debug中的p指令,提示FCB unavailable,下面一行提示Abort, Fail? 这时,输入f就会自动跳到下一条指令。(这一问题的原理暂时不了解,或许与中断机制有关,有待后日学习)】

–这里有一道题目,用到了双层循环,对于这类问题,我们需要将数据暂时存放,有些时候,我们可以将临时数据存放到空闲的寄存器中【这道题我们就是使用了dx来临时存放外层cx的状态】,但是寄存器的数量总是有限的,所以我们的解决办法是:提前开辟一段内存空间,我们将数据存储在这部分内存空间中。但是还有一个问题,这里我们只存放了一个数据,可是我们有时会遇到多个数据,这个时候如果我们还是用定值来确定内存位置会十分麻烦,所以:遇到要临时存放的数据,我们通常会使用栈来存放

–【备注:如果cx一开始为0,这样,运行到loop时,cx-1会得到ffffh】