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:
- Initialize the C runtime data (zero BSS, copy initialized data).
- Open the libraries declared with
#pragma libcallor that appear in the program. The standard set:exec.library(always),dos.library(forc.oto parse args and callmain). - Parse the Shell command line into
argc/argvfor themain(int, char **)form. - Run C++ static constructors (for C++ code).
- Call
main(argc, argv). - On
main()return, call C++ static destructors. - Close opened libraries.
- 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
mainis declared asint main(int argc, char **argv), the startup setsargc = 0andargvto a dummy structure. - If
mainis declared asint 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¶
- SAS/C Compiler Reference
- SAS/C Compilation and Linking
- SAS/C Extensions to C
- SAS/C Assembly Language
- DOS Objects Lifecycle — for
AllocDosObject()patterns - Hunk Version and Stack Cookies
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