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