【问题标题】:Linux: Managing virtual memory mapping within my process for fast emulationLinux:在我的进程中管理虚拟内存映射以进行快速仿真
【发布时间】:2016-04-03 23:20:44
【问题描述】:

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

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

Game Boy 的内存图大致如下:

  • 0x0000 - 0x7FFF:映射到磁带 ROM
    • 大多数磁带的 0x0000 - 0x3FFF 固定,0x4000 - 0x7FFF 可通过写入 0x2000 进行组切换
  • 0x8000 - 0x9FFF:视频 RAM(仅在当前未渲染时可访问)
  • 0xA000 - 0xBFFF:映射到盒式磁带(通常是电池供电的 RAM)
  • 0xC000 - 0xDFFF:内部 RAM(0xD000 - 0xDFFF 在 GB 颜色上进行组切换)
  • 0xE000 - 0xFDFF:内部 RAM 镜像
  • 0xFE00 - 0xFE9F:对象属性内存(sprite 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];

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

  • 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 bank 改变时,我们可以切换 0x4000 - 0x7FFF 映射的内存,但是我该怎么做呢?)
  • 在模拟系统具有 32 位或 64 位 CPU 的实际情况下,我可以映射整个前 4GB 或可能的整个内存空间吗?如何避免与已映射的内容(例如库、我的堆栈、内核)发生冲突?
  • 这真的会更快吗?还是投掷和捕捉 SIGSEGV 会比传统方式产生更多开销?
  • 如果无法在用户空间执行此操作,Linux 是否可能提供一种“接管”内核并在那里执行此操作的方法?所以我至少可以创建一个运行裸机的“模拟器操作系统”,同时仍然有一些 Linux 内核设施(如视频和文件系统驱动程序)可用?

【问题讨论】:

  • 我能想到两种解决方案:动态重新编译和hyoervisor(如kvm)

标签: linux memory virtual-memory emulation memory-mapping


【解决方案1】:

我希望生成一个 SIGSEGV、捕获它、处理它并恢复它会比在原始硬件上产生更多的性能开销,因此请安排它仅在实际出现可能很慢的错误时发生。

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

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


对此持保留态度:我没有在我编写的代码中使用任何这些。我刚刚听说它,并认为我了解它的工作原理和一些含义。

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

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

我已经看到 Linux 内核讨论/注释表明使用 mmap 玩把戏不值得仅仅memcpy 的单个页面。对于更大的内存块,或者对重复访问的检查更少,好处将超过设置开销。


您需要使用mprotect(2) 更改页面(范围)的权限。不,映射不能重叠。请参阅mmap(2) 中的MAP_FIXED 选项:

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

如果您可以在访问模拟内存时使用 x86 段寄存器做任何有用的事情,则 IDK 可以将访客地址 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

就像我说的那样,您可以在所有加载/存储到来宾(模拟机)内存时使用段寄存器覆盖,以将其重新映射到更合理的页面。或者可能只是使用 64kiB 的恒定偏移量(或更多,可能将其放在仿真软件的文本/数据/bss(堆)之上。或者使用指向映射的客户内存区域的基础,所以一切都与一个全局变量有关。@ 987654321@ IDK,你必须看看这是否有助于提高性能。一个恒定的偏移量最终会使每条访问客户内存的指令都需要一个寻址方式中的位移字段为32b,而不是0或8b。

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

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

【讨论】:

    【解决方案2】:

    一般的东西

    Dolphin 模拟器有一个名为 fastmem 的功能。 AFAIU,通过假设内存访问使用标准内存来对代码块进行 JIT。如果在某个时候指令正在访问硬件内存,则对指令进行修补以改用慢速(内存)路径。这是由模拟器处理的段错误触发的:

    1. 生成调用合适(慢内存路径)代码的蹦床;

    2. 现有指令被修补并替换为跳转到此蹦床。

    一些参考资料:

    这在某种程度上类似于您所描述的 JIT/修补程序可以分摊页面错误的成本(因为每次指令访问硬件地址时生成页面错误将是低效的)。

    顺便说一下,您可能想对emulated memory 的管理方式感兴趣。见MemoryMap_Setup()

    回答我们的问题

    映射是否可以重叠,但权限不同?

    如果您对与先前 VMA 重叠的内容进行 mmap 映射,则会将旧 VMA 的部分替换为新 VMA。

    我可以像这样将页面映射到任意地址吗? 无论系统的页面大小如何?

    不,VMA 始终与页面边界对齐(x86 和 x86_64 上为 4KiB)。如果您正在映射文件/共享内存,则偏移量也有对齐约束。

    我可以映射到地址 0 吗?

    至少,Linux 不允许你这样做。

    在模拟系统具有 32 位或 64 位 CPU 的实际案例中,> 我可以映射整个前 4GB,还是可能映射整个内存空间?

    您不能映射整个地址空间。 AFAIU,Dolphin 所做的是将模拟的 32 位地址空间映射到原生 64 位地址空间的固定偏移量。

    如何避免与已映射的内容发生冲突(例如 库、我的堆栈、内核)?

    拥有一个address space larger than the emulated one 对此有帮助。

    如果在用户空间中无法做到这一点,那么 Linux 也许提供一种“接管”内核并在那里做的方法? 所以我至少可以创建一个运行裸机的“模拟器操作系统” 同时仍然拥有一些 Linux 内核设施 (例如视频和文件系统驱动程序)可用?

    如果您尝试模拟本机 CPU,您可以使用虚拟化技术(例如 KVM)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-04-26
      • 1970-01-01
      • 2013-12-30
      • 1970-01-01
      • 2011-05-23
      相关资源
      最近更新 更多