Episode 3: Structure Your Code

In a modern language we'd structure our code into methods and classes and organize it into different files to keep it from becoming overwhelming. We can do a little of that in ASM. The instructions CALL and RET support constructs called "procedures" - a labeled block of instructions that can automatically return to where it was called from. Let's make our int-to-string code into a procedure:
; Converts the signed integer in AX to a string and puts it in [DI]
int_to_string:
mov bx, 10 ; Load divisor and clear counter
mov cx, 0
test ax, ax
jns .peelOffDigits ; Is AX signed? If not, go to 'continue'
mov byte [di], '-' ; Put a negative sign on the string and increment our pointer
inc di
neg ax ; Negate to make positive
.peelOffDigits:
xor dx, dx ; Zero the high-word of the divisor
div bx ; This will give us the remainder in DX
push dx ; Push it onto the stack
inc cl ; Increment our counter
test ax, ax ; If no remainder, we're done
jnz .peelOffDigits
.buildString:
pop ax ; Pop out the next digit
add al, '0' ; Add 48 to get the ascii value
stosb ; Store the char in AL into [DI], then increment DI
loop .buildString
mov byte [di], '$' ; Terminate the string
ret
All we have to do is label it and put a RET at the bottom. Now we can invoke the procedure from anywhere with call int_to_string
(after loading up AX and DI of course).
You may notice a bit of extra code near the top: Lines 5-9 allow the code to handle negative values. We test AX, and if it's negative (the 'signed' flag is set), we add a '-' to our buffer then negate the number to make it positive. Now our code handles signed 16-bit values in the range -32768 to 32767.
While we're organizing, let's move this procedure out of the main file altogether and into its own file named formatting.asm
. No here's how our test2.asm
looks:
[cpu 8086]
[org 100h]
jmp main
%include 'formatting.asm'
main:
mov ax, -1234
mov di, buf16
call int_to_string
mov dx, buf16
mov ah, 9
int 21h
mov ax, 4c00h
int 21h
buf16: times 16 db 0
A lot cleaner, right? We pull in our int_to_string
procedure with %include 'formatting.asm'
. One thing to note is the jmp main
which is now the very first instruction: The %include
directive literally inserts the code form formatting.asm
at that point in the file, so without the jump that procedure would get executed immediately on program startup. So we'll jump over it, then call it when necessary. The rest of the code is so much nicer now: We load up the number we want to format and a buffer to put it in, call int_to_string to format it, do an INT 21h call to print it out, and do another INT 21h to exit the app.
We can clean the code even further. Printing strings is a common function and we will be using it a lot in the future, so we would to make that call as short as possible. NASM allows macros:
; Prints the given '$'-terminated string.
%macro print 1
mov dx, %1
mov ah, 9h
int 21h
%endmacro
"print" is the name of the macro, "1" is the number of arguments it takes, and "%1" is a substitution for the first argument. So now our
mov dx, buf16
mov ah, 9
int 21h
can be reduced to print buf16
! You can put the macro wherever you want; I put mine in a separate file and include it after %include 'formatting.asm'
.
I got tired of typing in the commands to build and run the project, so I installed GNU Make for Win32 and created a Makefile:
#
# Makefile for PCjr ASM Game project
#
ifeq ($(OS),Windows_NT)
NASM="$(USERPROFILE)\AppData\Local\NASM\nasm"
DOSBOX="$(ProgramFiles)\DOSBox-0.74\dosbox"
RM=cmd \/C del
else
NASM=nasm
RM=rm
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin) # Mac OS X
DOSBOX=/Applications/DOSBox.app/Contents/MacOS/DOSBox
else # assume Linux
DOSBOX=DISPLAY=:0 dosbox
endif
endif
TARGET=test
TARGET.COM=$(TARGET).com
MACROS=stdio.mac
SRCS=formatting.asm
DEPS=$(MACROS) $(SRCS)
NASM_OPTS=-f bin
DOSBOX_OPTS=-conf dosbox.conf
$(TARGET.COM): $(TARGET).asm $(DEPS)
$(NASM) $(NASM_OPTS) -o $@ $<
run: $(TARGET.COM)
$(DOSBOX) $(DOSBOX_OPTS) $^
clean:
$(RM) $(TARGET.COM)
I won't bother to explain it here. If you're unfamiliar with Makefiles there are plenty of good tutorials on the web (like this one). In a nutshell, it allows us to build our executable from the console with make test.com
or just make
and run it in DOSBox with make run
. Our Makefile tracks dependencies; it knows that the executable depends on the .ASM files and the macro files, so if any of those change a rebuild is needed.
Full source for this episode can be downloaded at:
https://github.com/josh2112/pcjr-asm-game/tree/episode-3