Skip to main content

Learning Objectives:

  • Set up your computing environment for the rest of the quarter.
  • Compile and run C code on a Linux environment.
  • Observe C programming behaviors that will preview the topics covered in the later labs.
  • Fill out quiz on canvas

Be sure to read the tutorials page to set up your environment and pick a text editor before starting this lab.

Warmup

Using gdb to track a segfaulting program

Ask your TA

  • What does s, n and b and do in gdb?
  • How to print variable values ?
  • How to print pointers in hex ?

1. Compile program with appropriate debugging flag(s)

Assume you have the following program, segfault.c that is segfaulting (you can download it here :

#include <stdio.h>

  int main () {
    int foo[5], n;

    memset((char *)0x0, 1, 100);

    printf (" Initial value of n is %d \n", n);
    return 0;
  }

To use GDB, first compile your program using the -g option in cc or gcc, for example: By default, all CMPT-295 builds include the debugger flag.

$  gcc -g -o segfault segfault.c

2. Open GDB (within editor for added functionality)

# If logged into CSIL. You need to do this everytime you log in to CSIL
$ module load cmpt295/cgdb
# If on VM you do not need to do anything.
$ cgdb segfault

To investigate why the program is crashing, run the program first using the run command. Then, you could try the where command. It will show you a stack trace, with the source line number where each function in the stack was.

Note: The run command will cause your source code to be loaded in a second window, one of the many advantages of using cgdb.

In this simple case, it is obvious that the program crashed on the memset() function call.

3. Where is the crash ?

In real world programs with more complicated code, if the where command is not enough to isolate the problem, you’ll need to do debugging at a more detailed level. For that, you may need to set break points at spots where you think the problem might be. In the example shown below, break points are set at line numbers 3, 4, and 6. Then, the program is run once again, while stepping through each break point using the run and next commands respectively:

  (gdb) break 3
  Breakpoint 1 at 0x80483a8: file segfault.c, line 3.
  (gdb) break 4
  Breakpoint 2 at 0x80483b8: file segfault.c, line 4.
  (gdb) break 6
  Breakpoint 3 at 0x80483b8: file segfault.c, line 6.
  (gdb)
  (gdb) run
  The program being debugged has been started already.
  Start it from the beginning? (y or n) y

  Starting program: segfault

  Breakpoint 1, main () at segfault.c:3
  (gdb) next

  Breakpoint 2, main () at segfault.c:6
  (gdb) next

  Program received signal SIGSEGV, Segmentation fault.
  0x009d4b47 in memset () from /lib/tls/libc.so.6
  (gdb)

Given that the example was too simple in the first place, it should still be obvious that the call to memset() is what was causing the segfault.

4. Exit the debugger

Once you find the bug, you might want to kill the program (that crashed halfway through) using the kill command:

  (gdb) help
  List of classes of commands:

  aliases -- Aliases of other commands
  . . .

  Type "help" followed by a class name for a list of commands in that class.
  Type "help" followed by command name for full documentation.
  Command name abbreviations are allowed if unambiguous.
  (gdb)
  (gdb) kill
  Kill the program being debugged? (y or n) y
  (gdb)

Part 1

Editing Code

After acquiring the source file, you will need to open lab1.c in your emacs editor of choice. See the tutorials if you are unsure how to make edits.

** If you are familiar

The lab1.c file contains a number of comments explaining some basics of C. There are five different parts to this lab and you will need to modify or write some lines of code for each one. We recommend keeping a fresh copy of lab1.c around for reference (as you may lose track of all the changes you end up making). In particular, it will be helpful for this lab (and for using C moving forward) if you take a little time to familiarize yourself with the printf() function, which is used to output formatted messages to the console.

Compiling Code

The source file lab1.c won’t do anything by itself; you need a compiler (specifically the GNU C compiler) to generate to an executable from it. The GNU C compiler is available on the instructional Linux machines in the lab.

Using any one of these machines, open a terminal and execute gcc -v. We see:

$ gcc -v
Using built-in specs.
COLLECT\_GCC=gcc
COLLECT\_LTO\_WRAPPER=/opt/rh/devtoolset-7/root/usr/libexec/gcc/x86\_64-redhat-linux/7/lto-wrapper
Target: x86\_64-redhat-linux
Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,fortran,lto --prefix=/opt/rh/devtoolset-7/root/usr --mandir=/opt/rh/devtoolset-7/root/usr/share/man --infodir=/opt/rh/devtoolset-7/root/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-\_\_cxa\_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --enable-plugin --with-linker-hash-style=gnu --enable-initfini-array --with-default-libstdcxx-abi=gcc4-compatible --with-isl=/builddir/build/BUILD/gcc-7.3.1-20180303/obj-x86\_64-redhat-linux/isl-install --enable-libmpx --enable-gnu-indirect-function --with-tune=generic --with-arch\_32=i686 --build=x86\_64-redhat-linux
Thread model: posix
gcc version 7.3.1 20180303 (Red Hat 7.3.1-5) (GCC)

The output tells me a bunch of the configuration options for the my installation of GCC as well as the version number, which is 7.3.1. Assuming that you have saved lab1.c somewhere on your machine, navigate to that directory and then use GCC to compile it with the following command:

$ gcc -g -Wall -std=c99 -o lab1.bin lab1.c

It’s not that important right now for you to know what all of these options do, but -g tells the compiler to include debugging symbols, -Wall says to print warnings for all types of potential problems, -std=c99 says to use the C99 standard (now only 19 years old!), -o lab1 instructs the compiler to output the executable code to a file called lab1, and lab1.c is the source file being compiled.

During execution of that command, you can safely ignore warning about unused variables if you haven’t made any changes yet. This warning would not be shown if you removed -Wall from the gcc command, but you will want -Wall to catch potential errors when you write code yourself.

Having executed the gcc command, you should be able to see a file named lab1 in the same directory:

$ ls
lab1  lab1.c

Running Executables

The lab1 file is an executable file, which you can run using the command ./lab1.bin. You should see:

$ ./lab1.bin
Usage: ./lab1.bin <num>

In this case, the executable lab1 is expecting a command-line argument, which is text that is provided to the executable from the command-line when the program is run. In particular, lab1 wants a number from 1 to 5, corresponding to which part of the lab code you want to run. See main() in lab1.c for more details. For example (your values of p and q may differ):

$ ./lab1.bin 1
\*\*\* LAB 1 PART 1 \*\*\*
x = 295
y = 410
p = 0x7fffaec6a2ec
q = 0x7fffaec6a2e8
x & x = 295

Checking Your Work

With that, you should have everything you need to complete the assignment. Show your code to the TA; you will want to work on the different parts of the lab in order (from 1 to 5). Each question can be answered and/or verified by appropriate edits to the source code. Note that every time you want to test a code modification, you will need to use the gcc -g -Wall -std=c99 -o lab1.bin lab1.c command to produce an updated lab1.bin executable file (Tip: Use the up and down keys to scroll through previous terminal commands you’ve executed).

Most of the code behaviors will seem inexplicable at this point, but our goal is that you will be able to explain to someone else what is going on by the end of this course! =)

Ask your TA

  • How to print pointers in hex ?

Part 2

You will now debug a C program that implements a simple linked list.

Note that there are two driver files included in the tar file for this lab: driver1.c and driver2.c. Each uses the file linkedlist.c, a dynamically-allocated linked list with two bugs. driver1.c will find one bug, and driver2.c will find the other. Select either driver1.c or driver2.c and use that driver file to complete this exercise. Compile linkedlist.c and driver1.c (or driver2.c):

$ cd part2
$ gcc -g -Wall driver1.c linkedlist.c -o listtest

Run listtest, and notice that this program ends with a segmentation fault. Using GDB, find the bug in this linked list, and fix the code so that the test passes:

$ ./listtest
Test OK.

(If you get stuck, you can ask the TA for a hint or watch the instructor video on the lab page).

You are encouraged to discuss GDB with other students. You should try to find the bug on your own, and let others using the same driver to find it on their own as well.

We will reveal the bugs in the lab/class

Ask your TA

  • How to print argvp[] array?

Part 3

The way a running program accesses these additional parameters is that these are passed as parameters to the function main: Here argc means argument count and argument vector. The first argument is the number of parameters passed plus one to include the name of the program that was executed to get those process running. Thus, argc is always greater than zero and argv[0] is the name of the executable (including the path) that was run to begin this process. For example, if we run

int main( int argc, char *argv[] ) {
#include <stdio.h>

int main( int argc, char *argv[] ) {
        printf( "argv[0]:  %s\n", argv[0] );

        return 0;
}

First compile and run it so that the executable name is a.out and then we compile it again and run it so the executable name is arg:

$ gcc argument0.c
$ ./a.out 
argv[0]:  ./a.out
$ gcc -o arg argument0.c
$ ./arg 
argv[0]:  ./arg

If more additional command-line arguments are passed, the string of all characters is parsed and separated into substrings based on a few rules; however, if all the characters are either characters, numbers or spaces, the shell will separate the based on spaces and assign args[1] the address of the first, args[2] the address of the second, and so on.

The following program prints all the arguments:

#include <stdio.h>

int main( int argc, char *argv[] ) {
        int i;

        printf( "argc:     %d\n", argc );
        printf( "argv[0]:  %s\n", argv[0] );

        if ( argc == 1 ) {
                printf( "No arguments were passed.\n" );
        } else {
                printf( "Arguments:\n" );

                for ( i = 1; i < argc; ++i ) {
                        printf( "  %d. %s\n", i, argv[i] );
                }
        }

        return 0;
}

Here we execute this program with one and then twelve command-line arguments:

$ gcc argument.c
$ ./a.out first
argc:     2
argv[0]:  ./a.out
Arguments:
  1. first
$ ./a.out first second third fourth
argc:     5
argv[0]:  ./a.out
Arguments:
  1. first
  2. second
  3. third
  4. fourth