为什么这段汇编代码会无限循环?
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]
简要说明
该程序应使用 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]