Lab 2: C pointers/arrays/void *

Lab sessions Mon Jan 19 to Thu Jan 22

Lab written by Julie Zelenski

Learning goals

During this lab, you will:

  1. investigate how arrays and pointers work in C
  2. write code that manipulates arrays and pointers
  3. use gdb and valgrind to help understand and debug your use of memory

First things first! Find an open computer to share with a partner. Introduce yourself and tell them about your favorite music to listen to while coding.

Lab exercises

  1. Get started. Make a clone of the lab starter project using the command

        hg clone /afs/ir/class/cs107/repos/lab2/shared lab2
    

    This creates the lab2 directory which contains source files and a Makefile. Some of you have asked about how you can set permissions so that both you and your partner can access the working directory. The fs setacl command is used to add/remove permissions. Some examples are shown below, replace dirname and username with the directory and sunet you wish to change.

        fs setacl dirname username rl
        fs setacl dirname username rlidwka
        fs listacl dirname
    

    The first two commands change the permissions on dirname to give the user username read and list permissions (rl) or full permissions (rlidwka). The fs listacl command shows the directory's current permissions. You can use the variant fsr setacl to recursively apply permissions to all subdirectories as well. Note: on myth as of Oct 2014, fsr has to be invoked it via its full path /afs/cs/software/bin/fsr.

    Pull up the online lab checkoff and have it open in a browser so you'll be able to jot down things as you go.

  2. Arrays and pointers. The file arrptr.c contains a nonsense C program that uses arrays and pointers. Scan the source before compiling it. Start the program under gdb and set a breakpoint at main. Run the program. When you hit the breakpoint, step through the first few lines which initialize the variables before the call to the binky function and stop here to take a look around. First try out the gdb x command which is used to examine raw memory :

    (gdb) x/10wd arr
    

    x dumps the contents of memory starting at a given address. In the command above, the modifiers /10wd tell gdb to print 10 words (each word is 4 bytes) interpreting each word as a decimal integer. Use help x to read about other available modifiers (btw, help is available for all gdb commands). Try some of the other options on arr to see the change in results.

    Another key command to have in your gdb repertoire is print. You probably already know you can print the values of parameters and variables but you can also do much more! You can evaluate expressions, make function calls, change the values of variables by assigning to them, and so on. Let's use it to experiment with arrays and pointers. The expressions below all refer to arr. First try to figure out what the result of the expression should be, then use print (or shortcut p) in gdb to confirm that your understanding is correct.

    (gdb) p arr
    (gdb) p *arr
    (gdb) p sizeof(arr)
    (gdb) p arr[1]
    (gdb) p arr[1] = -99
    (gdb) p *(arr + 1)
    (gdb) p &arr[-1]
    (gdb) p &arr[3] - &arr[1]
    (gdb) p &arr[0]
    (gdb) p &arr
    

    Notice that main initializes ptr to arr, so it points to the same stack array. If you repeat the above expressions with ptr substituted for arr, most (but not) have the same result. Which ones are different? Why are they different? Resume execution from here using the gdb step command to single-step into the call to binky. The main function calls binky passing arr and ptr into the two arguments a and b. Within binky, try printing the above expressions on parameters a and b. Can you find any expression for which the two parameters differ?

  3. Passing pointers by reference. One often-misunderstood aspect of C arrays/pointers is knowing when and why you need to pass a pointer itself by reference and deal with the dreaded double **. Consider the functions chop_to_front and chop_to_back in arrptr.c. First manually trace the code and predict what effect each call will have on the variables in main. Then run under gdb and single-step through the calls using gdb print or x to observe what is happening in memory as you go. How is chop_to_front successful in making a persistent change? Why is chop_to_back not successful?

    Once you understand the fatal flaw in chop_to_back, change the function to enable the persistent change it is attempting. Because C has no pass-by-reference mechanism, you must manually add a level of indirection. Your new version of chop_to_back can change bufptr, but not buffer. Understanding the difference is tricky! Stop and reason it through. Do you see why buffer is not an L-value and how you cannot reassign where it points? (You may find it surprising that the expression &buffer is even legal, but the & is practically a no-op in this case-- compare what is printed for &buffer[0] versus &buffer)

  4. Memory errors and valgrind. Valgrind is a supremely helpful tool for tracking down memory errors. However, it takes some practice to learn how to interpret a Valgrind report, so that's what this exercise is about. If you haven't already, review the cs107 guide to valgrind written by legendary CS107 TA Nate.

    The buggy.c programs contains a set of memory errors, a few of which get compiler warnings, but most compile without a care. The buggy program is designed to be invoked with a command-line argument (a number from 1 to 9) that identifies which error to make. For each numbered error N, first peruse the code in buggy.c to see what the error is and then try to predict the consequence of that error. Run buggy N without Valgrind and see what (if any) symptoms appear during normal execution. Then run buggy N again under Valgrind and see what it detects. Read the Valgrind report and see how it identifies the type of error, how many bytes were involved, the size and kind of memory at fault (stack/heap/global), and the line of code when the error was detected. How could you use these facts from a Valgrind report to find and fix the root cause of the error?

    Becoming a skilled user of Valgrind is an invaluable asset when working on your assignments. We recommend that you run Valgrind early and often during your development cycle. Memory errors can be messy to de-tangle due to interference with one another, thus it's imperative to focus on resolving them one at a time. Your strategy should go something like this: run all newly-introduced code under valgrind, stop at the first error reported, study the report, follow the details to suspicious part of the code, ferret out root cause, resolve the problem, recompile, and re-test to see that this error has gone away. Repeat for any remaining errors. Although Valgrind is also handy for finding leaks, leaks don't demand the immediate attention that errors do. Leaks can (and should) be safely ignored until the final phase of polishing a working program.

  5. Function pointers. The numbers function in fnptr.c creates an array of random numbers and uses the qsort library function to sort the array into increasing order. Change the code so that array is sorted instead in decreasing order. The strings function reads strings from a file and prints them out. Add code to unique the strings, so that each string is only entered into the array once. Read the man page for lfind and lsearch for library functions appropriate for searching an unordered array.

If you finish with time remaining, we recommend doing the Assignment 2 fill-in-the-blank warmup exercise and work through any issues before starting on the assignment itself!

Check off with TA

Before you leave, complete your checkoff form and ask your lab TA to approve it so you are properly credited. If you don't complete all the exercises during the lab period, we strongly encourage you to followup and finish the remainder on your own.

xkcd pointers comic