From Assembly to Machine Code
Assembly Fundamentals
Chapter 9 · From Assembly to Machine Code
Every chapter since assembly1-1 has quietly leaned on labels — LOOP, DONE, N — without ever explaining how the assembler figures out the actual numeric address each one refers to. This chapter closes that loop for good: the real algorithm an assembler runs, how it builds and uses a symbol table, what those .FILL/.STRINGZ lines scattered through every example actually are, and — finally, in full mechanical detail — how assembly1-1's own linker stage resolves a label that lives in a completely different file.
The Problem: Forward References
Look back at assembly1-6's own summing loop: the very first real instruction, LD R1, N, refers to the label N — but N's own line doesn't appear until several lines later in the source file. To compute LD's PC-relative offset (per assembly1-3's formula), the assembler needs to know N's actual memory address — but if it only ever reads the file once, top to bottom, generating machine code as it goes, it would hit LD R1, N long before it has any idea where N will end up. This is a forward reference, and it's the exact problem the rest of this chapter exists to solve.
The Two-Pass Algorithm
Real assemblers — including LC-3's — solve this by reading the source file twice:
- Pass 1 — scan the file top to bottom without generating any machine code yet. Track a running location counter, starting at the program's origin address. Every time a label appears, record it — along with the current value of the location counter — in a symbol table. Advance the location counter by one for every instruction or data word, whether or not it has a label.
- Pass 2 — scan the file again, this time actually generating the 16-bit encoding for each line (per
assembly1-5's bitfield formats). Any time an operand is a label, look its address up in the symbol table built during Pass 1, and compute whatever offset the instruction's own addressing mode needs.
By the time Pass 2 needs N's address, Pass 1 has already scanned the entire file and recorded it — forward references stop being a problem because the symbol table is fully built before any actual encoding happens.
Directives — Instructions to the Assembler, Not the CPU
A few lines in every example so far aren't real LC-3 instructions at all — they're assembler directives: instructions for the assembler itself, about how to lay out memory, that never get fetched or executed by the CPU.
| Directive | What it tells the assembler |
|---|---|
| .ORIG | The starting address (location counter value) the program should be loaded at |
| .END | Marks the end of the source file — nothing after this line is assembled |
| .FILL | Reserve one word, initialized to a given value (used since assembly1-1's own pipeline example) |
| .STRINGZ | Reserve a null-terminated string, one character per word (used in assembly1-8) |
| .BLKW | Reserve a block of N words, left uninitialized |
A Worked Trace — Building the Symbol Table
Take assembly1-6's own summing loop, now with .ORIG/.END added, assembled starting at address x3000:
.ORIG x3000 AND R2, R2, #0 ; x3000 LD R1, N ; x3001 — forward reference to N LOOP ADD R2, R2, R1 ; x3002 ADD R1, R1, #-1 ; x3003 BRp LOOP ; x3004 — backward reference to LOOP ST R2, SUM ; x3005 TRAP x25 ; x3006 N .FILL #5 ; x3007 SUM .FILL #0 ; x3008 .END
Pass 1 walks top to bottom, building this symbol table:
| Label | Address |
|---|---|
| LOOP | x3002 |
| N | x3007 |
| SUM | x3008 |
Pass 2 now computes real offsets, using assembly1-3's PC + SEXT(offset9) formula (remembering the PC used is always the address after the current instruction):
LD R1, Nat x3001: offset = N − (x3001+1) = x3007 − x3002 = +5 (a forward reference, resolved only because Pass 1 already knew N's address)BRp LOOPat x3004: offset = LOOP − (x3004+1) = x3002 − x3005 = −3 (a backward reference — negative, since it points earlier in the program)ST R2, SUMat x3005: offset = SUM − (x3005+1) = x3008 − x3006 = +2
assembly1-3 and assembly1-6 both warned that PC-relative offsets are limited to roughly ±256 words (9 bits) or ±1024 words for JSR's 11-bit offset. During Pass 2, if a label's actual computed offset exceeds what the instruction's offset field can hold, the assembler can't just quietly truncate it — it reports a genuine assembly-time error. Those earlier warnings weren't abstract concerns; this is the exact moment they'd stop your program from assembling at all.
Object Files & the Linker — Multi-File Programs Resolved For Real
assembly1-1's own Exercise 2 asked what happens when main.asm references a label defined only in helper.asm. Here's the full mechanism: each file goes through its own independent two-pass assembly, producing its own object file and its own local symbol table. When main.asm's Pass 2 hits a reference to a label that never appeared anywhere in main.asm's own Pass 1 symbol table, the assembler can't compute a real offset — instead, it records that reference as unresolved, tagged as external, inside main.asm's object file.
The linker is what closes this gap: it reads every object file being combined, collects each one's exported symbols (labels meant to be visible to other files) and each one's unresolved external references, matches them up, and patches the real, final addresses into place — producing one complete, fully-resolved executable image with no unresolved references left anywhere.
Hands-On Exercises
Using this chapter's own two-pass algorithm, explain specifically why a single top-to-bottom pass (generating machine code immediately, line by line) cannot correctly assemble LD R1, N when N is defined later in the same file — and why Pass 1 solves it.
Using this chapter's own worked trace, compute the Pass 2 offset for a new instruction ST R1, N if it were inserted immediately after the existing LD R1, N line at address x3001 (so the new instruction sits at x3002, shifting LOOP and everything after it down by one address). Show your work.
Revisit assembly1-1's own Exercise 2 scenario (main.asm calling a label defined only in helper.asm) using this chapter's real mechanics. Name specifically what main.asm's object file contains for that reference after its own Pass 2, and what the linker does to finally resolve it.
📄 View solutionChapter 9 Quick Reference
- Forward reference — a label used before its own definition is reached in a single top-to-bottom scan
- Pass 1 — builds the symbol table (label → address) without generating any machine code
- Pass 2 — generates real machine code, looking up label addresses in the symbol table Pass 1 built
- Directives (.ORIG, .END, .FILL, .STRINGZ, .BLKW) — instructions to the assembler about memory layout, never executed by the CPU
- Offsets are computed as target address − (this instruction's address + 1) — forward references are positive, backward references are negative
- A label's offset exceeding its field's range (±256 for 9 bits, ±1024 for 11 bits) is a real Pass 2 assembly error, not just a theoretical limit
- Object file — one file's own assembled output, with any cross-file references left marked unresolved
- Linker — combines multiple object files, matching unresolved external references against other files' exported symbols and patching in real addresses