8086跳转指令
目前为止,我们的程序的指令执行都是线性的,从上到下,由CPU自动的增加IP的值,顺序的执行指令。但对于复杂的需求,只有线性的指令执行方式是远远不够的。
对于高级语言,有着如if/else的逻辑跳转分支,如for/while的循环结构,还有函数子程序的调用与返回等等。正是有了这些能够控制程序执行指令的不同方式,才能具有足够的表达能力,满足足够复杂的需求,成为一门图灵完备的语言。那么上述的逻辑跳转、循环,在基于图灵机的CPU硬件上是如何实现的呢?通过8086汇编的跳转指令的学习,我们得以一窥究竟。
CPU是通过CS:IP来获取下一条指令的值,那么通过指令修改CS、IP这两个寄存器的值,便可以控制CPU所执行的指令了。可由于控制CPU执行指令的CS、IP十分的关键,因此8086并不允许像其它普通的寄存器一般使用mov等指令对CS、IP修改(mov IP,1000H是非法的),而是提供了专门的指令来控制CS、IP的值,这一类指令被称为8086跳转指令。
跳转指令按照类型可以分为五种:无条件跳转指令、有条件跳转指令、循环指令、过程调用与返回指令以及中断指令。
无条件跳转指令(jmp)
jmp既可以只修改IP,也可以同时修改CS和IP。作为跳转指令,在编程时需要指定跳转的位置,进而修改CS/IP的值。
段内转移
段内短转移(IP 变化-128~127):段内短转移的格式为 jmp short [标号]。
assume cs:codesg codesg segment start:mov ax,0 jmp short s add ax,1 s:inc ax codesg ends end start
段内近转移(IP 变化-32768~32767):当所要跳转的间隔大于短转移的时候,就需要使用段内近转移。段内近转移和短转移类似,格式为 jmp near ptr [标号]。
段内转移只修改IP,不修改CS的值。
段间转移
当跳转的间隔超过了段内近转移的限制时,就需要使用段间转移了。段间转移的格式为jmp far ptr [标号]。和内存寻址一样,jmp指令所要跳转的位置也可以通过寄存器或是指令中的立即数指定。
jmp寄存器跳转
jmp [16位寄存器] 例如 jmp ax,寄存器跳转属于段内跳转。
jmp内存跳转
jmp word ptr [内存单元地址] 例如: jmp word ptr 2345H,jmp word ptr [bx] ,[]内只要是符合内存寻址方式的语法皆可。jmp word ptr处理的是16位数,属于段内转移。
jmp dword ptr [内存单元地址] jmp dword ptr和jmp word ptr类似,只不过会将对应地址的处的两个字/四字节的数据作为偏移地址,其中IP等于指定的内存地址,CS等于指定的内存地址+2(示例)。jmp dword ptr处理的是32位数,属于段间转移。
跳转指令原理
就转移指令的实现原理来看,段内转移是通过相对地址偏移量来控制的。段内短转移可以使得IP偏移2^8的范围,即(-128~127),而段内近转移可以使得IP偏移2^16的范围,即(-32768~32767)。
8086的CPU是16位的,在20位的寻址范围内进行更大幅度的跳转,16位的偏移地址是不够的,因此段间转移的指令是通过绝对地址来实现的。
虽然理论上段内转移都可以使用段间转移来实现,但是由于不同的跳转指令所占用的内存空间是不一样的(段内短转移=8位指令+8位偏移地址=16bit,段内近转移=8位指令+16位偏移地址=24bit,段间转移=8位指令+16位段地址+16位偏移地址=40bit)。所以编程时,在满足需求的前提下还是尽可能的使用更简单,更节约内存的无条件跳转指令,提高效率。
jmp是最直接的无跳转指令,类似于C语言的goto。对于喜欢结构化编程的人来说,goto的跳转过于灵活很容易使得大项目中代码变得晦涩混乱,但是汇编程序所构建的项目不会特别大,jmp还是非常直接和方便的。(从另一个角度看,正是因为汇编语言的抽象能力不够强,导致很难构建出足够大型、复杂同时还很可靠的程序)
有条件跳转指令(jcxz)
jcxz有条件跳转指令,类似于段内短跳转jmp short,所能变化的ip范围同样为(-128~127)。格式为 jcxz [标号]。唯一的不同在于,只有当满足条件寄存器cx=0时,才会进行跳转,否则就和正常情况一样IP自增,按顺序执行下一条指令,这也是jcxz被称为有条件跳转指令的原因(只有满足条件才进行跳转)。
需要特别注意的是,通用寄存器ax/bx/cx/dx并不是完全等价的,在某些场合下会具有一些特别的作用,例如上述jcxz便依赖寄存器cx。其作用可以从寄存器的全名中可见一斑,ax/bx/cx/dx并不是英文字母abcd的简称,而分别是accumulate-register累加寄存器、based-register基地址寄存器、count-register计数寄存器、data-register 数据寄存器。
ax accumulate-register累加寄存器:ax一般用于存放算术、逻辑运算中的操作数或结果。同时I/O指令也都需要使用ax与外设接口传递数据。
bx based-register基地址寄存器 :bx一般用于存放访问内存时的地址。在8086内存寻址时,指定偏移地址时有提到过。
cx count-register计数寄存器:cx一般用于有条件跳转、循环、串操作指令。jcxz和loop循环等指令都依赖于cx寄存器。
dx data-register 数据寄存器:dx一般用于寄存器间接寻址中的I/O指令中存放I/O端口的地址。
循环指令(loop)
循环指令同样依赖寄存器cx。格式为loop [标号]。loop指令的语义是,首先将cx自减1,如果cx不为0,则跳转至标号处。否则什么也不做,离开循环,顺序执行下移。
循环指令的跳转范围和有条件跳转指令一样,ip的变化范围为(-128~127)。
用C风格的伪代码表示为:
cx--; if(cx == 0){ jmp short 【标号】 }else{ 顺序执行下一条指令 }
过程调用/返回以及中断指令(call/ret、int等)
过程调用以及CPU处理硬件中断时,同样涉及到了程序执行指令的跳转,分别对应了过程调用/返回指令(例如 call/ret),中断指令(例如 int)。
基于内容的相关性,过程调用会在后面的8086汇编子程序进行详细介绍,而中断指令则会在中断相关部分进行展开。
总结
不同的跳转指令都有着跳转间隔的限制,如果超出了跳转指令所约定的范围,则编译器会在编译时发现并报错。
跳转指令,特别是无条件跳转指令需要慎用,无所顾及的使用跳转指令很容易使得程序的可读性降低,变成一团剪不断,理还乱的面团。这在高级语言程序的开发中同样适用,人的大脑所能同时理解的内容是有限的,必须通过合理的抽象将复杂程序构建成有着良好结构的黑箱子。而随意的全局变量和不必要的输入/输出则会破坏这种模块化的结构,使人费解,在迭代中逐步脱离开发人员的控制,变成一个吞噬时间的无底洞。