Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Assembly (x86-64, AArch64, RISC-V)  Module 16 – Privilege, exceptions and bare metal

Bare metal: hello from the UART on QEMU

A RISC-V and an AArch64 program with no operating system under it: its own stack, a linker script, a UART at a fixed address and a clean exit from QEMU.

  • Advanced
  • 20 minutes
  • Examples run with Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: A bare-metal program sets up what an operating system would · QEMU's virt boards publish where RAM and each device live · The image is linked at the board's RAM, and the ELF file proves it · Practise: change the image and predict the read-back. With a pass: 2 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Explain what a bare-metal program must set up itself that an operating system normally provides
  • Find the RAM, UART and exit addresses of QEMU's RISC-V and AArch64 virt boards
  • Build a bare-metal ELF image and check its entry point and segments with llvm-readelf
  • Write a linker script that places code, data and a stack, and detect a wrong link before it runs
  • Use a polling UART routine and an exit device to report a status from QEMU

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain what a bare-metal program must set up itself that an operating system normally provides
  • Find the RAM, UART and exit addresses of QEMU's RISC-V and AArch64 virt boards
  • Build a bare-metal ELF image and check its entry point and segments with llvm-readelf
  • Write a linker script that places code, data and a stack, and detect a wrong link before it runs
  • Use a polling UART routine and an exit device to report a status from QEMU

Before you start

On this page

On QEMU’s RISC-V virt board started with -bios none, the first instruction of your program runs at 0x80000000, and nothing of yours has run before it: no loader you wrote, no stack, no C library and no system calls. Such a program is bare metal: Software that runs directly on the hardware with no operating system under it. It places itself in memory through its linker script, sets up its own stack and drives devices through their registers., the only software on the machine, so it sets up its own stack and prints by storing bytes to a device.

You will build one for RISC-V and one for AArch64. Each sends a hello message through its board’s UART: Universal asynchronous receiver-transmitter, the device that turns bytes into the bits of a serial line and back. A program sends a byte by writing it to the UART's data register. and then asks QEMU to end the run with a status.

A bare-metal program sets up what an operating system would

Under Linux or macOS the kernel’s loader: The kernel code that reads an executable's load commands or program headers, maps each segment into the new process with the right permissions and jumps to the entry-point address. More in “From source to executable: objects and linking” maps each segment and builds a stack before the C library’s start-up code: The code a compiler driver links around a C program, such as crt1.o's _start, which reads the initial stack, prepares the C library, calls main and passes its return value to exit. More in “Process startup: argc, argv, envp and auxv” calls main, as process startup shows. On bare metal nobody does any of that. A linker script: A file that tells the linker where each output section goes in memory and which symbols to define. A bare-metal image needs one because no operating system's loader chooses its addresses. fixes the image’s load address: The memory address a program's bytes are copied to before it runs. A bare-metal image is linked for the address its board's reset code or loader uses., the first instruction sets the stack pointer: The register that addresses the top of the stack: rsp on x86-64, sp on AArch64 and sp (x2) on RISC-V. Push, pop and function prologues move it; losing it loses the stack. More in “Registers on x86-64, AArch64 and RISC-V”, and there is no system call to ask for anything.

Output goes through memory-mapped I/O: Reaching a device through ordinary loads and stores: the device answers a range of physical addresses, so a store there writes one of its registers instead of RAM.: a device answers a range of physical addresses, so an ordinary store to one of them is a command to the device. On the RISC-V board a byte stored to 0x10000000 goes to the UART’s transmit register. With -bios none the hart runs the program in machine mode, the highest privilege mode: RISC-V's name for a privilege level: U-mode for applications, S-mode for an operating-system kernel and M-mode, machine mode, the highest and the only one every RISC-V processor must have. More in “Privilege levels, exceptions and interrupts” of privilege levels and exceptions, so every address is open to it. The AArch64 board starts its core at EL1, since it leaves EL2 and EL3 off unless asked.

Interactive · message by message hello_rv.elf from QEMU's loader to the exit write Step through one run of hello_rv.elf. Watch who sets up what, and which address each store goes to.
hello_rv.elf from QEMU's loader to the exit write, step 6 of 6. Messages so far: QEMU to Hart 0: pc = 0x1000; Hart 0 to Devices: sb to 0x10000000; Hart 0 to Devices: sw 0x5555.QEMU3 segmentsHart 0sp readyDevicesshutdownpc = 0x1000sb to 0x10000000sw 0x5555

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Segments copied to RAM. QEMU reads the three LOAD program headers and copies the code to 0x80000000 and the message to 0x80000038. The third segment has no file bytes, so its 4 KiB of stack start as zeros.
  2. Reset code at 0x1000. The hart leaves reset at 0x1000, where QEMU wrote a few words of its own: they put the hart ID in a0 and the device tree address in a1, then jump.
  3. Jump to the first byte of RAM. With -bios none the jump goes to the base of RAM, 0x80000000, whatever the ELF entry says. Nothing else of QEMU runs on the hart after this.
  4. A stack, set by the program. auipc and addi put 0x80001060 in sp, the end of the 4 KiB reserved in .bss. No code ran before this that could have set it.
  5. 29 stores to the UART. The loop stores each byte of the message to 0x10000000, the transmit register of the 16550 model, which hands it to the terminal at once.
  6. 0x5555 ends the run. sw writes 0x5555 to 0x100000. The SiFive test device reads it as a pass with code 0 and asks QEMU to shut down, so QEMU exits with status 0.

QEMU’s virt boards publish where RAM and each device live

virt is a board QEMU designed for guests rather than a copy of a real chip. Each one is a table of base addresses in QEMU’s source, hw/riscv/virt.c and hw/arm/virt.c.

Board RAM starts at UART Exit path
RISC-V virt 0x80000000 NS16550A at 0x10000000 SiFive test device at 0x100000
AArch64 virt 0x40000000 PL011 at 0x09000000 A semihosting call, hlt #0xf000

For an ELF image on the AArch64 board, QEMU writes the device tree: A data structure that describes a board's hardware: where its RAM is and which devices it has, at which addresses and with which interrupts. Firmware or an emulator passes it to the software it starts. as a 1 MiB block at the first byte of RAM, and leaves it out when the image starts inside that megabyte, so the program is linked 2 MiB in, at 0x40200000. QEMU’s Arm page promises only the RAM and flash addresses, so a portable program reads every other address from the device tree.

Interactive · step by step Where QEMU's two virt boards put RAM and devices Step through both maps. Compare where each board starts its RAM and where its UART and exit path live.
Where QEMU's two virt boards put RAM and devices, step 6 of 6. Showing RAM (from 0x80000000), UART0 16550 (0x10000000), Test device (0x100000), Reset code (0x1000), RAM (from 0x40000000), UART0 PL011 (0x09000000), Interrupts GIC (0x08000000), Flash (from 0x0). In focus: RAM, Flash.RAMfrom 0x80000000UART0 165500x10000000Test device0x100000Reset code0x1000RAMfrom 0x40000000UART0 PL0110x09000000Interrupts GIC0x08000000Flashfrom 0x0

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Two boards, two maps. Each virt board is QEMU's own design, not a copy of a chip. Its source file lists every device as a base address and a size, and that table is the map.
  2. RISC-V starts at 0x1000. The hart resets to 0x1000, where QEMU keeps a few words of reset code. With -bios none they jump to the first byte of RAM, so the image is linked there.
  3. RISC-V devices sit low. The NS16550A UART answers at 0x10000000 and the SiFive test device at 0x100000. A store to either one is a device access, not a write to memory.
  4. AArch64 RAM starts at 1 GiB. RAM begins at 0x40000000. For an ELF image QEMU writes the device tree at that first byte as a 1 MiB block, so the lesson links its code 2 MiB higher, at 0x40200000.
  5. The PL011 and the flash. The PL011 UART answers at 0x09000000, the interrupt controller at 0x08000000, and flash fills the bottom 128 MiB from address 0. No test device: the exit uses semihosting.
  6. Only some addresses are fixed. QEMU's Arm page promises only flash at 0 and RAM at 0x40000000; the rest may move between versions and is read from the device tree.

Checkpoint

Check: What bare metal sets up itself · The virt boards' memory maps

  1. Question 1 of 2 On QEMU's RISC-V virt board started with -bios none, what has set up a stack when your first instruction runs?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing, so the program's own first instructions must set sp

    Bare metal means nothing of an operating system runs before or under the program, so it sets sp to the end of the space it reserved before anything uses the stack.

  2. Question 2 of 2 A program on the RISC-V virt board stores the byte 0x41 to address 0x10000000. What does that store do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It hands 0x41, the letter A, to the NS16550A UART as the next byte to send

    Memory-mapped I/O puts device registers in the physical address space. The board's table says which addresses are RAM and which belong to each device.

The image is linked at the board’s RAM, and the ELF file proves it

hello.s sets sp, stores the message to the UART one byte at a time and then stores 0x5555 to the test device. The AArch64 twin does the same through the PL011 and leaves with a semihosting: A convention by which a program asks the debugger or emulator running it for a host service, such as printing text or ending the run, through a special instruction. Arm defines it, and QEMU answers it when started with -semihosting. call, which asks QEMU itself to end the run. link.ld places the .text.boot section, the one that holds _start, first, at 0x80000000.

hello.s for both boards, and the RISC-V linker script

RISC-V · riscv/hello.s

# hello.s: a bare-metal program for QEMU's RISC-V virt machine, started with -bios none.
# Only QEMU's reset code runs before _start, so the program sets up its own stack.
    .equ UART0, 0x10000000          # NS16550A UART: offset 0 is the transmit register
    .equ TEST, 0x100000             # SiFive test device: a write of 0x5555 ends QEMU

    .section .text.boot, "ax"
    .globl _start
_start:
    la sp, stack_top                # the stack grows down from the end of .bss
    la a0, msg
    li t1, UART0
next:
    lbu t0, 0(a0)                   # one byte of the message
    beqz t0, done                   # a zero byte ends it
    sb t0, 0(t1)                    # store it to the UART's transmit register
    addi a0, a0, 1
    j next
done:
    li t1, TEST
    li t0, 0x5555                   # "pass"
    sw t0, 0(t1)
park:
    wfi                             # if nothing ends the machine, wait here
    j park

    .section .rodata
msg: .asciz "Hello from bare-metal RISC-V\n"

    .section .bss
    .balign 16
    .space 4096                     # 4 KiB of stack
stack_top:

AArch64 · aarch64/hello.s

// hello.s: a bare-metal program for QEMU's AArch64 virt machine, started with -semihosting.
    .equ UART0, 0x09000000          // PL011 UART: offset 0 is the data register

    .section .text.boot, "ax"
    .globl _start
_start:
    ldr x0, =stack_top
    mov sp, x0                      // the stack grows down from the end of .bss
    ldr x1, =UART0
    adr x2, msg
next:
    ldrb w3, [x2], #1               // one byte, then step to the next
    cbz w3, done                    // a zero byte ends the message
    strb w3, [x1]                   // store it to the UART's data register
    b next
done:
    mov x0, #0x18                   // semihosting operation SYS_EXIT
    adr x1, exit_block              // the address of its two-word argument block
    hlt #0xf000                     // the A64 semihosting call
park:
    wfi
    b park

    .section .rodata
    .balign 8
exit_block: .quad 0x20026, 0        // reason ADP_Stopped_ApplicationExit, exit code 0
msg: .asciz "Hello from bare-metal AArch64\n"

    .section .bss
    .balign 16
    .space 4096                     // 4 KiB of stack
stack_top:

riscv/link.ld

/* link.ld: place hello.s where QEMU's RISC-V virt machine starts it with -bios none. */
/* QEMU's reset code jumps to the first byte of RAM, 0x80000000, whatever the ELF entry says. */
ENTRY(_start)
SECTIONS
{
  . = 0x80000000;                        /* the first byte of RAM on virt */
  .text   : { *(.text.boot) *(.text*) }  /* _start first, so it lands at 0x80000000 */
  .rodata : { *(.rodata*) }
  .data   : { *(.data*) }
  .bss    : { *(.bss*) }
}
Build both images and read back where they load Python · build_hello_session.py
"""Build both hello programs as bare-metal ELF images and read back where they will load."""

import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run("""
# Assemble for 64-bit RISC-V and link with the script that starts at 0x80000000
clang-23 --target=riscv64-unknown-elf -march=rv64gc -mabi=lp64 -c riscv/hello.s -o hello_rv.o
ld.lld -T riscv/link.ld hello_rv.o -o hello_rv.elf
# The header names the machine and the entry point
llvm-readelf -h hello_rv.elf | grep -E "Machine|Entry"
# The program headers: what a loader copies, to which address, with which permissions
llvm-readelf -lW hello_rv.elf | grep -E "Type|LOAD"
# The code at the first byte of RAM: stack, message address, then the store to the UART
llvm-objdump -d --no-show-raw-insn hello_rv.elf | grep -E "^8000"
# The AArch64 twin, linked 2 MiB into RAM
clang-23 --target=aarch64-none-elf -c aarch64/hello.s -o hello_a64.o
ld.lld -T aarch64/link.ld hello_a64.o -o hello_a64.elf
llvm-readelf -h hello_a64.elf | grep -E "Machine|Entry"
llvm-objdump -d --no-show-raw-insn hello_a64.elf | grep -E "^4020" | head -n 7
""")

Output

# Assemble for 64-bit RISC-V and link with the script that starts at 0x80000000
$ clang-23 --target=riscv64-unknown-elf -march=rv64gc -mabi=lp64 -c riscv/hello.s -o hello_rv.o
$ ld.lld -T riscv/link.ld hello_rv.o -o hello_rv.elf
# The header names the machine and the entry point
$ llvm-readelf -h hello_rv.elf | grep -E "Machine|Entry"
  Machine:                           RISC-V
  Entry point address:               0x80000000
# The program headers: what a loader copies, to which address, with which permissions
$ llvm-readelf -lW hello_rv.elf | grep -E "Type|LOAD"
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x001000 0x0000000080000000 0x0000000080000000 0x000038 0x000038 R E 0x1000
  LOAD           0x001038 0x0000000080000038 0x0000000080000038 0x00001e 0x00001e R   0x1000
  LOAD           0x001060 0x0000000080000060 0x0000000080000060 0x000000 0x001000 RW  0x1000
# The code at the first byte of RAM: stack, message address, then the store to the UART
$ llvm-objdump -d --no-show-raw-insn hello_rv.elf | grep -E "^8000"
80000000:      	auipc	sp, 0x1
80000004:      	addi	sp, sp, 0x60
80000008:      	auipc	a0, 0x0
8000000c:      	addi	a0, a0, 0x30
80000010:      	lui	t1, 0x10000
80000014:      	lbu	t0, 0x0(a0)
80000018:      	beqz	t0, 0x80000024 <done>
8000001c:      	sb	t0, 0x0(t1)
80000020:      	addi	a0, a0, 0x1
80000022:      	j	0x80000014 <next>
80000024:      	lui	t1, 0x100
80000028:      	lui	t0, 0x5
8000002a:      	addi	t0, t0, 0x555
8000002e:      	sw	t0, 0x0(t1)
80000032:      	wfi
80000036:      	j	0x80000032 <park>
# The AArch64 twin, linked 2 MiB into RAM
$ clang-23 --target=aarch64-none-elf -c aarch64/hello.s -o hello_a64.o
$ ld.lld -T aarch64/link.ld hello_a64.o -o hello_a64.elf
$ llvm-readelf -h hello_a64.elf | grep -E "Machine|Entry"
  Machine:                           AArch64
  Entry point address:               0x40200000
$ llvm-objdump -d --no-show-raw-insn hello_a64.elf | grep -E "^4020" | head -n 7
40200000:      	ldr	x0, 0x40200038 <park+0xc>
40200004:      	mov	sp, x0
40200008:      	mov	x1, #0x9000000          // =150994944
4020000c:      	adr	x2, 0x40200050 <msg>
40200010:      	ldrb	w3, [x2], #0x1
40200014:      	cbz	w3, 0x40200020 <done>
40200018:      	strb	w3, [x1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 build_hello_session.py

The entry point: The address where the kernel's loader starts a program: the value the executable's header names (e_entry in an ELF header, LC_MAIN in a Mach-O file). The Linux convention is the symbol _start; a C program's main runs later, called by the startup code. More in “First program: return an exit status” is 0x80000000, and the three LOAD program header: An entry of an ELF file's program header table that describes one segment: its type, where its bytes are in the file, the address to load them at, its size in memory and its permissions. are the code (R E, 0x38 bytes), the message (R, 0x1e bytes: 29 characters and a zero byte) and a read-write segment of 0x1000 bytes with nothing in the file, which is the stack. auipc and addi build sp = 0x80001060 from the pc, and lui t1, 0x10000 loads the UART’s address, 0x10000000. The AArch64 image enters at 0x40200000. The script below starts both images on QEMU in your Linux lab:

run_on_qemu.sh: start both images on QEMU Shell · run_on_qemu.sh
#!/bin/sh
# Run the lesson's images on QEMU's virt machines, in your Linux lab (QEMU's system emulators installed).
# -machine virt       the board: RAM, UART and exit device at the addresses the lesson uses
# -bios none          RISC-V only: no firmware, so the reset code jumps straight to 0x80000000
# -cpu cortex-a57     AArch64 only: the virt default is a 32-bit core, so name a 64-bit one
# -nographic          no window: the UART is wired to this terminal (Ctrl-a x leaves QEMU by hand)
# -semihosting        AArch64 only: let hlt #0xf000 reach QEMU, so SYS_EXIT can end it
# -kernel <file>      load the ELF image's segments at the addresses in its program headers
set -eu
clang --target=riscv64-unknown-elf -march=rv64gc -mabi=lp64 -c riscv/hello.s -o hello_rv.o
ld.lld -T riscv/link.ld hello_rv.o -o hello_rv.elf
clang --target=aarch64-none-elf -c aarch64/hello.s -o hello_a64.o
ld.lld -T aarch64/link.ld hello_a64.o -o hello_a64.elf

status=0
qemu-system-riscv64 -machine virt -bios none -nographic -kernel hello_rv.elf || status=$?
echo "qemu exit status: $status"

status=0
qemu-system-aarch64 -machine virt -cpu cortex-a57 -nographic -semihosting -kernel hello_a64.elf || status=$?
echo "qemu exit status: $status"

# Write the device tree QEMU generates for each board to a file and stop, to read its memory map
qemu-system-riscv64 -machine virt,dumpdtb=virt-riscv.dtb -bios none -nographic
qemu-system-aarch64 -machine virt,dumpdtb=virt-arm.dtb -cpu cortex-a57 -nographic
run_on_qemu.sh in the Linux lab Python · qemu_lab_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import linux_lab

# In the Linux lab: both images built and run on QEMU's virt boards, then each board's device tree written to a file.
linux_lab.run("""
sh run_on_qemu.sh
ls virt-*.dtb
""")

Output

$ sh run_on_qemu.sh
Hello from bare-metal RISC-V
qemu exit status: 0
Hello from bare-metal AArch64
qemu exit status: 0
$ ls virt-*.dtb
virt-arm.dtb  virt-riscv.dtb

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 qemu_lab_session.py

Both messages arrived, QEMU ended with status 0 each time, and the last two lines wrote both device trees to files.

A linker script can also name regions and segments and stop a bad link, and a real UART needs a driver that waits before each byte.

Checkpoint

Check: Linking at RAM, checked in the ELF

  1. Question 1 of 2 You change . = 0x80000000; to . = 0x80001000; in link.ld and build again. What does llvm-readelf -h print as the entry point?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 0x80001000

    The entry point is the address of _start after linking. On this board with -bios none it must equal 0x80000000, so a moved image is visible in the header before it ever runs.

  2. Question 2 of 2 The third LOAD segment of hello_rv.elf has file size 0 and memory size 0x1000. How many bytes of stack does it reserve?

    Type a number.

    A hint

    The memory size is in hexadecimal; nothing of it is stored in the file.

    Show the answer: Question 2 of 2

    Answer: 4096

    0x1000 is 4,096 bytes, the .space 4096 in hello.s's .bss. It takes memory but no file bytes, which is why the file size is 0.

Practise: change the image and predict the read-back

Copy hello.s, link.ld and the build session, and predict each result before you run the copy.

  1. Lengthen the message by five characters and predict the file size of the second LOAD segment.
  2. Change . = 0x80000000; to . = 0x80200000; in link.ld, then predict the entry point and the value the first two instructions put in sp.
  3. Link the AArch64 object with the RISC-V link.ld and predict its entry point. A board gets 128 MiB of RAM unless -m asks for more: use the table to say why the AArch64 board has none at that address.

Practice that adapts to you

Practise this lesson

8 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 8 Which of these can a bare-metal program on QEMU's virt board use as soon as it starts?

    Choose one answer.

    Show the answer: Question 1 of 8

    Answer: The device registers, reached with ordinary loads and stores

    With no operating system there are no services to ask, only the hardware: RAM to use and device registers to load from and store to.

  2. Question 2 of 8 Why does nothing stop hello.s from storing to the UART on the RISC-V virt board started with -bios none?

    Choose one answer.

    Show the answer: Question 2 of 8

    Answer: The hart runs it in machine mode, the highest privilege mode, so no protection set by lower levels applies

    Privilege is a property of the mode the hart runs in. -bios none starts the image in machine mode with nothing above it.

  3. Question 3 of 8 Where does RAM start on QEMU's AArch64 virt board?

    Choose one answer.

    Show the answer: Question 3 of 8

    Answer: 0x40000000

    The two virt boards differ: RISC-V RAM starts at 0x80000000 and AArch64 RAM at 0x40000000.

  4. Question 4 of 8 Why is the AArch64 hello image linked at 0x40200000 instead of at the start of RAM?
    Show the answer: Question 4 of 8

    Answer: QEMU writes the device tree at the first byte of RAM when it loads an ELF image

    The image and the 1 MiB device tree must not overlap: an image that starts inside that megabyte gets no device tree at all, so the script starts the code above it.

  5. Question 5 of 8 Your AArch64 program must keep working on later QEMU versions of the virt board. Which address may it hard-code?

    Choose one answer.

    Show the answer: Question 5 of 8

    Answer: The start of RAM, 0x40000000

    Hard-code what the board's documentation promises and read the rest from the device tree QEMU passes in.

  6. Question 6 of 8 You lengthen hello.s's message from 29 to 34 characters. What file size, in bytes, will the second LOAD segment show?
    A hint

    .asciz adds one zero byte after the characters.

    Show the answer: Question 6 of 8

    Answer: 35

    The read-only segment holds only the message: 34 characters and the terminating zero byte make 35 bytes, 0x23, where the run showed 0x1e for 29 + 1.

  7. Question 7 of 8 With . = 0x80200000; in link.ld, which value do the first two instructions of hello_rv.elf put in sp?
    Show the answer: Question 7 of 8

    Answer: 0x80201060

    auipc and addi build an address relative to the pc, so moving the whole image moves sp by the same amount.

  8. Question 8 of 8 You link hello.s at 0x80200000 and start it on the RISC-V virt board with -bios none. Does _start run first?
    Show the answer: Question 8 of 8

    Answer: No, the reset code jumps to 0x80000000, where none of the image's code is

    Check the entry point against the address the board starts from before you run anything: llvm-readelf -h shows it.

Placing code, data and a stack with a linker script Pro: opens with a Pro, Premium or Ultimate pass

A linker script decides where every byte of a bare-metal image lives and which permissions each part of it gets. Writing those decisions down as named regions, named segments and an explicit check turns two quiet mistakes into errors at link time.

In this part:

  • Naming the RAM and three segments with MEMORY and PHDRS
  • A stack and a zeroed .bss before the first C call
  • Two wrong links reproduced, and the checks that stop them

4 runnable examples (Python, link_checked.ld, link_naive.ld, link_regions.ld, main.c, start.s) · about 7 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Driving a UART properly and leaving QEMU with a status Pro: opens with a Pro, Premium or Ultimate pass

QEMU’s UART models take every byte the moment it is stored, so a program that never asks whether the transmitter has room still prints on QEMU. A driver meant for real chips waits on a status bit first, and an exit convention has to survive the whole trip from the guest’s store to the status a shell or a test job reads.

In this part:

  • Waiting for the transmitter on the 16550 and the PL011
  • A hex printer, checked on the host before it meets a board
  • From the stored word to the shell’s exit status, with the arithmetic checked
  • Reading a real board’s map from its device tree

3 runnable examples (AArch64, Host harness, Python, RISC-V) · about 8 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

What is memory-mapped I/O, and how does a bare-metal program on QEMU’s RISC-V virt board print one character with it?

A device answers a range of physical addresses, so loads and stores there reach its registers instead of RAM: that is memory-mapped I/O. To print a character, the program stores its byte to the UART’s transmit register, the first byte of the NS16550A at 0x10000000 on QEMU’s RISC-V virt board. No system call is involved: the store itself is the command. The address comes from the board’s memory map, in QEMU’s source or the device tree it passes in.

Three longer questions in the module’s bank build on this one: an image that prints nothing, failures a test job reads as passes, and the move to a real board.

  1. Mid to senior

    A bare-metal RISC-V image builds and links without a message, but on QEMU's virt board started with -bios none it prints nothing and never exits. How would you find the cause before running it again, and what would you fix?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    A CI job runs bare-metal tests on QEMU and treats exit status 0 as a pass, yet some failing tests show 0. With QEMU's RISC-V test device and Arm semihosting as the exit paths, explain how a failure can arrive as 0 and how you would make every failure visible.

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    Your UART hello program works on QEMU's RISC-V virt board, and now it has to run on a real RISC-V development board. What would you check and change before it prints reliably there, and where would you find each answer?

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What must a bare-metal program set up for itself?

    Everything an operating system would: its linker script fixes where it loads, and its first instructions set up a stack before anything else runs. It is the only software on the machine.

  2. How does a bare-metal program on QEMU's RISC-V virt board print a character?

    By memory-mapped I/O: it stores the byte to the UART's transmit register, the first byte of the NS16550A at 0x10000000. The store itself is the command, with no system call involved.

  3. How does a bare-metal program end a QEMU run on the RISC-V virt board?

    It stores 0x5555 to the test device at 0x100000.

  4. Where must a RISC-V bare-metal image for QEMU's virt board begin when QEMU runs with -bios none?

    At 0x80000000, the first byte of the board's RAM, because that is where execution starts; the image is linked for that address.

  5. How do you check an image's entry point and segments before running it?

    llvm-readelf -h -lW prints the entry point and the program headers, the segments a loader copies and where they go.

Key takeaways

  • A bare-metal program is the only software on the machine: its linker script fixes where it loads, and its first instructions set up its stack.
  • Devices are reached by memory-mapped I/O: on QEMU’s RISC-V virt board a byte stored to 0x10000000 goes to the UART, and 0x5555 stored to 0x100000 ends the run.
  • With -bios none, QEMU’s RISC-V reset code jumps to 0x80000000, the first byte of RAM, whatever the ELF entry point says, so the image must begin there.
  • llvm-readelf -h -lW shows an image’s entry point and the segments a loader copies, before the image runs.

Terms in this lesson

bare metal
Software that runs directly on the hardware with no operating system under it. It places itself in memory through its linker script, sets up its own stack and drives devices through their registers.
device tree
A data structure that describes a board's hardware: where its RAM is and which devices it has, at which addresses and with which interrupts. Firmware or an emulator passes it to the software it starts.
linker script
A file that tells the linker where each output section goes in memory and which symbols to define. A bare-metal image needs one because no operating system's loader chooses its addresses.
load address
The memory address a program's bytes are copied to before it runs. A bare-metal image is linked for the address its board's reset code or loader uses.
memory-mapped I/O
Reaching a device through ordinary loads and stores: the device answers a range of physical addresses, so a store there writes one of its registers instead of RAM.
program header
An entry of an ELF file's program header table that describes one segment: its type, where its bytes are in the file, the address to load them at, its size in memory and its permissions.
semihosting
A convention by which a program asks the debugger or emulator running it for a host service, such as printing text or ending the run, through a special instruction. Arm defines it, and QEMU answers it when started with -semihosting.
UART
Universal asynchronous receiver-transmitter, the device that turns bytes into the bits of a serial line and back. A program sends a byte by writing it to the UART's data register.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.