The two samples don't quite do the same thing. The first sample starts by setting the location of the stack and data segments to some 'magic' location in memory:
mov ax, 07C0h
add ax, 288
mov ss, ax
mov sp, 4096
mov ax, 07C0h
mov ds, ax
The second sample doesn't (so it will use the SS and DS locations which are left in those registers by the O/S which loaded the program). I don't understand what 07C0h is and why there is that difference between the two programs.
Also the styles of assembly are different, for example one uses 0Eh
and the other uses 0x0E
. Are they both supported by NASM?
The first sample ends with jmp $
which I don't understand: does it mean "jump to here" i.e. "infinite loop"? The .done: ret
label seems to be unreachable.
The second sample ends with exit_function: ret
which I guess returns to the O/S.
The most significant place in both code for me is the loop for printing a char from the whole string, so I want you to tell me which variant is more correct (or maybe both variants are bad and you can provide me a much better sample).
Your using BIOS int 10h which Wikipedia says is:
Teletype output AH=0Eh AL = Character, BH = Page Number, BL = Color (only in graphic mode)
The first sample doesn't initialize the BX register (so it may may contain a semi-random value), the second does. BX may or may not have an effect (in fact this whole "BIOS Teletype output" API may or may not work) depending on current the video mode.
If you're interested in speed then the following algorithm can be much faster than using the BIOS interrupt:
- Set the right video mode
- Set registers (for example, es:di) to point to the main memory location which is shared with the video card (perhaps 0xB0000 if I recall correctly)
- Write the character into video memory
Or instead of writing characters the the BIOS one at a time, there's a BIOS API which lets you write an entire string. I copied the following from 8086 BIOS and DOS interrupts (IBM PC):
INT 10h / AH = 13h - write string.
input:
AL = write mode:
bit 0: update cursor after writing;
bit 1: string contains attributes.
BH = page number.
BL = attribute if string contains only characters (bit 1 of AL is zero).
CX = number of characters in string (attributes are not counted).
DL,DH = column, row at which to start writing.
ES:BP points to string to be printed.
example:
mov al, 1
mov bh, 0
mov bl, 0011_1011b
mov cx, msg1end - offset msg1 ; calculate message size.
mov dl, 10
mov dh, 7
push cs
pop es
mov bp, offset msg1
mov ah, 13h
int 10h
jmp msg1end
msg1 db " hello, world! "
msg1end:
As for your print loops:
print_string:
mov ah, 0Eh
.repeat:
lodsb
cmp al, 0
je .done
int 10h
jmp .repeat
The first one (above) looks tight: i.e. it's good and short, with the mov ah, 0Eh
performed once outside the loop. lodsb
doesn't affect flags, so the separate cmp
is necessary. There may be a faster way to compare al
with 0, perhaps something like or al,al
.
I don't have a BIOS reference manual, but one thing it's lacking is a check of the 'return code' to see whether the output might have failed. This suggests that if the call fails it might leave a return code in ah
. You want your ah
preserved, so you might want to check that at least the first one didn't fail, and then have a loop which assumes that subsequent won't fail.
If your string weren't null-terminated, if instead you knew the length of it ...
HelloString:
dw 0bh
db 'Hello World'
... then you could load the length into cx
and use the loop
instruction, something like:
mov si, HelloString
lodsw
mov cx,ax
mov ah,0eh
.repeat:
lodsb
int 10h
loop .repeat
; .done:
ret
Beware that the above may not be the fastest way to loop, anymore. I once read that the more complicated opcodes (like loop
) perform more slowly on modern CPUs than using several primitive instructions (e.g. dec cx
and jnz
). The reason for that is that a developer (or compiler) will choose and place those more primitive opcodes in such as way as to use both pipelines and not cause a stall. Writing assembly which keeps both pipelines busy is an advanced art (which, they try to build-in to compilers). If I look at the output from a compiler, I hardly recognize it any more! Because, it often uses several instructions instead of one or two. Nevertheless I think that the loop above is compact and idiomatic (if you're writing code for another programmer to read, not writing code in order use every last CPU cycle).
The second one doesn't seem to be optimized for size or speed:
PrintCharacter:
mov ah, 0x0E
mov bh, 0x00
mov bl, 0x07
int 0x10
ret
PrintString:
next_character:
mov al, [si]
inc si
or al, al
jz exit_function
call PrintCharacter
jmp next_character
exit_function:
ret
- Why not use
lodsb
instead of mov al, [si]
and inc si
?
- Why set
bl
and bh
instead of setting bx
?
- Why not set
bx
and ah
before the loop begins (I think that a call to int 10h
should preserve the contents of the bx
register?
- Why make
PrintCharacter
a subroutine to be called, instead of including those instructions inline?
Also, about code style. I don't know which style is more correct in ASM, for e.g. in the 1st variant I'm using the name of the functions like: print_string, but as for the 2nd variant, I'm using the style, which is closer to C# name style PrintCharacter, so which variant of naming is more suitable in ASM language?
The NASM manual seems to use naming conventions more like print_string
. Chapter 9 talks about writing assembly called from high-level languages. Names of HLL functions are subject to name mangling so if you want to write the C printf
function in assembly you might need to call it _printf
.