// SPDX-License-Identifier: GPL-3.0-or-later #include "prepare.h" #include "elf.h" #include #include #include #include #include namespace start { namespace { constexpr std::uint64_t kLimit = 512ull << 20; const char* const kSupportedTitle = "CUSA03173"; const char* const kSupportedVersion = "01.09"; const char* const kSupportedImage = "071df19c8880086d97182dbc057bc8cb37badaca57d9112683836b24a0444c0a"; constexpr std::uint64_t kChunk = 1 << 16; constexpr std::size_t kPatchedChunks = 32; bool IsLoad(const Ph& p) { return p.type == 1 || p.type == 0x61000010; } std::optional DifferingChunks(const Bytes& image, const fs::path& engine) { std::vector expected; try { for (const std::string& line : Split(ReadText(engine / "data" / "image_109_hashes.txt"), '\n')) { const std::string word = Trim(line); if (word.size() == 16 && word[0] != '#') expected.push_back(word); } } catch (const Error&) { return std::nullopt; } if ((image.size() + kChunk - 1) / kChunk != expected.size()) return std::nullopt; std::size_t differing = 0; for (std::size_t i = 0; i < expected.size(); ++i) { const std::size_t at = i * kChunk, n = std::min(kChunk, image.size() - at); if (Sha256Hex(image.data() + at, n).substr(0, 16) != expected[i]) ++differing; } return differing; } bool checked_code_109 = false, game_check_skipped = false; void CheckGame(const fs::path& game, const Bytes& image, const fs::path& engine) { checked_code_109 = false; game_check_skipped = Env("BB_SKIP_GAME_CHECK", "") == "1"; if (game_check_skipped) return; std::string title = "?", version = "?"; try { const auto sfo = ParseSfo(ReadFile(game / "sce_sys" / "param.sfo")); title = SfoText(sfo, "TITLE_ID"); version = SfoText(sfo, "APP_VER"); } catch (const Error&) { } if (Sha256Hex(image) == kSupportedImage) { checked_code_109 = true; return; } if (const auto n = DifferingChunks(image, engine); n && *n <= kPatchedChunks) { checked_code_109 = true; Log("Game check: eboot.bin is 1.09 with %zu of %zu 64 KiB pieces changed (a patched eboot, e.g. a 60 FPS " "package); starting anyway. If the game crashes, use the clean eboot.bin from the dumped 1.09 update.", *n, std::size_t((image.size() + kChunk - 1) / kChunk)); return; } const std::string found = "Found " + title + " version " + version + " (sce_sys/param.sfo)."; if (title != kSupportedTitle) { Log("Game check: %s Works best with Bloodborne CUSA03173 (update 1.09): this edition's executable differs, " "so it starts without the FPS, resolution and engine patches made for that one (30 FPS, the game's own " "settings). Expect missing features; crashes here are not supported yet.", found.c_str()); return; } if (version != kSupportedVersion) throw GameCheckError(found + " Lantern Engine needs the 1.09 update merged into the game folder: copy everything " "from the dumped 1.09 update (e.g. CUSA03173-patch) into the game folder, " "replacing files (eboot.bin and sce_sys too)."); throw GameCheckError(found + " param.sfo is from 1.09 but eboot.bin is not: copy eboot.bin from the dumped " "1.09 update into the game folder, replacing the old one."); } Executable ReadExecutable(const fs::path& path, const char* shown) { const Bytes data = ReadFile(path); try { if (data.size() < 64) throw Error(data.empty() ? "it is empty (0 bytes)" : "it is too small"); return ParseSelf(data); } catch (const GameCheckError&) { throw; } catch (const Error& e) { throw GameCheckError(std::string(shown) + " is damaged (" + e.what() + "). Copy it again from your game dump or the dumped 1.09 update (both if unsure), " "and check that your antivirus has not quarantined or cut it."); } } struct Identity { std::string nid; std::string library; std::uint16_t library_version = 0; std::string module; std::uint16_t module_version = 0; bool operator==(const Identity&) const = default; }; struct IdentityHash { std::size_t operator()(const Identity& id) const { return std::hash{}(id.nid + '#' + id.library + '#' + id.module) ^ (std::size_t(id.library_version) << 16 | id.module_version); } }; struct Symbol { std::string name; std::uint8_t type, binding; std::uint16_t section; std::uint64_t value, size; std::optional identity; }; struct Relocation { std::uint64_t target; std::uint32_t kind, symbol; std::int64_t addend; }; struct Module { Executable exe; std::map tags; std::vector> dynamic; std::vector symbols; std::vector relocs; std::map> libraries, modules; Bytes blob; std::size_t strings_at = 0, strings_size = 0; std::string String(std::uint64_t offset) const { if (offset >= strings_size) throw Error("string out of bounds"); const auto* begin = blob.data() + strings_at + offset; const auto* end = blob.data() + strings_at + strings_size; const auto* nul = std::find(begin, end, 0); if (nul == end) throw Error("unterminated string"); return std::string(begin, nul); } std::optional IdentityOf(const std::string& name) const { const auto parts = Split(name, '#'); if (parts.size() != 3) return std::nullopt; const auto lib = libraries.find(parts[1]); const auto mod = modules.find(parts[2]); if (lib == libraries.end() || mod == modules.end()) throw Error("unknown library or module in " + name); return Identity{parts[0], lib->second.first, lib->second.second, mod->second.first, mod->second.second}; } const Ph* Find(std::uint32_t type) const { for (const Ph& p : exe.ph) if (p.type == type) return &p; return nullptr; } }; std::string EncodeId(std::uint64_t value) { static const char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-"; std::string out(1, alphabet[value & 63]); value >>= 6; while (value) { out.insert(out.begin(), alphabet[value & 63]); value >>= 6; } return out; } Module ReadModule(Executable exe, bool stop_at_null) { Module m; m.exe = std::move(exe); const Ph* dp = m.Find(2); if (!dp) throw Error("no dynamic segment"); for (std::uint64_t pos = dp->offset; pos < dp->offset + dp->filesz; pos += 16) { const std::uint8_t* e = Span(m.exe.elf, pos, 16); const std::uint64_t tag = Load(e), value = Load(e + 8); if (stop_at_null && !tag) break; m.dynamic.emplace_back(tag, value); m.tags[tag] = value; } const Ph* lp = m.Find(0x61000000); if (!lp) throw Error("no dynamic library data"); const std::uint8_t* blob = Span(m.exe.elf, lp->offset, lp->filesz); m.blob.assign(blob, blob + lp->filesz); const auto tag = [&](std::uint64_t t) { const auto it = m.tags.find(t); if (it == m.tags.end()) throw Error("missing dynamic tag " + Hex(t)); return it->second; }; m.strings_at = tag(0x61000035); m.strings_size = tag(0x61000037); Span(m.blob, m.strings_at, m.strings_size); for (const auto& [t, value] : m.dynamic) { if (t == 0x61000013 || t == 0x61000015 || t == 0x6100000d || t == 0x6100000f) { auto& table = (t == 0x61000013 || t == 0x61000015) ? m.libraries : m.modules; const std::string key = EncodeId(value >> 48); std::pair identity{m.String(value & 0xffffffff), std::uint16_t(value >> 32)}; const auto it = table.find(key); if (it != table.end() && it->second != identity) throw Error("conflicting module/library IDs"); table[key] = identity; } } const std::uint64_t syms_at = tag(0x61000039), syms_size = tag(0x6100003f); const std::uint8_t* syms = Span(m.blob, syms_at, syms_size); for (std::uint64_t pos = 0; pos + 24 <= syms_size; pos += 24) { const std::uint8_t* s = syms + pos; Symbol symbol; symbol.name = m.String(Load(s)); symbol.type = s[4] & 15; symbol.binding = s[4] >> 4; symbol.section = Load(s + 6); symbol.value = Load(s + 8); symbol.size = Load(s + 16); if (!symbol.name.empty()) symbol.identity = m.IdentityOf(symbol.name); m.symbols.push_back(std::move(symbol)); } for (const auto& [offset_tag, size_tag] : {std::pair{0x61000029ull, 0x6100002dull}, std::pair{0x6100002full, 0x61000031ull}}) { const std::uint64_t at = tag(offset_tag), size = tag(size_tag); const std::uint8_t* table = Span(m.blob, at, size); for (std::uint64_t pos = 0; pos + 24 <= size; pos += 24) { const std::uint64_t info = Load(table + pos + 8); m.relocs.push_back({Load(table + pos), std::uint32_t(info), std::uint32_t(info >> 32), Load(table + pos + 16)}); } } return m; } struct Segment { std::uint64_t address, size, flags; }; struct Reloc { std::uint64_t target, kind; std::int64_t value, addend; }; struct Boot { Bytes image; std::uint64_t entry = 0, flags = 0; std::vector segments; std::vector names; std::vector relocs; }; std::string Nid(const std::string& name) { static const std::uint8_t salt[] = {0x51, 0x8d, 0x64, 0xa6, 0x35, 0xde, 0xd8, 0xc1, 0xe6, 0xb0, 0x39, 0xb1, 0xc3, 0xe5, 0x52, 0x30}; std::string input = name; input.append(reinterpret_cast(salt), sizeof salt); const Bytes digest = Sha1(input.data(), input.size()); std::uint8_t eight[8]; for (int i = 0; i < 8; ++i) eight[i] = digest[7 - i]; static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; std::string out; std::uint32_t bits = 0; int count = 0; for (std::uint8_t byte : eight) { bits = bits << 8 | byte; count += 8; while (count >= 6) { out += b64[(bits >> (count - 6)) & 63]; count -= 6; } } if (count) out += b64[(bits << (6 - count)) & 63]; std::replace(out.begin(), out.end(), '/', '-'); return out; } bool InitEnvIsRet(const fs::path& libc) { Module m = ReadModule(ReadExecutable(libc, "sce_module/libc.prx"), false); const std::string init_env = Nid("_init_env"); bool is_ret = false; for (const Symbol& s : m.symbols) { if (Split(s.name, '#')[0] != init_env || !s.section || s.type != 2) continue; const Ph* p = nullptr; for (const Ph& q : m.exe.ph) if (q.type == 1 && (q.flags & 1) && q.vaddr <= s.value && s.value < q.vaddr + q.filesz) { p = &q; break; } if (!p) throw Error("libc _init_env outside its code"); const std::uint8_t* code = Span(m.exe.elf, p->offset + s.value - p->vaddr, s.size); is_ret = s.size == 1 && code[0] == 0xc3; } return is_ret; } Boot PrepareBoot(const fs::path& game, Module& main, const fs::path& engine) { Boot boot; std::vector loads; for (const Ph& p : main.exe.ph) if (IsLoad(p)) loads.push_back(&p); const auto mapped = [&](std::uint64_t address, std::uint64_t size = 8) { return std::any_of(loads.begin(), loads.end(), [&](const Ph* p) { return p->vaddr <= address && address + size <= p->vaddr + p->memsz; }); }; std::uint64_t size = 0; for (const Ph* p : loads) size = std::max(size, p->vaddr + p->memsz); if (size > kLimit || !mapped(main.exe.entry, 1)) throw Error("invalid memory image"); boot.image.assign(size, 0); for (const Ph* p : loads) { if (p->filesz > p->memsz) throw Error("segment file size exceeds memory size"); const std::uint8_t* data = Span(main.exe.elf, p->offset, p->filesz); std::copy_n(data, p->filesz, boot.image.begin() + std::ptrdiff_t(p->vaddr)); } CheckGame(game, boot.image, engine); boot.entry = main.exe.entry; for (const Ph* p : loads) boot.segments.push_back({p->vaddr, std::min((p->memsz + 0x3fffu) & ~std::uint64_t(0x3fff), size - p->vaddr), p->flags}); std::map imports; std::vector import_order; for (const Relocation& r : main.relocs) { if (!mapped(r.target)) throw Error("relocation outside mapped segments: " + Hex(r.target)); if (r.kind == 8) { boot.relocs.push_back({r.target, 0, r.addend, 0}); } else if (r.kind == 1 || r.kind == 6 || r.kind == 7) { const Symbol& symbol = main.symbols.at(r.symbol); if (symbol.section) { if (symbol.section == 0xfff1) Store(boot.image.data() + r.target, symbol.value + std::uint64_t(r.addend)); else boot.relocs.push_back({r.target, 0, std::int64_t(symbol.value) + r.addend, 0}); } else { if (!imports.count(r.symbol)) { imports[r.symbol] = import_order.size(); import_order.push_back(r.symbol); } const std::uint64_t kind = symbol.type == 1 ? 2 : 1; if ((kind == 1 && r.addend) || r.addend < 0 || r.addend >= 4096) throw Error("unsupported import addend"); boot.relocs.push_back({r.target, kind, std::int64_t(imports[r.symbol]), r.addend}); } } else { throw Error("unsupported relocation " + std::to_string(r.kind)); } } for (const std::uint32_t index : import_order) boot.names.push_back(main.symbols[index].name); boot.flags = InitEnvIsRet(game / "sce_module" / "libc.prx") ? 1 : 0; std::string title = "?"; try { title = SfoText(ParseSfo(ReadFile(game / "sce_sys" / "param.sfo")), "TITLE", "None"); } catch (const Error&) { } std::size_t total_relocs = main.relocs.size(), needed = 0; for (const auto& [t, v] : main.dynamic) needed += t == 1; Log("%s | entry=%s | image=%s bytes", title.c_str(), Hex(boot.entry).c_str(), [&] { std::string n = std::to_string(size); for (int i = int(n.size()) - 3; i > 0; i -= 3) n.insert(std::size_t(i), ","); return n; }().c_str()); Log("%zu imported symbols; %zu relocations; %zu required modules", boot.names.size(), total_relocs, needed); Log("libc _init_env verified RET: %s", boot.flags ? "True" : "False"); return boot; } const Bytes kFsLoad = {0x64, 0x48, 0x8b, 0x04, 0x25, 0x00, 0x00, 0x00, 0x00}; const Bytes kLeaDown = {0x48, 0x8d, 0x64, 0x24, 0x80}; const Bytes kLeaUp = {0x48, 0x8d, 0xa4, 0x24, 0x80, 0x00, 0x00, 0x00}; int PatchFsLoads(Bytes& image, const std::vector& ph, std::uint64_t base) { int patched = 0; for (const Ph& p : ph) { if (p.type != 1 || !(p.flags & 1)) continue; const std::uint64_t start = base + p.vaddr, end = std::min(base + p.vaddr + p.filesz, image.size()); auto from = image.begin() + std::ptrdiff_t(start); const auto stop = image.begin() + std::ptrdiff_t(end); while (from < stop) { const auto at = std::search(from, stop, kFsLoad.begin(), kFsLoad.end()); if (at == stop) break; *at = 0x65; ++patched; from = at + std::ptrdiff_t(kFsLoad.size()); } } return patched; } Bytes ProtectRedZones(Bytes& image, const nlohmann::json& sites, std::uint64_t base, const std::vector& skip, int& applied) { Bytes code; for (const auto& site : sites) { const std::uint64_t offset = site.at(0).get(); const Bytes original = FromHex(site.at(1).get()); const std::uint64_t end = offset + original.size(); const auto next_reloc = std::upper_bound(skip.begin(), skip.end(), offset - 8); if (end > image.size() || !std::equal(original.begin(), original.end(), image.begin() + std::ptrdiff_t(offset)) || (next_reloc != skip.end() && *next_reloc < end)) continue; const std::uint64_t here = base + code.size(); std::uint64_t pos = offset; for (const auto& length_json : site.at(2)) { const std::int64_t length = length_json.get(); const std::uint64_t n = std::uint64_t(length < 0 ? -length : length); const auto body = image.begin() + std::ptrdiff_t(pos); if (length < 0) code.insert(code.end(), kLeaDown.begin(), kLeaDown.end()); code.insert(code.end(), body, body + std::ptrdiff_t(n)); if (length < 0) code.insert(code.end(), kLeaUp.begin(), kLeaUp.end()); pos += n; } code.push_back(0xe9); Append(code, std::int32_t(std::int64_t(end) - std::int64_t(base + code.size() + 4))); while (code.size() % 16) code.push_back(0xcc); image[offset] = 0xe9; Store(image.data() + offset + 1, std::int32_t(std::int64_t(here) - std::int64_t(offset + 5))); std::fill(image.begin() + std::ptrdiff_t(offset + 5), image.begin() + std::ptrdiff_t(end), 0xcc); ++applied; } return code; } Bytes RewriteRedZoneFunctions(Bytes& image, const nlohmann::json& functions, std::uint64_t base, const std::vector& skip, int& applied) { Bytes code; for (const auto& f : functions) { const std::uint64_t start = f.at("offset").get(); const std::uint64_t size = f.at("size").get(); const std::uint64_t end = start + size; const auto next_reloc = std::upper_bound(skip.begin(), skip.end(), start - 8); if (size < 5 || end > image.size() || Sha256Hex(image.data() + start, size) != f.at("sha256").get() || (next_reloc != skip.end() && *next_reloc < end)) continue; const std::uint64_t here = base + code.size(); Bytes body; std::unordered_map at; std::vector> jumps; std::uint64_t pos = 0; bool bad = false; for (const auto& item : f.at("insns")) { const std::uint64_t n = item.at(0).get(); const int kind = item.size() > 1 ? item.at(1).get() : 0; if (pos + n > size) { bad = true; break; } Bytes insn(image.begin() + std::ptrdiff_t(start + pos), image.begin() + std::ptrdiff_t(start + pos + n)); at[pos] = body.size(); if (kind == 1) { const std::size_t modrm = item.at(2).get(), disp = item.at(3).get(); const int width = item.at(4).get(); const std::int32_t old = width == 0 ? 0 : width == 1 ? std::int32_t(std::int8_t(insn[disp])) : Load(insn.data() + disp); const std::int32_t value = old + 0x80; if (width == 1 && value >= -128 && value <= 127) { insn[disp] = std::uint8_t(value); } else if (width == 4) { Store(insn.data() + disp, value); } else { insn[modrm] = std::uint8_t((insn[modrm] & 0x3f) | 0x80); Bytes disp32; Append(disp32, value); insn.erase(insn.begin() + std::ptrdiff_t(disp), insn.begin() + std::ptrdiff_t(disp + std::size_t(width))); insn.insert(insn.begin() + std::ptrdiff_t(disp), disp32.begin(), disp32.end()); } body.insert(body.end(), insn.begin(), insn.end()); } else if (kind == 2) { const std::size_t disp = item.at(2).get(); const std::int64_t target = std::int64_t(start + pos + n) + Load(insn.data() + disp); Store(insn.data() + disp, std::int32_t(target - std::int64_t(here + body.size() + n))); body.insert(body.end(), insn.begin(), insn.end()); } else if (kind == 3) { const int cc = item.at(2).get(); if (cc == 16) { body.push_back(0xe9); } else { body.push_back(0x0f); body.push_back(std::uint8_t(0x80 + cc)); } jumps.push_back({body.size(), item.at(3).get()}); body.insert(body.end(), 4, 0); } else if (kind == 4) { body.insert(body.end(), insn.begin(), insn.end()); body.insert(body.end(), kLeaDown.begin(), kLeaDown.end()); } else if (kind == 5) { body.insert(body.end(), kLeaUp.begin(), kLeaUp.end()); body.insert(body.end(), insn.begin(), insn.end()); } else if (kind == 0) { body.insert(body.end(), insn.begin(), insn.end()); } else { bad = true; break; } pos += n; } for (const auto& jump : jumps) bad = bad || !at.count(jump.second); if (bad || pos != size) { Log("Red zone: the recipe for %s does not fit; left as it is", Hex(start).c_str()); continue; } for (const auto& [where, target] : jumps) Store(body.data() + where, std::int32_t(std::int64_t(at[target]) - std::int64_t(where + 4))); code.insert(code.end(), body.begin(), body.end()); while (code.size() % 16) code.push_back(0xcc); image[start] = 0xe9; Store(image.data() + start + 1, std::int32_t(std::int64_t(here) - std::int64_t(start + 5))); ++applied; } return code; } struct Export { std::uint64_t address, kind; std::string file; }; void LinkModules(const fs::path& game, const fs::path& out, Module& main, Boot boot, const fs::path& engine) { Bytes& image = boot.image; const std::uint64_t size = image.size(); std::vector> order; std::vector index_of; std::unordered_map position; std::optional none_position; const auto set_identity = [&](const std::optional& id, std::size_t index) { if (!id) { if (none_position) { index_of[*none_position] = index; } else { none_position = order.size(); order.push_back(id); index_of.push_back(index); } return; } const auto it = position.find(*id); if (it != position.end()) { index_of[it->second] = index; } else { position[*id] = order.size(); order.push_back(id); index_of.push_back(index); } }; for (std::size_t i = 0; i < boot.names.size(); ++i) set_identity(main.IdentityOf(boot.names[i]), i); std::map, std::string> main_libraries, main_modules; for (const auto& [key, id] : main.libraries) main_libraries[id] = key; for (const auto& [key, id] : main.modules) main_modules[id] = key; const auto imported = [&](const Symbol& symbol) -> std::size_t { if (!symbol.identity) throw Error("import without identity: " + symbol.name); const Identity& id = *symbol.identity; const auto it = position.find(id); if (it != position.end()) return index_of[it->second]; const auto lib = main_libraries.find({id.library, id.library_version}); const auto mod = main_modules.find({id.module, id.module_version}); const std::string name = lib != main_libraries.end() && mod != main_modules.end() ? id.nid + "#" + lib->second + "#" + mod->second : id.nid + "#" + id.library; const std::size_t index = boot.names.size(); set_identity(id, index); boot.names.push_back(name); return index; }; std::unordered_map exports; std::map, std::vector> by_nid; struct Entry { std::string file; std::uint64_t base, size, init, tls_address, tls_memsz, tls_filesz, tls_module; }; std::vector table; std::uint64_t base = (size + 65535) & ~65535ull; int fs_patched = PatchFsLoads(image, main.exe.ph, 0); std::uint64_t tls_module = 2; for (const char* filename : {"libc.prx", "libSceFios2.prx"}) { Module m = ReadModule(ReadExecutable(game / "sce_module" / filename, (std::string("sce_module/") + filename).c_str()), true); std::vector loads; for (const Ph& p : m.exe.ph) if (IsLoad(p)) loads.push_back(&p); std::uint64_t modsize = 0; for (const Ph* p : loads) modsize = std::max(modsize, p->vaddr + p->memsz); if (base + modsize > kLimit) throw Error("linked image exceeds probe limit"); if (image.size() < base + modsize) image.resize(base + modsize, 0); const auto mapped = [&](std::uint64_t address, std::uint64_t length = 8) { return std::any_of(loads.begin(), loads.end(), [&](const Ph* p) { return p->vaddr <= address && address + length <= p->vaddr + p->memsz; }); }; for (const Ph* p : loads) { if (p->filesz > p->memsz) throw Error(std::string(filename) + ": invalid segment size"); std::copy_n(Span(m.exe.elf, p->offset, p->filesz), p->filesz, image.begin() + std::ptrdiff_t(base + p->vaddr)); boot.segments.push_back({base + p->vaddr, p->memsz, p->flags}); } const Ph* tls = m.Find(7); std::uint64_t module_id = 0; if (tls) { if (tls->filesz > tls->memsz || !mapped(tls->vaddr, tls->memsz)) throw Error(std::string(filename) + ": unsupported TLS layout"); module_id = tls_module++; } for (const Relocation& r : m.relocs) { if (!mapped(r.target)) throw Error(std::string(filename) + ": relocation outside image " + Hex(r.target)); const std::uint64_t target = r.target + base; if (r.kind == 8) { boot.relocs.push_back({target, 0, std::int64_t(base) + r.addend, 0}); } else if (r.kind == 16) { const Symbol& symbol = m.symbols.at(r.symbol); if (!symbol.name.empty() || symbol.type != 3 || r.addend || !module_id) throw Error(std::string(filename) + ": unsupported external TLS relocation"); Store(image.data() + target, module_id); } else if (r.kind == 1 || r.kind == 6 || r.kind == 7) { const Symbol& symbol = m.symbols.at(r.symbol); if (symbol.section) { if (symbol.section == 0xfff1) Store(image.data() + target, symbol.value + std::uint64_t(r.addend)); else boot.relocs.push_back({target, 0, std::int64_t(base + symbol.value) + r.addend, 0}); } else if (!symbol.identity && symbol.binding == 2) { Store(image.data() + target, 0); } else { const std::uint64_t kind = symbol.type == 1 ? 2 : 1; if ((kind == 1 && r.addend) || r.addend < 0 || r.addend >= 4096) throw Error(std::string(filename) + ": unsupported import addend"); boot.relocs.push_back({target, kind, std::int64_t(imported(symbol)), r.addend}); } } else { throw Error(std::string(filename) + ": unsupported relocation type " + std::to_string(r.kind)); } } for (const Symbol& s : m.symbols) { if (!s.section || (s.binding != 1 && s.binding != 2) || !s.identity || (s.type != 1 && s.type != 2)) continue; if (!mapped(s.value, std::max(1, s.size))) throw Error(std::string(filename) + ": export outside load segments"); if (exports.count(*s.identity)) throw Error(std::string(filename) + ": ambiguous export"); const Export entry{base + s.value, std::uint64_t(s.type == 1 ? 2 : 1), filename}; exports[*s.identity] = entry; by_nid[{s.identity->nid, s.identity->library}].push_back(entry); } fs_patched += PatchFsLoads(image, m.exe.ph, base); const auto init = m.tags.find(12); table.push_back({filename, base, modsize, base + (init == m.tags.end() ? 0 : init->second), tls ? base + tls->vaddr : 0, tls ? tls->memsz : 0, tls ? tls->filesz : 0, module_id}); base = (base + modsize + 65535) & ~65535ull; } int red_zone_sites = 0; const fs::path sites_path = engine / "patches" / "redzone.json"; const fs::path functions_path = engine / "patches" / "redzone_functions.json"; std::error_code error; std::vector skip; for (const Reloc& r : boot.relocs) skip.push_back(r.target); std::sort(skip.begin(), skip.end()); Bytes code; if (Env("BB_RED_ZONE_FUNCTIONS", "") == "1" && fs::is_regular_file(functions_path, error)) { const auto functions = nlohmann::json::parse(ReadText(functions_path)).at("functions"); int moved = 0; code = RewriteRedZoneFunctions(image, functions, base, skip, moved); Log("Red zone: %d of %zu functions run with their locals above rsp (BB_RED_ZONE_FUNCTIONS=1)", moved, functions.size()); } if (fs::is_regular_file(sites_path, error)) { const auto sites = nlohmann::json::parse(ReadText(sites_path)).at("sites"); const Bytes more = ProtectRedZones(image, sites, base + code.size(), skip, red_zone_sites); code.insert(code.end(), more.begin(), more.end()); } if (!code.empty()) { image.resize(base, 0); image.insert(image.end(), code.begin(), code.end()); boot.segments.push_back({base, code.size(), 5}); } std::vector> bindings; std::size_t unresolved = 0; for (std::size_t i = 0; i < order.size(); ++i) { const auto& id = order[i]; const Export* found = nullptr; if (id) { const auto it = exports.find(*id); if (it != exports.end()) found = &it->second; if (!found && id->library == "libSceLibcInternal") { const auto candidates = by_nid.find({id->nid, "libc"}); if (candidates != by_nid.end() && candidates->second.size() == 1) found = &candidates->second[0]; } } if (found) bindings.push_back({index_of[i], found->address, found->kind}); else ++unresolved; } const Ph* main_tls = main.Find(7); const std::array main_tls_values = main_tls ? std::array{main_tls->vaddr, main_tls->filesz, main_tls->memsz, main_tls->align} : std::array{0, 0, 0, 0}; if (main_tls && (main_tls->filesz > main_tls->memsz || main_tls->memsz > (1u << 20) || main_tls->vaddr + main_tls->filesz > size)) throw Error("unsupported eboot TLS layout"); const Ph* procparam = main.Find(0x61000001); if (!procparam) throw Error("no process parameters"); Bytes linked; linked.reserve(image.size() + boot.relocs.size() * 32 + boot.names.size() * 128 + 4096); Append(linked, "BBPROBE5", 8); for (std::uint64_t v : {std::uint64_t(image.size()), boot.entry, std::uint64_t(boot.segments.size()), std::uint64_t(boot.relocs.size()), std::uint64_t(boot.names.size()), boot.flags}) Append(linked, v); Append(linked, procparam->vaddr); for (std::uint64_t v : main_tls_values) Append(linked, v); Append(linked, table.size()); for (const Entry& t : table) for (std::uint64_t v : {t.base, t.size, t.init, t.tls_address, t.tls_memsz, t.tls_filesz, t.tls_module}) Append(linked, v); Append(linked, bindings.size()); for (const auto& b : bindings) for (std::uint64_t v : b) Append(linked, v); for (const Segment& s : boot.segments) { Append(linked, s.address); Append(linked, s.size); Append(linked, s.flags); } for (const std::string& name : boot.names) { if (name.size() >= 128) throw Error("name too long"); char padded[128] = {}; std::memcpy(padded, name.data(), name.size()); Append(linked, padded, sizeof padded); } for (const Reloc& r : boot.relocs) { Append(linked, r.target); Append(linked, r.kind); Append(linked, r.value); Append(linked, r.addend); } Append(linked, image.data(), image.size()); WriteFile(out / "boot-linked.bin", linked); std::string summary; for (const Entry& t : table) summary += (summary.empty() ? "" : ", ") + t.file + "@" + Hex(t.base); Log("Linked modules: %s; %zu native bindings, %zu imports left to the host runtime, fs->gs patched=%d, " "red-zone-safe stores at %d sites", summary.c_str(), bindings.size(), unresolved, fs_patched, red_zone_sites); } void ContentProfile(const fs::path& game, const fs::path& out, const std::string& sku) { const auto sfo = ParseSfo(ReadFile(game / "sce_sys" / "param.sfo")); std::uint32_t params[4] = {}; for (int i = 0; i < 4; ++i) { const auto it = sfo.find("USER_DEFINED_PARAM_" + std::to_string(i + 1)); if (it == sfo.end()) continue; const auto* value = std::get_if(&it->second); if (!value) throw Error("invalid user-defined parameter"); params[i] = *value; } Bytes data; Append(data, "BBCONT01", 8); Append(data, sku == "trial" ? 1 : 3); for (std::uint32_t v : params) Append(data, v); WriteFile(out / "content.bin", data); Log("AppContent profile: SKU=%s (probe setting), user params=[%u, %u, %u, %u], mounted add-ons=0", sku.c_str(), params[0], params[1], params[2], params[3]); } const char* const kPrepared[] = {"boot-linked.bin", "eboot.elf", "content.bin", "game-code.txt"}; std::string PrepareKey(const fs::path& game, const std::string& sku, const fs::path& engine) { Sha256Stream digest; std::vector paths{game / "eboot.bin", game / "sce_sys" / "param.sfo"}; std::error_code error; std::vector modules; if (fs::is_directory(game / "sce_module", error)) for (const auto& entry : fs::directory_iterator(game / "sce_module", error)) modules.push_back(entry.path()); std::sort(modules.begin(), modules.end()); paths.insert(paths.end(), modules.begin(), modules.end()); wchar_t self[MAX_PATH * 4]; paths.push_back(fs::path(std::wstring(self, GetModuleFileNameW(nullptr, self, DWORD(std::size(self)))))); paths.push_back(engine / "data" / "image_109_hashes.txt"); paths.push_back(engine / "patches" / "redzone.json"); paths.push_back(engine / "patches" / "redzone_functions.json"); for (const fs::path& path : paths) { const std::string name = Utf8(path.filename()); digest.Update(name); digest.Update("\0", 1); if (fs::is_regular_file(path, error)) { const Bytes data = ReadFile(path); digest.Update(data.data(), data.size()); } digest.Update("\0", 1); } digest.Update("sku=" + sku + ";check=" + Env("BB_SKIP_GAME_CHECK", "") + ";redzone_functions=" + Env("BB_RED_ZONE_FUNCTIONS", "")); return digest.HexDigest(); } } void PrepareImage(const fs::path& game, const fs::path& out, const fs::path& engine) { const auto started = std::chrono::steady_clock::now(); const auto elapsed = [&] { return std::chrono::duration(std::chrono::steady_clock::now() - started).count(); }; const std::string sku = Env("BB_CONTENT_SKU", "full"); const fs::path stamp = out / "prepared.json"; const std::string key = PrepareKey(game, sku, engine); std::error_code error; bool reusable = false; try { reusable = nlohmann::json::parse(ReadText(stamp)).value("key", "") == key; } catch (const std::exception&) { } if (reusable && std::all_of(std::begin(kPrepared), std::end(kPrepared), [&](const char* name) { return fs::is_regular_file(out / name, error); })) { Log("Boot image unchanged: reused (%.1f s)", elapsed()); return; } fs::remove(stamp, error); fs::create_directories(out); Module main = ReadModule(ReadExecutable(game / "eboot.bin", "eboot.bin"), true); WriteFile(out / "eboot.elf", main.exe.elf); Boot boot = PrepareBoot(game, main, engine); WriteText(out / "game-code.txt", checked_code_109 ? "1.09\n" : game_check_skipped ? "unchecked\n" : "other\n"); LinkModules(game, out, main, std::move(boot), engine); ContentProfile(game, out, sku); WriteText(stamp, nlohmann::json{{"key", key}}.dump() + "\n"); Log("Boot image prepared in %.1f s (reused on later launches)", elapsed()); } }