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:

  1. 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.
  2. The data are read backwards, starting with the long word just below that address:
  3. 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;
  4. 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;
  5. 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 (GLOBVEC 0/-3, and the CLI): the binder sets GV entry 1 to BCPLStart and calls it. BCPL passes the startup packet as a BPTR in d1; the stub converts it into a pointer in a0. BCPL also has its own calling convention: a6 holds the address of the BCPL return code that unwinds the stack frame, and a0, a2, a4, a5 and a6 must be preserved. The stub therefore saves a0–a6 and leaves with jmp (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 harmless ORI instruction, and execution falls through into CStart, which calls main() with a0 = NULL. NULL tells main() to wait for the startup packet on its pr_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


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