From d379b84ec33d3540ede0ae8714fdea41f88ef581 Mon Sep 17 00:00:00 2001 From: Christopher Williams Date: Wed, 23 Sep 2026 21:09:07 -0400 Subject: [PATCH] phase6: reproduce a gp-relative function and record the small-data evidence --- config/regions.tsv | 1 + config/symbols.tsv | 9 ++- docs/MATCHING_CONVENTIONS.md | 6 +- docs/MATCHING_COOKBOOK.md | 22 +++++++- docs/PHASE6_SMALL_DATA.md | 101 ++++++++++++++++++++++++++++++++++ phase-ends/CURRENT_PHASE.md | 38 ++++++++++++- src/func_80012780.c | 32 +++++++++++ tools/sf3_match | 82 ++++++++++++++++++++++++--- tools/tests/test_sf3_match.py | 43 +++++++++++++-- 9 files changed, 315 insertions(+), 19 deletions(-) create mode 100644 docs/PHASE6_SMALL_DATA.md create mode 100644 src/func_80012780.c diff --git a/config/regions.tsv b/config/regions.tsv index 0db5db6..9ba9729 100644 --- a/config/regions.tsv +++ b/config/regions.tsv @@ -13,3 +13,4 @@ 0x80017AD4 0x80017AE8 src/func_80017AD4.c 0x8002D2A0 0x8002D2BC src/func_8002D2A0.c 0x8002D2BC 0x8002D2D4 src/func_8002D2BC.c +0x80012780 0x8001278C src/func_80012780.c diff --git a/config/symbols.tsv b/config/symbols.tsv index 9e40c01..4cc6fd1 100644 --- a/config/symbols.tsv +++ b/config/symbols.tsv @@ -1,11 +1,18 @@ # Symbol registry: absolute addresses for cross-references used by C regions. # -# Columns: NAMEaddress (address is hex; 0x prefix optional) +# Columns: NAMEaddress[gp] (address is hex; 0x prefix optional) # # These name unmatched functions and globals so a C region can reference them # by name. The harness passes each row to the linker as # `--defsym NAME=0xADDR`. A name here is a placeholder for an address, not a # claim about the object's meaning; real names are earned from evidence. +# +# A `gp` marker means the original accessed the symbol gp-relative, so the +# harness rewrites the macro access to explicit `%gp_rel`. `_gp` is the small +# data pointer value crt0 loads at 0x800FB3E4 (`lui gp,0x8012` + +# `addiu gp,gp,6456`), and is needed to resolve R_MIPS_GPREL16. g_80122354 0x80122354 D_8012E2C8 0x8012E2C8 D_8010F354 0x8010F354 +_gp 0x80121938 +D_80121974 0x80121974 gp diff --git a/docs/MATCHING_CONVENTIONS.md b/docs/MATCHING_CONVENTIONS.md index c4b04fd..5b27bd9 100644 --- a/docs/MATCHING_CONVENTIONS.md +++ b/docs/MATCHING_CONVENTIONS.md @@ -88,7 +88,11 @@ g_80122354 0x80122354 `make code` and `make gate` pass `config/symbols.tsv` automatically; `range` accepts `--symbols` (and `--defsym` for one-off names). A name in the registry is an address placeholder, not a claim -about the object's meaning. `0x8002D2A0` and `0x8002D2BC` are registered and gated this way. +about the object's meaning. `0x8002D2A0`, `0x8002D2BC` and `0x80012780` are registered and gated this way. + +An optional third column `gp` marks a symbol the original accessed `gp`-relative (small data). The +harness rewrites that symbol's macro accesses to explicit `%gp_rel(sym)($gp)`, and the linker resolves +`R_MIPS_GPREL16` against the `_gp` row. See [PHASE6_SMALL_DATA.md](PHASE6_SMALL_DATA.md). Symbols are resolved by the **linker**, not the assembler: the assembler leaves them undefined and emits `%hi`/`%lo` relocations, and the linker applies the HI16 carry adjustment. This is what makes a diff --git a/docs/MATCHING_COOKBOOK.md b/docs/MATCHING_COOKBOOK.md index 3c0077b..36f8bc0 100644 --- a/docs/MATCHING_COOKBOOK.md +++ b/docs/MATCHING_COOKBOOK.md @@ -112,9 +112,27 @@ this executable overwhelmingly `t0`. *Limit:* the register choice is context-dependent, so it cannot be used to identify the compiler; it only matters when reproducing a specific function. +### 10. `gp`-relative access is per symbol, forced with `%gp_rel` + +- The `gp` base is **`0x80121938`**: crt0 does `lui gp,0x8012` + `addiu gp,gp,6456` at `0x800FB3E4`. +- The original accesses some globals `gp`-relative and others absolutely **even within ±32 KB of + `gp`**: `0x80122354` is absolute (`func_8002D2A0`) while `0x80121974` is `gp`-relative + (`func_80012780`). The choice is the object's **size/section**, not its distance from `gp`. +- GNU `as` only expands `lw`/`sw` macros to `%gp_rel` for an undefined symbol when `-G>0`, and it does + not do so for `sw` at all without the symbol's section. The harness therefore marks `gp` symbols in + the registry and rewrites their accesses to explicit `%gp_rel(sym)($gp)`; the linker resolves + `R_MIPS_GPREL16` against `_gp`. + +*Basis:* `func_80012780` (`sw a0,60(gp)`) byte-identical; a census of 3,976 `gp`-relative accesses +over 1,286 distinct addresses. See [PHASE6_SMALL_DATA.md](PHASE6_SMALL_DATA.md). +*Limit:* the numeric `-G` threshold is **not recoverable** from the code (object sizes are unknown); +`-mgpopt`/`-mno-gpopt` produce identical `cc1` output for these functions. + ## Open questions -- The exact `-G` small-data threshold (the executable clearly uses `gp`-relative data in places). -- Whether `-mgpopt` was passed. +- The exact `-G` small-data threshold is not recoverable from the code; the per-symbol `gp` form is + reconstructed from the original's accesses instead (finding 10). +- Whether `-mgpopt` was passed is not observable: it does not change `cc1` output for the cases + tested. - One reconstructed function (`0x8005DEF8`) whose constant multiply the original emits as a real `mult` while every compiler tested synthesises it — the reconstruction is the likely cause. diff --git a/docs/PHASE6_SMALL_DATA.md b/docs/PHASE6_SMALL_DATA.md new file mode 100644 index 0000000..a1b2154 --- /dev/null +++ b/docs/PHASE6_SMALL_DATA.md @@ -0,0 +1,101 @@ +# Phase 6 — Small Data (`gp`) Evidence + +**Scope:** P6-T4 — determine the small-data threshold from the executable's `gp`-relative accesses and +reproduce at least one `gp`-relative function byte-for-byte. +**Status:** complete. One `gp`-relative function matched; the numeric threshold is recorded as **not +uniquely recoverable** from the code, with the evidence and limits stated. + +## The `gp` base + +crt0 loads `gp` at `0x800FB3E4` from an absolute value: + +| Address | Instruction | Value | +|---|---|---| +| `0x800FB3E4` | `lui gp,0x8012` | high half | +| `0x800FB3E8` | `addiu gp,gp,6456` | `gp = 0x80121938` | + +So the small-data pointer for this executable is **`0x80121938`**. + +## Access census + +Scanning every load/store with an immediate base register of `gp`: + +- **3,976** such instructions. +- **1,286** distinct target addresses, spanning `0x80119938..0x801298CF` (≈64 KB), with 257 gaps + larger than 64 bytes — i.e. many separate small objects. +- The most common offsets are 32–46 (addresses `0x80121958..0x80121966`), suggesting one or more + small objects close to `gp`. +- `gp`-relative **address** materialisation (`addiu rt,gp,imm`) also occurs, e.g. `addiu r4,gp,44`, + so the original also took the address of small objects. + +## Why the numeric threshold is not recoverable here + +GNU `as` behaviour (measured): with `-G N` where `N > 0`, the symbol macros `lw rt,sym` / `sw rt,sym` +for an **undefined** symbol expand to `%gp_rel`; with `-G0` they expand to `lui`/`%lo` absolute form. + +But the original's choice is **per symbol**, not a single range test. The counter-example is decisive: + +| Symbol | Address | Original access | +|---|---|---| +| `g_80122354` (`func_8002D2A0`) | `0x80122354` | **absolute** (`lui v0,0x8012` / `lw v0,9044(v0)`) | +| `D_80121974` (`func_80012780`) | `0x80121974` | **`gp`-relative** (`sw a0,60(gp)`) | + +`0x80122354` is inside ±32 KB of `gp` (offset `+0xA1C`), yet is accessed absolutely. The difference is +the **size/section** of each object (`-G` puts objects ≤ N bytes in `.sdata`/`.sbss`), and object sizes +are not observable from the code because the data is the untouched payload fallback. + +**Conclusion:** the bytes determine *which symbols* were accessed `gp`-relative, not the numeric `-G` +value. A lower bound is that the small-data region spans the full signed 16-bit offset range; the +largest `gp`-relative object's size would bound `-G` from above, but that size is unknown. + +## `-mgpopt` evidence + +`cc1` output is **identical** for `-G0`, `-G4`, `-G8`, `-G16`, `-G32`, and identical with `-mgpopt` +versus `-mno-gpopt`, for `extern int`/`char`/struct loads and stores: + +``` +cc1 -quiet -O2 -G8 -mno-split-addresses [-mgpopt|-mno-gpopt] -> identical .s +``` + +The `gp`-versus-absolute decision is therefore made by the assembler (from the symbol's section) and +the linker, not by `cc1`. `-mgpopt` is **not observable** from these functions and is left unselected. + +## The reproduction + +`func_80012780` (`0x80012780..0x8001278C`, 12 bytes): + +``` +sw a0,60(gp) +jr ra +nop +``` + +Mechanism (all now in the harness): + +1. `config/symbols.tsv` marks `D_80121974` with the `gp` marker and defines `_gp = 0x80121938`. +2. The harness rewrites the maspsx output `sw $4,D_80121974` to + `sw $4,%gp_rel(D_80121974)($gp)` — GNU `as` will not do this itself for a symbol whose section it + cannot see. +3. GNU `as -G0` emits `R_MIPS_GPREL16`; the linker resolves it against `--defsym _gp=0x80121938`. + +Result: `sf3_match range` → 0 differing bytes; `make gate` → `c_regions=4`, 0 differing bytes, SHA-1 +`e173426c157384ebf1b6caf8c6fea18a85a14af9`. + +Exact commands: + +```bash +./tools/sf3_match range \ + --exe 'extracted/SCUS_946.40;1' --source src/func_80012780.c \ + --start 0x80012780 --end 0x8001278C --work .run/p6-t4-gp \ + --symbols config/symbols.tsv +# -> result=MATCH +``` + +## Limits + +- `gp` symbols are identified from the original's access form, not from a recovered symbol table; the + registry marker is the reconstruction of that per-symbol property. +- The `la`-of-a-`gp`-symbol rewrite (`addiu rt,$gp,%gp_rel(sym)`) is implemented but not yet exercised + on a real function. +- The numeric `-G` remains unresolved and is not needed by the current mechanism; it would matter only + if a region defined its own small data. diff --git a/phase-ends/CURRENT_PHASE.md b/phase-ends/CURRENT_PHASE.md index 23cc4cc..b7211f2 100644 --- a/phase-ends/CURRENT_PHASE.md +++ b/phase-ends/CURRENT_PHASE.md @@ -9,8 +9,8 @@ - [x] **P6-T1 — Phase control records, baseline revalidation, and open-item triage** (complete) - [x] **P6-T2 — Registry symbol support and per-region flag overrides** (complete) - [x] **P6-T3 — `maspsx` integration and the ASPSX `la` verification** (complete) -- [ ] P6-T4 — `-G` small-data threshold from byte evidence -- [ ] Rules check +- [x] **P6-T4 — `-G` small-data threshold from byte evidence** (complete) +- [x] **Rules check** — re-read `AGENTS.md` mandatory behavior after P6-T4 and stated the required continuation notice. - [ ] P6-T5 — Evidence-graded function-boundary inventory - [ ] P6-T6 — First matching batch, with duplicate sharing - [ ] P6-T7 — Cookbook, conventions, verification record, and phase gate @@ -129,3 +129,37 @@ next code). This confirms the Phase 5 conclusion that the entry is CRT startup, **Limits:** maspsx is exercised on `la`, symbol load/store and delay-slot scheduling; `-G`/gp-relative behaviour is not yet exercised (P6-T4). The clear-loop negative is bounded to the tested reconstruction and symbol names. + +## P6-T4 — Small-data (`gp`) threshold from byte evidence (2026-09-23) + +**Delivered:** + +- `config/symbols.tsv` gained an optional `gp` marker and the `_gp` row (`0x80121938`, from crt0's + `lui gp,0x8012` + `addiu gp,gp,6456` at `0x800FB3E4`). +- The harness rewrites `gp`-marked symbols' macro accesses to explicit `%gp_rel(sym)($gp)`; the linker + resolves `R_MIPS_GPREL16` against `_gp`. +- Registered `0x80012780..0x8001278C` (`src/func_80012780.c`) — the first `gp`-relative match. +- Full evidence and limits in `docs/PHASE6_SMALL_DATA.md`. + +**Findings:** + +| Question | Byte-evidenced answer | +|---|---| +| `gp` base | `0x80121938` (crt0, `0x800FB3E4`) | +| How many `gp`-relative accesses | 3,976 over 1,286 distinct addresses, spanning ≈64 KB | +| Is the threshold recoverable? | **No** — `0x80122354` is absolute and `0x80121974` is `gp`-relative, both within ±32 KB of `gp`; the choice is the object's size/section, which is unobservable | +| Does `-mgpopt` matter? | Not observable: `-mgpopt`/`-mno-gpopt` (and `-G0..-G32`) give identical `cc1` output | + +**Verification:** + +| Check | Result | +|---|---| +| `range --symbols config/symbols.tsv` for `func_80012780` | 12 bytes, 0 differing, `result=MATCH` | +| `make gate` | `c_regions=4`, 0 differing, SHA-1 `e173426c…`, exit 0 | +| Synthetic suite | 71 tests pass (gp-marker parsing, unknown-marker rejection, end-to-end gp rewrite) | + +**Limits:** `gp` symbols are identified from the original's access form, not a recovered symbol table; +the numeric `-G` is unresolved and unnecessary under this mechanism; the `la`-of-a-`gp`-symbol rewrite +is implemented but not yet exercised on a real function. + +**Rules check — re-read complete. Continuing with P6-T5.** diff --git a/src/func_80012780.c b/src/func_80012780.c new file mode 100644 index 0000000..21fe8ed --- /dev/null +++ b/src/func_80012780.c @@ -0,0 +1,32 @@ +/* + * func_80012780 — 12 bytes at 0x80012780..0x8001278C + * + * Byte-identical reconstruction of a leaf setter that stores its argument to a + * **gp-relative** global. This is the project's first match whose bytes depend + * on the small-data (`gp`) addressing form. + * + * The observed instructions are: + * sw a0,60(gp) D_80121974 = x + * jr ra + * nop + * + * `gp` is 0x80121938: crt0 loads it at 0x800FB3E4 (`lui gp,0x8012` + + * `addiu gp,gp,6456`). The target is therefore 0x80121974. + * + * GNU `as` only turns the `sw rt,sym` macro into `%gp_rel` when it can see the + * symbol defined in `.sdata`/`.sbss`. Here the data is the untouched payload + * fallback, so the harness rewrites the access to explicit `%gp_rel` for + * symbols marked `gp` in `config/symbols.tsv`; the linker resolves + * R_MIPS_GPREL16 against `_gp`. + * + * LIMITS: the function name and the global's name and type are hypotheses + * reconstructed from the disassembly; only the compiled bytes are evidence. + * The global's size is unknown, so this match does not by itself pin the + * original `-G` threshold (see docs/PHASE6_SMALL_DATA.md). + */ + +extern int D_80121974; + +void func_80012780(int x) { + D_80121974 = x; +} diff --git a/tools/sf3_match b/tools/sf3_match index 366d90a..5cae6b8 100755 --- a/tools/sf3_match +++ b/tools/sf3_match @@ -231,30 +231,48 @@ def parse_regions(text: str) -> list[Region]: return regions -# A tracked symbol registry: `NAMEaddress` rows naming absolute addresses. +# A tracked symbol registry: `NAMEaddress[gp]` rows naming absolute +# addresses. The optional `gp` marker forces a gp-relative access. _SYMBOL_NAME = re.compile(r"[A-Za-z_.][A-Za-z0-9_.]*\Z") -def parse_symbols(text: str) -> list[str]: - """Parse a symbol registry into assembler `--defsym NAME=0xADDR` arguments.""" +@dataclass(frozen=True) +class SymbolTable: + defsyms: tuple[str, ...] = () + gp_names: frozenset[str] = frozenset() + + +def parse_symbols(text: str) -> SymbolTable: + """Parse a symbol registry into linker `--defsym` args and gp markers. + + Columns are `NAMEaddress[gp]`. A `gp` marker means the original + accessed the symbol `gp`-relative, so the harness rewrites its macro + accesses to explicit `%gp_rel` (GNU `as` will not do this for a symbol whose + section it cannot see). + """ defsyms: list[str] = [] + gp_names: set[str] = set() seen: set[str] = set() for number, raw in enumerate(text.splitlines(), 1): line = raw.split("#", 1)[0].strip() if not line: continue fields = line.split() - if len(fields) != 2: - raise ToolError(f"symbols line {number}: expected 'NAME address'") + if len(fields) not in (2, 3): + raise ToolError(f"symbols line {number}: expected 'NAME address [gp]'") name = fields[0] if not _SYMBOL_NAME.match(name): raise ToolError(f"symbols line {number}: invalid symbol name {name!r}") if name in seen: raise ToolError(f"symbols line {number}: duplicate symbol {name!r}") address = parse_hex_address(fields[1], number) + if len(fields) == 3: + if fields[2] != "gp": + raise ToolError(f"symbols line {number}: unknown marker {fields[2]!r}") + gp_names.add(name) seen.add(name) defsyms.append(f"{name}=0x{address:X}") - return defsyms + return SymbolTable(tuple(defsyms), frozenset(gp_names)) def validate_regions(regions: Sequence[Region], exe: PsxExe) -> None: @@ -390,10 +408,49 @@ class Toolchain: cc1_flags: Sequence[str] as_flags: Sequence[str] defsyms: Sequence[str] + gp_symbols: frozenset[str] = frozenset() maspsx: Path | None = None aspsx_version: str = DEFAULT_ASPSX_VERSION +# A symbol macro access that can be forced gp-relative. +_GP_ACCESS = re.compile( + r"^(?P\s*)(?Plw|lh|lhu|lb|lbu|lwl|lwr|sw|sh|sb|swl|swr)" + r"\s+(?P\$[A-Za-z0-9]+),\s*(?P[A-Za-z_.][A-Za-z0-9_.]*)" + r"(?P[+-][0-9]+)?\s*$" +) +_GP_LA = re.compile( + r"^(?P\s*)la\s+(?P\$[A-Za-z0-9]+),\s*" + r"(?P[A-Za-z_.][A-Za-z0-9_.]*)(?P[+-][0-9]+)?\s*$" +) + + +def rewrite_gp_accesses(text: str, gp_symbols: frozenset[str]) -> str: + """Force `%gp_rel` access for symbols the original read through `gp`.""" + if not gp_symbols: + return text + lines: list[str] = [] + for line in text.splitlines(): + match = _GP_ACCESS.match(line) + if match and match.group("sym") in gp_symbols: + symbol = match.group("sym") + (match.group("addend") or "") + lines.append( + f"{match.group('indent')}{match.group('op')} " + f"{match.group('rt')},%gp_rel({symbol})($gp)" + ) + continue + match = _GP_LA.match(line) + if match and match.group("sym") in gp_symbols: + symbol = match.group("sym") + (match.group("addend") or "") + lines.append( + f"{match.group('indent')}addiu {match.group('rt')}," + f"$gp,%gp_rel({symbol})" + ) + continue + lines.append(line) + return "\n".join(lines) + ("\n" if text.endswith("\n") else "") + + def compile_c(source: Path, out_object: Path, work: Path, tools: Toolchain) -> None: """Preprocess, compile, ASPSX-emulate and assemble one C source. @@ -415,6 +472,13 @@ def compile_c(source: Path, out_object: Path, work: Path, tools: Toolchain) -> N assembly, transformed, ) assembly = transformed + if tools.gp_symbols: + rewritten = work / (out_object.stem + ".gp.s") + rewritten.write_text( + rewrite_gp_accesses(assembly.read_text(encoding="utf-8"), tools.gp_symbols), + encoding="utf-8", + ) + assembly = rewritten run([str(tools.assembler), *tools.as_flags, "-o", str(out_object), str(assembly)]) @@ -680,9 +744,12 @@ def resolve_toolchain(args: argparse.Namespace) -> Toolchain: raise ToolError("no preprocessor found; pass --cpp") cpp = Path(located) defsyms = list(args.defsym) + gp_names: set[str] = set() if args.symbols is not None: symbols_path = require_file(args.symbols, "symbol registry") - defsyms = parse_symbols(symbols_path.read_text(encoding="utf-8")) + defsyms + table = parse_symbols(symbols_path.read_text(encoding="utf-8")) + defsyms = list(table.defsyms) + defsyms + gp_names |= table.gp_names maspsx = None if not args.no_maspsx: maspsx = require_file(args.maspsx, "maspsx") @@ -696,6 +763,7 @@ def resolve_toolchain(args: argparse.Namespace) -> Toolchain: cc1_flags=args.cc1_flag or DEFAULT_CC1_FLAGS, as_flags=args.as_flag or DEFAULT_AS_FLAGS, defsyms=defsyms, + gp_symbols=frozenset(gp_names), maspsx=maspsx, aspsx_version=args.aspsx_version, ) diff --git a/tools/tests/test_sf3_match.py b/tools/tests/test_sf3_match.py index 06a651a..1415f0d 100644 --- a/tools/tests/test_sf3_match.py +++ b/tools/tests/test_sf3_match.py @@ -199,10 +199,20 @@ class RegionOptionTests(unittest.TestCase): class SymbolTests(unittest.TestCase): def test_parses_names_comments_and_hex(self) -> None: - defsyms = sf3_match.parse_symbols( + table = sf3_match.parse_symbols( "# comment\n\ng_80122354\t0x80122354\nfunc_1\t80123456\n" ) - self.assertEqual(defsyms, ["g_80122354=0x80122354", "func_1=0x80123456"]) + self.assertEqual(list(table.defsyms), + ["g_80122354=0x80122354", "func_1=0x80123456"]) + self.assertEqual(table.gp_names, frozenset()) + + def test_parses_a_gp_marker(self) -> None: + table = sf3_match.parse_symbols("g\t0x80121974\tgp\nother\t0x80122354\n") + self.assertEqual(table.gp_names, frozenset({"g"})) + + def test_rejects_an_unknown_marker(self) -> None: + with self.assertRaises(sf3_match.ToolError): + sf3_match.parse_symbols("g\t0x80121974\tbig\n") def test_rejects_a_malformed_line(self) -> None: with self.assertRaises(sf3_match.ToolError): @@ -240,7 +250,7 @@ int synthetic_get(Synthetic *p) { return p->value; } class EndToEndTests(unittest.TestCase): """Build a synthetic executable whose payload starts with real compiler output.""" - def _toolchain(self, *, cc1_flags=(), as_flags=(), defsyms=()) -> object: + def _toolchain(self, *, cc1_flags=(), as_flags=(), defsyms=(), symbols=None) -> object: args = type("Args", (), {})() args.cpp = None args.cc1 = sf3_match.DEFAULT_CC1 @@ -251,7 +261,7 @@ class EndToEndTests(unittest.TestCase): args.cc1_flag = list(cc1_flags) args.as_flag = list(as_flags) args.defsym = list(defsyms) - args.symbols = None + args.symbols = symbols args.maspsx = sf3_match.DEFAULT_MASPSX args.no_maspsx = False args.aspsx_version = sf3_match.DEFAULT_ASPSX_VERSION @@ -259,13 +269,14 @@ class EndToEndTests(unittest.TestCase): def _compile_payload( self, root: Path, *, source_text: str = SOURCE, cc1_flags=(), - as_flags=(), defsyms=(), corrupt: bool = False, + as_flags=(), defsyms=(), symbols=None, corrupt: bool = False, ) -> tuple[Path, int]: source = root / "synthetic.c" source.write_text(source_text, encoding="ascii") work = root / "work" obj = root / "synthetic.o" - tools = self._toolchain(cc1_flags=cc1_flags, as_flags=as_flags, defsyms=defsyms) + tools = self._toolchain(cc1_flags=cc1_flags, as_flags=as_flags, + defsyms=defsyms, symbols=symbols) sf3_match.compile_c(source, obj, work, tools) code = sf3_match.link_object_bytes(obj, PAYLOAD, work, tools) payload = bytearray(max(0x40, len(code) + 0x10)) @@ -354,6 +365,26 @@ class EndToEndTests(unittest.TestCase): self.assertEqual(rc, 0) self.assertEqual((out / "scus_946_40.rebuilt").read_bytes(), exe.read_bytes()) + def test_gp_marker_forces_a_gp_relative_access(self) -> None: + source = "extern int g_gp;\nvoid synthetic_gp(int x) { g_gp = x; }\n" + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + symbols = self._symbols(root, "g_gp\t0x80121974\tgp\n_gp\t0x80121938\n") + exe, code_length = self._compile_payload( + root, source_text=source, symbols=symbols + ) + regions = self._registry(root, code_length) + out = root / "out" + rc = sf3_match.main(["gate", "--exe", str(exe), "--regions", str(regions), + "--symbols", str(symbols), "--out", str(out)]) + self.assertEqual(rc, 0) + self.assertEqual((out / "scus_946_40.rebuilt").read_bytes(), exe.read_bytes()) + # Without the gp marker the access is absolute and the gate fails. + plain = self._symbols(root, "g_gp\t0x80121974\n_gp\t0x80121938\n") + rc = sf3_match.main(["gate", "--exe", str(exe), "--regions", str(regions), + "--symbols", str(plain), "--out", str(root / "out2")]) + self.assertEqual(rc, 1) + def test_refuses_an_existing_destination(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp)