在PlayStation 2上运行Golang
Golang on the PlayStation 2
作者:Ricardo
2025年3月23日
我一直想在游戏主机上做一些奇怪的事情。我也不知道为什么,但让设备做一些意想不到的事情对我来说总是一个有趣的话题。这同样适用于PlayStation 2,索尼在2000年发布的游戏机。
另外,索尼,别因为这个起诉我,哈哈。
让我们直接进入正题:我想在游戏主机上运行代码(_为什么_这样做会在以后的文章中说明)。通常这是用较低级的语言完成的,但是现在我们有了更好、更易于使用的语言,例如Go。所以我就想……为什么不呢?
然而,在网上搜索后,没有找到简单的方法,所以我决定自己解决这个问题。
请注意,我写这篇文章时,很多研究已经完成并经过测试。这意味着这里的很多实验都来自记忆和追溯我的步骤,所以这里可能存在一些不一致之处。
另请注意,这一切都在模拟器中运行。我确实拥有一台PS2,我可以测试它,但我太懒了,不想设置它。另外,我希望在这样做之前有功能齐全的演示。
最后说明:代码将在稍后发布,我会相应地更新帖子。
挑战
默认情况下,Go仅支持少数平台,但不幸的是,这不包括PS2。事实上,Go似乎需要在幕后有一个操作系统,而我们甚至没有(不考虑PS2 Linux)。然而,为了解决这个问题,我们有TinyGo,这是一个用于小型设备的Go编译器,例如微控制器和嵌入式系统。它的基本工作方式是获取Go代码,将其转换为LLVM IR,然后再将其转换为我们要为其编写代码的任何目标的二进制代码。
PS2的主CPU被称为Emotion Engine,它基于MIPS R5900。它实现了MIPS-III和MIPS-IV的指令,以及一些自定义的东西。它也缺少一些其他东西(稍后会详细介绍)。Go实际上已经可以为MIPS构建代码,这应该可以节省我一些时间,但不是很多,因为我需要让TinyGo在它上面工作。TinyGo依赖于LLVM 19,它确实支持MIPS-III,但不支持直接支持R5900 CPU。
这些都是技术问题。还有一个更紧迫的问题:我不知道PS2是如何工作的。
好吧,这应该很有趣。
ps2dev SDK及其特性
如果你在网上搜索为PS2开发代码的方法,你可能会遇到ps2dev。这是一个功能齐全的SDK,可以让你以一种非常简洁的方式生成二进制文件。最酷的事情是它已经为图形操作、调试、I/O等提供了一堆库——甚至还提供了一个stdlib!因此,我想:嘿,也许我想链接到他们的代码——这将允许用Go更容易、更快地实现PS2上的任何东西。可以把它看作是一个“OS” API(从技术上讲不是吗?),我们可以随时调用它来做我们不想重新实现(或者有时甚至不值得尝试)的事情。
也就是说,这带来了一些问题。首先,ps2dev库被编译为MIPS-III N32标准。这意味着,无论我们生成什么代码,都需要以相同的东西为目标。相同的硬浮点,相同的N32 ABI等等。这有点烦人,但可以管理。它需要匹配的原因是,我们将把我们的代码链接到他们预先构建的库,而链接器并不是真的很喜欢处理不同的目标。
为了澄清:MIPS-III N32意味着它针对的是一个实现了MIPS-III指令集的MIPS CPU。这是一个64位的CPU,但是由于N32,它运行的是32位的代码,其中包含一些64位的指令来处理64位的整数。这非常令人困惑,但是你可以查看这里来阅读更多关于它的信息。
因此,你将在接下来的步骤中看到我尝试用mips3
CPU作为目标的mipsel
,即使从技术上讲,它应该是一个mips64el
,因为这是一个64位的CPU。N32应该强制事情以32位模式运行,即使我们的目标应该支持64位的代码。然而,Clang/LLVM和TinyGo在这方面有点混乱,这变得非常令人困惑和复杂。此外,为mips64el
构建会导致TinyGo在生成带有LLVM的代码时未能通过一些验证步骤,并且clang拒绝正确构建它,因为代码有点损坏。因为我_真的_想继续前进,所以我放弃了,选择生成带有N32 ABI的mipsel
代码,这将迫使clang在内部将其更改为MIPS64,但仍然生成有效的代码。就像我说的,这非常奇怪。请耐心等待,这一切对我来说也是全新的!:D
可能需要进一步重申这个话题才能把它弄对,但我现在不会深入研究这个地狱。将来,我们可以尝试放弃ps2dev,直接用Go做事,但是需要一些汇编——字面意义上和比喻意义上。
让TinyGo生成一些代码
为了让TinyGo了解某个目标,它需要一个定义它的文件——我们称之为ps2.json
。它定义了一堆我们目前不太关心的非常有趣的东西,但这里是最重要的:
{
"llvm-target": "mipsel-unknown-unknown",
"cpu": "mips3",
"features": "-noabicalls",
"build-tags": ["ps2", "baremetal", "mipsel"],
"goos": "linux",
"goarch": "mipsle",
"linker": "ld.lld",
"rtlib": "compiler-rt",
"libc": "",
"cflags": [
],
"ldflags": [
],
"linkerscript": "",
"extra-files": [
],
"gdb": []
}
这个文件是经过许多、许多天测试不同配置的最终结果。它只是部分功能正常。它还不能生成目标文件(更多内容见下文),因此我没有费心填写编译和链接代码的标志。不过,我需要解释一些相关的事情,所以我们开始吧:
- 目标是
mipsel-unknown-unknown
。这是我们的LLVM目标。由于我之前解释的原因,我在这里坚持使用mipsel
。 - 这些功能具有
-noabicalls
。这是必需的,否则情况会变得非常糟糕,什么都无法工作(生成的LLVM IR会损坏)。 - 我已将其设置为不使用任何
libc
。这是因为ps2dev已经提供了一个,我不想搞乱它(相信我)。另外,既然我们将链接到他们的代码,我们不妨使用他们的版本。
这是我们需要的基本目标文件,以便TinyGo至少_知道_什么是PS2。但这还不是全部——我们需要定义一堆特定于目标的函数。
裸机定义
我们的目标需要一个裸机配置——baremetal_ps2.go
。通常默认的裸机文件就足够了,但在我们的例子中,我选择创建一个自定义的文件,以便我可以重新定义一些东西。
来自未来的说明:可以通过调整链接器文件来改进这一点,以便它找到正确的extern。我可能会最终这样做,然后稍后再回到这里。
//go:build ps2
package runtime
import "C"
import (
"unsafe"
)
//go:extern _heap_start
var heapStartSymbol [0]byte
//go:extern _heap_end
var heapEndSymbol [0]byte
//go:extern _fdata
var globalsStartSymbol [0]byte
//go:extern _edata
var globalsEndSymbol [0]byte
//go:extern _stack_top
var stackTopSymbol [0]byte
var (
heapStart = uintptr(unsafe.Pointer(&heapStartSymbol))
heapEnd = uintptr(unsafe.Pointer(&heapEndSymbol))
globalsStart = uintptr(unsafe.Pointer(&globalsStartSymbol))
globalsEnd = uintptr(unsafe.Pointer(&globalsEndSymbol))
stackTop = uintptr(unsafe.Pointer(&stackTopSymbol))
)
func growHeap() bool {
// On baremetal, there is no way the heap can be grown.
return false
}
//export runtime_putchar
func runtime_putchar(c byte) {
putchar(c)
}
//go:linkname syscall_Exit syscall.Exit
func syscall_Exit(code int) {
// TODO
exit(code)
}
const baremetal = true
var timeOffset int64
//go:linkname now time.now
func now() (sec int64, nsec int32, mono int64) {
mono = nanotime()
sec = (mono + timeOffset) / (1000 * 1000 * 1000)
nsec = int32((mono + timeOffset) - sec*(1000*1000*1000))
return
}
func AdjustTimeOffset(offset int64) {
timeOffset += offset
}
var errno int32
//export __errno_location
func libc_errno_location() *int32 {
return &errno
}
我们需要了解其中大部分是如何工作的吗?不,我们不需要。不仅如此,其中大部分都是从普通的baremetal.go
实现中复制粘贴的。如果需要,我们可以稍后进行调整,不用担心。就像我说的那样,我们主要需要这个来_构建_,这样我们就可以找出哪里出了问题并相应地修复它。
注意:为了使这个工作,你仍然需要禁用为我们的目标构建原始的
baremetal.go
,所以我们需要将其构建标志更改为//go:build baremetal && !ps2
。
运行时
我们的目标需要一个运行时定义文件——runtime_ps2.go
。这是定义一堆特定于目标的函数的地方,包括如何实现putchar
、exit
,甚至是main
(稍后)。如果我说了算,这是代码中非常酷的部分。
一个非常基本的实现如下所示:
//go:build ps2
package runtime
/*
extern void _exit(int status);
extern void* malloc(unsigned int size);
extern void free(void *ptr);
extern void scr_printf(const char *format, ...);
*/
import "C"
import "unsafe"
// timeUnit in nanoseconds
type timeUnit int64
func initUART() {
// Unsupported.
}
func putchar(c byte) {
// This is a very hacky way of doing this. It assumes the debug screen is already active, and prints
// a whole string for a single char every single time. Very slow, but works. We can improve it later.
x := C.CString(string(c))
C.scr_printf(x)
C.free(unsafe.Pointer(x))
}
func getchar() byte {
// TODO
return 0
}
func buffered() int {
// TODO
return 0
}
func sleepWDT(period uint8) {
// TODO
}
func exit(code int) {
// This just delegates it to the ps2dev _exit(int) function.
C._exit(C.int(code))
}
func abort() {
// TODO
}
func ticksToNanoseconds(ticks timeUnit) int64 {
// TODO
return int64(ticks)
}
func nanosecondsToTicks(ns int64) timeUnit {
// TODO
return timeUnit(ns)
}
func sleepTicks(d timeUnit) {
// TODO
}
func ticks() (ticksReturn timeUnit) {
// TODO
return 0
}
其中有很多没有实现,这是故意的——我目前不会使用这些东西,所以我不在乎它们。我们可以稍后相应地实现它们,并使它们按预期工作。其中一些甚至可以通过ps2dev的C函数来完成,例如。
中断
我们需要的另一个基本文件是中断定义——interrupt_ps2.go
。我知道ps2dev已经实现了这些调用,但我选择暂时不调用它们。目前,我们不需要中断,所以我们只是实现虚拟函数:
//go:build ps2
package interrupt
type State uintptr
func Disable() (state State) {
return 0
}
func Restore(state State) {}
func In() bool {
return false
}
有了这些,我们_应该_能够构建一些Go代码。所以让我们尝试一下。
从C调用Go函数
让我们从一个简单的例子开始:让我们的C代码返回一个数字和一个字符串。没什么大不了的。我们将把它分成两部分:加载器(用C编写)和我们的Go代码。它的工作方式如下:
它起作用了!
这是我们的Go代码:
//export aGoString
func aGoString() *C.char {
return C.CString("The answer for everything is")
}
//export aGoNumber
func aGoNumber() C.int {
return C.int(42)
}
以及我们的加载器,其中包含我们的main
函数:
// Our go functions, exported before.
extern char* aGoString();
extern int aGoNumber();
int main() {
// Initialize our debug screen.
sceSifInitRpc(0);
init_scr();
// Print stuff we get from Go functions.
scr_printf("%s: %d\n", aGoString(), aGoNumber());
// Infinite loop so we keep the program running.
while (1) {}
return 0;
}
非常简单的代码,对吧?让我们构建它。
好吧,不,等等。有个问题。默认情况下,TinyGo希望你用它来生成最终的ELF(.elf
)或目标文件(.o
)。然而,ELF需要添加一个链接文件和其他一些额外的代码位,我们离此还很远。目前,我们只想以一种我们可以链接的方式获得一些函数——所以我们应该能够只使用目标文件。
然而,尝试这样做会生成一个不正确的文件:
$ tinygo build -target ps2 -o test.o
$ file test.o
test.o: ELF 32-bit LSB relocatable, MIPS, MIPS-III version 1 (SYSV), with debug_info, not stripped
请注意字符串中缺少N32
我想:哦,好吧,我们只是缺少正确的cflags
和ldflags
,对吧?所以让我们尝试添加它:
{
// (...)
"cflags": [
"-mabi=n32"
],
"ldflags": [
"-mabi=n32"
],
// (...)
这些可能不是正确的标志,但根据一些文档,它看起来像是。
$ tinygo build -target ps2 -o test.o
$ file test.o
test.o: ELF 32-bit LSB relocatable, MIPS, MIPS-III version 1 (SYSV), with debug_info, not stripped
哦。好吧。
由于TinyGo出于某种原因,表现不佳,所以我选择将它分解成我可以更轻松控制的步骤。TinyGo内部将从你的Go代码生成一些LLVM IR,然后构建它。让我们停留在LLVM IR级别:
$ tinygo build -target ps2 -o build/go.ll
这将生成一个有效的LLVM IR文件!🎉 现在我们可以手动将其构建为具有我们想要格式的目标文件:
$ clang -fno-pic -c --target=mips64el -mcpu=mips3 -fno-inline-functions -mabi=n32 -mhard-float -mxgot -mlittle-endian -o build/go.o build/go.ll
这里的标志很重要。我们的目标是一个MIPS64(只有TinyGo不满意它),小端,带有MIPS-III指令集,使用N32 ABI。它使用硬件浮点数,并且-fno-pic
和-mxgot
是为了处理链接时出现的全局偏移表大小限制问题。有了这一切,我们得到了什么:
$ file build/go.o
build/go.o: ELF 32-bit LSB relocatable, MIPS, N32 MIPS-III version 1 (SYSV), with debug_info, not stripped
终于! 从这里,我们可以用我们的C代码链接。为此,我选择使用ps2dev链接命令(从Makefile和一些测试中提取),并将我们的Go代码添加到其中:
mips64r5900el-ps2-elf-gcc \
-Tlinkfile \
-L/usr/local/ps2dev/ps2sdk/ee/lib \
-L/usr/local/ps2dev/ps2sdk/ports/lib \
-L/usr/local/ps2dev/gsKit/lib/ \
-Lmodules/ds34bt/ee/ \
-Lmodules/ds34usb/ee/ \
-Wl,-zmax-page-size=128 \
-lpatches \
-lfileXio \
-lpad \
-ldebug \
-lmath3d \
-ljpeg \
-lfreetype \
-lgskit_toolkit \
-lgskit \
-ldmakit \
-lpng \
-lz \
-lmc \
-laudsrv \
-lelf-loader \
-laudsrv \
-lc \
-mhard-float \
-msingle-float \
-o build/main.elf \
build/loader.o \
build/asm_mipsx.o \
build/go.o
Loader是我们的C代码,Go是我们的……好吧,Go代码。
注意:
asm_mipsx.o
是TinyGo提供的一些汇编代码,我只是将其复制到项目中并用clang构建。你可以在这里找到它。
有了这个,我们构建了我们的新应用程序!
$ file build/main.elf
build/main.elf: ELF 32-bit LSB executable, MIPS, N32 MIPS-III version 1 (SYSV), statically linked, with debug_info, not stripped
运行它会产生成功:
它起作用了!这是PCSX2 v2.3.223。
切换到Go的main函数
现在正在调用的main
函数不在Go中,而是在C中——这就是我们目前所称的_加载器_。然而,Go应用程序可以在没有基于C的加载器的情况下自行启动——如果我们的~~游戏~~PS2应用程序能够这样就好了!
运行时更改
允许Go应用程序在没有我们的加载器的情况下运行的第一步是让Go公开main
函数。我们可以在我们的runtime_ps2.go
中做到这一点:
//export main
func main() {
preinit()
run()
preexit()
exit(0)
}
const (
memSize = uint(24 * 1024 * 1024)
)
var (
goMemoryAddr uintptr
)
func preinit() {
// NOTE: no need to clear .bss and other memory areas as crt0 is already doing that in __start.
// Since we're loading into whatever ps2dev kernel thingy that exists, it's safer for us to do
// a proper malloc before proceeding. This guarantees that the heap location is ours. We will
// need to free it later on though.
goMemoryAddr = uintptr(unsafe.Pointer(C.malloc(C.uint(memSize))))
heapStart = goMemoryAddr
heapEnd = goMemoryAddr + uintptr(memSize)
}
func preexit() {
C.free(unsafe.Pointer(heapStart))
}
这里有一些重要的事情需要注意:
- 堆的开始和结束可以由链接器文件定义。具有讽刺意味的是,它们确实被定义了。然而,ps2dev提供的
crt0
会出于某种原因清除这些变量,使其有点损坏。 1. 我们可以假设某个内存地址之上的任何东西都是我们的,_但是_ps2dev可能想要使用更多的内存,我不想现在处理这个问题。 2. 我们将按照代码中所述,使用ps2dev的malloc
分配内存。这将保证该内存区域是我们的——如果库需要更多内存,它们应该仍然有一些剩余内存,因为PS2应该有32MB,而我们只分配了24MB。 3. 从技术上讲,我们可以按需增长堆——但这是未来我的问题。 - 我们会在使用后有意识地释放内存。不是真的需要,但是_以防万一_。
run
函数负责调用我们main
包中的main
函数。这不是我们需要处理的事情——TinyGo的代码为我们做了,我们只需要调用它。
它的工作方式基本上如下:
Exitpoint 甚至是一个词?!
这在技术上是一种混合方法:它既是裸机——因为它在没有适当的操作系统的情况下运行——但它也不是——因为它分配内存,进入和退出应用程序。
有趣的事实:一旦代码退出,它就会显示存储卡选择界面!
我们的Go代码
然后让我们用Go编写一些代码。第一步是有东西可以调用,所以让我们创建一个名为debug
的包,其中包含调试屏幕函数:
package debug
/*
extern void free(void *ptr);
extern void sceSifInitRpc(int mode);
extern void init_scr(void);
extern void scr_printf(const char *format, ...);
*/
import "C"
import (
"fmt"
"unsafe"
)
func Init() {
C.sceSifInitRpc(0)
C.init_scr()
}
func Printf(format string, args ...interface{}) {
formatted := fmt.Sprintf(format, args...)
str := C.CString(formatted)
C.scr_printf(str)
C.free(unsafe.Pointer(str))
}
是的,
free
函数有一个extern
,可以用stdlib替换。我目前避免了这样做,因为这需要为包含路径添加一些C标志,这使得它很混乱。它是这样的:
/* #cgo CFLAGS: -I/Users/ricardo/dev/ps2dev/ee/mips64r5900el-ps2-elf/include -I/Users/ricardo/dev/ps2dev/ee/lib/gcc/mips64r5900el-ps2-elf/14.2.0/include/ -I/Users/ricardo/dev/ps2dev/gsKit/include -I/Users/ricardo/dev/ps2dev/ps2sdk/common/include -I/Users/ricardo/dev/ps2dev/ps2sdk/ports/include/freetype2 -I/Users/ricardo/dev/ps2dev/ps2sdk/ports/include/zlib #include <stdlib.h> extern void sceSifInitRpc(int mode); extern void init_scr(void); extern void scr_printf(const char *format, ...); */
> 可以通过将这些标志外部移动到构建过程中来改进这一点,但这是未来我的问题,一旦这个发布。
总的来说,这没什么太疯狂的——这只是ps2dev公开的普通调试函数([在此处](https://rgsilva.com/blog/ps2-go-part-1/<https:/ps2dev.github.io/ps2sdk/ee_2debug_2include_2debug_8h.html>)声明,[在此处](https://rgsilva.com/blog/ps2-go-part-1/<https:/github.com/ps2dev/ps2sdk/blob/master/ee/debug/src/scr_printf.c>)实现)。然后我们只是调用它:
package main import ( "ps2go/debug" ) func main() { debug.Init() debug.Printf("Hello world from Go!\n") debug.Printf(`
/ | ___ _ __ _ _ _ __ _ __ () __ __ _ ___ _ __
| | _ / _ \ | '__| | | | ' | '_ | | '_ \ / ' | / _ | ' \
| || | () | | | | || | | | | | | | | | | | (| | | () | | | |
_|_/ || _,|| ||| |||| ||_, | ___/|| ||
____ _ ____ _ _ _ |/ ____
| _ | | __ _ _ / __|| | __ | |() ___ _ __ |_ \
| |) | |/ ' | | | _ | / ' | | |/ _ | ' \ __) |
| __/| | (| | || |) | || (| | || | () | | | | / /
|| ||_,|_, |/ __,|_||_/|| || ||
|/
`)
for {
// Infinite loop to not exit!
}
}
很花哨,不是吗?让我们构建代码,看看会发生什么:
$ tinygo build -target ps2 -o build/go.ll
$ clang -fno-pic -c --target=mips64el -mcpu=mips3 -fno-inline-functions -mabi=n32 -mhard-float -mxgot -mlittle-endian -o build/go.o build/go.ll
$ mips64r5900el-ps2-elf-gcc
-Tlinkfile
-L/usr/local/ps2dev/ps2sdk/ee/lib
-L/usr/local/ps2dev/ps2sdk/ports/lib
-L/usr/local/ps2dev/gsKit/lib/
-Lmodules/ds34bt/ee/
-Lmodules/ds34usb/ee/
-Wl,-zmax-page-size=128
-lpatches
-lfileXio
-lpad
-ldebug
-lmath3d
-ljpeg
-lfreetype
-lgskit_toolkit
-lgskit
-ldmakit
-lpng
-lz
-lmc
-laudsrv
-lelf-loader
-laudsrv
-lc
-mhard-float
-msingle-float
-o build/main.elf
build/asm_mipsx.o
build/go.o
很简单,不是吗?
这将构建ELF文件。现在让我们在模拟器中加载它,看看会发生什么!
 耶!
成功! 🎉
 必须爱上表情包
## DDIVU问题
在测试一些基本功能时,我注意到`fmt.Sprintf`无法正常工作。看看这个非常简单的基本代码:
func main() { debug.Init() for i := -32; i <= 32; i++ { debug.Printf("%02d, ", i) } for { // Infinite loop to not exit! } }
 嗯,这很尴尬
好的,这不正常。`-9`和`+9`之间的数字是正确的,而其他一切都是错误的。这个特定的问题花了我_几天_的时间来弄清楚到底发生了什么。我最终将其缩小到`fmt`包中的`Sprintf`使用的`fmtInteger`实现的[这一部分](https://rgsilva.com/blog/ps2-go-part-1/<https:/cs.opensource.google/go/go/+/master:src/fmt/format.go;l=243-249?q=fmtinteger&ss=go%2Fgo>):
func (f fmt) fmtInteger(u uint64, base int, isSigned bool, verb rune, digits string) { // (... bunch of code here ...) switch base { case 10: for u >= 10 { i-- next := u / 10 buf[i] = byte('0' + u - next10) u = next } // (... bunch of code here ...) }
看看TinyGo是如何为此生成LLVM IR代码的:
!875 = !DIFile(filename: "format.go", directory: "/usr/local/go/src/fmt") !15696 = !DILocalVariable(name: "next", scope: !15679, file: !875, line: 243, type: !373) ; (...) lookup.next: ; preds = %for.body %31 = udiv i64 %27, 10, !dbg !15759 #dbg_value(i64 %31, !15696, !DIExpression(), !15757) %.neg = mul i64 %31, 246, !dbg !15760 %32 = add i64 %27, 48, !dbg !15761 %33 = add i64 %32, %.neg, !dbg !15762 %34 = trunc i64 %33 to i8, !dbg !15763 %35 = getelementptr inbounds i8, ptr %.pn75, i32 %30, !dbg !15758 store i8 %34, ptr %35, align 1, !dbg !15758 #dbg_value(i64 %31, !15696, !DIExpression(), !15764) #dbg_value(i64 %31, !15684, !DIExpression(), !15765) br label %for.loop, !dbg !15700
希望这是代码的正确部分,哈哈
这一切看起来都很好。深入研究后,有一个特定的东西:`udiv i64 %27, 10`——这是一个64位整数除以10的无符号除法。记住这个64位部分。
这将生成以下MIPS汇编代码:
.LBB139_23: # %lookup.next # in Loop: Header=BB139_19 Depth=1 #DEBUG_VALUE: (*fmt.fmt).fmtInteger:i <- [DW_OP_plus_uconst 176] [$sp+0] #DEBUG_VALUE: (*fmt.fmt).fmtInteger:u <- [DW_OP_plus_uconst 184] [$sp+0] #DEBUG_VALUE: (*fmt.fmt).fmtInteger:negative <- [DW_OP_plus_uconst 332] [$sp+0] #DEBUG_VALUE: (*fmt.fmt).fmtInteger:digits <- [DW_OP_LLVM_fragment 32 32] 17 #DEBUG_VALUE: (*fmt.fmt).fmtInteger:base <- [DW_OP_plus_uconst 316] [$sp+0] #DEBUG_VALUE: (*fmt.fmt).fmtInteger:verb <- [DW_OP_plus_uconst 312] [$sp+0] #DEBUG_VALUE: (*fmt.fmt).fmtInteger:digits <- [DW_OP_plus_uconst 308, DW_OP_LLVM_fragment 0 32] [$sp+0] .loc 129 0 7 is_stmt 0 # format.go:0:7 lw $1, 176($sp) # 4-byte Folded Reload lw $4, 272($sp) # 4-byte Folded Reload ld $3, 184($sp) # 8-byte Folded Reload daddiu $2, $zero, 10 .loc 129 243 14 is_stmt 1 # format.go:243:14 ddivu $zero, $3, $2 teq $2, $zero, 7 mflo $2
让我们忽略大部分内容,专注于一个特定的事情:`ddivu $zero, $3, $2`。看起来是正确的,不是吗?
好吧……让我们看看PCSX2如何加载这个: