SAS/C Startup Modules and Auto-Initialization

When you compile a program with sc link, the linker pulls in a startup module that runs before your main(). The startup module opens the libraries your program needs, sets up argc/argv, calls constructors for C++ static objects, and only then transfers control to main(). Source: Vol 1 Ch.10, pp. 153–168.

What the startup does

The standard startup (provided by c.o) does, in order:

  1. Initialize the C runtime data (zero BSS, copy initialized data).
  2. Open the libraries declared with #pragma libcall or that appear in the program. The standard set: exec.library (always), dos.library (for c.o to parse args and call main).
  3. Parse the Shell command line into argc/argv for the main(int, char **) form.
  4. Run C++ static constructors (for C++ code).
  5. Call main(argc, argv).
  6. On main() return, call C++ static destructors.
  7. Close opened libraries.
  8. Return to the OS.

The startup is c.o for normal C programs, cplus.o for C++, cppstart.o for C++-with-exceptions. The linker pulls the right one based on your language.

Argument parsing

The startup reads the command line from the process's input stream (pr_CIS). For Shell-launched programs, that's the Shell's command line:

myprog arg1 arg2 "arg 3"

The startup tokenizes this into: - argc = 4 - argv = { "myprog", "arg1", "arg2", "arg 3" }

The tokenization matches ReadArgs() rules (quotes, escapes), not Shell substitution. ReadArgs() is recommended for serious argument parsing — see AmigaDOS Shell.

Workbench arguments

For Workbench-launched programs, the startup is different. The process's pr_CIS is NIL:, and argc == 0. The startup does not call main(argc, argv) with (0, NULL). Instead:

  • If main is declared as int main(int argc, char **argv), the startup sets argc = 0 and argv to a dummy structure.
  • If main is declared as int main(void), the Workbench path is taken.

To detect Workbench launch, check argc == 0 or check if the process is associated with a Workbench window via IntuitionBase->FirstScreen etc.

Library auto-opening

sc automatically generates code to open any library you use. The mechanism:

/* When you call: */
LONG result = Open("foo", MODE_OLDFILE);

/* sc emits a call via the NDK proto header that looks like: */
LONG result = Open((STRPTR)"foo", MODE_OLDFILE);

The Open function comes from proto/dos.h, which declares it as a library call. The linker resolves the call by including the right base register setup and offset:

/* The actual call emits: */
move.l 4.W,A6         ; DOSBase
jsr    -$1E(A6)       ; Open at offset -30

The c.o startup opens dos.library so that DOSBase is valid when the call is made.

Custom startup modules

You can write your own startup instead of using c.o. Useful for:

  • Programs that don't need a Shell-style main() (e.g., device drivers, libraries).
  • Programs with very small code/data footprint that can't afford the c.o overhead.
  • Programs that need to control library opening explicitly.

Minimal startup

/* entry.c — replaces c.o */
#include <exec/exec.h>
#include <dos/dos.h>

extern struct ExecBase *SysBase;

void __startup my_entry(void)
{
    /* A6 = ExecBase guaranteed by the OS */
    SysBase = *(struct ExecBase **)4;   /* conventional location */

    /* Do whatever you need */
    /* ... */

    /* Exit cleanly */
    _exit(0);
}

Link with entry.o instead of c.o:

slink from entry.o prog.o lib:sc.lib lib:amiga.lib to prog

For a startup that doesn't pull in any of the c runtime, you can even drop lib:sc.lib.

Auto-initialization (#pragma libcall and friends)

sc provides pragmas to declare how to call library functions, replacing the older Aztec-style manual jump-table offsets. The most common is #pragma libcall:

#include <proto/dos.h>

#pragma libcall DosBase Open 1E 801
LONG Open(STRPTR name, LONG accessMode);

The pragma declares: function Open is at offset 0x1E (negative 30 = -30 = $FFFFFFE2) from DosBase, takes arguments per the standard AmigaOS register convention, returns a LONG in D0.

The 801 at the end is the tag: - High nibble 8 = returns a value (D0) - Low nibble 1 = takes 1 register arg in D0? Actually no — let me check.

Actually the standard tags for SAS/C are: - 0xA (or 10) = 1 register arg (D0) - 0xB (or 11) = 2 register args (D0, D1) - High bit (0x80000000) = preserve D0/D1 (rare) - Plus library base setup

The exact format is documented in the SAS/C manual. For typical use, just use the NDK proto/*.h headers — they include the correct pragmas.

Other pragmas

Pragma Effect
#pragma libcall Standard register-based library call
#pragma flibcall Library call with FFP register conventions (for FFP math)
#pragma amicall Aztec C-compatible library call (for porting Aztec source)
#pragma regcall Register-based internal call (Aztec compat)
#pragma syscall System call (reserved for OS internal use)
#pragma tagcall Tag-based call (e.g., for OpenScreenTagList)
#pragma msg Control diagnostic message severity

The NDK proto/ headers already set up #pragma libcall for every documented function. You don't need to write them by hand unless porting from another compiler or working without NDK headers.

Autoinit constructors and destructors

sc supports C++ static constructors/destructors. For C, you can register a function to run at startup with __autoinit:

void __autoinit my_setup(void)
{
    /* runs before main() */
    /* typical use: register hooks, initialize tables */
}

void __autoexit my_teardown(void)
{
    /* runs after main() returns */
}

__autoinit is a SAS/C extension. It's similar to __attribute__((constructor)) in GCC.

The functions are called in declaration order for __autoinit and reverse order for __autoexit (mimicking C++ RAII for the C subset).

The startup module list

sc chooses the startup module based on the source code's main signature:

Signature Startup Notes
int main(int argc, char **argv) c.o Standard C program
int main(void) c.o argc/argv omitted
void main(void) c.o K&R style; equivalent to int main(void) with implicit return 0
int main(int argc, char **argv, char **envp) cplus.o Like C++, has envp
int _main(void) c.o Use to avoid name conflict
__saveds void entry(void) entry.o (you provide) Custom entry point, no startup
int main(...) with C++ cplus.o C++ static init/destruct

The linker picks the right startup automatically based on which object defines the main/_main symbol.

Debugging the startup

cdbg can step through the startup before reaching main():

(cdbg) break _main
(cdbg) continue

Or, to start stepping from the very first instruction:

(cdbg) break *0x100        # PC of the first HUNK_CODE in the executable
(cdbg) continue

The startup code is part of c.o, not your source, so symbol-level breakpoints require debug=line on c.o (the SAS/C debug build of c.o is shipped with the compiler in sc:debug/).

See Also


Sources: SAS Institute Inc., "SAS/C Development System User's Guide, Volume 1" Ch.10, Version 6.50 (1993), pp. 153–168. Raw: raw/sasc/sasc-startup-modules-extract.md Updated: 2026-10-02