Debugging in the playground
prerequisite
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 = 80The 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:
| expression | what it shows |
|---|---|
w19 | the 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 |
*x21 | 8 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 = 79The 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
- The program has stopped, and the highlighted line is
add sum_r, sum_r, w9. Has that add run yet? - A watch on
[fp, scores_s]reads0x0000005500000048. Which two scores does it show, and which of them sits at the lower address? - Why does a watch on
i_rnot work, and what do you type instead? - A run pauses with a message after 1,000,000 instructions. What is the most likely cause?
- You stepped one instruction too far. Which button takes you back without starting over?
answers
show answers
- No. The highlighted line is the next one to run.
- 85 and 72. 72 sits at the lower address,
fp + 16, because the low half comes first in memory. i_ris an m4 name, and m4 replaces it withw19before the program is assembled. Watches know register names, so typew19.- A loop whose exit test never comes true.
- 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.
- The overeager counter: a countdown that prints one line too many.
- Fix the loop bound: a loop that stops one step too early.
- The vanishing value: a swap of two registers that loses one of the values.
- The leaky frame: a frame whose size breaks the 16-byte rule, so the program dies before it prints.
To check the tools themselves:
- Basic quiz: debugging in the playground: where a breakpoint stops, what reset keeps, what a watch on memory shows, and what the stack rows and the diagnostic bundle hold.