Using NASM code with QuickBASIC

This is a tutorial and reference on how to use assembly code in QuickBASIC programs, by creating a library. I will focus on NASM, but the coding techniques will work with any assembler. This is not a tutorial on assembly language, however.

The primary source for this tutorial was a forum post by user "Artelius".

The general template

Create a new text file with the ASM extension and open it. Copy the following template into it:

GLOBAL MyProcedure

GROUP DGROUP

SECTION CODE

MyProcedure:
	; Code goes here

Now let's analyze it. At the beginning, you specify which procedures (labels really) you want to be accessible to QuickBASIC programs with GLOBAL. Each procedure needs to be on its own line with its own GLOBAL directive. Then GROUP DGROUP tells the linker that your code will access variables which reside in QuickBASIC's default segment, which is called DGROUP (but you can still access data in other segments; see below). SECTION CODE marks the beginning of code. After that, you can begin writing the procedures.

Procedures

All procedures (both SUBs and FUNCTIONs) must have the following form (after the label):

	push bp
	mov bp, sp

	; Procedure body goes here

	pop bp
	retf x

All instructions except RETF are actually optional, but if you want to access parameters that QuickBASIC pushes to you (or modify them if you are being passed pointers), you will need them. Replace the x after RETF with the number of parameters you are being passed, times two (since they're 16 bits wide). For more information on FUNCTIONs and parameters, see Advanced techniques below.

Assembling the code and making the library

Your code can now be assembled and linked. Create a batch file with the following contents:

@DEL %1.LIB
N:\NASM -o %1.OBJ -f obj %1.ASM
T:\LINK /Q %1.OBJ, %1.QLB,,T:\BQLB45.LIB;
T:\LIB %1.LIB +%1.OBJ

Replace N:\ and T:\ with the locations of NASM and QuickBASIC respectively, or use the SUBST (DOS/Windows) or MOUNT (DOSBox) command to create virtual drives pointing to them. Now, whenever you want to make a NASM library, just type ASMLINK library and you'll get a QuickBASIC library ready to be used.

Procedure declarations for QuickBASIC

Before QuickBASIC can use any of the procedures in your library, they have to be declared. It's good to create a BI file to contain the declarations. The declarations are written the exact same way as they are for BASIC procedures (with the DECLARE statement), however you may wish to pass certain parameters as BYVAL if you will not be modifying them. After you've written and $INCLUDEd your BI file, you can use your procedures.

Advanced techniques

This is more of a reference section for things that weren't explained in the steps above.

Registers

You can generally use any register.

If your procedure is a FUNCTION, AX and DX are used for returning values (see below).

QuickBASIC expects SI, DI, SP, BP, DS (I think), SS, and CS (obviously) to be preserved. You can PUSH and POP these registers, but sometimes (for example when calling a BIOS routine) you'll want to preserve SP because it will get destroyed. You can do a MOV ES, SP as the equivalent of PUSH and MOV SP, ES as the equivalent of POP (actually, any 16-bit register will work, but I use ES the least). I don't think QuickBASIC expects ES to be preserved; this technique has worked for me so far.

The starting values of the registers are undefined.

Returning values (FUNCTIONs)

To return a value, your BI file needs to DECLARE the procedure as a FUNCTION. Then:

Here's an example of a FUNCTION that returns an INTEGER:

GLOBAL GetDefaultDrive

GROUP DGROUP

SECTION CODE

GetDefaultDrive:
	; An example of getting the current default drive. Declare as:
	; DECLARE FUNCTION GetDefaultDrive% ()

	mov ah, 19h	; DOS function
	int 21h
	mov ah, 0	; Discard the higher byte
	add al, 65	; Convert to ASCII
	retf		; Return to QuickBASIC (result is in AX)

Accessing parameters (general)

QuickBASIC passes parameters to your procedure by pushing them onto the stack. You get them with BP-relative displacements. The last parameter DECLAREd has the lowest displacement, which is always 6 (because the two words below it store CS and IP as they were before your procedure was entered). For example, if you push a%, b%, and c%, then a% will have a displacement of +6, b% will have +8, and c% will have +10. The displacements are always even, but the size of parameters varies depending on their type and whether they were pushed by value or by reference. You can mix parameters passed by value and by reference; they don't all have to be the same. When your procedure is finished, make sure RETF has the correct operand (see Procedures above).

Accessing parameters by value

The easiest way to read parameters, especially INTEGERs, is to pass them by value. You do this by putting BYVAL before their name in the DECLARE statement in your BI file. Unfortunately, you cannot modify the parameters when they are passed by value, but you can read them directly by MOVing them into registers. Only INTEGERs, LONGs, SINGLEs, and DOUBLEs can be passed by value. Here's an example of getting an INTEGER by value:

GLOBAL PrintChar

GROUP DGROUP

SECTION CODE

PrintChar:
	; An example of getting a character BYVAL and printing it. Declare as:
	; DECLARE SUB PrintChar (BYVAL CharCode%)

	; Set up the stack
	push bp
	mov bp, sp

	mov ah, 2	; DOS function (print character)
	mov dx, [bp+6]	; Character to print (high byte is ignored)

	int 21h		; Print it

	; Return to QuickBASIC
	pop bp
	retf 2

Accessing parameters by reference

By default, parameters are passed by reference so you can modify them. This also has the nice side effect that, since you get their offsets, they will each occupy 2 bytes on the stack. However, it requires some more work to get to them: first, you have to MOV the offset from the stack into a register, and only then you can use that register as a pointer. Here's an example of accessing an INTEGER by reference:

GLOBAL Increment

GROUP DGROUP

SECTION CODE

Increment:
	; An example of incrementing an INTEGER by reference. Declare as:
	; DECLARE SUB Increment (What%)

	; Set up the stack
	push bp
	mov bp, sp

	mov bx, [bp+6]	; Get pointer
	inc word [bx]	; Increment the variable

	; Return to QuickBASIC
	pop bp
	retf 2

Accessing parameters that are "far" (not in DGROUP)

To get a far parameter by reference, pass its segment by value first (with the VARSEG function), then pass it by reference as usual. There's another way involving passing it with SEG (instead of putting BYVAL before the name, you put SEG, and you don't manually pass the segment), but I haven't experimented with that yet.

Accessing strings

Note: this applies to QuickBASIC 4.5 and possibly other versions, but I haven't tested with them. It will not work with QuickBASIC 7.1 (and probably Visual Basic for DOS) because it uses a different way of storing strings.

Reading strings (I will not discuss modifying; see Calling QuickBASIC's internal procedures below), no matter whether they are dynamic or fixed-length (they temporarily get converted to dynamic strings), works the following way: QuickBASIC passes you the offset of the string descriptor. This is a 4-byte structure, which contains (in the following order) the length of the string, and a pointer (offset) to its contents. Both are 16 bits wide. You can also manually pass the string's length (get it with LEN) and offset (get it with SADD). You can then use an index register to point to the string and CX (or any other register) as a counter, then loop through the string. Here's an example of reading a "near" string:

GLOBAL StrPrint

GROUP DGROUP

SECTION CODE

StrPrint:
	; An example of printing a QuickBASIC string. Declare as:
	; DECLARE SUB StrPrint (What$)

	; Set up the stack
	push bp
	mov bp, sp
	push si		; SI has to be preserved

	mov si, [bp+6]	; Get pointer to string descriptor
	mov cx, [si]	; Get string length
	mov bx, [si+2]	; Get pointer to string contents
	mov ah, 2	; DOS function (print character)

.next:
	mov dl, [bx]	; Get a character
	int 21h		; Print it
	inc bx		; Move to next
	dec cx		; Decrease counter
	jnz .next	; Repeat if any characters left

	; Return to QuickBASIC
	pop si
	pop bp
	retf 2

Accessing variables of user-defined type

User-defined types are just a list of regular variables stored one after another. The exceptions are strings, which are always fixed-length and don't use string descriptors; they're just raw bytes, but without any way to get their length (you have to know it in advance). User-defined types are always "far"; see Accessing parameters that are "far" (not in DGROUP) above.

Accessing arrays

Arrays appear to be stored "far" and column-major (though I think the compiler can be forced to use row-major order). They are just raw lists of data. Dynamic string arrays are lists of string descriptors, and fixed-length string arrays are just one fixed-length string after another, without any length information (you must know the length in advance). To get the offset of the beginning of an array, use VARPTR with the first element in the array.

Calling QuickBASIC's internal procedures

It is also possible to call routines that QuickBASIC itself uses to implement its statements and functions. I haven't tried it myself though. To use QuickBASIC's routines, declare them with the EXTERN directive between the GROUP DGROUP and SECTION CODE lines. For example, to declare B$SCAT (the routine used to concatenate strings), insert EXTERN B$SCAT. If you want to research how QuickBASIC's routines work and what parameters they require, I suggest you compile some programs with the /A parameter (on the command line; the compiler executable is called BC) to get an assembly listing of the compiled code.


First published on .
Last updated on .

Table of contents

Contact me