1. Goals for this week:

  1. See the different parts of memory in an example program.

  2. Debugging C programs using gdb.

  3. Debugging memory errors using valgrind.

  4. Practice Writing and Compiling Assembly Code

  5. Introduction to Lab 4.

2. Starting Point Code

Start by creating a week05 in your cs31/weeklylab subdirectory and copying over some files:

$ cd ~/cs31/weeklylab
$ mkdir week05
$ cd week05
$ pwd
/home/you/cs31/weeklylab/week05
$ cp ~kwebb/public/cs31/week05/* ./
$ ls
Makefile    badprog.c      dosomething.s  loops.c     segfaulter.c
assembly.c  dosomething.c  functions.c    memparts.c  valtester.c

3. Parts of Memory

Let’s start by looking at memparts.c. This program prints out the memory address of different parts of the program: global variables, local variables on the stack, instructions, and heap memory locations for malloc’ed space.

Let’s just run this and see where some things are:

./memparts

The thing to note now is that heap memory locations (malloc’ed space) and local variable locations (on the stack) are at very different addresses. We will revisit this program later in the semester when we talk about other parts of program memory.

4. Debugging C programs using gdb

GDB is the GNU debugger. Its primary use is to debug C programs. In an earlier weekly lab, we introduced gdb (intro gdb). This week, we’ll revisit some of the basics of using gdb and take a closer look at using gdb to examine the stack and to examine function calls with pass-by-pointer parameters.

4.1. common gdb commands

We won’t go through this together, but as a good reminder of some of the commonly used gdb commands that we covered in week 2 (intro gdb), you can try running gdb on the badprog program, and follow along with a debugging session of it from the gdb guide: badprog example

The course textbook Section 3.1 contains a similar example, and Section 3.2 discuss gdb commands in more detail.

4.2. Examining stack contents

Let’s start by opening up functions.c and looking at the code. This program contains a lot of functions, and we’ll use it to see gdb’s support for examining the state of the program stack. Let’s run in gdb, set breakpoints at some of the functions, and run until the breakpoint in function g is reached:

$ make
$ gdb ./functions
(gdb) break main     # break at main
(gdb) break g
(gdb) run
(gdb) where          # list stack at break point in main
(gdb) cont
(gdb) where          # list stack at break point in g

At this point we can print out local variables and parameters in the stack from of function g (the function on the top of the stack). We can also move into the context of a different frame on the stack and examine its local variables and parameters.

(gdb) where          # list stack at break point in g
#0  g (x=41) at functions.c:43
#1  0x0000555555555224 in f (y=40) at functions.c:55
#2  0x000055555555528b in blah (y=0x7fffffffe5ac) at functions.c:66
#3  0x00005555555552de in foo (x=40) at functions.c:77
#4  0x0000555555555191 in main (argc=1, argv=0x7fffffffe6f8) at functions.c:31

(gdb) list
(gdb) print x           # prints out function g's x
(gdb) frame 3           # move into foo's stack frame
(gdb) list
(gdb) print x           # print out foo's x variable value
(gdb) print &x          # print out the address of foo's x
(gdb) frame 2           # move into stack frame 2's context (blah)
(gdb) list
(gdb) print y           # print value of blah's y parameter
(gdb) print *y          # print value of what blah's y parameter points to
(gdb) where             # we are still at the same point in execution
(gdb) cont

Tired of typing list all the time? GDB has an option to always show the current assembly code in part of the window:

(gdb) layout src

The only caveat is that it doesn’t always play nicely when the program you’re debugging produces output (e.g., with printf). If you’re using this mode and the display gets scrambled, try pressing CTRL-L (or resizing the terminal window).

4.3. Finding where a program segfaults

Next, let’s run segfaulter. Unsurprisingly, it crashed with a segfault. Let’s run it through gdb, to see how it can help:

$ gdb ./segfaulter

(gdb) run

GDB will immediately tell us which line we segfaulted on:

Program received signal SIGSEGV, Segmentation fault.
0x00005555555551b8 in initfunc (array=0x0, len=100) at segfaulter.c:21
21	        array[i] = i;

From there, we can interrogate the function call stack with where:

(gdb) where
#0  0x00005555555551b8 in initfunc (array=0x0, len=100) at segfaulter.c:21
#1  0x000055555555526b in main (argc=1, argv=0x7fffffffe6f8) at segfaulter.c:44

We can also take a look at surrounding code with the list command:

(gdb) list
16	int initfunc(int *array, int len) {
17
18	    int i;
19
20	    for (i = 1; i <= len; i++) {
21	        array[i] = i;
22	    }
23	    return 0;
24	}
25

Since we’re crashing on line 21, it seems like there might be a problem with the array variable. Let’s try printing it:

(gdb) p array
$1 = (int *) 0x0

Looks like array is a NULL pointer. We can confirm this by switching to main’s stack frame to print the value of arr:

(gdb) frame 1
#1  0x000055555555526b in main (argc=1, argv=0x7fffffffe6f8) at segfaulter.c:44
44	    if (initfunc(arr, 100) != 0 ) {

(gdb) p arr
$2 = (int *) 0x0

The course textbook has an example in 3.1.2 that is the same as this example with more explanation.

4.4. Conditional breakpoints

We won’t go over this example, but loops.c can be used to practice setting conditional breakpoints (a breakpoint that is only hit when a certain expression is true). Conditional breakpoints are useful if the buggy behavior only happens on certain conditions. For example, it may only happen after the 1,000th iteration of a loop. In this case, a conditional breakpoint can be set on the loop counter variable to only break when the loop counter’s value is greater than or equal to 1000. See the comment at the top of the file for how to do this, or follow along with the example in the gdb guide: setting conditional breakpoints example

For more information, see Commands for Setting and Manipulating Breakpoints in section 3.2 of the textbook.

5. Debugging C programs using Valgrind

Next, we will use the valtester.c program to demo valgrind, following along with the example from the valgrind guide.

Chapt. 3.3 of the textbook also covers valgrind.

The valtester.c program has comments associated with every bad memory access error, which is designed to help explain valgrind output. Let’s open that file and take a look.

Valgrind is a tool for finding Heap memory access errors in programs. Memory errors are the most difficult bugs to find in programs. When debugging programs that use pointer variables to access dynamically allocated heap memory space (malloc and free memory), using valgrind can save you hours of debugging time.

6. Writing ARM64 Assembly

To connect to one of the ARM machines, you can use ssh:

ssh arm.cs.swarthmore.edu

Currently, our ARM machines are named after planets in the solar system (i.e., mercury, venus, earth, mars, jupiter, and saturn), so ensure that your terminal prompt is showing the name of a planet to confirm that you’re connected to an ARM machine!

Together, we’re going to write some assembly code, compile it, and test it out.

As we go, let’s refer to the ARM64 instruction reference sheet.

First, open the assembly.c file. You will see that it reads in an int value from the user and then makes a call to the long dosomething(long n) function that returns the result of some arithmetic operation on its parameter value. We are going to implement this function in assembly code in the dosomething.s file.

Next, let’s look at the start of the dosomething function written in assembly. The assembly code in this file doesn’t really do much yet; the function just returns the value 0 (mov x0, #0). Let’s try compiling and running it.

$ make assembly
$ ./assembly

6.1. Write, compile, and run ARM64 assembly code

We are going to implement the following function in assembly:

long dosomething(long n) {
  long x, res;

  x = n + 20;
  res = x*3;

  return res;
}

Open dosomething.s, and we’ll add assembly instructions to implement the body of this function (the stack setup and function return statement are already implemented).

We will implement the body of the dosomething function in a few steps to try out accessing parameter and local variable space on the stack, and compile and run after each step to test out what we have done.

The value of parameter n will be available in register x0.

Work with your neighbor(s) toward making this function implement the dosomething function listed above.

7. Lab 4 Intro

Let’s look at Lab 4, and then you can use the remaining time to get started. Part 1 is a C programming assignment using pointers — refer to this page with information on using gdb and valgrind to debug your C programs. Part 2 is ARM64 Assembly programming. Refer to the assembly writing we did in lab today as you work on this part.

8. Handy Resources