[ Beneath the Waves ]

TDR: Need For Speed 4

article and software by Ben Lincoln

 

This is a basic walkthrough of decompiling the 1999-02-22 prototype version of Need For Speed 4 using Ghidra and This Dust Remembers What It Once Was.

  1. In this version of Need For Speed 4, everything is already named in a way that requires no changes. Copy NFS4.EXE, and NFS4.SYM to your working directory.
  2. Open a command prompt or PowerShell prompt and change directory to your working directory.
  3. Convert NFS4.EXE to ELF format by running the following command:

    PlayStationELFConverter.exe --exe2elf NFS4.EXE NFS4.ELF > PlayStationELFConverter_Log.txt 2>&1

  4. Generate the JSON version of the debug symbols by running the following command:

    SymDumpTE.exe --rename-for-compatibility --json NFS4.SYM NFS4.json > SymDumpTE_Log.txt 2>&1

  5. Generate the monolithic header and Ghidra Java scripts by running the following command:

    CreateSkeleton.exe --create-playstation-memory --assume-sn-gp-base --name NFS4 --externs-to-labels --output Output NFS4.json > CreateSkeleton_Log.txt 2>&1

  6. Similar to Soul Reaver, there are two minor changes that are necessary, and they're detailed below. These changes are easiest to make in the NFS4.H file, so you should not re-run the CreateSkeleton.exe command afterward or it will overwrite your changes. Skip down to that section, then come back here when you've made the changes.
  7. Launch Ghidra, and create a new project. For the base directory, use the Output directory created by TDR. For the project name, use NFS4.
  8. Import the ELF file you generated earlier. Ghidra will default to 64-bit MIPS, which is wrong. Click the ... button next to the Language field. Scroll up in the list and choose MIPS/default/32/little/default processor architecture, which will show up as MIPS:LE:32:default:default in the import file window. Click OK to begin the import.
  9. Close the import summary dialogue.
  10. Double-click on NFS4.ELF in the project list.
  11. An Analyze prompt will appear. Click No, because you don't want that to happen until the debug symbols have been imported.
  12. From the Edit menu, choose Tool Options.
  13. Expand Decompiler, and select Analysis. Uncheck Eliminate unreachable code. Click OK.
  14. From the File menu, choose Parse C Source option. Click the green plus sign button. Open the NFS4.H file in the Output/source-stubs directory. Click Parse to Program.
  15. Click Parse to Program. Click Continue. Click Continue?.
  16. After a moment, you should receive a message indicating that the header has been parsed successfully. If you don't, make sure you resolved any naming conflicts in the JSON file, re-run the CreateSkeleton.exe above, and then re-import the NFS4.H file. Otherwise, Click OK, then click Dismiss.
  17. Copy the NFS4TDRDefineFunctions.java and NFS4TDRDecompile.java scripts from the Output/ghidra_files/ directory into your own Ghidra scripts directory (probably something like C:\Users\yourname\ghidra_scripts). Note: these files are dynamically generated, so you will need to re-copy them (overwriting the existing copies if necessary) every time they change, or when working on multiple projects.
  18. In Ghidra, from the Window menu, choose Script Manager option.
  19. In the Script Manager window, click on the the NFS4TDRDefineFunctions.java entry, then click the green-and-white play button in the upper-right corner of the window.
  20. After a noticeable delay, you should see a NFS4TDRDefineFunctions.java> Finished! message in the console at the bottom of the main Ghidra window.
  21. From the Analysis menu, choose Auto Analyze 'NFS4.ELF'. Check the Decompiler Parameter ID box if it's not already checked. Switch to the MIPS Constant Reference Analyzer section. Uncheck Recover global GP register writes if it's checked. Optionally, check Attempt to recover switch tables. Click Analyze.
  22. Wait for the analysis to complete (progress is in the lower-right corner of the main Ghidra window.
  23. This should be enough for a basic demonstration of the toolchain. However, if you're really trying to fully reverse-engineer the game, at this point, you would do all of that work in Ghidra. That will take awhile, and is outside the scope of this walkthrough.
  24. When you're ready to proceed with generating source code, go back to the Script Manager window, and run the NFS4TDRDecompile.java script.
  25. Wait for the decompilation to happen. This will usually take awhile. You should see a NFS4TDRDecompile.java> Finished! message in the console at the bottom of the main Ghidra window when it's complete.
  26. Back in the command prompt, create another set of C source code files which contain the decompiled functions output by Ghidra by running the following command:

    PopulateSkeleton.exe --name NFS4 --input-json NFS4.json --input-source Output\NFS4.C --output Output > PopulateSkeleton_Log.txt 2>&1

  27. Examine the contents of Output/source-decompiled, which should contain TDR's best attempt at reconstructing the original source code in all of the separate files that were originally used. Anything not matched to one of those files will be placed in Unmatched_Decompiled_Functions.C instead.

As discussed above, there are two minor changes that need to be made in order for Ghidra to process the data from TDR without throwing errors.

Unlike Soul Reaver, it's easiest to make these changes in the NFS4.H file, rather than NFS4.json.

The first issue is that quite a few places in the code refer to a struct named __vtbl_ptr_type, but that struct is not defined in the debug symbols for the game, and it is not a default struct in Ghidra. As a workaround, open Output\source-stubs\NFS4.H in a text editor and make the following change:

Locate the line which reads /* Begin structs */. Immediately below this line, add the following struct definition:

struct __vtbl_ptr_type

{

unsigned int value;

};

The second issue is that the game explicitly declares wchar_t, and this conflicts with Ghidra's definition of that type.

Locate the following line:

typedef unsigned char wchar_t;

Comment out or delete that line.

Proceed with parsing the header file in Ghidra and running the generated scripts, as discussed above.

As of 0.6, need to comment these out:

struct MATRIX {

short (m[3])[3]; long t[3]; };

actually, this is a bug, should be:

struct MATRIX // hashcode: 0x3BD346F0 (dec: 1003702000)

{

short m[3][3]; // size=18, offset=0 // hashcode: 0x00000000 (dec: 0), parent name: 'struct MATRIX', parent hashcode: 0x00000000 (dec: 0)

long t[3]; // size=12, offset=20 // hashcode: 0x00000000 (dec: 0), parent name: 'struct MATRIX', parent hashcode: 0x00000000 (dec: 0)

};

Why is this . not getting replaced?

struct AIDataRecord_t {

int numElements_; int bSize_; char name_[64]; char *dataBuffer_; char *preAllocatedBuffer_; enum AIDataRecord_RecordMethod_t recordMethod_; struct __vtbl_ptr_type (*.vf)[3]; };

 
[ Page Icon ]