如何使用 sigsegv 捕获内存读写?

How to trap memory reads and writes using sigsegv?

如何欺骗 linux 认为记忆 read/write 是成功的?我正在编写一个 C++ 库,以便所有 reads/writes 都被重定向并透明地处理给最终用户。任何时候写入或读取变量时,库都需要捕获该请求并将其发送到硬件模拟,硬件模拟将从那里处理数据。

请注意,我的库依赖于以下平台:

Linux ubuntu 3.16.0-39-generic #53~14.04.1-Ubuntu SMP x86_64 GNU/Linux

gcc (Ubuntu 4.8.2-19ubuntu1) 4.8.2

当前方法:捕获 SIGSEGV 并递增 REG_RIP

我目前的方法是使用 mmap() 获取内存区域并使用 mprotect() 关闭访问。我有一个 SIGSEGV 处理程序来获取包含内存地址的信息,将 read/write 导出到其他地方,然后增加上下文 REG_RIP.

void handle_sigsegv(int code, siginfo_t *info, void *ctx)
{
    void *addr = info->si_addr;
    ucontext_t *u = (ucontext_t *)ctx;
    int err = u->uc_mcontext.gregs[REG_ERR];
    bool is_write = (err & 0x2);
    // send data read/write to simulation...
    // then continue execution of program by incrementing RIP
    u->uc_mcontext.gregs[REG_RIP] += 6;
}

这适用于非常简单的情况,例如:

int *num_ptr = (int *)nullptr;
*num_ptr = 10;                          // write segfault

但是对于任何稍微复杂一点的东西,我都会收到一个 SIGABRT:

30729 Illegal instruction (core dumped) ./$target

在 SIGSEGV 处理程序中使用 mprotect()

如果我不增加 REG_RIP,handle_sigsegv() 将被内核一遍又一遍地调用,直到内存区域可用于读取或写入。我可以 运行 mprotect() 该特定地址,但有多个注意事项:

编写设备驱动程序

我还尝试编写一个设备模块,以便库可以在字符设备上调用 mmap(),驱动程序将从那里处理读取和写入。这在理论上是有道理的,但我无法(或不具备知识)捕获处理器向设备发出的每个 load/store 问题。我试图覆盖映射的 vm_operations_struct and/or 索引节点的 address_space_operations 结构,但只有当页面出现故障或页面被刷新到后备存储时才会调用 reads/writes。

也许我可以使用 mmap()mprotect(),就像上面解释的那样,在无处写入数据的设备上(类似于 /dev/null),然后有一个进程可以识别 reads/writes 并从那里路由数据 (?)。

利用syscall()并提供恢复器组装功能

以下内容来自 segvcatch 项目1,它将段错误转换为异常。

#define RESTORE(name, syscall) RESTORE2(name, syscall)
#define RESTORE2(name, syscall)\
asm(\
    ".text\n"\
    ".byte 0\n"\
    ".align 16\n"\
    "__" #name ":\n"\
    "   movq $" #syscall ", %rax\n"\
    "   syscall\n"\
);
RESTORE(restore_rt, __NR_rt_sigreturn)
void restore_rt(void) asm("__restore_rt") __attribute__
((visibility("hidden")));

extern "C" {
    struct kernel_sigaction {
        void (*k_sa_sigaction)(int, siginfo_t *, void *); 
        unsigned long k_sa_flags;
        void (*k_sa_restorer)(void);
        sigset_t k_sa_mask;
    };  
}

// then within main ...
struct kernel_sigaction act;
act.k_sa_sigaction = handle_sigegv;
sigemptyset(&act.k_sa_mask);
act.k_sa_flags = SA_SIGINFO|0x4000000;
act.k_sa_restorer = restore_rt;
syscall(SYS_rt_sigaction, SIGSEGV, &act, NULL, _NSIG / 8); 

但这最终的功能与常规 sigaction() 配置没有什么不同。如果我不设置恢复函数,信号处理程序不会被多次调用,即使内存区域仍然不可用。也许我可以在这里对内核信号做一些其他的技巧。


同样,整个 objective 库是透明地处理对内存的读取和写入。也许有更好的做事方式,可能使用 ptrace() 甚至更新生成段错误信号的内核代码,但重要的部分是最终用户的代码不需要更改。我已经看到使用 setjmp()longjmp() 在段错误后继续的示例,但这需要将这些调用添加到每个内存访问中。将段错误转换为 try/catch.

也是如此


1 segvcatch project

您可以使用 mprotect 并通过让 SIGSEGV 处理程序在标志寄存器中设置 T 标志来避免您注意到的第一个问题。然后,您添加一个 SIGTRAP 处理程序来恢复受保护的内存并清除 T 标志。

T 标志导致处理器单步执行,因此当 SEGV 处理程序 returns 它将执行该单条指令,然后立即 TRAP。

这仍然会给您留下第二个问题——read/write 指令实际上会发生。您可以通过在两个信号处理程序中的指令之后 and/or 之前仔细修改内存来解决该问题...