Monday, March 30, 2026

SPARC V9 Boot Process – Part 2: sysboot

Introduction

This article is a follow-up to the earlier SPARC V9 Boot Process – Part 1: bootblk and takes a closer look at the second-stage bootloader of the SPARC V9 architecture.

The design of OpenBoot firmware limits the size of the first-stage bootloader to around 8 KiB. This is just enough to locate the boot device and load the second-stage bootloader, which can be much larger and contains all the logic required to load the operating system kernel. On some operating systems, the second-stage bootloader is called ufsboot or ofwboot; however, in this article, it is referred to as sysboot.

The rest of the article describes how to build SPARC V9 development tools for cross-compilation. It also demonstrates how the first-stage bootloader can use OpenBoot client interface services to locate the boot device and load the second-stage bootloader.

Development Tools for Cross-Compilation

Because we are performing bare-metal programming, which does not rely on operating systems or standard libraries, we can easily build a set of tools to generate SPARC V9 target binaries. My build platform is Linux on ARM aarch64; however, other platforms should work just as well.

The script below performs the following tasks:

  1. Downloads source code tar files for Binutils and GCC.
  2. Unpacks tar files.
  3. Builds Binutils and then GCC development tools.
  4. Installs development tools in user's home directory.

When configuring and building tools for cross-compilation, it is essential to explicitly specify the --build, --host, and --target options:

  • --build and --host specify the platforms on which the development tools are built and run, respectively.
  • --target specifies the platform for which the development tools produce runtime binaries.

🖙 It is important to specify: --target=sparc64-unknown-elf, which produces 64-bit SPARC ELF binaries for a bare-metal (no operating system) target.

# Create various build directories
BUILD_ROOT="${HOME:?}/gcc_build" &&
TAR_DIR="${BUILD_ROOT:?}/tar" &&
SRC_DIR="${BUILD_ROOT:?}/src" &&
OBJ_DIR="${BUILD_ROOT:?}/obj"
mkdir -p ${TAR_DIR:?} ${SRC_DIR:?} ${OBJ_DIR:?}

# Download the latest Binutils and GCC
BINUTILS_VER="2.46.0" &&
GCC_VER="15.2.0" &&
cd ${TAR_DIR:?} &&
wget https://ftp.gnu.org/gnu/binutils/binutils-${BINUTILS_VER:?}.tar.gz &&
wget https://ftp.gnu.org/gnu/gcc/gcc-${GCC_VER:?}/gcc-${GCC_VER:?}.tar.gz

# Unpack all downloaded files
cd ${SRC_DIR:?} &&
for i in $(ls ${TAR_DIR:?}); do tar -xf ${TAR_DIR:?}/$i & done; wait

# Before building GCC we need to download a few prerequisites
cd ${SRC_DIR:?}/gcc-${GCC_VER:?} &&
/bin/sh contrib/download_prerequisites

# Set various build options
MAKE_JOBS=4 &&
BUILD="aarch64-linux-gnu" &&
HOST="${BUILD:?}" &&
TARGET="sparc64-unknown-elf" &&
PREFIX="${HOME:?}/gcc-${GCC_VER:?}_${TARGET:?}" &&
PATH="${PREFIX:?}/bin:${PATH}" &&
CC="gcc" &&
CXX="g++" &&
unset LDFLAGS &&
export PATH CC CXX

# Build and install Binutils with static standard libs
pkg="binutils-${BINUTILS_VER:?}"; cd ${OBJ_DIR:?} &&
test ! -d ${pkg:?} && mkdir ${pkg:?}; cd ${pkg:?} &&
${SRC_DIR:?}/${pkg:?}/configure --prefix=${PREFIX:?} \
--build="${BUILD:?}" --host="${HOST:?}" --target="${TARGET:?}" \
--enable-shared --enable-64-bit-bfd \
--with-static-standard-libraries --disable-nls &&
gmake -j "${MAKE_JOBS:?}" && gmake install

# Build and install GCC
pkg="gcc-${GCC_VER:?}"; cd ${OBJ_DIR:?} &&
test ! -d ${pkg:?} && mkdir ${pkg:?}; cd ${pkg:?} &&
${SRC_DIR:?}/${pkg:?}/configure --prefix="${PREFIX:?}" \
--build="${BUILD:?}" --host="${HOST:?}" --target="${TARGET:?}" --with-cpu=v9 \
--enable-languages=c --enable-shared --disable-libssp --disable-nls &&
gmake --output-sync=target -j "${MAKE_JOBS:?}" && gmake install

The development tools are installed at:
${HOME}/gcc-15.2.0_sparc64-unknown-elf

When compiling code for SPARC V9, the following GCC options should be used:
-ffreestanding -nostdlib -nostartfiles

Now that the development tools for cross-compilation are in place, we can proceed to the next steps: developing SPARC V9 bootloaders.

SPARC V9 Boot Device Layout

To keep things simple, I use the following boot device layout: the first 32 KiB are reserved for the Sun VTOC label and the stage-1 bootloader, while the next 1 MiB is reserved for the stage-2 bootloader.

The boot sequence is as follows:

  1. The OpenBoot firmware reads the first 512 bytes and verifies the Sun VTOC label.
  2. The OpenBoot firmware then reads the next 8 KiB containing the stage-1 bootloader (boot1) and executes it.
  3. The stage-1 bootloader allocates a 1 MiB memory buffer, opens a handle to the specified boot device, seeks to offset 32,768, reads 1 MiB of data into the buffer, and then executes the stage-2 bootloader (boot2) from that memory.

My current stage-1 bootloader is about 5 KiB in size, but it could be expanded to roughly 8 KiB. The stage-2 bootloader is currently around 10 KiB and could be expanded to as much as 1 MiB.

My SPARC V9 test machine is a Sun T5220 that supports booting from a USB flash drive. I use SPARC V9 development tools to build the Sun VTOC utility along with the stage-1 and stage-2 bootloaders, and then write them to the USB drive.

# Include SPARC V9 cross-tools in current PATH
export PATH=${HOME:?}/gcc-15.2.0_sparc64-unknown-elf/bin:${PATH}

# Build native vtoc_label utility
cd ${SRC_DIR}/vtoc_label && gcc -O2 -std=c11 -Wall -pedantic \
  -D_FILE_OFFSET_BITS=64 -D_POSIX_C_SOURCE=200809L \
  -o ${BIN_DIR}/vtoc_label vtoc_label.c

# Build SPARC V9 stage-1 bootloader
cd ${SRC_DIR}/boot1 && sparc64-unknown-elf-gcc \
  -Os -std=c11 -Wall -pedantic -ffreestanding -nostdlib -nostartfiles \
  -o ${BIN_DIR}/boot1.bin start.s main.c ofw.c util.c && \
sparc64-unknown-elf-strip ${BIN_DIR}/boot1.bin

# Build SPARC V9 stage-2 bootloader
cd ${SRC_DIR}/boot2 && sparc64-unknown-elf-gcc \
  -O2 -std=c11 -Wall -pedantic -ffreestanding -nostdlib -nostartfiles \
  -o ${BIN_DIR}/boot2.bin start.s main.c ofw.c util.c v9reg.c v9reg_save.s && \
sparc64-unknown-elf-strip ${BIN_DIR}/boot2.bin

# Write Sun VTOC disk label and bootloaders
${BIN_DIR}/vtoc_label write <device> &&
dd if=${BIN_DIR}/boot1.bin of=<device> bs=512 seek=1 &&
dd if=${BIN_DIR}/boot2.bin of=<device> bs=1024 seek=32 &&
sync

The bootloaders are placed at fixed offsets at the start of the boot device. This simple layout avoids the complexity of file systems and is straightforward to implement.

Source Code and Boot Demo

The mechanism by which the stage-1 bootloader loads and executes the stage-2 bootloader is provided by the OpenBoot client interface services. They provide specific functions for opening a boot device and reading from or writing to data at designated offsets. My stage-1 bootloader follows the following sequence:

  1. Invokes the OpenBoot finddevice() client interface to open a handle for the /chosen node.
  2. Invokes the OpenBoot getprop() client interface to open handles for stdin, stdout, and bootpath, which are located under the /chosen node.
  3. Invokes the OpenBoot claim() client interface to allocate a memory buffer for the stage-2 bootloader.
  4. Invokes the OpenBoot seek() and read() client interfaces to load the stage-2 bootloader into the allocated memory buffer.
  5. Invokes the OpenBoot execute-buffer() client interface to execute the stage-2 bootloader.

Once the stage-2 bootloader starts executing, it captures the current state of the machine registers and prints them to the console. The complete source code for the bootloaders is located here: sparcv9_bootloader.tar.gz.

{0} ok boot /pci@0/pci@0/pci@1/pci@0/pci@1/pci@0/usb@0,2/storage@3/disk@0
Boot device: /pci@0/pci@0/pci@1/pci@0/pci@1/pci@0/usb@0,2/storage@3/disk@0  File and args: 

Running SPARC V9 stage-1 bootloader

Allocating memory for stage-2 bootloader:
addr=0xFE982000, size=0x100000

Reading stage-2 bootloader:
ihandle_bootpath=0xFEB50608, offset=0x8000, size=0x100000


Running SPARC V9 stage-2 bootloader

Displaying machine registers:

i0          0x00000000FEBA9D88  4273642888
i1          0x0000000000000027  39
i2          0x0000000000000000  0
i3          0x00000000000000FF  255
i4          0x0000000000000030  48
i5          0x0000000000000036  54
i6 (fp)     0x00000000FEBA93D1  4273640401
i7          0x0000000000100138  1048888

l0          0x000000000000298E  10638
l1          0x0000000000000000  0
l2          0x000000000000257D  9597
l3          0x00000000000010AC  4268
l4          0x0000000000040008  262152
l5          0x0000000000000001  1
l6          0x0000000000004000  16384
l7          0x00000000080010D0  134222032

o0          0x0000000000000000  0
o1          0x000000000003C40A  246794
o2          0x000000000004D32C  316204
o3          0x00000000F025EFE4  4029018084
o4          0x00000000FEE82F10  4276629264
o5          0x00000000FEE81798  4276623256
o6 (sp)     0x00000000FEBA9321  4273640225
o7          0x0000000000100490  1049744

g0          0x0000000000000000  0
g1          0x0000000000000021  33
g2          0x0000000000000001  1
g3          0x0000000000000000  0
g4          0x0000000000000000  0
g5          0x0000000000000000  0
g6          0x0000000000000000  0
g7          0x0000000000000000  0

pc          0x0000000000101A54
ccr         0b00010001 (xcc=c, icc=c)
asi         0x0000000000000000
tick        0x0000000E02465966

pil         13
tba         0x00000000F0200000
tl          0
gl          0
pstate      0b0000000010110 (tct=0, cle=0, tle=0, mm=0, pef=1, am=0, priv=1, ie=1)

cwp         2
cansave     4
canrestore  2
cleanwin    7
otherwin    0
wstate      0x00

Press any key to exit: 
Program terminated
{0} ok

For further details on the SPARC V9 architecture and its registers, refer to the February 1993 Microprocessor Report article listed in the References section below.

References

Microprocessor Report SPARC V9 Adds Wealth Of New Features