Reputation: 313
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
a a
a a a
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.
[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.
Upvotes: 1
Views: 1512
Reputation: 2755
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]
Upvotes: 4