Linux:pipe理我的进程中的虚拟内存映射以进行快速仿真

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

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

Game Boy的内存映射大概是:

  • 0x0000 – 0x7FFF:映射到磁带ROM
    • 大多数墨盒都具有0x0000 – 0x3FFF固定和0x4000 – 0x7FFF银行 – 开关通过写入到0x2000
  • 0x8000 – 0x9FFF:videoRAM(仅当当前不呈现时才可访问)
  • 0xA000 – 0xBFFF:映射到盒式磁带(通常由电池供电的RAM)
  • 0xC000 – 0xDFFF:内部RAM(0xD000 – 0xDFFF在GB颜色上被切换)
  • 0xE000 – 0xFDFF:内部RAM的镜像
  • 0xFE00 – 0xFE9F:对象属性存储器(sprite RAM)
  • 0xFEA0 – 0xFEFF:未映射(打开总线或某些东西,不确定)
  • 0xFF00 – 0xFF7F:内存映射I / O(音响系统,video控制等)
  • 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–(没有执行标志,因为本机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会引发exception,我们可以处理。 我们可以将VRAM(0x8000 – 0x9FFF)的权限更改为RW-当它应该是可访问的,以及 – 当它不应该时。 理论上它可以更快,因为它不需要模拟器手动映射软件中的每个存储器访问。

我知道我可以使用mmap()来映射具有不同权限的固定地址的页面。 我不知道的是:

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

我希望生成一个SIGSEGV,捕获它,处理它,然后恢复,会比原始硬件有更多的性能开销,所以安排它只发生在实际存在一个可能很慢的错误时。

对于内存保护/数组边界检查,当违规情况很少时,这是一个很好的技术,如果它们很慢,这也是可以的。 加速常见的情况是一个胜利,即使它使得例外情况慢得多,当正常模拟代码中不会发生例外情况时,胜利是一个胜利。

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


用一点盐做这个:我没有用我写的代码中的任何一个。 我只是刚刚听到,并认为我明白它是如何工作和一些影响。

希望这会让你开始看文档,告诉你实际上可以做些什么。

更新页面表格相当缓慢 。 尽量找到一个平衡点,在这里你可以利用用户空间内存保护来进行一些检查,但是在模拟代码所做的“常见情况”期间,你并不是经常从内存空间映射/取消映射页面。 预测分支运行非常快,尤其是。 如果他们预计采取。

我已经看过Linux内核讨论/笔记,指出使用mmap玩技巧不值得它仅仅是单个页面的memcpy 。 对于较大的内存块,或者对重复访问进行较少的检查,益处将超过设置开销。


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

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

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

你的软件的用户将不得不与sysctl(与葡萄酒一样)启用它:

 # 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 (eg wine, dosemu). Therefore, this value # is reset to the secure default each time the sysctl values are loaded. vm.mmap_min_addr = 65536 

就像我所说的,也可以在所有加载/存储到guest(模拟机器)内存时使用段寄存器覆盖,以将其重新映射到更合理的页面。 或者,也许只是使用一个不变的偏移量64kB(或者更多,可以把它放在仿真软件的text / data / bss(heap)上面,或者使用一个指向你的mmap的客户存储器所以一切都是相对于一个全局变量而言的, 对于gcc来说,这可能是一个很好的候选方案,可以让gcc把所有的函数都保存在一个注册表中, IDK,你必须看看是否有帮助。一个不变的偏移量最终会使访问客存储器的每条指令在寻址模式下都需要一个32b的位移域,而不是0或8b。

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

你也许可以定义一个全局的char *const guest_mem = (void*)0x2000000; 或者什么的,然后使用MAP_FIXED mmap来强制映射内存呢? 然后访客内存访问可以编译为更高效的单寄存器增强模式。

一般的东西

海豚模拟器有一个称为fastmem的功能。 AFAIU,通过假定存储器访问使用标准存储器来打乱代码块。 如果在某些时候指令正在访问硬件存储器,则修改指令以便使用慢速(存储器)路径。 这由模拟器处理的段错误触发:

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

  2. 现有的指令修补,并跳转到这个蹦床。

一些参考:

  • HandleFault()处理段错误;

  • BackPatch ,修补现有的代码;

  • GenerateWriteTrampoline和GenerateReadTrampoline生成蹦床到Read_U64()Write_U64()

这在某种程度上类似于JIT /修补程序描述的内容,可以减少页面错误的代价(因为每次访问硬件地址的指令都会产生页面错误将是低效的)。

顺便说一下,您可能想要感兴趣的是如何管理模拟内存 。 请参阅MemoryMap_Setup() 。

回答我们的问题

映射可以重叠,具有不同的权限?

如果您将mmap与之前的VMA重叠,则会将旧VMA的部分替换为新的VMA。

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

不,VMA始终与页面边界对齐(x86和x86_64上的4KiB)。 如果你正在映射一个文件/共享内存,你也有偏移限制。

我可以映射到地址0吗?

至少,Linux不会让你这样做。

在仿真系统具有32位或64位CPU的实际情况下,>我能映射整个前4GB,还是可能映射整个内存空间?

你不能映射整个地址空间。 AFAIU,Dolphin所做的就是将仿真的32位地址空间映射到本地64位地址空间的固定偏移量上。

我将如何避免与已经映射的任何内容(例如,库,我的堆栈,内核)冲突?

有一个地址空间大于模拟的帮助。

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

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