Commodore logo

Cenbe’s Commentary on GeckOS

(last updated 2024-05-25)
Commodore logo
GeckOS console
GeckOS console (click to enlarge)

GeckOS is under active development; André has just released version 2.2, so by now this page is quite out of date. You can find the latest source in André's repo on GitHub. In the source, you'll find a directory named doc/ with thorough documentation in AsciiDoc.


GeckOS is a Unix-like 6502 operating system by André Fachat with preemptive multi-tasking, signals, semaphores, redirection, a standard library, and its own relocatable file format. I gave a talk about GeckOS at VCFMW 2019; here's the video, and here are my slides. I also gave a talk at the World of Commodore show in Toronto (December 2019), highlighting some of the recent enhancements in GeckOS; here are the slides and video from that talk.

I've been studying the GeckOS source code, and this page is where I'm documenting my analysis of the Commodore 64 version. The code is a bit of a labyrinth since it supports so many possible architectures and devices via #define directives (C64, PET, André's homebrew machine...), but here's what I've found out about it so far. Note that there may be errors; this is a work in progress (search for "TODO") as I continue to learn GeckOS (please send corrections to cenbe at protonmail dot com). If you'd like to follow along at home, you can have a look at the source code on GitHub. NOTE: Filenames and line numbers in this document refer to the 2.0.9 release.

Building GeckOS for the Commodore 64
System Initialization
Starting the ROM Image Programs
The Scheduler
Running Programs from the lsh Shell
Enhancements
Long-Range Questions


Building GeckOS for the Commodore 64

For information on building GeckOS on a C64 and a memory map, see README.c64 in the docs. Also of interest is doc/files.txt, which briefly describes what's in each source file. You can build with the latest version of xa, as it has had some recent bugfixes.

For those who believe that "real men never read the docs", the short version goes like this (assuming Linux):

After a successful build, you'll find a file named arch/c64/c64rom.lab, which is a listing of labels and corresponding addresses. Sorting that file will give you an invaluable tool for exploration.

The central source file of the C64 port is arch/c64/c64rom.a65 (the "ROM image", so called because it appears in ROM on André's homebrew machine). Note the load address of $1800 in line 172. The following occurs when you boot the operating system from disk by loading and running LOADER:

Source File Layout

The source making up the ROM image (arch/c64/c64rom.a65) starts with the device drivers: at line 180, devices/c64dev.a65 is included, which starts with a small header, then includes:

Back in c64rom.a65, the following are included:

Then follows an autostart header for the lsh shell, which is loaded from disk.

After this, lib6502.a65 (the lib6502 runtime) is included, which brings in the following files:

We then come to the "hole" from $D000-$EFFF (see lines 285-291 of c64rom.a65). Continuing at $F000, kernel/kernel.a65 is included, which brings in:


System initialization

The first entry in the jump table (at $F000) points to preset in kernel/init.a65, which is the entry point jumped to by BOOT after relocation. After disabling interrupts and clearing decimal mode, the C64-specific portion is run (in arch/c64/kernel/kinit.a65), which does the following:

After this, a series of initialization routines is run starting at kernel/init.a65, line 132:

CIA timer usage  The CIA timers are used as follows:

CIA1 timer A RS-232 send
CIA1 timer B scheduler, device service
CIA2 timer A unused
CIA2 timer B serial I/O (IEC), RS-232 receive

The TOD clocks are not used.


Starting the ROM Image Programs

After all of the initialization routines have been called, the ROM image is scanned for executable headers (line 158 in kernel/init.a65). For an overview of this process, see the ROM bootup section of the kernel documentation. Only programs of type PK_DEV or PK_INIT are started here; since the ROM image begins with devices and then sysapps/init/init.a65, those are the autostarts that get run first. When the init in sysapps runs, it makes a second pass through the headers and starts programs of types PK_PRG (a standalone pogram not depending on lib6502), PK_FS (a filesystem), and PK_LIB (a lib6502 program). It's this process that eventually starts both an "old-style" shell/monitor on console 2 and an lsh shell (which superseded it) on console 1.

PK_DEV (device initialization: start ROM programs, pass 1)

The devices in the C64 version are:

PK_INIT (prepare sysapps/init to run)

The next startup program is sysapps/init/init.a65 (header at line 76). When kernel/init.a65 encounters the PK_INIT header at line 186, it branches to ifs (line 201; C64's code actually begins at line 250), which sets up a FORK call to run sysapps/init using information in the header (passed in PCBUF):

starting the scheduler

Control returns to kernel/init, and the loop to start ROM programs eventually terminates, as none of the rest are of type PK_DEV or PK_INIT. Now kernel/init has finished his work, and at line 172, he jumps to pstart to start the scheduler (kernel/tasks.a65, line 478). This jump performs the following:

sysapps/init (start remaining ROM programs: pass 2)

TODO:


The Scheduler

The important constants for task switching (arch/c64/config.i65, lines 53-55) are:

The task table (taskTab) is MAXNTASKS * TT_SLEN long (12 * 14 = 168 bytes); the offsets for each task structure are found in kernel/tasks.a65 at line 53:

The thread table (threadTab) is MAXNTHREADS * TH_SLEN long (12 * 8 = 96); the offsets for each thread structure are found on line 67:

The possible states for a thread (include/kdefs.i65, line 211) are:

Each task has at least one thread; the task ID in the thread table is an offset into the task table. Thread IDs are also an offset into the corresponding table (see opening comments in kernel/tasks.a65).

arch/c64/c64rom.a65 #defines STACKCOPY, which results in the allocation of Stacks with a size of MAXNTHREADS * STACKSIZE (12 * 64 = 768). The stack is split into two parts, one for the kernel and one for threads. When a new task's thread is created in the fork kernel API call (kernel/tasks.a65, line 192), initsp is called at line 259. initsp (arch/c64/kernel/kenv.a65, line 188) sets the stack pointer for the new task's thread to STACKSIZE - 1; i.e. the lower 64 bytes of the stack are used by tasks/threads, and the upper 192 bytes by the kernel.

To perform a context switch (see setthread in arch/c64/kernel/kenv.a65, line 77), the current thread's stack is first copied into Stacks. The offset into Stacks is computed by first multiplying the thread ID by 8. Since a thread ID is an index into the thread table (which has 8-byte entries) this has the effect of indexing by 64-byte increments, which is the value of STACKSIZE. The thread's stack pointer is then used as an index to copy from the 6510 stack to the thread's entry in the Stacks table. The same process is then used to copy the new thread's stack from Stacks to the 6510 stack.

Kernel APIs switch from user space to kernel space by calling memsys (arch/c64/kernel/kenv.a65, line 304), which saves the calling thread's registers and stack pointer and switches the stack pointer to the system stack (SSP). memtask (line 268) reverses this process to return to a task from the kernel.

Forking a new process via the FORK kernel API call works like this:

FORK expects a structure with the following offsets at PCBUF (defined in include/kdefs.i65, line 325):

The IRQ Service Routine

The IRQ routine is responsible for process scheduling and device handling; it's at pirq (kernel/init.a65, line 576), and is called directly from the 6502 vector at $FFFE (see kernel/end.a65). It runs as follows:


Running Programs from the lsh Shell


Enhancements

Some of the enhancements I mentioned in my VCFMW talk have already found their way into GeckOS; I'll describe them here. I also spoke about them at my "Hacking GeckOS" talk at World of Commodore in Toronto.

info command
original info command (click to enlarge)
info command
new ps command (click to enlarge)

process name, exec address in info command

The old shell has an info built-in command that is like the Unix ps command. It calls the kernel GETINFO API (which reads the task table), but GETINFO didn't originally return process names or exec addresses. There is space for the first six characters of the process name in the getinfo struct (include/kdefs.i65, line 227), but the task table had no entry for process name. Storing a task's execute address was not implemented at all. Having both of these items in a ps command for the lsh shell is invaluable for debugging.

The first screenshot on the right shows the output of the original version of the info command. Note the lack of process names (lsh is in fact incorrect and should be init) and exec addresses.

The task table could have been expanded to include these two items, but it would require changing a lot of code that would have to deal with index register overflow. André's solution was to split it into two smaller tables of the same length, which solved this (the PID is still an index into these tables).

But there's a catch: populating these two new fields is not straightforward for lib6502 programs because of how they call FORK:

These problems were solved like this:

standalone ps command with process name and exec address

At this point, André ported the info command to the lsh shell as ps; the second screenshot on the right shows this new code. Note that the process names and exec addresses are now present. There are even -a (show all task table entries, even inactive) and -l (show all fields, resulting in two lines per process) options. Of course, it's now theoretically possible for the output to scroll off the screen, so a more command was added. On the C64, there is no pipe character on the keyboard, so use a single-quote, e.g. ps -al ' more.

a proper kill command

I wrote a kill command for the lsh shell based on the one in the old shell, except that it can send arbitrary signals like the standard Unix command (this program was merged to the master branch). Note that the SETSIG/SENDSIG section of the kernel documentation makes a distinction between calling the kernel KILL API and sending a SIG_TERM signal, in order to allow programs to release their resources before ending. However, no such signal is listed in include/kdefs.i65. Interestingly, there is a SIG_KILL, which doesn't appear to be used anywhere, and a SIG_BRK (also not used), whose comment reads "ctrl-C received". Things to keep in mind for future reference!

further plans

possible bugs (needs more study)

segment boundaries and load addresses -- a warning

When doing anything that changes the size of memory structures in c64rom, make sure to look at the build output for the line "c64rom.o65: o65 version 0 executable file" and check the following lines to make sure none of the segments overlap. If so, adjust the segment locations on line 11 of arch/c64/Makefile. The -b option in the xa assembler sets the segment start addresses: t (text), d (data), z (zero), and b (bss).

For example, here is the output if you split the task table into two tables with 12-byte entries:

c64rom.o65: o65 version 0 executable file
 mode: 0000 =[executable][16bit][byte relocation][CPU 6502][align 1]
 text segment @ $77fe - $10000 [$8802 bytes]
 data segment @ $0300 - $0b04 [$0804 bytes]
 bss  segment @ $0a90 - $15d5 [$0b45 bytes]
 zero segment @ $0008 - $006f [$0067 bytes]
Note the overlap of the data segment into the bss segment! The original line in the makefile is:
${XA} -R -bt 30718 -bd 768 -bz 8 -bb 2704 c64rom.a65 -o c64rom.o65 -l c64rom.lab ;\
and it would have to be changed to:
${XA} -R -bt 30718 -bd 768 -bz 8 -bb 2822 c64rom.a65 -o c64rom.o65 -l c64rom.lab ;\

It may also be necessary to adjust the load address upward for c64rom.a65 (Memstart, at the end of the source file).


Long-Range Questions

Could CMD-style directory and partition commands be supported on CMD devices and μIEC?

Could the networking capabilities of a 1541 Ultimate II+ be exposed as a device?

Could the stack swap during a context switch be done using an REU?

A text editor with emacs-like keybindings would be very nice.