Почему этот ассемблерный код имеет бесконечное значение l oop? - PullRequest
1 голос
/ 31 марта 2020

Краткое объяснение

Программа должна напечатать пирамиду с заданным символом в ASCII, используя int 0x10 , ожидаемый результат для 3 строк (количество, использованное в коде ниже) будет:

a

aa

aaa

Для компиляции и запуска кода я компилирую его с помощью 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.

1 Ответ

4 голосов
/ 31 марта 2020

Вы не можете просто переключить сборку NASM в режим [bits 64] и ожидать, что qemu выполнит ваш код в длинном режиме. То, как вы вызываете, предполагает использование режима Real 8086, который равен bits 16 (по умолчанию для NASM). Возможно, из-за того, что вы используете многие 8-битные или размерно-зависимые c операции, код работает в любом случае, но не так, как ожидалось.

Кроме того, вы прокомментировали mov al, sil как "переместить счетчик" al, но это не счетчик. И начальный mov sil, CHAR не помещает символ, на который указывает «CHAR», в sil, он фактически помещает адрес CHAR в регистр (предназначенный как sil, но независимо от того, что это будет интерпретировано как в R86M) , И add sil, 48 также не имеет никакого смысла. 48 (30h) - это правильное значение, которое нужно добавить, чтобы преобразовать десятичное число (от 0 до 9) из числового значения c в ASCII-ди git этого числа. Это не обобщение c «для отображения в виде ASCII» преобразования, оно работает только для десятичных синглов-ди git чисел.

Вы также не смогли упомянуть, что прогон qemu застревает зацикливаться навсегда, отображение различных символов в бесконечном l oop.

Вот разборка в 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

Давайте go через шаг дизассемблированного машинного кода шаг за шагом.

Бит 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').

Следующие две инструкции представляют собой 8-битные операции без байтов префикса REX, поэтому они интерпретируются так, как вы задумали :

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

Затем вы получите mov bx, 1, который собран с префиксом O16 (OSIZE в 32- или 64-битном режиме). Вместо этого это интерпретируется как O32, так как мы находимся в 16-битном сегменте кода:

0000000B  66BB01006689      mov ebx,0x89660001

Теперь дизассемблер продолжает неверную инструкцию, так как ваш BB с префиксом O32 равен mov ebx, imm32 вместо вашего предполагаемого mov bx, imm16.

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

Это, по сути, инструкция без операций в этом контексте. Затем мы перейдем к прыжку:

00000015  7424              jz 0x3b
00000017  EB00              jmp short 0x19

Я полагаю, что inc ax оставит флаги в состоянии Not-Zero (NZ), скорее всего (а fiadd не изменит его), поэтому ваш jz здесь не разветвляется.

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

Это сравнение выполняется в целом edx. Из-за оптимизированной формы с 8-разрядным немедленным расширением знака единственное изменение от намеченного O16 к O32 состоит в том, что будет сравниваться весь регистр edx. Однако, поскольку задействовано старшее слово edx, этот l oop может выполняться более 4 гигабит итераций.

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

Снова регистр sil вместо этого декодируется как байт префикса REX (inc), затем доступ dh. По этой причине бесконечный l oop показывает разные символы: вы инициализируете al из среднего байта счетчика l oop.

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

Здесь никаких сюрпризов, все интерпретируются как и предполагалось .

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

Это добавление делает очень длинным l oop, в зависимости от начального значения в edx, передаваемого вашей программе из 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

Не так много сюрпризов Вот. Однако ebx увеличивается вместо bx.

0000003B  EBFE              jmp short 0x3b

Эта остановка l oop интерпретируется так, как вы хотите.

l oop возвращается к метка 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
[...]

Таким образом, он инициализирует счетчик l oop edx до полного значения ebx. Это снова делает очень долго oop. cmp cl, 0 интерпретируется как задумано.


Вот исправленная перезапись вашей программы. Он больше не использует sil, потому что вы не можете использовать sil в 16-битном режиме, и он все равно не понадобился. Он не использует bx как внутреннее значение сброса счетчика l oop, поскольку bx может использоваться службой прерывания 10h ah = 0Eh. Кроме того, он использует полный cx в качестве внешнего счетчика l oop, что не является обязательным, но позволяет использовать инструкцию loop вместо dec cl \ jnz .loop_outer.

Кроме того, я исправил два больше ошибок в вашей программе:

  • За последним символом, построившим пирамиду, на строку следовал завершающий пробел. Я изменил программу, чтобы сначала отображалось пустое, а затем другой символ.

  • Вы отображали только код символа 10 (0Ah, перевод строки) для переноса строки. Правильным является значение 13 (возврат каретки), а затем 10 (перевод строки) для уровня обслуживания прерывания 10 ч.

Еще одна проблема заключается в том, что вы использовали простой jmp для остановки самого себя. Это потребляет много процессорного времени, поскольку оно зацикливается навсегда. Вместо этого я использовал последовательность sti \ hlt \ jmp, которая сохраняет время ЦП процесса qemu близким к нулю при остановке.

Вот источник:

        ; 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]
...