Lab 3 - Debugging Assembly

Accept the Assignment

Accept the assignment on Github Classroom here.

Breaking the Compiler

This repository contains the hw3 stencil code, but we've introduced a bug into the compiler's implementation of binary primitives. If you run dune test, you'll see that there's a failing test involving addition--take a look at the test input, the interpreter and compiler outputs, and the relevant code in lib/compile.ml.

Finding the Problem

What's wrong with the compiler may not be immediately obvious, which is why we're going to use a tool called GDB to debug it. You may have already had some practice with GDB in a course like 33, but if not, you'll learn everything you need to know about it for CSCI 1260 in this lab.

First, we need to get a binary we can execute to demonstrate the bad behavior. Create a new test called add.lisp with the program '(+ 1 (+ 2 3))'. Compile the failing test with dune exec ./bin/compile.exe -- examples/add.lisp out. This will put an assembly file add.lisp.s and an executable add.lisp.exe in the ./out directory.

Working with GDB

To open the executable with GDB, run gdb out/add.lisp.exe. Once opened, GDB will present you with a prompt you can use to step through and introspect your program. Before getting started, enter the command set disassembly-flavor intel to switch GDB to use the flavor of assembly we use in the course.

Next, enter the command layout asm. This will split the interface into two horizontal panes, the bottom one displaying the prompt as before and the top one displaying the assembly instructions making up the program. You can use the arrow keys to navigate the program's instructions.

To tell GDB to run a program, enter the run command. It'll output something like the following:

Starting program: lab3/out/add.lisp.exe
4[Inferior 1 (process 1148955) exited normally]

This gives us the output of the program (4), but isn't terribly useful, since we could have found that out by running the program normally. Instead, we'll add a breakpoint, which tells GDB to pause execution of a program at a certain point. For this example and for most of the course, the first breakpoint you'll want to set is lisp_entry, the section containing the instructions generated by our compiler and linked into the runtime. To add this breakpoint, enter the command b lisp_entry (b short for break).

Now, enter the run command again. You'll see a message like the following stating that execution has been paused at the breakpoint.

Breakpoint 1, 0x0000000000401130 in lisp_entry ()

Note also that the first instruction of lisp_entry is highlighted in the top pane, indicating it is the next instruction that will be executed. To step through instructions, enter si (step instruction) and you'll see the highlight shift down to the next instruction.

Inspecting Program State

At any point in time, you can inspect the state of the program in various ways. One of the most useful for our purposes will be to inspect the state of registers like rax. To do so, enter info registers (or i r for short), and GDB will show you the current values of each register.

You can also inspect values in memory using the x command. For instance, to inspect the value located 8 bytes below the stack pointer, enter:

x/d $rsp-8

This will output a value in decimal form, but there are other formatters you can use like hex (x/h). Note also the $ in front of rsp--that tells GDB to use the value contained in the rsp register.

Task: Use GDB to step through the failing test and find the point at which things go wrong. Once you've found it, call a TA and show them how you worked through it. If you're having trouble check out some of the GDB tricks below.

GDB Tips and Tricks

div

To get more practice working with GDB, take a look at the division operator of HW3 (if you haven't already) and take a stab at implementing it in lab! As you work on it, try using GDB to trace through simple programs that make use of it, and keep an eye on the contents of the relevant registers.