This thing boots off of the network. The (256K, PLCC) boot ROM is socketed, so one could, conceivably, burn a loader into a flash chip and stick it in there. That would make loader upgrades a pain (though not necessarily impossible), and it would also require anybody wanting to do this (such as me) to buy a burner. Additionally, I don't know how large of a flash that socket will support and I haven't found any flash large enough to hold a kernel in a 32 pin PLCC. Instead, I have opted to use the existing ROM to get the image into the machine at boot. Very little is known about what services the ROM provides, but I am able to get it to run a program with some hoop jumping.
Since we are using the existing ROM, we are at its mercy regarding how we configure certain things, such as the method by which it gets its boot image. Fortunately, the defaults are reasonable, we can get key parameters (IP address, route, boot image path, name, and host, etc.) from a BOOTP server. The image fetching method defaults to TFTP. The BOOTP information is no longer available to us by the time our code starts running. All we have is a text section.
I had previously thought that the initial frame pointer pointed to some point beyond the end of the text section. That was true with small images, but not with large ones. The size of the image seems to have no effect on where that frame pointer is. That means not only that we have to relocate the stack before using it, but also that we have to invalidate the saved register cache to keep our program from being trodden upon. I have done so by resetting the CPU.
Once our image is running, that is the end of our exposure to the existing boot ROM, except that the area of address space that it is mapped in is uninteresting to us, and we have to either ignore some of the jumper positions or use them for their intended purpose. The latter part is because the ROM sets up the VGA based on those jumper settings, and we don't want to fry a monitor with zany settings.
So, the boot loader must act in stages.
The stage 2 and stage 3 payloads are b.out executables, rather than COFF because it is simpler. Simple is good when starting essentially from scratch with no way to display diagnostic information. Stage 1 must be COFF, because that is what the boot ROM expects.
The purpose of stage 1 is to set up an environment in which stage 2 has access to its .data and .bss sections. Its code will probably be overwritten by stage 2. Stage 1 must be written such that it does not make use of any .data or .bss sections.
The purpose of stage 2 is to locate the stage 3 payload at the beginning of RAM, where it knows that it is supposed to be. stage 2's code will be abandoned, probably used by stage 3. Stage 2 does not set up a stack for stage3. It is the responsibity of stage 3 to set up its own stack if it might use the area of RAM where stage 2 was loaded or needs a larger stack than the one that stage 2 provides.
We're jumping through a bunch of hoops because we don't know if we have .data or .bss sections. Even if we do become aware of how to make that work, though, this would still come in handy for zImage uncompressing and initrd, if we decide to implement them.
As you can see, much of stages 1 and 2 are quite similar, and the b.out stuff is actually implemented as a subroutine which is called by them, making the remainder of them pretty simple.