汇编语言JMP和LOOP(转移)指令

< 上一页间接寻址 64位MOV指令下一页 >
默认情况下,CPU 是顺序加载并执行程序。但是,当前指令有可能是有条件的,也就是说,它按照 CPU 状态标志(零标志、符号标志、进位标志等)的值,把控制转向程序中的新位置。

汇编语言程序使用条件指令来实现如 IF 语句的高级语句与循环。每条条件指令都包含了一个可能的转向不同内存地址的转移(跳转)。控制转移,或分支,是一种改变语句执行顺序的方法,它有两种基本类型:
  • 无条件转移:无论什么情况都会转移到新地址。新地址加载到指令指针寄存器,使得程序在新地址进行执行。JMP 指令实现这种转移。
  • 条件转移:满足某种条件,则程序出现分支。各种条件转移指令还可以组合起来,形成条件逻辑结构。CPU 基于 ECX 和标志寄存器的内容来解释真 / 假条件。

JMP 指令

JMP 指令无条件跳转到目标地址,该地址用代码标号来标识,并被汇编器转换为偏移 量。语法如下所示:
JMP destination
当 CPU 执行一个无条件转移时,目标地址的偏移量被送入指令指针寄存器,从而导致迈从新地址开始继续执行。

JMP 指令提供了一种简单的方法来创建循环,即跳转到循环开始时的标号:
top:
    .
    .
    jmp top     ;不断地循环
JMP 是无条件的,因此循环会无休止地进行下去,除非找到其他方法退岀循环。

LOOP 指令

LOOP 指令,正式称为按照 ECX 计数器循环,将程序块重复特定次数。ECX 自动成为计数器,每循环一次计数值减 1。语法如下所示:
LOOP destination
循环目标必须距离当前地址计数器 -128 到 +127 字节范围内。LOOP 指令的执行有两个步骤:
  • 第一步,ECX 减 1。
  • 第二步,将 ECX 与 0 比较。

如果 ECX 不等于 0,则跳转到由目标给岀的标号。否则,如果 ECX 等于 0,则不发生跳转,并将控制传递到循环后面的指令。

实地址模式中,CX 是 LOOP 指令的默认循环计数器。同时,LOOPD 指令使用 ECX 为循环计数器,LOOPW 指令使用 CX 为循环计数器。

下面的例子中,每次循环是将 AX 加 1。当循环结束时,AX=5, ECX=0:
    mov ax,0
    mov ecx,5
L1:
    inc ax
    loop L1
一个常见的编程错误是,在循环开始之前,无意间将 ECX 初始化为 0。如果执行了这个操作,LOOP 指令将 ECX 减 1 后,其值就为 FFFFFFFFh,那么循环次数就变成了 4 294 967 296!如果计数器是 CX (实地址模式下),那么循环次数就为 65 536。

有时,可能会创建一个太大的循环,以至于超过了 LOOP 指令允许的相对跳转范围。下面给出是 MASM 产生的一条错误信息,其原因就是 LOOP 指令的跳转目标太远了:

error A2075: jump destination too far : by 14 byte(s)

基本上,在一个循环中不用显式的修改 ECX,否则,LOOP 指令可能无法正常工作。下例中,每次循环 ECX 加 1。这样 ECX 的值永远不能到 0,因此循环也永远不会停止:
top:
    .
    .
    inc ecx
    loop top
如果需要在循环中修改 ECX,可以在循环开始时,将 ECX 的值保存在变量中,再在 LOOP 指令之前恢复被保存的计数值:
.data
count DWORD ?
.code
    mov ecx, 100        ;设置循环计数值
top:
    mov count, ecx      ;保存计数值
    .
    mov ecx, 20         ;修改 ECX
    .
    mov ecx, count      ;恢复计数值
    loop top

循环嵌套

当在一个循环中再创建一个循环时,就必须特别考虑外层循环的计数器 ECX,可以将它保存在一个变量中:
.data
count DWORD ?
.code
    mov ecx, 100    ;设置外层循环计数值
L1:
    mov count, ecx  ;保存外层循环计数值
    mov ecx, 20     ;设置内层循环计数值
L2 :
    loop L2         ;重复内层循环
    mov ecx, count  ;恢复外层循环计数值
    loop L1         ;重复外层循环

提示:作为一般规则,多于两重的循环嵌套难以编写。如果使用的算法需要多重循环,则将一些内层循环用子程序来实现。

在 Visual Studio 调试器中显示数组

在调试期间,如果想要显示数组的内容,步骤如下:选择 Debug 菜单 -> 选择 Windows -> 选择 Memory -> 选择Memory 1。则出现内存窗口,可以用鼠标拖动并停靠在 Visual Studio 工作区的任何一边。还可以右键点击该窗口的标题栏,表明要这个窗口浮动在编辑窗口之上。

在内存窗口上端的 Address 栏里, 键入 & 符号和数组名称,然后点击 Enter。比如,&myArray 就是一个有效的地址表达式。内存窗口将显示从这个数组地址开始的内存块,如下图所示。

使用调试内存窗口显示数组

如果数组的值是双字,可以在内存窗口中,点击右键并在弹出菜单里选择 4-byte integer。还有不同的格式可供选择,包括 Hexadecimal Display,Signed Display(有符号显示),和 Unsigned Display(无符号显示)。下图显示了所有的选项。

调试器内存窗口的右键菜单

整数数组求和

在刚开始编程时,几乎没有任务比计算数组元素总和更常见了。汇编语言实现数组求和步骤如下:
  • 指定一个寄存器作变址操作数,存放数组地址。
  • 循环计数器初始化为数组的长度。
  • 指定一个寄存器存放累积和数,并赋值为0。
  • 创建标号来标记循环开始的地方。
  • 在循环体内,将和数与一个数组元素相加。
  • 指向下一个数组元素。
  • 用LOOP指令重复循环。

步骤 1 到步骤 3 可以按照任何顺序执行。下面的短程序实现对一个 16 位整数数组求和。
; 数组求和(SumArray. asm)
.386
.model flat,stdcall
.stack 4096
ExitProcess proto,dwExitCode:dword
.data
intarray DWORD 10000h,20000h,30000h,40000h
.code
main PROC

    mov edi, OFFSET intarray   ; 1: EDI=intarray 地址
    mov ecx, LENGTHOF intarray ; 2 :循环计数器初始化
    mov    eax,0               ; 3:    sum=0
L1:                            ; 4:标记循环开始的地方
    add    eax,    [edi]       ; 5:加一个整数
    add edi, TYPE intarray     ; 6:指向下一个元素
    loop    L1                 ; 7:重复,直到 ECX=0
    invoke ExitProcess, 0
main ENDP
END main

复制字符串

程序常常要将大块数据从一个位置复制到另一个位置。这些数据可能是数组或字符串,但是它们可以包括任何类型的对象。

现在看看在汇编语言中如何实现这种操作,用循环来复制一个字符串,而字符串表示为带有一个空终止值的字节数组。变址寻址很适合于这种操作,因为可以用同一个变址寄存器来引用两个字符串。目标字符串必须有足够的空间来接收 被复制的字符,包括最后的空字节:
;复制字符串    (CopyStr.asm)
.386
.model flat,stdcall
.stack 4096
ExitProcess proto,dwExitCode:dword
.data
source BYTE "This is the source string", 0
target BYTE SIZEOF source DUP(0)
.code
main PROC
    mov    esi, 0                      ;变址寄存器
    mov ecx, SIZEOF source             ;循环计数器
L1:                                   ;从源字符串获取一个字符
    mov    al, source [esi]            ;保存到目标字符串
    mov target [esi] , al              ;指向下一个字符
    inc esi                            ;重复,直到整个字符串完成
    loop L1
    invoke ExitProcess,0
main ENDP
END main
MOV 指令不能同时有两个内存操作数,所以,每个源字符串字符送入 AL,然后再从 AL 送入目标字符串。
< 上一页间接寻址 64位MOV指令下一页 >