GDB - Init File

Learning Outcome

Able to add basic commands to a .gdbinit file as default commands (e.g. breakpoints) to avoid entering them every debug session.

Able to write advanced user-defined GDB commands, and use them to debug more complex data structures including linked lists and binary trees.

Introduction

Often it is tedious to debug complex data structures. Debugging a linked list often requires printing a node by dereferencing the pointer to its struct, then accessing the next node and doing the same to it. This can become tedious and messy, resulting in commands like:

(gdb) print *(curr)
(gdb) print *(curr->next)
(gdb) print *(curr->next->next)
...

However, a command to print out a linked list can be created and placed inside the .gdbinit file.

Applicable subjects

COMP2521, COMP3231


GDB User Initialization File

The user initialization file contains commands that are executed upon the startup of GDB. It is located in your home directory under the path:

~/.gdbinit

Create/edit this file and add the following line to this file:

set auto-load safe-path /

This allows our programs to use a current directory initialization file, i.e. you can create a .gdbinit for each project you’re debugging.

GDB Current Directory Initialization File

The current directory initialization file contains commands to be executed upon the startup of GDB within the current directory.

~/<file_path>/.gdbinit

Create/edit this file to contain any startup behaviour and user defined commands. Use either standard gdb commands or the GDB scripting syntax (which is described below).

Basic Use

.gdbinit files can be used to always execute commands when you run GDB in a particular directory. For example, if you always want to break at a certain function (for those doing OS, this might be the panic() function) then you can place the following in the file:

break <function_name>

GDB Scripting Syntax

Define a command

You can create a user defined command in the .gdbinit file:

define <command>
    <code>
end

Document a command:

You can document a command in the .gdbinit file:

document <command>
    <information about the command>
end

This information appears when the help feature is used:

(gdb) help <command>
<information about the command>

Parameters

When a user defined command is called in GBD, arguments can be passed in:

(gdb) <command> <arg0> <arg1> <arg2> ...

The number of arguments, and the arguments themselves can be referenced inside the command definition using the following variables:

$argc
$arg0
$arg1
$arg2
...

Convenience Variables

Convenience variables are used to store variables as your script runs. They are specified by putting a ‘$’ in front of a name (note there are some reserved names, such as $1):

$<variable_name>

Note, gdb scripting uses one flat namespace - which makes recursion difficult. To use recursion you must use unique names for each of the variables in each stack frame. This can be generated using a name base, plus the value of another variable, separated by an underscore:

$<name_base>_$<other_variable>

Setting a variable

You can set the value of a variable using the set command:

set $<variable_name> = <value_or_expression>

If statements

If statements can be used to conditionally execute GDB commands/ scripts:

if <condition>
    <code>
else
    <code>
end

While Loops

While loops can be used to repeat a section of code for as long as a condition is true:

while <condition>
    <code>
end

Printing

Printing in GDB scripting is very similar to printing in c:

printf "<format string>", <arg0>, <arg1>, ...

Using a User Defined Command

User defined commands are used in the same way as non-user defined commands (such as print). Within gdb, the command name is specified followed by any arguments the command definition requires

(gdb) <command> <arg0> <arg1> <arg2> ...

Examples

Break on a given function every time

OS/EOS students will want to always break on panic() so that when there is an error, the program halts and gdb can be used. There are several things that are required in the .gdbinit file for OS, one of which being a break on panic.

Place the following in the ~/.gdbinit file. This lets GDB use the commands in ./~gdbinit.

set auto-load safe-path /

Place the following in GDB in the ./~gdbinit file. This breaks when the panic function is called.

break panic

Having these two files is effectively like running break panic when GDB is first started. GDB can then be used as normal, and when panic() is called, the execution is automatically stopped.

Debugging Linked lists

There are two main ways to debug code containing linked lists in GDB. You can manually go through and print each node in the linked list (which can become tedious), or you can write a script to do it for you. In this example, we will learn how to write a GDB script to traverse the linked list given in linked_list.c.

linked_list.c .gdbinit

linked_list.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//Makes a linked list of length 7 and prints it out
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>


struct node {
    int data;
    struct node *next;
};

struct node *create_node(int data);
struct node *create_list(int length);
void print_list(struct node *list);

int main(void){
    struct node *list1 = create_list(7);
    print_list(list1);

    return 0;
}

struct node *create_node(int data){
    struct node *new = malloc(sizeof(struct node));
    assert(new != NULL);
    new->data = data;
    new->next = NULL;
    return new;
}

struct node *create_list(int length) {

    struct node *head = NULL;
    if (length > 0) {
        head = create_node(0);
        int i = 1;
        struct node *curr = head;
        while (i < length) {
            curr->next = create_node(i);
            curr = curr->next;
            i++;
        }
    }
    return head;
}

void print_list(struct node *list){
    struct node *curr = list;

    while (curr != NULL) {
        printf("%d->", curr->data);
        curr = curr->next;
    }
    printf("X\n");
}

Place the following in the ~/.gdbinit file:

set auto-load safe-path /

Place the following script to print out a linked list in GDB in the ./~gdbinit file:

.gdbinit
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
define p_generic_list
  set var $n = $arg0
  while $n
    print *($n)
    set var $n = $n->next
  end
end

document p_generic_list
        p_generic_list LIST_HEAD_POINTER
        Print all the fields of the nodes in the linked list pointed to by LIST_HEAD_POINTER. Assumes there is a next field in the struct.
end



define indentby
    printf "\n"
    set $i_$arg0 = $arg0
    while $i_$arg0 > 10
        set $i_$arg0 = $i_$arg0 - 1
        printf "%c", ' '
    end
end

This script is the same as a C program to print out a linked list, excluding a few key syntax differences.

We want our script to traverse the list given in the first argument to the command ($arg0). So we create a convenience variable to store our current pointer and set it to $arg0:

set var $n = $arg0

We might want to change our node structure later on, so we create a command that will print out every field in the node, regardless of what it is. We do this by using the gdb command print on the dereferenced nodes (we could use printf, but we would have to individually specify each field).

print *($n)

The only part of the linked list node that we assume will always be present is the next pointer, which we use to move through our program in the line :

set var $n = $n->next

When our p_generic_list command is run in gdb, it prints out all of the nodes until it reaches a NULL:

$ gcc -g -o linked_list linked_list.c
$ gdb -q ./linked_list
(gdb) br 18
Breakpoint 1 at 0x40061c: file linked_list.c, line 18.
(gdb) r
Starting program: /mnt/d/code/debugging/modules/linked_list
Breakpoint 1, main () at linked_list.c:18
18          print_list(list1);
(gdb) p_generic_list list1
$1 = {data = 0, next = 0x602030}
$2 = {data = 1, next = 0x602050}
$3 = {data = 2, next = 0x602070}
$4 = {data = 3, next = 0x602090}
$5 = {data = 4, next = 0x6020b0}
$6 = {data = 5, next = 0x6020d0}
$7 = {data = 6, next = 0x0}
(gdb)

Debugging Binary Trees

Debugging binary trees can be even more tedious than debugging linked lists, because there is no one, easy path to print it out. This is where a script can be used, to print out a BST graphically.

The following code was adapted from lab 3 of COMP2521, which is just one of many direct applications of GDB in your coursework. All these files do is create a simple BST, which we will then print out in 2D using GDB.

btree.c btree.h test_btree.c .gdbinit

btree.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//An implementation of a binary search tree

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "btree.h"


struct treeNode {
    TreeItem item;
    treelink left;
    treelink right;
};

static treelink createNode(TreeItem item){
     treelink t = (treelink) malloc (sizeof (*t));
     t->item = item;
     t->left = NULL;
     t->right = NULL;
     return t;
}



//This function inserts duplcates on the left
treelink insertTreeNode (treelink tree, TreeItem item) {
  if (tree == NULL) {
      tree = createNode(item);
      tree->item = item;
      tree->left = NULL;
      tree->right = NULL;
   
  } else if (tree->item < item) {
      tree->right = insertTreeNode (tree->right, item);
  } else {
      tree->left = insertTreeNode (tree->left, item);
  }
  return tree;
}
btree.h
1
2
3
4
5
6
7
8
typedef int TreeItem;
typedef struct treeNode * treelink;

//Functions that are provided
treelink insertTreeNode (treelink tree, TreeItem item);


		
test_btree.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include "btree.h"



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

    treelink t1 = insertTreeNode (NULL, 10);
    insertTreeNode (t1, 20);
    insertTreeNode (t1, 5);
    insertTreeNode (t1, 30);
    insertTreeNode (t1, 15);
    insertTreeNode (t1, 25);
    insertTreeNode (t1, 24);


    printf("We can test our tree here\n");
    
    
    return EXIT_SUCCESS;
}

Place the following in the ~/.gdbinit file:

set auto-load safe-path /

Place the following script to print out a 2D BST in GDB in the ./~gdbinit file:

.gdbinit
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
define indentby
    printf "\n"
    set $i_$arg0 = $arg0
    while $i_$arg0 > 10
        set $i_$arg0 = $i_$arg0 - 1
        printf "%c", ' '
    end
end

define draw_sideways_btree
        set $node_$arg0 = ($arg1)
        set $leftid_$arg0 = $arg0 + 1
        set $rightid_$arg0 = $arg0 + 2
        set $space_$arg0 = ($arg2) + 10
        
        if $node_$arg0->right
                draw_sideways_btree $rightid_$arg0 $node_$arg0->right $space_$arg0
        end
        
        indentby $space_$arg0

        printf "%d\n", $node_$arg0->item

        if $node_$arg0->left
                draw_sideways_btree $leftid_$arg0 $node_$arg0->left $space_$arg0
        end    
end

define start_draw_sideways_btree
        draw_sideways_btree 0 $arg0 0
end

document start_draw_sideways_btree
        start_draw_sideways_btree ROOT_POINTER_NAME
        Draw a sideways representation of the binary tree pointed to by ROOT_POINTER_NAME 
end

The easiest way to print out a 2D tree (and the approach taken in the script above) is a rotated anticlockwise tree because that uses a simple reverse inorder traversal.

However, most implementations of this require recursion - which becomes difficult with a flat namespace (like the one in GDB scripting).

To use a recursive approach, we will need unique names for each of the convenience variables. These can be created by appending the node number to a base variable name. For example:

$node_$arg0

The standard reverse indorder traversal is as follows:

  1. If the right node is not NULL, call the function on the right node.

if $node_$arg0->right
        draw_sideways_btree $rightid_$arg0 $node_$arg0->right $space_$arg0
end
  1. Print out spaces (the number of spaces printed increases by 10 with each level down the tree).

indentby $space_$arg0
  1. Print out the data in the current node

printf "%d\n", $node_$arg0->item
  1. If the left node is not NULL, call the function on the right node

if $node_$arg0->left
        draw_sideways_btree $leftid_$arg0 $node_$arg0->left $space_$arg0
end

We can then run our start_draw_sideways_btree command on our BST.

$ gcc -g -o test_btree test_btree.c btree.c
$ gdb -q ./test_btree
Reading symbols from ./test_btree...done.
(gdb) br 17
Breakpoint 1 at 0x4005ee: file test_btree.c, line 17.
(gdb) r
Starting program: /mnt/d/code/debugging/modules/test_btree

Breakpoint 1, main (argc=1, argv=0x7ffffffee118) at test_btree.c:19
19          printf("We can test our tree here\n");
(gdb) start_draw_sideways_btree t1

                    30

                              25

                                        24

          20

                    15

10

          5

A sideways representation of our tree is produced. The implicit links between the nodes are illustrated below:

                    30
                   .      .
                 .            25
               .                  .
             .                        24
           .
          20
       .         .
    .               15
 .
10
     .
          5

Which, when we rotate to be the correct way up becomes:

../../_images/btree.jpg

Module author: Liz Willer <e.willer@unsw.edu.au>

Date

2020-01-21