From 626ee4c9f88ebe59f35a73756af7bf8d9666768b Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Wed, 24 Jun 2026 23:39:11 +0100 Subject: [PATCH] fix(linker): ship own _start; drop system CRT startup objects (#142) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native internal linker linked every program against system CRT startup objects — Scrt1.o for _start plus crtbeginS.o/crtendS.o/crti.o/crtn.o — located by scanning a hard-coded list of multiarch and versioned gcc directories (/usr/lib/gcc///). That layout varies by distro, so the scan failed with "internal linker: CRT startup objects not found" on distributions whose triple/gcc version was not listed (issue #142, Mageia 9). Asking gcc (cc -print-file-name) would locate them portably but reintroduces a link-time gcc dependency, defeating the internal linker's purpose (toolchain independence). Instead, follow FPC's approach: the runtime now supplies its OWN _start (runtime/src/main/asm/blaise_start_x86_64.s), which marshals argc/argv and tail-calls __libc_start_main(main, ...) — the modern glibc convention (init/fini = NULL; glibc runs the init array itself). Blaise consumes nothing else from the CRT set: main is a plain C-style entry, unit init runs explicitly from main, and the emitted code references no crtbegin/crtend symbol. So the internal linker now needs no gcc-provided object and no versioned gcc directory; FindCrtObjects and its directory lists are removed. The new _start ships inside blaise_rtl.a; AddArchive includes all members unconditionally, so the entry symbol is always present. Tests: two TInternalAsmE2ETests cases drive the REAL compiler binary through the internal assembler + internal linker (the only harness that exercises the runtime _start; the e2e suite links via cc) and assert the two things _start owns — exit-code propagation (Halt(7)) and argc/argv forwarding (ParamCount/ ParamStr). The native fixpoints already link the compiler itself via this path. Design note: docs/self-contained-start-design.adoc. --- .../pascal/blaise.codegen.native.driver.pas | 12 +- .../src/main/pascal/blaise.linker.elf.pas | 15 +- compiler/src/main/pascal/uToolchain.pas | 62 ------ .../src/test/pascal/cp.test.assembler.pas | 88 ++++++++ docs/self-contained-start-design.adoc | 203 ++++++++++++++++++ runtime/Makefile | 3 +- runtime/src/main/asm/blaise_start_x86_64.s | 53 +++++ 7 files changed, 357 insertions(+), 79 deletions(-) create mode 100644 docs/self-contained-start-design.adoc create mode 100644 runtime/src/main/asm/blaise_start_x86_64.s diff --git a/compiler/src/main/pascal/blaise.codegen.native.driver.pas b/compiler/src/main/pascal/blaise.codegen.native.driver.pas index 9866ed4..b25957c 100644 --- a/compiler/src/main/pascal/blaise.codegen.native.driver.pas +++ b/compiler/src/main/pascal/blaise.codegen.native.driver.pas @@ -188,13 +188,9 @@ var Lk: TLinker; Obj: TElfObjectFile; TC: TToolchain; - Crt1, Crti, Crtn, CrtBeginS, CrtEndS: string; I: Integer; begin Result := ''; - if not FindCrtObjects(Crt1, Crti, Crtn, CrtBeginS, CrtEndS) then - Exit('internal linker: CRT startup objects not found'); - TC := ResolveToolchain(AOpts.Target); { Every Blaise program links blaise_rtl.a — the program entry emits @@ -213,9 +209,6 @@ begin try try Lk.SetDynamic(True); - Lk.AddCrtObject(Crt1); - Lk.AddCrtObject(Crti); - Lk.AddCrtObject(CrtBeginS); Obj := ReadElfObjectFile(AObjFile); Lk.AddOwnedObject(Obj); @@ -227,10 +220,11 @@ begin Lk.AddOwnedObject(Obj); end; + { _start now lives in blaise_rtl.a (blaise_start_x86_64.s) — no system + Scrt1.o / crtbegin / crtend / crti / crtn needed. AddArchive adds all + members unconditionally, so the entry symbol is always present. } Lk.AddArchive(TC.RTLPath); - Lk.AddCrtObject(CrtEndS); - Lk.AddCrtObject(Crtn); Lk.Link('_start', AOutputFile); except on E: Exception do diff --git a/compiler/src/main/pascal/blaise.linker.elf.pas b/compiler/src/main/pascal/blaise.linker.elf.pas index 81644e8..fd19ac3 100644 --- a/compiler/src/main/pascal/blaise.linker.elf.pas +++ b/compiler/src/main/pascal/blaise.linker.elf.pas @@ -37,13 +37,14 @@ unit blaise.linker.elf; with Phase B alone. Phase C — TLinker in dynamic mode (SetDynamic(True)) produces a - PIE (ET_DYN) executable linked against libc. It reads CRT startup - objects, generates GOT/PLT for undefined (libc) symbols, emits - .dynamic/.dynsym/.dynstr/.hash sections, resolves R_X86_64_64 as - R_X86_64_RELATIVE in .rela.dyn, handles R_X86_64_TPOFF32 for TLS, - and merges .init_array/.fini_array from CRT objects. The entry - point is _start (from Scrt1.o), which calls __libc_start_main with - main as the program's entry. } + PIE (ET_DYN) executable linked against libc. It generates GOT/PLT + for undefined (libc) symbols, emits .dynamic/.dynsym/.dynstr/.hash + sections, resolves R_X86_64_64 as R_X86_64_RELATIVE in .rela.dyn, + handles R_X86_64_TPOFF32 for TLS, and merges any .init_array/ + .fini_array a linked object contributes. The entry point is _start, + which the runtime archive supplies (blaise_start_x86_64.s) and which + calls __libc_start_main with main as the program's entry — no system + Scrt1.o / crtbegin / crtend / crti / crtn is needed (issue #142). } interface diff --git a/compiler/src/main/pascal/uToolchain.pas b/compiler/src/main/pascal/uToolchain.pas index 86e5183..e36916d 100644 --- a/compiler/src/main/pascal/uToolchain.pas +++ b/compiler/src/main/pascal/uToolchain.pas @@ -89,12 +89,6 @@ function ResolveAssembler: TTool; function ResolveLinker(const ATarget: TTargetDesc): TTool; function FindRTLArchive(const ATarget: TTargetDesc): string; -{ Locate the system CRT startup objects needed for dynamic (PIE) linking. - Returns True when all five objects are found; on False the paths are empty - and the caller should fall back to the external linker. } -function FindCrtObjects(out ACrt1, ACrti, ACrtn, - ACrtBeginS, ACrtEndS: string): Boolean; - { Walks $PATH for the first file named ABaseName that exists. Returns the absolute path on hit, '' on miss. On Windows hosts also tries '.exe'. } function WhichInPath(const ABaseName: string): string; @@ -337,62 +331,6 @@ begin Result := ''; end; -{ ------------------------------------------------------------------ } -{ CRT object discovery } -{ ------------------------------------------------------------------ } - -function FindCrtObjects(out ACrt1, ACrti, ACrtn, - ACrtBeginS, ACrtEndS: string): Boolean; -const - CrtDirs: array[0..2] of string = ( - '/usr/lib/x86_64-linux-gnu', - '/usr/lib64', - '/usr/lib' - ); - GccVers: array[0..3] of string = ('14', '13', '12', '11'); - GccBases: array[0..1] of string = ( - '/usr/lib/gcc/x86_64-linux-gnu', - '/usr/lib/gcc/x86_64-redhat-linux' - ); -var - CrtDir, GccDir: string; - I, J: Integer; -begin - ACrt1 := ''; - ACrti := ''; - ACrtn := ''; - ACrtBeginS := ''; - ACrtEndS := ''; - Result := False; - - for I := 0 to High(CrtDirs) do - begin - CrtDir := CrtDirs[I]; - if FileExists(CrtDir + '/Scrt1.o') then - begin - ACrt1 := CrtDir + '/Scrt1.o'; - ACrti := CrtDir + '/crti.o'; - ACrtn := CrtDir + '/crtn.o'; - Break; - end; - end; - if ACrt1 = '' then Exit; - - for I := 0 to High(GccBases) do - for J := 0 to High(GccVers) do - begin - GccDir := GccBases[I] + '/' + GccVers[J]; - if FileExists(GccDir + '/crtbeginS.o') then - begin - ACrtBeginS := GccDir + '/crtbeginS.o'; - ACrtEndS := GccDir + '/crtendS.o'; - Result := FileExists(ACrti) and FileExists(ACrtn) - and FileExists(ACrtEndS); - Exit; - end; - end; -end; - { ------------------------------------------------------------------ } { Top-level resolver } { ------------------------------------------------------------------ } diff --git a/compiler/src/test/pascal/cp.test.assembler.pas b/compiler/src/test/pascal/cp.test.assembler.pas index 01f597b..8af9cb3 100644 --- a/compiler/src/test/pascal/cp.test.assembler.pas +++ b/compiler/src/test/pascal/cp.test.assembler.pas @@ -81,6 +81,9 @@ type function RunProcNoArgs(const AExe: string; out AStdout: string): Integer; function CompileAndRun(const ASrc: string; out AStdout: string; out AExitCode: Integer): Boolean; + function CompileAndRunArgs(const ASrc: string; + const AArgs: array of string; + out AStdout: string; out AExitCode: Integer): Boolean; protected procedure SetUp; override; published @@ -97,6 +100,12 @@ type procedure TestClassVirtualCall; procedure TestFloatArithmetic; procedure TestFormatFloatArg; + { Self-contained program entry (issue #142): the runtime supplies its own + _start (blaise_start_x86_64.s) so the internal linker needs no system + Scrt1.o / crtbegin / crtend. These assert the two things _start owns — + argc/argv forwarding and exit-code propagation through __libc_start_main. } + procedure TestEntry_ExitCodePropagates; + procedure TestEntry_ArgsForwarded; end; implementation @@ -675,6 +684,39 @@ begin Result := True; end; +{ Like CompileAndRun but runs the produced binary WITH command-line arguments, + so argc/argv forwarding through the runtime's own _start is exercised. } +function TInternalAsmE2ETests.CompileAndRunArgs(const ASrc: string; + const AArgs: array of string; + out AStdout: string; out AExitCode: Integer): Boolean; +var + SrcFile, OutFile, CompOut: string; + Rc: Integer; +begin + Result := False; + if not Self.CompilerAvailable() then Exit; + + FCounter := FCounter + 1; + SrcFile := FScratch + 'test_asm_' + IntToStr(FCounter) + '.pas'; + OutFile := FScratch + 'test_asm_' + IntToStr(FCounter); + + WriteFile(SrcFile, ASrc); + + Rc := Self.RunProc(FCompiler, [ + '--source', SrcFile, + '--unit-path', FRTLPath, + '--unit-path', FStdlibPath, + '--output', OutFile, + '--backend', 'native', + '--assembler', 'internal' + ], CompOut); + if Rc <> 0 then + Fail('compile failed (rc=' + IntToStr(Rc) + '): ' + CompOut); + + AExitCode := Self.RunProc(OutFile, AArgs, AStdout); + Result := True; +end; + { ---- Test methods ---- } procedure TInternalAsmE2ETests.TestSimpleAdd; @@ -989,6 +1031,52 @@ begin AssertEquals('1.2500' + LineEnding, Out_); end; +procedure TInternalAsmE2ETests.TestEntry_ExitCodePropagates; +var + Out_: string; + EC: Integer; +begin + { Halt(N): the runtime's own _start calls __libc_start_main(main, ...) and + glibc exits with main's return value. A broken entry sequence would crash + at startup or return the wrong code. No system Scrt1.o is involved. } + if not CompileAndRun( + 'program test_exit;' + LineEnding + + 'begin' + LineEnding + + ' WriteLn(''before'');' + LineEnding + + ' Halt(7)' + LineEnding + + 'end.', + Out_, EC) then + begin + Ignore(''); + Exit; + end; + AssertEquals(7, EC); + AssertEquals('before' + LineEnding, Out_); +end; + +procedure TInternalAsmE2ETests.TestEntry_ArgsForwarded; +var + Out_: string; + EC: Integer; +begin + { argc/argv reach the program through _start -> __libc_start_main -> main -> + _SetArgs. ParamStr(0) is the program path; the passed args follow. } + if not CompileAndRunArgs( + 'program test_args;' + LineEnding + + 'begin' + LineEnding + + ' WriteLn(ParamCount());' + LineEnding + + ' WriteLn(ParamStr(1));' + LineEnding + + ' WriteLn(ParamStr(2))' + LineEnding + + 'end.', + ['alpha', 'beta'], Out_, EC) then + begin + Ignore(''); + Exit; + end; + AssertEquals(0, EC); + AssertEquals('2' + LineEnding + 'alpha' + LineEnding + 'beta' + LineEnding, Out_); +end; + { ---- Registration ---- } initialization diff --git a/docs/self-contained-start-design.adoc b/docs/self-contained-start-design.adoc new file mode 100644 index 0000000..dc9c966 --- /dev/null +++ b/docs/self-contained-start-design.adoc @@ -0,0 +1,203 @@ += 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///`). 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=` 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 + `_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=` (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. diff --git a/runtime/Makefile b/runtime/Makefile index d140272..a351c84 100644 --- a/runtime/Makefile +++ b/runtime/Makefile @@ -36,7 +36,8 @@ BLAISE = $(COMPILER_BIN)/blaise BLAISE_FLAGS = --no-incremental # Assembly sources — platform-specific, no C compiler needed. -ASM_OBJS = $(OBJ_DIR)/blaise_setjmp_x86_64.o \ +ASM_OBJS = $(OBJ_DIR)/blaise_start_x86_64.o \ + $(OBJ_DIR)/blaise_setjmp_x86_64.o \ $(OBJ_DIR)/blaise_atomic_x86_64.o \ $(OBJ_DIR)/blaise_utf8_x86_64.o diff --git a/runtime/src/main/asm/blaise_start_x86_64.s b/runtime/src/main/asm/blaise_start_x86_64.s new file mode 100644 index 0000000..b640d05 --- /dev/null +++ b/runtime/src/main/asm/blaise_start_x86_64.s @@ -0,0 +1,53 @@ +# +# Blaise — An Object Pascal Compiler +# Copyright (c) 2026 Graeme Geldenhuys +# SPDX-License-Identifier: Apache-2.0 WITH Swift-exception +# Licensed under the Apache License v2.0 with Runtime Library Exception. +# See LICENSE file in the project root for full license terms. +# +# Program entry point (x86_64, System V ABI, glibc-compatible). +# +# This replaces the system Scrt1.o so the internal linker needs no +# gcc-provided startup object and no versioned gcc directory (issue #142). +# The runtime ships its own _start; the linker (TLinker.Link '_start') uses +# this symbol as the ELF entry point. +# +# On entry the kernel hands us the initial process stack: +# (%rsp) argc +# 8(%rsp) argv[0] +# ... argv[argc] = NULL, then envp, then the auxiliary vector +# and %rdx holds the dynamic linker's finaliser (rtld_fini) for PIE images. +# +# We marshal these into the arguments __libc_start_main expects: +# __libc_start_main(main, argc, argv, init, fini, rtld_fini, stack_end) +# %rdi = main (the program's C-style entry) +# %rsi = argc +# %rdx = argv +# %rcx = init = NULL (unit init runs from main, not .init_array) +# %r8 = fini = NULL +# %r9 = rtld_fini +# stack_end pushed on the stack +# glibc runs the init array (if any), calls main(argc, argv, envp), then +# exit(main's return value). This mirrors modern glibc's own Scrt1.o. + +.text + +.globl _start +.type _start, @function +_start: + endbr64 + xor %ebp, %ebp # outermost frame marker (ABI) + mov %rdx, %r9 # rtld_fini -> arg 7 + pop %rsi # argc -> arg 2 + mov %rsp, %rdx # argv -> arg 3 (now at stack top) + and $0xfffffffffffffff0, %rsp # re-align stack to 16 bytes + push %rax # padding (8 bytes) ... + push %rsp # ... and stack_end, keeping alignment + xor %r8d, %r8d # fini = NULL -> arg 5 + xor %ecx, %ecx # init = NULL -> arg 4 + lea main(%rip), %rdi # main -> arg 1 + call __libc_start_main@PLT # does not return + hlt # trap if it ever does +.size _start, .-_start + +.section .note.GNU-stack,"",@progbits