Category : Assembly Language Source Code
Archive   : ASMTUT2.ZIP
Filename : BAS2-2.DOC

Output of file : BAS2-2.DOC contained in archive : ASMTUT2.ZIP

The PC Assembler Tutor mmxviii

If you now load the user library along with BASIC, you can look
at the segments and offsets of the arrays. Here is a program with
a couple of arrays:

k% = 12000
DIM array1!(k%), array2!(k%), array3!(12000)

value1! = VARPTR (array1!(0))
value2! = VARPTR (array2!(0))
value3! = VARPTR (array3!(0))
PRINT value1!, value2!, value3!

Each array is 12001 * 4 (bytes) or 48004 bytes long. The first
two have been defined as dynamic, so they can be anywhere in
memory as long as they're after the start of DS. The third one is
static, so it must be entirely in DS. Here's the output:

68032 116064 6252

Remember, these offsets are not relative to the start of memory,
they are relative to the start of the DS segment. The DS segment
only goes up to offset 65535 so the first two are outside of DS
while the third one is totally inside (6252 + 48004 = 54006 which
is less than 65536). PTR86 is going to give us a SEGMENT:OFFSET
pair relative to the start of memory.{1} The form for the call

CALL PTR86 (segment%, offset%, value!)

where value! is the number returned by VARPTR.

We'll add the following code to the bottom of the above program:

CALL PTR86 ( seg1% , off1%, value1! )
CALL PTR86 ( seg2% , off2%, value2! )
CALL PTR86 ( seg3% , off3%, value3! )

PRINT seg1%, off1%
PRINT seg2%, off2%
PRINT seg3%, off3%

What does the program print now?

68032 116064 6252

1. If you are using QuickBasic 4.0 or later this has become
simplified. You can just use VARSEG and VARPTR. These will give
you a segment offset pair which is usable in a subroutine call.


The PC Assembler Tutor - Copyright (C) 1990 Chuck Nelson

BASIC II - Interfacing BASIC With Assembler mmxix

19125 0
22127 0
15263 12

PTR86 has calculated the segments. The first two offsets are 0
and the third one is 12. Even though the third array is in the DS
segment, PTR86 has recalculated the segment to find the highest
segment which contains the first byte of the data. If you want to
do a little calculation, you can figure out that while this
program was running, DS was set to segment 14873.

VARPTR and PTR86 can be used to do this calculation for any array
element, not just array(0). Here's VARPTR:

value1! = VARPTR (array1!(0))
value2! = VARPTR (array1!(196))
value3! = VARPTR (array1!(2781))
PRINT value1!, value2!, value3!

And here's the output:

68032 68816 79156

From now on, we will have VARPTR inside of the PTR86 call:

CALL PTR86 ( seg1% , off1%, VARPTR (array1!(0)) )
CALL PTR86 ( seg2% , off2%, VARPTR (array2!(0)) )
CALL PTR86 ( seg3% , off3%, VARPTR (array3!(0)) )

It is clearer and uses less space. Of course, we can use this for
any element in the array, not just the beginning of the array:

CALL PTR86 ( seg1% , off1%, VARPTR (array1!(0)) )
CALL PTR86 ( seg2% , off2%, VARPTR (array1!(5076)) )
CALL PTR86 ( seg3% , off3%, VARPTR (array1!(1983)) )

We now have all the ammunition we need to do a quicker disk
write. We can pass the offset address of any numeric data in the
DS segment, we can pass the string descriptor address of any
string, and we can pass the SEGMENT:OFFSET pair of any array

Before doing our disk write program, I need to say something
about subroutine calls. You notice that when I show a subroutine
call I am always showing what numeric type I am passing. There is
a reason for this. Let's step back from assembler for a minute
and do a BASIC program with a subroutine. Here's the program:

floatA! = 27.925
floatB! = 16.96
integerA% = 300
integerB% = 140

The PC Assembler Tutor mmxx

CALL CheckTheNumbers (integerA%, integerB%, floatA!, floatB!)
PRINT floatA!, floatB!, integerA%, integerB%

SUB CheckTheNumbers ( int1%, int2%, flt1!, flt2!) STATIC
int1% = int1% + 7
int2% = int2% * 45
flt1! = flt1! + 19.0
flt2! = flt2! * 43.0

This gives the following output:

46.925 729.28 307 6300

This is nothing earthshaking. Now let's change one line in the

CALL CheckTheNumbers (floatA!, floatB!, integerA%, integerB%)

In the call statement I have put single precision numbers where
the integers were and integers where the single precision numbers
were. Now let's look at the output:

ENTER to debug, SPACEBAR to edit

We had an overflow. Here's where the debugger said the error was:

int2% = int2% * 45

But int2% received the floating point number for floatB!, and
that is 16.96. Since 16.96 * 45 = 763.2, where was the overflow?
What the subroutine saw was not 16.96 but the first two binary
bytes of floatB! (since we passed the address of floatB!). It
thought these were an integer, performed a multiplication and got
an error because the result was too big for an integer. Now,
change the value in floatB! to:

floatB! = 1.696E-29

and run the program again. Your results should be:

27.92501 1.693756E-29 0 16792

These numbers have no relation to either what we started out with
or what we did. Why? Because the subroutine mixed binary
information from single precision numbers with binary information
from integers and came up with unmitigated garbage. It was not
only doing that, it was also writing 4 bytes of information into
a 2 byte integer. Both flt1! and flt2! were writing past the end
of the data and overwriting something else.

There is NEVER any checking between a subroutine and the calling
program to see that the correct numeric types are being passed
(integers, long integers, single precision, double precision). In

BASIC II - Interfacing BASIC With Assembler mmxxi

C and Pascal this checking is done by the compiler at compile
time and the compiler will howl if you try to do something like
this. In BASIC (my BASIC at least), this checking is not being
done. Therefore, it is IMPERATIVE that you make sure that you
pass the correct data types.

Having given that warning, we are going to build an assembler
subprogram that opens a file for writing, then writes a block of
data from memory to disk. The form of the call will be:

CALL BlockToDisk ("filename$"+CHR$(0), seg%, offset%, # of bytes)

Notice that there MUST be a 0 after the filename. When you use
this function, you must always have:

filename$ = "" + CHR$(0)

The disk interrupt that is used in this subroutine expects a 'C'
string (terminated by a number 0, not an ASCII character '0'). If
you don't do it, you will almost certainly get an error.

This is followed by the segment of the first byte of data, the
offset of the first byte of data, and the number of BYTES (not
array elements) to write.

Which way does BASIC load the arguments to a subroutine? From
left to right, just like PASCAL. Also, in BASIC, ALL subroutine
calls are far calls. Therefore, BASIC will do the following when
it calls BlockToDisk:

PUSH address of file_descriptor
PUSH address of block segment
PUSH address of block offset
PUSH address of length

There are two things to notice here. First, these are all
ADDRESSES of the data, not the data items themselves. Upon entry
to the subroutine and initialization of BP, the stack will look
like this:

address of file_descriptor bp+12
address of block segment bp+10
address of block offset bp+8
address of length bp+6
old CS bp+4
old IP bp+2
BP-> old BP bp

Secondly, the name of the subroutine does not have any periods.
BASIC allows periods '.' but does not allow underscores '_' while
assembler allows underscores but doesn't allow periods. (Periods
have a special meaning in assembler; they are used in

In order to go on from here, you need a book about interrupts.
This information is from "DOS Programmer's Reference" by Terry

The PC Assembler Tutor mmxxii

Dettmann, but if you have "The Peter Norton Programmer's Guide to
The IBM PC", that's fine too. I'm going to give only partial
information about these interrupts and you should have complete

Here's the program. The explaination will come afterwards.

include \pushregs.mac
; - - - - - - - - - - - - - - - - - - - - -
file_handle dw ?
error_message db "Disk i/o error #"
error_byte db " ", 13, 10, "$"
; - - - - - - - - - - - - - - - - - - - - -
; - - - - - - - - - - - - - - - - - - - - -
print_error proc far
mov ah, 9 ; print error message
mov dx, offset DGROUP:error_message
int 21h
print_error endp
; - - - - - - - - - - - - - - - - - - - - - - - - -
; BlockToDisk ( filename , array SEG, array OFF, # of bytes)
; this is for BASIC


push bp
mov bp, sp
PUSHREGS ax, bx, cx, dx, si, ds

; open a new file or truncate an old one
mov ah, 3Ch ; open new or truncate old
mov cx, 0 ; normal file attribute
mov dx, [si+2] ; [si] =length, [si+2] =location
int 21h
jnc write_the_file ; ok if CF=0, error if CF=1

mov error_byte, '1' ; cannot open
call print_error
jmp exit

mov file_handle, ax ; store handle for later use
mov bx, ax ; file_handle to bx

BASIC II - Interfacing BASIC With Assembler mmxxiii

mov ah, 40h ; int 21h ah = 40h, write block
mov cx, [si] ; # of bytes into CX
push ds ; save BASIC's DS
mov dx, [si] ; offset to DX
mov ds, [si] ; segment to DS
int 21h
pop ds ; restore BASIC's DS
jnc normal_exit ; ok if CF=0, error if CF=1
mov error_byte, '2' ; bad file write
call print_error

mov ah, 3Eh ; close the file
mov bx, file_handle
int 21h

POPREGS ax, bx, cx, dx, si, ds
mov sp, bp
pop bp
ret (8) ; pop 4 words (8 bytes off the stack)

; - - - - - - - - - - - - - - - - - - - - - - - - -

This is using the standardized segment names so the data will be
in the DS segment. Notice the DGROUP GROUP declaration. Also
notice that in the 'print_error' subroutine, we have done an
offset override with 'offset DGROUP:error_message'. You need to
do this every time to avoid errors with the offset addressing
whenever you are using the DGROUP GROUP directive. Go back to the
discussion of simplified segment directives if you don't remember

The main program has 3 interrupts. The first one opens a new file
or truncates an old one to zero length. The file will be usable
for reading and/or writing:

Int 21h function 3Ch
AH = 3Ch
CX = 0 0 indicates a normal file
DS:DX address of ASCII filename (terminated by 0)

AX = file handle if CF = 0
or: AX = error code if CF = 1

This filename can be any legitimate pathname specification, and
must be terminated by a zero. Like all the disk interrupts we
will see in this chapter, if there is an error, the interrupt
will set CF = 1. Otherwise it will clear CF = 0. If CF = 0 you

The PC Assembler Tutor mmxxiv

can go on; if CF = 1, there was an error and you need to
terminate the subroutine and do some error reporting. The file
handle is a number from 0 to 65535 which the operating system
gives your program to uniquely identify that open file. There is
no other open file in the system which has that number. Guard it
carefully because it is your ONLY access to the file.

The second interrupt writes a block of data to disk.

Int 21h function 40h
AH = 40h
BX = file handle
CX = number of bytes to write
DS:DX = address of first byte of data

AX = actual number of bytes written if CF = 0
AX = error code if CF = 1

This too sets the carry flag if there was an error and clears it
if there wasn't. It is limited to writing 65535 bytes at a time,
but the largest array we can have is 65535 bytes (actually 65534
since all data types have an even number of bytes), so this is no

The third interrupt closes the file.

Int 21h function 3Eh
AH = 3Eh
BX = File handle

Also, the print-error subroutine has an interrupt

Int 21h function 09h
AH = 9
DS:DX = first byte of string.

This string must be terminated by a dollar sign '$' (of all
things). The message is on two lines so we can insert an error
number into the middle of the message. This is a quick and dirty
interrupt for string printing.

All interrupt numbers and function numbers are hex. This is
standard for interrupts. If things go wierd, always check first
to make sure that you have a hex number and not a decimal number.

The data has an 'ASSUME ds:DGROUP' statement.

Like Pascal, BASIC requires that the CALLED subroutine pop the
arguments off the stack, so we pop 4 extra words (8 extra bytes)

ret (8)

Assemble this program and put the object file in a library with
the other object files by using BUILDLIB.EXE. Now all we need is
a BASIC program to use this. Here it is:

BASIC II - Interfacing BASIC With Assembler mmxxv

DIM large.array! (10000)

FOR i% = 1 to 10000
large.array! (i%) = 2.167832E+19

filename$ = "blocktxt.doc" + CHR$ (0)
length% = 40000 - 65536
PRINT time$
CALL PTR86 (segment%, offset%, VARPTR (large.array!(1)) )
CALL BlockToDisk ( filename$ , segment% , offset%, length% )
PRINT time$

There is an extra PRINT statement there which I will explain
later. We are starting at large.array!(1) because that is where
we started with the other programs. why are we subtracting 65536?
Because BASIC has a limit of -32768 to +32767, so we store 40000
as its modular equivalent (mod 65536).

How long does the disk write take? From 2 to 3 seconds, and much
of that time was spent opening and truncating the file. This is
significantly better than the other ways of doing i/o. In fact,
the limits of this routine are the limits of your system. It is
literally impossible to do disk i/o any faster than this.

Try using a filename that doesn't have a CHR$(0) at the end. You
should get an error message. In my BASIC, here is the output:

Disk i/o error #1

Now remove that lone PRINT statement (the next to the last line).
Here's my output:

D9:50:00 error #1

For the QuickBASIC 3.0 environment, BASIC thinks that it has
complete control of screen i/o, so it is not doing its i/o in a
standard way and is overwriting the error message. If you are
going to do any screen i/o from assembler, you will have to think
of a way to live in harmony with BASIC.{2} We simply trick BASIC
into writing an empty line where the message was. This may not
always work correctly, especially if the window is scrolling up.


2. The easiest way to do this is to save the whole screen
image and cursor location, do what you want using the whole
screen, and then restore the screen and the cursor before

The PC Assembler Tutor mmxxvi

This whole program only involved interrupts. There is nothing
intrinsically assembler-like in its capabilities. In fact, we'll
do its disk read counterpart entirely in BASIC.

Let's do something that requires assembler language. The BASIC
FOR i% = 1 to 10000
to.array!(i%) = from.array!(i%)

get's the job done, but is isn't all that fast. It requires about
5.5 seconds. This is a natural for assembler. Dive down into the
assembler level, move the string, and come back up for air. Our
BASIC program will be:

n% = 10000
DIM from.array! (n%), to.array! (n%)

PRINT time$
FOR i% = 1 to 10000
to.array!(i%) = from.array!(i%)
PRINT time$
FOR j% = 1 to 50
cnt% = 40000 - 65536 'count
CALL PTR86 (from.seg%,, VARPTR( from.array!(1)) )
CALL PTR86 (to.seg%,, VARPTR ( to.array!(1)) )
CALL BlockMove(from.seg%,, to.seg%,, cnt%)
PRINT time$

We are doing 50 repeats of the bottom section of code so you will
be able to average the time. Here's the assembler program:

; - - - - - - - - - -
include /pushregs.mac
PUBLIC BlockMove
; - - - - - - - - - -
; BlockMove ( from.seg,, to.seg,, byte.count)
; for BASIC
; MOVSW is from DS:[SI] to ES:[DI]


; - - - - - - - - - -
BlockMove proc far
push bp
mov bp, sp

BASIC II - Interfacing BASIC With Assembler mmxxvii

PUSHREGS ax, bx, cx, dx, si, di, es, ds

mov es, [si] ; to_seg to ES
mov di, [si] ; to_offset to DI
mov cx, [si] ; byte count to CX
mov ax, [si] ; temporary storage for new DS
mov si, [si] ; from_offset to SI
mov ds, ax ; now move from_seg to DS
sub bx, bx ; clear BX
shr cx, 1 ; divide by 2, remainder in CF
rcl bx, 1 ; move CF to low bit of BX
cld ; clear DF (go up)
rep movsw ; the block move (count in CX)
and bx, bx ; one extra byte?
jz exit
movsb ; move one last byte

POPREGS ax, bx, cx, dx, si, di, es, ds
mov sp, bp
pop bp
ret (10)

BlockMove endp
; - - - - - - - - - -
; - - - - - - - - - -

This is a string block move using MOVSW. The count is the number
of BYTES, not the number of array elements. CX contains the byte
count. It is divided by 2 so we can move words, and if there is a
remainder (i.e. if the number was odd), BX is set to 1. We move
words instead of bytes, and afterwards we check BX to see if we
need to move 1 byte more. This routine takes about 1/8 second
instead of 5.5 seconds. This is a considerable savings in time.
There is a small problem, however. If the FROM block and the TO
block overlap (e.g. move 400 bytes from array!(11) to
array!(26)), then the data may be compromised. To be exact, if
the start of the FROM data is below the start of the TO data, the
data will be screwed up. The general solution of this for BASIC
is in BLKMOVE.ASM, which is in a file called MISHMASH.DOC which
is in \XTRAFILE.

Finally, here's the disk read done entirely in BASIC. Once again
you need your DOS interrupt book.

' reads a block from the disk into memory

DIM in.regs%(9), out.regs%(9)

The PC Assembler Tutor mmxxviii

DIM big.array! (10000)

AX% = 0
BX% = 1
CX% = 2
DX% = 3
BP% = 4
SI% = 5
DI% = 6
FLGS% = 7
DS% = 8
ES% = 9

filename$ = "blocktxt.doc" + CHR$(0)

PRINT time$
' open an existing file for reading
in.regs%(AX%) = &H3D00
in.regs%(DX%) = SADD (filename$)
CALL INT86 (&H21,VARPTR (in.regs%(0)),VARPTR (out.regs%(0)))

IF (out.regs%(FLGS%) AND &H0001) <> 0 THEN
PRINT "Can't open the file."
GOTO ExitProgram

' set the i/o pointer to 0
file.handle% = out.regs%(AX%)
in.regs%(AX%) = &H4200
in.regs%(BX%) = file.handle%
in.regs%(CX%) = 0
in.regs%(DX%) = 0
CALL INT86 (&H21, VARPTR(in.regs%(0)), VARPTR(out.regs%(0)))

IF (out.regs%(FLGS%) AND &H0001) <> 0 THEN
PRINT "File pointer error"
GOTO CloseFile

in.regs%(AX%) = &H3F00
in.regs%(BX%) = file.handle%
in.regs%(CX%) = 40000 - 65536
CALL PTR86 ( segment%, offset%, VARPTR (big.array!(1) ))
in.regs%(DX%) = offset%
in.regs%(DS%) = segment%
CALL INT86X (&H21,VARPTR(in.regs%(0)),VARPTR(out.regs%(0)))
IF (out.regs%(FLGS%) AND &H0001) <> 0 THEN
PRINT "Disk read error"

in.regs%(AX%) = &H3E00
in.regs%(BX%) = file.handle%
CALL INT86 (&H21, VARPTR(in.regs%(0)), VARPTR(out.regs%(0)))

PRINT time$

BASIC II - Interfacing BASIC With Assembler mmxxix


This shows the use of INT86 in BASIC. We have two 10 element
integer arrays (0 - 9). One is used for putting the data into the
registers before the interrupt call and the other is used for
getting the data out of the registers after the interrupt. At the
top we have substituted variable names for the array elements
they represent. This is the only way to make sense of things in
BASIC. What is the difference between INT86 and INT86X? INT86X
also changes ES and DS.

We need a different file opening call because the last one
TRUNCATED the file. This one just opens a pre-existing file and
we put 00 in AL to signal a file read:

INT 21h Function 3Dh
AH = 3dh ; open a pre-existing file
AL = 0 ; file read
DS:DX ; pointer to a 00h terminated string

AX = file handle if CF = 0
AX = error code if CF = 1

We use SADD to get the filename offset in DS because this is
exactly what DOS wants. SADD is a function that gives the offset
of a string (the string itself) relative to the DS segment. It
gives no length information.

At every step along the way we check CF to make sure it is 0 and
not 1. We need to make sure that the file pointer is at the
beginning of the file. This is:

INT 21h Function 42h
AH = 42h ; move file pointer
AL = 0 ; count from beginning of the file
CX:DX = 0 ; 4 byte offset from beginning of file

DX:AX = new file-pointer location if CF = 0
AX = error code if CF = 1

Then we do the block read:

INT 21h Function 3Fh
AH = 3Fh ; read a block from disk
BX = file handle
CX = byte count
DS:DX = pointer to first byte of block

AX = # of bytes read (can be less than CX) if CF = 0
AX = error code if CF = 1

The PC Assembler Tutor mmxxx

Finally, we close the file:

INT 21h Function 3Eh
AH = 3Eh ; close a file
BX = file handle

nothing if CF = 0 (close was successful)
AX = error code if CF = 1

All you need to know about CF is that when the flags are
represented as a word (2 bytes) CF = &H0001.

Is this faster than our other access methods? Our worst case
before took 79 seconds and this one takes 1 second. This is
certainly worth using for large disk reads. We don't need to go
down to the assembler level, either.

What's the difference between this and bload? Bload requires that
the file has already been stored from memory. When you use BSAVE,
the binary information is written to disk, but the first seven
bytes is BLOAD information. The first byte (0FDh) is a signature
byte. The next six bytes are three words. (1) the segment where
the data came from, (2) the offset where the data came from, and
(3) the length of the data. Of course, having these seven bytes
in the front makes the file incompatible with everything else in
the world. It even makes it difficult to load the information
into BASIC the first time, since this seven byte header is
missing in the original data unless the data came from a BASIC

So what sorts of things are candidates for assembler subroutines?
Things that are cumbersome in BASIC. If you want the top byte of
a number, that's difficult. If you want to rotate the bits of a
number, that's extremely hard. Shifting bits is hard. Practically
everything involving unsigned numbers is problematic. For every
assembler instruction, if you can't do it easily in BASIC you
should make a subroutine that does it in assembler. How about one
that does unsigned division? Another subroutine that you might
want to make is one that returns both the quotient and the
remainder from signed division. This cuts the work in half if you
need both of them.

Well, use BASIC any way you want, but most of all, have fun!

BASIC II - Interfacing BASIC With Assembler mmxxxi


BASIC strings are defined by STRING DESCRIPTORS. A string

descriptor is a 4 byte block that contains the LENGTH of the
string and its LOCATION in the DS segment. Though you may modify
individual bytes of a string from the assembler level, you may
not alter the length without interfering with BASIC's memory
management system.

BASIC passes all arguments by reference. That is it sends the
offset address of the data instead of the data itself. The ruiles

1) If it is a single piece of numeric data, the offset is
relative to the DS segment.

2) If it is a string, the address is the address of the
STRING DESCRIPTOR which contains both the length and
location of the string.

3) To reference an array, use VARPTR (array(0)) to get the
offset and then PTR86 to convert this to a SEGMENT:OFFSET
pair which is usable by the assembler subroutine.

4) If you pass a single array element array(x) instead of
using VARPTR, BASIC will pass the location of that element,
but the element might be separated from the rest of the
array, so only pass an individual element if you want the
element itself and not the array.{3}

SADD (stringname$)
SADD [Microsoft's string address function] is used to pass
the offset address of stringname$ relative to BASIC's DS
segment. It should only be used with 00h terminated strings
since this gives no length information. It can, however, be
used in conjunction with LEN (stringname$).

number! = VARPTR (variable)
In older BASICs, VARPTR gives the offset address of
"variable" relative to the first byte of the DS segment.
This variable can be anywhere in memory from DS:0000 to the
end of memory, and the number returned will be a single
precision number in the range 0 to 1,048,576.

In more recent BASICs, using a combination of VARSEG and

3. The rule here is that if the array itself is outside of the
DS segment (if it is a dynamic array), BASIC will make a copy of
the element inside of DS before the CALL, give you the address of
the COPY, and return the copy to its appropriate place in the
array after the CALL. This copy can be hundreds of thousands of
bytes away from the actual array. If you want the element itself
this works properly, but if you want the array, the address will
be the wrong address.

The PC Assembler Tutor mmxxxii

VARPTR has supplanted the use of PTR86.

PTR86 ( segment%, offset%, VARPTR (variable) )
PTR86 [Microsoft's segmentation scheme] takes the result
provided by VARPTR and adds it to DS to come up with a total
address. It then converts this absolute address into a
SEGMENT:OFFSET pair where the segment is the highest segment
that contains the first byte of the variable from VARPTR and
the offset is a number from 0 to 15 which is the offset of
this variable in this segment.

RET (x)
When executing a return, all called subroutines must pop the
arguments passed to them by BASIC. The number of BYTES
popped is twice the number of arguments (as long as you are
passing addresses and not actual data).

CALL MY_ROUTINE (arg1, arg2, arg3, etc)
Arguments are always PUSHed on the stack from left to right,
so this call will:

PUSH address of arg1
PUSH address of arg2
PUSH address of arg3

in that order.

INT86 ( interrupt.number%, in.reg.array%(9), out.reg.array%(9) )
INT86 executes a DOS interrupt (interrupt.number%). The
integers in in.reg.array% are put into the arithmetic
registers before the call and the arithmetic registers are
put into out.reg.array after the call. INT86X does the same
thing but also changes the DS and ES segment registers.
Consult your BASIC manual for the proper ordering of the
registers in the array. Neither of these changes CS, SS or