9
\$\begingroup\$

Which style of printing strings from the SI registry in NASM is more correct?

I'm learning assembly and x86 architecture, but I'm rather new in it, so I've prepared two styles (in NASM) of printing a string, which is held in the SI register.

For a clear text, the samples are at the bottom of the question content. I also want you to provide me with some information about the correctness of the code style in ASM. 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).

Both samples don't need an OS and could be run as *.img (floopy image) in virtual machine or with the mkisofs (or similar software) be prepared as *.iso (CD image). In these code samples - I'm working with the BIOS to print the string.

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?

To compile in NASM, just use this syntax:

nasm -f bin -o file.img file.asm

The first one (http://pastebin.com/32mDPump):

[BITS 16]

start:
    mov ax, 07C0h
    add ax, 288
    mov ss, ax
    mov sp, 4096
    mov ax, 07C0h
    mov ds, ax
    mov si, text_string
    call print_string
    jmp $

text_string db 'Hello World!', 0

print_string:
    mov ah, 0Eh

.repeat:
    lodsb
    cmp al, 0
    je .done
    int 10h 
    jmp .repeat

.done:
    ret

times 510-($-$$) db 0
dw 0xAA55

The second one (http://pastebin.com/FnP0NGTY):

[BITS 16]

mov si, HelloString
call PrintString
jmp $

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

HelloString db 'Hello World', 0

times 510-($-$$) db 0
dw 0xAA55
\$\endgroup\$
5
\$\begingroup\$

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.

|improve this answer|||||
\$\endgroup\$
  • \$\begingroup\$ Quick note: I don't know enough about assembly to know whether it's always a bug, but the push cs in the AH = 13h example should be push ss when you're calling assembly from C. With Open Watcom C/C++, it'll work when building a .com, but output garbage when you build a .exe with a memory model where cs=ss is not longer true. (cs = "code segment" and ss = "stack segment", for people who are unaware.) \$\endgroup\$ – ssokolow Oct 27 '18 at 16:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy