CPU执行程序是依靠CS:IP来实现的,我们可以通过改变CS和IP的值来灵活的控制我们期望执行的程序。汇编中提供了丰富的转移指令来改变CS和IP的值。
汇编程序中有很多的标号使用,有些标号作为段的地址,如标号code, data。有些标号作为某一段程序的开始,如标号start,标号s。标号的名称可以自己命名。在程序中的标号代表了某一段指令的偏移地址,可以通过 offset 标号 指令来获取标号处指令的偏移地址,如:
assume cs:code
code segment
start:
mov ax, offset start
s:
mov bx, offset s
code ends
end start
虽然可以获取标号的偏移地址,但不把CS:IP指向对应的位置,还是不能控制程序按照期望的来执行,下面来看看汇编中提供了哪些指令来修改CS和IP的值的:
-
无条件转移指令 JMP
jmp short 标号
该指令可以跳转到标号处的位置执行相应的指令,但并不是直接去修改寄存器 IP 的值为标号处的偏移地址来控制跳转的,而是通过指定标号处的偏移地址和当前 IP 中的偏移地址的偏移量来修改 IP 的值从而控制跳转的。
该指令为段内短转移,可以跳转到偏移量为8位的指令处,所以前后偏移的范围为-128~127个字节。这个偏移量由编译器计算。如:
assume cs:code code segment start: mov ax, 0 jmp short s add ax, 2 ;此处占用3个字节 s: inc ax mov ax, 4c00H int 21H code ends code start
当CPU执行到 jmp short s 时,IP指向指令 add ax,2 处,当执行 jmp short s 后,会把IP + 3,从而指向 标号s 处开始执行,一直到程序结束。此时ax的值为1。
为了更好的理解该指令的跳转是靠偏移量来计算IP值的,可以看以下程序:
assume cs:code code segment mov ax, 4c00H int 21H start: mov ax, 0 s: nop ;空指令,占用一个字节,不做操作 nop mov di, offset s mov si, offset s2 mov ax, cs:[si] mov cs:[di], ax s0: jmp short s s1: mov ax, 0 int 21H mov ax, 0 s2: jmp short s1;占用两个字节 nop code ends end start
当程序执行完指令 mov cs:[di], ax,会把 标号s2 处的指令 jmp short s1 写入覆盖 标号s 处的两条指令 nop,在此程序中,jmp short s1 编译后实际指定了 IP 向上的偏移量,同理,在执行完下一条指令 jmp short s 后,IP会指向 标号s 处,此时会执行指令 jmp short s1,而该条指令实际是指定了 IP 向上的偏移量,执行完后,IP根据偏移量实际会指向mov ax, 4c00H处,然后程序退出执行完毕。而s1处的指令根本不会执行。
jmp near ptr 标号
该指令实现的是段内近转移,和 jmp short 标号 功能相近,都是通过偏移量来跳转的,只是该指令的偏移量为16位,即偏移量范围位:-32768~32767。同样由编译器计算得出。
jmp far ptr 标号
该指令为段间转移,又称远转移。该指令和上述的段内近转移指令不同,不是通过偏移量来跳转的,而是通过标号处的段地址修改CS,标号处的偏移地址修改IP,从而实现跳转的。如:
assume cs:code code segment start: mov ax, 0 mov bx, 0 jmp far ptr s db 256 dup(0);该指令表示定义256个字节,内容为0。而数据0000,又可以看成指令,所以此处相当于定义了若干条指令 s: add ax, 1 inc ax code ends end start
除了通过标号来控制程序跳转外,还可以通过寄存器或内存来直接修改CS或IP的值控制程序的跳转:
jmp 16位寄存器 ;段内转移,直接修改IP的值来实现跳转 jmp word ptr 内存单元地址 ;段内转移,通过内存单元中的内容修改IP的值 jmp dword ptr 内存单元地址 ;段间转移,通过内存单元中高16位的数值作为段地址修改CS的值,低16位的数值作为偏移地址修改IP的值
如:
mov ax, 0123H mov ds:[0], ax mov word ptr ds:[2], 0 jmp dword ptr ds:[0]
执行后,CX = 0, IP=0123H
-
有条件转移指令 jcxz
jcxz 标号
该指令和 jmp 指令类似,同样是段内短转移,通过标号处的偏移量来控制,偏移量范围为:-128~127。
该指令和寄存器 CX 联合使用,当 CX 中的值为 0 时才会跳转,否则,什么也不做,继续执行下一跳指令:
assume cs:code, ds:data data segment db 'abcd', 0 data ends code segment start: mov ax, data mov ds, ax mov bx, 0 s: mov ch, 0 mov cl, ds:[bx] jcxz ok inc bx jmp short s ok: mov dx, bx mov ax, 4c00H int 21H code ends end start
该程序把数据段中的0的偏移地址放入dx,通过在dosbox执行后,可以发现,0处的偏移地址已存入dx中:
-
循环指令 loop
loop 标号 ;循环指令,段内短转移,也是通过偏移量来控制转移的,同样是8位的偏移量,范围:-128~127
-
指令 call
call 标号
该指令也是通过偏移量来改变IP的值,从而实现转移的,和 jmp 不同的是,该指令会把 IP 的值压入栈中进行保存,相当于进行了以下操作:
push ip jmp near ptr 标号
所以,该指令的可修改的 IP 的偏移量范围:-32768~32767。
call far ptr 标号
该指令实现了段间转移,在转移之前会把CS和IP的值分别压入栈中进行保存,相当于执行了以下操作:
push cx push ip jmp far ptr 标号
同样,call指令也可以通过寄存器和内存来进行跳转:
通过寄存器来跳转:call 16位寄存器
相当于执行了以下操作:
push ip jmp 16位寄存器
通过内存单元来跳转:
call word ptr 内存单元地址 call dword ptr 内存单元地址
相当于执行了以下操作:
push ip jmp word ptr 内存单元 或 push ip jmp dword ptr 内存单元
-
指令 ret和retf
ret ;该指令用栈中的数据修改IP的内容,从而实现段内近转移 reft ;该指令用栈中的数据修改CS和IP的内容,从而实现段间转移
相当于分别执行了以下操作:
pop ip 或 pop ip pop cs
该指令通常和call指令联合使用,可以实现类似高级语言中方法调用等功能。
如:assume cs:code code segment start: mov ax, 1 mov cx, 3 call s ;把当前 IP 的值压入栈中,然后跳转到 标号s 处执行,当 s 处指令执行结束后,恢复 IP 的值,开始执行下一条指令:mov bx,ax mov bx, ax mov ax, 4c00H int 21H s: add ax, ax loop s ret ;从栈中弹出值到IP中 code ends end start
灵活的跳转方式可以更好的控制程序的执行,通过跳转功能可以使用更加灵活、更加模块化的编程方式来实现我们的功能。一个标号处的代码,相当于一个模块,一个子程序。通过这种方式,可以把多个相互联系、功能独立的子程序组合起来实现一个复杂的实际问题。