Files

440 lines
15 KiB
C++

#include "unwind.h"
#include <algorithm>
#include <cstring>
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<FunctionEntry> 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<FunctionEntry> 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<PEModule &>(*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<uint8_t> 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<Step> 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<PEModule &>(*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<uint8_t>(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;
}
}