使用MinGW构建的标准可执行文件中的陷阱内存访问

时间:2019-02-26 16:54:02

标签: gcc x86 mingw device-emulation trap

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

我有一些平台相关的代码(嵌入式系统),可以写入在特定地址进行硬编码的MMIO位置。

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

为减轻硬编码的指针,我只是将它们重新定义为内存池中的一些变量。而且效果很好。

问题在于某些MMIO位置(例如w1c)上存在特定的硬件行为,这使得“正确”测试变得几乎不可能。

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

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

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

3-使用某种仿真。这肯定会起作用,但是将使在标准计算机上快速本机运行的全部目的无效。

4-虚拟化?可能需要花费很多时间来实施。不确定如何获得收益。

有人能在不深入的情况下实现这一目标吗?也许有一种方法可以某种方式操纵编译器来定义一个内存区域,每次访问都会为该内存区域生成一个回调。并不是x86 / gcc方面的专家。

编辑:似乎不太可能以独立于平台的方式执行此操作,并且由于它仅是Windows,因此我将使用可用的API(似乎按预期工作)。在这里找到这个问题:

Is set single step trap available on win 7?

我将整个“模拟”寄存器文件放在多个页面中,加以保护,并触发一个回调,从该回调中我将提取所有必要的信息,执行我的工作,然后继续执行。

谢谢大家的回复。

3 个答案:

答案 0 :(得分:3)

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

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

但是,您真正想要做的是模拟内存访问,然后让segv处理程序在访问后返回指令。此示例代码在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;
}

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

答案 1 :(得分:2)

这是Windows版本prl's answer的样子:

#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;
}

答案 2 :(得分:0)

因此,解决方案(代码段)如下:

首先,我有一个变量:

__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;

先触发PRE_WRITE再执行WRITE

当我这样做时:

int x = *(int *)&g_test;

将发出READ。

通过这种方式,我可以以不需要修改原始源代码的方式来操纵数据流。 注意:这旨在用作测试框架的一部分,并且任何罚金都可以接受。

例如,可以完成W1C(写入1以清除)操作:

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发出一个,并跟踪“虚拟寄存器文件”,因此会受到更大的损失。这样,根据注册的监视器,我只能保护特定的内存区域或不保护任何区域。