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:
- Downloads source code tar files for Binutils and GCC.
- Unpacks tar files.
- Builds Binutils and then GCC development tools.
- 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:
- The OpenBoot firmware reads the first 512 bytes and verifies the Sun VTOC label.
- The OpenBoot firmware then reads the next 8 KiB containing the stage-1 bootloader (boot1) and executes it.
- 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:
- Invokes the OpenBoot finddevice() client interface to open a handle for the /chosen node.
- Invokes the OpenBoot getprop() client interface to open handles for stdin, stdout, and bootpath, which are located under the /chosen node.
- Invokes the OpenBoot claim() client interface to allocate a memory buffer for the stage-2 bootloader.
- Invokes the OpenBoot seek() and read() client interfaces to load the stage-2 bootloader into the allocated memory buffer.
- 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.

