Written on 4 Oct 2015.
Last modified 16 Feb 2016.

This article is about calling into assembler code from Microsoft QBasic 1.1.

I figured out most of this during Flashback 2015 because Jimage was working on something in Basic and wanted to use the VGA ROM font, which I know you can get a pointer to by calling into the BIOS.

I'd just like to interject for a moment: what I'm referring to as QBasic is different from QuickBASIC. Both were produced by Microsoft, but QBasic is more recent and shipped as part of MS-DOS, and Windows up to 2000. Whereas QBasic is an interpreter, QuickBASIC 4.50 has a compiler and can produce EXE files.

If you want to play along at home, you can download QBasic and run it in DOSBox.

Syntax for CALL ABSOLUTE

My plan is to put the binary code for my function into a$, then take its address with SADD(), then CALL it, in the sense of the assembler CALL instruction. This looks like:

' a$ = "TODO"
CALL ABSOLUTE(SADD(a$))

Building the function

Here's a simple bit of code that draws a yellow X in the corner of the screen:

push ds
push 0b800h  ; segment for textmode framebuffer
pop ds
mov word [ds:0], 'X' | 14 << 8  ; text = 'X', attr = 14
pop ds
retf

I build it with nasm and post-process the output:

nasm code.asm && ndisasm code | \
  perl -pe 's{(\S+\s\s\S+)(\s+)}{\1")\2'\'' };s{^\S+  }{a\$ = a\$ + asm("}'

Then wrap some Basic around it:

DECLARE FUNCTION asm$ (in AS STRING)
a$ = ""
a$ = a$ + asm("1E")                ' push ds
a$ = a$ + asm("6800B8")            ' push word 0xb800
a$ = a$ + asm("1F")                ' pop ds
a$ = a$ + asm("3EC7060000580E")    ' mov word [ds:0x0],0xe58
a$ = a$ + asm("1F")                ' pop ds
a$ = a$ + asm("CB")                ' retf
CLS
PRINT
CALL ABSOLUTE(SADD(a$))

FUNCTION asm$ (in AS STRING)
  out$ = ""
  FOR i = 1 TO LEN(in) STEP 2
    byte$ = MID$(in, i, 2)
    value = VAL("&H" + byte$)
    out$ = out$ + CHR$(value)
  NEXT i
  asm = out$
END FUNCTION

yellow.bas

Function arguments

Back to the ROM font exercise: I want to allocate a string in Basic and pass its address to the assembler code.

I couldn't find a good reference for the calling convention, so I wrote a program to dump the stack and played with different calls.

Doing this in Basic:

CALL ABSOLUTE(9, SADD(a$))

Results in a stack that looks like this:

[bp+00] 0x7e6e <- saved SP
[bp+02] 0x2d94
[bp+04] 0x23fa
[bp+06] 0x7e60
[bp+08] 0x06d4
[bp+10] 0x0000
[bp+12] 0x0009 <- argument
[bp+14] 0x8a8c
[bp+16] 0x0000
[bp+18] 0x0000

I have no idea what most of those values are.

Passing two arguments results in a longer stack:

; CALL ABSOLUTE(8, 9, SADD(a$))
[bp+00] 0x7e6e <- saved SP
[bp+02] 0x2d94
[bp+04] 0x23fa
[bp+06] 0x7e60
[bp+08] 0x7e5e <- ?
[bp+10] 0x070e
[bp+12] 0x0000
[bp+14] 0x0008 <- argument
[bp+16] 0x0009 <- argument
[bp+18] 0x8a8c
[bp+20] 0x0000
[bp+22] 0x0000

Passing a string pushes its address followed by its length. I'm planning to just pass the address of the string though.

My code for dumping the stack: stack.bas

Grabbing the ROM font

The plan is to allocate a string long enough to hold the font, call into the video part of the BIOS (int 10h) and get a pointer to the ROM font (ax = 1130h), then copy the data into the string.

This ends up looking like:

DECLARE FUNCTION fromhex$ (in AS STRING)
DECLARE FUNCTION peekhex$ (addr AS INTEGER)
DECLARE FUNCTION tohex$ (in AS STRING)
DECLARE FUNCTION asm$ (in AS STRING)

' Allocate a string to hold the font data.
' Each byte encodes a line of the font.
' There are 256 chars in the font * 16 lines per char.
font$ = SPACE$(256 * 16)

a$ = ""
a$ = a$ + asm("55")                ' push bp
a$ = a$ + asm("89E5")              ' mov bp,sp
a$ = a$ + asm("56")                ' push si
a$ = a$ + asm("57")                ' push di
a$ = a$ + asm("1E")                ' push ds
a$ = a$ + asm("06")                ' push es
a$ = a$ + asm("50")                ' push ax
a$ = a$ + asm("53")                ' push bx
a$ = a$ + asm("51")                ' push cx
a$ = a$ + asm("52")                ' push dx
a$ = a$ + asm("368B7E0C")          ' mov di,[ss:bp+0xc]
a$ = a$ + asm("B83011")            ' mov ax,0x1130
a$ = a$ + asm("B706")              ' mov bh,0x6
a$ = a$ + asm("CD10")              ' int 0x10
a$ = a$ + asm("1E")                ' push ds
a$ = a$ + asm("06")                ' push es
a$ = a$ + asm("1F")                ' pop ds
a$ = a$ + asm("07")                ' pop es
a$ = a$ + asm("89EE")              ' mov si,bp
a$ = a$ + asm("B90004")            ' mov cx,0x400
a$ = a$ + asm("F366A5")            ' rep movsd
a$ = a$ + asm("5A")                ' pop dx
a$ = a$ + asm("59")                ' pop cx
a$ = a$ + asm("5B")                ' pop bx
a$ = a$ + asm("58")                ' pop ax
a$ = a$ + asm("07")                ' pop es
a$ = a$ + asm("1F")                ' pop ds
a$ = a$ + asm("5F")                ' pop di
a$ = a$ + asm("5E")                ' pop si
a$ = a$ + asm("5D")                ' pop bp
a$ = a$ + asm("CA0200")            ' retf 0x2

CALL ABSOLUTE(SADD(font$), SADD(a$))

' Draw a character from the font.
char = ASC("@")
FOR y = 0 TO 15
  fontline = ASC(MID$(font$, 16 * char + y + 1, 1))
  FOR x = 0 TO 7
    bit = fontline AND (2 ^ (7 - x))
    IF bit > 0 THEN
      PRINT "X";
    ELSE
      PRINT ".";
    END IF
  NEXT x
  PRINT ""
NEXT y

FUNCTION asm$ (in AS STRING)
  out$ = ""
  FOR i = 1 TO LEN(in) STEP 2
    byte$ = MID$(in, i, 2)
    value = VAL("&H" + byte$)
    out$ = out$ + CHR$(value)
  NEXT i
  asm = out$
END FUNCTION

FUNCTION fromhex$ (in AS STRING)
  out$ = ""
  FOR i = 1 TO LEN(in) STEP 2
    byte$ = MID$(in, i, 2)
    value = VAL("&H" + byte$)
    out$ = out$ + CHR$(value)
  NEXT i
  fromhex = out$
END FUNCTION

FUNCTION peekhex$ (addr AS INTEGER)
  peekhex = tohex(CHR$(PEEK(addr)) + CHR$(PEEK(addr + 1)))
END FUNCTION

FUNCTION tohex$ (in AS STRING)
  out$ = ""
  map$ = "0123456789ABCDEF"
  FOR i = 1 TO LEN(in)
    byte = ASC(MID$(in, i, 1))
    high = byte \ 16
    low = byte MOD 16
    out$ = out$ + MID$(map$, high + 1, 1) + MID$(map$, low + 1, 1) + " "
  NEXT i
  tohex = out$
END FUNCTION

font.asm
font.bas

Open question: why do I need retf 2 instead of plain retf to get the stack pointer back to the correct value? I don't understand the calling convention.

References

http://www.qb45.com/
http://www.qb64.net/wiki/index.php/CALL_ABSOLUTE
https://en.wikipedia.org/wiki/QBasic