我正在实现一个编译器,它将模拟处理器的指令集编译为x86指令中的代码。不存在在待模拟指令集上运行的物理处理器;只有x86上的模拟。执行编译的机器代码(模拟)时,我想确保只读取或写入为模拟指定的内存。这有两个目的:1)在指定的内存区域外访问可能意味着我的编译器中有一个错误。我想找到这样的错误。 2)在指定的存储区域之外的访问也可能意味着我编译的源指令具有逻辑错误,因此尝试访问我的模拟中不存在的存储器地址,因此应该引发错误。
以更简单的形式,您可以将我的代码想象成这样:
void simulate(char* designated_memory, size_t len) {
// code intended to access *designated_memory till *(designated_memory + len - 1) only
}
在x86-64和/或linux中是否有办法强制simulate()
只能访问自己的堆栈和designated_memory
,而任何其他访问都会产生错误。例如。代码可能如下所示:
restrict_access_to(designated_memory, designated_memory + len - 1);
simulate(designated_memory, len);
remove_access_restriction();
C中的解决方案会很好; asm也很好。
更新:
根据Jester的评论,我来试试这个:
#include <stdio.h>
#include <unistd.h>
#include <malloc.h>
#include <sys/mman.h>
int main() {
size_t pagesize = sysconf(_SC_PAGESIZE);
printf("pagesize...........: %lu\n", pagesize);
char* m;
size_t len = 12345;
len = (len + pagesize - 1) / pagesize * pagesize;
posix_memalign(&m, pagesize, len);
printf("page aligned memory: %lx - %lx\n", (unsigned long) m, (unsigned long) m + len);
printf("protecting 0 till m..."); fflush(stdout);
mprotect(0, (size_t) m, PROT_NONE);
printf("done\n");
printf("protecting (m + len) till ?..."); fflush(stdout);
mprotect(m + len, 0x7fffffff, PROT_NONE);
printf("done\n");
printf("trying to modify memory..."); fflush(stdout);
*(m - 1000) = 5;
printf("done: %i\n", *(m - 1000));
free(m);
}
哪个输出:
pagesize...........: 4096
page aligned memory: 9ac000 - 9b0000
protecting 0 till m...done
protecting (m + len) till ?...done
trying to modify memory...done: 5
Segmentation fault (core dumped)
我认为这表明修改允许区域之外的数据仍然有效,但这不应该发生。
答案 0 :(得分:1)
我不使用口译员。我使用编译器将模拟的&gt;指令编译为x86指令。出于性能原因,我不想在编译中包含所有范围检查&gt; x86指令。
因此,您的任务与qemu
非常相似,当它在ARM
上进行模拟x86
时,会将arm
指令转换为x86
说明,
所以我建议你看一下qemu的源代码:http://wiki.qemu.org/Main_Page
下一个与您的项目非常相似的是valrgrind
,
valgrind
的工作方式与您的工作方式非常相似:
它在某种虚拟cpu
上执行程序来检查内存访问,
并加快使用jit
(http://valgrind.org/)。
最后一个开源项目,解决类似的问题 https://github.com/google/sanitizers/tree/master/address-sanitizer
是的,它是经过检测的代码,但结果比valgrind
快得多,
如何处理生成的代码,但将性能保持在适当的级别,您可以在此视频中找到asan
内部:
答案 1 :(得分:1)
如果您在用户空间代码中执行任意汇编指令,则实际上没有办法强制保护内存 - 内存保护通常需要能够在CPU上激活内核模式标志。
但是,由于您正在编写模拟器,并将另一种语言编译为您选择的一组汇编指令,因此您有另一种选择:控制正在发送的指令。仿真编译器不是发出用于模拟存储器访问的原始存储器访问指令,而是替换将使用调用您自己设计的存储器访问功能的指令访问存储器的指令,并在该功能中实现存储器访问保护。这也可以作为Lorehead状态通过使存储区域像std::vector
。
请注意,仅使用您自己的移动函数替换正常移动指令实际上不足以保护您自己的包装器代码不被“模拟”代码攻击。如果您的模拟发出原始跳转命令,那么它可以被模拟代码打破,模拟代码跳过模拟。或者通过精心策划的推/弹指令,这些指令会导致包装器代码在您离开模拟后再返回到其他模拟器代码。
实际上(某种程度上)安全(这绝不是详尽的,或保证足以使模拟代码安全),您需要确保模拟编译器生成安全代码;影响可访问内存的东西,包括mov,jump,push,pop,call和ret,需要用安全执行等效操作的函数调用替换,而不是只执行你不能信任的汇编代码。
您还需要确保将此包装在代码中,以便为外部程序保存寄存器,因为内部代码可能会随意更改其内容。
答案 2 :(得分:1)
您写道,出于性能原因,您不希望添加运行时边界检查。我建议您仔细查看实际的运行时成本并重新考虑:如果您使用间接寻址(ja
),它实际上只是一条指令,即使最简单的分支预测变量可以在最后放置异常处理程序时也能正确。如果你能识别并优化(某些)循环,你可以在循环之前进行一次边界检查,而不是在循环内的每次访问之前进行一次边界检查。
如果你真的负担不起这个开销,你可以尝试的一个选项是编译可执行文件并分叉一个在其地址空间中没有其他内存的进程。但是,如果您要在使用堆栈的进程中运行,理论上可能本机代码可能会破坏堆栈。如果将地址空间的大小精确地设置为2 ^ 16或2 ^ 32字节并且已编译的代码使用该大小寄存器访问它,则还可以使得无法访问地址空间外的内存。否则,你会试图毒害地址空间的每一个其他页面。