Merge pull request #5269 from Nixitov/feat/relinker-weak-imports

fix(tools): do not report weak imports as a title's current stop
This commit is contained in:
Adrià authored and GitHub committed 2026-10-14 20:52:19 +02:00
commit a1b6be7ef0
6 files changed
+55 -13

No files matched your search

@@ -112,6 +112,7 @@ struct CallRegistryEntry {
FileByteOffset TargetOffset;
std::vector<FileByteOffset> CallSites;
bool CallSitesResolved;
bool Weak = false;
};
}
@@ -52,7 +52,8 @@ std::string CallRegistryWriter::WriteCallRegistry(const std::vector<CallRegistry
}
out << "],\n";
out << " \"callSitesResolved\": " << (e.CallSitesResolved ? "true" : "false") << "\n";
out << " \"callSitesResolved\": " << (e.CallSitesResolved ? "true" : "false") << ",\n";
out << " \"weak\": " << (e.Weak ? "true" : "false") << "\n";
out << " }";
if (i + 1 < entries.size()) out << ",";
out << "\n";
@@ -525,6 +525,7 @@ RelinkResult RelinkerPipeline::Relink() {
entry.TargetSection = ".got";
entry.TargetOffset = ref.RelocationAddress;
entry.CallSitesResolved = false;
entry.Weak = ref.Weak;
entries.push_back(std::move(entry));
}
@@ -35,9 +35,28 @@ def main():
entries = json.loads(registry.read_text(encoding="utf-8"))
expected = [{"nid": name, "library": "", "relocationType": "R_X86_64_GLOB_DAT",
"relocationOffset": "0x700", "targetSection": ".got", "targetOffset": "0x300",
"callSites": ["0x200"], "callSitesResolved": True}]
"callSites": ["0x200"], "callSitesResolved": True, "weak": False}]
if entries != expected:
raise AssertionError((name, options, entries))
strings = b"\0lib.so\0" + names[0].encode("utf-8") + b"\0"
image = fixture(str_size=len(strings), table_bytes=strings)
struct.pack_into("<H", image, 56, 6)
struct.pack_into("<IIQQQQQQ", image, 64,
1, 7, 0x200, 0x200, 0x200, len(image) - 0x200, len(image) - 0x200, 0x1000)
for slot in (3, 4, 5):
struct.pack_into("<IIQQQQQQ", image, 64 + slot * 56,
0x6fffff01, 0, 0, 0, 0, 0, 0, 1)
image[0x63C] = 0x20
source = work / "weak.elf"
source.write_bytes(image)
output = work / "weak-windows.exe"
result = subprocess.run([str(relinker), "--skip-sce-module", "--registry", "--windows",
str(source), str(output)], capture_output=True, text=True, timeout=20)
if result.returncode != 0 or not output.exists():
raise AssertionError((result.returncode, result.stdout, result.stderr))
entries = json.loads(output.with_suffix(".registry.json").read_text(encoding="utf-8"))
if not entries or entries[0]["weak"] is not True:
raise AssertionError(("weak", entries))
print("Call registry JSON tests passed")
+9 -6
View File
@@ -385,7 +385,7 @@ def read_registry(path):
nid = entry["nid"].split("#", 1)[0]
if not NID.fullmatch(nid):
raise AuditError(f"{path}: entry {index} has an invalid NID {entry['nid']!r}")
imports.append((nid, entry["library"]))
imports.append((nid, entry["library"], bool(entry.get("weak", False))))
return imports
@@ -408,12 +408,13 @@ def classify(nid, library, providers, stubs, modules):
def audit(imports, exports, stubs, libraries, modules, module_imports=None, module_exports=None):
records = {}
for nid, library in imports:
record = records.setdefault((nid, library), {"nid": nid, "library": library, "references": 0})
for nid, library, weak in imports:
record = records.setdefault((nid, library), {"nid": nid, "library": library, "references": 0, "weak": weak})
record["references"] += 1
record["weak"] = weak
for module, (symbols, needed) in (module_imports or {}).items():
for nid in symbols:
record = records.setdefault((nid, None, module), {"nid": nid, "library": None, "module": module, "references": 0})
record = records.setdefault((nid, None, module), {"nid": nid, "library": None, "module": module, "references": 0, "weak": False})
record["references"] += 1
for record in records.values():
nid, library = record["nid"], record["library"]
@@ -440,9 +441,11 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu
def current_stop(imports, records):
"""The first import the loader cannot resolve, in registry order."""
"""The first import the loader cannot resolve, in registry order; a weak import resolves to null instead."""
classes = {(record["nid"], record["library"]): record for record in records}
for nid, library in imports:
for nid, library, weak in imports:
if weak:
continue
record = classes.get((nid, library))
if record is not None and record["class"] == "absent":
return record
+22 -5
View File
@@ -143,7 +143,13 @@ def write_source(root, library, text):
def write_registry(path, entries, encoding="utf-8"):
path.write_text(json.dumps([{"nid": nid, "library": library, "relocationType": "R_X86_64_GLOB_DAT"} for nid, library in entries]), encoding=encoding)
records = []
for entry in entries:
record = {"nid": entry[0], "library": entry[1], "relocationType": "R_X86_64_GLOB_DAT"}
if len(entry) > 2:
record["weak"] = entry[2]
records.append(record)
path.write_text(json.dumps(records), encoding=encoding)
class Workspace:
@@ -485,6 +491,17 @@ class ImportAuditTests(unittest.TestCase):
self.assertEqual(data["stops"], [{"registry": str(registry), "nid": None, "library": None, "name": None}])
self.assertIn(f" {registry}: none", out)
def test_a_weak_unresolved_import_is_not_a_stop(self):
work = self.work
(work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA"))
registry = work.root / "game.json"
write_registry(registry, [("BBBBBBBBBBB", "libSceA.prx", True), ("CCCCCCCCCCC", "libSceA.prx")])
result = work.root / "audit.json"
code, out, err = work.run(registry, "--json", str(result))
self.assertEqual((code, err), (1, ""))
data = json.loads(result.read_text())
self.assertEqual(data["stops"], [{"registry": str(registry), "nid": "CCCCCCCCCCC", "library": "libSceA.prx", "name": None}])
def test_removing_an_export_flips_the_class(self):
work = self.work
registry = work.root / "game.json"
@@ -517,11 +534,11 @@ class ImportAuditTests(unittest.TestCase):
nid = nid_names.compute_nid("sceTwinCall")
stubs = import_audit.source_stubs(source)
exports = {nid: ["libA.prx", "libB.prx"]}
records, _ = import_audit.audit([(nid, "libB.prx")], {nid: ["libB.prx"]}, stubs, {"libB.prx"}, set())
records, _ = import_audit.audit([(nid, "libB.prx", False)], {nid: ["libB.prx"]}, stubs, {"libB.prx"}, set())
self.assertEqual((records[0]["class"], records[0]["name"]), ("stub", "sceTwinCall"))
records, _ = import_audit.audit([(nid, "libA.prx")], {nid: ["libA.prx"]}, stubs, {"libA.prx"}, set())
records, _ = import_audit.audit([(nid, "libA.prx", False)], {nid: ["libA.prx"]}, stubs, {"libA.prx"}, set())
self.assertEqual((records[0]["class"], records[0]["name"]), ("implemented", None))
records, _ = import_audit.audit([(nid, "libB.prx")], exports, stubs, {"libA.prx", "libB.prx"}, set())
records, _ = import_audit.audit([(nid, "libB.prx", False)], exports, stubs, {"libA.prx", "libB.prx"}, set())
self.assertEqual(records[0]["class"], "implemented")
def test_cmake_shared_library_preserves_stub_names_and_implemented_exports(self):
@@ -889,7 +906,7 @@ class ImportAuditTests(unittest.TestCase):
with self.assertRaisesRegex(import_audit.AuditError, r"entry 1 has an invalid NID 'bad\\x01nid'"):
import_audit.read_registry(path)
write_registry(path, [("AAAAAAAAAAA#A#B", "libSceA.prx")], encoding="utf-8-sig")
self.assertEqual(import_audit.read_registry(path), [("AAAAAAAAAAA", "libSceA.prx")])
self.assertEqual(import_audit.read_registry(path), [("AAAAAAAAAAA", "libSceA.prx", False)])
def test_names_never_download(self):
work = self.work