Patches/PPU: Extend and improve patching capabilities (code allocations, jumps to any address) (#10779)

* Patches/PPU: Implement dynamic code allocation + Any-Address jump patches

Also fix deallocation path of fixed allocation patches.
This commit is contained in:
Eladash 2021-09-01 13:38:17 +03:00 committed by GitHub
parent ee6e4c493d
commit b40ed5bdb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 339 additions and 72 deletions

View file

@ -38,6 +38,8 @@ void fmt_class_string<patch_type>::format(std::string& out, u64 arg)
{
case patch_type::invalid: return "invalid";
case patch_type::alloc: return "alloc";
case patch_type::code_alloc: return "calloc";
case patch_type::jump: return "jump";
case patch_type::load: return "load";
case patch_type::byte: return "byte";
case patch_type::le16: return "le16";
@ -516,10 +518,23 @@ void patch_engine::append_title_patches(const std::string& title_id)
}
void ppu_register_range(u32 addr, u32 size);
void ppu_register_function_at(u32 addr, u32 size, u64 ptr);
bool ppu_form_branch_to_code(u32 entry, u32 target);
static std::basic_string<u32> apply_modification(const patch_engine::patch_info& patch, u8* dst, u32 filesz, u32 min_addr)
void unmap_vm_area(std::shared_ptr<vm::block_t>& ptr)
{
std::basic_string<u32> applied;
if (ptr && ptr->flags & (1ull << 62))
{
const u32 addr = ptr->addr;
ptr.reset();
vm::unmap(addr, true);
}
}
// Returns old 'applied' size
static usz apply_modification(std::basic_string<u32>& applied, const patch_engine::patch_info& patch, u8* dst, u32 filesz, u32 min_addr)
{
const usz old_applied_size = applied.size();
for (const auto& p : patch.data_list)
{
@ -531,22 +546,22 @@ static std::basic_string<u32> apply_modification(const patch_engine::patch_info&
const u32 alloc_size = utils::align(static_cast<u32>(p.value.long_value) + alloc_at % 4096, 4096);
// Allocate map if needed, if allocated flags will indicate that bit 62 is set (unique identifier)
auto alloc_map = vm::reserve_map(vm::any, alloc_at & -0x10000, utils::align(alloc_size, 0x10000), vm::page_size_64k | vm::preallocated | vm::bf0_0x2 | (1ull << 62));
auto alloc_map = vm::reserve_map(vm::any, alloc_at & -0x10000, utils::align(alloc_size, 0x10000), vm::page_size_64k | vm::preallocated | (1ull << 62));
u64 flags = 0;
u64 flags = vm::page_readable;
switch (p.offset % patch_engine::mem_protection::mask)
{
case patch_engine::mem_protection::wx: flags |= vm::page_writable + vm::page_readable + vm::page_executable; break;
case patch_engine::mem_protection::ro: flags |= vm::page_readable; break;
case patch_engine::mem_protection::rx: flags |= vm::page_writable + vm::page_executable; break;
case patch_engine::mem_protection::rw: flags |= vm::page_writable + vm::page_readable; break;
case patch_engine::mem_protection::wx: flags |= vm::page_writable + vm::page_executable; break;
case patch_engine::mem_protection::ro: break;
case patch_engine::mem_protection::rx: flags |= vm::page_executable; break;
case patch_engine::mem_protection::rw: flags |= vm::page_writable; break;
default: ensure(false);
}
if (alloc_map)
{
if (alloc_map->falloc(alloc_at, alloc_size))
if ((p.alloc_addr = alloc_map->falloc(alloc_at, alloc_size)))
{
vm::page_protect(alloc_at, alloc_size, 0, flags, flags ^ (vm::page_writable + vm::page_readable + vm::page_executable));
@ -560,45 +575,41 @@ static std::basic_string<u32> apply_modification(const patch_engine::patch_info&
}
// Revert if allocated map before failure
if (alloc_map->flags & (1ull << 62))
{
vm::unmap(vm::any, alloc_map->addr);
}
unmap_vm_area(alloc_map);
}
}
// Revert in case of failure
for (u32 index : applied)
std::for_each(applied.begin() + old_applied_size, applied.end(), [&](u32 index)
{
const u32 addr = patch.data_list[index].offset & -4096;
const u32 addr = std::exchange(patch.data_list[index].alloc_addr, 0);
// Try different alignments until works
if (!vm::dealloc(addr))
{
if (!vm::dealloc(addr & -0x10000))
{
vm::dealloc(addr & -0x100000);
}
}
vm::dealloc(addr);
if (auto alloc_map = vm::get(vm::any, addr); alloc_map->flags & (1ull << 62))
{
vm::unmap(vm::any, alloc_map->addr);
}
}
auto alloc_map = vm::get(vm::any, addr);
unmap_vm_area(alloc_map);
});
applied.clear();
return applied;
applied.resize(old_applied_size);
return old_applied_size;
}
// Fixup values from before
std::fill(applied.begin(), applied.end(), u32{umax});
std::fill(applied.begin() + old_applied_size, applied.end(), u32{umax});
u32 relocate_instructions_at = 0;
for (const auto& p : patch.data_list)
{
u32 offset = p.offset;
if (offset < min_addr || offset - min_addr >= filesz)
if (relocate_instructions_at && vm::read32(relocate_instructions_at) != 0x6000'0000u)
{
// No longer points a NOP to be filled, meaning we ran out of instructions
relocate_instructions_at = 0;
}
if (!relocate_instructions_at && (offset < min_addr || offset - min_addr >= filesz))
{
// This patch is out of range for this segment
continue;
@ -608,6 +619,13 @@ static std::basic_string<u32> apply_modification(const patch_engine::patch_info&
auto ptr = dst + offset;
if (relocate_instructions_at)
{
offset = relocate_instructions_at;
ptr = vm::get_super_ptr<u8>(relocate_instructions_at);
relocate_instructions_at += 4; // Advance to the next instruction on dynamic memory
}
u32 resval = umax;
switch (p.type)
@ -623,6 +641,86 @@ static std::basic_string<u32> apply_modification(const patch_engine::patch_info&
// Applied before
continue;
}
case patch_type::code_alloc:
{
relocate_instructions_at = 0;
const u32 out_branch = vm::try_get_addr(dst + (offset & -4)).first;
// Allow only if points to a PPU executable instruction
if (out_branch < 0x10000 || out_branch >= 0x4000'0000 || !vm::check_addr<4>(out_branch, vm::page_executable))
{
continue;
}
const u32 alloc_size = utils::align(static_cast<u32>(p.value.long_value + 1) * 4, 0x10000);
// Always executable
u64 flags = vm::page_executable | vm::page_readable;
switch (p.offset % patch_engine::mem_protection::mask)
{
case patch_engine::mem_protection::rw:
case patch_engine::mem_protection::wx:
{
flags |= vm::page_writable;
break;
}
case patch_engine::mem_protection::ro:
case patch_engine::mem_protection::rx:
{
break;
}
default: ensure(false);
}
const auto alloc_map = ensure(vm::get(vm::any, out_branch));
// Range allowed for absolute branches to operate at
// It takes into account that we need to put a branch for return at the end of memory space
const u32 addr = p.alloc_addr = alloc_map->alloc(alloc_size, nullptr, 0x10000, flags);
if (!addr)
{
patch_log.error("Failed to allocate 0x%x bytes for code (entry=0x%x)", alloc_size, addr, out_branch);
continue;
}
patch_log.success("Allocated 0x%x for code at 0x%x (entry=0x%x)", alloc_size, addr, out_branch);
// NOP filled
std::fill_n(vm::get_super_ptr<u32>(addr), p.value.long_value, 0x60000000);
// Register code
ppu_register_range(addr, alloc_size);
ppu_register_function_at(addr, static_cast<u32>(p.value.long_value), 0);
// Write branch to code
ppu_form_branch_to_code(out_branch, addr);
resval = out_branch & -4;
// Write address of the allocated memory to the code entry
*vm::get_super_ptr<u32>(resval) = addr;
// Write branch to return to code
ppu_form_branch_to_code(addr + static_cast<u32>(p.value.long_value) * 4, resval + 4);
relocate_instructions_at = addr;
break;
}
case patch_type::jump:
{
const u32 out_branch = vm::try_get_addr(dst + (offset & -4)).first;
const u32 dest = static_cast<u32>(p.value.long_value);
// Allow only if points to a PPU executable instruction
if (!ppu_form_branch_to_code(out_branch, dest))
{
continue;
}
resval = out_branch & -4;
break;
}
case patch_type::byte:
{
*ptr = static_cast<u8>(p.value.long_value);
@ -721,7 +819,7 @@ static std::basic_string<u32> apply_modification(const patch_engine::patch_info&
applied.push_back(resval);
}
return applied;
return old_applied_size;
}
std::basic_string<u32> patch_engine::apply(const std::string& name, u8* dst, u32 filesz, u32 min_addr)
@ -812,11 +910,12 @@ std::basic_string<u32> patch_engine::apply(const std::string& name, u8* dst, u32
// Apply modifications sequentially
auto apply_func = [&](const patch_info& patch)
{
auto applied = apply_modification(patch, dst, filesz, min_addr);
const usz old_size = apply_modification(applied_total, patch, dst, filesz, min_addr);
applied_total += applied;
patch_log.success("Applied patch (hash='%s', description='%s', author='%s', patch_version='%s', file_version='%s') (<- %u)", patch.hash, patch.description, patch.author, patch.patch_version, patch.version, applied.size());
if (applied_total.size() != old_size)
{
patch_log.success("Applied patch (hash='%s', description='%s', author='%s', patch_version='%s', file_version='%s') (<- %u)", patch.hash, patch.description, patch.author, patch.patch_version, patch.version, applied_total.size() - old_size);
}
};
// Sort specific patches after global patches
@ -858,6 +957,34 @@ std::basic_string<u32> patch_engine::apply(const std::string& name, u8* dst, u32
return applied_total;
}
void patch_engine::unload(const std::string& name)
{
if (m_map.find(name) == m_map.cend())
{
return;
}
const auto& container = m_map.at(name);
for (const auto& [description, patch] : container.patch_info_map)
{
for (const auto& [title, serials] : patch.titles)
{
for (auto& entry : patch.data_list)
{
// Deallocate used memory
if (u32 addr = std::exchange(entry.alloc_addr, 0))
{
vm::dealloc(addr);
auto alloc_map = vm::get(vm::any, addr);
unmap_vm_area(alloc_map);
}
}
}
}
}
void patch_engine::save_config(const patch_map& patches_map)
{
const std::string path = get_patch_config_path();