feat(debug): link --debug-opdf binaries as PIE

pdr now resolves the ASLR slide correctly (load base from the
binary's offset-0 mapping), so the -no-pie guard in both native link
paths is no longer needed.  Debug binaries are position-independent
again, matching the platform default.

Verified under live ASLR: breakpoints, var-param drilldown, captured
vars, dynamic arrays, TList<T> inspection, callstack and stepping all
work against PIE binaries.

Remove the 'PIE (ASLR) support in PDR' section from
future-improvements.adoc — implemented.
This commit is contained in:
Graeme Geldenhuys 2026-06-11 15:43:02 +01:00
parent 248dccf6ca
commit ca21b884bc
2 changed files with 0 additions and 104 deletions

View file

@ -483,8 +483,6 @@ begin
try
Args.Add('-o');
Args.Add(AOutputFile);
if (AOPDFAsmFile <> '') or AOpdfDebug then
Args.Add('-no-pie'); { OPDF addresses are absolute; PIE relocation breaks them }
Args.Add(AsmFile);
if (AOPDFAsmFile <> '') and FileExists(AOPDFAsmFile) then
Args.Add(AOPDFAsmFile);
@ -532,8 +530,6 @@ begin
try
Args.Add('-o');
Args.Add(AOutputFile);
if (AOPDFAsmFile <> '') or AOpdfDebug then
Args.Add('-no-pie'); { OPDF addresses are absolute; PIE relocation breaks them }
Args.Add(AAsmFile);
if (AOPDFAsmFile <> '') and FileExists(AOPDFAsmFile) then
Args.Add(AOPDFAsmFile);

View file

@ -29,11 +29,6 @@ prioritise them.
== Language
=== Require `()` on all zero-argument function/procedure/method calls
*Status:* Implemented. See `docs/language-rationale.adoc` §_Mandatory
parentheses on zero-argument calls_ for the design rationale.
=== Procedural types
- reference to function/procedure (anonymous methods)
@ -232,44 +227,6 @@ replacements. `Extended` maps to `Double` with a precision-loss warning.
*Effort.* Medium — primarily RTL/stdlib work once operator overloading is in place.
No compiler changes needed.
== Debugger
=== PIE (ASLR) support in PDR
*What.* When a Blaise binary is compiled with `--debug-opdf`, the compiler
currently passes `-no-pie` to gcc to ensure OPDF `.quad symbol` entries
contain absolute link-time addresses. The PDR debugger should instead read
the binary's actual load base at runtime and adjust all OPDF addresses by the
ASLR slide, allowing debug-opdf builds to remain PIE.
*Why it matters.* PIE (Position Independent Executable) is the default on all
modern Linux distributions and is required for ASLR protection. Disabling it
in debug builds is a pragmatic workaround that is acceptable for local
development but is not appropriate for any scenario where the binary is
distributed or run in a production-adjacent environment. Lifting the
restriction makes `--debug-opdf` safe to enable in CI and staging environments.
*Implementation notes.*
* On Linux, `/proc/<pid>/maps` lists each memory mapping with its base
address and the backing file. At debugger attach time, read this file,
find the entry whose backing path matches the target binary, and record
its base address as `LoadBase`.
* On FreeBSD, PIE and ASLR are supported by default from FreeBSD 12 onwards.
FreeBSD's procfs is not mounted by default, so `/proc/<pid>/map` is not
reliable. The correct approach is `sysctl(KERN_PROC_VMMAP, pid)`, which
returns the full VM map for a process; find the entry matching the binary
path and extract its start address as `LoadBase`.
* On macOS, the equivalent is `task_info(TASK_DYLD_INFO)` or parsing the
Mach-O load commands; `DYLD_IMAGE_BASE` is available via the dyld shared
cache APIs.
* At symbol-resolution time on all platforms: `RuntimeAddr = OPDFAddr + LoadBase`.
* Once PIE-aware address resolution is in place, remove the `-no-pie` guard
in `Blaise.pas` (`CompileToNative`).
*Effort.* Small on Linux and FreeBSD (one sysctl/procfs read at attach time);
moderate on macOS (dyld integration).
== macOS — Debugging and Code-Signing
=== Debugger entitlement requirements
@ -1058,60 +1015,3 @@ to distinguish this from simple enums so existing code is unaffected.
is acceptable provided it is a new keyword.
== ARC: elide retain/release for const value parameters (IMPLEMENTED)
A `const` parameter promises the caller keeps the argument alive for the
duration of the call, so the callee-side `_StringAddRef`/`_StringRelease`
(and `_ClassAddRef`/`_ClassRelease`) pair on entry/exit is, in principle,
redundant. Eliding it removes two RTL calls per const string/class/interface
parameter — a measurable saving on hot paths that thread strings through many
`const` parameters (the parser and semantic analyser do this heavily).
Status: shipped. The elision is in place (the four entry/exit ARC loops skip
`IsConstParam`), paired with a *caller-side* retain for transient string
arguments — `EnsureConstStringRef`/`ReleaseConstStringArgs` wrap a value-mode
argument to a const-string parameter with `_StringAddRef` before the call and
`_StringRelease` after, which is a no-op for immortal literals and nets to zero
for owned strings, so the cost lands only on the `+0` transients that need it.
The remainder of this section is retained as the design record.
The first attempt (commit `5a5b5d4`) elided the pair *without* the caller-side
retain and was reverted after it introduced a use-after-free. The reason it was
unsound as stated:
* The "caller keeps it alive" premise holds for a *named* argument the caller
owns for the whole call, but *not* for a *temporary* bound to the const
parameter — e.g. `Use(A + ' ' + B)`, where the concatenation result is a
freshly-allocated string whose only reference is the argument slot itself.
With the callee-side retain elided, that temporary's refcount reaches zero
at the call boundary and the string is freed *before* the callee reads it.
* This bites the RTL especially hard: routines such as `_StringCopy` and
`StrHead` take `const string` parameters and are frequently called with
built-at-runtime temporaries. A miscompiled RTL is self-reproducing under
self-hosting — the defect only manifests at the *second* generation (the
compiler that emits the broken RTL is itself fine), so a single fixpoint
step (stage-2 == stage-3) does not necessarily expose it, and the compiler's
own sources may not trigger the exact aliasing case. The original change
passed its valgrind test only because that test passed a string *literal*
(immortal), not a temporary.
To re-attempt the optimisation safely, the elision must be conditioned on the
*argument*, not merely the parameter:
* Elide the callee-side retain/release only when the caller can prove the
argument outlives the call without the callee's reference — i.e. a named
local/global/parameter the caller already retains — and *keep* the
retain/release when the argument is a temporary (function/property result,
concatenation, cast, or any `ExprOwnsRef` expression).
* Equivalently, move the responsibility to the *call site*: for a const
parameter bound to a temporary, the caller retains the temporary across the
call and releases it afterwards (caller-side balancing), leaving the callee
free of ARC traffic. This keeps the optimisation's benefit for the common
named-argument case while remaining correct for temporaries.
* Mandatory regression coverage: a valgrind e2e test that passes a
*concatenation result* (not a literal) as a `const string` parameter and
reads it in the callee — see `TestRun_ConstStringTemp_StaysAlive_Valgrind`
in `cp.test.e2e.arc.pas`. Add the class- and interface-typed analogues.