Skip to content
Contents

The toolchain and pseudo-instructions

Section 9.1 8 min read

  • the four tools: gcc -S, as, ld, objdump -d
  • the toolchain flags and what each is for
  • pseudo-instructions and $at
  • reading a dump line

Checked against: Carolyne examples/compile_tool (target.py: the mips32 flags) carolyne_ref/dumps/hello_o0.dump (bfc001ac–bfc001f0) carolyne_ref/crt0_mips.S (.set noreorder, .set noat) GNU as: MIPS dependent features

Before you start

  • §1.1: a program is a list of words; the contract
  • §3.2: lui + ori and %hi/%lo

By the end, you can

  • Name the four tools and the file each one produces
  • Say what each of your compiler flags does
  • Expand any pseudo-instruction into the real word or words
  • Read a dump line and know which parts objdump computed

Four tools

The is four programs, each producing one kind of file:

From hello.c to words in memory, and back to text: four tools

Each box is one program run by the compile tool. Each arrow carries one kind of file.

hello.c hello.s hello.o hello.elf hello.dump gcc -SC → assembly text astext → words (.o) ld+ linker script:addresses, symbols objdump -dwords → text again for (i = 0; i < 5; i++) slti $2,$2,5 bne $2,$0,.L3 28420005 1440???? (offset open) 28420005 at bfc001e8 1440fff2 at bfc001ec slti v0,v0,5 bnez v0,bfc001b8 instruction memory 8 KB at 0xBFC00000 .text data memory 16 KB at 0x10000000 .data .rodata stack doors the compile tool splits the ELF into the two images the core loads at reset everything left of the ELF is software; the images are what the core sees (§1.1)

What to notice

Only the last box on the top row is a translation back. The ELF in the middle is the real product. The compile tool splits it into the two memory images the core loads. Everything to its left is the software side of §1.1’s contract line.

  1. gcc -S hello.c runs the compiler: C in, assembly text out. The text uses register numbers ($2) or names and local labels (.L3).
  2. as is the : text in, words out. It fills branch offsets from labels and expands the pseudo-instructions below.
  3. ld is the . It joins crt0 and hello.o, and places them according to the . The result is an file. In it, every jal and every lui/addiu pair holds its final address.
  4. objdump -d prints the : the dump files this book has quoted since §1.2.

Your flags, one line each

The compile tool passes these to gcc for the MIPS target:

flagwhat it does
-march=mips32r2allow release-2 instructions (seb, mul, ext, …)
-mabi=32the o32 convention: $a0–$a3, 8-byte frames, the argument area (§2.2, §4.4)
-msoft-floatno floating-point instructions: your core has none
-mno-abicalls -fno-picplain absolute code, no position-independent tables
-G0nothing in the small-data area, so $gp is set but unused
-mno-imaddno madd/msub: four sources, outside your core’s set (§7.2)
-mno-check-zero-divisionno teq after divides: teq is outside the set (§7.2)
-fno-delayed-branchevery delay slot is a nop: your core squashes the slot (§5.4)
-mno-branch-likelyno beql/bnel: the likely branches are outside the set

Each flag closes a gap between what GCC could emit and what your core implements. The compile tool then verifies every word against the description. A flag that slipped would be caught before the core ran.

Pseudo-instructions: spellings, not instructions

The assembler accepts spellings that are not in the instruction set and rewrites them. You have met most of them already. Here they are in one table. Each row ends with a word from a real dump, or from the site’s own assembler:

Pseudo-instructions and what the assembler really emits

hello_o0.dump, fib_o0.dump, and the site's assembler for the rest

move rd,rsor rd,rs,$zero00000825 = move at,zerohello_o0.dump bfc00000 (crt0)
li rt,10addiu rt,$zero,10 (fits 16 signed bits)2404000a = li a0,10hello_o0.dump bfc001cc
li rt,0xffffori rt,$zero,0xffff (fits 16 unsigned)3409ffff = li t1,0xffffthe site's assembler
li rt,0x10003ff0lui rt,0x1000 + ori rt,rt,0x3ff03c021000 + 34423ff0hello_o0.dump bfc000c8, bfc000cc
la rt,_gplui rt,%hi(_gp) + addiu rt,rt,%lo(_gp)3c1c1001 + 279c8010hello_o0.dump bfc0007c, bfc00080
b labelbeq $zero,$zero,label1000000c = b bfc001e4hello_o0.dump bfc001b0
bal labelbgezal $zero,label04110000the site's assembler
beqz rs,labelbeq rs,$zero,label10400004 = beqz v0,bfc002a4fib_o0.dump bfc00290
bnez rs,labelbne rs,$zero,label1440fff2 = bnez v0,bfc001b8hello_o0.dump bfc001ec
nopsll $zero,$zero,000000000every delay slot in every dump
negu rd,rssubu rd,$zero,rs00094823 = negu t1,t1the site's assembler
not rd,rsnor rd,rs,$zero01204027 = not t0,t1the site's assembler

What to notice

Every entry in the third column is an ordinary word from the 63. move is an or with $zero. nop is a shift of $zero. b is a beq of $zero with itself. objdump prints the alias back when it recognises the pattern. That is why the dumps look as if these instructions existed.

Some expansions need a scratch register. A two-register comparison such as blt is one example. That is why $1 is $at, the assembler temporary, and why compilers never keep a value there. crt0_mips.S says .set noat because it uses none of those forms. It says .set noreorder because it writes its own delay slots (§5.4).

Reading a dump line

Here is the loop of hello.c three ways. The C, the -O0 assembly as objdump prints it, and the raw words. Click a C line.

One loop, three views: C, disassembly, words (hello_o0.dump bfc001ac–bfc001f0)

carolyne_ref/programs/hello.c; carolyne_ref/dumps/hello_o0.dump

hello.c
1for (i = 0;
2 i < 5;
3 i++) {
4 print_int(i * i);
5 print_char('\n');
6}
objdump -d
bfc001acsw zero,16(s8)
bfc001b0b bfc001e4 <main+0x58>
bfc001b4nop
bfc001b8lw v0,16(s8)
bfc001bcmul v0,v0,v0
bfc001c0move a0,v0
bfc001c4jal bfc000ec <print_int>
bfc001c8nop
bfc001ccli a0,10
bfc001d0jal bfc000b0 <print_char>
bfc001d4nop
bfc001d8lw v0,16(s8)
bfc001dcaddiu v0,v0,1
bfc001e0sw v0,16(s8)
bfc001e4lw v0,16(s8)
bfc001e8slti v0,v0,5
bfc001ecbnez v0,bfc001b8 <main+0x2c>
bfc001f0nop
the words
1afc00010
21000000c
300000000
48fc20010
570421002
600402025
70ff0003b
800000000
92404000a
100ff0002c
1100000000
128fc20010
1324420001
14afc20010
158fc20010
1628420005
171440fff2
1800000000

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

What to notice

Three things in the middle pane were computed by objdump, not stored in the word. The branch targets. The <symbol+offset> annotations. The aliases b, move, li, bnez. The right pane is what the core actually reads. Every dump line is address: word mnemonic operands, and only the first two columns are facts about memory.

Before reading on, decide:

A dump line reads `bfc00090: 00000000 nop`. Which parts of it are stored in memory, and which did objdump add?

Go deeper: what the compile tool adds, and why it verifies every word

Carolyne’s examples/compile_tool runs the four steps for either target from one description of the machine’s memories. It generates the linker script and carolyne_io.h from one MemoryLayout. So the addresses in the header, the script and the hardware cannot disagree. After linking it does one thing GCC never does. It decodes every executable word with the real decoder functions of the ISA description. A word that matches zero instructions, or two, is refused. For MIPS it also refuses any delay slot that is not nop.

That check is why the flags above are not just preferences. -mno-imadd without the verifier would let a madd slip through and fail silently on the core. With it, the build stops and names the address. The census over 34 builds, quoted often in this book, ran exactly that decoder over every word and counted mnemonics.

Can you now explain it?

gcc -S writes text, as makes words and expands pseudo-instructions, ld places the words by the linker script into an ELF, objdump -d prints them back with aliases and computed targets. Only the words are real.

If you remember only three things

  1. Four tools, four files. hello.c → .s → .o → .elf → .dump. The ELF is the product; the dump is a reading of it.
  2. Pseudo-instructions are spellings. move is or, li is addiu or lui + ori, b is beq $zero,$zero, nop is sll $zero. $at exists so the assembler has a scratch register.
  3. A dump line has two facts and one opinion. The address and the word are memory; the mnemonic, the target and the <symbol> are objdump's.

Now try to explain this one yourself: the compile tool builds with -fno-delayed-branch and then checks every slot is nop. If someone dropped the flag but kept the check, what would happen, and at which tool?

Go deeper: a model answer, once you have tried

GCC would fill many slots with useful instructions, as it does by default. A move or an addiu would move under a branch. The assembler and linker would accept them, since the words are legal. The compile tool’s verifier runs after the link. It would find the first slot that is not 0x00000000, stop the build, and name the branch’s address. Nothing would reach the core. That is the point of checking the words rather than trusting the flags. The guard is at the last step, where every earlier mistake is visible.

Check your understanding

0/5 answered
  1. 1.What does the assembler emit for `li $t0,0x12345678`?

  2. 2.Why is register $1 called $at and kept free by compilers?

  3. 3.Which compiler flag makes every delay slot in your dumps a nop?

  4. 4.The dump shows `00000825 move at,zero`. What instruction is the word 0x00000825 really?

  5. 5.A dump line ends with `bnez v0,bfc001b8 <main+0x2c>`. What does `<main+0x2c>` mean?


Next: §9.2 — booting: the reset vector, crt0 and the memory map.

Where this comes back

One later section builds on this one.