Skip to content

Instantly share code, notes, and snippets.

@EllipticEllipsis
Last active January 21, 2023 01:10
Show Gist options
  • Select an option

  • Save EllipticEllipsis/27eef11205c7a59d8ea85632bc49224d to your computer and use it in GitHub Desktop.

Select an option

Save EllipticEllipsis/27eef11205c7a59d8ea85632bc49224d to your computer and use it in GitHub Desktop.
Converting a MIPS decompilation project to ABI FPR names

Converting a MIPS decompilation project to ABI FPR names

What?

If you've worked on a Nintendo 64 project, you're probably used to the floating-point registers in MIPS assembly being numbered, in a rather random-looking way, as the even numbers from 0-30. Some seem to be used for returns, some for arguments, some for temps, but there doesn't seem to be any much logic to it. And indeed there isn't, especially for the temp registers:

  • f0,f2 are return values (like v0,v1)
  • f12,f14 are function arguments (like a0,a1,...)
  • The other even-numbered ones are temporary and saved registers
  • Odd-numbered ones show up occasionally if doubles are about

Wouldn't it be nice if there was a better way of labelling them? There actually is: the o32 ABI specifies names for the floating-point registers in the same format as the general-purpose registers:

Numeric o32 Notes
$f0 $fv0 First return value
$f2 $fv1 Second return value
$f4 $ft0 Temporary
$f6 $ft1 "
$f8 $ft2 "
$f10 $ft3 "
$f12 $fa0 Argument
$f14 $fa1 "
$f16 $ft4 Temporary
$f18 $ft5 "
$f20 $fs0 Saved
$f22 $fs1 "
$f24 $fs2 "
$f26 $fs3 "
$f28 $fs4 "
$f30 $fs5 "

and their corresponding odd ones by suffixing an f, i.e. $f1 = $fv0f etc.

Why?

It's simple to do and makes that damn float regalloc a lot easier to understand (if not fix).

You might also ask yourself if you'd put up with this for the GPRs: just because the default is to use the ABI names for the GPRs and numeric for the FPRs, doesn't mean it's sensible.

What is progress on support?

The good news is that almost all of the common Nintendo 64 tools have already been made compatible:

  • mips_to_c
  • asm-differ
  • asm-processor
  • decomp-permuter
  • decomp.me
  • spimdisasm
  • splat

The following still need support to be added:

  • debuggers
  • old assemblers

There is also the possibility that GAS (i.e. the GNU assembler) may support them in future, although it is a long and winding road to get anything accepted to GCC.

How can I move my project over?

prelude.inc/macro.inc

You will need to update this file to include the aliases, since no assembler seems to recognise the ABI names. It is sufficient to add

.set $fv0,          $f0
.set $fv0f,         $f1
.set $fv1,          $f2
.set $fv1f,         $f3
.set $ft0,          $f4
.set $ft0f,         $f5
.set $ft1,          $f6
.set $ft1f,         $f7
.set $ft2,          $f8
.set $ft2f,         $f9
.set $ft3,          $f10
.set $ft3f,         $f11
.set $fa0,          $f12
.set $fa0f,         $f13
.set $fa1,          $f14
.set $fa1f,         $f15
.set $ft4,          $f16
.set $ft4f,         $f17
.set $ft5,          $f18
.set $ft5f,         $f19
.set $fs0,          $f20
.set $fs0f,         $f21
.set $fs1,          $f22
.set $fs1f,         $f23
.set $fs2,          $f24
.set $fs2f,         $f25
.set $fs3,          $f26
.set $fs3f,         $f27
.set $fs4,          $f28
.set $fs4f,         $f29
.set $fs5,          $f30
.set $fs5f,         $f31

to the bottom.

Assembly

If you are using transient assembly files, you are essentially on your own implementing the names in your disassembler.

If you have assembly in the repo, you can just run this python script on the assembly folders to swap over.

mips_to_c

As of 28 March (commit d74ba87), has automatic support, will automatically use the names from the assembly file to name the phis and temps, and can take ABI names as --reg-vars.

asm-differ

As of 23 February (commit 07e617e) has support, but needs an extra objdump option: add

    config['objdump_flags'] = ['-M','reg-names=32']

to diff_settings.py to output the ABI names.

asm-processor

As of 27 March (commit fd28ec1), has automatic support; if you are not subrepoing/submoduling it, you will need to copy across the new prelude.inc or include your project-wide one.

decomp-permuter

Supports as of 30 March (commit 85644d6). No special changes are needed apart from updating.

decomp.me

As of the 5 April 2022 update, supports input, diff display with them, and output them with its copy of mips_to_c. This can be enabled under "Scratch options": image

They can also be added as the default output for a project.

spimdisasm

As of 20 May 2022 (commit ae5dc3f). Support as the command-line options --Mfpr-names o32 or --Mreg-names o32

splat

Supported as of version 0.9.0, via mips_abi_float_regs: o32 as a top-level option in the yaml.

mips-linux-gnu-objdump

If you use this directly e.g. for troubleshooting, you can get it to output the ABI names with the

-Mreg-names=32

flag.

N.B.

If your project uses a different ABI, the float registers are different! Currently we have only worked on implementing the o32 ABI's names, since that is the one the Nintendo 64 uses, and most of the projects we are involved in are Nintendo 64.

Appendix: what is an ABI, anyway?

Essentially an ABI is a set of rules to the compiler for how to construct the assembly, including

  • width of the registers
  • which registers to treat as saved/temporary (i.e. which it can consider preserved by function calls and which it has to save manually)
  • which registers are used for arguments/returns
  • how to treat the stack

etc. The processor is not aware of the ABI itself but it may have flags that need to be set for it to comply with the rules of the ABI (such as one that tells it to use 32-bit registers, for example). (It is also useful to note that the ABI can only change so much: it cannot change how the RA register works, for example.)

There are three ABIs available for Nintendo 64-era MIPS:

  • o32: 32-bit wide registers, must reserve stack for arguments, 4 argument registers, even float registers are used except for doubles, which use pairs.
  • n32: 64-bit wide registers, 32-bit pointers, registers completely rearranged (e.g. 8 argument registers available).
  • n64: everything 64-bit, float registers different again.

Most importantly, all Nintendo 64 code is compiled for the o32 ABI, because libultra was only distributed compiled with o32 ABI, and code from different ABIs cannot be mixed (due to the differing calling conventions).

Because the registers change categories between ABIs, there are different mappings from the numeric values to the register names, which objdump offers with the -Mreg-names option (as well as the more restrictive -Mgpr-names and -Mfpr-names options, which can take one of numeric, 32, n32, 64. For tables of the different register uses/names in different ABIs, you can consult MIPS registers.

For some reason, the general-purpose registers get their ABI names by default in objdump (and these names are accepted by the GNU assembler), but the floating-point registers do not. Yet, anyway.

MIPS registers

There are 3 MIPS ABIs:

  • o32 (old/original 32-bit)
  • n32 (new 32-bit: 64-bit registers, 32-bit addresses)
  • n64 (64-bit)

General-purpose registers

Numeric o32 n32/n64 Notes
$0 zero zero Always 0
$1 at at Reserved for assembler
$2 v0 v0 First return value
$3 v1 v1 Second return value
$4 a0 a0 Argument registers
$5 a1 a1 "
$6 a2 a2 "
$7 a3 a3 "
$8 t0 a4 Temporary (o32) / arguments (n64)
$9 t1 a5 "
$10 t2 a6 "
$11 t3 a7 "
$12 t4 t0 Temporary
$13 t5 t1 "
$14 t6 t2 "
$15 t7 t3 "
$16 s0 s0 Saved
$17 s1 s1 "
$18 s2 s2 "
$19 s3 s3 "
$20 s4 s4 "
$21 s5 s5 "
$22 s6 s6 "
$23 s7 s7 "
$24 t8 t8 "
$25 t9 t9 "
$26 k0 k0 Reserved for kernel
$27 k1 k1 "
$28 gp gp Global pointer
$29 sp sp Stack pointer
$30 fp/s8 fp/s8 Frame pointer (or an extra s reg)
$31 ra ra Return address

Floating-point registers

Numeric o32 n32 n64 Notes
$f0 fv0 fv0 fv0 First return value
$f1 fv0f ft14 ft12
$f2 fv1 fv1 fv1 Second return value
$f3 fv1f ft15 ft13
$f4 ft0 ft0 ft0
$f5 ft0f ft1 ft1
$f6 ft1 ft2 ft2
$f7 ft1f ft3 ft3
$f8 ft2 ft4 ft4
$f9 ft2f ft5 ft5
$f10 ft3 ft6 ft6
$f11 ft3f ft7 ft7
$f12 fa0 fa0 fa0
$f13 fa0f fa1 fa1
$f14 fa1 fa2 fa2
$f15 fa1f fa3 fa3
$f16 ft4 fa4 fa4
$f17 ft4f fa5 fa5
$f18 ft5 fa6 fa6
$f19 ft5f fa7 fa7
$f20 fs0 fs0 ft8
$f21 fs0f ft8 ft9
$f22 fs1 fs1 ft10
$f23 fs1f ft9 ft11
$f24 fs2 fs2 fs0
$f25 fs2f ft10 fs1
$f26 fs3 fs3 fs2
$f27 fs3f ft11 fs3
$f28 fs4 fs4 fs4
$f29 fs4f ft12 fs5
$f30 fs5 fs5 fs6
$f31 fs5f ft13 fs7

The ..f registers are used very rarely (but are used, for the other half of a double).

Simplified o32 version excluding the odd registers:

Numeric o32 Notes
$f0 fv0 First return value
$f2 fv1 Second return value
$f4 ft0 Temporaries
$f6 ft1 "
$f8 ft2 "
$f10 ft3 "
$f12 fa0 Argument
$f14 fa1 "
$f16 ft4 Temporary
$f18 ft5 "
$f20 fs0 Saved
$f22 fs1 "
$f24 fs2 "
$f26 fs3 "
$f28 fs4 "
$f30 fs5 "
#!/usr/bin/env python3
import os
import argparse
regReplace = {
"$f0": "$fv0",
"$f1": "$fv0f",
"$f2": "$fv1",
"$f3": "$fv1f",
"$f4": "$ft0",
"$f5": "$ft0f",
"$f6": "$ft1",
"$f7": "$ft1f",
"$f8": "$ft2",
"$f9": "$ft2f",
"$f10": "$ft3",
"$f11": "$ft3f",
"$f12": "$fa0",
"$f13": "$fa0f",
"$f14": "$fa1",
"$f15": "$fa1f",
"$f16": "$ft4",
"$f17": "$ft4f",
"$f18": "$ft5",
"$f19": "$ft5f",
"$f20": "$fs0",
"$f21": "$fs0f",
"$f22": "$fs1",
"$f23": "$fs1f",
"$f24": "$fs2",
"$f25": "$fs2f",
"$f26": "$fs3",
"$f27": "$fs3f",
"$f28": "$fs4",
"$f29": "$fs4f",
"$f30": "$fs5",
"$f31": "$fs5f",
}
# [a-zA-Z0-9_]
def is_word_char(c):
return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '_'
def replace_single(file):
with open(file, 'r', encoding = 'utf-8') as infile:
srcdata = infile.read()
changesCount = 0
for old, new in regReplace.items():
# replace `old` with `new` if the occurence of `old` is the whole word
oldStartIdx = srcdata.find(old)
if oldStartIdx >= 0:
old_start_as_word = is_word_char(old[0])
old_end_as_word = is_word_char(old[-1])
replaceCount = 0
while oldStartIdx >= 0:
replace = True
if old_start_as_word:
if oldStartIdx == 0:
pass
elif is_word_char(srcdata[oldStartIdx-1]):
replace = False
if old_end_as_word:
oldEndIdx = oldStartIdx + len(old)
if oldEndIdx >= len(srcdata):
pass
elif is_word_char(srcdata[oldEndIdx]):
replace = False
if replace:
srcdata = srcdata[:oldStartIdx] + new + srcdata[oldEndIdx:]
replaceCount += 1
oldStartIdx = srcdata.find(old, oldStartIdx + len(new))
if replaceCount > 0:
changesCount += 1
print(old, "->", new)
if changesCount > 0:
print('Changed', changesCount, 'entry' if changesCount == 1 else 'entries', 'in', file)
with open(file, 'w', encoding = 'utf-8', newline = '\n') as outfile:
outfile.write(srcdata)
def replace_all(dir):
for subdir, dirs, files in os.walk(dir):
for filename in files:
file = subdir + os.sep + filename
replace_single(file)
def main():
parser = argparse.ArgumentParser(description='Apply function renames to a file')
parser.add_argument('dir', help="directory to be processed.")
args = parser.parse_args()
replace_all(args.dir)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment