BCPL Runtime Binder¶
AmigaDOS was originally written in BCPL, and BCPL programs reach shared functions and data through a Global Vector (GV) rather than through linked symbols. dos.library still contains a small runtime binder that builds a program's Global Vector from tables inside its loaded segments. Since V47 no BCPL-compiled code is left in the system, but the mechanism is still in use: every handler mounted with GLOBVEC 0 or -3 (among them the ROM Port-Handler, whose mount entry is created in Kickstart, and the FFS when it serves OFS floppies) and the CLI resident segment are started through it.
You only need this if you write a handler that must work with BCPL startup, a dual-mode handler like the FFS, or a replacement for the CLI. New code should use C startup (GLOBVEC -1 or -2).
Who uses BCPL startup¶
The mountlist keyword GLOBVEC (stored in dol_GlobVec) selects the handler type:
GLOBVEC |
Handler type | Device-list access during startup |
|---|---|---|
-1 |
C/assembler | Exclusive |
-2 |
C/assembler | Shared |
0 |
BCPL, system GV extended by the binder | Exclusive |
-3 |
BCPL, system GV extended by the binder | Shared |
> 0 |
BCPL, BPTR to a custom GV | Exclusive |
Values > 0 are only used for device-list entries created inside Kickstart. For BCPL handlers the mountlist STACKSIZE (dol_StackSize) is in long words; for C handlers it is in bytes.
| Start mode | Entry point | Startup packet |
|---|---|---|
C (-1, -2) |
First byte of the first segment | Arrives as a message on pr_MsgPort |
BCPL (0, -3) |
GV entry 1 (G_START) |
BPTR to the DosPacket in register d1 |
The CLI resident is started the BCPL way; the Shell and BootShell residents, built from the same source, use C startup.
Process fields that belong to BCPL¶
| Field | Purpose |
|---|---|
pr_SegList |
BPTR to the segment array the binder scans |
pr_GlobVec |
The process's Global Vector: a private one for BCPL programs, the shared system vector for C/assembler programs |
pr_ReturnAddr |
Pointer to the BCPL stack frame used by Exit() |
The segment array (pr_SegList)¶
| Entry | Contents |
|---|---|
| 0 | Count of entries the binder scans (a plain integer) |
| 1 | klib, the BCPL "kernel library" system segment |
| 2 | blib, the BCPL support library system segment |
| 3 | The program's own seglist |
| 4 | The shell's seglist, if the process is a shell (the shell startup moves itself from entry 3 to 4, freeing 3 for commands) |
Entries may be ZERO when unused. Entries 1 and 2 are always counted; entry 3 only for BCPL programs, so the count is 3 for a BCPL handler and 2 for C/assembler code (whose segments carry no binder data and must not be scanned). The functions in klib and blib are trivial or available as dos.library calls. This describes V47; later versions may change or drop the array.
pr_ReturnAddr and the BCPL stack frame¶
pr_ReturnAddr points to the BCPL stack frame of the process, or of the command running on it. Exit() removes that frame, restores the previous stack and returns to whoever created the frame — the process shutdown code of CreateNewProc()/CreateProc(), or the command cleanup installed by RunCommand(). The frame layout is undocumented:
struct BCPLStackFrame {
ULONG bpsf_StackSize; /* size of the current stack in bytes */
APTR bpsf_PreviousStack; /* caller's stack, loaded back into a7 */
};
Leave pr_ReturnAddr alone; C programs should return from main() or call the C library exit().
pr_GlobVec¶
For C and assembler processes this is the shared system Global Vector, holding dos.library BCPL functions and private data such as library base pointers. Its entries are not documented and there is nothing to gain from using them. The oh_GV field of an overlay header gets the same public vector.
How the binder reads a segment¶
For each segment it scans, the binder expects:
- First long word of the segment (which would be the entry point for C startup) = offset in long words from the start of the segment to the end of the initialization data.
- The data are read backwards, starting with the long word just below that address:
- first the size of the Global Vector the program needs. It is only used to validate the entries; the public GV has 150 entries, which is a safe value;
- then pairs of long words, still read towards lower addresses: the higher one is the byte offset of a function from the start of the segment, the lower one the GV index to fill;
- an offset of 0 ends the list.
For GLOBVEC 0/-3 the binder first fills the vector with the system functions, then applies these entries to extend or override them. An index beyond the vector bounds makes initialization fail with the undocumented error ERROR_GLOBALS (111), which is not defined in the NDK headers.
Memory layout for the stub below (each line is at a higher address than the one above; the binder reads from CodeEnd upwards in this picture):
CodeHeader: DC.L (CodeEnd-CodeHeader)/4 <- offset in long words
... code ...
BCPLTable: DC.L 0 <- terminator (offset 0)
DC.L G_START <- GV index (lower)
DC.L BCPLStart-CodeHeader <- byte offset (higher)
DC.L G_GLOBMAX <- GV size, read first
CodeEnd:
The dual-mode startup stub¶
This assembler stub, placed as the first code of an otherwise C-based handler (or shell), works with both C and BCPL startup:
SECTION text,code
XREF _main ; handler or shell main
G_GLOBMAX EQU 150 ; size of GV
G_START EQU 1 ; BCPL START function
CodeHeader: DC.L (CodeEnd-CodeHeader)/4
* C startup, used for GLOBVEC -1 or -2
CStart: sub.l a0,a0 ; no startup message
bsr.w _main ; main() must GetMsg() it
rts
CNOP 0,4
* BCPL startup, used for GLOBVEC 0 or -3 (and the CLI)
BCPLStart: movem.l a0-a6,-(a7) ; save for BCPL
lsl.l #2,d1 ; BPTR -> startup packet
move.l d1,a0 ; pass it in a0
bsr.w _main
movem.l (a7)+,a0-a6 ; restore everything
jmp (a6) ; BCPL-style return
CNOP 0,4
BCPLTable: DC.L 0 ; end of global list
DC.L G_START,BCPLStart-CodeHeader
DC.L G_GLOBMAX ; max global used
CodeEnd:
END
How it works:
- BCPL startup (
GLOBVEC0/-3, and theCLI): the binder sets GV entry 1 toBCPLStartand calls it. BCPL passes the startup packet as a BPTR ind1; the stub converts it into a pointer ina0. BCPL also has its own calling convention:a6holds the address of the BCPL return code that unwinds the stack frame, anda0,a2,a4,a5anda6must be preserved. The stub therefore savesa0–a6and leaves withjmp (a6). - C startup (
GLOBVEC-1/-2): the process starts at the first byte of the segment, which is the offset long word itself. No branch is needed: as long as the offset is below 64K it decodes as a harmlessORIinstruction, and execution falls through intoCStart, which callsmain()witha0 = NULL.NULLtellsmain()to wait for the startup packet on itspr_MsgPort.
CStart is never used under pure BCPL startup; it is what makes the handler dual-mode. The FFS uses this technique to run as a BCPL handler for OFS floppies and as a C handler on hard-disk partitions. The same stub also works as a universal "BCPL kludge" for a CLI replacement, since the CLI is started through G_START too.
main() must handle both cases:
/* SAS/C register syntax. Called from the stub:
a0 = startup packet (BCPL start) or NULL (C start) */
LONG __asm main(register __a0 struct DosPacket *startup)
{
struct Process *me = (struct Process *)FindTask(NULL);
if (startup == NULL) {
struct Message *msg;
WaitPort(&me->pr_MsgPort);
msg = GetMsg(&me->pr_MsgPort);
startup = (struct DosPacket *)msg->mn_Node.ln_Name;
}
/* ... interpret startup->dp_Arg1..dp_Arg3, reply it, run the loop ... */
return 0;
}
The mn_Node.ln_Name field of a packet message points to its DosPacket; see DOS Packets.
See Also¶
- BPTR, BSTR, and BCPL Legacy
- Hunk Version and Stack Cookies
- AmigaDOS Hunk Binary Format
- Process Creation and Structure
- LoadSeg and Segment Lists
- DOS Device List
- IFF Format and Mountlist
Sources: Thomas Richter, Amiga ROM Kernel Reference Manual: AmigaDOS (2024, v1.27), ch. 10 (pp. 141–143), §8.1 tab. 8.2 (pp. 97–98), §10.2.9 (p. 155), §11 intro (p. 163), §11.5.4 (pp. 192–194); NDK 3.2 R4 dos/dosextens.h, dos/dos.h Updated: 2026-11-13