PowerPC OpenFirmware Implementation Details

From DisNCord Community Wiki
Revision as of 05:37, 11 November 2022 by Kraaabs (talk | contribs) (finally yeeting my deadname :D ~Sarah)
Jump to navigation Jump to search

This page aims to describe implementation details for OpenFirmware client programs on PowerPC platforms (with a specific focus on Apple machines). Much of this information is taken from the POBARISNA Wiki. There isn't a very complete high level overview of what actually needs to be implemented when creating OpenFirmware client programs, so the hope is that this page can provide something like that.

What is OpenFirmware?

OpenFirmware is an implementation of the IEEE-1275 Standard for Boot (Initialization Configuration) Firmware. The standard essentially describes a boot firmware with a FORTH interpreter built in, supporting a tree of devices) that various client programs can interact with.

Client Programs

A client program utilizes the OpenFirmware client interface to perform some task. Client programs don't need to be kernels or bootloaders, but in practice (and in the context of what you're probably using OpenFirmware for), they usually are. Generally speaking, most of the time you're only going to be running one client program ever in an OF session, but this isn't necessarily a rule; the reasons for this will be explained later, as it's out of the scope of what I want to explain to you here. The client program interface varies a lot between different IEEE-1275/OpenFirmware implementations on different architectures; as a general rule, core concepts stay mostly the same, but any code samples here are going to be for Apple PowerPC platforms.

The Client Interface

Essentially, there are 3 ways to interact with OpenFirmware. There's a user interface, which is the 0> or ok prompt you've probably seen before when using OF-based systems, a device interface for things like PCI card BIOSs to interact with, and the client interface, for client programs (which you'll mainly be concerning yourself with when targeting OpenFirmware).

To put it simply, the client interface is a set of functions that live inside OpenFirmware that client programs are meant to use to accomplish things like calling OF commands, reading information about devices, or manipulating the OpenFirmware built-in console and framebuffer. These functions (referred to as "services" in typical OpenFirmware parlance) are exposed to a client program through an entry point (described later on this page). Exactly what services are provided is outside of the scope of this page, but the IEEE-1275 standard linked above has a very thorough list of the services you should typically have available on any standards-compliant OpenFirmware/IEEE-1275-based system.

Accessing the Client Interface from PowerPC C/C++ code

There aren't actually all that many functions (usually referred to as services in OF parlance) that the standard defines for the client interface. Different system-specific IEEE-1275 implementations can certainly implement whatever special services they want, but you obviously can't just rely on those being there on every system you're writing for.

The process for calling client interface services depends on what platform you're on, but on PowerPC, your client program is passed an OpenFirmware entry point, with a signature that looks something like this:

int openfirm_entry(void *args)

This entry point is provided as a function pointer on PowerPC, in register "r5" on Apple systems. The entry point that gets branched to in POBARISNA looks like this, to give you an idea of how to access it:

extern "C" void external_kmain(uint32_t r3, uint32_t r4, int (*openfw)(void*))

To call a service, you need to pass in a struct with space for the name of the service, the number of args you're passing, the number of return values you're expecting back, and appropriately sized fields for those args and return values. As an example, the *client interface* `open` function call in POBARISNA looks something like this (modified and commented to help you understand better):

static struct {
     // this is the field that'll hold the name of the service we want to call.
    const char *cmd; 
    // this is the number of args we pass to OF
    int num_args;
    // this is the number of returns we want back
    int num_returns;
    // this is an argument
    const char *device_name;
    // this is a return value
    int handle;
} args = {
    "open",  // we want to call "open"
    1,       // it takes 1 argument
    1        // ...and returns 1 thing
};

// since device_name is an argument we're passing, we should set it to something.
args.device_name = device;

// now, call our entry point, passing in our args array. 
special::openfirm(&args);

// now args.handle (our return value) holds the handle OF opened for us.
return args.handle;

As you can probably gather, the structure has a pretty strict... structure.

  1. name of service
  2. number of args (uint32)
  3. number of returns (uint32)
  4. num_args fields for arguments
  5. num_returns fields for return values

Thankfully, that's about all you need to do to actually start interacting with OpenFirmware, at least on PowerPC Macs. You'll be using this pattern a lot across your codebase, so get real used to it real fast. I suggest making all of these stubs right away so that you can easily just call any client service you want from C/C++/whatever language.

The Device Tree

If you've heard of device trees before, it may have been in the context of ARM/ARM64 platforms. This is where those came from! In fact, many of the conventions and standard device names have stayed almost exactly the same, so this section will be kept fairly brief, as documentation on modern flattened device trees is fairly easy to come by. However, a basic overview is as follows:

A single device/node in the tree can be thought of as this structure:

struct of_device {
    of_device* parent;
    of_device* next_sibling;
    of_device* children[];

    of_property* properties[];
};

Where an "of_property" is simply:

struct of_property {
    of_property* next;
    char name[32];
    void* content;
    uint32_t content_size;
};

These nodes (the "of_device" structure above) are arranged in a hierarchical tree-like structure. There are a few key devices that are present on (most) OpenFirmware systems. These are:

Important Device Tree Nodes
Name Function
/chosen
Carries important info about the system, such as stdin and stdout for the OpenFirmware console.
/chosen/stdout
The OpenFirmware console output. Can be written to with a call to the "write" client service.
/memory
Contains information about memory currently available to the system (size, number of banks, etc)
/cpus
Contains information about currently installed CPUs, such as clock speed, timer frequency, number of cores, etc.

This is obviously not an extensive list of devices available to you when doing OpenFirmware development. Running "dev / ls" will give you a more exhaustive list.

Device types

All OpenFirmware devices fit into a few different categories, to make it easier to interact with them. These categories are:

OpenFirmware device types
Type Purpose Typical methods Typical properties
display
Could be something like a framebuffer, console output, etc.
open, close, write, draw-logo, restore
N/A
block
Usually stuff like hard drives, etc. Random access.
open, close, read, write, seek, load
N/A
byte
Sort of like block, but sequential access. (think a tape drive)
open, close, read, write, seek, load
N/A
network
Network devices.
open, close, read, write, load
local-mac-address, mac-address, address-bits, max-frame-size
serial
Serial devices
open, close, read, write, install-abort, remove-abort, restore, ring-bell
N/A

Getting your client program running

The Toolchain

Most OpenFirmware implementations can load XCOFF or ELF binaries. Generally speaking, ELF toolchains are easier to come by (and build), and ELF is very well documented. POBARISNA is built as an ELF executable.

If you're building with GCC, you can specify the triple as either powerpc-elf or powerpc-unknown-elf (these are the same thing). Other triples will probably work, but these are easy to get going.

Building a powerpc-elf GCC toolchain is fairly easy. There are easy scripts to build a newlib-enabled toolchain here. Instructions on building a toolchain manually are somewhat out of the scope of this page, but keep in mind one important rule: the toolchain you use for OpenFirmware isn't special. You can do anything you could do on any other bare metal platform (use newlib, use llvm, whatever). As long as it can target powerpc-elf, you're well on your way.

crt0

As with any platform, executables on OpenFirmware have an entry point. On most higher-level platforms such as Linux, Windows, or macOS, the entry point is implicitly added at compile time. On lower-level platforms such as OpenFirmware, we'll need to provide that ourselves.

Not much needs to be done before you can start running C (or C++, or any other language) code. The basic steps are:

  1. Set up a stack
  2. Zero some registers
  3. Set up BSS
  4. Jump into your code

An excellent resource for an example of how to do this is the l4ka::pistachio source code, as always. To abridge that code into a minimal example:

/* set up our stack. it's fine to just put it in BSS. */
	.section ".bss"
	.globl	_init_stack_bottom
	.globl	_init_stack_top

#define INIT_STACK_SIZE (4096 * 3)
_init_stack_bottom:
// .lcomm is a pseudo-mnemonic that sets up a "storage area" in RAM.
.lcomm	init_stack, INIT_STACK_SIZE, 16   
_init_stack_top:

	.section ".text"
	.align	2
	.globl	_start
_start:
	/*  Use our local stack.  */
	lis	%r1, init_stack@ha
	la	%r1, init_stack@l(%r1)
	addi	%r1, %r1, INIT_STACK_SIZE-32

	/* initialize the system reserved register */
	li	%r2, 0
	/* point to 0 for the small data area */
	li	%r13, 0

	/*  Initialize .bss (which also zeros the stack).  */
#define BSS_START	__bss_start
#define BSS_END		_end
	lis	%r10, BSS_START@ha
	la	%r10, BSS_START@l(%r10)
	subi	%r10, %r10, 4
	lis	%r11, BSS_END@ha
	la	%r11, BSS_END@l(%r11)
	subi	%r11, %r11, 4
	li	%r12, 0
1:	cmp	0, %r10, %r11
	beq	2f
	stwu	%r12, 4(%r10)
	b	1b

	/*  Jump into C code.  */
2:
    li %r10, 0
	mtsrr1 %r10
    // external_kmain here is a function in your code with C calling convention.
	bl	external_kmain
	
3:	b	3b		/* we should never execute this line.  */

Once you're in C/C++ land, you're good to go. Feel free to start writing your kernel, or your client program, or whatever it is you're writing.

Things to remember

The PowerPC MSR is the register that controls most of the processor state; everything from endianness to memory management state to exception masking. You WILL likely have to mess with the MSR in order to get anything too interesting done, but there is an important caveat. Modifying the MSR from an OpenFirmware client program is expected, but doing so has no guarantee of keeping the OpenFirmware client services functional. I'd recommend making sure everything you need to do client service wise is either done with before you break OF. It's also totally possible to modify the MSR in a way that doesn't break OpenFirmware. This is "bad practice", but it works, and it's an option.

You'll quickly find out that OpenFirmware on PowerPC is NOT one consistent standard. PReP and CHRP exist, but most vendors (read: not IBM) didn't really follow either standard too closely. Be sure to watch out for platform differences. Apple in particular uses some rather different conventions from other OpenFirmware vendors.

If you're targeting PowerPC Macs, you'll have a hard time testing in QEMU. It's possible to properly bless an HFS CD image and run your code inside qemu-system-ppc, but you'll likely run into issues rather quickly. This is because QEMU's Apple OpenFirmware implementation is somewhat incomplete; it's based on OpenBIOS, an open-source OpenFirmware implementation. QEMU's OpenBIOS is modified somewhat to behave a little more like a Macintosh, but it's got some holes, and it really only has solid OpenFirmware 2 support. Therefore, some client services won't act like they do on a real Mac, and supporting either real hardware or QEMU doesn't guarantee your code will work on the other. At the time of writing, I (Krabs) haven't tried writing OpenFirmware client programs for other architectures/platforms, but I can imagine you might run into similar issues. I've been looking into solutions for potentially more accurate Apple OpenFirmware emulation.

External Resources

These resources have been invaluable to Krabs on their OpenFirmware PPC journey:

IEEE-1275 Standard for Boot (Initialization Configuration) Firmware

Programming Environments Manual for 32-bit Implementations of the PowerPC Architecture

PowerPC Microprocessor Family: The Programming Environments Manual for 32 and 64-bit Microprocessors

The l4ka::pistachio source code

The OpenBSD macppc source code