LC-3 assembly language: directives, the two-pass assembler, the symbol table
Labels, .ORIG/.FILL/.BLKW/.STRINGZ/.END, the two passes of the assembler and the symbol table; complete programs with loops, string processing and subroutines (JSR/RET), and their outputs.
In weeks 7-8 you wrote machine code by hand: you counted offsets, you placed bits. This is both slow and error-prone. Assembly language leaves this job to a program, the assembler. You write instructions by their names (ADD, LD) and addresses by labels (LOOP, COUNT); the assembler computes the addresses and produces the string of 0s and 1s.
This week’s exam questions are usually like this: build the symbol table of a program, find the machine code of a line from the symbol table, say what a program writes to the screen or what it changes in memory, find an assembler error.
The structure of a line
LOOP ADD R1, R1, #-1 ; decrement the counter
| Field | Example | Required? |
|---|---|---|
| Label | LOOP | no |
| Opcode or pseudo-op | ADD | yes |
| Operands | R1, R1, #-1 | depends on the instruction |
| Comment | ; decrement the counter | no |
Numbers: # is decimal (#-1, #48), x is hexadecimal (x3000, xFE00). A label is the name of an address: LOOP stands for the address of that line in memory.
PITFALL · making an opcode a label. Names like
STR,ADD,BR,R3cannot be labels. If the first word of a line is an opcode, the assembler takes it as an instruction. For a “string” writeTEXTorMSGinstead ofSTR.
Pseudo-ops (assembler directives)
A pseudo-op is not an instruction that runs at run time; it is an instruction given to the assembler. All of them start with a dot.
| Directive | What does it do? | How many words? |
|---|---|---|
.ORIG x3000 | The starting address of the program | 0 |
.FILL #4 | Put the value 4 at this location | 1 |
.FILL LOOP | Put the address of LOOP at this location | 1 |
.BLKW 3 | Reserve an empty space of 3 words | 3 |
.STRINGZ "Hi" | Each character one word, x0000 at the end | length + 1 = 3 |
.END | The end of the source code | 0 |
.END is not where the program stops. The program stops with HALT; .END only tells the assembler “what follows is not source”.
The two-pass assembler
The problem: when it reaches the line LD R1, COUNT, COUNT may be defined further down. To compute the offset, the address of COUNT is needed. This is called a forward reference.
The solution: read the program twice.
- First pass: translate nothing. Keep a location counter: start it with .ORIG, add 1 for each instruction and .FILL, n for .BLKW n, length + 1 for .STRINGZ. Write the address of each labeled line into the symbol table.
- Second pass: translate each line to machine code. When a label is used, take its address from the symbol table and compute the offset.
Worked example: the symbol table
.ORIG x3000
LEA R0, MSG ; x3000
PUTS ; x3001
LD R1, COUNT ; x3002
LOOP ADD R1, R1, #-1 ; x3003
BRp LOOP ; x3004
HALT ; x3005
COUNT .FILL #4 ; x3006
MSG .STRINGZ "Hello" ; x3007 - x300C (6 words)
BUF .BLKW 3 ; x300D - x300F
END1 .FILL x0041 ; x3010
.END
| Symbol | Address |
|---|---|
| LOOP | x3003 |
| COUNT | x3006 |
| MSG | x3007 |
| BUF | x300D |
| END1 | x3010 |
Part of the code produced by the second pass:
x3000 xE006 LEA R0, MSG offset = x3007 - x3001 = 6
x3001 xF022 PUTS
x3002 x2203 LD R1, COUNT offset = x3006 - x3003 = 3
x3003 x127F ADD R1, R1, #-1
x3004 x03FE BRp LOOP offset = x3003 - x3005 = -2
x3005 xF025 HALT
x3006 x0004
x3007 x0048 x0065 x006C x006C x006F x0000 "Hello"
When the program runs, it writes Hello to the screen, then counts R1 from 4 down to 0 and stops.
Assembler errors
| Line | Error |
|---|---|
ADD R1, R1, #20 | outside imm5 -16..15 |
LD R1, FAR (FAR is 300 words away) | outside PCoffset9 -256..255 |
BRz DONEE | undefined label (typo) |
LOOP defined twice | the same label with two addresses |
STR .FILL #1 | an opcode cannot be used as a label |
Programming patterns
Searching for a character
The way to compare a character: put its negative in a constant, add, and if the result is zero they are equal.
.ORIG x3000
AND R2, R2, #0 ; R2 = counter
LEA R1, TEXT ; R1 = string pointer
LD R4, NEGT ; R4 = -'t'
NEXT LDR R3, R1, #0 ; R3 = *R1
BRz DONE ; null → finished
ADD R5, R3, R4 ; R5 = character - 't'
BRnp SKIP ; not equal → skip
ADD R2, R2, #1 ; equal → count
SKIP ADD R1, R1, #1 ; next character
BRnzp NEXT
DONE LD R0, ZERO
ADD R0, R0, R2 ; turn the count into an ASCII digit
OUT
HALT
NEGT .FILL #-116 ; 't' = 116
ZERO .FILL x30 ; '0'
TEXT .STRINGZ "test it"
.END
3
If the counter exceeds 9, this method cannot print a single digit; that is why exam questions come with small numbers.
Read from the keyboard, process, write
.ORIG x3000
LD R2, NEGLF ; R2 = -10 (end of line)
READ GETC ; R0 = character
ADD R3, R0, R2
BRz STOP ; Enter → stop
LD R4, MASK
AND R0, R0, R4 ; convert to uppercase
OUT
BRnzp READ
STOP HALT
NEGLF .FILL #-10
MASK .FILL xDF
.END
Input bau and Enter:
BAU
Subroutines: JSR and RET
To use the same code in many places, a subroutine is written.
JSR LABEL: R7 ← PC (the return address), PC ← LABEL. The offset is 11 bits (-1024..1023).JSRR R3: the same thing, with the target in a register.RET: PC ← R7.
.ORIG x3000
LD R2, TIMES
AGAIN JSR PRINT
ADD R2, R2, #-1
BRp AGAIN
HALT
PRINT ST R7, SAVE7 ; save the return address
LEA R0, TXT
PUTS ; TRAP: overwrites R7!
LD R7, SAVE7 ; get the return address back
RET
TIMES .FILL #3
SAVE7 .BLKW 1
TXT .STRINGZ "ab"
.END
ababab
PITFALL · not saving R7. If there is a TRAP or another JSR inside the subroutine, R7 is overwritten. If you do not save it, RET returns to the wrong place. It is also a good habit to save the registers the subroutine disturbs (callee-save). In C this job is done with the runtime stack (week 12).
Common mistakes
| Wrong idea | The truth |
|---|---|
.STRINGZ "Hi" is 2 words | 3: there is an x0000 at the end |
.BLKW 3 is 1 word | 3 words |
.END stops the program | HALT stops it; .END is the end of the source |
.ORIG takes one word | It takes none |
| One pass is enough | Two passes, for forward references |
| Offset = label - line address | label - (line address + 1) |
ADD R1, R1, #20 is valid | imm5 is at most 15 |
| TRAP preserves R7 | It writes the return address into R7 |
Test yourself
Exam-style questions. Solve them on paper first, then reveal the answer.
How many memory locations does MSG .STRINGZ "Hello" occupy?
Show answer
Each character is written into one word (16 bits): H, e, l, l, o = 5. An x0000 (null terminator) is added at the end. Total 6. The general rule: length + 1.
Build the symbol table for this program.
.ORIG x3000
LEA R0, MSG
PUTS
LD R1, COUNT
LOOP ADD R1, R1, #-1
BRp LOOP
HALT
COUNT .FILL #4
MSG .STRINGZ "Hello"
BUF .BLKW 3
END1 .FILL x0041
.ENDShow answer
The location counter starts at x3000; each instruction and .FILL takes 1 place. LEA x3000, PUTS x3001, LD x3002, LOOP x3003, BRp x3004, HALT x3005, COUNT x3006, MSG x3007.
.STRINGZ "Hello" takes 6 places: x3007-x300C. BUF x300D, .BLKW 3 → x300D-x300F. END1 x3010.
Using the symbol table above, what is the machine code of LD R1, COUNT at address x3002?
Show answer
offset = COUNT - (x3002 + 1) = x3006 - x3003 = 3. 0010 001 000000011 = x2203. The assembler does this calculation in the second pass with the symbol table.
Why does the LC-3 assembler make two passes over the program?
Show answer
When it reaches the line LD R1, COUNT, COUNT may not have been defined yet (it is further down). The first pass only counts addresses and builds the symbol table. The second pass translates each line to machine code; now the address of every label is known.
What is the difference between an instruction such as ADD and a pseudo-op such as .FILL?
Show answer
A pseudo-op (assembler directive) does not run at run time, it has no opcode. It tells the assembler what to do: .ORIG the starting address, .FILL put a value, .BLKW reserve space, .STRINGZ put a string, .END the source is over.
Which of these lines generate no memory words at all: .ORIG x3000, .FILL #0, .BLKW 1, .END, HALT?
Show answer
.ORIG only sets the location counter, .END only says that the source is over. .FILL #0 and .BLKW 1 each reserve one word (x0000), and HALT produces an instruction (xF025).
The assembler reports an error for ADD R1, R1, #20. Why, and how can you add 20 to R1?
Show answer
imm5 is a 5-bit 2's complement value, 20 does not fit. The fix: ADD R1, R1, #10 twice, or define TWENTY .FILL #20, then LD R2, TWENTY, ADD R1, R1, R2.
What is printed by this program?
.ORIG x3000
AND R2, R2, #0
LEA R1, TEXT
LD R4, NEGT
NEXT LDR R3, R1, #0
BRz DONE
ADD R5, R3, R4
BRnp SKIP
ADD R2, R2, #1
SKIP ADD R1, R1, #1
BRnzp NEXT
DONE LD R0, ZERO
ADD R0, R0, R2
OUT
HALT
NEGT .FILL #-116
ZERO .FILL x30
TEXT .STRINGZ "test it"
.ENDShow answer
-116 = -'t' (ASCII 't' = 116). For each character R5 = character - 't'; if it is zero, a 't' was found and R2 is incremented. When the null (0) arrives, the loop ends.
There are 3 't's in "test it". R0 = x30 + 3 = x33 = '3', OUT prints it.
What is the sum stored at TOTAL?
AND R0, R0, #0
LEA R1, NUMS
LD R2, N
LOOP LDR R3, R1, #0
ADD R0, R0, R3
ADD R1, R1, #1
ADD R2, R2, #-1
BRp LOOP
ST R0, TOTAL
HALT
N .FILL #4
NUMS .FILL #12
.FILL #-5
.FILL #30
.FILL #-7
TOTAL .BLKW 1Show answer
The counter loop adds up 4 elements: 12 + (-5) + 30 + (-7) = 30. R1 is a pointer (the start of the array via LEA), and in each round it moves to the next word.
What is printed by this program? Why does PRINT save R7?
LD R2, TIMES
AGAIN JSR PRINT
ADD R2, R2, #-1
BRp AGAIN
HALT
PRINT ST R7, SAVE7
LEA R0, TXT
PUTS
LD R7, SAVE7
RET
TIMES .FILL #3
SAVE7 .BLKW 1
TXT .STRINGZ "ab"Show answer
JSR writes the return address into R7. The PUTS inside PRINT is a TRAP, and it overwrites R7 too. If R7 were not saved, RET would return to the wrong place (the line after PUTS) and the program would enter an infinite loop. Because it is saved and restored, the subroutine is called 3 times: ababab.
A program reads characters with GETC until the newline character (ASCII 10), ANDs each with xDF and prints it. What is printed for the input bau followed by Enter?
Show answer
AND xDF clears bit 5: a lowercase letter turns into uppercase. 'b' x62 → x42 'B', 'a' x61 → x41 'A', 'u' x75 → x55 'U'. When the newline (10) arrives, the loop ends. Note: digits are also affected by this mask ('3' x33 → x13), so it is used only on letters.
Why can't you name a label STR or ADD in LC-3 assembly?
Show answer
The assembler looks at the first word of a line: if it is an opcode or a pseudo-op it is an instruction, otherwise a label. Because STR and ADD are opcodes, they are not read as labels and the line is parsed wrongly. Labels start with a letter and cannot be an opcode, a register name (R0-R7) or a number.
Verification: All symbol tables, machine codes, assembler errors and program outputs were produced with our own two-pass LC-3 assembler + simulator (verify/SEN2201/lc3.mjs, verify/SEN2201/10.mjs).