SLIDESHOW LC-3 Assembly Language

Overview of LC-3 Assembly Language and Tooling

  • Transition from Machine Code to Assembly Language:

    • Writing programs directly in hexadecimal or binary machine code requires manually calculating opcodes, register specifiers, and addressing offsets.

    • Assembly language introduces human-readable mnemonics and symbolic labels, allowing automated translation through an assembler.

    • The assembler performs background computations, automatically determining memory addresses, resolving relative offsets, and reserving storage blocks.

  • Core Components of Assembly Language:

    • Machine Instructions: Symbolic representations of hardware operations defined in the LC-3 Instruction Set Architecture (ISA), including ADD, AND, NOT, LD, ST, LDR, STR, JSR, JSRR, BR, and RET.

    • Pseudo-operations (Assembler Directives): Instructions directed to the assembler rather than the processor. Examples include .ORIG, .FILL, .BLKW, .STRINGZ, .END, and .SUB.

    • Labels: Symbolic names placed at the beginning of an instruction or directive to represent its specific memory location.

    • Comments: Prefaced by a semicolon (;). All characters from the semicolon to the end of the line are ignored by the assembler and do not generate machine code.

    • Literal Prefixes: Decimal immediate values are prefixed with # (e.g., ADD R0, R0, #1), while hexadecimal values are prefixed with x (e.g., .ORIG x3000).

  • Assembler Execution Workflow:

    • Programs are written as source text files and saved with the .asm file extension.

    • The source file is loaded into the LC-3 assembler tool.

    • Upon loading or initiating assembly, the assembler analyzes the text, performs validation, and generates a hexadecimal machine code object file (.hex or .obj), a symbol table file (.sym), and an assembly listing file (.lst).

    • Any syntax errors, unresolved symbols, or invalid offsets are displayed in the status pane of the assembler alongside the assembly timestamp.

Formatting Standards and Program Structure Rules

  • Strict Column Alignment and Whitespace Rules:

    • Labels: Must be placed flush against the far-left margin (Column 0). Labels must never be indented.

    • Instructions and Directives: Must be indented by exactly one tab stop from the left margin.

    • Pseudo-ops: Must be indented by exactly one tab stop from the left margin.

    • Operands: Must follow the instruction or directive separated by exactly one space.

    • Commas: Must immediately follow the operand being delimited with no preceding space, followed by exactly one space before the subsequent operand (e.g., ADD R1, R2, #0).

    • Comments: May begin flush at the left margin on dedicated lines, indented with tabs, or placed after instruction operands.

  • Strict Execution and Program Structure Constraints:

    • Prohibition of Manual Hex Conversion: Code must not be converted to hexadecimal by hand; the automated assembler must be used to calculate offsets and generate binary representations.

    • Single Program Entry and Exit: Every complete LC-3 program must begin with exactly one .ORIG directive and contain exactly one HALT instruction.

    • Reachability of HALT: The program must deterministically reach its single HALT instruction during simulation. Entering an infinite loop or terminating improperly results in failure.

    • Subroutine Structure: Every subroutine must be declared with an initial identifying label and must exit through exactly one RET instruction.

Labels and Offset Resolution

  • Function of Assembly Labels:

    • A label instructs the assembler to record and remember a specific memory address within the symbol table.

    • Labels act as target destinations for conditional branching (BR) and subroutine invocation (JSR).

    • Labels function like variable identifiers when performing memory access operations (LD, LDI, LEA, ST, STI).

    • The assembler calculates the exact relative offset between the incremented Program Counter (PC\text{PC}) and the target label address (PCoffset9\text{PCoffset9} or PCoffset11\text{PCoffset11}).

  • Prohibition of Numerical Offsets:

    • Specifying explicit numerical constants for relative offsets in PC-relative instructions is strictly prohibited.

    • Incorrect and Invalid Syntax:

    • LD R1, #5

    • BRN #-7

    • LEA R0, 5

    • JSR 100

    • Correct and Required Syntax (Using Labels):

    • LD R1, N48

    • BRN TOP

    • LEA R0, PROMPT

    • JSR GETNUM

Assembler Directives (Pseudo-Ops)

  • Definition: Directives provide control information to the assembler during translation and are not part of the physical LC-3 hardware instruction set.

  • Catalog of Directives:

    • .ORIG:

    • Specifies the starting memory address where the translated machine code is placed.

    • Example: .ORIG x3000 sets the initial address counter to x3000x3000.

    • .FILL:

    • Allocates a single 1616-bit memory location and initializes it with the specified value.

    • Example: A .FILL 65 initializes memory location A with the value 0041160041_{16} (651065_{10}).

    • .BLKW (Block of Words):

    • Allocates a contiguous block of memory consisting of nn consecutive 1616-bit words.

    • Reserves the space, initializing words to 0000160000_{16}.

    • Example: C .BLKW 5 reserves 55 consecutive words.

    • .STRINGZ:

    • Allocates contiguous memory words initialized with the ASCII values of a given character string.

    • Automatically appends an additional terminating null character (0000160000_{16}) at the end of the character array.

    • .END:

    • Signifies the physical end of the source program for the assembler.

    • Any text or instructions appearing after .END are ignored.

    • .SUB:

    • Declares a subroutine identifier to the assembler.

    • Must be placed at the top of the program, immediately beneath the .ORIG directive (e.g., .SUB MULT).

  • Memory Allocation Example (.FILL and .BLKW):

    • Assembly Source Code: text .orig x3000 LEA R6, C LD R0, A HALT A .fill 65 B .fill 66 C .BLKW 5 D .fill 68 .end     

    • Memory Layout and Machine Code Translation:

    • x3000x3000: EC04 (LEA R6, C)

    • x3001x3001: 2001 (LD R0, A)

    • x3002x3002: F025 (HALT)

    • x3003x3003 (Label A): 0041 (Stores decimal 6565)

    • x3004x3004 (Label B): 0042 (Stores decimal 6666)

    • x3005x3005 (Label C): 0000 (First reserved word of BLKW 5)

    • x3006x3006: 0000 (Second reserved word)

    • x3007x3007: 0000 (Third reserved word)

    • x3008x3008: 0000 (Fourth reserved word)

    • x3009x3009: 0000 (Fifth reserved word)

    • x300Ax300A (Label D): 0044 (Stores decimal 6868)

Memory mapping table showing memory addresses and hex contents for .FILL and .BLKW directives
  • String Allocation Example (.STRINGZ):

    • Assembly Source Code: text .ORIG x3000 LEA R0, HELLO PUTS HALT HELLO .STRINGZ "Hello, World!" A .fill 5     

    • Memory Layout and Machine Code Translation:

    • x3000x3000: E002 (LEA R0, HELLO)

    • x3001x3001: F022 (PUTS)

    • x3002x3002: F025 (HALT)

    • x3003x3003: 0048 (ASCII 'H')

    • x3004x3004: 0065 (ASCII 'e')

    • x3005x3005: 006C (ASCII 'l')

    • x3006x3006: 006C (ASCII 'l')

    • x3007x3007: 006F (ASCII 'o')

    • x3008x3008: 002C (ASCII ',')

    • x3009x3009: 0020 (ASCII Space)

    • x300Ax300A: 0057 (ASCII 'W')

    • x300Bx300B: 006F (ASCII 'o')

    • x300Cx300C: 0072 (ASCII 'r')

    • x300Dx300D: 006C (ASCII 'l')

    • x300Ex300E: 0064 (ASCII 'd')

    • x300Fx300F: 0021 (ASCII '!')

    • x3010x3010: 0000 (Null terminator added by .STRINGZ)

    • x3011x3011: 0005 (.FILL 5)

Assembly code and hex memory dump for .STRINGZ directive

System TRAP Routines vs. Assembly Mnemonics

  • TRAP Operations in LC-3:

    • TRAP instructions invoke operating system service routines handling hardware I/O and program lifecycle control.

    • Programmers must use the designated assembly mnemonics instead of direct TRAP x## opcodes.

  • Mandatory Trap Mnemonics:

    • Use GETC instead of TRAP x20 (Reads a single character from the keyboard into R0R_0; no character echo).

    • Use OUT instead of TRAP x21 (Outputs the character in R0[7:0]R_0[7:0] to the console display).

    • Use PUTS instead of TRAP x22 (Outputs a contiguous null-terminated string to the console, starting at the memory address held in R0R_0).

    • Use HALT instead of TRAP x25 (Halts program execution and prints a halt message to the console).

  • Explicitly Prohibited Trap Mnemonics:

    • IN (TRAP x23) must not be used.

    • PUTSP (TRAP x24) must not be used.

  • Grading Penalty:

    • Using TRAP x## in place of mnemonic keywords (GETC, OUT, PUTS, HALT) incurs an explicit penalty on quizzes and examinations.

The Two-Pass Assembler Process and Output Files

  • Mechanics of the Two-Pass Assembler:

    • Pass 1 (Address Calculation and Symbol Table Construction):

    • Scans the source file sequentially from .ORIG to .END.

    • Maintains an internal Location Counter (initialized to the address specified in .ORIG).

    • Determines the address for each instruction, accounting for words allocated by directives like .BLKW and .STRINGZ.

    • Identifies all labels and records their corresponding memory addresses in the Symbol Table file (.sym).

    • Pass 2 (Machine Code Generation):

    • Scans the source file a second time.

    • Translates symbolic opcodes and operands into binary/hexadecimal instructions.

    • Resolves relative addresses by subtracting the incremented instruction address from the target label address found in the Symbol Table.

    • Outputs the machine code file (.hex or .obj) and the human-readable listing file (.lst).

  • Symbol Table Example (assign5.sym):

    • Symbol names mapped to assigned 1616-bit addresses:

    • A: 3006163006_{16}

    • B: 3007163007_{16}

    • C: 3008163008_{16}

    • MULT: 3009163009_{16}

    • MTOP: 300D16300D_{16}

    • SAVE_R1: 3013163013_{16}

    • SAVE_R2: 3014163014_{16}

  • Assembly Listing File Structure (assign5.lst):

    • Displays the source line number, hexadecimal memory address, machine code bit pattern (1616-bit binary), and original source statement:

    • Line 2: (3000) 0011000000000000 -> .ORIG x3000

    • Line 3: (3000) 2005 0010000000000101 -> LD R0 A

    • Line 4: (3001) 2205 0010001000000101 -> LD R1 B

    • Line 5: (3002) 4806 0100100000000110 -> JSR MULT

    • Line 6: (3003) F021 1111000000100001 -> TRAP x21

    • Line 7: (3004) 3003 0011000000000011 -> ST R0 C

    • Line 8: (3005) F025 1111000000100101 -> TRAP x25

    • Line 9: (3006) 0008 0000000000001000 -> A .FILL x0008

    • Line 10: (3007) 0009 0000000000001001 -> B .FILL x0009

    • Line 11: (3008) 0000 0000000000000000 -> C .FILL x0000

    • Line 22: (3009) 3209 0011001000001001 -> MULT ST R1 SAVE_R1

    • Line 23: (300A) 3409 0011010000001001 -> ST R2 SAVE_R2

    • Line 26: (300B) 1420 0001010000100000 -> ADD R2 R0 #0

    • Line 27: (300C) 5020 0101000000100000 -> AND R0 R0 #0

    • Line 33: (300D) 1040 0001000001000000 -> MTOP ADD R0 R1 R0

    • Line 34: (300E) 14BF 0001010010111111 -> ADD R2 R2 #-1

    • Line 35: (300F) 03FD 0000001111111101 -> BRP MTOP

    • Line 38: (3010) 2202 0010001000000010 -> LD R1 SAVE_R1

    • Line 39: (3011) 2402 0010010000000010 -> LD R2 SAVE_R2

    • Line 40: (3012) C1C0 1100000111000000 -> RET

    • Line 51: (3013) 0000 0000000000000000 -> SAVE_R1 .FILL x0000

    • Line 52: (3014) 0000 0000000000000000 -> SAVE_R2 .FILL x0000

Subroutine Linkage, Link Registers, and Save Conventions

  • Subroutine Invocation Instructions:

    • JSR Label (Jump to Subroutine): Evaluates relative offset PCoffset11\text{PCoffset11}, stores the incremented Program Counter into link register R7R_7, and sets PC→Target Address\text{PC} \to \text{Target Address}.

    • JSRR BaseR (Jump to Subroutine Register): Stores the incremented Program Counter into R7R_7 and sets PC→BaseR\text{PC} \to \text{BaseR}.

    • Usage Constraint: JSR and JSRR must only be used when returning to the calling sequence is intended. For arbitrary jumps where return is not expected, use BRNZP or JMP.

    • Return Linkage: Every subroutine executes RET (JMP R7), restoring PC→R7\text{PC} \to R_7 to resume execution after the call site.

  • Passing Arguments and Return Values:

    • High-level languages frequently use the runtime stack in memory to pass parameters.

    • In baseline LC-3 assembly, parameters and return values are passed directly within general-purpose registers.

  • The Register Modification Problem:

    • Subroutines typically require intermediate registers to perform computations.

    • If a subroutine alters registers containing live variables required by the calling code, execution state becomes corrupted.

    • Caller: The code block that issues the JSR instruction.

    • Callee: The subroutine code invoked by JSR.

  • Callee-Save Convention:

    • The callee knows precisely which registers it uses for its local computations.

    • Therefore, the callee-save methodology is mandated: the subroutine itself must preserve and restore all registers it modifies.

    • Core Callee-Save Rules:

    1. Save upon Entry: The very first operations in a subroutine must store the values of all registers modified by the subroutine into dedicated memory locations using ST.

    2. Exception for Return Registers: Never save or restore registers explicitly used to pass return values back to the caller (restoring would overwrite the newly computed result with the stale original value).

    3. Restore before Return: The very last operations prior to RET must reload the original values using LD in reverse or matching order.

    4. Unique Labeling: Dedicated .FILL 0 memory locations used for register saving must have unique labels for each subroutine.

    • Caller-Save Exception: If the caller requires the value of a register that it knows will be overwritten by the callee's return output, the caller must preserve that register prior to calling the subroutine.

  • Handling Nested Subroutines and System Traps (The R7R_7 Overwrite Problem):

    • Calling another subroutine via JSR or invoking any TRAP instruction (e.g., OUT, PUTS, GETC) modifies link register R7R_7.

    • If a subroutine calls an inner subroutine or TRAP without preserving R7R_7, the outer subroutine's original return address is permanently lost, causing an infinite loop or crash upon executing RET.

    • Rule: Any subroutine that invokes another subroutine or TRAP must save R7R_7 at its start and restore R7R_7 immediately before RET.

  • Callee-Save Implementation Pattern:

  .orig x3000
  .sub TEST
  MAIN
      JSR TEST
      HALT

  TEST
      ST R1, T_SR1      ; Save R1
      ST R2, T_SR2      ; Save R2
      ST R7, T_SR7      ; Save R7 (Required because OUT modifies R7)

      ; Subroutine body
      OUT
      BRNZP T_END

  T_END
      LD R1, T_SR1      ; Restore R1
      LD R2, T_SR2      ; Restore R2
      LD R7, T_SR7      ; Restore R7
      RET               ; Return to caller

  T_SR1  .fill 0        ; Storage for R1
  T_SR2  .fill 0        ; Storage for R2
  T_SR7  .fill 0        ; Storage for R7
  .end
  ```


# Control Flow and Structured Programming Constructs

* **Conditional Branching (If-Else Pattern):**
  * High-level languages implement branching based on relational comparisons (e.g., `if (R1 > R2)`).
  * LC-3 requires evaluating conditions using two's complement subtraction: `Result=R1−R2\text{Result} = R_1 - R_2`.
  * To negate `R2R_2`, compute `NOT R3,R2\text{NOT } R_3, R_2` followed by `\text{ADD } R_3, R_3, #1`.
  * Add `R1R_1` and `R3R_3` (`ADD R3,R1,R3\text{ADD } R_3, R_1, R_3`), which updates condition codes (N,Z,PN, Z, P).

![High-Level Language vs Assembly If-Else implementation table](https://assets.knowt.com/pdf-flow-prod/2f6ebd6f-8ebb-4b92-b326-d52ecd292378-figures/1.png)

* **If-Else Assembly Code:**

text .orig x3000 NOT R3, R2 ADD R3, R3, #1 ADD R3, R1, R3 ; R3 = R1 - R2 BRNZ Block2 ; If R1 <= R2, branch to Block 2

BLOCK1 ; High-level: R1 = R1 - R2 NOT R2, R2 ADD R2, R2, #1 ADD R1, R2, R1 BRNZP IF_END_1 ; Skip Block 2

Block2 ; High-level: R1 = R2 - R1 NOT R1, R1 ADD R1, R1, #1 ADD R1, R2, R1

IF_END_1 HALT .end   ```

  • Loop Construction with Labels:

    • Iterative loops maintain a counter or test condition evaluated on each iteration.

    • Unlabeled offset loops (e.g., BRP #-3) are prohibited because code modifications alter line distances, breaking the branch target.

    • Correct Labeled Loop Pattern:text AND R0, R0, #0 AND R1, R1, #0 ADD R0, R0, #6 ; Initialize counter to 6 TOP ADD R1, R1, R0 ; Accumulate sum ADD R0, R0, #-1 ; Decrement counter BRP TOP ; Branch while counter > 0 HALT &nbsp;&nbsp;&nbsp;&nbsp;

  • Short-Circuit Evaluation of Boolean Logic:

    • Logical AND (A > B && A > C):

    • Involves positional, cascading checks. Every condition must pass; failure of any test skips immediately to the end.

    • Source Code:

      ; Assume R0 = A, R1 = B, R2 = C
      NOT R3, R0
      ADD R3, R3, #1    ; R3 = -A
      ADD R4, R3, R1    ; R4 = B - A
      BRZP ENDIF        ; If A <= B, condition fails; skip block
    
      ADD R4, R3, R2    ; R4 = C - A
      BRZP ENDIF        ; If A <= C, condition fails; skip block
    
      OUT               ; Executes only if both tests pass
      ENDIF
      HALT
    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;```
    * **Generalized Rule for Multiple ANDs:** For `if (test1 && test2 && ... && testn) { BLOCK }`, if any individual test fails, branch immediately to `END`. The block is placed after the final test.
    
    * **Logical OR (`A > B || A > C`):**
    * Passes if any test succeeds. Success jumps immediately to the executed code block.
    * Source Code (Pattern 1):
    

    text ; Assume R0 = A, R1 = B, R2 = C NOT R3, R0 ADD R3, R3, #1 ; R3 = -A ADD R4, R3, R1 ; R4 = B - A BRN PRINTA ; If A > B, success; execute block

    ADD R4, R3, R2 ; R4 = C - A BRN PRINTA ; If A > C, success; execute block BRNZP ENDIF ; Both failed; skip block

    PRINTA OUT ENDIF HALT       ```

    • Source Code (Pattern 2 - Negating the Terminal Test): text NOT R3, R0 ADD R3, R3, #1 ADD R4, R3, R1 BRN PRINTA ; Test 1 passes: execute block ADD R4, R3, R2 BRZP ENDIF ; Test 2 fails: skip block PRINTA OUT ENDIF HALT &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;

    • Generalized Rule for Multiple ORs: For if (test1 || test2 || ... || testn) { BLOCK }, if any test succeeds, branch immediately to CODE. After testn, place an unconditional jump (BRNZP END) to skip CODE if all tests fail.

    • Mixed Logic (test1 && test2 || test3):

    • If test1 fails, skip to evaluate test3.

    • If test2 fails, skip to evaluate test3.

    • If both test1 and test2 pass, jump unconditionally to execute CODE.

    • If reaching label T3, evaluate test3; if it fails, jump to ENDIF.

    • Execute CODE block and exit through label ENDIF.

Rules for Clearing Registers

  • When NOT to Clear a Register:

    • Before Overwriting Operations: Clearing a register immediately prior to an operation that completely overwrites it is redundant.

    • Before GETC: The character input is loaded directly into R0R_0, overwriting its previous contents.

    • Before LD, LDR, LDI: The memory load directly writes a 1616-bit word to the destination register, overwriting previous contents.

    • Before Non-Accumulative Additions: In statements like ADD R1, R2, #5 (R1=R2+5R_1 = R_2 + 5) or ADD R1, R2, R3 (R1=R2+R3R_1 = R_2 + R_3), the result overwrites R1R_1. Prior clearing of R1R_1 is redundant.

  • When Clearing a Register IS Required:

    • Accumulators in Iterative Loops: When summing values into a register (e.g., ADD R1, R1, R2 within a loop), the register must be initialized to zero before the loop starts:

    AND R1, R1, #0      ; Initialize accumulator to 0
    TOP
    ADD R1, R1, R2
    BRN TOP
    &nbsp;&nbsp;&nbsp;&nbsp;```
    * **Constructing Constants via Immediate Addition:** To place a known constant into a register using immediate addition, the register must be cleared first:
    

    text AND R0, R0, #0 ; Clear R0 ADD R0, R0, #10 ; R0 is now 10     ```

  • Diagnostic Criterion:

    • If an operation adds a register to itself (R0=R0+R1R_0 = R_0 + R_1), evaluate whether R0R_0 needs clearing based on whether it is an accumulator.

    • If an operation writes the sum of two distinct source registers into a destination (R0=R1+R2R_0 = R_1 + R_2), clearing R0R_0 is never necessary.

Debugging and Simulator Commands

  • Interactive Simulator Tools (lc3sim Command Interface):

    • step: Executes exactly one instruction. If the instruction is a subroutine call (JSR or JSRR), it enters into the subroutine code.

    • next: Executes exactly one instruction. If the instruction is JSR or JSRR, it runs the entire subroutine atomically and stops at the subsequent instruction in the calling code.

    • finish: If currently executing within a subroutine, runs the remaining subroutine instructions until it returns via RET, then pauses.

    • continue: Resumes continuous execution until the program reaches a breakpoint or HALT.

    • printregs: Displays the contents of all general-purpose registers (R0R_0 through R7R_7), the Program Counter (PC\text{PC}), and condition codes (N,Z,PN, Z, P).

    • dump: Displays memory addresses and their contents across specified memory ranges.

    • help: Prints the complete list of available simulator commands.

Grading Criteria and Common Coding Errors

  • Point Deduction Violations:

    • Redundant Register Clearing: Clearing a register prior to LD, GETC, or non-accumulating instructions.

    • Formatting Non-Compliance:

    • Placing labels anywhere other than the far-left margin.

    • Failing to indent instructions and directives with a single tab.

    • Separating operands with formatting other than a single space.

    • Adding a space before a comma, or omitting the space following a comma.

    • Missing Core Directives: Omitting .ORIG or HALT in an LC-3 program.

    • Malformed Subroutines: Omitting an entry label or omitting RET in a subroutine.

    • Unnecessary Arithmetic for Negative Constants: Allocating a positive constant using .FILL and then executing NOT/ADD #1 in code to negate it. Negative values must be stored directly in .FILL (e.g., .FILL -15).

    • Multiple HALT Instructions: Including more than one HALT in a program file.

    • Multiple RET Instructions: Including more than one RET within a single subroutine.

    • Using Numerical Offsets: Writing numerical offsets instead of assembly labels for PC-relative operations.

    • Excessive Immediate Additions: Using more than two consecutive ADD instructions to build a constant value. If a constant cannot be built in one or two additions, it must be loaded using .FILL and LD.

    • Redundant Constant Reloading: Re-executing LD instructions to fetch constants inside loop bodies instead of loading them once prior to loop entry.

    • Direct TRAP Calls: Using TRAP x## opcodes rather than designated assembly mnemonics (GETC, OUT, PUTS, HALT).

    • Excessively Complex or External Code: Implementing solutions using unintroduced syntax or copied internet code leads to zero credit.