Linux:在我的进程中管理虚拟内存映射以实现快速仿真

Ren*_*ena 5 linux memory emulation virtual-memory memory-mapping

最近我发现很多模拟器都很慢,因为它们不仅需要模拟CPU,还要模拟模拟设备的内存.当设备具有内存映射I/O,虚拟内存或仅未使用的地址空间时,必须在软件中模拟每个内存访问.

如果操作系统通过虚拟内存为我们这样做,我觉得它可能会快得多.为了简单起见,我将使用Game Boy仿真作为示例,但显然这种方法对于更新,更强大的机器会更好.

Game Boy记忆图大致是:

  • 0x0000 - 0x7FFF:映射到盒式ROM
    • 大多数墨盒固定为0x0000 - 0x3FFF,写入0x2000时可将0x4000 - 0x7FFF存储区切换
  • 0x8000 - 0x9FFF:视频RAM(仅在当前未呈现时可访问)
  • 0xA000 - 0xBFFF:映射到盒式磁带(通常由电池供电的RAM)
  • 0xC000 - 0xDFFF:内部RAM(0xD000 - 0xDFFF是GB颜色上的bankwitched)
  • 0xE000 - 0xFDFF:内部RAM的镜像
  • 0xFE00 - 0xFE9F:对象属性存储器(精灵RAM)
  • 0xFEA0 - 0xFEFF:未映射(打开总线或其他东西,不确定)
  • 0xFF00 - 0xFF7F:内存映射I/O(音响系统,视频控制等)
  • 0xFE80 - 0xFFFF:内部RAM

因此,传统的模拟器必须转换每个内存访问,如:

if(addr < 0x4000) return rom[addr];
else if(addr < 0x8000) return rom[(addr - 0x4000) + (0x4000 * cur_rom_bank)];
else if(addr < 0xA000) {
    if(vram_accessible) return vram[addr - 0x8000];
    else return 0xFF;
}
else if(addr < 0xC000) return saveram[addr - 0xA000];
else if(addr < 0xE000) return ram[addr - 0xC000];
else if(addr < 0xFE00) return ram[addr - 0xE000];
else if(addr < 0xFE9F) return oam[addr - 0xFE00];
else if(addr < 0xFF00) return 0xFF; //or whatever should be here
else if(addr < 0xFF80) return handle_io_read(addr);
else return hram[addr - 0xFF80];
Run Code Online (Sandbox Code Playgroud)

显然,可以通过使用开关或表进行优化,但仍然需要为每次内存访问运行大量代码.通过将一些页面映射到我们进程的内存映射中的那些地址,我们可以提高仿真速度:

  • 0x0000 - 0x3FFF:R--(没有Exec标志,因为本机CPU不执行它)
  • 0x4000 - 0x7FFF:R--
  • 0x8000 - 0x9FFF:---
  • 0xA000 - 0xBFFF:---
  • 0xC000 - 0xDFFF:RW-
  • 0xE000 - 0xFDFF:RW-(并映射到与0xC000相同的物理页面 - 0xDFFF)
  • 0xFE00 - 0xFE9F:---
  • 0xFEA0 - 0xFEFF:---
  • 0xFF00 - 0xFF7F:---
  • 0xFF80 - 0xFFFF:RW-

然后处理访问这些页面时得到的SIGSEGV(或者生成的任何信号).因此,可以直接执行从ROM读取或写入RAM,并且写入ROM将引发我们可以处理的异常.我们可以将VRAM(0x8000 - 0x9FFF)的权限更改为RW-应该可以访问时 - 以及 - 何时不应该访问.理论上它可以更快,因为它不需要模拟器手动映射软件中的每个存储器访问.

我知道我可以使用mmap()各种权限在固定地址映射页面.我不知道的是:

  • 映射是否可以重叠,具有不同的权限?
  • 无论系统的页面大小如何,我都可以将页面映射到这样的任意地址吗?我可以映射到地址0吗?
  • 如何更改映射指向的内存?(例如,当ROM库被更改时,我们可以切换内存映射到0x4000 - 0x7FFF,但我该怎么做?)
  • 在仿真系统具有32位或64位CPU的实际情况下,我可以映射整个前4GB,还是整个内存空间?如何避免与已经映射的内容冲突(例如库,我的堆栈,内核)?
  • 这真的会更快吗?或者投掷和捕获SIGSEGV会产生比传统方式更多的开销吗?
  • 如果在用户空间中无法做到这一点,Linux是否可以提供一种"接管"内核并在那里进行的方法?所以我至少可以创建一个运行裸机的"仿真器OS",同时还有一些Linux内核工具(如视频和文件系统驱动程序)可用?

Pet*_*des 3

我预计生成 SIGSEGV、捕获它、处理它并恢复,会比原始硬件有更多的性能开销,因此安排它仅在实际存在可能很慢的错误时发生。

当违规很少发生时,这是一种很好的内存保护/数组边界检查技术,如果它们很慢也没关系。稍微加快常见情况的速度是一种胜利,即使它使异常情况慢得多,当异常情况在正常模拟代码中没有发生时也是一种胜利。

我听说 Javascript 模拟器这样做是为了获得更便宜的数组边界检查:分配一个数组,使其在页面顶部结束,其中下一页未映射。


请对此持保留态度:我在编写的代码中没有使用过这些内容。我刚刚听说它,并认为我了解它是如何工作的以及一些影响。

希望这能让您开始查看文档,告诉您实际可以做什么。

更新页表相当慢。尝试找到一种平衡,在这种平衡中,您可以利用用户空间内存保护来进行某些检查,但在模拟代码执行的“常见情况”期间,您不会不断地从内存空间映射/取消映射页面。预测的分支运行得非常快,尤其是。如果他们被预测没有被采取。

memcpy我看过 Linux 内核讨论/注释,表明仅仅在单个页面上玩弄 mmap 是不值得的。对于较大的内存块或较少检查重复访问,其好处将超过设置开销。


您将需要用于mprotect(2)更改页面(范围)上的权限。不可以,映射不能重叠。请参阅MAP_FIXED以下选项mmap(2)

如果 addr 和 len 指定的内存区域与任何现有映射的页面重叠,则现有映射的重叠部分将被丢弃。

IDK 如果您可以在访问模拟内存时使用 x86 段寄存器执行任何有用的操作,以将访客地址 0 映射到进程虚拟地址空间中的某个其他地址。您可以映射虚拟地址 0,但默认情况下 Linux 会禁用它,因此 NULL 指针取消引用不会默默地工作!

软件的用户必须使用 sysctl(与 WINE 相同)来启用它:

# Ubuntu's /etc/sysctl.d/10-zeropage.conf
# Protect the zero page of memory from userspace mmap to prevent kernel
# NULL-dereference attacks against potential future kernel security
# vulnerabilities.  (Added in kernel 2.6.23.)
#
# While this default is built into the Ubuntu kernel, there is no way to
# restore the kernel default if the value is changed during runtime; for
# example via package removal (e.g. wine, dosemu).  Therefore, this value
# is reset to the secure default each time the sysctl values are loaded.
vm.mmap_min_addr = 65536
Run Code Online (Sandbox Code Playgroud)

就像我说的,您可以在所有加载/存储到来宾(模拟机)内存中的情况下使用段寄存器覆盖,将其重新映射到更合理的页面。或者可能只使用 64kiB 的常量偏移量(或更多,可以将其放在模拟软件的文本/数据/bss(堆)之上。或者使用指向映射客户内存基址的指针的非常量偏移量区域,所以一切都与全局变量相关。 对于 gcc,这可能是请求 gcc 将全局变量保留在所有函数的寄存器中的良好候选者。IDK ,您必须看看这是否有助于性能。恒定的偏移量最终会使访问客户内存的每条指令在寻址模式下都需要 32b 的位移字段,而不是 0 或 8b。

AFAIK,如果段寄存器按照我认为的方式工作(作为常量偏移量,您可以使用段覆盖前缀而不是 32b 位移修饰符来应用),那么编译器将很难生成段寄存器。如果只是加载/存储,那就是一回事:您可以使用内联 asm 包装器来加载和存储 insn。但对于高效的 x86 代码,各种 ALU 指令都应使用内存操作数,通过微融合减少前端瓶颈。

您也许可以定义一个全局变量char *const guest_mem = (void*)0x2000000;或其他东西,然后使用mmapwithMAP_FIXED强制映射内存?然后,客户内存访问可以编译为更高效的单寄存器寻址模式。