转移指令的原理
可以修改IP,或同时修改CS和IP的指令通称为转移指令。
8086CPU的转义行为有一下几类。
jmp ax
。jmp 1000:0
由于转移指令对IP的修改范围不同,段内转移又分为:短转移和近转移。
8086CPU的转移指令分为以下几类。
这些转移指令的区别在于前提条件不同,但转移的原理是相同的。
我们在这里通过深入学习无条件转移指令jmp
来理解CPU执行转移指令的基本原理
offset
的功能是取的标号的偏移地址,该指令由编译器执行。
jmp
为无条件转移指令,可以只修改IP,也可以同时修改CS和IP。
jmp
指令需要给出两种信息:
jmp
指令jmp short 标号(转到标号处执行指令)
我们从一段汇编程序开始。
观察这段汇编指令对应的机器码,汇编指令中的[idata]立即数,不论是否是数据还是内存单元的偏移地址,都会在对应的机器指令中出现,CPU执行的机器指令,它必须要处理这些数据和地址。
注意jmp
指令一行,机器指令中不包含转移的目的地址。
多次测试可以发现,CPU执行jmp
指令的时候不需要转移的目的地址。
回忆CPU执行指令的过程。
jmp
指令对应的机器码EB03
中的’03‘其实是转移的位移。
jmp short 标号
的功能为:(IP)=(IP)+8位移。
类似的指令jmp near ptr 标号
,实现的是段内近转移,不过它的位移范围是-32768~32767。
这里简单介绍下这个位移范围如何得到,这和补码的含义有关,正数的补码就是其本省,负数的补码是在原码的基础上,符号位(第一位)不变,其余各位取反,最后+1,8位位移的范围只能11111111~01111111(-128~127),16位位移也类似。
jmp
指令前面说的jmp far ptr 标号
指令,对应的机器码为”EA 0801 6C07”,这里我特意空格分隔开,方便观察。
“EA 0801 6C07”是在指令中的内存中的排列顺序,高地址“6C07”是转移的段地址:076C,低地址的“0801”是偏移地址”0108”
jmp
指令指令格式:jmp 16位 reg
,功能(IP)=(16位reg),这里不在详述。
jmp
指令jmp word ptr 内存单元地址
(段内转移)功能:从内存单元地址处开始存放着一个字,是转移的目的偏移地址。
内存单元地址可用寻址方式的任一格式给出。
jmp dword ptr 内存单元地址
(段间转移)功能:从内存中单元地址处存放着两个字,高地址处的字是转移的目的段地址,低地址处是转移的目的偏移地址。
jcxz
指令jcxz指令为有条件转移指令,所有的有条件转移指令都是短转移,对IP的修改范围:-128~127(用补码表示)。
8位位移由编译程序在编译时算出。
功能:如果$()(CX)=0$,转移的标号处执行;如果$(CX) \neq0$,什么也不做(程序向下执行)。
jcxz= jmp CX zero
loop
指令loop
指令为循环指令,所有的循环指令都是短转移。
可能有读者说,这不和
jcxa
指令、jmp short 标号
的性质差不多。是的,确实类似,所有有部分我忽略没写。
它的功能和jcxz
类似又有不同:先将$(CX)=(CX)-1$,如果$(CX) \neq0$,转移到标号处执行;如果$()(CX)=0$,什么也不做(程序向下执行)
到现在为止,已经用过的指令有:
jmp short 标号
jmp near ptr 标号
jcxz 标号
loop 标号
它们对应的机器码中只有到目的地址的位移,这种设计方便了程序段在内存中的浮动装配,
如果在程序段写入内存地址,则对程序的执行有了前提限制。
根据位移进行转移的指令,它的转移范围受到转移位移的限制,如果源程序中出现了转移位移超界的问题,编译时,编译器将报错。
前面在Debug模式中使用过jmp 2000:0100
的转移指令,汇编编译器并不认识。源程序中使用编译会出错。
程序如上,我们一步步来理解。
start
开始执行,使用offset
分别获取了标号“s”、”s2“处的偏移地址,并保存在DI、DI寄存器中。mov
指令将标号”s2“处的字型数据复制的AX中,也就是指令jmp short s1
8位位移=标号出的地址-jmp指令后的第一个字节的地址,联系CPU执行指令的过程,这个位移实际上就是标号与jmp
指令的长度,也就是说从nop
指令到标号“s1”处的指令长度为位移,这里为10个字节。往前面移动,也就是负的,得到补码”F6“,机器码为”EBF6”。
nop
指令被覆盖jmp short s
,这里的位移计算不在详述,跳转到标号”s”处,mov ax,4c00h
,CALL
和RET
指令CALL
和RET
指令都是转移指令,它们都修改IP,或同时修改CS和IP。
这一章,我们讲解call
和ret
指令的原理
ret
和retf
ret
指令用栈中的数据,修改IP的内容,从而实现近转移。
retf
指令用栈中的数据,修改CS和IP的内容,从而实现远转移。
CPU执行ret指令时,进行了下面两步操作:
CPU执行retf
指令时,进行了下面4不操作:
估计你有点懵,用汇编指令来解释ret
和retf
指令,则:
CPU执行ret
指令时,相当于pop IP
CPU执行retf
指令时,相当于进行:
pop IP
pop CS
call
指令CPU执行call
指令时,进行两步操作:
call
指令call 标号
(将当前的IP压入栈后,转到标号出执行指令)
CPU执行此种格式的call
指令时,进行了如下的操作:
16位位移=标号出的地址-call指令后的第一个字节的地址;
16位位移范围为-32768~32767,用补码表示;
16位位移由编译程序在编译时算出
用汇编指令解释,相当于。
call
指令前面讲的call
指令,对应的机器码中并没有转移的目的地址,而是位移。
call far ptr 标号
实现的段间转移
CPU执行此格式的call
指令是,进行了如下操作。
用汇编指令解释,相当与进行。
push CS
push IP ;IP为call指令的下一条指令的IP
jmp far ptr 标号
call
指令指令格式:call 16位 reg
功能:
汇编指令解释,相当于进行:
push IP;同上
jmp 16位reg
注意:这里的地址应理解为偏移地址(IP寄存器中存放),请回忆jmp
指令的原理。
call
指令转移指令在内存中的call
指令有两种格式。
call word ptr 内存单元地址
用汇编指令解释:
push IP;同上
jmp word ptr 内存单元地址
call dword ptr 内存单元地址
用汇编指令解释:
push CS
push IP
这里检测点第一题居然书貌似错了,看视频果然不一样。 因为call指令执行过程中对栈进行了操作,为了分析,可以手动记录栈空间的变化,这样易于分析。
call
和ret
的配合使用先分析一段程序。
call
指令读取后,IP指向下一条指令mov ax,4c00h
,指令执行,将IP中的值入栈,IP寄存器指向标号”s”处。add ax,ax
。ret
指令执行后,相当于进行了pop IP
,IP更改,IP重新指向mov ax,4c00h
回忆一下call
指令和ret
指令的功能。
call 标号
指令相当于。push IP
jmp near ptr 标号
ret
指令相当于。pop IP
在回顾这个程序,我们将标号“s”处的具有一定功能的程序段称为子程序,call
指令在转去执行子程序之前,call指令将后面指令的地址存储到栈中,在子程序末,使用ret
指令,用栈中的数据设置IP中的值,回到call指令后的代码处继续执行。
我都说到这里,有没有人有大胆的想法:-P。
标号:
指令
ret
我们在指令中可以使用call 标号
,再通过ret
返回不断的执行后续的call
。
这样写有什么好处?
我们说要保持源程序的阅读性,随着功能的不断增加,代码阅读性下降是必然的,而上述的这种结构,为我们提供了一种组织代码的方式,是的大量代码的情况下仍然有一定的阅读性。
mul
指令因下面会用到,这里介绍下mul
指令,mul
是乘法指令,使用mul
做乘法时,需要主要以下几点。
从上面我们看到,call
与ret
指令共同支持了汇编语言程序编程中的模块化设计。利用call
和ret
指令,可以用简洁的方法,实现多个相互联、功能独立的子程序来解决一个复杂的问题。
下面,我们来看一下子程序设计过程中的相关问题和解决方法。
讨论参数和返回值传递的问题,实际上就是在探讨,应该如何存储子程序需要的参数和产生的返回值。
我们最先想到的是用寄存器了存储,对于存放参数的存储器和存储结果的存储器,调用者和子程序的读写恰恰相反:调用者将参数送入参数寄存器,从结果寄存器中取到返回值;子程序从参数寄存器中取到参数,将返回值送入寄存器中。
这是一段将data段第一组数据的3次方,结果保存在后面一组的dword单元中的程序,请理解此过程中的参数和结果传递。
注意,如果写好的程序你自己还要使用,或者要给别人使用,都请写好注释。
前面的程序参数和结果只有一个,可以用两个寄存器来存放,寄存器数量终究有限的,我们不可能简单第用寄存器来存放多个需要传递的数据。返回值也一样。
这里将批量数据放在内存中,所在内存空间的首地址放在寄存器中,传递给需要的子程序。
我们也可以用以前使用loop
指令来实现。
call
指令给我们的启发call
指令的原理不在详述,call
指令告诉了我们一种组织数据和组织代码的方式
上述过程可以描述为。
从组织的方式解释。
从计算机原理层面抽象。
有一个一般化的问题:子程序中使用的寄存器,很可能在主程序中也要使用,造成了寄存器使用上的冲突。
解决这个问题的简捷方法是,在子程序开始将子程序所有用到的寄存器的内存都保存起来,在子程序返回前恢复,可以用栈来保存寄存器中的内容。
要求使用子程序“show_str”,在指定的位置,用指定的颜色,显示一个用0结束的字符串。
提供的参数有:(DH)=行号,(DL)=列号
回忆一下实验九,这里我实现一种颜色。
根据要求不难写出如下代码(笔者其实边调试编写写了半小时)
优化一下。
该程序的内部处理和显存的结构相关,通过调用子程序,进行字符串的显示可以不必了解显存的结构,为编程提供了方便。
问题:div
指令可以做除法。进行8为除法的时候,用AL存储接结果的商,AH存储结果的余数;进行16位除法的时候,用AX存储结果的商,DX存储结果余数。
那么,下面的程序段呢?
mov ax,1000h
mov dx,1
mov bx,1
div bx ;商应为11000H,而11000H在AX中存放不下。
当CPU执行div
等除法指令的时候,如果出现这样的情况,将引发CPU的内部错误:除法溢出。
我们这里写个子程序来解决除法溢出的问题。
详细的解释过程,注释都有,这道题主要是这个公式:X/N=int(H/N)*65526+[rem(H/N)*65536+L]/N,证明过程不重要,其实和我们用的十进制除法是一样的。
功能:将word型数据转变为十进制数的字符串,字符串以0为结尾符。
如果不看提示,这个感觉做不出来。
分析:
jcxz
指令就可以完成。无能嘤嘤……
这个地方卡了一周(太难了,不想做,做一半,想不出,不想。重新开始想,圆原地踏步).使用8位除法得到余数,排列重新求余,B站找了别人的代码。你们感受一下。
和题意有点区别,代码我都注释了,看不懂的看我注释。
看不清的请移步个人博客
个人状态有点问题,最后一部分思考偏了,我本人很同一往一个地方深挖,后来明白是挖不完的,适度就好。想不通了不妨换个方向。
文;伍默
排版;喵喵
正文共:6167 字 21 图