How Code Starts Running

Learning Tock from the ground up · Chapter 3

‹ Contents

How Code Starts Running

Chapter 1 followed one instruction down to a wire changing voltage. Something had to be running that instruction. This chapter follows the chip from the instant power arrives to the first line of Tock's main() — and most of that journey turns out to be a search.

What you'll be able to do at the end

  • Say where this chip looks for its first instruction, and why it is not the start of your program.
  • Read the twenty-eight bytes at the front of a Tock image and say what each word tells the chip.
  • Say what "set up memory" means, and why a variable starting at zero costs no flash while one starting at five does.

Plan on forty minutes. Chapter 1 is assumed: what an address is, and what a store does.

In the book: doc/startup, which walks the same sequence in prose. This chapter watches it happen on an RP2350.

Seven words this chapter needs

Each is one sentence now and repeated in context later. None is hard.

flash
Memory that keeps what it holds when the power is off. Your program lives here.
boot ROM
Code burned into the chip when it was made. It cannot be changed, and it runs before anything of yours.
image
Your compiled program, as it sits in flash.
block
A small self-describing structure with a marker at each end. The boot ROM hunts for one.
RAM
Memory that forgets everything when the power goes off, and can be rewritten as often as you like. Variables live here.
vector table
A list of addresses at the front of a program, saying where to go when something happens.
reset handler
The first of your own instructions to run.

The first instruction is never yours

Power arrives. Both processors come out of reset and begin fetching instructions — but not from your program.

They begin at address 0x00000000, which chapter 1's map of the address space labelled ROM: built-in code, burned in at the factory. It is unchangeable, it is the same on every RP2350 ever made, and it has never heard of Tock.

So the chip's first job is to find your program. Most of what follows is that search.

Both processors start together, and they do not stay that way. The boot code's first decision is to read CPUID, which lives at 0xD0000000 — the SIO block chapter 1 spent its whole length on. Core 1 reads a 1 there, goes into a deep sleep, and takes no further part. Everything below is core 0.

Figure 1 Three moments, and who is running in each

Pick a moment. The panel says who is in control and what they are doing.

Both processors come out of reset and start together. Within a few instructions core 1 is asleep, and nothing of yours has been looked at yet.

Code burned into the chip at manufacture takes control at 0x00000000. It searches flash for something that looks like a program, and it will hand over only if it finds one.

The reset handler runs, sets up memory, and calls main(). Everything chapter 1 described happens after this point.

Notice how much happens before the third panel. Two of the three moments are code you did not write and cannot change.

What the boot ROM is looking for

The ROM does not trust flash to contain a program. Flash might be blank, half-written, or holding something that was never meant to run.

So it searches the first four kilobytes for a block: a structure that says, in a format the ROM knows, "I am an executable image, here is how to start me."

Tock's block is twenty-eight bytes long and sits at the very front of the image. Every word of it is doing a job.

Figure 2 The twenty-eight bytes at the front of a Tock image

Click a word. These are seven words at consecutive addresses at the very front of flash, which is the only reason a scan can find them; the panel says what the ROM does with each.

The marker that says a block starts here. The ROM scans for exactly this number, which was chosen to be unlikely to appear in real Arm or RISC-V instructions.

One item, one word long, describing the image: it is executable, it runs in Secure mode, it is Arm rather than RISC-V, and it is for this chip family.

The header of a second item, two words long. Its type says vector table, so the next word is an address.

That address. It is where the vector table sits, and it is also where the kernel itself begins — one kilobyte into flash, right after this block.

The item that says there are no more. Its size field counts the words of items above it, which is three: one for the image type, two for the vector table.

Where the next block is, relative to this one. Zero means this block, so the list of blocks is a loop containing only itself.

The matching end marker. A block with no footer is not a block, and the ROM keeps looking.

Notice that only one of the seven words is an address. The rest describe the image, or describe the block itself.

Those bytes are METADATA_BLOCK in boards/raspberry_pi_pico_2/src/flash_bootloader.rs, and the format is the RP2350 datasheet, §5.9.

The rest of that first kilobyte

The block is twenty-eight bytes and the kernel does not start until 0x10000400. Chapter 1 said two things share that space. Here is the other one.

28 bytes
the block Figure 2 just took apart, which is how the ROM recognises the image at all
256 bytes
a loader that reconfigures the connection to the flash chip, so that code runs from it faster

The loader is the interesting one, because it is no longer required. On the RP2040 every image had to begin with a routine like this, checksummed, or the chip would not boot. The RP2350's own ROM sets the flash up well enough to run before it hands over.

Tock ships one anyway, and the reason is speed rather than correctness: the ROM's setup is deliberately cautious, and a program is free to replace it with a faster one. That is what these 256 bytes are.

The bytes are FLASH_BOOTLOADER in the same file, and its comment records where they came from. The datasheet is explicit that they are optional now — §5.9.5.

What is at that address

The ROM now knows where to look, and what it finds there is not an instruction. It is a list of addresses.

A Cortex-M processor expects a program to begin with a vector table: one entry for each thing that can happen to it. The first two entries are the only ones that matter at boot.

Figure 3 The list at 0x10000400

Click either of the first two entries. The rest are for later.

+8 … +60   fourteen more, for faults and interrupts

Not code — a number. The processor loads it into the stack pointer before running anything, because code needs somewhere to keep its working values. Tock puts that stack at the bottom of RAM on purpose: overflowing it runs off the end into nothing, which faults, instead of quietly overwriting something.

The reset handler: the address of the first instruction of yours that runs. The processor jumps here, and everything after this point is Tock.

Notice that starting a program takes two numbers, not one. Somewhere to put the stack, and somewhere to begin.

What "set up memory" means

The reset handler has a name that says exactly what it does: initialize_ram_jump_to_main. It is two loops and a call.

It has to be, because RAM at power-on holds whatever it happened to hold. Not zero. Whatever was there, from the last time the board ran or from nothing at all. Every static — every variable that lives for the whole run, rather than one inside a function — has to be put there by somebody, and that somebody is these two loops. Variables inside a function are a different story, set up by that function as it runs.

Figure 4 Two loops, and then main()

Pick a loop and step it. Both walk a range of memory one word at a time, using the store from chapter 1 — and the first thing to notice is what RAM holds before either of them has run.

flash, from _etext — the values the linker put there
0x00000005 0x0000000C 0x00000007 0x00000063 0x00000001 0x000003E8
RAM, from _szero — six words of it
0x8F2A01C4 0x00000000 0xD41B77E0 0x0000002A 0xB0C3FF19 0x5E00A184
r0 — where it writes 0x20000100
r1 — where it stops 0x20000118
r2 0
words still to go 6

RAM at power-on holds whatever it held. Not zero — whatever was there. Step the loop and watch it become something a program can rely on. Zeroing. Each step is one store of r2, which is 0, and the ! on stm r0! is what moves the address on. Notice the second word: it was already zero, and the loop writes it anyway, because nothing told it not to. Copying. Each step is a load from flash and a store into RAM, with both pointers stepping. These are the values a static mut N: u32 = 5 needs, which is why they had to be kept in flash until now. Done, and the loop stopped because r0 reached r1. Nothing counted anything: the comparison is against an address the linker wrote, and that is the whole termination condition.

Writes zero, one word at a time, from _szero to _ezero. That range is every static your program expects to start at zero. The ! on stm r0! is what steps the address forward, so the loop needs no separate add.

Copies, one word at a time, from _etext in flash into the range starting at _srelocate in RAM. That range is every static expecting to start at something other than zero, and the values it copies were stored in flash by the linker. Then bl main, and Tock is running.

Notice that neither loop knows what any of those bytes mean. One writes zeros over a range; the other copies a range. The meaning was decided by the linker, before the chip was ever powered on.

Both loops are initialize_ram_jump_to_main in arch/cortex-m/src/lib.rs, shortened here only by dropping the comments. The five names beginning with an underscore are defined by the linker script, not by any Rust code.

Which loop a variable belongs to is decided by one thing: what it starts as. The consequence is not the one people expect, and it is easier to drag than to read.

Two declarations in your program: a 4 kB buffer of zeroes, and a 100-byte table of constants. Which one takes more room in flash?

Bigger in RAM, and free in flash. Nothing needs storing, because the value is zero and there is already a loop that writes zeroes over a range.

Right, and it is the opposite of the instinct. Asking for more memory can cost nothing; asking for a particular value costs flash, one byte per byte. Those hundred bytes have to be written down somewhere and copied in before your code runs.

Both end up in RAM. Only one of them has to be carried in flash to get there, and it is the one whose contents somebody chose.

There is no page rounding in this accounting. The image carries exactly the bytes whose values were specified, and a range of zeroes specifies nothing.

Figure 5 What a variable costs before it runs

Pick what it starts as, then drag how big it is. One of the two bars grows and the other does not, and which is which is the whole of this figure.

1024 bytes

static mut BUF: [u8; 1024] = [0; 1024];

in RAM 1024 B
in flash 0 B
lives in .bss
costs in flash 0 bytes
set up by the first loop

Starting at zero costs nothing to store, because zero is what the first loop writes anyway. The linker records the range and no more.

Starting at five means the five has to be kept somewhere until the chip runs. It is kept in flash, four bytes of it, and the second loop copies it into RAM.

A kilobyte of RAM, and still nothing in flash. The first loop writes zeros over the range, so a large buffer full of zeros is free to store and only costs the time to clear it.

Notice which way round that is. Asking for more memory can cost nothing; asking for a particular value costs flash, one byte per byte.

The linker script says both halves out loud. _szero and _ezero "define the range of the BSS, SRAM that Tock will zero on boot". The data section is placed with AT (_etext), which is what puts its initial values in flash. Both lines are in boards/build_scripts/tock_kernel_layout.ld.

The whole thing, in order

Every step above, as one list. Nothing here is new, but walking it end to end is where the shape shows: how much of starting a program is the chip looking for it.

Eight moments between power reaching the chip and your kernel running. At which of the eight does code you compiled first execute?

It jumps straight to something, and that something is not yours. Address zero is ROM burned in at the factory, and it is where both processors start looking.

Still not yours. Five of the eight are the chip looking for your program: reading ROM, finding flash, reading a header, checking it, setting flash up so code can run out of it.

Right, and it is further in than almost anyone guesses. Five of the eight are the chip hunting for your program, one is the loader it found, and only the last two are your program running.

By the eighth the kernel is already running. The handover is one step earlier, and what happens at the eighth is your own code continuing.

Figure 6 Power to main(), in eight steps

Walk it from power to main(). Four things about the machine are shown above the steps, and the moment worth watching for is the one where three of the four change at once.

holding the processor the chip
core 1 awake
fetching from 0x00000000
RAM holds whatever it held
step 1 of 8
Notice where the handover falls. Five of the eight steps are the chip looking for your program, and only the last two are your program running.

One thing that is not the boot path

The linker script names an entry point, jump_to_bootloader, and it is tempting to read that as where the chip starts. It is not.

It is there for the debugger. Loading a program over the debug probe does not reset the chip. Without this stub the debugger would start you at the front of flash, which, as Figure 2 showed, is metadata rather than code.

So this stub sends the chip back to the beginning, using five instructions you have already seen and one you have not.

Figure 7 Six instructions that fake a power-on

Click a line. Five of the six you have already met; the panel says which one you have not.

The source writes the second constant as 0xe0000000 + 0x0000ed08, which is the same number said in two halves.

Put zero in a register. It is about to be used as an address, and as a value.

The address of the register that tells the processor where its vector table is. Chapter 1's map labels that whole stripe as the processor's own registers.

The store from chapter 1, doing something drastic: point the vector table back at zero, which is the boot ROM's own.

Read the first two words at zero — the ROM's stack pointer and its entry address — exactly as the hardware would at reset.

Set the stack pointer to the first of them. This is the one new instruction: msr writes one of the processor's special registers, and the stack pointer is one of those rather than an ordinary numbered one.

Jump to the second. The chip is now doing what it would have done on a power-on, and the whole search happens again.

Notice that it never calls anything. It rearranges two numbers and jumps, which is all starting a program ever was.

Check yourself

Four questions, three choices each. Commit to one and the reasoning opens underneath it.

1. Where does this chip fetch its first instruction?

Right. Not quite. From 0x00000000, which is the boot ROM. That code was burned in at manufacture and is the same on every one of these chips. Your program does not run until the ROM has found it and handed over.

2. Why does a Tock image begin with twenty-eight bytes that are not instructions?

Right. Not quite. So the boot ROM can recognise it. Flash might be blank, or half-written, or holding something never meant to run. The block is how an image says what it is, and where to start it.

3. What does the first loop of the reset handler do?

Right. Not quite. Writes zeros over the range between _szero and _ezero. RAM at power-on holds whatever it held, so anything expecting to start at zero has to be put there.

4. One variable is declared = 0, another = 5. Which costs flash?

Right. Not quite. The one set to 5. Its value has to be kept somewhere until the chip runs, so it sits in flash and the second loop copies it. Zero costs nothing, because zero is what the first loop writes anyway.

So here is the next problem

Tock is running. It has the whole chip: every address, every peripheral, and nothing standing in the way — which is exactly where chapter 1 left off.

The kernel is not one program, though. It is a core with drivers bolted on, and a driver is where most of the code, and most of the mistakes, live.

So the question chapter 4 asks is: when you add a driver, what should it be allowed to touch?

Every word, collected

block
A small self-describing structure with a marker at each end, which the boot ROM hunts for in flash.
boot ROM
Code burned into the chip when it was made, unchangeable, which runs before anything of yours.
flash
Memory that keeps what it holds when the power is off.
image
Your compiled program, as it sits in flash.
reset handler
The first of your own instructions to run.
vector table
A list of addresses at the front of a program, saying where to go when something happens.

Everything above, checked against source

Every claim on this page comes from the Tock tree or the RP2350 datasheet. Nothing here was written from memory.

  • Both cores starting together, then core 1 sleeping — RP2350 datasheet §3.1.2 and §7.4 step 12. The first says it plainly: "both cores start running simultaneously, core 1 goes into a deep sleep state, and core 0 continues the main boot sequence".
  • CPUID sitting at SIO's base, offset 0x000chips/rp2350/src/gpio.rs, the SIORegisters block
  • The ROM searching flash for a block that is part of a closed loop, and why the loop rule exists — RP2350 datasheet §5.1.5.1
  • The block markers 0xffffded3 and 0xab123579, and the rule that the last item counts the words of the block's items — RP2350 datasheet §5.9.1
  • The minimum Arm image being twenty bytes, which is Tock's twenty-eight without the vector-table item — RP2350 datasheet §5.9.5.1
  • The vector-table item having type 0x03 — RP2350 datasheet §5.9.3.3
  • Tock's twenty-eight bytes themselves — boards/raspberry_pi_pico_2/src/flash_bootloader.rs
  • The 256-byte loader sharing that kilobyte, and being optional on RP2350 where RP2040 required a checksummed one at flash address 0 — FLASH_BOOTLOADER in the same file, and RP2350 datasheet §5.9.5
  • 0x10000400 being where the kernel begins — the rom region's origin in boards/raspberry_pi_pico_2/layout.ld
  • The vector table's first two entries being the initial stack pointer and initialize_ram_jump_to_mainchips/rp2350/src/lib.rs
  • The two loops, and the five linker symbols they use — arch/cortex-m/src/lib.rs
  • Tock placing the kernel stack at the lowest address in SRAM so that overflowing it faults rather than "silently overwriting valuable data" — the comment on the .stack section in boards/build_scripts/tock_kernel_layout.ld
  • _szero and _ezero marking "the range of the BSS, SRAM that Tock will zero on boot" — boards/build_scripts/tock_kernel_layout.ld
  • The data section placed with AT (_etext), which is what puts its initial values in flash — the same file
  • The debugger entry point existing because the probe does not reset the chip — the comment on jump_to_bootloader in boards/raspberry_pi_pico_2/src/main.rs
  • 0xE000ED08 being the vector table offset register — RP2350 datasheet, Table 202
  • RP2350 not requiring a checksummed loader at flash address 0, unlike RP2040 — RP2350 datasheet §5.9.5. The 256-byte loader Tock ships beside the block is a flash-speed setup, not a boot requirement.

Text, diagrams and interactive figures © Jon Hillesheim 2026, licensed CC BY-SA 4.0 — share and adapt freely with credit, under the same license. Tock source excerpts quoted above remain under their own Apache-2.0 OR MIT license and are not relicensed here.