AArch64 Playground
4.20 · Debugging in the playground

Debugging in the playground

A program that assembles and runs can still print the wrong answer, and reading the source again rarely shows why. A debugger lets you stop a program at any line and look at every register and every byte of its memory, so you can see the first place a value goes wrong. The playground has one built in. This lesson uses it to find the bug in one short program.

A program with a bug

The program below should average five quiz scores: 72, 85, 90, 64 and 88. They add up to 399, and 399 / 5 is 79 once the fraction is dropped. It prints this instead:

sum = 404, average = 80
loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 1try it: run it, or step one instruction at a timeOpen in playground

The sum is 5 too high. The rest of this lesson tracks down where the extra 5 comes from.

The editor above has run, step, back and reset. The watches, the stack and the memory view are in the full playground. Open the program there in a new tab, so this page stays open beside it: Ctrl+click the Open in playground link under the editor (Cmd+click on a Mac), or on a phone, press and hold the link and choose to open it in a new tab. A plain click opens the playground in place of this page. On a phone, the playground keeps those views under more.

Step, back, run, and reset

The editor above has four buttons:

  • run starts the program, or carries on from where it stopped, until it ends, waits for input, or reaches a breakpoint. On the first press, after the program finishes, and after you change the source, run first assembles the program: it turns the source into machine code and loads it. So a press always runs the code you see.
  • step runs one instruction, assembling first in the same cases. The highlighted line is the one that runs next, not the one that just ran.
  • back undoes the last step, registers and memory included, so you can step past a line and come back to it.
  • reset starts the program over and keeps your breakpoints.

The full playground has the same buttons, an assemble button, and a key for each one. The keys work only there; inside a lesson, use the buttons.

  • assemble (F6) assembles the program without running it, so you can step from the start.
  • run (F5) assembles first when nothing is loaded, the program has finished, or the source, files or arguments changed since the last assemble. While a run is going, the same button pauses it.
  • step (F10) runs one instruction. It never assembles, so press assemble first.
  • back (Shift+F10) and reset (Shift+F5) work as they do above.

A run that goes 1,000,000 instructions without finishing pauses and says so. Press run to carry on, or look for a loop whose exit test never comes true.

The reference's directives and debugger tab lists these keys, the views, and the watch forms on one page.

Breakpoints

Stepping through five passes of a loop one instruction at a time is slow. A breakpoint is a mark on a line that makes run stop just before that line runs. To set one, click in the narrow strip to the left of the line number, or tap it on a touch screen. Click again to clear it.

Set a breakpoint on add sum_r, sum_r, w9, the line that adds a score to the sum, and press run. The program stops with that line highlighted, and the registers panel shows the score just loaded in x9: 72, which is 0x48 in hex. The panel marks the registers the last instruction changed, and its dec and hex buttons switch how the values are written. Press run again, and the program stops on the next pass with 85.

Watches

On every stop you can hunt for the same few values in the registers panel, or let the watches tab show them for you. Type an expression in its box and press add. These are the forms it understands:

expressionwhat it shows
w19the register w19; any x or w register, sp, fp or lr works the same way
[fp, 16]8 bytes of memory at the address fp + 16
[fp, scores_s]the same, with the offset named by a scores_s = 16 line
scores_s[1]8 bytes at fp + scores_s + 8, the 8-byte slot after scores_s
*x218 bytes of memory at the address held in x21

Watches show their values in hex. They know register names, the names from = lines, and a .data label with an index after it, but not the m4 names from define, so watch w19, not i_r.

Keep the breakpoint on the add line, add the watches w19 (the index i) and w9 (the score just loaded), and press run until the program finishes. Read in decimal, the stops show i and the score as 0 and 72, 1 and 85, 2 and 90, 3 and 64, and 4 and 88. Then comes a sixth stop that should not happen: i is 5 and w9 holds 5.

note

Each memory form in the table reads 8 bytes, so a watch on one word of an array of words shows that word and the one after it. At the first stop, [fp, scores_s] shows 0x0000005500000048: 85 (0x55) in the high half and 72 (0x48) in the low half. The machine is little-endian, so the word at the lower address, 72, fills the low half.

The stack and the memory view

The stack tab lists 16 rows of 8 bytes, starting at sp. It marks the row that fp points at and names each row above it by its offset from fp, or by the = line's name when one matches, such as [fp, scores_s]. On a narrow screen the column of names is hidden, and the fp mark with it. In this program fp equals sp, so count down from the top row, 8 bytes a row: [fp, 32] is the fifth row. At the sixth stop, the row [fp, 32] reads 0x0000000500000058: 88, the fifth score, in its low half, and 5 in its high half. That 5 is the count the program stored at count_s, right after the scores. The sixth pass read the count as if it were a sixth score.

The memory tab shows the raw bytes, 16 to a row (8 on a narrow screen), with the matching characters on the right. Its section list jumps to .text, .rodata, .data, .bss or the stack, and its address box takes an address in hex, such as 0x7fffffe0, or in decimal. The bytes the last step wrote are marked.

The fix

The loop should run for i from 0 to 4, but b.le sends it round once more when i is 5. With b.lt, the test fails as soon as i reaches 5. The fixed program prints:

sum = 399, average = 79
loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 2try it: run it, or step one instruction at a timeOpen in playground

The diagnostic bundle

Sometimes a program does something you cannot explain and you want to ask someone about it. The diagnostic bundle button, in the full playground's tools (in the menu on a phone), packs the program, its output, the registers, the flags, the stack, and the .data and .bss sections into one report. It ends with two headings for you to fill in: what you expected, and what you got. The report is shown to you in full before anything else happens, and nothing is sent anywhere. Copy report puts it on your clipboard, ready to paste into a question, and copy link makes a link that opens the same program.

Check yourself

  1. The program has stopped, and the highlighted line is add sum_r, sum_r, w9. Has that add run yet?
  2. A watch on [fp, scores_s] reads 0x0000005500000048. Which two scores does it show, and which of them sits at the lower address?
  3. Why does a watch on i_r not work, and what do you type instead?
  4. A run pauses with a message after 1,000,000 instructions. What is the most likely cause?
  5. You stepped one instruction too far. Which button takes you back without starting over?

answers

show answers
  1. No. The highlighted line is the next one to run.
  2. 85 and 72. 72 sits at the lower address, fp + 16, because the low half comes first in memory.
  3. i_r is an m4 name, and m4 replaces it with w19 before the program is assembled. Watches know register names, so type w19.
  4. A loop whose exit test never comes true.
  5. The back button. Reset would start the program over.

Practice

Each of these programs has a bug. Find it with a breakpoint and a watch or two before you change any code.

To check the tools themselves: