为什么这段汇编代码会无限循环?

Why does this assembly code goes on an infinite loop?

简要说明

该程序应使用 int 0x10 以 ASCII 格式打印具有给定字符的金字塔,3 行的预期结果(在下面的代码中使用的数量)将是:

a

a a

a a a

为了编译和 运行 我用 nasm 编译它的代码,然后我用 qemu 来模拟:

nasm pyramid.asm
qemu-system-x86_64 -drive file=pyramid,format=raw,index=0,media=disk

但是程序在打印所有 ASCII 值时卡住了。此外,如果有任何用于 nasm 代码的调试器,可以让您逐行 运行,允许您检查寄存器值,这对学习也很有帮助。

代码:

[bits 64]
[org 0x7c00]   

mov sil, CHAR            ; Save the char in the sil register.
add sil, 48              ; Adds 48 to display it as ASCII.
mov ah, 0x0e             ; Value in 'ah' needed to be able to use 'int 0x10'.
mov cl, 0x3              ; Setting the counter of lines remaining.
mov bx, 0x1              ; Setting the amount of characters to print.

pyramid: 
    mov dx,bx            ; Creates a copy bx in dx.
    cmp cl,0             ; If we already printed all the lines we exit the program.
    je exit              ;
    jmp printLine        ; Otherwise we print the next line.

printLine:
    cmp dx,0             ; If all characters from the line were printed goes to next line
    je endPrintLine      ; 
    printChar:
        mov al, sil      ; We move the counter to the 'al' register. 
        int 0x10         ; Interruption that prints the content of the register al.
        mov al,0x20      ; We move the value 0x20 (space) to the 'al' register.
        int 0x10         ; Interruption that prints the content of the register al.
        add dx,-1        ; Decrement by 1 the amount of characters remaining.
        jmp printLine    ; Print the next line.
    endPrintLine:        ;
        mov al,0xA       ; We move the vale 0xA (next line) to the 'al' register.
        int 0x10         ; Interruption that prints the content of the register al.
        add cl,-1        ; Decrement by 1 the amount of lines remaining.
        add bx,1         ; Icrement the amount of chars to print by 1.
        jmp pyramid      ;

exit:
    jmp $

CHAR: db "a",0           ; Character we want to make the pyramid of. 

times 510-($-$$) db 0    ; Fill with 0s.
dw 0xaa55                ; Save in 0x511 '0xaa55' to indicate it's bootable.

您不能只是将 NASM 的汇编切换到 [bits 64] 模式并期望 qemu 运行 您的代码处于长模式。您调用的方式似乎建议您使用真正的 8086 模式,即 bits 16(NASM 的默认值)。可能是由于您使用了许多 8 位或与大小无关的操作,代码在某种程度上确实 运行,但没有按预期 运行。

此外,您将 mov al, sil 评论为 "move the counter to" al,但这不是反击。而最初的mov sil, CHAR并没有将"CHAR"指向的字符放入sil,而是将CHAR的地址放入寄存器(原意为sil,但无论如何这将被解释为在 R86M 中)。而 add sil, 48 也没有任何意义。 48 (30h) 是将十进制数字(0 到 9)从数值转换为该数字的 ASCII 数字的正确值。它不是通用的 "to display it as ASCII" 转换,它只适用于十进制的一位数。

您也没有提到 qemu 运行 卡在无限循环中,无限循环显示各种字符。

这是您代码的 16 位模式反汇编:

$ ndisasm -b 16 -k 0x3D,$((512 - 0x3D)) pyramid
00000000  40                inc ax
00000001  B63D              mov dh,0x3d
00000003  40                inc ax
00000004  80C630            add dh,0x30
00000007  B40E              mov ah,0xe
00000009  B103              mov cl,0x3
0000000B  66BB01006689      mov ebx,0x89660001
00000011  DA80F900          fiadd dword [bx+si+0xf9]
00000015  7424              jz 0x3b
00000017  EB00              jmp short 0x19
00000019  6683FA00          cmp edx,byte +0x0
0000001D  740F              jz 0x2e
0000001F  40                inc ax
00000020  88F0              mov al,dh
00000022  CD10              int 0x10
00000024  B020              mov al,0x20
00000026  CD10              int 0x10
00000028  6683C2FF          add edx,byte -0x1
0000002C  EBEB              jmp short 0x19
0000002E  B00A              mov al,0xa
00000030  CD10              int 0x10
00000032  80C1FF            add cl,0xff
00000035  6683C301          add ebx,byte +0x1
00000039  EBD4              jmp short 0xf
0000003B  EBFE              jmp short 0x3b
0000003D  skipping 0x1C3 bytes

让我们一步步过一遍反汇编的机器码。

mov sil, CHAR位被解码为inc ax(REX前缀字节,40h)然后mov dh, 3Dh

00000000  40                inc ax
00000001  B63D              mov dh,0x3d

然后是另一个 REX 前缀字节和 add dh, 30h:

00000003  40                inc ax
00000004  80C630            add dh,0x30

现在 dh 等于 6Dh ('m')。

接下来的两条指令是没有 REX 前缀字节的 8 位操作,因此它们会按照您的意图进行解释:

00000007  B40E              mov ah,0xe
00000009  B103              mov cl,0x3

然后你会得到 mov bx, 1,它使用 O16 前缀(32 位或 64 位模式下的 OSIZE)进行汇编。这被解释为 O32 而不是我们在 16 位代码段中:

0000000B  66BB01006689      mov ebx,0x89660001

现在反汇编程序继续执行错误指令,因为您的 O32 前缀 BBh 是 mov ebx, imm32 而不是您预期的 mov bx, imm16.

00000011  DA80F900          fiadd dword [bx+si+0xf9]

在此上下文中,这实质上是一条无操作指令。然后我们开始跳跃:

00000015  7424              jz 0x3b
00000017  EB00              jmp short 0x19

我相信 inc ax 最有可能将标志留在非零 (NZ) 状态(并且 fiadd 不会改变它),所以你的 jz这里就不分了。

00000019  6683FA00          cmp edx,byte +0x0
0000001D  740F              jz 0x2e

这个比较是在edx的整体上进行的。由于优化形式,使用符号扩展的 8 位立即数,从预期的 O16 到 O32 的唯一变化是将比较整个 edx 寄存器。然而,由于涉及edx的高位字,这个循环可能运行超过4 giga次迭代。

0000001F  40                inc ax
00000020  88F0              mov al,dh

再次将 sil 寄存器解码为 REX 前缀字节 (inc),然后访问 dh。这就是无限循环显示不同字符的原因:您正在从循环计数器的中间字节初始化 al

00000022  CD10              int 0x10
00000024  B020              mov al,0x20
00000026  CD10              int 0x10

这里没有惊喜,都是按预期解释的。

00000028  6683C2FF          add edx,byte -0x1
0000002C  EBEB              jmp short 0x19

此添加会导致一个很长的循环,具体取决于从 qemu 传递给您的程序的 edx 中的初始值。

0000002E  B00A              mov al,0xa
00000030  CD10              int 0x10
00000032  80C1FF            add cl,0xff
00000035  6683C301          add ebx,byte +0x1
00000039  EBD4              jmp short 0xf

这里没有太多惊喜。但是,ebx 会递增,而不是 bx

0000003B  EBFE              jmp short 0x3b

这个暂停循环被解释成你想要的样子。

返回标签 pyramid 的循环解释该代码片段如下:

$ ndisasm -b 16 -s 0xF -k 0x3D,$((512 - 0x3D)) pyramid
[...]
0000000F  6689DA            mov edx,ebx
00000012  80F900            cmp cl,0x0
00000015  7424              jz 0x3b
[...]

因此它将循环计数器 edx 初始化为 ebx 的完整值。这又造成了一个很长的循环。 cmp cl, 0 按预期解释。


这是您程序的固定重写。它不再使用 sil,因为您不能在 16 位模式下使用 sil,而且也不需要它。它不使用 bx 作为内循环计数器复位值,因为 bx 可能被中断 10h ah=0Eh 服务使用。此外,它使用完整的 cx 作为外循环计数器,这不是必需的但允许使用 loop 指令而不是 dec cl \ jnz .loop_outer.

此外,我修复了您程序中的另外两个错误:

  • 每行构建金字塔的最后一个字符后跟一个尾随空白。我把程序改成先显示空白再显示其他字符。

  • 您只为换行符显示了 10(0Ah,换行)字符代码。对于中断 10h 服务级别,正确​​的是 13(Carriage Return)然后 10(Line Feed)。

还有一个问题是您使用普通 jmp 来停止。这将消耗大量 CPU 时间,因为它会永远循环。我改用 sti \ hlt \ jmp 序列,它使 qemu 进程的 CPU 时间在停止时接近于零。

来源如下:

        ; cpu 386       ; no 32-bit registers used or needed here!
        cpu 8086
        bits 16
        org 0x7c00

start:
        mov ah, 0Eh     ; value to call "display TTY" int 10h service
        mov cx, 3       ; outer loop counter
        mov di, 1       ; inner loop counter initialisation,
                        ;  incremented by each outer loop
        mov bx, 7       ; bx initialised to 7 for Int10.0E "page" and "colour".

                ; Note: Do not use bp register, as it may be overwritten by
                ;        the Int10.0E call.

.loop_outer:
        mov dx, di      ; reset inner loop counter to di

.loop_inner:
        mov al, ' '
        int 10h         ; display a blank
        mov al, 'a'
        int 10h         ; display the character we want to show
        dec dx
        jnz .loop_inner ; loop inner loop

        mov al, 13
        int 10h
        mov al, 10
        int 10h         ; display a line break

        inc di          ; increment reset value for inner loop counter
        loop .loop_outer; loop outer loop

halt:
        sti
        hlt             ; halt the system (without using much CPU time)
        jmp halt

        times 510-($-$$) db 0
        dw 0AA55h

运行如下:

$ nasm p.asm -l p.lst -o p.bin
$ qemu-system-x86_64 -drive file=p.bin,format=raw,index=0,media=disk

它在我的 qemu 上显示以下输出:

[...]

Booting from Hard Disk...
 a
 a a
 a a a
[cursor here]