Subscribe Now
Trending News

Blog Post

Cenbe’s Commentary on GeckOS

Cenbe’s Commentary on GeckOS 

GeckOS console

GeckOS console (click to enlarge)

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. The latest release is 2.0.9, but
there’s active development
on GitHub. I gave a
talk about it
; 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;
at some point I’ll lose the line numbers and update the commentary for
the latest development version.

Building GeckOS for the Commodore 64
System Initialization
Starting the ROM Image Programs
The Scheduler
Running Programs from the lsh Shell
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):

  • download xa
    and extract to a convenient location
  • change to the directory and run make, make
    , and (as root) make install
  • download the GeckOS source,
    either version
    the master
    , and extract to a convenient location
  • change to the directory and run make clean, then
    descend into arch/c64/boot and run make
  • load and run LOADER in the resulting disk

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

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:

  • LOADER (a BASIC program) loads C64ROM
    from $1800-$9FFF
  • LOADER loads a small ML program
    called BOOT (arch/c64/boot/boot.a65) from $0C00 to
  • LOADER passes control to BOOT, which
    initializes the hardware and then relocates C64ROM
    from $7800 to $FFFF, leaving a “hole” from $D000 to $EFFF (see
    lines 285-291 of c64rom.a65). $D000-$DFFF, of course, is where
    the C64 maps in I/O, and GeckOS maps the screen buffers for its
    four consoles at $E000-$EFFF.
  • Once the relocation is complete, BOOT jumps
    to $F000

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:

  • devices/console.a65
    (includes CONSOLE_DEVICE,
    defined as devices/con_64.a65)
  • devices/nulldev.a65
  • devices/spooler.a65
  • devices/ser_9600.a65 (up9600 serial driver)
  • devices/ser_acia.a65 (6551 at $D600) Note: this is a custom
    device of André’s, and could be commented out
    (arch/c64/c64rom.a65, line 65).

Back in c64rom.a65, the following are included:

  • sysapps/init/init.a65 (main init)
  • sysapps/fs/fsdev.a65
  • sysapps/fs/fsiec.a65
    (includes FSIEC_DEVICE,
    defined as devices/siec_64.a65)
  • sysapps/mon/mon.a65
    (embedded “old-style” shell
    and ML monitor)
    (includes sysapps/mon/shmon.a65 and sysapps/mon/shdir.a65)

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

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

  • lib6502/libdef.a65
  • lib6502/libglob.a65
  • lib6502/libmem.a65
  • lib6502/libsem.a65
  • lib6502/libsig.a65
  • lib6502/libfile.a65
  • lib6502/libexec.a65
  • lib6502/libloader.a65
  • lib6502/libnet.a65
  • lib6502/libenv.a65
  • lib6502/libjmp.a65 (jump table)

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:

  • kernel/jmptbl.a65
  • kernel/init.a65
    (includes arch/c64/kernel/kinit.a65, which
    includes kernel/ramtest.a65 and kernel/zerotest.a65)
  • kernel/tasks.a65
    (includes arch/c64/kernel/kenv.a65)
  • kernel/streams.a65
  • kernel/devices.a65
  • kernel/files.a65
  • kernel/end.a65 (interrupt and reset vectors)

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:

  • The stack pointer is initialized to #$FF.
  • The C64 ROMs are banked out.
  • Timer B of CIA 1 is set up to generate an interrupt every 20ms
    (counting system clock pulses) for the scheduler. Note that
    interrupts are still disabled at this point.
  • Memory is tested.

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

  • ininmi (kernel/init.a65, line 390) sets the NMI
    vector to point to an RTI.
  • initthreads (kernel/tasks.a65, line 110) zeros the
    thread and task tables.
  • inienv (arch/c64/kernel/kenv.a65, line 67) sets the
    active thread and task IDs
    (actThread, actTask) to #$ff and sets
    the kernel interrupt flag (Syscnt).
  • inidev (kernel/devices.a65, line 57) sets the
    device count to zero and sets the in-device-driver flag
    (adev) to #$FF. MAXDEV (maximum number
    of devices) is defined as 16 in arch/c64/config.i65. The system
    frequency (freq) is set to 0 on a C64, meaning

    TODO: Figure out exactly what adev does. It
    seems to be used to trick the kernel into thinking that we’re
    not in kernel space during a call to DEVCMD (see
    kernel/devices.a65, line 239).
  • initstream (kernel/streams.a65, line 81) zeros the
    streams table. ANZSTRM (number of streams) is set
    to 16 in arch/c64/config.i65.
    (Hint: ANZ is an abbreviation
    for the German anzahl, or number.)
  • inisem (kernel/tasks.a65, line 1212) clears the
    semaphore tables. ANZSEM (number of semaphores) is
    set to 8 in arch/c64/config.i65; SYSSEM (number of
    system semaphores) is also set to 8.
  • fminit (kernel/files.a65, line 42)
    sets anzfs (number of file systems) to 0 and
    clears fstab (the file server
    table). MAXFS (maximum number of fileserver tasks)
    is set to 4 in arch/c64/config.i65. See
    the filesystem
    documentation for more details.

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 first ROM program header (c64dev.a65, line 48) has a
    P_KIND of PK_DEV and
    a P_ADDR pointing to devstart (line
    64). The kernel init (kernel/init.a65, line 189) passes this
    address to the DEVCMD kernel API in .X and .Y, with
    a device command of DC_REGDEV (register devices) in
    .A. The address points to a structure described in the
    kernel API docs
    (see DEVCMD): it’s a linked list with each item
    containing a jump to the device’s message handler and the device
    name. Note that PK_DEV programs do not get
    entries in the task table.

  • When DEVCMD registers devices
    at regdev (kernel/devices.a65, line 157), it walks
    this table. If the number of devices (ANZDEV) has
    reached the maximum (MAXDEV), the device is
    ignored. Otherwise, the address of the jump is copied to the
    device table, DEVTAB (kernel/devices.a65, lines
    48-54), indexed by the number of devices (i.e. the device being
    added). DEVTAB is actually three tables with
    one-byte elements: DVT_ADRL, DVT_ADRH,
    and DVT_ENV.

  • After incrementing ANZDEV, DEVCMD
    calls itself recursively, this time with a device command
    of DC_RES (initialize/restart device). Control gets
    passed to exejmp (kernel/devices.a65, line 221),
    which ends up doing an indirect JSR to the jump command.

  • The remainder of the DC_REGDEV table is walked,
    and eventually the next ROM program is evaluated.

The devices in the C64 version are:

  • video1video4  These are
    the four consoles. Variables for each are stored in a table
    called vtab (devices/console.a65, lines 431 – 443)
    as well as a few smaller tables (lines 67 – 71). For the first
    console only, hardware initialization is performed
    by console_init (arch/c64/devices/con_c64.a65, line
    54). This sets up the VIC chip, cursor variables, and keyboard
    hardware. The VIC chip is set not to generate any
  • nuldev  Null device. A 16-byte character
    buffer at instr is initialized to all $FF.
  • spooler  Status and number of input
    streams are initialized to zero.
  • ser1, ser2   Serial (RS232, not IEC)
    ports. Both devices/ser_9600.a65 (a “up9600” driver) and
    devices/ser_acia.a65 (custom hardware) are included, in that
    order. The code is written so that the first device will be
    appear as ser1, the next as ser2,
    etc. (I’ve commented out the ser_acia driver in my
    build). ser_9600 sets up CIA1 for sending (timer A
    continuous mode, shift register out, no IRQ) and enables an IRQ
    on shift register full. CIA2’s shift register is set for input,
    the user port data pins are configured, and the NMI vector is
    set to NMIserial (ser_9600.a65, line 330).

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):

    are set to STDNUL ($FC).
  • Priority is set to 0 (inherit parent task’s priority).
  • Required RAM is set to three pages (768 bytes).
  • All available memory is requested for shared memory.
  • Execution address is set to the start of sysapps/init (for the
    C64 version, line 150).
  • The FORK call is executed, and assigns task ID 0 to
    sysapps/init, filling in its entries in taskTab
    and threadTab. The scheduler is not yet running, so
    sysapps/init doesn’t start.

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:

  • Clear the table of redirectable tasks (Xenv).
  • Set thread 0 (sysapps/init) as the active thread.
  • Enable interrupts for pre-emptive multi-tasking.
  • Fall through to the scheduler proper. There’s only one thread,
    and newly-created threads have a status of TS_RDY,
    so sysapps/init runs immediately.

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

  • The C64 version starts at sysapps/init/init.a65, line 150.
  • Set up for output of messages, print “Init v1.0 booting” to
  • Set the signal address for SIG_CHLD (handler at
    line 651).
  • Make another pass through the autostart programs
    (sysapps/init/init.a65, lines 216-276), this time processing
    types PK_PRG, PK_FS,
    and PK_LIB. For each program with the PK_AUTOEXEC
    flag set, call dostart (line 385):

    • Call YIELD (this does nothing the first time it’s called, as
      nothing’s been forked yet).
    • If PK_LIB, jump to execlib
      (see lsh startup), else:
    • Assign streams.
    • Block until semaphore lock on PCBUF is free (call
      PSEM w/carry clear).
    • Set up PCBUF for a fork.
    • Print “Start…” message on the console.
    • Call FORK to run the program
      (init_forkto for PK_LIB programs),
      using the information in the ROM header.
    • On return from dostart (at line 245), if the
      PK_RESTART ($40) flag is set, add the ROM struct address and
      the task ID to tables
      (lpa, hpa, pid) used
      for restarting such processes when they die, and print
      “Prepared restart!” to console.

    The remaining autostart programs run here are:

    • sysapps/fs/fsdev.a65 (PK_FS)

      • Set buffer states to F_FL_FRE.
      • Call SEND, passing message
        type FM_REG (register filesystem) in .A and target
        task SEND_FM in .X. PCBUF contains
        the number of drives to register (2)
        and fsdev‘s task ID. SEND
        calls fm (kernel/files.a65, line 48) to store
        the number of drives in the fstab table and the
        task ID in the fstask table (but see the note
        below for the fsiec driver). This driver
        responds for drives a: (device list) and b: (system programs
      • Release the PCBUF semaphore (call VSEM).
      • SEND returned E_OK with carry clear, so
        continue at loopt (line 128).
      • Since the number of open files (anz) is 0,
        call RECEIVE with carry set (i.e. wait for a
      • RECEIVE looks for a thread with
        status TS_WFRX (waiting for receive); not
        finding one, it sets fsdev‘s status to TS_WFTX
        (waiting for transmission) and exits by jumping
        to nexttask. fsdev is now waiting
        for messages to be sent to it; when the scheduler runs it
        again, it will resume after the call
        to RECEIVE.
      • The next task will be init, which returns to
        the line after the YIELD
        in dostart (line 388) to continue processing
    • sysapps/fs/fsiec.a65 (PK_FS), which includes

      • Call IECINIT (in siec_64) to initialize CIA2
        for serial I/O.
      • Set buffer states to F_FL_FRE.
      • Call SEND, passing message
        type FM_REG and target
        task SEND_FM. PCBUF contains the
        number of drives to register (4) and fsiec‘s
        task ID. SEND calls fm
        (kernel/files.a65, line 48) to store the number of drives in
        the fstab table and the task ID in
        the fstask table. These four drives correspond
        to device numbers 8-11 on the C64 and drive letters c:
        through f: in GeckOS. Note: the number of drives
        is added to the previous entry so that the correct
        filesystem driver can be found from a drive number by
        the fm routine in kernel/files.a65.
      • Release the PCBUF semaphore (call VSEM).
      • Lock the IEC semaphore (line 216), wait 20ms, then release
        it. (I’m not sure what this is for; there’s a German comment
        in the code that I think means “after switching on autostart
      • Enter a loop (line 321), starting with YIELD,
        then calling RECEIVE. If there is no
        message, fsiec‘s state is set
        to TS_WFTX (waiting for
        transmission). fsiec is now waiting for
        messages to be sent to it. If a message is received, it’s
        processed at rxmess (line 764).

      See the
      GeckOS documentation for
      more information on the filesystem interface.

    • sysapps/mon/mon.a65 (embedded
      old-style” shell and ML
      monitor, both PK_PRG): shell‘s
      program type has the autostart bit set,
      while mon‘s does not. Note that this
      shell’s monitor builtin has SHORTMON
      defined (c64rom.a65, line 219), which means that it has been
      made smaller by the removal of the assemble and disassemble
      commands. These commands work if you load mon
      from the lsh shell.

      • Release the PCBUF semaphore (call VSEM).
      • Set the signal address for SIG_CHLD.
      • Set terminal to full-screen mode (putc TC_ECHO to
      • Initialize streams, files, &c.
      • Print shell copyright message to screen and show prompt.
      • The shell then enters a loop (lines 294-324) reading and
        executing commands.
    • lsh shell
      (PK_LIB, program header is directly in
      c64rom.a65): PK_LIB programs are loaded from disk,
      and take the branch to execlib
      (sysapps/init/init.a65, line 696).

      • Set streams.
      • Block until semaphore lock on PCBUF is free
        (call PSEM w/carry clear).
      • Set up PCBUF for init_forkto
        (calls init_forkto_i at lib6502/libexec.a65,
        line 104). The differences between a
        regular FORK call and init_forkto
        are as follows:

        • FORK_SIZE is set to Memend
        • FORK_SHARED is set
          to #>-ROMSTART
        • FORK_PRIORITY is set to 0
        • FORK_ADDR is set to the address
          of libfork (lib6502/libexec.a65, line
        • The first byte of
          FORK_NAME is set to the stream number for
          the loader (init_forkto later copies the
          name to FORK_NAME+1).
      • Print Start "c:lsh -d c: ": to console.
      • Call init_forkto (lib6502/libexec.a65, line
        104). This is a lib6502 entry point, which sets the fork
        address in PCBUF to libfork (line
        291) before calling FORK. When the task runs
        and libfork is called, the program name is
        passed to doload, which opens the program file
        and calls loader (line 545).
      • loader (lib6502/loader.a65, line 58) takes care
        of reading the program into memory, aligning, relocating,
        &c., and returns the start address.
      • On return, libfork jumps to the program’s start
        address (at line 383).
      • On return from init_forkto,
        print ok! to console and return.
  • When all the autostarts have been forked, jump
    to restarts (sysapps/init/init.a65, line 598),
    which spins in a loop calling YIELD
    (kernel/tasks.a65, line 591, a.k.a. “suspend”) and then waiting
    for kpid (number of killed tasks,
    sysapps/init/init.a65, line 105) to become
    TODO: document this routine (is this
    where restartable programs are restarted?).

  • When startup is complete, running info on
    console 2 should show five processes. Their names aren’t shown,
    but they
    are init, fsdev, fsiec, shell,
    and lsh. (Note: in newer versions of GeckOS, the
    names are shown; you can also use the


  • Find out how ls a: returns a list of device names.
  • What happens with kernel/userspace in exejmp?

The Scheduler

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

  • MAXNTASKS=12 (max. number of tasks)
  • MAXNTHREADS=12 (max. number of threads)
  • STACKSIZE=64 (stack size per user thread)

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:

  • TT_STDIN 0 (Stdin)
  • TT_STDOUT 1 (Stdout)
  • TT_STDERR 2 (Stderr)
  • TT_PARENT 3 (parent process ID)
  • TT_SIGADR 4 (addr of signal routine)
  • TT_LIBSAVE 6 (word pointer to lib6502 data,
    allocated by taskinit at lib6502/libglob.a65, line
  • TT_ENV 8 (environment number)
  • TT_NTHREADS 9 (number of running threads)
  • TT_SIGMASK 10 (mask of allowed signals)
  • TT_SIGPEND 11 (mask of pending signals)
  • TT_PRIORITY 12 (task priority)
  • TT_RETCODE 13 (return code after kill)

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

  • TH_ST=0 (thread state)
  • TH_TASK=1 (parent task ID)
  • TH_SP=2 (thread’s stack pointer)
  • TH_LIBSAVE=3 (word pointer to lib6502 data, set
    at sysapps/init.a65, line 723)
  • TH_PAR=5 (3 bytes, parameters for kernel calls)

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

  • TS_FREE=0
  • TS_ENV=1 (allocated but not running)
  • TS_IBRK=2 (task has BRK’d in IRQ routine)
  • TS_BRK=3 (task has BRK’d)
  • TS_RDY=4 (task did YIELD call)
  • TS_IRQ=5 (task is interrupted)
  • TS_WFRX=6 (waiting for other task to RECEIVE)
  • TS_WFTX=7 (waiting for other task to SEND)
  • TS_WXTX=8 (wait for a specific task to SEND)
  • TS_WFSEM=9 (wait for semaphore)
  • TS_WSIG=10 (wait for signal)

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
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:

  • Switch to kernel space.
  • Get new task and thread IDs.
  • Fill in new entries in the task and thread tables with the data
    passed in PCBUF.
  • Push the fork address – 1 onto the new thread’s stack.
  • Return from kernel space.
  • A newly-forked process receives its own PID in the X
    register when it runs.
  • Note that FORK doesn’t actually start the new
    process; that’s left to the scheduler.

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

  • FORK_SIZE 0 (size of needed memory in 256-byte blocks from
    address 0 up)
  • FORK_SHARED 1 (size of shared memory mapping in blocks from
    address $ffff down)
  • FORK_PRIORITY 2 (priority of task – 0=parent’s)
  • FORK_STDIN 3 (stdin stream)
  • FORK_STDOUT 4 (stdout stream)
  • FORK_STDERR 5 (stderr stream)
  • FORK_ADDR 6 (start address of task execution)
  • FORK_NAME 8 (task name ended with nullbyte)

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:

  • If the interrupt was from a BRK instruction (software
    interrupt), jump to pbrk (kernel/tasks.a65, line
    1367). This routine just sets the thread’s state
    to TS_BRK, which the scheduler doesn’t appear to
    check for.
  • If not, enter kernel space by calling memsys.
  • At line 617, call each device’s IRQ routine with a JSR
    to irqdev (kernel/devices.a65, line 65). For each
    device, a DC_IRQ command is
    TODO: Document each device’s IRQ routine.
  • Clear the interrupt (LDA $DC0D).
  • If we’re already servicing an interrupt in kernel code
    (Syscnt=1), return from IRQ.
  • If we’re already servicing a device interrupt (adev
    high bit set), return from IRQ.
  • At line 648, jump to irqloop (kernel/tasks.a65,
    line 550) to run the scheduler. The current thread’s priority
    was stored in Irqcnt; start by decrementing that,
    and if it’s non-zero, return from IRQ (this acts as a simple way
    to keep high-priority threads running longer).
  • The current thread’s state is set to TS_IRQ,
    followed by a jump to nexttask (line 460), which is
    the heart of the scheduler. In the C64 version, it always
    branches to ok (line 492) to walk the thread table
    looking for the next eligible thread to run.
  • If the next thread’s task has a signal mask, check if any
    signals are pending. If so, JSR to checksig
    (line 967).

    • If the thread’s status is TS_RDY
      or TS_WSIG (waiting for signal), make the thread
      active, clear its signal pending mask
      (TT_SIGPEND), and run the signal routine whose
      address is stored in the thread table.
    • If the thread’s status is TS_IRQ (interrupted
      by scheduler), set it to TS_RDY and run it.
    • Check the task’s signal mask
      for SIG_INTABLE (interruptable flag). If
      it’s clear, do nothing and return.
    • If the thread’s status
      or TS_WFSEM, set it to TS_RDY and
      run it. The accumulator will be set
      to E_INT.
    • Failing any of these conditions, do nothing and
  • If the thread’s status is TS_RDY, restore its
    registers, store its priority in Irqcnt and run
    it. The three parameter bytes at TH_PAR are loaded
    to .X, .Y, and .A respectively. (Note that the registers are
    stored in this order when calling YIELD.)
  • If the thread’s status is TS_IRQ, store its
    priority in Irqcnt and run it.
  • Loop to the next thread. If no threads are runnable, a fatal
    error has occurred. Flash the screen border and jump
    to RESET to restart
    the operating system.

Running Programs from the lsh Shell

  • The code for the lsh shell is at
  • When lsh starts, it installs
    a SIG_CHLD signal handler which points
    to sigrchld (line 1115). This routine saves the
    registers in tables
    (ctabh, ctabl, ctabr) and
    increments the table pointer, cnum. At the top of
    the command loop (see below), checksig is called to
    print an error message for each entry and purge the
    table. TODO: What’s in the registers at that point and
    how does it get there? It looks like .X/.Y is the PID and .A is
    an error code.
  • The shell then quickly enters a command loop (at line 87). The
    command line is read and parsed by getcmd (line
    181), which calls tokenize (line
    868). tokenize replaces blanks between program name
    and arguments with nulls. Arguments in quotes have blanks
    preserved. This is the format expected by forkto
    (see below).
  • At line 118, there’s a call to execfork (line 933),
    which checks for builtin commands from a table at line 1001. If
    found, the builtin is run (line 977) from a table of addresses
    at line 1000.
  • For external commands (line 968), the tokenized input buffer is
    passed to the lib6502 routine forkto (libexec.a65,
    line 55). This sets up a call to FORK in the
    kernel, with the start address set to libfork
    (libexec line 291). When the process runs, libfork
    calls taskinit (libglob.a65, line 57) to set up
    the LT_* structure, then doload (line
    499), which sets up a file descriptor and
    calls loader (libloader.a65, line 88) to read the
    file from disk and do the relocation.
  • When the size of the new task’s zero-page segment is
    known, zalloc is called. This allocates zero-page
    in 4-byte blocks, each of which have an owner (in
    the zmem table). Allocation is done
    above Zerostart, which marks the end of the area
    used by the kernel (defined as $70).
  • doload returns with the start of the text
    segment (or the main method) in .A and .Y.
  • TODO: Right after this (still in libfork),
    what does the PUTC do?
  • In libfork, the address of term is
    pushed onto the stack (so the program can terminate with an RTS),
    the address of the program’s command-line is put to .A/.Y, and
    there’s a jump to the start address. This means that programs
    started from the shell will find their complete command line
    (including program name) in .A/.Y on startup.
  • Meanwhile, back in lsh, if the program was not
    backgrounded or piped, there’s a call to waitpid,
    which waits for the program’s PID to show up in
    the SIG_CHLD table (see signal handler discussion
    above). Once the program has terminated there’s a jump back to
    the top of the command loop.


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

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:

  • lib6502 was passing a stream index in the first byte of
    the FORK_NAME field
  • lib6502 passes the address of libfork (see above
    discussion about running programs from the lsh
    shell) in the FORK_ADDR field (it doesn’t know the exec address
    until the program has been loaded and relocated)

These problems were solved like this:

  • change the way lib6502 calls FORK so that the
    stream number is sent as a parameter
  • add a kernel SETINFO API that
    lib6502’s execfork could use to update the task
    table with the execute address and the process name after it
    calls FORK (a process can only change its own exec

standalone ps command with process name and exec

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 '

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

  • With the new implementation of process name and exec address
    complete (along with working ps and kill
    commands), it’s now possible to achieve the Grand Unification of the
    Shells (cut a build without the old shell starting at boot time, and
    the monitor available as a standalone app).
  • The error messages in lsh could be made clearer; some
    of them are a bit cryptic.
  • The ls command in lsh could be improved
    to show more detail (André has already added an ls -l
    option). A long-range project would be to show date stamps and
    subdirectories on devices that support it; you’d have to be able to
    tell what kind of device the drive is, and I don’t know if
    the fsiec driver stores that information.

possible bugs (needs more study)

  • In lsh, re-use of the argp variable to
    hold the .X/.Y registers after the call to execfork in
    line 118: some debugging indicated that the value of argp
    could change before .X/.Y were reloaded at lines 127-128 (presumably
    due to multi-tasking).
  • A program run from the shell sometimes ends with a message
    like 0046,00 : Received sigchld error, although it
    appears to have ended normally. See the discussion
    about how the shell runs programs for more
  • If you start a new lsh shell on console 3, then kill
    the first one from there, the first one fails to restart with the
    messages Received sigchld from 30 (the first shell’s PID)
    and Could not set device stream! It’s my understanding
    that the first shell should restart, as the kernel starts it with
    the PK_RESTART flag.

segment boundaries and load addresses — a warning

When doing anything that changes the size of memory structures
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.

Read More

Related posts

© Copyright 2022, All Rights Reserved