Using the LC-3 Debugger

lc3db is a functional simulator/debugger for the LC-3 architecture. It provides a GUI using the GNU ddd debugger.

This is the First Edition of Using the LC-3 Debugger, 2004-04-19, for lc3db Version 0.3.0.


The goals of lc3db are to create a robust and complete simulator package for the LC-3 architecture. It differs from other LC-3 simulators in a few ways. It is structured more like a debugger than a simulator. This was done to leverage existing GUI interfaces. The current GUI mode is more or less as functional as the existing simulator GUI (and has a few additional features). However, like all new code, there could be some bugs. Feel free to report any behavior you find to be odd or things that you think are just plain bugs to Anthony Liguori.

Sample Session

The first thing you need to do is run the simulator. You will need to either be on a UT CS machine or using some sort of remote client capable of displaying X. Basically, the same requirements for the other LC-3 simulator.

You can run the simulator with the following command:

aliguori@bubble:~$ /p/bin/lc3db --ddd
This should bring up a Window that looks as shown below. If a window pops with the query
Options have changed on disk. Reload them?
then click Cancel.

lc3db Startup

Data Window

The very top portion of the screen is the data window. The GUI allows you to plot data graphs. This is an advanced feature that we will not cover in full except to say that this plot contains all of the machine state. It will automatically be updated every cycle and the data that changed will be highlighted.

Machine Code Window

The middle portion of the screen contains a disassembled version of the memory for about a thousand bytes after the PC. Some important things to note about this code:

If you want to inspect other areas of memory not visible, you can use the print or dump command (more below).

Command Window (GDB Console)

The command window is the very bottom portion of the screen. All aspects of lc3db can be accessed via this command interface. Before we discuss some of the important commands, we'll cover the three buttons that appear on the top of the window.


This command does what you'd expect it to. It boots the LittleOS operating system. If you are writing code that uses interrupts, it is very important that you do this before loading/running anything else. This command will also launch an additional IO console which is useful if you want the input/output for the simulator to be separate from the console.


The step command steps through a single instruction in the simulator. This means it will enter into JSR and TRAP instructions.


The next command will step through a single instruction in the simulator. It differs from the step command in that it will not enter into a JSR or TRAP instruction. This is very useful if you do not want to sift through all of the builtin TRAP routines.

You can see all of the builtin console commands by typing help into the console.

Loading/Executing Your Program

After you've compiled your program (see the section on Assembling for more info on this), you can load your program by using the File => Open Program menu (or the console). Using the File => Open Program menu would look something like:

Loading a file

You can then run your program using the step and next commands.


lc3db has a builtin in assembler. This section will discuss some of the differences in syntax of the assembly code and how to use it to compiler your programs. It is perfectly fine to use the assembler from the previous simulator to compile your programs. However, if you wish for it to show up in Open Program dialog, it must have execute privileges. Also, the symbol table format is not compatible between assemblers so you will lose all debugging information.

Invoking the Assembler

There is a special script installed on the UT CS network to make using the builtin compiler quite simple. Its usage is pretty straight forward. To compile filename.asm, use the following command:

aliguori@bubble:~$ /p/bin/lc3asm filename.asm
This will produce two files: filename.obj and filename.dbg.

Differences in Assembler

  1. x must be used for hex prefixes. 0x and $ are not valid.
  2. .FILL cannot be used for strings. Use .STRINGZ instead.

Running Locally

lc3db can be run locally on an Intel-based Linux machine. The only requirement is that GNU ddd is installed if you wish to use a GUI interface. GNU ddd is available from the GNU ddd website.

There is no support provided if you run lc3db locally. It's import to run the static version from the directory you have it installed in. For instance, if you install lc3db in a directory called lc3 in your home directory, it may look something like:

[anthony@rockhopper lc3]$ ./lc3db --ddd

Source: lc3db-1.0.tgz (32k)

LittleOS Overview

LittleOS is a basic operating system for the LC-3. It supports all of the standard traps as defined in P&P (except for PUTSP). The one thing that makes LittleOS unique is that it defines a boot sequence (including a boot interrupt). The following is the code for this boot sequence:

          .BLKW x80
          .FILL BANNER
          .FILL x0000
          .FILL x8000

          ADD R6, R6, -3
You'll notice this code simply loads R6 with the address of a supervisor stack (with some default entries on it), adjusts R6, and executes an RTI. This effectively boot straps the SSP register with a stack that will be used for the rest of the machines execution. You'll notice the stack is initially populated with a PC, PSR, and default USP. The reasons for this are discussed in the next section.

You can access the source code for LittleOS on the UT CS network under /p/share/lc3db/los/los.asm.

Interrupts in LittleOS

lc3db is the only LC-3 simulator (that I know of at least) that has a complete implementation of interrupts. However, the implementation is slightly different than described in P&P mainly to support pre-emptive multi-tasking. An interrupt is essential a function call that is called by the processor during normal execution. Just like a function call, a stack has to be used to be stored things in a particular way for interrupts to work correctly.

Generating an Interrupt

How interrupts are trigger will not be covered here. Instead, assume that for whatever reason, the processor has decided to take an interrupt. As programmers, we're interested in what information we're given in the interrupt handler itself.

When the interrupt begins, the only register with a known value is R6. This register will hold the top of the supervisor stack. The top three elements on this stack will also be known. The top of the stack will contain the PC before an interrupt occurred. The next element of the stack will be the value of R6 before the interrupt occurred. The third element will be the PSR before the interrupt. The interrupt handler is free to modify these values and, indeed will be forced to in implementing a timer interrupt.

The Timer Interrupt: interrupt x02

The timer interrupt relies on two special registers: the MCR and the MCC. The MCR is the machine control register and is located at address xFFFE. The values of MCR are as follows:

MCR[15] = clock enable
MCR[14] = timer interrupt enable
MCR[13:0] = cycle interval between timer interrupts
Additionally, the MCC, or machine cycle counter, is located at xFFFF. This value is incremented every time a cycle occurs. The timer interrupt is actually triggered when MCC[15:0] >= ZEXT(MCR[13:0]).

Anthony Liguori © 2004, All Rights Reserved