= Self-Contained Program Entry: Replacing System CRT Startup Objects
:toc: macro
:toclevels: 3

toc::[]

== Summary

The native backend's internal linker currently links every Blaise program
against three categories of system CRT startup object:

* `Scrt1.o` — provides the `_start` entry symbol, which calls
  `__libc_start_main(main, argc, argv, ...)`.
* `crtbeginS.o` / `crtendS.o` — gcc-provided, supply the `.init_array` /
  `.fini_array` frame and registration glue.
* `crti.o` / `crtn.o` — supply the `_init` / `_fini` C-runtime function
  prologue/epilogue.

These objects are located by `uToolchain.FindCrtObjects`, which scans a
hard-coded list of multiarch and *versioned gcc* directories
(`/usr/lib/gcc/<triple>/<ver>/`).  That directory layout varies by
distribution, so the scan fails on distributions whose triple or gcc version
is not in the list — issue #142 reports the failure on Mageia 9
(`internal linker: CRT startup objects not found`).  The QBE backend is
unaffected because it links through the system `cc`, which knows its own
startup-object paths.

This design removes the dependency on `Scrt1.o`, `crtbeginS.o`, and
`crtendS.o` entirely by shipping Blaise's *own* `_start` in the runtime
archive — the same approach FPC takes (`rtl/linux/x86_64/si_c.inc`).  After
the change the internal linker needs no gcc-provided object and no versioned
gcc directory, so it links correctly on any glibc-based distribution without
consulting gcc.

== Motivation

The internal linker exists to make Blaise *toolchain-independent* — to produce
a self-contained executable without shelling out to `gcc` / `ld`
(`docs/toolchain-independence.adoc`).  Resolving the #142 failure by asking
`gcc -print-file-name=<obj>` would work on every distribution, but it
reintroduces a link-time gcc dependency and so defeats the internal linker's
purpose.  The correct fix removes the need for the gcc-specific objects rather
than locating them more cleverly.

== Background: what each CRT object contributes

On modern glibc (>= 2.34) the program entry sequence is:

. The kernel transfers control to `_start` (the ELF entry point) with the
  initial stack holding `argc`, `argv`, `envp`, and the auxiliary vector.
. `_start` (from `Scrt1.o`) marshals these into the System V argument
  registers and calls
  `__libc_start_main(main, argc, argv, init, fini, rtld_fini, stack_end)`.
+
On glibc >= 2.34 the legacy `__libc_csu_init` / `__libc_csu_fini` were
removed; modern `Scrt1.o` passes `init = NULL` and `fini = NULL` (`%rcx` and
`%r8` zeroed) and glibc runs `.init_array` / `.fini_array` itself.
. `__libc_start_main` performs libc setup, runs the init array, calls
  `main(argc, argv, envp)`, and on return calls `exit(main_result)`.

The relevant disassembly of the host's `Scrt1.o` (the exact sequence to
reproduce):

[source,asm]
----
_start:
    endbr64
    xor    %ebp, %ebp            ; outermost frame marker
    mov    %rdx, %r9             ; rtld_fini (passed in %rdx at entry)
    pop    %rsi                  ; argc
    mov    %rsp, %rdx            ; argv
    and    $0xfffffffffffffff0, %rsp   ; 16-byte align
    push   %rax                  ; pad
    push   %rsp                  ; stack_end
    xor    %r8d, %r8d            ; fini = NULL
    xor    %ecx, %ecx            ; init = NULL
    mov    main@GOTPCREL(%rip), %rdi   ; main
    call   *__libc_start_main@GOTPCREL(%rip)
    hlt
----

`crtbeginS.o` / `crtendS.o` provide the `.init_array` / `.fini_array`
terminator words and the `__dso_handle` / `__cxa_atexit` registration glue
that C and C++ static constructors rely on.  `crti.o` / `crtn.o` provide the
`_init` / `_fini` function frames.

=== What Blaise actually uses

Verification on the current native backend (empty and hello-world programs):

* Blaise's emitted assembly defines a standard `main(argc, argv)` returning
  `int`.  `main` itself calls `_SetArgs`, `_BlaiseInit`, each imported unit's
  `<Unit>_init`, then the program body, then returns 0.  Unit initialisation is
  therefore driven explicitly from `main`, *not* through `.init_array`.
* Neither the emitted program assembly nor `blaise_rtl.a` references any
  crtbegin/crtend-provided symbol (`__dso_handle`, `__cxa_atexit`,
  `__gmon_start__`, `_ITM_*`, `__TMC_END__`).
* The compiler emits nothing into `.init_array` / `.fini_array`.

Consequently the *only* contribution Blaise consumes from the whole CRT set is
the `_start` entry symbol.  `crtbeginS.o` / `crtendS.o` / `crti.o` / `crtn.o`
supply machinery Blaise does not exercise.

== Design

=== Ship Blaise's own `_start`

Add a new platform assembly source `runtime/src/main/asm/blaise_start_x86_64.s`
that defines the `_start` symbol, reproducing the modern-glibc sequence above:
clear `%rbp`, marshal `argc` / `argv` / `rtld_fini` / `stack_end`, zero the
`init` / `fini` arguments, load the address of `main`, and tail-call
`__libc_start_main`.

`main` and `__libc_start_main` are referenced through the GOT
(`@GOTPCREL`), matching the relocation style the internal linker already
handles for PIE output (`R_X86_64_REX_GOTPCRELX` / `R_X86_64_GOTPCRELX`).
`main` is defined by the program object; `__libc_start_main` is an undefined
libc import resolved through the PLT/GOT exactly as the existing linked libc
symbols are.

The object is assembled into `blaise_rtl.a` alongside the existing
`blaise_setjmp_x86_64.o` / `blaise_atomic_x86_64.o` / `blaise_utf8_x86_64.o`.
`TLinker.AddArchive` adds *all* archive members unconditionally (it does not
pull members on demand by undefined symbol), so the `_start` member is always
present in the link; the entry-name argument to `TLinker.Link` (`'_start'`)
resolves against it.

=== Stop linking the system CRT objects

In `blaise.codegen.native.driver.pas`, `LinkViaInternalLinker` no longer calls
`FindCrtObjects` and no longer issues `AddCrtObject` for `Scrt1.o`,
`crtbeginS.o`, `crtendS.o`, `crti.o`, or `crtn.o`.  The link line becomes:

* program object,
* extra unit objects,
* `blaise_rtl.a` (now containing `_start`),
* entry `_start`.

The `.init_array` / `.fini_array` merge paths in `blaise.linker.elf.pas`
remain — they tolerate an absent array (size 0, no `DT_INIT_ARRAY`) and cost
nothing when no object contributes one.  They are retained so that a future
object legitimately emitting an init-array (e.g. an `external 'C'` static
library a user links) still works.

=== Removed surface

* `uToolchain.FindCrtObjects` and its hard-coded `CrtDirs` / `GccVers` /
  `GccBases` lists are deleted; nothing else calls them.
* The `Crt1 … CrtEndS` locals and the five `AddCrtObject` calls in
  `LinkViaInternalLinker` are removed.

`TLinker.AddCrtObject` itself is retained: it is a generic "add this object,
keeping its `.init_array` contribution" entry point that the linker core still
uses internally and that the external-library path may need later.

== Platform scope

The change is x86-64 Linux (glibc) only — the single platform the native
internal linker currently targets.  The `_start` sequence is architecture- and
libc-specific:

* The startup object name carries the architecture suffix
  (`blaise_start_x86_64.s`), matching the existing `*_x86_64.s` convention, so
  an i386 / arm64 port adds a sibling file behind the same archive slot.
* musl's `_start` calls `__libc_start_main` with the same prototype, so the
  glibc sequence is musl-compatible; this is consistent with the
  toolchain-independence design's musl note.

== Risks and verification

The risk is a malformed entry sequence: a mis-aligned stack at the
`__libc_start_main` call, wrong argument marshalling, or a bad relocation on
`main` / `__libc_start_main` would crash at process start rather than produce a
diagnostic.  The IR-only test harness cannot see this (it never links), so the
guard must be executable end-to-end tests.

Verification:

. *E2E (both backends):* existing `AssertRunsOnAll` programs already compile →
  native link → run.  Every native arm now exercises the Blaise `_start`.  A
  passing suite proves the entry sequence, argument passing (`argc` / `argv`
  reach `_SetArgs`), and exit-code propagation.
. *A dedicated entry test:* a program that reads `ParamCount` / `ParamStr`
  and returns a non-zero exit code, asserting both the forwarded arguments and
  the propagated exit status — the two things `_start` is responsible for.
. *All four fixpoints:* the compiler links itself with the internal linker on
  the native path, so a broken `_start` breaks `fixpoint-native.sh` and
  `fixpoint-native-internal.sh` immediately.
. *Issue #142 regression:* with the system gcc CRT directory made
  unavailable (or on a distribution without it), the internal-linker link must
  still succeed — the objects are no longer consulted.

== Alternatives considered

* *Probe `gcc -print-file-name=<obj>` (rejected).* Portable across
  distributions but reintroduces a link-time gcc dependency, contradicting the
  internal linker's reason to exist.
* *Broaden the hard-coded directory scan (rejected).* Adding Mageia's triple
  and more gcc versions fixes the reported case but keeps the fundamentally
  brittle "guess the versioned gcc directory" strategy; the next unusual
  distribution re-opens the issue.
* *Ship our own `_start` (chosen).* Removes the dependency on the
  distribution-variable objects outright and matches FPC's proven approach.
