在使用 MinGW 构建的标准可执行文件中捕获内存访问

Trap memory accesses inside a standard executable built with MinGW

所以我的问题听起来像这样。

我有一些依赖于平台的代码(嵌入式系统)写入到一些硬编码在特定地址的 MMIO 位置。

我用标准可执行文件中的一些管理代码编译此代码(主要用于测试),但也用于模拟(因为在实际硬件平台中查找基本错误需要更长的时间)。

为了减轻硬编码指针,我只是将它们重新定义为内存池中的一些变量。这真的很好用。

问题是某些 MMIO 位置(例如 w1c)存在特定的硬件行为,这使得 "correct" 测试变得困难甚至不可能。

这些是我想到的解决方案:

1 - 以某种方式重新定义对这些寄存器的访问并尝试插入一些即时函数来模拟动态行为。这并不是真正有用的,因为有多种方法可以写入 MMIO 位置(指针和东西)。

2 - 以某种方式保留地址硬编码并通过段错误捕获非法访问,找到触发的位置,提取访问的确切位置,处理和 return。我不太确定这将如何工作(即使可能)。

3 - 使用某种模拟。这肯定会起作用,但它会使 运行 在标准计算机上快速和本机的整个目的无效。

4 - 虚拟化 ??可能需要很长时间才能实施。不太确定收益是否合理。

有没有人知道这是否可以在不深入的情况下完成?也许有办法以某种方式操纵编译器来定义每次访问都会为其生成回调的内存区域。不是 x86/gcc 方面的专家。

编辑:似乎不太可能以独立于平台的方式执行此操作,并且由于它只是 windows,我将使用可用的 API(这似乎有效正如预期的那样)。在这里找到这个问题:

Is set single step trap available on win 7?

我会将整个 "simulated" 寄存器文件放在多个页面中,保护它们,并触发一个回调,我将从中提取所有必要的信息,做我的事情然后继续执行。

感谢大家的回复。

我认为#2 是最好的方法。我通常使用方法 #4,但我用它来测试内核中 运行 的代码,因此我需要内核下方的一层来捕获和模拟访问。由于您已经将代码放入用户模式应用程序,#2 应该更简单。

这个问题的答案可能有助于实施#2。 How to write a signal handler to catch SIGSEGV?

不过,您真正想要做的是模拟内存访问,然后让 segv 处理程序 return 访问后的指令。此示例代码适用于 Linux。不过,我不确定它利用的行为是否未定义。

#include <stdint.h>
#include <stdio.h>
#include <signal.h>

#define REG_ADDR ((volatile uint32_t *)0x12340000f000ULL)

static uint32_t read_reg(volatile uint32_t *reg_addr)
{
    uint32_t r;
    asm("mov (%1), %0" : "=a"(r) : "r"(reg_addr));
    return r;
}

static void segv_handler(int, siginfo_t *, void *);

int main()
{
    struct sigaction action = { 0, };
    action.sa_sigaction = segv_handler;
    action.sa_flags = SA_SIGINFO;
    sigaction(SIGSEGV, &action, NULL);

    // force sigsegv
    uint32_t a = read_reg(REG_ADDR);

    printf("after segv, a = %d\n", a);

    return 0;
}


static void segv_handler(int, siginfo_t *info, void *ucontext_arg)
{
    ucontext_t *ucontext = static_cast<ucontext_t *>(ucontext_arg);
    ucontext->uc_mcontext.gregs[REG_RAX] = 1234;
    ucontext->uc_mcontext.gregs[REG_RIP] += 2;
}

读取寄存器的代码是用汇编编写的,以确保目标寄存器和指令长度都是已知的。

这就是 的 Windows 版本的样子:

#include <stdint.h>
#include <stdio.h>
#include <windows.h>

#define REG_ADDR ((volatile uint32_t *)0x12340000f000ULL)

static uint32_t read_reg(volatile uint32_t *reg_addr)
{
  uint32_t r;
  asm("mov (%1), %0" : "=a"(r) : "r"(reg_addr));
  return r;
}

static LONG WINAPI segv_handler(EXCEPTION_POINTERS *);

int main()
{
  SetUnhandledExceptionFilter(segv_handler);

  // force sigsegv
  uint32_t a = read_reg(REG_ADDR);

  printf("after segv, a = %d\n", a);

  return 0;
}


static LONG WINAPI segv_handler(EXCEPTION_POINTERS *ep)
{
  // only handle read access violation of REG_ADDR
  if (ep->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION ||
      ep->ExceptionRecord->ExceptionInformation[0] != 0 ||
      ep->ExceptionRecord->ExceptionInformation[1] != (ULONG_PTR)REG_ADDR)
    return EXCEPTION_CONTINUE_SEARCH;

  ep->ContextRecord->Rax = 1234;
  ep->ContextRecord->Rip += 2;
  return EXCEPTION_CONTINUE_EXECUTION;
}

于是,解决方案(代码片段)如下:

首先,我有一个变量:

__attribute__ ((aligned (4096))) int g_test;

其次,在我的主要功能中,我执行以下操作:

AddVectoredExceptionHandler(1, VectoredHandler);
DWORD old; 
VirtualProtect(&g_test, 4096, PAGE_READWRITE | PAGE_GUARD, &old);

处理程序如下所示:

LONG WINAPI VectoredHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
    static DWORD last_addr;

    if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION) {
        last_addr = ExceptionInfo->ExceptionRecord->ExceptionInformation[1];
        ExceptionInfo->ContextRecord->EFlags |= 0x100; /* Single step to trigger the next one */
        return EXCEPTION_CONTINUE_EXECUTION;
    }

    if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) {
        DWORD old;
        VirtualProtect((PVOID)(last_addr & ~PAGE_MASK), 4096, PAGE_READWRITE | PAGE_GUARD, &old);
        return EXCEPTION_CONTINUE_EXECUTION;
    }

    return EXCEPTION_CONTINUE_SEARCH;
}

这只是功能的基本框架。基本上我保护变量所在的页面,我有一些链接列表,其中我保存指向相关地址的函数和值的指针。我检查了故障生成地址是否在我的列表中,然后我触发了回调。

第一次防御命中时,页面保护将被系统禁用,但我可以调用我的 PRE_WRITE 回调,我可以在其中保存变量状态。因为单步是通过 EFlags 发出的,所以紧随其后的是单步异常(这意味着变量已写入),我可以触发 WRITE 回调。操作所需的所有数据都包含在 ExceptionInformation 数组中。

当有人试图写入该变量时:

*(int *)&g_test = 1;

A PRE_WRITE 后跟 WRITE 将被触发,

当我这样做时:

int x = *(int *)&g_test;

将发出 READ。

这样我就可以在不需要修改原始源代码的情况下操纵数据流。 注意:这旨在用作测试框架的一部分,任何惩罚命中都被认为是可以接受的。

例如W1C(Write 1 to clear)操作可以完成:

void MYREG_hook(reg_cbk_t type)
{
    /** We need to save the pre-write state
      * This is safe since we are assured to be called with
      * both PRE_WRITE and WRITE in the correct order 
      */
    static int pre;

    switch (type) {
        case REG_READ: /* Called pre-read */
            break;

        case REG_PRE_WRITE: /* Called pre-write */
            pre = g_test;
            break;

        case REG_WRITE: /* Called after write */
            g_test = pre & ~g_test; /* W1C */
            break;

        default:
            break;    
    }
}

对于非法地址的段错误,这也是可能的,但我必须为每个 R/W 发出一个,并跟踪 "virtual register file",这样会受到更大的惩罚。这样,我只能保护特定的内存区域或 none,具体取决于已注册的监视器。