abrasax: (Default)
[personal profile] abrasax
I'll preface this by specifying it's only valid for the C64's BASIC v2 and, obvs, 6502 assembly. Further, BASIC didn't require spaces between keywords & arguments; in fact, BASIC programs were smaller and faster when the spaces were omitted. Even further - and ridiculously technical - the C64 used PETSCII not ASCII (see C64 PETSCII Codes). And yet another point of order, values will be given in $HEX / DECIMAL the first time you see them.


On ancient systems, like the 8-bit C64, machine language (ML) programs were written to be LOAD'ed into specific memory addresses only. Locations like the cassette buffer (starting at $033C / 828) or the 4KB area starting at $C000 / 49152 were popular. Once LOAD'ed into the location, you'd activate them by SYS'ing to said location. This often meant remembering those crazy memory address values. Ungodly values like $8000 / 32768, etc. Oh, the SYS command only accepted decimal values.

This is different from BASIC programs; those always loaded to $0801 / 2049. That's why, for BASIC, all you'd do is:

LOAD "*",8
RUN


.... and it worked.

It is possible to make ML programs that LOAD into $0801 and that activate with RUN by using something called a BASIC stub. Just to be clear - it wasn't always possible or desirable to do that. A lot of time the ML program was a tiny utility meant to operate like a TSR (terminate and stay resident) program or an IRQ (interrupt request) handler (to replace or enhance the C64's IRQ handler). Other times the ML was a monitor that you'd definitely want to keep out of the BASIC RAM area altogether. So the use case for a BASIC stub tended to be for large games or applications and not background stuff. Exceptions always existed and after nearly 50 years of people toying with the C64 you just never know what you'll encounter....

To the user, a BASIC stub appears to be one or more lines of BASIC code sitting right at the start of the ML program. There might be REM statements with the program's meta (name, copyright, revision, etc). There might be explanatory text. It could be very elaborate. What you actually needed, however, was a single BASIC statement that SYS'ed to the actual start of the ML program.

So, how's it done?

Nearly every assembler of that era gave you psuedo op codes or directives to let you drop a stream of bytes anywhere you wanted. On LADS this was confined to the .BYTE or .B directive. .B "TEXT" or .B 32 210 255 both implanted into the ML's byte stream the PETSCII values of TEXT or the literal values 32 210 255. My fork of LADS uses non-standard directives to accomplish this so I won't reference that. I'll stick to LADS's for this.

I'll stick to a single line of BASIC of the form 1000 SYSZZZZ; where ZZZZ is the plain text, decimal memory location where the ML program starts. I presume for this discussion that this memory location sits exactly where the BASIC stub ends. That's not a requirement! The ML program is free to start anywhere beyond the BASIC stub. And, in fact, if you happen to know exactly where, the BASIC stub is approx 50% easier to create.

Just to form even a single line of BASIC, using only assembly directives, it's important to know how BASIC stores its code. The 'simulated' BASIC line we create has to follow that paradigm exactly.

A BASIC program is stored in RAM as forward-link list of a somewhat fixed structure. Let's call this structure a BASIC record. It's fixed in so far as a BASIC record always contains three fields. (My terminology is non-standard, obviously, as I'm borrowing ideas from databases.) Field #1 is a two byte vector that points to where the next BASIC record exists in memory. That's the forward-link. Field #2 is the two byte representation of the line number. BASIC itself imposed the restriction that line numbers ranged from 0 to 63999 (inclusive for both; yes; line number 0 was valid as was 63999). Field #3 is the 'tricky' part; it's a free form byte stream that terminates with a null byte ($00 / 0). This byte stream contains the line's code as a mixture of plain text and binary / tokenized data.

Tokenized?

The C64 used a peculiar variant of the original ASCII table. We refer to it as PETSCII. PETSCII from $80 / 128 to $FF / 255 were the 'shifted' versions of $00 / 0 to $7F / 127. PETSCII from $00 to $1F / 31 and from $80 / 128 to $9F / 159 were referred to as control codes. Many of them, if 'printed' set the text's color, moved the cursor's position, cleared the screen, etc. The rest of the PETSCII was mapped to letters, numbers, symbols, and glyphs that Commodore developed for their PET computers in the 70s. Hence why we call this PETSCII....

A valid BASIC line (excluding anything between quotes) only contained 'unshifted' PETSCII characters between $20 / 32 to $7F / 127. Quotes were the exception to the rule. The C64 let you put anything you wanted between quotes no questions asked. Everything else had to be between that 'unshifted' range. OK, ready ... to save space and to simplify the BASIC interpreter just a tad, all BASIC keywords were tokenized; ie, replaced ... by a single byte value of the 'shifted' range $80 / 128 to $FF / 255. Keywords like FOR or NEXT were replaced by the bytes $81 / 129 and $82 / 130. No other aspect of a BASIC line was tokenized. So if you typed GOTO 1000; GOTO became $89 / 137 but the 1000 remained plaintext 1000.

And here's the last quibble about BASIC that concerns us - if it's a forward-link list, how do we know when we reach the end? The end comes the moment we encounter a forward vector of $00 | $00 or $0000. That signals the end of the BASIC program.

And now for a point of order. The 6502 is a little endian processor. What does that imply? It implies that when we feed it 2 byte values we do in lo-byte, hi-byte order. $0000 is not particularly instructive, is it? How about $0801? We are big endian as a rule. So when we see $0801, we'd say, oh, eight, oh, one, correct? We start at the highest (placement) and work to the lowest (placement). The 6502 tosses that convention aside. It reads lo byte then hi byte, in that order. So when we look at $0801 how do we determine hi vs lo? Split the baby: 08 | 01 and the left pair is the hi byte and the right pair is the lo byte. Hence the 6502 reads it as $01 | $08.

OK, enough of that, what about this damn BASIC stub you keep promising?

I want to know, essentially, the byte stream that represents the single BASIC line: 1000 SYS ZZZZ (where ZZZZ will be a plain text memory address). Yes, I assume for this example that the ML program starts immediately after the BASIC stub stops. If you happen to know exactly where the ML starts, ZZZZ is replaced by that value and 1/2 the work is finished.

As a first approximation, let's start with this idea:

.* $0801 ; set program counter / start address to $0801 / 2049

.B $XX $YY ; the forward vector $YYXX expressed as lo | hi byte order
.B $E8 $03 ; the line number expressed as lo | hi byte order 1000 dec = $03E8 hex
.B $9E ; the tokenized SYS command
.B "ZZZZ" ; the plain text SYS argument, note that if the number of digits vary (ZZZ vs ZZZZZ), this affects the byte counts
.B $00 ; the null byte to terminate the BASIC's Field #3 (as discussed above)
.B $00 $00 ; the zero vector to terminate the BASIC program (as discussed above)

< I assume the ML program starts immediately after the BASIC stub >


Let's start filling the details I left vague. Thanks to the .* directive, we know that the stub's very first line (.B $XX $YY) sits at $0801. That $YYXX needs to reflect where the stub's very last line (.B $00 $00) sits at. To find this, count how many bytes it takes to walk from the 'right / hi byte' $YY to the 'left / lo byte' $00. Every 1 byte value and character in quotes counts as a single byte. Just as a sanity check - so we're all on the same page - I count four bytes just to get from $YY to $9E and five bytes to get to the single stand alone $00 at the .B $00 line . Hence I'd need yet another byte to reach the 'left / lo byte' $00 of the .B $00 $00 line. That totals four plus five plus one or ten bytes to reach the next line of BASIC. I need to add ten bytes to $0801; in hex ten is $0A, and $0801 + $A = $080B. So $YYXX = $080B or $XX = $0B and $YY is $08

Perfect, what about the ZZZZ? So at this point we know the 'left / lo byte' $00 of the .B $00 $00 line is at $080B; the 'right / hi byte' is at $080C; and the very very next byte after that - where I assume the actual ML program starts - that would be $080D / 2061; so ZZZZ is 2061. Putting all of it together:

.B $0B $08 ; the forward vector $080B expressed as lo | hi byte order
.B $E8 $03 ; the line number expressed as lo | hi byte order 1000 dec = $03E8 hex
.B $9E ; the tokenized SYS command
.B "2061" ; the plain text SYS argument
.B $00 ; the null byte to terminate the BASIC's Field #3
.B $00 $00 ; the zero vector to terminate the BASIC program


That's if we do it the simple / easy way, where it's only a single line of BASIC code. Nothing stops us from adding REM'arks for context, like, program's name, revision, etc. But if we do that we add to the stub's complexity. The easiest way to accomplish a complex stub would be to type it as if it were any other BASIC program. Then start your monitor of choice and examine memory starting at $0801. That ought to reveal the entire binary structure of your BASIC linked list. Jot the values and include them as .B directives at the start of the assembly code.

Anyway, it's one of those things that may be useful. For my next TED talk I'll demonstrate how 'startup parameters' in either a SYS or a RUN command could be used in conjunction with a ML program.

Profile

abrasax: (Default)
Abraxas

Badges

NSFW/18+LGBT PrideBelieves in ScienceFilm BuffWriterPhotographerAvatarVarang from Avatar Fire and Ash

November 2026

S M T W T F S
1 2 3 4 5 6 7
8 9 10 11121314
15161718192021
22232425262728
2930     

Style Credit

Powered by Dreamwidth Studios
Page generated 2026-11-11 09:00 pm