Apploader

From GC-Forever Wiki
Jump to navigation Jump to search

Gamecube apploader reverse engineering for CubeDocumented project

This reversing is not pretending to be compilable, and can be used for education purposes only. I dont hold any responsibility for damage of your Gamecube clock settings or memory card saves or else, produced from using code/information represented in this document.

Documented by Andrei Chestakov (org). Version 1.1. (26 Apr 2005) Additions and corrections by tmbinc.

Overview

Complete Luigi's Apploader. Reverse work by org <kvzorganic_at_mail_dot_ru> Other games seems to contain the same code with minor changes due to revision, so you may think, that apploader is the same for all games. Except Datel's FreeLoader and ActionReplay DVDs, which are using custom apploader binary (simplified Nintendo's apploader).

Apploader is a small program layer between GC bootrom and main application. Since main application's executable binary format may vary, apploader is needed to boot executable in main memory, using bootrom's low-level DVD read routines. In practice, all known game apploaders are using Nintendo's DOL format for executable files.

Format of apploader binary is simple. First goes apploader header, and then actual apploader code.

Apploader header

typedef struct
{
   char    revision[16];       // Revision compile date (example "2001/12/17")
   u32     entryPoint;         // Apploader's entrypoint
   s32     size;               // Size of apploader code
   s32     trailerSize;        // Size of safe trailer
   u8      padding[4];         // zeroes
} LDRImage;

Full apploader size is sum of size and trailerSize, rounded up to 32 bytes. Trailer is need for future apploader versions.

Apploader is located at 0x2440 offset on DVD and loaded by bootrom at 0x81200000 address. Then bootrom jumps to apploader's entrypoint:

void AplEntry(
   void    (**Init)(void (*OSReportCallback)(char *msg, ...)),
   BOOL    (**Main)(void **addr, s32 *length, s32 *offset),
   void*   (**Close)())
{                                      
   // Return location of apploader's Init, Main and Close routines.
   *Init  = AplInit;
   *Main  = AplMain;
   *Close = AplClose;
}

After executing of entrypoint, bootrom will call in order: apploader's Init, then Main, then Close. Main is called in loop, until it will not return 0.

PART I. Apploader Init

Init call is clearing apploader's internal data, and saving there callback of OSReport call (which is located inside IPL). OSReport is used to output some debug messages, during apploader runtime.

Apploader's internal data:

   // Some DVD transfers require multiple times to load.
   BOOL        SecondTimeForThePart;
   void*       SecondTimeAddr;
   s32         SecondTimeLength;
   s32         SecondTimeOffset;
   int         SecondTimePart;

   DVDBB2      bb2;        // DVD boot block
   DolImage    DolImage;   // main DOL executable header

   int         Section;    // DOL section
   int         Part;       // Apploader's step
   u32         +0x148      // ?? only cleared in Init

   // OSReport routine itself is placed somewhere in IPL
   void        (*OSReport)(char *msg, ...);

#pragma pack(32)
   // First 32 bytes of DVDBI2 will be loaded here. This is a bit hacky.
   // We dont use whole DVDBI2 (its too big), to save some memory.
   // 32 bytes - because its minimum DVD DMA transfer size (?)
   s32         debugMonSize;
   s32         simMemSize;
   u32         argOffset;
   u32         debugFlag;
   int         FSTHigh;    // 1, if FST is resided on DVD above DOL
   u8          dummy[12];  // ending bytes
#pragma pack(0)

void AplInit(void (*OSReportCallback)(char *msg, ...))
{
   // Clear some structures
   memset(&bb2, 0, sizeof(DVDBB2));
   memset(&DolImage, 0, sizeof(DolImage));

   Section = 0;
   Part = 0;
   [+0x148] = 0;

   OSReport = OSReportCallback;    // Save report callback

   // Print apploader version info
   OSReport("Apploader Initialized.  $ Revision: 28 $\n");
   OSReport("This Apploader built %s %s\n", __DATE__, __TIME__);
}

PART II. Apploader Main

Main is called in loop by IPL, until all data not loaded. IPL is using Main output parameters, to load data from DVD. IPL keeps loading new data, until Main will not return 0, signaling that all data loaded. Just read through comments to understand code.

BOOL AplMain(void **addr, s32 *length, s32 *offset)
{
   OSLoMem *LoMem = OSPhysicalToCached(0x0000);

   switch(Part)
   {
       // Load BB2 structure.
       case 0:
       case 1:
           *addr = &bb2;
           *length = sizeof(DVDBB2);
           *offset = DVD_BB2_OFFSET;

           Part = 2;
           DCInvalidateRange(*addr, *length);
           break;

       // Check FST length and load first 32 bytes of BI2 structure.
       case 2:
           u32 FSTLength = bb2.FSTLength;
           u32 FSTMaxLength = bb2.FSTMaxLength;

           if(FSTLength > FSTMaxLength)
           {
               OSReport(
                   "APPLOADER ERROR >>> FSTLength(%d) in BB2 is greater "
                   "than FSTMaxLength(%d)\n", FSTLength, FSTMaxLength );
               PPCHalt();
           }

           *addr   = &debugMonSize;
           *length = 32;
           *offset = DVD_BI2_OFFSET;

           Part = 3;
           DCInvalidateRange(*addr, *length);
           break;

       // Setup some OS lomem variables by BI2 values, and check memory configuration.
       // Copy DVD BI2 in main memory.
       case 3:
           __OSDebugMonSize = debugMonSize;
           __OSDebugMon = OSRoundDown32B(__OSPhysMemSize - __OSDebugMonSize);
           __OSSimMemSize   = simMemSize;

           // Check debug monitor size alignment
           if(__OSDebugMonSize & 31)
           {
               OSReport(
                   "APPLOADER ERROR >>> Debug monitor size (%d) should "
                   "be a multiple of 32\n", __OSDebugMonSize );
               PPCHalt();
           }

           // Check console simulated memory size alignment
           if(__OSSimMemSize & 31)
           {
               OSReport(
                   "APPLOADER ERROR >>> simulated memory size (%d) "
                   "should be a multiple of 32\n", __OSSimMemSize );
               PPCHalt();
           }

           if(__OSSimMemSize == 0)
           {
               __OSSimMemSize = __OSPhysMemSize;
           }

           if(__OSSimMemSize < __OSPhysMemSize)
           {
               if(__OSDebugMonSize >= (__OSPhysMemSize - __OSSimMemSize))
               {
                   OSReport(
                       "APPLOADER ERROR >>> [Physical memory size(0x%x)] "
                       "- [Console simulated memory size(0x%x)]\n"
                       "APPLOADER ERROR >>> must be greater than debug 
                       "monitor size(0x%x)\n", 
                       __OSPhysMemSize, __OSSimMemSize, __OSDebugMonSize );
                   PPCHalt();
               }

               // Move down FST address
               bb2.FSTAddress = OSRoundDown32B(__OSSimMemSize - bb2.FSTMaxSize);
           }
           else
           {
               if(__OSSimMemSize == __OSPhysMemSize)
               {
                   bb2.FSTAddress = OSRoundDown32B(__OSDebugMon - bb2.FSTMaxSize);
               }
               else
               {
                   OSReport(
                       "APPLOADER ERROR >>> Physical memory size is 0x%x bytes.\n"
                       "APPLOADER ERROR >>> Console simulated memory size "
                       "must be smaller than or equal to the Physical memory size\n",
                       __OSPhysMemSize, __OSSimMemSize );
                   PPCHalt();
               }
           }

           // Load whole BI2 in main memory. Reside it before FST.
           __OSBootInfo2 = bb2.FSTAddress - OS_BI2_SIZE;

           *addr   = __OSBootInfo2;
           *length = OS_BI2_SIZE;
           *offset = DVD_BI2_OFFSET;

           Part = 4;
           DCInvalidateRange(*addr, *length);
           break;

       // Load FST, only if FST placed before boot file.
       case 4:
           FSTHigh = bb2.bootFilePosition < bb2.FSTPosition;
           if(FSTHigh)
           {
               // Continue on part 5. Load FST later.
               Part = 5;
           }
           else
           {
               *addr   = bb2.FSTAddress;
               *length = OSRoundUp32B(bb2.FSTLength);
               *offset = bb2.FSTPosition;

               Part = 5;

               if(*addr > 0x81700000)
               {
                   OSReport(
                       "APPLOADER ERROR >>> Illegal FST "
                       "destination address! (0x%x)\n", *addr );
                   PPCHalt();
               }

               DCInvalidateRange(*addr, *length);
               LoadBigData(addr, length, offset, &Part);
               break;
           }

       // Load main DOL header.
       case 5:
           *addr   = &DolImage;
           *length = sizeof(DolImage);
           *offset = bb2.bootFilePosition;

           Part = 6;
           DCInvalidateRange(*addr, *length);
           break;

       // Check out DOL properties. Clear BSS.
       case 6:
           s32 dolSize = DOLSize();

           // Halt apploader, if DOL size exceeds limit.
           if( (dolSize > __OSBootInfo2->dolLimit) && __OSBootInfo2->dolLimit)
           {
               OSReport(
                   "APPLOADER ERROR >>> Total size of text/data sections "
                   "of the dol file are too big (%d(0x%08x) bytes). "
                   "Currently the limit is set as %d(0x%08x) bytes\n",
                   dolSize, __OSBootInfo2->dolLimit );
               PPCHalt();
           }

           // Save DOL size in OS globals
           if(FSTHigh) __OSDOLSize = dolSize + OSRoundUp32B(bb2.FSTLength);
           else __OSDOLSize = dolSize;

           // Address limit?
           #define DOL_ADDRESS_LIMIT   0x80700000  // production boards
           #define DOL_ADDRESS_LIMITD  0x81200000  // development boards
           if(!(OSGetConsoleType() & OS_CONSOLE_DEVELOPMENT))
           {
               if(AddressLimit(DOL_ADDRESS_LIMIT) == FALSE)
               {
                   OSReport(
                       "APPLOADER ERROR >>> One of the sections in the dol file "
                       "exceeded its boundary. All the sections should not exceed "
                       "0x%08x (production mode).\n", DOL_ADDRESS_LIMIT );
                   PPCHalt();
               }
           }
           else
           {
               // Check twice : first for production mode, then for development.

               if(AddressLimit(DOL_ADDRESS_LIMIT) == FALSE)
               {
                   OSReport(
                       "APPLOADER WARNING >>> One of the sections in the dol file "
                       "exceeded its boundary. All the sections should not exceed "
                       "0x%08x in production mode.\n", DOL_ADDRESS_LIMIT );
                   // Do not halt apploader (show only warning).
               }

               if(AddressLimit(DOL_ADDRESS_LIMITD) == FALSE)
               {
                   OSReport(
                       "APPLOADER ERROR >>> One of the sections in the dol file "
                       "exceeded its boundary. All the sections should not exceed "
                       "0x%08x (development mode).\n", DOL_ADDRESS_LIMITD );
                   PPCHalt();
               }
           }

           // Clear BSS
           memset(DolImage.bss, 0, DolImage.bssLen);
           DCFlushRange(DolImage.bss, DolImage.bssLen);

           Part = 7;

       // Load text sections.
       case 7:
           if(Section<DOL_MAX_TEXT)
           {
               if(DolImage.textData[Section])
               {
                   *addr   = DolImage.text[Section];
                   *length = OSRoundUp32B(DolImage.textLen[Section]);
                   *offset = bb2.bootFilePosition + DolImage.textData[Section];

                   if( (*addr + *length) > 0x81200000 )
                   {
                       OSReport(
                           "APPLOADER ERROR >>> Too big text segment! (0x%x - 0x%x)\n",
                           *addr, *addr + *length );
                       PPCHalt();
                   }

                   Section++;
                   DCInvalidateRange(*addr, *length);
                   break;
               }
           }
           else
           {
               Section = 0;
               Part = 8;
           }

       // Load data sections.
       case 8:
           if(Section<DOL_MAX_DATA)
           {
               if(DolImage.dataData[Section])
               {
                   *addr   = DolImage.data[Section];
                   *length = OSRoundUp32B(DolImage.dataLen[Section]);
                   *offset = bb2.bootFilePosition + DolImage.dataData[Section];

                   if( (*addr + *length) > 0x81200000 )
                   {
                       OSReport(
                           "APPLOADER ERROR >>> Too big data segment! (0x%x - 0x%x)\n",
                           *addr, *addr + *length );
                       PPCHalt();
                   }

                   Section++;
                   DCInvalidateRange(*addr, *length);
                   break;
               }
           }
           else
           {
               Section = 0;
               Part = 9;
           }

       // Load FST, only if FST placed after boot file.
       case 9:
           if(FSTHigh)
           {
               *addr   = bb2.FSTAddress;
               *length = OSRoundUp32B(bb2.FSTLength);
               *offset = bb2.FSTPosition;

               Part = 10;

               if(*addr > 0x81700000)
               {
                   OSReport(
                       "APPLOADER ERROR >>> Illegal FST "
                       "destination address! (0x%x)\n", *addr );
                   PPCHalt();
               }

               DCInvalidateRange(*addr, *length);
               LoadBigData(addr, length, offset, &Part);
               break;
           }
           else
           {
               // Continue on part 10.
               Part = 10;
           }

       // Init misc. low memory variables. Continue on part 11.
       case 10:
           InitLoMem(LoMem);
           Part = 11;

       // Read button state of 4th controller and save it in OS global.
       // It's for testing pre-master dvds (NR Discs). But as NR discs gets
       // written without change to the dvdroms the retail apploaders contains
       // it, too. If you plug in a special device (dunno what it is exactly -
       // controller with special id? some bit set?) the apploader starts making
       // a crc of the whole disc and compare that to a crc stored on memory card.
       case 11:
           PADStatus padst;
           PadRead(PAD_CHAN3, &padst);
           __OSPADButton = padst.button;

           // All data loaded. End of apploader.
           return FALSE;

       // This part will be called, if DVD need to load more, than 64 sectors.
       case 12:
           ASSERT(SecondTimeForThePart == TRUE);
           LoadBigData(addr, length, offset, &Part);
           break;

       // Unknown part. Abort data loading.
       default:
           return FALSE;
   }

   // We need to load more data. Tell IPL to do that.
   return TRUE;
}

PART III. Apploader Close

Close call is executed after apploader's Main.

void* AplClose(void)
{
   // Return entrypoint of main DOL executable to IPL
   return DolImage.entry;
}

Now IPL jumps to DOL's entrypoint (__start).

Appendix A: Helper calls

Calculate full size of DOL text and data sections

s32 DOLSize(void)
{
   DolImage *dol = &DolImage;
   s32 totalBytes = 0, i;

   for(i=0; i<DOL_MAX_TEXT; i++)
   {
       if(dol->textData[i])
       {
           // Aligned to 32 byte boundary
           totalBytes += OSRoundUp32B(dol->textLen[i]);
       }
   }

   for(i=0; i<DOL_MAX_DATA; i++)
   {
       if(dol->dataData[i])
       {
           // Aligned to 32 byte boundary
           totalBytes += OSRoundUp32B(dol->dataLen[i]);
       }
   }

   return totalBytes;
}

Return FALSE, if any DOL section, including BSS exceed address limit

BOOL AddressLimit(u32 limit)
{
   DolImage *dol = &DolImage;
   s32 i, end;

   // Text sections
   for(i=0; i<DOL_MAX_TEXT; i++)
   {
       if(dol->textData[i])
       {
           end = dol->text[i] + dol->textLen[i];

           if( ((dol->text[i] < 0x81100000) || (end > 0x81300000)) && (end > limit) )
           {
               return FALSE;
           }
       }
   }

   // Data sections
   for(i=0; i<DOL_MAX_DATA; i++)
   {
       if(dol->dataData[i])
       {
           end = dol->data[i] + dol->dataLen[i];

           if( ((dol->data[i] < 0x81100000) || (end > 0x81300000)) && (end > limit) )
           {
               return FALSE;
           }
       }
   }

   // BSS
   end = dol->bss + dol->bssLen;
   if( ((dol->bss < 0x81100000) || (end > 0x81300000)) && (end > limit) )
   {
       return FALSE;
   }

   return TRUE;
}

Some big transfers requires multiple load times for single apploader part (Limit is set to 64 DVD sectors - WHY?).

void LoadBigData(void **addr, s32 *length, s32 *offset, int *part)
{
   if(SecondTimeForThePart)
   {
       SecondTimeForThePart = FALSE;

       *addr   = SecondTimeAddr;
       *length = SecondTimeLength;
       *offset = SecondTimeOffset;
       *part   = SecondTimePart;
   }
   else
   {
       if(*length > 64*2048)       // 64 DVD sectors
       {
           // Initiate multiple DVD load.
           SecondTimeForThePart = TRUE;

           SecondTimeAddr = *addr + 64*2048;
           SecondTimeLength = *length - 64*2048;
           SecondTimeOffset = *offset + 64*2048;
           SecondTimePart = *part;

           *length = 64*2048;
           *part = 12;
       }
   }
}

Init some OS low memory variables

void InitLoMem(OSLoMem *LoMem)
{
   LoMem->bi.magic = OS_BOOTINFO_MAGIC;    // 0xD15EA5E
   LoMem->bi.version = 1;
   // Clear debug interface (db/DBInterface.h)
   LoMem->db.bPresent = 
   LoMem->db.exceptionMask =
   LoMem->db.ExceptionDestination = 
   LoMem->db.exceptionReturn = NULL;
}

Read PAD input buffer (buttons state). If PAD is not polled, return zeroes

void PadRead(int port, u32 *buf)
{
   // If controller polling enabled
   if(SI_POLL & (0x80 >> port))
   {
       // Read SI input buffer
       buf[0] = SI_CHAN_INBUFH(port);
       buf[1] = SI_CHAN_INBUFL(port);
   }
   else
   {
       buf[0] = buf[1] = 0;
   }
}

Appendix B: DVD structures

#define DVD_BB2_OFFSET      0x420
#define DVD_BI2_OFFSET      0x440

#define OS_BI2_SIZE         0x2000  // Size of BI2 structure

// DVD Boot Block
typedef struct
{
   u32     bootFilePosition;       // offset of main DOL executable
   u32     FSTPosition;            // offset of primary FST
   u32     FSTLength;              // primary FST size (in bytes)
   u32     FSTMaxLength;           // size of biggest additional FST
   u32     FSTAddress;             // FST address in main memory
   u32     userPosition;
   u32     userLength;
   u8      padding0[4];            // reserved, should be 0
} DVDBB2;

FST is File String Table. User area on DVD is where all files are placed. On some DVDs, user area is configured wrong. To know correct user area, you should parse whole FST and find min and max file offsets.

// DVD Boot Info
typedef struct
{
   s32     debugMonSize;           // size of debug monitor
   s32     simMemSize;             // simulated memory size (in bytes)
   u32     argOffset;              // command line arguments
   u32     debugFlag;              // debug present (set to 3, if CodeWarrior on GDEV)
   u32     TRKLocation;            // target resident kernel location
   s32     TRKSize;                // size of TRK
   u32     countryCode;            // country code
   u32     ?       +1C
   u32     ?       +20
   u32     ?       +24
   u32     dolLimit;               // maximum total size of DOL text/data sections (0 - unlimited)
   u32     ?       +2C

   u8      padded0[OS_BI2_SIZE - 0x30]; // reserved, should be 0
} DVDBI2;

Somewhere in +1C, +20 or +24 must present :
u32     longFileNameSupport;    // 1: use long file names, 0: use 8.3 names
u32     padSpec;                // controller version for PADSetSpec

Appendix C: Used OS low memory variables

s32     __OSPhysMemSize     AT_ADDRESS(OS_BASE_CACHED | 0x0028);
s32     __OSDebugMonSize    AT_ADDRESS(OS_BASE_CACHED | 0x00E8);
void*   __OSDebugMon        AT_ADDRESS(OS_BASE_CACHED | 0x00EC);
s32     __OSSimMemSize      AT_ADDRESS(OS_BASE_CACHED | 0x00F0);
DVDBI2* __OSBootInfo2       AT_ADDRESS(OS_BASE_CACHED | 0x00F4);
s32     __OSDOLSize         AT_ADDRESS(OS_BASE_CACHED | 0x30D4);
u16     __OSPADButton       AT_ADDRESS(OS_BASE_CACHED | 0x30E4);

Strange Things

What's [+0x148] in apploader data? Its only cleared in Init, but not used.

Why FST loading is so strange : if FST placed above DOL, then its loaded after DOL, otherwise its loaded before DOL.

if(FSTHigh) __OSDOLSize = dolSize + OSRoundUp32B(bb2.FSTLength);
else __OSDOLSize = dolSize;

Why so?

Why FST is loaded in pieces, if its greater than 64 DVD sectors, but big DOL sections are not loaded in same way?

Some code is placed after apploader data. Is it garbage or what?

ToDo

  • Need more BI2 details.
  • Get Zelda apploader and crosscheck it with current reversing. Maybe some interesting things will be discovered.
  • Count how many apploader revisions are present today.

Source

This information was found at: [1]