It’s time to move on from BASIC integration to standalone machine-code programs. As a rule, after all, I like to write my programs for this blog in a way such that every byte in the program image is there because I put it there. Furthermore, if we want our program to be as broadly runnable as possible, we can’t rely on BASIC at all—BASIC was provided as an optional cartridge on the Atari, and on later models of it having it active would lock away some of your RAM and not let you get it back.
Part of having BASIC be optional, though, is that this means that the Atari DOS systems needed to be self-contained enough to take commands and load and run programs on their own. In this article we’ll take our little Hello World program and bundle it up in a way that DOS would load it, and then we’ll move beyond DOS and pack the program directly into its own bootloader, for a disk image that will set itself up and run without intervention at all.
Atari DOS files
Executable binaries in Atari DOS have a very clever and flexible format. Pretty much every other format we’ve seen—including the formats for modern operating systems—have each loadable file be a single contiguous span of address space. The Atari binary format though (which for some reason is usually rendered XEX when it’s a standalone file to be passed to emulators) is composed of a series of contiguous chunks that are loaded individually. Furthermore, the load may be interrupted at various points to run code before the load proceeds, and a start address for running the program as a whole once the load is completed is available as a separate option.
This is really, really nice, and it gives you a lot of capabilities “for free” that required clever and occasionally abusive techniques to get working on the C64. Even if we ignore the fact that C64 programs rarely would run themselves upon load (this was not an intentional ability, but you could play some tricks where you loaded a stub program into the middle of critical OS variables to hijack the return to BASIC), there’s a lot of individual phases here that let you produce a very nice user experience:
- Since the hardware on Atari systems could be so different, you could first load a very tiny program into page 6 that just checks the configuration to ensure that everything is compatible, and errors out if it is not.
- Loading programs took a long time, so you often would want to have some kind of loading screen to look at while the load is going on. This could be set up as a chunk of its own, directly into screen memory, even, if you so chose.
- Once the program had finished loading, it would then proceed to the main program.
On the C64, the Apple, the Spectrum, or in MS-DOS, this would be done by having three different files, each of which loaded the next in succession. Here you can represent the entire program as a single file without compromising any of it.
Format of an Atari DOS Executable
A binary file begins with two $FF bytes, followed by one or more chunks. A chunk is two addresses representing the first and last address to load to, followed by enough bytes to fill that space. Note that unlike many range-based systems, the end address here is inclusive, so the number of bytes used is end-start+1, not simply end-start.
Two chunks have special meanings.
- A chunk spanning $02E2-$02E3 specifies a routine to call mid-load.
- A chunk spanning ,tt>$02E0-$02E1 specifies the start address for the entire program. If there are multiple writes to these locations over the course of the load, only the last one counts.
There are otherwise no requirements other than that we return to the system with an RTS instruction, as if the whole program were a procedure call. This matches what we saw on the C64 and the Apple II. (In fact, there is not technically a need to set a run address at all; in such cases the DOS will load the program into memory and do nothing, like the Apple’s BLOAD command, and expect the user to give a “run program at address” command later on.)
Hello World as an Atari Executable
We only need two chunks for this; the program itself, and the specification of where to run it from. We start with the binary header and the assembler directives to let it compute everything we need for us:
.word end - 1
Then, because I’m going to get tired of filling in IO control blocks by hand, I’ll define a pair of macros to store byte and word values, respectively:
;; `iostob OFFSET, VALUE
;; `iostow OFFSET, VALUE
;; Channel is in most significant nybble of X register
sta $340 + _1,x
`iostob _1, <_2
`iostob _1+1, >_2
Then we have the program itself, bracketed by the start and end labels that we need to compute our chunk size up top:
;; Channel Zero (E:)
;; Write message and exit
`iostob 2, 11
`iostow 4, msg
`iostow 8, msgend-msg
msg: .byte "Hello, world!",$9B
And finally we define the chunk to specify the start address:
.word $02e0, $02e1, start
Assembling this and putting the result on a disk that has DOS on it lets us run the program from the DOS menu:
This is quite nice, but it does leave open the rather sticky question of where binary programs should load themselves. Different versions of DOS took up different amounts of RAM, and you didn’t really want to trash the DOS while you were loading unless you had no intention of returning to DOS afterwards.
On the other hand, if you were distributing your program on a disk instead of as a specific file, as you likely were, then you did not need to care about any version of DOS but the one on your disk, and if you were a game or other similar application that is expected to completely usurp all system operations, did you really need a DOS at all?
Doing Away With DOS
The main OS ROM included its own logic for bootloading, and it’s a bit more powerful than the on-ROM bootloaders I’m used to from the PC’s BIOS. What I’m used to is a boot process where the first sector of the disk is loaded and then run—what the Atari provides is a system where the first 128-byte sector is loaded and then that specifies how many additional sectors to load, where to load it, and where its initialization code lives. For a DOS bootstrapper, or a large program that would be loading different bits of data in at different times, this would be a small program that’s responsible for loading the rest of the program—a bootstrap loader. But for a simple program that just needs to get itself into memory, we can have the “bootstrap loader” be the entire program.
The Atari Operating System Reference Manual includes a skeletal program that will manage being booted and then handing control over to an application program. We can make that work with our Hello World program, too. It also provides a cartridge-style program intended to write a program onto a master disk. That is less useful to us. What we will want to do is to create a bootable disk image directly. Fortunately, the Atari disk image format is really simple:
- Two file identifier bytes, $96 $02
- The size of the disk image, divided by 16
- The size of a sector, in bytes (always 128, for us)
- Padding out with zeroes so the full header is 16 bytes
- The contents of each sector, in sector order, from 1 until the end
Since there isn’t any metadata at the edges of sectors, we are free to have our bootloader program simply assemble in place at the start of sector 1 and keep going until it hits the end of the last sector it needs. Our Hello World program is very short and will fit in a single sector:
;;; ATR disk header
.word $0296, $8, $80
The format is flexible enough that we seem to be entirely allowed to specify a disk image for a disk with only one sector.
What then follows is the boot information; this is six bytes that specify the number of sectors to load, the address to load the bootstrapper into, and the start address for that code. The earliest location that definitely belongs to disk-booted software is $700—this is where DOS loads its memory resident portions, and in some sense we are DOS now, so we’ll load there.
;; Boot data
.byte 0 ; Flags
.byte 1 ; Number of sectors
.word $0700 ; Load address
.word init ; Init address
Before the init routine is called, though, the rest of the bootstrapper then runs. This code starts right after the end of those header bytes, and its job is to initialize some memory locations to tell the ROM systems how much memory we’re taking up. We need to put the first address available to the system into the locations named MEMLO and APPMHI…
And since we’re taking over the system and are not some kind of DOS-like support program, we also need to set the DOSVEC address to point to us.
Then we clear the carry flag to indicate the boot was a success, then return:
We don’t really have any initialization to do either, so we can point the init label at that RTS statement. The main program needs no real changes—the OS ROM opens the screen editor as device 0 for us before it even looks to see if a disk is in the drive—but we do need to add an infinite loop at the end of the program instead of returning back to to a non-existent BASIC or DOS system, and we’ll also need to pad out the rest of the disk sector to complete the image.
loop: jmp loop
Assembling this produces a 144-byte disk image file that Altirra happily loads and runs.