Monster is an all-in-one editor/assembler/debugger for the Commodore Vic-20. The design philosophy is uncompromising maximalism. This is in contrast to most existing Vic-20 assemblers (and most native development tools on 8-bit computers), which mostly designed with memory efficiency in mind. The primary reason for this was (or is) to leave the majority of the RAM available to the programmer.
Large RAM expansions have become ubiquitous on the platform, so the philosophy of this project is to choose feature richness without much concern for the memory footprint. Virtually any feature that I deem valuable in an editor/assembler is included.
Some of its features are:
- 40 column bitmap-based editor
- vi-like keybindings
- interactive visual debugger
- breakpoint editor
- memory viewer/editor
- file I/O (save/load)
- directory viewer
- symbol viewer
- auto-formatter and realtime syntax checking
- macro support
- user program/source/editor isolation
- many more...
For now Monster requires a Final Expansion to function. It could easily be modified for other 512k+ carts. Much of this RAM is used to store the multiple source code buffers (up to 8), but it is also used to store debug info and some code.
The banked memory allows the user program to execute in almost complete isolation. This means that, although this environment consumes a vast amount of memory itself, everything except address $9c02 (the bank select register) and a couply tiny interrupt handlers is preserved when control moves between the editor and the user program. Moreso even than small monitor cartridges, the program itself is virtually unaware of the resident tooling.
Building the source requires ca65
. The easiest way to install this is to
install the latest release of cc65. I've tested
with v2.18.
The build process also requires python3
(any version should do).
Monster can be built for two targets: disk and cartridge. The disk format is useful if you want to test on your Final Expansion 3 without erasing its firmware.
The cartridge is a better choice for emulators or power users on real hardware. It loads significantly faster
Clone this repo git clone https://github.com/gummyworm/monster.git
cd
to the directory you cloned to and run make
for the appropriate target
To build the disk version, run
make disk
To build the cart version, run
make cart
The Disk Makefile generates two PRG's: BOOT.PRG and MASM.PRG. You may write these to your disk of choice and load Monster as you would any other program on your Vic-20:
LOAD "BOOT.PRG",8,1
RUN
The Cart Makefile produces a single binary file, which is the cartridge image
for the Final Expansion 3. To flash this to your FE3 for use on real hardware, copy it to
your IEC storage device along with the installer (install.prg
).
LOAD "INSTALLER.PRG",8,1
RUN
This will flash Monster to the Final Expansion. Reset the computer to enter Monster.
If you wish to run Monster in an emulator (VICE), ensure that VICE is installed on your
machine and run make start-disk
or make start-cart
from the root of the project to attach the
corresponding disk or cartridge image.
The editor is a substantial part of this assembler. In addition to offering a high-density 40-column display, it has, by 8-bit editor standards, advanced navigation functionality.
Below are the basic commands along with their associated key combinations. These commands are available regardless of insertion mode (see the Editor Modes section below for more info on modes).
Key | Name | Description |
---|---|---|
C= + b | Set Breakpoint | sets a breakpoint at the current line |
C= + c | Refresh | refrehshes the screen by redrawing the source buffer |
C= + h | Help | displays the help menu |
C= + l | List | list directory, shows the files on the current disk |
C= + n | New buffer | creates a new source buffer and sets it as the active buffer |
C= + q | Close buffer | closes the current buffer and opens the next one that is open |
C= + v | MemView | enters the memory viewer/editor (press <- to exit) |
C= + y | Show Symbols | lists the symbol table for the assembled program |
C= + e | Next Error | if there are errors from the last assembly, navigatest to the next one |
F3 | Assemble | assembles the code in the buffer to memory |
F4 | Debug | assembles the code in the buffer to memory with debug info |
F5 | Show buffers | displays a list of the currently open buffers |
C= + + | Next Drive | Selects the next drive (limited to #15) |
C= + - | Prev Drive | Selects the previous drive (limited to #8) |
: | Ex Command | Accepts a command + argument(s) and executes the command |
The following commands are entered at the "Ex Command" prompt (accessed with the :
key).
Most accept an argument (as described in each commands description below)
Key | Name | Args | Description |
---|---|---|---|
a | Assemble File | Filename | assembles the given filename |
B | export Binary | Filename | exports the active assembly to a binary file (no .PRG header) |
d | Start Debugger | Symbol to debug at (optional) | begins debugging at the given label |
D | Disassemble | Start address, End address | Disassembles the given address range |
e | Edit | Filename | loads the buffer with the contents of the given file |
g | Goto | Symbol to run at (optional) | executes the program at the address of the given symbol |
F | Disassemble File | Filename | disassembles the given file to a new source buffer |
P | export .PRG | Filename | exports the active assembly to a .PRG file |
r | Rename | Name | renames the buffer to the given name |
s | Save | Filename | saves the buffer to the given filename |
S | Save All | N/A | saves all modified buffers that are open currently |
x | Scratch | Filename | scratches (deletes) the given filename |
Assembles the contents of the given file. This is functionally the same as opening the given file and assembling it with debug information (F4).
Invoking the debugger will invoke it for the last assembled file (not the current source buffer) in this scenario. The debugger cares about the active debug information not the active file.
Example:
:a HELLO.S
Exports the active assembly (F3/F4) to the given file as binary. This means no load address is prepended to the file. This can be useful if you are using Monster to create level data or other code loaded by your main program. It can also be used to export things like data tables for use with .INCBIN
Begins debugging at the given symbol using the active debug information.
If no symbol is given, the program will begin and the debugger invoked at the lowest defined origin (.ORG) in the program. See Debugger for more details on debugging.
Example:
:d START
Disassembles the contents of the virtual memory between the given range.
e.g. :D $1001, $1040
.
Expressions may be used in addtion to literal addresses when defining the disassembly range.
This could be useful, for example, if your program generates code and you want to view the results.
The result of the disassembly is opened in a new buffer, where you can edit it as you would any of your handwritten source.
Example:
:D PROC, PEND
Loads the given filename to a new buffer and activates it.
Example:
:e HELLO.S
Disassembles the contents of the given binary file.
e.g. :F EXAMPLE.PRG
The result of the disassembly is opened in a new buffer, where you can edit it as you would any of your handwritten source.
Exports the active assembly (F3/F4) to the given file as a .PRG file. This means a load address is prepended to the file prior to export. This produces a standalone executable you can use when you are done working on your program.
Renames the active buffer to the given name.
Example:
:r TEST2.S
Saves the active buffer to a file with the given name. If no name is given, the active buffer's name is used.
Adding an @
to this command (s@
) will delete the file before saving. This
allows you to overwrite the existing file if it exists.
Examples:
:s NEW.S
:s@ OLD.S
Saves all buffers that have been modified since they were last saved.
As with the Save command, adding @
to the command (S@
) will overwrite
existing files if they exist.
Example:
:S@
Deletes the file of the given name.
Example:
:x TEST.S
The editor is a modal editor, that is, it behaves differently depending on which mode it is
in. The modes are all accessed from the default mode (called COMMAND mode) and each mode returns
to COMMAND mode when the <-
key is pressed. Below is a list of the modes along with
the key that enters that mode, and the editor behavior while in that mode.
Command mode is the default mode. The primary function of command mode is to navigate around the
source code and to enter other modes.
Navigation behaves similar to vi
and many basic vi
commands are supported.
The following keys are handled in COMMAND mode.
Key | Name | Description |
---|---|---|
HOME | Home | moves the cursor to column 0 |
C= + m | Goto line | prompts for a line number and moves the cursor to that line |
C= + [1-8] | Goto Buffer | opens the buffer corresponding to the number key that is pressed |
C= + < | Prev Buffer | opens the buffer before the active one (if there is one) |
C= + > | Next Buffer | opens the buffer after the active one (if there is one) |
C= + i | Jump up | jumps forward to the next source position that was "jumped" to |
C= + o | Jump back | jumps back to the last source position that was "jumped" to |
$ | End of Line | moves the cursor to the end of the current line |
;; | Banner | inserts a banner (full line of semicolons) below the cursor |
gg | Top of File | moves the cursor to the first character in the file |
gd | Goto Def | if the cursor is on a label reference, navigates to that label |
G | End of File | moves the cursor to the last line in the file |
h | Left | moves the cursor left |
j | Down | moves the cursor down |
k | Up | moves the cursor up |
l | Right | moves the cursor right |
H | Home | moves the cursor to the top left of the screen |
L | Last | moves the cursor to the bottom left of the screen |
dw | Delete Word | deletes the next word |
dd | Delete Line | deletes the next line |
J | Join lines | moves the contents of the next line to the end of the current one |
0 | Column 0 | moves the cursor to the first column of the current line |
a | append char | enters insert mode and moves to the next character |
A | append line | enters insert mode and moves to the last character in the current line |
o | open line | opens a new line below the cursror and moves to it |
O | open line ^ | opens a new line above the cursor and moves to it |
p | paste below | pastes the contents of the copy-buffer to the line below the cursor |
P | paste above | pastes the contents of the copy-buffer to the line above the cursor |
I | Insert line | enters insert mode and moves to the first character in the current line |
[ | Prev Block | moves to the previous empty line or start of file if there isn't one |
] | Next Block | moves to the next empty line or end of file if there isn't one |
Entering insert mode allows the user to enter text at the cursor location. Keystrokes are interpreted as their corresponding ASCII character value in this mode, so there are no special commands accessed via them.
In VISUAL mode (accessed via v
in COMMAND mode), the user can select
a block of text which may then be deleted or copied. Below is the table of supported commands
while in visual mode. The <-
key will return the user to to COMMAND mode.
Key | Name | Description |
---|---|---|
d | delete | deletes the selected text and copies it to the copy buffer |
y | yank | copies the selected text (in VISUAL mode) to the copy buffer |
VISUAL LINE, which is entered with the SHIFT - v
key combination from COMMAND mode is similar to VISUAL mode,
but selections include only entire lines. Upon entering VISUAL LINE mode, the current row is selected.
Navigating to rows above or below will select additional lines. The delete and yank keys behave the same as they do
in VISUAL mode.
When text is deleted (delete line, delete word) or yanked, it is stored to a buffer where
it may be recalled by the paste commands (p
, paste below and P
paste above).
When the paste command is executed, the buffer is cleared.
The copy buffer is $1e00 bytes, which is enough for ~3.5 completely full screens of text (22 rows and 40 columns).
When the user "jumps" to a different position in the source (gg
, G
, goto line
,
find
, [
, and ]
) the editor saves the old position. To recall the positions
that were "jumped" from are two commands: jump-forward (C= + i
) and jump-backward (C= + o
).
Lines are checked and formatted according to their contents each time they are completed (RETURN is pressed). While this should reduce the number of errors you encounter when assembling, it does not guarantee it. The following assumptions are made when checking the syntax of a line: - macros are defined - labels may not be defined - origin may not be set This means that lines using undefined labels are treated as valid. If the label does not exist at assembly time, of course this will result in an error. Macros, however, are expected to be defined.
Although labels aren't required to be defined, they are internally tracked while editing. Because their addresses aren't valid til assembly, you cannot access them (e.g. in the symbol viewer) until then.
The assembler syntax is very similar to any other major assembler. For basic instructions, the canonical 6502 assembly syntax is supported. That means '$' denotes a hex value, '#' and immediate operand, parentheses an indirect address, etc.
Operands, in addition to basic values and labels, may contain an expression, which is evaluated at assemble-time to a value.
The supproted operators are: '+', '-', '*', and '/'. Multiplication and division have higher precedence than addition and subtraction.
Expressions may also contain parentheses, which are evaluated as you would expect, but note that if the entire expression is enclosed in parentheses, the assembler will interpret this as indirect addressing. For example:
JMP (1+3) ; jump-indirect to the address in memory address (4)
JMP 1+3 ; jump-absolute to address 4
Immediate addressing and indirect addressing are mutually exclusive, so the assembler
will allow you to enclose the whole expression in parentheses for immediate expressions
prefixed with a '#' (e.g. LDA #(2+4)
)
Labels are supported in expressions and will evaluate to their address when assembled.
LDA #<LABEL1
Hexadecimal and decimal numbers are supported. Hexadecimal numbers must be prefixed with a '$'.
LDA #(10+$20)
Character literals are also supported. These are represented as a character enquoted within
single quotes.
LDA #'x'
Character literals must contain exactly one character and always resolve to
a 1 byte value.
Spacing is not important, but instructions are auto-formatted so that they are TAB indented. Labels and directives are, by convention, not indented. The formatter will also take care of this.
Labels begin with either an alpha-character or, in the case of local labels, a '@' character. They are limited to 16 characters, but it is advisable to keep them shorter (8 characters or less). Long labels are harder to squeeze onto a line.
Labels are case-insensitive (a
and A
refer to the same label).
Local labels are defined by prefixing the label with a '@' symbol. This does count toward the 16 character label limit. Local labels are valid until the next non-local label is defined as shown in the following example.
PROC0:
@L0:
DEX
BNE L0
RTS
PROC1:
@L0:
DEY
BNE L0
RTS
Note that the scope of the @L0
defined under PROC0
is valid until the next
non-local label (PROC1
) at which point the name is recylced and may be used
again.
Because of the way local labels are implemented they are not totally inaccessible. They can be accessed by prepending the global label that encapsulates them. This can be used to emulate structural data types e.g.
PLAYER
@X: .db 0
@Y: .db 0
GAME:
LDA PLAYER@X
Anonymous labels can be declared with ':'. Anonymous labels are useful when you need to do a short branch where a descriptive label name isn't necessary.
A colon followed by a + or - character is used to reference these labels. Pluses (+) refers to the next forward anonymous label and minuses (-) refer to the previous backward anonymous label.
for example
.ORG $1000
: JMP :+ ; JMP $1003
: JMP :- ; JMP $1003
: JMP :-- ; JMP $1003
Using multiple +'s or -'s will count the same number of references before landing on the corresponding anonymous label. for example:
JMP :+++
: nop
: nop
: nop ; will jump here
Directives begin with a .
character and instead of being directly assembled,
as with an instruction, tell the assembler to generate some special code or data
based on the operands.
Some directives (.MAC
and .REP
) generate a variable amount of code or data based on the value
of their operands.
For these directives, the expressions used as arguments must be resolvable
in pass 1 of the assembler. This means any labels used in the expression
must be declared before the directive.
The following example illustrates why this is necessary:
.REP NUM, I
ASL
.ENDREP
.EQU NUM 5
Note that NUM
is not declared until after the .REP
directive. Because of this
the assembler does not know how many times to repeat the ASL
. We could assume
the label is an arbitrary 16-bit value as we do with labels that are undefined
in pass 1, but any subsequent labels would have the wrong address if we guessed
any number other than 5.
Defines a sequence of bytes from the comma-separated list that follows.
Examples:
code | generated binary |
---|---|
.DB $00, $01, $02 | $00 $01 $02 |
.DB "HI",0 | $48 $49 $00 |
deines a sequence of words from the comma-separated list that |
Examples:
code | generated binary |
---|---|
.DW $00, $01, $02 | $00 $00 $01 $00 $02 $00 |
Ends a .IF block
See .IF
Defines a constant which may be used in expressions
.EQU BITMAP $1100
LDA #$00
STA BITMAP+20
Evaluates the expression
Conditionally assembles the lines between this directive and its matching
.ENDIF
.
.IF NTSC
.EQU CYCLES_PER_LINE 65
.EQU LINES 261
.ELSE
.EQU CYCLES_PER_LINE 71
.EQU LINES 312
.ENDIF
Evaluates to TRUE if label is defined. This is different from .IF because label may be defined to be 0 and this will still evaluate to TRUE. This can be useful inside macros to determine if a paramter was provided or not.
Includes a file at the line of the directive. The file is loaded line-by-line from disk and assembled as if the code was copy/pasted in place of the include directive.
.INC "KERNAL.INC"
LDA #$00
JSR CHROUT
Includes the binary file. The binary contents are stored at the current location of the assembly target when this directive is encountered
.EQ BITMAP $1100
LDX #$07
L0:
LDA SPRITES,X
STA BITMAP,X
DEX
BPL L0
SPRITES:
.INCBIN "SPRITES.BIN"
Defines a macro
.MAC LDXY VAL
LDX #<VAL
LDY #>VAL
.ENDMAC
LDXY $1234
Will generate the following code:
LDX #$34
LDY #$12
Macro definitions begin with the .MAC
directive followed by the name of the
macro and a comma-separated list of the parameters for the macro.
Macros are invoked with the name of the macro followed by a comma-separated list of the parameters.
Sets the address to assemble code to
.ORG $1000
; start up code
.ORG $2000
; main code
Sets the address the code will run at when executed. This is useful for code that will be relocated prior to execution.
.ORG $1000
.RORG $00
; some tight loop
LDA #$01
STA *+3
LDA #$00
STA $900F
Note that the .RORG
directive must follow the .ORG
directive in order to
avoid the virtual PC being overwritten.
.ORG
will set the virtual PC to the same location as the physical PC.
Assembles the code between this directive and .ENDREP
for the given number of
times.
.REP 3
ASL
.ENDREP
Becomes
ASL
ASL
ASL
An optional parameter can be given that will be assigned the value of the current iteration of repetition during assembly.
.REP 5,I
INC $F0+I
.ENDREP
Becomes
INC $F0
INC $F1
INC $F2
INC $F3
INC $F4
Macros offer a conveinient way to abstract patterns that you find yourself frequently writing.
They may be recursive as in this example:
.MAC LDXY VAL
LDX VAL
LDY VAL+1
.ENDMAC
.MAC STXY ADDR
STX ADDR
STY ADDR+1
.ENDMAC
.MAC SET DST, SRC
LDXY SRC
STXY DST
.ENDMAC
You may omit arguments to a macro if your macro knows how to deal with less than the maximum number it expects as in this example:
.MAC SAVEBYTES A, B, C
.IFDEF A
LDA A
PHA
.ENDIF
.IFDEF B
LDA B
PHA
.ENDIF
.IFDEF C
LDA C
PHA
.ENDIF
.ENDMAC
There are some limitations on the number of macros and overall size of the macros per assembly. The source for all macros must be less than $1400 bytes. There is also a 128 macro limit.
Each macro can be at most 16 lines or 512 bytes, whichever is lower. This restriction also applies to .REP.
Comments are excluded from the internal context buffer, so using them will not count toward the byte limit.
Although Monster strives to feel like unbothered by the physical limits of the Vic-20, certain usage patterns can cause issues. By following these practices, you should have a smooth experience without running into some of the limitations that you may hit if you are unconcious about them.
When possible, try to stick to 8 or fewer files for your project. This will allow all of them to be stored in memory during assembly, which will greatly speed up your edit/debug loop. When debugging, this practice will also allow all files to be available in RAM when you are stepping through the program, which will be a much smoother experience than programs that continuously must page to disk.
Anonymous labels take up no space for the label names, only address. Using them is much more efficient than labels, and so this should be done for short branches that don't require much description. Using too many labels, in the extreme case, can push your program over the symbol limit.
You will be warned when transferring control out of the debugger, so try to heed this. If the CPU encounters a JAM instruction there is no way to recover. When stepping or tracing, the debugger will take care to ensure that no JAMs are executed.
For things that cannot be traced, like cycle-based effects, you will need to give full control to the user program to get rid of the the overhead tracing introuduces. In these cases, you can press RESTORE to return to the debugger (assuming the user program has not overwritten the NMI vector at $0318 or disabled NMIs).
In the event of a JAM or an unreoverable state (NMI disabled or vector overwritten), you will need to reset the machine.
Here is a basic hello world program to demonstrate some of the assembler's features
.ORG $1400
MSG:
.DB "HELLO WORLD!",0
START:
JSR $E5B5
LDX #$00
LDA #' '
CLR:
STA $1000,X
STA $1100,X
DEX
BNE CLR
DISP:
LDA MSG,X
BEQ DONE
JSR $FFD2
INX
BNE DISP
DONE:
JMP DONE
Monster holds the active source file in memory (for editing), but assembles all included files directly from file. Files are stored with $0d line endings, but if you save your file with UNIX style line-endings, they will be automatically converted when the file is read in.
As with any work done with Commodore disk I/O, it is wise to regularly back up your files
Selects the next or previous drive (within the valid range of 8-15).
C= + + (plus)
selects the next available drive and C= + - (minus)
selects
the previous available drive.
The directory viewer offers a paginated view of all files on the disk.
Pressing RETURN
while the row of the desired file is highlighted will load
that file into a new buffer and activate that buffer.
The symbol viewer, activated with C= + Y
, displays all the labels in the program
along with their corresponding address.
The up/down cursor keys navigate between pages of symbols. The back-arrow
key returns to the debugger.
debugger.mov
The debugger allows you to step through code, set breakpoints, and watch data as you execute your program.
Upon entering the debugger, a view of the system state is displayed at the current step or breakpoint. This include the state of the registers (A, X, Y, P, SP, and PC) as well as any effective address that was calculated for reading/writing by the last instruction.
While debugging, most navigation commands work as normal. Breakpoints may be set as they would in the editor prior to assembly, and they will be installed in realtime. Other edits are not allowed, however, while the debugger is active.
Both the debugger and the user program's RAM is saved/restored when control transfers between the two. That is the screen data ($1000-$2000), the zeropage, and color RAM.
The following commands are supported by the debugger and are accessed by their respective Key in the table below.
Key | Name | Description |
---|---|---|
F1 | Source View | maximizes the screen area for viewing the source code |
F2 | Register Editor | enters the register editor |
F3 | Mem View | activates the memory window, which takes control until <- is pressed |
F5 | Break View | displays the breakpoints that have been set and allows them to be enabled/disabled |
s | Step Over | steps to the next instruction. If it is a JSR, continues AFTER the target subroutine |
y | Step Out | steps until the next RTS instruction |
z | Step | steps to the next instruction. |
t | Trace | like GO but the debugger takes control between each instruction |
C= + g | Go | begins execution at the cursor |
C= + j | Jump to | sets the PC to the address corresponding to the line the cursor is on |
C= + r | Reset Stopwatch | resets the value of the stopwatch to 0 |
C= + t | Enter monitor | enters the text-based debug interface (see the monitor commands section for more info |
C= + x | Quit Debugger | Prompts the user for confirmation then quits the debugger upon receiving it |
<- | Exit | exits the debugger and returns to the editor |
SPACE | Swap prog | swaps in the internal memory for the user program (allows user to see screen state) |
^ (up arrow) | Goto Break | navigates to the address that the debugger is currently paused at |
Pressing F2 moves the cursor to the register contents and allows the user to enter
new values for them. Pressing RETURN
will confirm the new register values
and update them to those values the next time the user program is invoked.
Pressing <-
will abort this process and leave the old register values
intact.
Next to the registers, under the CLK label, is a 24-bit counter that displays the
number of cycles executed by the instructions that have been STEP'd into.
The stopwatch can be reset to 0 with the C= + r
key combination.
Note that the number of cylces is displayed in decimal unlike the rest of the information in the debug view, which is displayed in hexadecimal.
Stepping is a common way to debug a program line-by-line. Stepping into code will, if possible, return to the debugger after the next instruction (the one currently highlighted if we have debug information) is executed. There is a scenario where this is not possible: if the next instruction is in ROM. In this case, step into behaves the same as step over: execution begins after the current instruction. Note that because we don't know what will happen in ROM, it is possible execution will never return to the debugger.
Keeping in mind the aforementioned caveat with ROM, stepping into code gives us a lot of information about the instructions we are executed. The debugger behaves almost as a 6502 simulator in this scenario. When an instruction that affects a given register, that register is highlighted even if the register value hasn't changed. The same is true of watches. We can activate a watch even if we don't store a new value to it. In fact, we can activate them when a value is loaded.
Step over behaves the same as step into, but if the next
instruction is a subroutine call (JSR
), execution continues until the
instruction after the JSR
(after the subroutine returns).
Step out is different from the other STEP commands in that it runs by tracing. The step out command traces the program until the next RTS instruction. The RTS instruction is run and control then returns to the debug session.
The go command begins execution and returns to the debugger only when a breakpoint is encountered or when RUN/STOP is pressed.
Trace is similar to GO
, but the debugger executes the program as a series
of STEPs instead of running the program binary directly.
This is useful because it allows the debugger to break if any watched memory
location is accessed or if a JAM would occur.
While the debugger and user program have isolated memory banks in the address space
above $1fff
, the RAM below this, [$00, $2000)
, is internal to the Vic-20
and cannot be swapped out between debug steps. The debugger has no choice but to
share this address space with the user program as it is also the only RAM
that is visible to the video chip (the VIC-I). It also contains the stack and
zeropage, which we could avoid, but as long as we need to use the $10th-$20th pages
of RAM, it makes sense to handle them in the same way.
To handle this, the debugger calculates the bytes that need to be saved in
between steps and saves these values in between calls to the program. Values
that will be used by the user program are then swapped in so that the program
behaves as if the debugger is not running.
The full internal state of the user program and debugger occupy buffers in the
debugger and are available to be swapped in/out on command with the C= + SPACE
key combination. This is useful if you'd like to see what the internal RAM
state, which is the only place that the screen state may live, looks like
at the current step in the program.
If we aren't stepping into code in RAM (go, step over) we are unable to calculate the addresses that will be affected when we hand over control to the user program, we instead save the entire debugger state of the internal RAM and restore the entire user state. Although this is a rather large amount of memory, it is mitigated by being handled by a mostly unrolled loop and therefore takes only a fraction of a second to occur. Nonetheless, it is apparent when this is happening if you've changed the setup of the VIC registers, or anything in the VIC's visible address range, as the screen will briefly flash with the state of the user program.
Within the debugger, there are 3 auxiliary views that may be activated with the function keys. Each shows information about the machine or debug state. Each viewer also contains an editor, which is activated with the keys enumerated below next to their corresponding editor.
Pressing the <-
key will return the user from the auxiliary editor to the
source code editor. And F1
will hide the active view to maximize the
source editor's screen size.
The memory viewer displays the contents of RAM at a given address. The memory viewer is updated upon reentry to the debugger (if active). Memory values may be updated by navigating to the value the user wishes to change and overwriting it with a new hex value. The change occurs immediately.
In addition to hexadecimal keys to edit memory values, the following commands are supported within the memory viewer:
Shortcut | Name | Description |
---|---|---|
C= + w | Add watch | Add watch to the highlighted address |
/ | Find Value | Seeks from current memory address for given value |
<- | Exit | Returns to the debugger |
^ (up-arrow) | Set Addr | Sets the viewer's address to the given value |
Watches may be placed while navigating in the memory editor. This is done
by pressing the C= + w
key-combination while the cursor is on the desired
byte to watch. See the Watch Viewer section for more information on
watches.
Prompts the user for an 8 or 16 bit value (determined by the number of characters provided) and looks for that value in memory. If it is found, the memory view is updated to begin at the first address that was found containing the specified value.
Note that when seeking for a 16 bit value, the value is searched in little-endian
format. If the input for the search is given as $1234
the result will be
the first occurrence of the byte value $34
followed by $12
.
Moves the cursor to the address of the viewer, then prompts the user for a new
value to set the memory viewer to. Pressing RETURN
confirms the new address
and <-
cancels and returns the user to the editor without changing the address
The breakpoint viewer displays all the breakpoints that have been set by the user. A circle is displayed next to those that are currently active. The user simply navigates the list with the cursor keys and presses RETURN to toggle those which he/she wishes to enable/disable.
Note that breakpoints correspond to the debug information generated with the F4 command. If the line numbers change after this information is generated, breakpoints are unlikely to behave in expected ways.
The watch viewer displays all watches that have been set in the memory viewer. The current value of a watch is shown along with its previous value (if it has changed since the debugger last took over).
A watched address (or range) will also be prefixed with a '!' if it was modified during the trace or step. This is especially important for knowing that a range was modified as ranges do not list the previous or current values for the watch.
The following keys are supported within the watch viewer:
Shortcut | Name | Description |
---|---|---|
C= + w | Add watch | Prompt the user for expressions to watch |
RETURN | Select/Edit | Enters the memory editor at the watch's address |
<- | Exit | Returns to the debugger |
While in the watch editor, the C= + w
key combination prompts the user for an
address or address range to watch. These are given as expressions, so you may
provide, for example myval+3
to set a watch at the address of the label myval plus 3.
To set a watch for an address range, simply provide two expressions, separated by a comma,
at the prompt. If the expression(s) are invalid, no watch is added.
Pressing RETURN will invoke the memory editor at the location of the watch that was selected. Returning from the memory editor will return the user back to the watch editor.
Breakpoints may be set/removed during both normal editing and while debugging. Setting a breakpoint inserts a special character into the source buffer, which tells the assembler to generate a breakpoint for the line that this character resides on.
Because the breakpoint is represented as a character within the source code itself, it will automatically move as lines are inserted and deleted. The character itself is not editable (the cursor will not move to breakpoint characters). You may remove it by toggling the breakpoint off or by deleting the entire line.
NOTE: Debug information is only generated for instructions NOT data. This means
that, for example, you can set a breakpoint on LDA #$00
or a macro that expands
to such an instruction, but setting one on .DB $00
has no effect.
During normal editing, breakpoints may be set and removed with the C= + b
key combination.
A breakpoint symbol (a filled circle) is placed at the beginning of a line to
indicate that a breakpoint has been added.
Pressing the same key combination (C= + b
) will also remove a breakpoint
if it is pressed while on a line that already has one.
Watches are set within the memory editor (F3
). When the cursor is over the
desired byte to watch, the press C= + w
to add a watch to the address of the
byte under the cursor. A beep will confirm that the watch
was added.
The watch editor (F7
) shows all active watches. This window displays the old
value of a watch and what it was changed to when it is updated.
When a value is changed the watch view is activated to alert the user to the alteration. If a read or write is detected while stepping into the code, the viewer is also activated.
The monitor is a text based interface for debugging programs and manipulating program state.
Command | Name | Parameters | Description |
---|---|---|---|
a | assemble | expression instruction | assembles the given instruction at the address of the provided expression |
d | disassemble | expression (expression) | disassembles from the given start address or, if an end address is given, up to the the given end address. |
m | show memory | expression (expression) | displays the contents of memory from the given start address or, if given, up to the given end address |
z | step | runs the next instrcution and returns to the the monitor prompt | |
s | step over | runs the next instruction, treating JSR's as a single instruction | |
wa | add watch | expression (expression) | adds a watch at the given start and (optional) stop address |
wr | remove watch | id | removes the watch with the given id |
F1 | view screen | N/A | toggles the view of the user-memory (swaps the the $1000-$2000 range monitor <-> program |