SEN2201 · Week 10 · Assembly Language

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.

assembly languageassemblerlabelopcodeoperandcommentpseudo-op (assembler directive).ORIG.FILL.BLKW.STRINGZ.ENDtwo-pass assemblysymbol tablelocation countersubroutineJSRRET

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
FieldExampleRequired?
LabelLOOPno
Opcode or pseudo-opADDyes
OperandsR1, R1, #-1depends on the instruction
Comment; decrement the counterno

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, R3 cannot be labels. If the first word of a line is an opcode, the assembler takes it as an instruction. For a “string” write TEXT or MSG instead of STR.

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.

DirectiveWhat does it do?How many words?
.ORIG x3000The starting address of the program0
.FILL #4Put the value 4 at this location1
.FILL LOOPPut the address of LOOP at this location1
.BLKW 3Reserve an empty space of 3 words3
.STRINGZ "Hi"Each character one word, x0000 at the endlength + 1 = 3
.ENDThe end of the source code0

.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.

  1. 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.
  2. 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
SymbolAddress
LOOPx3003
COUNTx3006
MSGx3007
BUFx300D
END1x3010

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

LineError
ADD R1, R1, #20outside imm5 -16..15
LD R1, FAR (FAR is 300 words away)outside PCoffset9 -256..255
BRz DONEEundefined label (typo)
LOOP defined twicethe same label with two addresses
STR .FILL #1an 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 ideaThe truth
.STRINGZ "Hi" is 2 words3: there is an x0000 at the end
.BLKW 3 is 1 word3 words
.END stops the programHALT stops it; .END is the end of the source
.ORIG takes one wordIt takes none
One pass is enoughTwo passes, for forward references
Offset = label - line addresslabel - (line address + 1)
ADD R1, R1, #20 is validimm5 is at most 15
TRAP preserves R7It writes the return address into R7

Test yourself

Exam-style questions. Solve them on paper first, then reveal the answer.

Question 1easy

How many memory locations does MSG .STRINGZ "Hello" occupy?

Show answer
6

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.

Question 2medium

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
        .END
Show answer
LOOP x3003, COUNT x3006, MSG x3007, BUF x300D, END1 x3010

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.

Question 3medium

Using the symbol table above, what is the machine code of LD R1, COUNT at address x3002?

Show answer
x2203

offset = COUNT - (x3002 + 1) = x3006 - x3003 = 3. 0010 001 000000011 = x2203. The assembler does this calculation in the second pass with the symbol table.

Question 4easy

Why does the LC-3 assembler make two passes over the program?

Show answer
To resolve forward references: labels used before they are defined

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.

Question 5easy

What is the difference between an instruction such as ADD and a pseudo-op such as .FILL?

Show answer
ADD becomes a machine instruction executed at run time; .FILL is a directive to the assembler

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.

Question 6medium

Which of these lines generate no memory words at all: .ORIG x3000, .FILL #0, .BLKW 1, .END, HALT?

Show answer
.ORIG and .END

.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).

Question 7medium

The assembler reports an error for ADD R1, R1, #20. Why, and how can you add 20 to R1?

Show answer
imm5 range is -16..15; load 20 from a .FILL or use two ADDs

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.

Question 8medium

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"
        .END
Show answer
3

-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.

Question 9easy

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 1
Show answer
30

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.

Question 10hard

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
ababab; PUTS (a TRAP) overwrites R7

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.

Question 11medium

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
BAU

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.

Question 12hard

Why can't you name a label STR or ADD in LC-3 assembly?

Show answer
They are opcodes (reserved words)

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).

Review card

Line: [LABEL] OPCODE OPERANDS [; comment] .ORIG x3000 start | .END end of source (neither takes memory) .FILL value -> 1 word | .BLKW n -> n words (0) | .STRINGZ "abc" -> 4 words (x0000 at the end) Pseudo-op = an order to the assembler, does not run, has no opcode Pass 1: count the labels with the location counter -> SYMBOL TABLE Pass 2: translate each line to machine code (offset = label - (address+1)) Why 2 passes: forward references (a label defined further down) Errors: outside imm5 (#20), outside offset9 (a label too far), undefined label, using an opcode as a label Need a constant > 15: X .FILL #20 + LD Compare a character: add .FILL #-'c', then BRz JSR LABEL: R7 <- PC, PC <- label | RET: PC <- R7 If the subroutine contains TRAP/JSR, save R7 (ST R7, SAVE7 ... LD R7, SAVE7)