← board

An i386 object carries .text relocations as soon as it uses sysutils

Measured

Identical program, one line different, --emit-obj --target=i386, counting R_386_32 in .rel.text:

program Mini;
[uses sysutils;]
var G: Integer = 1; cvar;
function mini_get: Integer; cdecl; begin mini_get := G; end;
begin end.
uses sysutils; R_386_32 in .rel.text
absent 0
present 62

ld then warns relocation in read-only section '.text' on any link, and a -pie -Wl,-z,text link would fail outright.

Why the existing row does not catch it

test-emit-obj asserts "the Pascal i386 .o links+runs as a hardened PIE too", and that row is green. It is measured on test/test_emit_obj.pas, which uses no RTL unit at all — a fixture whose count is 0 for reasons that have nothing to do with the guarantee. The row is not wrong; its scope is one shape.

The C analogue was found and fixed ([[bug-a-an-i386-object-from-the-c-frontend-carries-text-relocations]]), so the emitters the C path uses are already PC-relative. Whatever sysutils pulls in reaches an absolute form the ticket above did not cover.

Not caused by, but found beside, the data-symbol work

d402147d6 is not involved: the count is 62 both with and without the cvar directives on the same source, and 0 for a bare program that carries them. The cvar marker was the control.

First step

62 relocations is a small enough set to enumerate. readelf -rW gives the offsets; objdump -d at those offsets names the shapes, and the count almost certainly collapses to two or three emitters that never went through EmitGlobRef/EmitDataRef — which is exactly how the C ticket's five survivors were found. Enumerate from the artefact, not from the source.

Acceptance

RESOLVED — cd4af7824

Enumerated from the artefact as the ticket said to: 32 c7 00 and 30 50 68, nothing else. One family, and the reason it survived four landed conversions is structural rather than accidental — the load conversion borrows the instruction's own dead destination, the store conversion borrows a push/pop scratch, and an address used as an IMMEDIATE has neither. The relocated field is the immediate; the memory operand, where there is one, is a plain register base, so nothing either function inspects is present.

The replacement builds the address on the stack and touches no flags. The shorter call .L; add dword [esp], imm32 was written first and thrown away for that reason: push imm32 and mov [reg], imm32 write no flags, so making their replacement write flags puts "never materialise a data address between a cmp and its Jcc" on every future emitter, enforced by nothing. frankC checked all thirteen sites and it holds today. That is exactly the shape of correct-when-written this repo keeps paying for.

Not done by sniffing the end of the code buffer, which is how the other families are recognised: c7 00 is two bytes that mov [eax+disp32], imm32 also ends in whenever its displacement ends $00 $C7, and a ~13MB .data makes that reachable rather than theoretical. Thirteen call sites carry the intent explicitly instead.

Measured — both arms built to converged, shas asserted to differ

before a4c67a5e6cc8 after aad81be5ae4e
i386 R_386_32 in .rel.text, fixture with uses sysutils 62 0
gcc -m32 -pie -Wl,-z,text rc=2, no binary links, runs, no DT_TEXTREL
x86-64 --emit-obj object byte-identical
i386 --threadsafe executable byte-identical

The two identity rows are the inertness claim and they are measurements, not readings: both conversions are gated on EmitObjMode, and the two identical files come from compilers whose shas differ.

Both new rows fail on the pre-fix binary — checked, not assumed. The count row is 62 against a bound of 0, and it is paired with a floor on R_386_PC32 so "zero absolute" cannot pass on an object where nothing was emitted at all. The unlock-stub row asserts its locator FOUND the stub before comparing anything, because a disassembly search that matches nothing cannot fail.

A second, worse defect found beside it — and it was already there

The acceptance did not ask for this and it is the more dangerous half. In --threadsafe --emit-obj --target=i386, the I/O unlock stub's jnz carried a hand-counted +8 over a span containing a global store — and under --emit-obj that store grows a push/anchor/pop wrapper. The jump landed on the pop INSIDE it:

23c: dec DWORD PTR ds:0x92c0
242: jne 24c            <- meant to reach the ret at 254
244: xor ecx,ecx
246: push eax        }
247: call .+0        }   the wrapper this span did not used to contain
24c: pop  eax            <- lands here
24d: mov [eax+0x92bb],ecx
253: pop  eax
254: ret

The still-nested path popped the caller's return address into eax, stored through it — a wild write to that address plus 37563 — and returned to garbage. The object built clean; no relocation count, symbol table or link could see it. Byte-identical under the pre-change compiler, so pre-existing.

All three jumps in those two stubs go through PatchRel8 now, which measures the span after it is emitted and refuses what does not fit. The je +16's "spin block must be exactly 16 bytes" assert went with them: it existed only to pin a literal, and a patched jump has nothing to pin.

How it was found is the transferable part. I asked frankC whether any rel8 span could contain one of MY thirteen sites. They answered no — and then named the population I had not asked about: hand-written short jumps with literal displacements, which never reach CheckRel8 at all. Their cross-reference cleared my sites; checking the remainder found one already wrong. The question was about my change; the answer that mattered was about the guard's coverage.

Still open

Nothing from this ticket. [[meta-a-pxx-produces-linkable-code]] listed i386 position independence as one of its two remaining gaps; that line is closed.

CORRECTION 2026-09-06 (frankA) — the 0 was a measurement, not a property

test-emit-obj is red again, at 1 absolute .text relocation, and nothing in this ticket regressed. The correction belongs here because this ticket's own acceptance is what a reader will check against, and its first bullet — "emits an i386 object with 0 R_386_32 in .rel.text" — reads as an invariant the fix established. It is not one.

The surviving relocation is in PXXIoCheck, and it is refused by the guard rather than missed by an emitter:

28f22:  8b 45 f0        mov  -0x10(%ebp),%eax
28f25:  a3 b8 92 00 00  mov  %eax,0x92b8        <- R_386_32

TryI386PcRelStore handles A3. Its I386PrefixBefore(CodeLen-1) guard reads the byte before the opcode — here the f0 of the displacement -0x10 — and takes it for a LOCK prefix.

The count is data-dependent, which is the part this ticket could not have known and the next reader needs to. One unrelated extra local in PXXIoCheck moves code off -0x10 and the count goes 1 -> 0; removing it restores the 1. So 62 -> 0 was true at cd4af7824 and is a fact about the frame offsets that existed that day, not about position-independence. 46 commits have touched lib/rtl since and one of them put a local at -0x10 in a routine that stores to a global.

Note the shape: this ticket's own resolution warns against recognising a family by "sniffing the end of the code buffer" and lists thirteen call sites that carry the intent explicitly for exactly that reason. The prefix guard is the remaining sniffer, and it has now failed in the direction the resolution predicted for the other one.

Diagnosed and filed, not fixed: [[bug-a-the-i386-pic-prefix-guard-reads-a-displacement-byte-as-a-prefix]]. The fix shape is the same one this ticket already argued for — announce from the call site — and it is a wider change than it looks, because a missed announcement turns a conservative refusal into a silent wrong-width access.

This ticket stays done. The work in it is correct and the acceptance was met when it was measured; what is corrected is the claim that the number stays put.

FOLLOW-UP the same day — the flap is fixed, so the acceptance bullet holds now

bug-a-the-i386-pic-prefix-guard-reads-a-displacement-byte-as-a-prefix landed X386InstrStart: a call site records where its instruction began and I386PrefixBefore stops guessing about the byte in front of the opcode. The perturbation that used to move the count no longer does — 0 unmodified, 0 with the extra local, 0 restored, where this ticket's compiler gave 1 / 0 / 1.

So the first acceptance bullet above is a property again rather than a measurement, and this correction records the interval in which it was not. test-emit-obj is still red, for an older reason this row was hiding: the xtensa link shim provides no ESP-IDF and the PAL backend imports it — [[bug-a-the-emit-obj-xtensa-link-shim-does-not-provide-the-pal-backends-esp-idf-symbols]].