1. Goals for this week:
-
See the different parts of memory in an example program.
-
Debugging C programs using gdb.
-
Debugging memory errors using valgrind.
-
Introduction to Lab 4.
2. Starting Point Code
Start by creating a week05
in your cs31/WeeklyLabs
subdirectory and copying over some files:
$ cd ~/cs31/WeeklyLabs
$ mkdir week05
$ cd week05
$ pwd
/home/you/cs31/WeeklyLabs/week05
$ cp ~sukrit/public/cs31/week05/* ./
$ ls
Makefile allocate.c memparts.c simple_valtester.c valtester.c
README badprog.c functions.c segfaulter.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 in the data segment,
local variables on the stack segment, instructions in the code/text segment,
and malloc’ed memory in the heap segment.
Let’s run this code and see where some things are:
$ make
$ ./memparts
Note that heap memory locations (malloc’ed space in the heap segment) and local variable locations (on the stack segment) are at very different addresses.
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 in In-Lab 2 (intro gdb). This week, we will 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 will not go through this together, but as a good reminder of
some of the commonly used gdb commands that we covered
in In-Lab 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 discusses gdb commands in more detail.
4.2. examining stack contents
We will start by opening up functions.c
and looking at the code:
$ vim functions.c
This program contains a lot of functions, and we will use it to
see gdb’s support for examining the state of the program stack. Let’s
run in gdb, and set breakpoints in 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.
Of particular note is the print command that lets us print addresses
of variables using the &
operator: print &x
, and it also lets us
dereference pointers using the *
operator: print *y
.
(gdb) where # list stack at break point in g
#0 g (x=41) at functions.c:15
#1 0x00005555555546a2 in f (y=40) at functions.c:23
#2 0x00005555555546ff in blah (y=0x7fffffffe2bc) at functions.c:33
#3 0x0000555555554748 in foo (x=40) at functions.c:40
#4 0x00005555555547a4 in main (argc=1, argv=0x7fffffffe3d8) at functions.c:53
(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
4.3. finding where program segfaults
Next, let’s run segfaulter
.
$ ./segfaulter
Segmentation fault (core dumped)
We are going to follow the gdb
guide to see how to find
where a program segfaults in order to help determine the
cause of the segfault and fix it:
gdb guide: segfaulter example.
The course textbook has an example in 3.1.2 that is the same
as this example with more explanation.
5. Arrays and Functions
The allocate.c
program demonstrates how to pass a pointer to a
function, how to return a pointer from a function, and how to use
malloc
and free
to allocate heap memory for an array.
Let’s trace through this program, drawing the stack and the heap as the program executes.
Here are the important takeaways:
-
In
main
, we pass the memory address ofarray_size
to theallocate_array
function. This allows theallocate_array
function to modify thearray_size
variable that is in the stack frame ofmain
. -
The
allocate_array
function usesmalloc
to allocate enough space on the heap to holdval
integers and then returns the memory address of the start of this block of memory. We save this memory address in the variablearray
. -
It’s good practice to check to make sure that the call to
malloc
succeeded. If themalloc
function returnsNULL
, it means that heap memory was not allocated because, e.g. there was no memory left to be allocated. -
The return type of the
allocate_array
function isint *
. This allows us to return the memory address of the allocated heap memory back tomain
. -
In
main
, we can access dynamically allocated memory through a pointer similar to how we would access a statically allocated array, e.g., array[1]. A statically allocated array is one that was declared like this, e.g.:int array[10];
while a dynamically allocated array is declared like this:int *array = malloc(10 * sizeof(int));
. -
When we are done using the heap-allocated memory, we need to
free
it.
6. Debugging C programs using Valgrind
Next, we will use the simple_valtester.c
program to demo Valgrind.
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.
In the simple_valtester.c
program, we see that it runs with no apparent
errors (although gcc did provide some warnings when compiling it).
$ ./simple_valtester
0
1
2
3
4
5
0
1
2
3
4
5
a = 32764
Now run valgrind
on this program, we will see that there are a number of errors:
$ valgrind ./valtester
...
==299734== Invalid write of size 1
==299734== at 0x10936A: foo (valtester.c:70)
==299734== by 0x1091E6: main (valtester.c:24)
==299734== Address 0x4a9c095 is 0 bytes after a block of size 5 allocd
==299734== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==299734== by 0x109305: foo (valtester.c:60)
==299734== by 0x1091E6: main (valtester.c:24)
==299734==
...
==299734== Conditional jump or move depends on uninitialised value(s)
==299734== at 0x484F229: strlen (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==299734== by 0x48F1D97: __printf_buffer (vfprintf-process-arg.c:435)
==299734== by 0x48F272A: __vfprintf_internal (vfprintf-internal.c:1544)
==299734== by 0x48E71A2: printf (printf.c:33)
==299734== by 0x109398: foo (valtester.c:75)
==299734== by 0x109227: main (valtester.c:29)
==299734==
...
==299734== Invalid free() / delete / delete[] / realloc()
==299734== at 0x484988F: free (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==299734== by 0x1092B7: main (valtester.c:40)
==299734== Address 0x4a9c660 is 0 bytes inside a block of size 8 freed
==299734== at 0x484988F: free (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==299734== by 0x1092AB: main (valtester.c:39)
==299734== Block was allocd at
==299734== at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==299734== by 0x109280: main (valtester.c:36)
...
Some errors include, but are not limited to: (1) accessing memory (read/write operations) that was not allocated to you, (2) declaring and using variables without initializing them, and (3) allocating memory without freeing it.
For a more detailed look at using Valgrind, follow along
with the example from the valgrind guide with valtester.c
. Chapter 3.3 of the textbook also covers valgrind.
7. Lab 4 Intro
Let’s look at Lab 4, and then you can use the remaining time to get started. The lab consists of a C programming assignment using pointers, and remember this page with information on using gdb and valgrind to debug your C programs.
8. Handy Resources
-
C programming
-
C debugging
-
Chapter 3 on gdb and valgrind
-
Unix