-
-
[翻译连载][Rust][双语]给新手的Playstation1游戏机模拟器制作教程
-
发表于: 2023-3-21 19:41 6291
-
阅读本文需要有计算机组成原理的基础、一定时间的编程经验,最好会汇编语言。Rust可以突击,并没有用到复杂的Rust编程技巧。
本书也在本人的GitBook连载,并且有更好的阅读体验(https://oynos.gitbook.io/playstation1-emulation-guide/)
作者: Lionel Flandrin(October 20, 2016)
译者:Gargoyles(karman5757@outlook.com)
1.序言(Introduction)
This is my attempt at documenting my implementation of a PlayStation emulator from scratch. I’ll write the document as I go and I’ll try to explain as much as possible along the way. You can find the complete source of the emulator itself in my GitHub repository.
本书是我从零实现PlayStation模拟器(emulator,也作仿真器)的记录。我边写程序边写这本书,并尽可能详细地阐述细节。你可以在我的GitHub仓库中找到模拟器本身的完整源代码。
Since my favorite pass time is to reinvent the wheel and recode things that already exist I decided that this time I might as well document it. This way maybe this time something useful will come out of it and it’ll give me the motivation to finish it.
由于我最喜欢通过重新发明轮子和重写已有的代码来消磨时间,我决定这次不妨把过程记录下来。这样也许会产生一些有用的东西,这也是我完成它的动力。
I will be using the Rust programming language but this is not meant as a Rust tutorial and knowledge of the language shouldn’t be necessary to follow this guide, although it won’t hurt.
我将使用Rust编程语言,这并不意味着本书是Rust教程。你阅读本书需要Rust的知识,不过可以边学边读。
1.1 模拟器真的复杂吗?Isn't emulation complicated?
Emulation requires some low-level knowledge about how computers work and some basics in electronics might help for certain things. Since this doc is meant as an introduction to emulation I’ll assume that the reader doesn’t bring anything with them beyond some decent programming skills. So don’t worry if you’re not familiar with registers, cache, memory-mapped IO, virtual memory, interrupts and other low-level fun: I’ll try to explain everything when needed. Emulators are a good introduction to low-level programming without having to bother with that pesky hardware in person!
学习硬件仿真(emulation)需要一些计算机组成原理和汇编语言的知识,电子学(electronics)的基础知识也会有帮助。由于本文档是硬件仿真的介绍,我将假设读者除了一定的编程功底外没有任何底层的知识。所以,如果你不熟悉寄存器(registers)、高速缓存(cache)、内存映射I/O(memory-mapped IO, MMIO)、虚拟内存(virtual memory,)、中断(interrupts)和其他底层知识,也不用担心,我在必要时会详细解释。模拟器(emulators)是初步学习底层编程的好材料,使我们不因硬件本身烦恼。
Since this is supposed to be a general guide about writing PlayStation emulators I won’t put the entire source code of the emulator here, only snippets relevant to the matter being discussed.
由于本文是编写PlayStation模拟器的通用指南,我不会把全部源代码放在这里,只摘录与当前内容有关的片段。
Finally, keep in mind that getting a PlayStation emulator even capable to run some games decently will require quite a lot of work. Don’t expect to play Final Fantasy VII on your brand new emulator in two days. If you want to start with something simpler to see if you have a taste for it you can search for Chip-8, Game Boy or NES emulation tutorials (by increasing complexity).
最后,使PlayStation模拟器能顺畅地运行游戏需要相当多的工作。别相信几天就能在这个不成熟的模拟器上玩《最终幻想7》。如果你更想从更容易的东西开始学习,可以搜索Chip-8、Game Boy或NES模拟器教程(复杂度递增)。太难的学习任务会让人丧失兴趣。
1.2 勘误 Feedback
If some part of this document is unclear, poorly written or incomplete please submit an issue so that I can fix or complete it. Corrections for grammar, syntax and typos are very welcome. Thank you!
如果本文档的某些部分不清楚、写得不好或不完整,请提交反馈,以便修复和完善。非常欢迎对语法、句法和错字的纠正。衷心地感谢你!
Ready? Let's begin! 准备好了吗?让我开始正文吧!
https://github.com/simias/psx-guide 作者Github仓库
Karman5757@outlook.com 译者邮箱
1.3 专有名词翻译参考
《牛汉英汉双解计算机词典》ISBN978544605465:
平面地址空间(flat address space):flat addressing 平面寻址(373页)
2,CPU: 指令与内存
2.1 CPU究竟是什么?What is a CPU, anyway?
That might seem like a silly question to some but I’m sure there are plenty of competent programmers out there who are used to program in high level managed environments haven’t seen a register in their entire life. Let me make the introductions.
CPU究竟是什么呢?也许这个问题很可笑,但我相信有很多有能力的程序员习惯于在高级托管环境(high level managed environments)中编程,他们一生都没有见过寄存器(register)。下面将介绍计算机组成原理的基础知识。
For our first version of the PlayStation CPU, I’m going to make some simplifying assumptions. I’m going to ignore the caches for instance and assume that it directly accesses the system bus. We’re going to implement a . As we make progress we’ll have to revisit this design to add the missing bits when they are needed.
初版的仿真PlayStation CPU将做一些简化。例如,我将忽略缓存(caches),并假设它直接访问系统总线(system bus)。我们将实现一个冯-诺依曼架构。随着项目进展,我们将不得不将重新审视这个设计,以便在需要的时候添加缺少的小组件。
The objective of this section is to implement all the instructions and try to reach the part of the BIOS where it starts to draw on the screen. As we’ll see there’s a bunch of boring initialization code to run before we get there.
本章节的目的是实现(implement)所有的指令(instructions),并尝试运行BIOS中开始在屏幕上绘图的部分。在此之前,有一堆无聊的初始化代码要运行。
There are 67 opcodes in the PlayStation MIPS CPU. Some take one line to implement, others will give us more trouble. To make the process more interactive and less tedious we’ll implement them as they’re encountered while we’re running the original BIOS code. This way we’ll immediately be able to see our emulator in action.
在Playstation MIPS CPU中有67个操作码(opcode, 写成机器码(machine codes)的指令)。有些只需一行代码即可实现,有些却比较麻烦。为了使这个过程更有互动性和不那么冗长乏味,我们将在遇到它们时再实现它们,这时我们运行着原始BIOS代码。这样我们就能立即看到模拟器的运行情况。
But first things first, before we start implementing instructions we need to explain how a CPU works.
但首先要做的是,在开始实现指令之前,我们需要解释一下CPU的工作原理。
2.2 架构 Architecture
A simple Von Neumann architecture looks like this: the CPU only sees a flat address space: A byte array(an array of bytes). The PlayStation uses 32-bit addresses so the CPU sees 1 << 32 addresses. In other words, it can address 4GB of memory. That’s why the PlayStation is said to be a 32-bit game console (that and the fact that it uses 32-bit registers in the CPU as we’ll see in a minute).
一个简单的冯-诺依曼架构是这样的:CPU只看到一个平面地址空间(flat address space):一个字节数组(byte array)。PlayStation使用32位地址,所以CPU看到的是2^32个地址(它可以寻址4GB(2^32Bits)的内存)。所以PlayStation是一个32位的游戏控制器(CPU使用32位通用寄存器,我们将在一分钟后看到)。
This address space contains all the external resources the CPU can access: the RAM of course but also the various peripherals (GPU, controllers, CD drive, BIOS...). That’s called memory-mapped IO. Note that in this context ”memory” doesn’t mean RAM. Rather it means that you access peripherals as if they were memory (instead of using dedicated instructions for instance). From the point of view of the CPU, everything is just a big array of bytes and it doesn’t know what’s out there.
这个地址空间包含了CPU可以访问的所有外部资源:RAM和各种外围设备(GPU、控制器、CD驱动器、BIOS...)。这就是所谓的内存映射I/O(memory-mapped IO)。注意,在这种情况下,"内存 "并不是指RAM。相反,它意味着你访问外围设备(peripheral)就像访问内存一样(而不是使用专用指令)。从CPU的角度来看,一切都只是一个很大的字节数组(byte array),它不知道外面有什么。
Of course, we’ll have to figure out how the devices and RAM are mapped in this address space to make sure the transactions end up at the right location when the CPU starts reading and writing to the bus. But first, we need to understand how the code is executed.
当然,我们必须弄清楚设备和RAM在这个地址空间中是如何被映射的,以确保当CPU开始对总线进行读写时,事务(transaction)最终出现在正确的位置。但首先,我们需要了解代码是如何执行的。
2.3 代码 The Code
In this architecture, the instructions live in the global address space along with everything else, typically in RAM. But again, the CPU doesn’t care what’s out there. If you want to run code from the controller input port I’m sure the console will let you. Probably not very useful but it’s all the same as far as the CPU is concerned.
在这种架构(architecture)中,指令和其他数据一同存在于全局地址空间(global address space)中,通常是在RAM中。再说一次,CPU并不关心外面有什么。如果你想从控制器的输入端口运行代码,我相信控制台(console)会让你运行,可能没什么用,但对于CPU,这些都是一样的。
So somewhere in this 4GB address space, there’s the next instruction for the CPU to run. How does it know the address of this instruction? By using a register of course!
因此,在这个4GB地址空间的某个位置,有供CPU运行的下一条指令。它如何知道这条指令的地址呢?当然是通过寄存器(register)了!
2.4 程序计数器 The Program Counter register
Registers are very small and very fast special-purpose memories built inside the CPU. Most CPU instructions manipulate those registers by adding them, multiplying them, masking them, storing their content to memory or fetching it back...
寄存器是内建于CPU的极小而极快的特定用途的存储器。大多数CPU指令通过加法、乘法、屏蔽将其内容存储至内存或取回以操作这些寄存器。
The Program Counter (henceforth referred to as PC) is one of the most elementary registers, it exists in one form or another on basically all computer architectures (although it goes by various names, on x86 for instance it’s called the Instruction Pointer, IP). Its job is simply to hold the address of the next instruction to be run.
程序计数器(后文中简称PC)是最基本的寄存器之一,它以某种形式存在于几乎所有的计算机体系结构中(尽管名字不同。例如,x86把它称为指令指针(Instruction Pointer, IP)寄存器。它的工作是简单地保存下一条要运行的指令的地址。
As we’ve seen, the PlayStation uses 32bit addresses, so the PC register is 32bit wide (as are all other CPU registers for that matter).
如你所见,PlayStation使用32位地址,所以程序计数器寄存器是32位宽的(就像所有其他CPU寄存器一样)。
A typical CPU execution cycle goes roughly like this:
1.Fetch the instruction located at address PC,
2.Increment the PC to point to the next instruction,
3.Execute the instruction,
4.Repeat
一个典型的CPU执行周期(execution cycle)大致如此:
1.取得程序计数器所指向的指令
2.增加程序计数器的值
3.执行指令
4.重复
We need to know how big an instruction is to know how many bytes to fetch and how much we need to increment the PC to point at the next instruction. Some architectures have variable-length instructions (x86 and derivatives are a common example) which means we’d have to decode the instruction to know how many bytes it takes. Fortunately for us, the PlayStation uses a fixed-length instruction set (The MIPS instruction set) and all instructions are 32bit long.
得知一条指令的大小,才能得知要取多少字节,和PC增加的量(以指向下一条指令)。一些架构有可变长度的指令(x86及其派生品(derivatives)是一个常见的例子),这意味着我们必须解码指令才能知道它需要多少个字节。幸运的是,PlayStation使用固定长度的指令集(MIPS指令集),所有的指令都是32位长。
With all that in mind, we can finally start writing some code!
Here’s what the CPU state looks like at that point:
考虑到所有这些,我们终于可以开始写一些代码了!
这是此时CPU的状态:
1 2 3 4 5 6 7 | / / / CPU state / / / CPU的状态 pub struct Cpu { / / / The program counter register / / / 程序计数器 pc : u32, } |
And here’s the implementation of our CPU cycle described above:
这是上述的 CPU 周期(cycle)的实现(implementation):
1 2 3 4 5 6 7 8 9 10 11 12 | impl Cpu { pub fn run_next_instruction(&mut self ) { let pc = self .pc; / / Fetch instruction at PC / / 取得程序计数器所指向的指令 let instruction = self .load32(pc); / / Increment PC to point to the next instruction / / 增加程序计数器的值以指向下一条指令 self .pc = pc.wrapping_add( 4 ); self .decode_and_execute(instruction); } } |
In Rust wrapping_add means that we want the PC to wrap back to 0 in case of an overflow (i.e. 0xfffffffc + 4 => 0x00000000). We'll see that most CPU operations wrap on overflow (although some instructions catch those overflows and generate an exception, we'll see that later).
在 Rust 中 wrapping_add的作用是程序计数器在溢出的情况下返回 0(即 0xfffffffc + 4 => 0x00000000)。 我们将看到大多数 CPU 操作都在溢出时结束(尽管有些指令会捕获这些溢出并生成异常,我们稍后会看到)。
If you’re coding in C you don’t need to worry about that if you use uint32_t since the C standard mandates that unsigned overflow wraps around in this fashion. Rust however says that overflows are undefined and will generate an error in debug builds if an unchecked overflow is detected, that’s why I need to write pc.wrapping_add(4) instead of pc + 4.
C语言在这种情况下无需担心是否使用 uint32_t,因为 C 标准要求无符号溢出以这种方式返回。 然而,Rust 规定溢出是未定义的,如果检测到未经检查的溢出,将在debug builds中产生错误,这就是为什么我需要编写 pc.wrapping_add(4) 而不是 pc + 4。
We now finally have some code but it doesn’t build yet.
We’re still missing 3 pieces of the puzzle before we can run this piece of code:
1.What’s the initial value of PC when starting up?
2.How do we implement the fetch32 function?
3.How do we implement the decode_and_execute function?
代码太少不够生成(build),运行代码前还有三个难题:
1.启动时程序计数器的初始值是多少?
2.如何实现 fetch32 函数?
3.如何实现 decode_and_execute 函数?
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)