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. The latest release is 2.0.9, but
there’s active development
on GitHub. I gave a
talk about it
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;
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
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):
- download xa
and extract to a convenient location - change to the directory and run
make
,make
, and (as root)
testmake install
- download the GeckOS source,
either version
2.0.9 or
the master
branch, and extract to a convenient location - change to the directory and run
make clean
, then
descend into arch/c64/boot and runmake
osa.d64 - load and run
LOADER
in the resulting disk
image
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
:
LOADER
(a BASIC program) loadsC64ROM
from $1800-$9FFFLOADER
loads a small ML program
calledBOOT
(arch/c64/boot/boot.a65) from $0C00 to
$0D05.LOADER
passes control toBOOT
, which
initializes the hardware and then relocatesC64ROM
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
(includesCONSOLE_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
(includesFSIEC_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
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:
- 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
1MHz.
TODO: Figure out exactly whatadev
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)
setsanzfs
(number of file systems) to 0 and
clearsfstab
(the file server
table).MAXFS
(maximum number of fileserver tasks)
is set to 4 in arch/c64/config.i65. See
the filesystem
interface 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
ofPK_DEV
and
aP_ADDR
pointing todevstart
(line
64). The kernel init (kernel/init.a65, line 189) passes this
address to theDEVCMD
kernel API in .X and .Y, with
a device command ofDC_REGDEV
(register devices) in
.A. The address points to a structure described in the
kernel API docs
(seeDEVCMD
): it’s a linked list with each item
containing a jump to the device’s message handler and the device
name. Note thatPK_DEV
programs do not get
entries in the task table. -
When
DEVCMD
registers devices
atregdev
(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
,
andDVT_ENV
. -
After incrementing
ANZDEV
,DEVCMD
calls itself recursively, this time with a device command
ofDC_RES
(initialize/restart device). Control gets
passed toexejmp
(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:
video1
–video4
These are
the four consoles. Variables for each are stored in a table
calledvtab
(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
byconsole_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
interrupts.nuldev
Null device. A 16-byte character
buffer atinstr
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 asser1
, the next asser2
,
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 toNMIserial
(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
):
STDERR
,STDOUT
, andSTDIN
are set toSTDNUL
($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 intaskTab
andthreadTab
. 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 ofTS_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
console. - 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
typesPK_PRG
,PK_FS
,
andPK_LIB
. For each program with the PK_AUTOEXEC
flag set, calldostart
(line 385):- Call YIELD (this does nothing the first time it’s called, as
nothing’s been forked yet). - If
PK_LIB
, jump toexeclib
(seelsh
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
forPK_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
typeFM_REG
(register filesystem) in .A and target
taskSEND_FM
in .X.PCBUF
contains
the number of drives to register (2)
andfsdev
‘s task ID.SEND
callsfm
(kernel/files.a65, line 48) to store
the number of drives in thefstab
table and the
task ID in thefstask
table (but see the note
below for thefsiec
driver). This driver
responds for drives a: (device list) and b: (system programs
list). - Release the
PCBUF
semaphore (call VSEM). SEND
returned E_OK with carry clear, so
continue atloopt
(line 128).- Since the number of open files (
anz
) is 0,
callRECEIVE
with carry set (i.e. wait for a
message). RECEIVE
looks for a thread with
statusTS_WFRX
(waiting for receive); not
finding one, it setsfsdev
‘s status to TS_WFTX
(waiting for transmission) and exits by jumping
tonexttask
.fsdev
is now waiting
for messages to be sent to it; when the scheduler runs it
again, it will resume after the call
toRECEIVE
.- The next task will be
init
, which returns to
the line after theYIELD
indostart
(line 388) to continue processing
fsiec
.
- Set buffer states to
-
sysapps/fs/fsiec.a65 (
PK_FS
), which includes
devices/siec_64.a65- Call
IECINIT
(in siec_64) to initialize CIA2
for serial I/O. - Set buffer states to
F_FL_FRE
. - Call
SEND
, passing message
typeFM_REG
and target
taskSEND_FM
.PCBUF
contains the
number of drives to register (4) andfsiec
‘s
task ID.SEND
callsfm
(kernel/files.a65, line 48) to store the number of drives in
thefstab
table and the task ID in
thefstask
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
thefm
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
VC1541”.) - Enter a loop (line 321), starting with
YIELD
,
then callingRECEIVE
. If there is no
message,fsiec
‘s state is set
toTS_WFTX
(waiting for
transmission).fsiec
is now waiting for
messages to be sent to it. If a message is received, it’s
processed atrxmess
(line 764).
See the
GeckOS documentation for
more information on the filesystem interface. - Call
-
sysapps/mon/mon.a65 (embedded
“old-style” shell and ML
monitor, bothPK_PRG
):shell
‘s
program type has the autostart bit set,
whilemon
‘s does not. Note that this
shell’smonitor
builtin hasSHORTMON
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 loadmon
from thelsh
shell.- Release the
PCBUF
semaphore (call VSEM). - Set the signal address for
SIG_CHLD
. - Set terminal to full-screen mode (putc TC_ECHO to
STDOUT). - 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.
- Release the
-
lsh shell
(PK_LIB
, program header is directly in
c64rom.a65):PK_LIB
programs are loaded from disk,
and take the branch toexeclib
(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
(callsinit_forkto_i
at lib6502/libexec.a65,
line 104). The differences between a
regularFORK
call andinit_forkto
are as follows:FORK_SIZE
is set to MemendFORK_SHARED
is set
to#>-ROMSTART
FORK_PRIORITY
is set to 0FORK_ADDR
is set to the address
oflibfork
(lib6502/libexec.a65, line
291)- The first byte of
FORK_NAME
is set to the stream number for
the loader (init_forkto
later copies the
name toFORK_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 inPCBUF
tolibfork
(line
291) before callingFORK
. When the task runs
andlibfork
is called, the program name is
passed todoload
, which opens the program file
and callsloader
(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
,
printok!
to console and return.
- Call YIELD (this does nothing the first time it’s called, as
-
When all the autostarts have been forked, jump
torestarts
(sysapps/init/init.a65, line 598),
which spins in a loop callingYIELD
(kernel/tasks.a65, line 591, a.k.a. “suspend”) and then waiting
forkpid
(number of killed tasks,
sysapps/init/init.a65, line 105) to become
non-zero…
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
areinit
,fsdev
,fsiec
,shell
,
andlsh
. (Note: in newer versions of GeckOS, the
names are shown; you can also use theps
command.)
TODO:
- 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 bytaskinit
at lib6502/libglob.a65, line
57)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)
are:
TS_FREE
=0TS_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
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:
- Switch to kernel space.
- Get new task and thread IDs.
- Fill in new entries in the task and thread tables with the data
passed inPCBUF
. - 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 topbrk
(kernel/tasks.a65, line
1367). This routine just sets the thread’s state
toTS_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
toirqdev
(kernel/devices.a65, line 65). For each
device, aDC_IRQ
command is
sent.
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 inIrqcnt
; 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 tonexttask
(line 460), which is
the heart of the scheduler. In the C64 version, it always
branches took
(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 tochecksig
(line 967).- If the thread’s status is
TS_RDY
orTS_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 toTS_RDY
and run it. - Check the task’s signal mask
forSIG_INTABLE
(interruptable flag). If
it’s clear, do nothing and return. - If the thread’s status
isTS_WFRX
,TS_WXTX
,TS_WFTX
,
orTS_WFSEM
, set it toTS_RDY
and
run it. The accumulator will be set
toE_INT
. - Failing any of these conditions, do nothing and
return.
- If the thread’s status is
- If the thread’s status is
TS_RDY
, restore its
registers, store its priority inIrqcnt
and run
it. The three parameter bytes atTH_PAR
are loaded
to .X, .Y, and .A respectively. (Note that the registers are
stored in this order when callingYIELD
.) - If the thread’s status is
TS_IRQ
, store its
priority inIrqcnt
and run it. - Loop to the next thread. If no threads are runnable, a fatal
error has occurred. Flash the screen border and jump
toRESET
to restart
the operating system.
Running Programs from the lsh
Shell
- The code for the
lsh
shell is at
apps/lsh/lsh.a65. - When
lsh
starts, it installs
aSIG_CHLD
signal handler which points
tosigrchld
(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 bygetcmd
(line
181), which callstokenize
(line
868).tokenize
replaces blanks between program name
and arguments with nulls. Arguments in quotes have blanks
preserved. This is the format expected byforkto
(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 routineforkto
(libexec.a65,
line 55). This sets up a call toFORK
in the
kernel, with the start address set tolibfork
(libexec line 291). When the process runs,libfork
callstaskinit
(libglob.a65, line 57) to set up
theLT_*
structure, thendoload
(line
499), which sets up a file descriptor and
callsloader
(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
thezmem
table). Allocation is done
aboveZerostart
, which marks the end of the area
used by the kernel (defined as $70). doload
returns with the start of the text
segment (or themain
method) in .A and .Y.- TODO: Right after this (still in
libfork
),
what does the PUTC do? - In
libfork
, the address ofterm
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 towaitpid
,
which waits for the program’s PID to show up in
theSIG_CHLD
table (see signal handler discussion
above). Once the program has terminated there’s a jump back to
the top of the command loop.
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.
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
theFORK_NAME
field - lib6502 passes the address of
libfork
(see above
discussion about running programs from thelsh
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’sexecfork
could use to update the task
table with the execute address and the process name after it
callsFORK
(a process can only change its own exec
address)
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
- With the new implementation of process name and exec address
complete (along with workingps
andkill
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 inlsh
could be improved
to show more detail (André has already added anls -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
thefsiec
driver stores that information.
possible bugs (needs more study)
- In
lsh
, re-use of theargp
variable to
hold the .X/.Y registers after the call toexecfork
in
line 118: some debugging indicated that the value ofargp
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
like0046,00 : Received sigchld error
, although it
appears to have ended normally. See the discussion
about how the shell runs programs for more
information. - 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
messagesReceived sigchld from 30
(the first shell’s PID)
andCould not set device stream!
It’s my understanding
that the first shell should restart, as the kernel starts it with
thePK_RESTART
flag.
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.
Related posts
Tips & Trick For Healthy Glowing Skin
Send to HN Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam laoreet, nunc et accumsan cursus, neque eros sodales…
My Fight With Depression. Concussions
Send to HN Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam laoreet, nunc et accumsan cursus, neque eros sodales…
How I Traveled The World With Only $100
Send to HN Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam laoreet, nunc et accumsan cursus, neque eros sodales…
What’re People Buzzing About? Your Content Should Join The Conversation
Send to HN Sed faucibus ultrices orci ac malesuada. Cras eu ante dapibus, imperdiet lacus ac, pulvinar nulla. Interdum et…
Does Coffee Help Deduce Stress Hormone Levels?
Send to HN Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam laoreet, nunc et accumsan cursus, neque eros sodales…
Review Of Healthy Breakfast Meals For Energy Boost
Send to HN Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque hendrerit fringilla enim, ut scelerisque dui. In hac…
How Much Time On Social Networks Is Considered Healthy
Send to HN Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam laoreet, nunc et accumsan cursus, neque eros sodales…
My Fight With Depression. Concussions
Send to HN Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam laoreet, nunc et accumsan cursus, neque eros sodales…