简体   繁体   中英

Why does this assembly code goes on an infinite loop?

Brief explanation

The program should print a pyramid with a given char in ASCII using int 0x10 , the expected result for 3 lines (amount used in the code bellow) would be:

a

aa

aaa

To compile and run the code I compile it with nasm and then I use qemu to emulate:

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

However the program get's stuck printing all ASCII values. Also if there is any debugger out there for nasm code that let's you run line by line, allowing you to check registers values that would be great for learning too.

Code:

[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.

You cannot just switch NASM's assembling into [bits 64] mode and expect qemu to run your code in long mode. The way you invoke seems to suggest you should use Real 8086 Mode, which is bits 16 (the default for NASM). It might be that due to your use of many 8-bit or size-agnostic operations the code does run anyway some of the way, but does not run as expected.

Also, you commented mov al, sil as "move the counter to" al , but that is not a counter. And the initial mov sil, CHAR does not put the character pointed-to-by "CHAR" into sil , it actually puts the address of CHAR into the register (intended as sil but whatever this will be interpreted as in R86M). And add sil, 48 also does not make any sense. 48 (30h) is the correct value to add to convert a decimal number (0 to 9) from a numeric value to an ASCII digit of that number. It is not a generic "to display it as ASCII" conversion, it only works for decimal single-digit numbers.

You also failed to mention that the qemu run gets stuck looping forever, displaying various characters in an infinite loop.

Here's a disassembly, in 16-bit mode, of your code:

$ 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

Let's go through the disassembled machine code step by step.

The mov sil, CHAR bit is decoded as inc ax (a REX prefix byte, 40h) then mov dh, 3Dh :

00000000  40                inc ax
00000001  B63D              mov dh,0x3d

Then another REX prefix byte and add dh, 30h :

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

Now dh equals 6Dh ('m').

The next two instructions are 8-bit operations without REX prefix bytes so they are interpreted as intended by you:

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

Then you get to mov bx, 1 which is assembled with an O16 prefix (OSIZE in 32- or 64-bit mode). This is interpreted as an O32 instead as we are in a 16-bit code segment:

0000000B  66BB01006689      mov ebx,0x89660001

Now the disassembler continues with the wrong instruction since your O32-prefixed BBh is a mov ebx, imm32 instead of your intended mov bx, imm16 .

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

This is essentially a no-operation instruction in this context. Then we get to a jump:

00000015  7424              jz 0x3b
00000017  EB00              jmp short 0x19

I believe the inc ax would leave the flags in a Not-Zero (NZ) state most likely (and the fiadd wouldn't change it), so your jz here does not branch.

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

This comparison is done on the whole of edx . Due to the optimised form, with a sign-extended 8-bit immediate, the only change from the intended O16 to an O32 is that the whole edx register will be compared. However, as the high word of edx is involved, this loop may run for more than 4 giga iterations.

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

Again the sil register is instead decoded as a REX prefix byte ( inc ) then an access of dh . This is the reason the infinite loop shows different characters: you're initialising al from a middle byte of the loop counter.

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

No surprises here, all interpreted as intended.

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

This addition makes for a very long loop, depending on the initial value in edx passed to your program from qemu.

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

Not many surprises here. However, ebx is incremented instead of bx .

0000003B  EBFE              jmp short 0x3b

This halting loop is interpreted as you want it to be.

The loop back to the label pyramid interprets that code fragment as follows:

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

So it initialises the loop counter edx to the full value of ebx . This makes for a very long loop again. The cmp cl, 0 is interpreted as intended.


Here's a fixed rewrite of your program. It does not use sil anymore because you cannot use sil in 16-bit mode, and it wasn't needed anyway. It does not use bx as inner loop counter reset value because bx may be used by the interrupt 10h ah=0Eh service. Further, it uses the full cx as outer loop counter, which is not required but allows using the loop instruction instead of dec cl \\ jnz .loop_outer .

Additionally, I fixed two more errors in your program:

  • The last character to build the pyramid, per line, was followed by a trailing blank. I changed the program to display the blank first and then the other character.

  • You only displayed a 10 (0Ah, Line Feed) character code for the linebreak. Correct is a 13 (Carriage Return) then 10 (Line Feed) for the interrupt 10h service level.

One more issue is that you used a plain jmp to itself to halt. This will eat a lot of CPU time as it loops forever. I used a sti \\ hlt \\ jmp sequence instead, which keeps the CPU time of the qemu process close to zero while halting.

Here's the source:

        ; 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

Run as follows:

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

It displays the following output on my qemu:

[...]

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM