|
GE-115 Emulator
An Emulator of the General Electrics GE-115 computer
|
A calling convention and data model for compiling C to the GE-120 / GE-130, as implemented by the in-tree compiler software/gemu/cc (gcc.c) emitting gasm assembly.
This document is normative for our toolchain: it is a convention we define, not one recovered from a GE C compiler (none existed — the machine predates C). Every choice is justified against the verified ISA (docs/ISA.md). Where the hardware forces a decision it is marked [hw]; where we chose freely among valid options it is marked [abi].
| Decision | Value |
|---|---|
char / signed char / unsigned char | 1 byte |
short / int | 2 bytes, big-endian, two's complement |
long | 4 bytes |
long long | 8 bytes |
| pointer (all) | 2 bytes (16-bit address space) |
float / double | not supported (no FP hardware; reserved for a future soft-float) |
| Endianness | big-endian [hw] — AB/SB and address fields are big-endian |
| Alignment | none — memory is byte-addressable; every type is byte-aligned [hw] |
| Char/string constants | GE-100 internal graphic code (ISA §2.1), not ASCII |
| Link register | R7 [hw] — JRT writes the return address here |
| Stack pointer | R6 [abi] |
| Frame pointer | R5 [abi] |
| Globals/abs base | R0 [abi] — held at identity 0x0000 |
| Scratch address regs | R1–R4 [abi] |
| Stack growth | upward (toward higher addresses) [abi] |
| Return value | fixed cell __rv (16 bytes) [abi] |
Why
int= 2 and not 1.AB/SBadd and subtract big-endian binary fields of 1–16 bytes in a single instruction (ISA §6.6), so wide integers cost nothing extra for+ - & | ^and compares. A 2-byteintalso matches the 16-bit pointer/register width, so anintcan hold a pointer — essential for idiomatic C. Only multiply, divide, and shift need software help, and that is independent of width (the ISA has no binary×/÷and no shift instruction — ISA §9). A 1-byteintis available via-mint=1but is a curiosity: it cannot hold a pointer and caps loop counters at 256.
16-bit address space; bit 15 of an encoded address field is the absolute/modified flag (ISA §4.2). Absolute fields (bit 15 = 0) are used verbatim and span 0x0000–0x7FFF; modified fields (bit 15 = 1, written disp(N)) resolve to change_register[N] + disp. The C model places its code+globals at absolute addresses (now above 0x1000) and reaches the frame/stack via disp(5)/disp(6) against the reprogrammed base registers — the C model lives in the low 32 KiB.
The split between .bss/heap and the stack base (0x6000) is a linker constant (crt0), adjustable per program. Programs that the integrated card reader bootstraps load .text at 0x0100 (the DUMP1 convention, ISA §4.1); unit tests that use ge_load_program load at 0x0000.
The machine has no general arithmetic registers; the eight change registers (memory-mapped 16-bit words at 0xF0+2N, ISA §4.3) are address/index registers. The ABI assigns them fixed roles:
| Reg | Addr | Role | Preserved across call? |
|---|---|---|---|
| R0 | 0xF0 | Globals / absolute base, held at 0x0000 (identity). Modifier 0 ⇒ absolute addressing. | yes (never changed) |
| R1 | 0xF2 | Scratch address register A (lhs pointer / dereference) | no (caller-saved) |
| R2 | 0xF4 | Scratch address register B (rhs pointer) | no (caller-saved) |
| R3 | 0xF6 | Scratch / array-index base | no (caller-saved) |
| R4 | 0xF8 | Scratch | no (caller-saved) |
| R5 | 0xFA | Frame pointer (FP) | yes (callee-saved) |
| R6 | 0xFC | Stack pointer (SP) | yes (callee-saved) |
| R7 | 0xFE | Link register (LR) — written by JRT [hw] | yes (callee-saved if non-leaf) |
Because computation is memory-to-memory, R1–R4 are used only to form effective addresses (pointer deref, array indexing) via the disp(N) address form (EA = disp + R_N, ISA §4.2). All actual data lives in memory.
Verified from CPU/GE 120 CENTRAL PROCESSOR [4].pdf §5.5.6.2 / §5.6.5.1 and the APS manual (EDV-AFL 03) p.123–134, and implemented + tested in gemu:
JRT 0xF0, callee — opcode 0x41, mask 0xF0 (unconditional). Deposits the address of the next instruction into R7 and jumps. One instruction. (SUB/*N…N in APS assemble to exactly this.)JU 0x000(7) — JU with modifier 7, displacement 0; effective address = 0 + R7 = the saved return address. Encodes as 47 F0 70 00.JRT clobbers R7, so a non-leaf function must spill R7 into its frame before making any call and restore it before returning. A leaf function may leave R7 alone (MIPS/ARM-style).The stack grows upward. On entry, R6 (SP) points at the base of the new frame — which is also where the caller has already written the outgoing arguments. Within a frame, everything is addressed as disp(5) (FP-relative), and disp is unsigned 0–0xFFF (ISA §4.2), so every slot is at a non-negative offset from FP — the reason the stack grows up and args sit at the bottom.
Frame layout (offsets from FP = R5), with A = total bytes of incoming args:
Prologue (non-leaf shown; a leaf omits the LR save):
Epilogue:
All of STR/LR/LA/JU/JRT are ✅-wired in gemu. (AMR/SMR exist for register arithmetic but the prologue/epilogue only need add-via-LA, since the stack grows up.)
[SP+0], [SP+k0], [SP+k1], … where each k is the running sum of prior argument sizes. After the JRT, that area is the callee's FP+0… incoming-argument block.__rv** (0x0010), big-endian, right-justified for integers. The caller copies __rv to its destination before any subsequent call (the cell is not reentrant, but a return value is always consumed before the next call — the same discipline GE system subroutines use with their "fixed store areas," APS p.134).void functions write nothing.JRT 0xF0,f; after return, copy __rv if the value is used. Must assume R1–R4 are destroyed; spill any live scratch first (the compiler keeps live values in memory anyway, so this is usually a no-op).disp(5); write the result to __rv; run the epilogue; JU 0x000(7).AB/SB process right-to-left from the least-significant (rightmost) byte (ISA §4.5), so the operand address passed to AB/SB is field_addr + size - 1.0x0000–0x7FFF).a[i] ⇒ EA = &a + i*sizeof(elem), computed into a scratch register (R1–R4) with LA/AMR.char / strings** — encoded in the GE-100 internal graphic set (ISA §2.1), e.g. ‘'0’=0x40,'A'=0x51, space=0x50— **not ASCII**. The compiler translates C character/string literals through this table so that console/printer output matches the machine."\0"terminator stays0x00`.The ISA has no binary multiply/divide and no shift (ISA §9). The compiler emits calls to assembly helpers in crt0/libgc. Helper convention (fast path, not the general stack ABI): operands in fixed cells **__a**, **__b** (2 bytes each at 0x0020/0x0022); result in **__rv**.
| Helper | C operator | Algorithm (wired instructions only) |
|---|---|---|
__mul | * | 16-iteration shift-add: walk bits of __b with TM masks; for each set bit add the running double of __a (AB acc,acc) into the result. No shift-right needed. |
__divu / __modu | / % (unsigned) | restoring division via SB/CMC/JC bit-walk; quotient→__rv, remainder kept for __mod. |
__div / __mod | signed | sign-adjust operands, call unsigned core, fix sign. |
__shl | << | n self-adds (AB x,x) — left shift = repeated doubling. |
__shru | >> (unsigned) | __divu by 2^n. |
__shr | >> (signed) | arithmetic: sign-extend then __divu, adjust. |
crt0 responsibilities: set R0 = 0x0000, R6 = 0x6000 (stack base), R5 = R6, then JRT 0xF0, main; on return, leave __rv in place and HLT (so a test harness or the console can read main's exit value from __rv).
main (non-leaf, 0 args, frame = saved-FP + saved-LR + outgoing-arg area):
(The exact AB field addressing and the arg-write sequence are what gcc.c emits; this sketch shows the frame mechanics.) After crt0 HLTs, __rv holds 42.
goto into blocks, no VLAs, no alloca.long long/64-bit and full long arithmetic rely on AB/SB width (≤16 bytes, free) but mul/div on widths >2 bytes are not yet in the helper set.0x6000–0x7FFF stack window (8 KiB).crt0gasm.disp(N), bit 15 = 1: local/stack access via disp(5)/disp(6)) resolve to change_register[N] + disp through the operand-fetch indexing micro-cycle. Because absolute addresses bypass the change registers, code+globals are no longer confined to low memory and never alias the reprogrammed base registers (R5/R6 = 0x6000); gec now places them above 0x1000.Status: the compiler (
cc/gec.c) implements this ABI and is verified end-to-end ongemu(cc/test.sh, run bymake check): arithmetic,* / %, comparisons,&& || !,if/while/for, recursion (R7 spill), pointers (& *, array decay) and arrays. The nativeJRTcall/return and the SS operand-fetch fix (V1 keeps its full field) make it work.
These are implementation limits of cc, not of the ABI — the convention above is complete enough to target more of C as the compiler grows.