Files
2026-10-31 15:17:17 +05:00

815 lines
38 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
#include "prepare.h"
#include "elf.h"
#include <windows.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <chrono>
#include <unordered_map>
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<std::size_t> DifferingChunks(const Bytes& image, const fs::path& engine) {
std::vector<std::string> 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<std::size_t>(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<std::string>{}(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> identity;
};
struct Relocation {
std::uint64_t target;
std::uint32_t kind, symbol;
std::int64_t addend;
};
struct Module {
Executable exe;
std::map<std::uint64_t, std::uint64_t> tags;
std::vector<std::pair<std::uint64_t, std::uint64_t>> dynamic;
std::vector<Symbol> symbols;
std::vector<Relocation> relocs;
std::map<std::string, std::pair<std::string, std::uint16_t>> 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<Identity> 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<std::uint64_t>(e), value = Load<std::uint64_t>(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<std::string, std::uint16_t> 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<std::uint32_t>(s));
symbol.type = s[4] & 15;
symbol.binding = s[4] >> 4;
symbol.section = Load<std::uint16_t>(s + 6);
symbol.value = Load<std::uint64_t>(s + 8);
symbol.size = Load<std::uint64_t>(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<std::uint64_t>(table + pos + 8);
m.relocs.push_back({Load<std::uint64_t>(table + pos), std::uint32_t(info), std::uint32_t(info >> 32),
Load<std::int64_t>(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<Segment> segments;
std::vector<std::string> names;
std::vector<Reloc> 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<const char*>(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<const Ph*> 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<std::uint32_t, std::size_t> imports;
std::vector<std::uint32_t> 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<std::uint64_t>(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>& 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<std::uint64_t>(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<std::uint64_t>& skip, int& applied) {
Bytes code;
for (const auto& site : sites) {
const std::uint64_t offset = site.at(0).get<std::uint64_t>();
const Bytes original = FromHex(site.at(1).get<std::string>());
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<std::int64_t>();
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<std::int32_t>(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<std::int32_t>(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<std::uint64_t>& skip, int& applied) {
Bytes code;
for (const auto& f : functions) {
const std::uint64_t start = f.at("offset").get<std::uint64_t>();
const std::uint64_t size = f.at("size").get<std::uint64_t>();
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<std::string>() ||
(next_reloc != skip.end() && *next_reloc < end))
continue;
const std::uint64_t here = base + code.size();
Bytes body;
std::unordered_map<std::uint64_t, std::size_t> at;
std::vector<std::pair<std::size_t, std::uint64_t>> 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<std::uint64_t>();
const int kind = item.size() > 1 ? item.at(1).get<int>() : 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<std::size_t>(), disp = item.at(3).get<std::size_t>();
const int width = item.at(4).get<int>();
const std::int32_t old = width == 0 ? 0
: width == 1 ? std::int32_t(std::int8_t(insn[disp]))
: Load<std::int32_t>(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<std::int32_t>(insn.data() + disp, value);
} else {
insn[modrm] = std::uint8_t((insn[modrm] & 0x3f) | 0x80);
Bytes disp32;
Append<std::int32_t>(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<std::size_t>();
const std::int64_t target = std::int64_t(start + pos + n) + Load<std::int32_t>(insn.data() + disp);
Store<std::int32_t>(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<int>();
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<std::uint64_t>()});
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<std::int32_t>(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<std::int32_t>(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<std::optional<Identity>> order;
std::vector<std::size_t> index_of;
std::unordered_map<Identity, std::size_t, IdentityHash> position;
std::optional<std::size_t> none_position;
const auto set_identity = [&](const std::optional<Identity>& 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::pair<std::string, std::uint16_t>, 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<Identity, Export, IdentityHash> exports;
std::map<std::pair<std::string, std::string>, std::vector<Export>> by_nid;
struct Entry {
std::string file;
std::uint64_t base, size, init, tls_address, tls_memsz, tls_filesz, tls_module;
};
std::vector<Entry> 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<const Ph*> 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<std::uint64_t>(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<std::uint64_t>(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<std::uint64_t>(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<std::uint64_t>(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<std::uint64_t> 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<std::array<std::uint64_t, 3>> 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<std::uint64_t, 4> main_tls_values =
main_tls ? std::array<std::uint64_t, 4>{main_tls->vaddr, main_tls->filesz, main_tls->memsz, main_tls->align}
: std::array<std::uint64_t, 4>{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<std::uint64_t>(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<std::uint64_t>(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<std::uint32_t>(&it->second);
if (!value) throw Error("invalid user-defined parameter");
params[i] = *value;
}
Bytes data;
Append(data, "BBCONT01", 8);
Append<std::uint32_t>(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<fs::path> paths{game / "eboot.bin", game / "sce_sys" / "param.sfo"};
std::error_code error;
std::vector<fs::path> 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<double>(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());
}
}