Skip to content
Contents

Booting: the reset vector, crt0 and the memory map

Section 9.2 8 min read

  • the reset vector as an ISA fact
  • what the linker script places, and where
  • crt0 line by line: symbols, not numbers
  • the exit door and _park

Checked against: carolyne_ref/crt0_mips.S carolyne_ref/dumps/hello_o0.dump (bfc00000–bfc000a4) carolyne_ref/isa_mips/field_match.py (RESET_PC) Carolyne examples/compile_tool/template/link.ld.in carolyne_ref/carolyne_io.h MIPS32 Vol. III: reset exception vector 0xBFC00000

Before you start

  • §2.2: crt0 fills the registers
  • §3.2: lui + addiu and %hi/%lo
  • §4.1: the memory map
  • §5.3: jal and the link register

By the end, you can

  • Say why the first instruction is at 0xBFC00000 and who decided
  • Read the linker script and say which memory each section goes to
  • Explain every line of crt0, including the two-step addresses
  • Trace main's return value from $v0 to the exit door

The reset vector is an ISA fact

A CPU needs a first address. On MIPS32 that address is the , 0xBFC00000, and the architecture fixes it. Every MIPS32 core, from a router chip to your generated one, fetches its first word from there.

Your description states it as a fact of the ISA: RESET_PC in field_match.py. The fetch stage resets its PC register to it. RISC-V does the opposite: the vector is the implementation’s choice. Your RV32IM description picks 0, and a machine may pick another value.

What the linker script places

Putting something useful there is the linker script’s job. The compile tool generates it from the same memory layout that sizes the hardware. Here is the part that matters, with the numbers filled in for your machine:

MEMORY {
  IMEM (rx) : ORIGIN = 0xbfc00000, LENGTH = 8192
  DMEM (rw) : ORIGIN = 0x10000000, LENGTH = 16368
}
__mmio_putchar = 0x10003ff0;
__mmio_putint  = 0x10003ff4;
__mmio_exit    = 0x10003ff8;
__stack_top    = 0x10003ff0;

SECTIONS {
  .text   : { KEEP(*(.text.start))  *(.text .text.*) } > IMEM   /* crt0 first: the reset vector */
  .rodata : { *(.rodata .rodata.*) }                   > DMEM   /* strings: a load must reach them */
  .data   : { *(.data .data.*)  _gp = . + 0x7ff0; }    > DMEM
  .bss    : { *(.bss .bss.*) }                         > DMEM   /* zeros: the image is zero-filled */
}

Two decisions are written here. First, .text.start, the section crt0 declares, is placed first in the instruction memory. So _start lands exactly on the reset vector. Second, everything a load can reach goes to the data memory, .rodata included. The string "hello from carolyne" is read-only. But lb reads the data memory, so that is where it must be.

Where things live: the test machine's memory map (16 KB of data)

Higher addresses are at the top. The stack starts just under the doors and grows downward.

0xFFFFFFFF 0x00000000 the program (instruction memory) crt0, main, the doors' helpers · 8 KB 0xBFC00000 ← the reset vector: the PC starts here (§9.2) … nothing is mapped between the regions … 0x10004000 exit door 0x10003FF8 putint door 0x10003FF4 putchar door 0x10003FF0 ← three words the harness watches (§1.2) 0x10003FF0 ← $sp starts here (crt0) the stack grows toward lower addresses (§4.4) .data and .rodata: globals, string constants reached only by lw / sw / lb / sb … 0x10000000 ← the data memory starts here

What to notice

This is the map from §4.1, and now you know who drew it. The MEMORY block is the two hardware memories. The four symbols are the doors and the stack top. crt0 and the C header both take their addresses from here. The data length stops at 0x10003FF0, so no variable can ever be placed on a door.

The four symbols are how crt0 avoids writing any number of its own. The C header carolyne_io.h is generated from the same layout. So the assembly, the C and the hardware cannot disagree about an address.

crt0, line by line

Here is the beside the words the linker made of it. Click a source line to see which dump lines it became.

crt0_mips.S beside its 42 words in hello_o0.dump (bfc00000–bfc000a4)

carolyne_ref/crt0_mips.S; carolyne_ref/dumps/hello_o0.dump

crt0_mips.S
1.set noreorder
2.set noat
3_start:
4.irp n, 1, 2, ... 31
5 move $\n, $zero
6.endr
7lui $gp, %hi(_gp)
8addiu $gp, $gp, %lo(_gp)
9lui $sp, %hi(__stack_top)
10addiu $sp, $sp, %lo(__stack_top)
11jal main
12nop
13lui $t0, %hi(__mmio_exit)
14addiu $t0, $t0, %lo(__mmio_exit)
15sw $v0, 0($t0)
16_park: b _park
17nop
hello_o0.dump
bfc0000000000825 move at,zero
bfc0000400001025 move v0,zero
bfc0000800001825 move v1,zero
bfc0000c00002025 move a0,zero
bfc0001000002825 move a1,zero
… … 23 more moves: a2 … k1
bfc000700000e825 move sp,zero
bfc000740000f025 move s8,zero
bfc000780000f825 move ra,zero
bfc0007c3c1c1001 lui gp,0x1001
bfc00080279c8010 addiu gp,gp,-32752
bfc000843c1d1000 lui sp,0x1000
bfc0008827bd3ff0 addiu sp,sp,16368
bfc0008c0ff00063 jal bfc0018c <main>
bfc0009000000000 nop
bfc000943c081000 lui t0,0x1000
bfc0009825083ff8 addiu t0,t0,16376
bfc0009cad020000 sw v0,0(t0)
bfc000a01000ffff b bfc000a0 <_park>
bfc000a400000000 nop

Click any highlighted line to see it light up in the other listing, and read what it does.

What to notice

Three source lines became 31 words. Every %hi and %lo became a number. The _gp pair shows the +1 rule of §3.2 in a real program. jal main became jal bfc0018c: the linker knew where main ended up. The two nop lines are in the source by hand, because noreorder means the assembler adds none.

Why does crt0 write 0 into thirty-one registers? Because a register powers up holding nothing you can rely on. In simulation it holds X, “unknown”, and any instruction reading X produces X. Thirty-one move words cost nothing next to a program, and after them every register holds a known value.

Watch it run

The runner below starts three registers with garbage, so you can see crt0 erase it. main is a stand-in that returns 7.

From the reset vector to the exit door: crt0 around a main that returns 7

What runs before main, what runs after it, and how does the return value leave the machine?

program (address · word · instruction)

PC →0xbfc000000x00000825_start:move $at,$zero
0xbfc000040x00001025move $v0,$zero
0xbfc000080x00001825move $v1,$zero
0xbfc0000c0x00002025move $a0,$zero
0xbfc000100x00002825move $a1,$zero
0xbfc000140x00003025move $a2,$zero
0xbfc000180x00003825move $a3,$zero
0xbfc0001c0x00004025move $t0,$zero
0xbfc000200x00004825move $t1,$zero
0xbfc000240x00005025move $t2,$zero
0xbfc000280x00005825move $t3,$zero
0xbfc0002c0x00006025move $t4,$zero
0xbfc000300x00006825move $t5,$zero
0xbfc000340x00007025move $t6,$zero
0xbfc000380x00007825move $t7,$zero
0xbfc0003c0x00008025move $s0,$zero
0xbfc000400x00008825move $s1,$zero
0xbfc000440x00009025move $s2,$zero
0xbfc000480x00009825move $s3,$zero
0xbfc0004c0x0000a025move $s4,$zero
0xbfc000500x0000a825move $s5,$zero
0xbfc000540x0000b025move $s6,$zero
0xbfc000580x0000b825move $s7,$zero
0xbfc0005c0x0000c025move $t8,$zero
0xbfc000600x0000c825move $t9,$zero
0xbfc000640x0000d025move $k0,$zero
0xbfc000680x0000d825move $k1,$zero
0xbfc0006c0x0000e025move $gp,$zero
0xbfc000700x0000e825move $sp,$zero
0xbfc000740x0000f025move $fp,$zero
0xbfc000780x0000f825move $ra,$zero
0xbfc0007c0x3c1c1001lui $gp,0x1001
0xbfc000800x279c8010addiu $gp,$gp,-32752
0xbfc000840x3c1d1000lui $sp,0x1000
0xbfc000880x27bd3ff0addiu $sp,$sp,16368
0xbfc0008c0x0ff00063jal 0xbfc0018c
0xbfc000900x00000000nop
0xbfc000940x3c081000lui $t0,0x1000
0xbfc000980x25083ff8addiu $t0,$t0,16376
0xbfc0009c0xad020000sw $v0,0($t0)
0xbfc000a00x1000ffff_park:b 0xbfc000a0
0xbfc000a40x00000000nop
0xbfc0018c0x24020007main:li $v0,7
0xbfc001900x03e00008jr $ra
0xbfc001940x00000000nop

registers — read · written this tick

HI 0x00000000LO 0x00000000PC 0xbfc00000

memory 0x10003fe0 … 0x10003ffb — touched this tick

003fe0
001
002
003
003fe4
005
006
007
003fe8
009
00a
00b
003fec
00d
00e
00f
003ff0putchar door
001
002
003
003ff4putint door
005
006
007
003ff8exit door
009
00a
00b

console (what the doors printed)

▮
cycle 0/431 tick = 1 instruction (not a cycle)

tick 0 of 43

Nothing has run yet. The PC (program counter) points at the first instruction.

Three registers start with garbage (t0, sp, ra) and crt0 erases it with the others. gp and sp become linker-script addresses in two steps each. jal links to the word after its slot. main puts 7 in v0. The last store writes 7 to the exit door at 0x10003FF8, and the run ends there: _park never runs.

What to notice

$t0, $sp and $ra start with garbage and turn to 0 as their move runs. $gp and $sp each take two instructions to reach their address. jal writes $ra with 0xBFC00094, the word after the slot. The last store writes 7 into the exit door, and the run stops there. The console is empty, the exit code is 7, and _park never runs.

Before reading on, decide:

crt0's jal main is at 0xBFC0008C. After main's jr $ra, at which address does crt0 continue?

Go deeper: what the ELF holds, and how it becomes two images

The linked ELF is a list of sections, plus an entry point. Each section has its load address and its bytes. The compile tool reads it and writes two images. A section whose address falls in IMEM goes to the instruction image. A section in DMEM goes to the data image. .bss has no bytes in the file, and needs none. The image starts as zeros, so crt0 needs no clearing loop.

The tool also refuses an ELF whose entry point is not reset_pc. The script puts _start there today, and the check is what states that promise. A few sections are discarded on purpose: .MIPS.abiflags, .reginfo, .pdr and the like. They describe the file to other tools. A core that only fetches words has no use for them.

Can you now explain it?

The architecture fixes the first address, the linker script puts crt0 there, crt0 sets the registers and the stack from linker symbols, calls main, and turns main's return value into a store to the exit door.

If you remember only three things

  1. 0xBFC00000 is the ISA's number. Every MIPS core starts there. The script places .text.start first so crt0 is what it finds.
  2. crt0 names symbols, never addresses. _gp, __stack_top and __mmio_exit come from the generated script. The linker fills each lui/addiu pair.
  3. Exit is a store. The core has no halt. crt0 stores $v0 to a watched address and the harness stops the run. _park keeps the PC busy in case it does not.

Now try to explain this one yourself: suppose the linker script placed .text.start after the C functions. What would the core fetch at reset, and what would go wrong?

Go deeper: a model answer, once you have tried

The first word at 0xBFC00000 would be whatever function the linker put first. For hello.c that is print_char’s addiu sp,sp,-8. The core would run it with every register undefined. $sp would be garbage, so the frame store sw s8,4(sp) would write somewhere random. The function would end with jr ra to an undefined address. Nothing would ever reach main. The compile tool catches this before the core sees it. The ELF’s entry point _start would no longer equal reset_pc, and the build stops with that message.

Check your understanding

0/5 answered
  1. 1.Where does a MIPS32 core fetch its first instruction after reset, and who decided that address?

  2. 2.crt0 writes `lui $sp,%hi(__stack_top)` and never a number. Where does the number come from?

  3. 3._gp is 0x10008010, yet crt0 becomes `lui gp,0x1001` then `addiu gp,gp,-32752`. Why 0x1001 and a negative number?

  4. 4.The compile tool splits the ELF into two images. Where does the string "hello from carolyne" go, and why?

  5. 5.On the real core, what happens right after crt0's `sw $v0,0($t0)` to the exit door?


Next: §9.3 — hello.c end to end: -O0, -O2 and the cycle count.