【发布时间】:2015-09-04 12:03:50
【问题描述】:
如何欺骗 linux 使其认为内存读/写成功?我正在编写一个 C++ 库,以便所有读/写都被重定向并透明地处理给最终用户。每当写入或读取变量时,库都需要捕获该请求并将其发送到硬件模拟,该硬件模拟将从那里处理数据。
请注意,我的库依赖于平台:
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 处理程序来获取包含内存地址的信息,将读/写导出到其他地方,然后增加上下文 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 非法指令(核心转储)./$target
在 SIGSEGV 处理程序中使用 mprotect()
如果我不增加 REG_RIP,handle_sigsegv() 将被内核一遍又一遍地调用,直到内存区域可供读取或写入。我可以为该特定地址运行mprotect(),但这有多个警告:
- 由于内存区域现在具有 PROT_WRITE 功能,后续内存访问不会触发 SIGSEGV。我试图创建一个线程,不断将该区域标记为 PROT_NONE,但这并没有忽略下一点:
-
mprotect()将在一天结束时执行对内存的读取或写入,使我的库的用例无效。
编写设备驱动程序
我还尝试编写一个设备模块,以便库可以在 char 设备上调用 mmap(),驱动程序将从那里处理读取和写入。这在理论上是有道理的,但我无法(或不知道)捕获每个加载/存储处理器问题到设备。我已经尝试覆盖映射的vm_operations_struct 和/或inode 的address_space_operations 结构,但这只会在页面出错或页面刷新到后备存储时调用读/写。
也许我可以使用mmap() 和mprotect(),就像上面解释的那样,在无处写入数据的设备上(类似于/dev/null),然后有一个进程来识别读/写并从那里路由数据(?)。
利用syscall()并提供restorer组装功能
以下内容来自将段错误转换为异常的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() 配置没有什么不同。如果我不设置恢复函数,则不会多次调用信号处理程序,即使内存区域仍然不可用。也许我可以在这里对内核信号做一些其他的诡计。
同样,库的整个目标是透明地处理对内存的读取和写入。也许有更好的处理方式,可能使用ptrace() 甚至更新生成段错误信号的内核代码,但重要的部分是最终用户的代码不需要更改。我已经看到使用setjmp() 和longjmp() 在段错误后继续的示例,但这需要将这些调用添加到每个内存访问中。将段错误转换为 try/catch 也是如此。
1segvcatch project
【问题讨论】:
-
为什么是
+=6?其次,读取将(比如说)设置一个寄存器值。我在您的代码中没有看到任何相关内容,您在做吗?您说您“将数据读/写发送到模拟”,但没有关于取回任何数据,这是故意的吗? -
+=6似乎是 x86_64 的指令宽度,至少对于最简单的情况。不幸的是,我无法提供这些知识的来源,但它似乎确实有效。当我说“发送数据读/写...”时,我的意思是我向我的硬件模拟(即加载/存储)发出了一条自定义指令(附有数据)。你是说我需要查明需要数据的处理器寄存器并加载它吗?这是有道理的...... -
将您的硬件包装在抽象层中,并且只调用抽象层。作为额外的好处,当您将软件移植到新硬件时,您只需更新抽象层。 (并处理由新时间和可能新编译器暴露的代码中的任何错误)
-
我不确定如何将全局抽象级别添加到任意用户程序。我将非常感谢任何讨论此问题的资源链接。需要注意的是,硬件模拟在其自己的进程中运行,其中自定义指令(和数据)通过套接字或共享内存空间发送。
-
我很抱歉。我读得太快了,开场假设不好。您的想法听起来更像是虚拟机,而不是构建测试台。
标签: c++ linux ubuntu memory kernel