#include "unwind.h" #include #include namespace raven { namespace { uint64_t load64(const ContextBytes &bytes, size_t at) { uint64_t value; std::memcpy(&value, bytes.data() + at, 8); return value; } void store64(ContextBytes &bytes, size_t at, uint64_t value) { std::memcpy(bytes.data() + at, &value, 8); } uint64_t read64(NativeProcess &process, uint64_t address) { const auto bytes = process.read_memory(address, 8); uint64_t value; std::memcpy(&value, bytes.data(), 8); return value; } constexpr size_t xmm_base = x64_context::float_area + 160; } uint64_t context_register(const ContextBytes &context, size_t number) { return load64(context, x64_context::general + number * 8); } void set_context_register(ContextBytes &context, size_t number, uint64_t value) { store64(context, x64_context::general + number * 8, value); } uint64_t context_rip(const ContextBytes &context) { return load64(context, x64_context::rip); } uint64_t context_rsp(const ContextBytes &context) { return load64(context, x64_context::rsp); } void set_context_rip(ContextBytes &context, uint64_t value) { store64(context, x64_context::rip, value); } void set_context_rsp(ContextBytes &context, uint64_t value) { store64(context, x64_context::rsp, value); } ContextBytes context_from_process(const NativeContext &native) { ContextBytes context(x64_context::size); const auto &r = native.registers; const uint32_t flags = x64_context::full_flags; std::memcpy(context.data() + x64_context::flags, &flags, 4); const uint64_t general[16] = {r.rax, r.rcx, r.rdx, r.rbx, r.rsp, r.rbp, r.rsi, r.rdi, r.r8, r.r9, r.r10, r.r11, r.r12, r.r13, r.r14, r.r15}; std::memcpy(context.data() + x64_context::general, general, sizeof general); store64(context, x64_context::rip, r.rip); const uint32_t flags_register = uint32_t(r.eflags); std::memcpy(context.data() + x64_context::eflags, &flags_register, 4); const uint16_t segments[6] = {uint16_t(r.cs), uint16_t(r.ds), uint16_t(r.es), uint16_t(r.fs), uint16_t(r.gs), uint16_t(r.ss)}; std::memcpy(context.data() + 0x38, segments, sizeof segments); // The FXSAVE image at the start of the XSAVE area has the layout of XMM_SAVE_AREA32. std::memcpy(context.data() + x64_context::float_area, native.xstate.data(), 512); std::memcpy(context.data() + x64_context::mxcsr, native.xstate.data() + 24, 4); // VectorRegister[26] mirrors the YMM-less XMM registers for consumers that read the vector area. return context; } void apply_context(NativeContext &native, const ContextBytes &context) { auto &r = native.registers; uint64_t general[16]; std::memcpy(general, context.data() + x64_context::general, sizeof general); r.rax = general[0]; r.rcx = general[1]; r.rdx = general[2]; r.rbx = general[3]; r.rsp = general[4]; r.rbp = general[5]; r.rsi = general[6]; r.rdi = general[7]; r.r8 = general[8]; r.r9 = general[9]; r.r10 = general[10]; r.r11 = general[11]; r.r12 = general[12]; r.r13 = general[13]; r.r14 = general[14]; r.r15 = general[15]; r.rip = load64(context, x64_context::rip); uint32_t flags; std::memcpy(&flags, context.data() + x64_context::eflags, 4); r.eflags = (flags & 0xcd5) | 0x202; r.orig_rax = uint64_t(-1); std::memcpy(native.xstate.data(), context.data() + x64_context::float_area, 512); } const PEModule *NativeUnwinder::module_at(uint64_t address) const { for (const auto &[name, module] : modules.all()) { const uint64_t base = module->loaded.base(); if (address >= base && address - base < module->image.image_size) return module.get(); } return nullptr; } bool NativeUnwinder::remove_table(uint64_t identifier) { const auto found = std::find_if(tables.begin(), tables.end(), [&](const auto &table) { return table.identifier == identifier; }); if (found == tables.end()) return false; tables.erase(found); return true; } std::optional NativeUnwinder::lookup_dynamic(uint64_t pc) const { for (auto table = tables.rbegin(); table != tables.rend(); ++table) { if (pc < table->base || pc - table->base >= table->length) continue; uint64_t address = 0; if (table->callback) { if (!guest_call) return std::nullopt; address = guest_call(table->callback, pc, table->context); } else { const uint32_t rva = uint32_t(pc - table->base); size_t low = 0, high = table->count; while (low < high && !address) { const size_t middle = (low + high) / 2; const auto row = process.read_memory(table->entries + middle * 12, 8); uint32_t begin, end; std::memcpy(&begin, row.data(), 4); std::memcpy(&end, row.data() + 4, 4); if (rva < begin) high = middle; else if (rva >= end) low = middle + 1; else address = table->entries + middle * 12; } } if (!address) return std::nullopt; const auto row = process.read_memory(address, 12); FunctionEntry entry{address, 0, 0, 0, table->base, nullptr}; std::memcpy(&entry.begin, row.data(), 4); std::memcpy(&entry.end, row.data() + 4, 4); std::memcpy(&entry.unwind, row.data() + 8, 4); return entry; } return std::nullopt; } std::optional NativeUnwinder::lookup(uint64_t pc) const { const auto *module = module_at(pc); if (!module) return lookup_dynamic(pc); if (module->runtime_api) return std::nullopt; const auto &table = module->image.directories[3]; if (!table.address || table.size < 12) return std::nullopt; const uint64_t base = module->loaded.base(); const uint32_t rva = uint32_t(pc - base); auto &mutable_module = const_cast(*module); const auto data = mutable_module.loaded.data(); if (size_t(table.address) + table.size > data.size()) return std::nullopt; const auto *entries = data.data() + table.address; size_t low = 0, high = table.size / 12; while (low < high) { const size_t middle = (low + high) / 2; uint32_t row[3]; std::memcpy(row, entries + middle * 12, 12); if (rva < row[0]) high = middle; else if (rva >= row[1]) low = middle + 1; else { // An entry whose unwind data has its low bit set stands for another entry of the table, the function's primary one. size_t at = table.address + middle * 12; for (int hops = 0; hops < 4 && (row[2] & 1); ++hops) { at = row[2] & ~uint32_t(1); if (at + 12 > data.size()) return std::nullopt; std::memcpy(row, data.data() + at, 12); } return FunctionEntry{base + at, row[0], row[1], row[2], base, module}; } } return std::nullopt; } void NativeUnwinder::unwind_leaf(ContextBytes &context) const { const uint64_t rsp = context_rsp(context); set_context_rip(context, read64(process, rsp)); set_context_rsp(context, rsp + 8); } // Emulates the rest of a function epilogue (add/lea rsp, pops, ret or tail jump) when pc sits inside one. bool NativeUnwinder::epilogue(uint64_t pc, ContextBytes &context, ContextPointers *pointers) const { std::vector code; try { code = process.read_memory(pc, 16); } catch (const std::runtime_error &) { return false; } size_t at = 0; struct Step { enum { adjust, load_rsp, pop } kind; int64_t value; size_t reg; }; std::vector steps; if (code[0] == 0x48 && code[1] == 0x83 && code[2] == 0xc4) { steps.push_back({Step::adjust, int8_t(code[3]), 0}); at = 4; } else if (code[0] == 0x48 && code[1] == 0x81 && code[2] == 0xc4) { int32_t value; std::memcpy(&value, &code[3], 4); steps.push_back({Step::adjust, value, 0}); at = 7; } if (!at && (code[0] == 0x48 || code[0] == 0x4c) && code[1] == 0x8d && ((code[2] & 0xc0) == 0x40 || (code[2] & 0xc0) == 0x80) && ((code[2] >> 3) & 7) == 4) { // lea rsp,[reg+disp] const size_t reg = (code[2] & 7) | ((code[0] & 1) ? 8 : 0); const bool wide = (code[2] & 0xc0) == 0x80; int32_t displacement = wide ? 0 : int8_t(code[3]); if (wide) std::memcpy(&displacement, &code[3], 4); steps.push_back({Step::load_rsp, displacement, reg}); at = wide ? 7 : 4; } for (;;) { if (at >= code.size()) return false; uint8_t byte = code[at]; size_t reg = 0; if (byte == 0x41 && at + 1 < code.size() && (code[at + 1] & 0xf8) == 0x58) { reg = 8 + (code[at + 1] & 7); steps.push_back({Step::pop, 0, reg}); at += 2; } else if ((byte & 0xf8) == 0x58) { steps.push_back({Step::pop, 0, size_t(byte & 7)}); ++at; } else { break; } } const uint8_t terminator = code[at]; const bool tail = terminator == 0xe9 || terminator == 0xeb || (terminator == 0x48 && at + 2 < code.size() && code[at + 1] == 0xff && (code[at + 2] & 0xf8) == 0xe0) || (terminator == 0xff && at + 1 < code.size() && (code[at + 1] & 0xf8) == 0x20); const bool plain_return = terminator == 0xc3 || (terminator == 0xf3 && at + 1 < code.size() && code[at + 1] == 0xc3); if (!plain_return && !tail) return false; // An instruction stream that merely starts like an epilogue is only one when it reaches a return. if (steps.empty() && !plain_return) return false; for (const auto &step : steps) { if (step.kind == Step::adjust) { set_context_rsp(context, context_rsp(context) + uint64_t(step.value)); } else if (step.kind == Step::load_rsp) { set_context_rsp(context, context_register(context, step.reg) + uint64_t(step.value)); } else { const uint64_t rsp = context_rsp(context); set_context_register(context, step.reg, read64(process, rsp)); if (pointers) (*pointers)[16 + step.reg] = rsp; set_context_rsp(context, rsp + 8); } } unwind_leaf(context); return true; } NativeUnwinder::Result NativeUnwinder::unwind(uint32_t handler_type, uint64_t pc, const FunctionEntry &function, ContextBytes &context, ContextPointers *pointers) const { Result result{}; const uint64_t base = function.image_base; // Unwind data at an offset from the image base: in the loaded image of a module, or in the program's memory for code a generator // emitted and described through a function table. const auto load = [&](uint64_t rva, size_t size) { if (!function.module) return process.read_memory(base + rva, size); const auto image = const_cast(*function.module).loaded.data(); if (rva > image.size() || size > image.size() - rva) throw std::runtime_error("native unwind data lies outside its image"); return std::vector(image.begin() + rva, image.begin() + rva + size); }; const uint64_t prolog_offset = pc - (base + function.begin); result.establisher = context_rsp(context); const auto original = context; const auto first = load(function.unwind, 4); const uint32_t frame_register = first[3] & 15, frame_offset = (first[3] >> 4) * 16u; const uint8_t prolog_size = first[1]; bool in_prolog = prolog_offset < prolog_size; if (!in_prolog && epilogue(pc, context, pointers)) { result.establisher = frame_register ? context_register(original, frame_register) - frame_offset : context_rsp(original); return result; } uint64_t limit = in_prolog ? prolog_offset : UINT64_MAX; uint64_t setfp_offset = UINT64_MAX; uint64_t info_rva = function.unwind; for (int depth = 0; depth < 32; ++depth) { const auto header = load(info_rva, 4); // The codes, padded to an even count, then a handler and its data or the parent function's entry. const size_t padded = (size_t(header[2]) + 1) & ~size_t(1); const auto record = load(info_rva, 4 + padded * 2 + ((header[0] >> 3) & 7 ? 12 : 0)); const uint8_t *info = record.data(); const uint8_t version = info[0] & 7, flags = info[0] >> 3, count = info[2]; (void)version; const uint32_t frame_reg = info[3] & 15, frame_off = (info[3] >> 4) * 16u; const uint8_t *codes = info + 4; for (size_t slot = 0; slot < count;) { const uint8_t offset = codes[slot * 2], op = codes[slot * 2 + 1] & 15, op_info = codes[slot * 2 + 1] >> 4; const bool applies = offset <= limit; size_t used = 1; const auto word = [&](size_t index) { uint16_t value; std::memcpy(&value, codes + (slot + index) * 2, 2); return value; }; const auto dword = [&](size_t index) { return uint32_t(word(index)) | uint32_t(word(index + 1)) << 16; }; switch (op) { case 0: if (applies) { const uint64_t rsp = context_rsp(context); set_context_register(context, op_info, read64(process, rsp)); if (pointers) (*pointers)[16 + op_info] = rsp; set_context_rsp(context, rsp + 8); } break; case 1: used = op_info ? 3 : 2; if (applies) set_context_rsp(context, context_rsp(context) + (op_info ? dword(1) : uint64_t(word(1)) * 8)); break; case 2: if (applies) set_context_rsp(context, context_rsp(context) + op_info * 8 + 8); break; case 3: if (applies) { set_context_rsp(context, context_register(context, frame_reg) - frame_off); } if (depth == 0) setfp_offset = offset; break; case 4: used = 2; if (applies) { const uint64_t slot = context_rsp(context) + uint64_t(word(1)) * 8; set_context_register(context, op_info, read64(process, slot)); if (pointers) (*pointers)[16 + op_info] = slot; } break; case 5: used = 3; if (applies) { const uint64_t slot = context_rsp(context) + dword(1); set_context_register(context, op_info, read64(process, slot)); if (pointers) (*pointers)[16 + op_info] = slot; } break; case 6: used = 2; break; case 7: used = 3; break; case 8: used = 2; if (applies) { const uint64_t slot = context_rsp(context) + uint64_t(word(1)) * 16; const auto bytes = process.read_memory(slot, 16); std::memcpy(context.data() + xmm_base + op_info * 16, bytes.data(), 16); if (pointers) (*pointers)[op_info] = slot; } break; case 9: used = 3; if (applies) { const uint64_t slot = context_rsp(context) + dword(1); const auto bytes = process.read_memory(slot, 16); std::memcpy(context.data() + xmm_base + op_info * 16, bytes.data(), 16); if (pointers) (*pointers)[op_info] = slot; } break; case 10: if (applies) { const uint64_t rsp = context_rsp(context) + (op_info ? 8 : 0); set_context_rip(context, read64(process, rsp)); set_context_rsp(context, read64(process, rsp + 24)); } break; default: throw std::runtime_error("native unwind data uses an unknown operation"); } slot += used; } const uint8_t *tail = codes + padded * 2; if (flags & 4) { // Chained unwind data continues in the parent function's own record, whose prolog has always completed. uint32_t parent[3]; std::memcpy(parent, tail, 12); // The parent may itself stand for another entry, as table entries with the low bit set do. for (int hops = 0; hops < 4 && (parent[2] & 1); ++hops) { const auto entry = load(parent[2] & ~uint32_t(1), 12); std::memcpy(parent, entry.data(), 12); } info_rva = parent[2]; limit = UINT64_MAX; continue; } if (flags & handler_type) { uint32_t handler_rva; std::memcpy(&handler_rva, tail, 4); result.handler = base + handler_rva; result.handler_data = base + info_rva + 4 + padded * 2 + 4; } break; } // The establisher is the frame base the function maintains: its frame register once SET_FPREG ran, the stack pointer before that. const bool established = frame_register && prolog_offset >= setfp_offset; result.establisher = established ? context_register(original, frame_register) - frame_offset : context_rsp(original); unwind_leaf(context); return result; } }