1

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

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

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

为了减轻硬编码的指针,我只是将它们重新定义为内存池中的一些变量。这非常有效。

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

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

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

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

3 - 使用某种仿真。这肯定会奏效,但它会破坏在标准计算机上快速和本地运行的全部目的。

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

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

编辑:似乎真的不可能以独立于平台的方式做到这一点,而且由于它只是 Windows,我将使用可用的 API(这似乎按预期工作)。在这里找到这个 Q:

win 7可以设置单步陷阱吗?

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

谢谢大家的回复。

4

3 回答 3

3

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

这个问题的答案可能有助于实施#2。如何编写信号处理程序来捕获 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;
}

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

于 2019-02-26T22:08:58.987 回答
2

这就是prl 答案的 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;
}
于 2019-02-27T16:08:10.910 回答
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(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 发出一个,并跟踪“虚拟寄存器文件”,因此受到更大的惩罚。通过这种方式,我只能保护特定的内存区域或不保护,具体取决于注册的监视器。

于 2019-02-28T12:29:39.537 回答