11 March 2016 - Forum Rules

Main Menu

N64 memory map howto?

Started by lmao_horses, November 25, 2014, 09:25:27 AM

Previous topic - Next topic


How would one go about figuring out pointers that don't actually go to anything directly in the ROM, like figuring out what it is essentially relative to (what I would have to -/+ to these to get an full ROM offset)

The simpler tables like 0x217640,0x222BA0,0x5193A0 in a Buck Bumble US rom I can spot from mile away, what I don't know how to go about are things like the following:

Gex 64 US:
0x708E0 - map data pointers 5 uint32s, and an 8 char ascii string
0x745F0 - 0x8 ascii object name, uint32 start offset, uint32 endoffset, total of 0x310 objects

Gex 3 US:
0x818C0 - 0x8 ascii object name, uint32 start offset, uint32 endoffset, total of 0x403 objects

Result of switching a few around:


  Funny thing, those offsets in Gex3 are correct.  It's compressed data (headerless zlib) ;*)

  Good question though.  You have to analyze the code.
  First, figure out the address they stuck the table at in rdram.  In the case of Gex3 (USA), that's 80080CC0.
This doesn't work for all titles, but for anything that's "always loaded" as part of the runtime you can compute this without doing a search in memory or other horrible thing.  In the ROM header, the pointer at +8 is the run address used when the game first starts or is reset.  With the exception of 1-2 CIC chips that throw this value off intentionally, that's where the code starting at 0x1000 in the ROM can be found.  So, add that pointer, subtract 0xC00, and you'll likely have the right address.  Works for Gex3: 80000400 + 0x818C0 - 0xC00 = 80080CC0.

  There's a couple ways to go about this.  The lazy way is to run the game in a debugger and set a read breakpoint on some of the values. 
  Another way is to search for usage.  It's terribly rare you wind up with just a pointer to the table.  The most likely scenario is they hardcoded the pointer in ASM, then add offsets to the base value to get entries and elements.  In this case, you need to know a little about ASM.
  The typical way to make a pointer is to load two halfwords into the same register, like:
LUI V0,8008
ADDIU V0,V0,0CC0 ;V0=80080CC0
LUI V0,8008
LW V1,0CC8 (V0) ;V1=@80080CC8: 0x159C4C0

  The tricky part is that both of these treat the lower HW as a signed value.  If hte bottom part is 0x8000 through 0xFFFF, you need to increase the upper part by 1.  An example:
goal: load a word from 800FDCD0
LUI V0,800F
LW V1,DCD0 ;V1=@800EDCD0: 800F0000+FFFFDCD0
LUI V0,8010
LW V1,DCD0 ;V1=@800FDCD0: 80100000+FFFFDCD0

Technically the LUI also treats the upper value as a signed value.  Point of interest:
Pointers are actually 64bit on the N64 despite it being in 32bit addressing mode.  LUI 8*** treats the value as a negative number, setting the upper 32bits will be FFFFFFFF.  Not setting them implies a hardware address.  On console not setting these is fatal, but virtually all emulators will presume you meant rdram.  Never, ever load a pointer using LWU!

  The point here is that the values are split.  You'll want to search for one half, then eyeball the surrounding code for the other half.  It's easiest done looking at disassembly, unless you're good at working it out without one.  You're looking for a 3C**8008, ****0CC0 for pulling filenames, then 3C**8008, ****0CC8/C for the values.  They aren't necessarily going to be near each other either.
  One somewhat helpful tool is the search mask feature in HexEdit.  You can mask away the registers from the opcodes, and search at 16 and 32bit aligned addresses.  It's not a super-user-friendly hex editor, but there's some nice special features hard to find in another freebie.

(Caveat: never looked at this one before and was working rather fast and distracted, so could be wrong about some of this.)
  So, searching for filename hits Gex3 you'll get only two hits, both in the same function:
80031128(string): find filename A0 in filetable and load file

  Within that, at 800312A8, is where the values are loaded.

A0 = p->buffer on the stack for the decompression object
S1 = start offset
@800FDCD0 = p->target, [spoiler]aligned to a QW boundry so the cache doesn't scream[/spoiler]
S2 = end - start, or compressed size of file

801DA920(p->object, p->target, available size of target)
  Should initialize a decompression object saved to the buffer A0, setting target to A1 of size A2.
801DA800(p->object, start, size)
  Basically a wrapper for the decompressor itself.  It works on blocks 0x4000 at a time and manages all that on the side.
  80031E70() loads data, 801DAAE0() decompresses

80031E70(hardware address, p->target, size aligned to DW)
  Posts a hardware read request for A2 bytes from hardware A0 to rdram A1
  80069B80(p->target, size) invalidates the cache before any magic happens
  8006C3D0(aegh...lazy...) posts a message to the PI request thread to pull the data
  80069F50(p->queue, p->target, mode) waits(1) until message queue A0 stuffs a message from the PI request thread in A1

801DAAE0(...aegh...still lazy...)
  Decompresses data.  Uses a pointer table at 801DC688 based on "modes".

  Long story short, follow that around and you find the zlib tables.  You can also tell by the routine: first bit is a "final block" bit, next two the type of block (store, lz+huff, lz IIRC), plus the annoying bitwise nature of it all.  They're all headerless, which should be type "Z".