Lab 1 - GDB Banking System Lab - Debugging with GDB

Class: CSCE-313


Notes:

Basic GDB Commands

gdb ./banking-system  # Start GDB
run (r)               # Run program
break (b) Class::function  # Set breakpoint in class method
break file.cpp:line   # Set breakpoint at line
delete 1              # Delete breakpoint number 1
continue (c)          # Continue execution
next (n)              # Execute next line (similar to step over)
step (s)              # Step into function
print (p) object      # Print object contents
x/nfu address         # Examine memory
backtrace (bt)        # Show call stack
info break            # List breakpoints
info locals           # Show local variables
quit (q)              # Exit GDB

scp command to share files to gcp vm:

scp part-1-task-1.png macc@34.68.217.91:/home/macc/Labs/lab-1-debugging-with-gdb-ZocoMacc/deliverables

Compile the program with the make command:

make

Part 1: Debugging Tasks

Task 1: Bank Account Array Memory Tracking

Start task 1 by starting gdb

gdb ./banking-system

Then, run the following commands in gdb:

(gdb) break 35
(gdb) run            # disable debuginfo when prompted
(gdb) ptype Account
(gdb) print bank.accounts[0]

part-1-task-1.png

Task 2: State monitoring

  1. Set breakpoints at login and main:
gdb ./banking-system
(gdb) break main
(gdb) info source      # determine which file is currently active
(gdb) b 35             # set a breakpoint at line 35 of the current source file. 
(gdb) b bank.cpp:7     # set a breakpoint at line 7 of bank.cpp
(gdb) info breakpoints # can shorten to "i b"

part-1-task-2-1.png
2. Watch the running variable.

A watchpoint (using watch <var>) tells GDB to automatically pause the program whenever that variable’s value changes. When it triggers, GDB shows the old value, the new value, and the exact line of code that caused the change.

(gdb) run

# Start tracking running variable
(gdb) watch running

(gdb) c

part-1-task-2-2.png

Task 3: Code Navigation

In this task, you will use multiple commands to navigate through the code and print out some information.

Step into the print_menu() function, look at the surrounding lines of code, and step out:

gdb ./banking-system
(gdb) b 35
(gdb) run
(gdb) step       # Step in
(gdb) list       # View surrounding code
(gdb) finish     # Step out

part-1-task-3-1.png
Examine the initial state of the bank’s first account:

# Look at the accounts array in different ways to understand the memory layout
(gdb) x/24xb &bank.accounts[0]     # Examine 24 bytes starting at first account
                             # x: hex format
                             # b: byte-by-byte

# Now look at the same memory as words (4 bytes each). Compare this output # with the previous one. Can you see that the two commands are essentially # printing the same bytes, just grouped differently?
(gdb) x/6xw &bank.accounts[0]      # w: word format

# To do this, GDB parses the expression 'bank.accounts[0]' and uses the 
# type information to interpret the raw memory as a structured object.
(gdb) print bank.accounts[0]       # See the same data as a structured Account

part-1-task-3-2.png

Task 4: Stack frame analysis

Continue until you are prompted by the program to select a choice on the banking system menu. Then, in the login function, view the call stack with and without variables.

gdb ./banking-system
(gdb) b bank.cpp:login
(gdb) run
=== Banking System Menu ===
1. Login
2. Deposit
3. Withdraw
4. View Balance
5. Upload File
6. Download File
7. Logout
8. Exit
Enter choice: 1
Enter user ID: 0

(gdb) backtrace              # view the call stack
(gdb) bt full                # view the call stack with variables

Task 5: Interrupting the Program

In this task, you will interrupt the program to use GDB instead of hitting a breakpoint.

Why is this useful?: This technique is essential when your program appears frozen, enters an infinite loop, or takes too long to execute. It allows you to pause execution immediately to inspect the current state and identify exactly which line of code is running, even if you didn't set a breakpoint beforehand.

Setup task 5 by logging in as user 0:

gdb ./banking-system
(gdb) run

# login as user 0
=== Banking System Menu ===
1. Login
2. Deposit
3. Withdraw
4. View Balance
5. Upload File
6. Download File
7. Logout
8. Exit
Enter choice: 1
Enter user ID: 0
Logged in as user 0

Send an interrupt when prompted for a choice in the menu by the program, and add a breakpoint. Then, continue to deposit 100.00, setup a watchpoint, and repeatedly step over until the watchpoint triggers

=== Banking System Menu ===
1. Login
2. Deposit
3. Withdraw
4. View Balance
5. Upload File
6. Download File
7. Logout
8. Exit
Enter choice: ^C    # interrupt the program (Ctrl+C)

# Note: You will see later in the semester that signals intended for the  
# program being debugged are delivered instead to gdb, which can then 
# display the gdb prompt and take commands.

(gdb) b main.cpp:76
(gdb) c

# after continuing, you are brought to the same line you interrupted from
# the banking system menu, now start a deposit by inputting
2
Enter amount to deposit: 100.00

(gdb) watch bank.accounts[0].balance
(gdb) next         # Step over
# Continue stepping over until the balance is updated, include watchpoint output

part-1-task-5.png

Part 2: Advanced Debugging Tasks

Task 1: Uninitialized Transaction Array

  1. Start GDB and run the program:
gdb ./banking-system
(gdb) run
  1. Login with user Id - 0 and attempt to deposit a $100 transaction. The program crashes when trying to attempt a deposit, resulting in a segmentation fault.

  2. If the program crashes, GDB pauses at the point of failure. Use the backtrace command to check where the crash occurred. The output will show the call stack (the hierarchy of active function calls) leading to the crash.

(gdb) backtrace

00 - TAMU Brain/6th Semester (Spring 26)/CSCE-313/Labs/Visual Aids/part-2-task-1.png
4. Use your GDB output to determine why the crash happened and determine its resolution.
- From the backtrace output we know we first should look at the types.h file.
- We also know that the line: transactions[transactionCount].accountId = id; is the point at which the program crashes.
- segfaults only if the pointer/index you’re writing through is invalid.

Original types.h:

#ifndef TYPES_H
#define TYPES_H

#include <cstring>
#include <stdexcept>

const int MAX_ACCOUNTS = 10000000;
const int MAX_TRANSACTIONS = 2;

// Transaction structure
struct Transaction {
    int accountId;
    double amount;
    char* description;
};

// Account structure
class Account {
public:
    int id;
    double balance;
    bool active;
    Transaction* transactions;
    int transactionCount;

    Account() {
        id = -1;
        balance = 0;
        active = false;
        transactionCount = 0;
        transactions = nullptr;
    }

    Account(int i): id(i) {
        active = true;
        balance = 0;
        transactionCount = 0;
    }

    Account& operator=(const Account& other) {
        if (this != &other) {
            delete[] transactions;

            id = other.id;
            balance = other.balance;
            active = other.active;
            transactionCount = other.transactionCount;
        }
        return *this;
    }

    bool addTransaction(double amount, const char* desc) {
        transactions[transactionCount].accountId = id;
        transactions[transactionCount].amount = amount;

        transactions[transactionCount].description = new char[strlen(desc) + 1];
        strcpy(transactions[transactionCount].description, desc);

        transactionCount++;
        return true;
    }

    ~Account() {

    }
};

// Bank class declaration
class Bank {
private:
    Account* accounts;
    int accountCount;

public:
    Account* current_account;

    Bank();
    ~Bank();
    bool login(int id);
    void logout();
    bool deposit(double amount);
    bool withdraw(double amount);
};

#endif // TYPES_H
  1. Fix the code, recompile it, and test the flow again.

Fixed code for part-2-task-1:

#ifndef TYPES_H
#define TYPES_H

#include <cstring>
#include <stdexcept>

const int MAX_ACCOUNTS = 10000000;
const int MAX_TRANSACTIONS = 2;

// Transaction structure
struct Transaction {
    int accountId;
    double amount;
    char* description;
};

// Account structure
class Account {
public:
    int id;
    double balance;
    bool active;
    Transaction* transactions;
    int transactionCount;

    Account() {
        id = -1;
        balance = 0;
        active = false;
        transactionCount = 0;
        
        // Initialize the transaction array appropriately
        transactions = new Transaction[MAX_TRANSACTIONS];
        for (int k = 0; k < MAX_TRANSACTIONS; k++) {
            transactions[k].description = nullptr;
        }
    }

    Account(int i) : id(i) {
        active = true;
        balance = 0;
        transactionCount = 0;
        
        // Initialize the transaction array appropriately
        transactions = new Transaction[MAX_TRANSACTIONS];
        for (int k = 0; k < MAX_TRANSACTIONS; k++) {
            transactions[k].description = nullptr;
        }
    }

    Account& operator=(const Account& other) {
        if (this != &other) {
            delete[] transactions;

            id = other.id;
            balance = other.balance;
            active = other.active;
            transactionCount = other.transactionCount;
        }
        return *this;
    }

    bool addTransaction(double amount, const char* desc) {
        transactions[transactionCount].accountId = id;
        transactions[transactionCount].amount = amount;

        transactions[transactionCount].description = new char[strlen(desc) + 1];
        strcpy(transactions[transactionCount].description, desc);

        transactionCount++;
        return true;
    }

    ~Account() {
	    // Destructor trashes transactions descriptions
        if (transactions != nullptr) {
            for (int k = 0; k < transactionCount && k < MAX_TRANSACTIONS; k++) {
                delete[] transactions[k].description;
            }
            delete[] transactions;
        }
    }
};

Task 2: Infinite Recursion in Login

The program hangs or crashes when logging in with ID = 1.

  1. Set breakpoints and run:
(gdb) break Bank::login 
  1. Try logging in with ID 1, continue and stop at the breakpoint multiple times, then examine the call stack:
(gdb) backtrace full
  1. Fix the code, recompile it, and test the flow again.

Original bank.cpp:

#include "types.h"

Bank::Bank() {
    accounts = new Account[MAX_ACCOUNTS];
    accountCount = 0;
    current_account = nullptr;
}

Bank::~Bank() {
    delete[] accounts;
}

bool Bank::login(int id) {
    if (id < 0 || id >= MAX_ACCOUNTS) {
        return false;
    }

    // Task 2: Infinite recursion potential
    if (!accounts[id].active) {
        accounts[id] = Account(id);
        if (id > 0) login(id + 1);
    }

    current_account = &accounts[id];
    return true;
}

void Bank::logout() {
    // Task 5: Double free potential
    delete current_account;
    current_account = nullptr;
}

bool Bank::deposit(double amount) {
    if (!current_account || amount <= 0) {
        return false;
    }

    if(current_account->addTransaction(amount, "deposit")){
        current_account->balance += amount;
    }
    return true;
}

bool Bank::withdraw(double amount) {
    if (!current_account || amount <= 0 || amount > current_account->balance) {
        return false;
    }

    if(current_account->addTransaction(-amount, "withdraw")){
        current_account->balance -= amount;
    }
    return true;
}

This line is the culprit:

if (id > 0) login(id + 1);

Also, the backtrace shows IDs like 104813 decreasing, which is consistent with recursion frames being unwound / optimized weirdly, but the important part is: Bank::login calling itself on line 21 repeatedly is exactly the infinite recursion pattern.

Fix: remove recursion entirely. Login should only log into one account.

if (!accounts[id].active) {
    accounts[id] = Account(id);
}
current_account = &accounts[id];
return true;

Fix also in types.h:

    Account& operator=(const Account& other) {
        if (this != &other) {
            // free existing descriptions
            if (transactions != nullptr) {
                for (int k = 0; k < transactionCount && k < MAX_TRANSACTIONS; k++) {
                    delete[] transactions[k].description;
                    transactions[k].description = nullptr;
                }
                delete[] transactions;
            }

            id = other.id;
            balance = other.balance;
            active = other.active;
            transactionCount = other.transactionCount;

            // deep copy transactions
            transactions = new Transaction[MAX_TRANSACTIONS];
            for (int k = 0; k < MAX_TRANSACTIONS; k++) {
                transactions[k].description = nullptr;
            }

            for (int k = 0; k < other.transactionCount && k < MAX_TRANSACTIONS; k++) {
                transactions[k].accountId = other.transactions[k].accountId;
                transactions[k].amount = other.transactions[k].amount;

                if (other.transactions[k].description != nullptr) {
                    size_t n = strlen(other.transactions[k].description);
                    transactions[k].description = new char[n + 1];
                    strcpy(transactions[k].description, other.transactions[k].description);
                }
            }
        }
        return *this;
    }

Task 3: Buffer Overflow in addTransaction

The program crashes or behaves unexpectedly after many transactions.

Login as user 0 and select 2(deposit).

  1. Attempt to deposit money multiple times. Eventually, the program would crash.

  2. Set a breakpoint and run:

(gdb) break Account::addTransaction
(gdb) run
(gdb) print transactionCount
(gdb) print MAX_TRANSACTIONS
(gdb) continue
  1. Monitor transactionCount during each iteration until the program crashes, determine the error, and resolve it.
    part-2-task-3.png
  2. Fix the code, recompile it, and test the flow again.

Original types.h:

    bool addTransaction(double amount, const char* desc) {
        transactions[transactionCount].accountId = id;
        transactions[transactionCount].amount = amount;

        transactions[transactionCount].description = new char[strlen(desc) + 1];
        strcpy(transactions[transactionCount].description, desc);

        transactionCount++;
        return true;
    }

Fixed types.h:

	bool addTransaction(double amount, const char* desc) {
	    if (transactionCount >= MAX_TRANSACTIONS) {
	        return false; // prevents overflow
	    }
	
	    transactions[transactionCount].accountId = id;
	    transactions[transactionCount].amount = amount;
	
	    transactions[transactionCount].description = new char[strlen(desc) + 1];
	    strcpy(transactions[transactionCount].description, desc);
	
	    transactionCount++;
	    return true;
	}

Also changed bank.cpp:

bool Bank::deposit(double amount) {
    if (!current_account || amount <= 0) return false;

    if (!current_account->addTransaction(amount, "deposit")) {
        return false; // transaction buffer full
    }

    current_account->balance += amount;
    return true;
}

Task 4: Memory Leak in Account and Transaction Class

The goal is to identify and track memory leaks in the Account and Transaction classes, specifically focusing on the dynamically allocated memory for description in each Transaction. The program gradually consumes more memory as transactions are added.

  1. Modify the MAX_TRANSACTIONS = 100;
  2. Set breakpoints to track memory allocation:
(gdb) break Account::addTransaction 
(gdb) run
  1. Examine memory patterns across 03 transactions (for each transaction):
# Print the full transaction object
(gdb) print transactions[0] 

# Examine the memory address of description
(gdb) print transactions[0].description 

# View the actual string content at that address
(gdb) x/s transactions[0].description

(gdb) continue

part-2-task-4-1.png

  1. Memory Tracking Analysis (after all three transactions). This shows that the previous allocations remain in memory and never get freed. Across all transactions, observe the pattern of memory growth with each added transaction:
# Examine all transaction descriptions 
(gdb) print transactions[0].description 
(gdb) print transactions[1].description 
(gdb) print transactions[2].description

# Look at memory addresses - this shows if memory is being allocated but never freed 
(gdb) p/x &transactions[0] 
(gdb) p/x transactions[0].description 
(gdb) p/x transactions[1].description 
(gdb) p/x transactions[2].description

# Examine the memory layout for the first 32 bytes of transactions[0] 
(gdb) x/32x &transactions[0]

part-2-task-4-2.png

  1. Fix the code.

Original types.h:

#ifndef TYPES_H
#define TYPES_H

#include <cstring>
#include <stdexcept>

const int MAX_ACCOUNTS = 10000000;
const int MAX_TRANSACTIONS = 2;

// Transaction structure
struct Transaction {
    int accountId;
    double amount;
    char* description;
};

// Account structure
class Account {
public:
    int id;
    double balance;
    bool active;
    Transaction* transactions;
    int transactionCount;

    Account() {
        id = -1;
        balance = 0;
        active = false;
        transactionCount = 0;

        // Initialize the transactions array appropriately
        transactions = new Transaction[MAX_TRANSACTIONS];
        for (int k = 0; k < MAX_TRANSACTIONS; k++) {
            transactions[k].description = nullptr;
        }
    }

    Account(int i): id(i) {
        active = true;
        balance = 0;
        transactionCount = 0;

        // Initialize the transactions array appropriately
        transactions = new Transaction[MAX_TRANSACTIONS];
        for (int k = 0; k < MAX_TRANSACTIONS; k++) {
            transactions[k].description = nullptr;
        }
    }

    Account& operator=(const Account& other) {
        if (this != &other) {
            // free existing descriptions
            if (transactions != nullptr) {
                for (int k = 0; k < transactionCount && k < MAX_TRANSACTIONS; k++) {
                    delete[] transactions[k].description;
                    transactions[k].description = nullptr;
                }
                delete[] transactions;
                transactions = nullptr;
            }

            id = other.id;
            balance = other.balance;
            active = other.active;
            transactionCount = other.transactionCount;

            // deep copy transactions
            transactions = new Transaction[MAX_TRANSACTIONS];
            for (int k = 0; k < MAX_TRANSACTIONS; k++) {
                transactions[k].description = nullptr;
            }

            if (other.transactions != nullptr) {
                for (int k = 0; k < other.transactionCount && k < MAX_TRANSACTIONS; k++) {
                    transactions[k].accountId = other.transactions[k].accountId;
                    transactions[k].amount = other.transactions[k].amount;

                    if (other.transactions[k].description != nullptr) {
                        size_t n = strlen(other.transactions[k].description);
                        transactions[k].description = new char[n + 1];
                        strcpy(transactions[k].description, other.transactions[k].description);
                    }
                }
            }
        }
        return *this;
    }

    bool addTransaction(double amount, const char* desc) {
        // Check array bounds
        if (transactionCount >= MAX_TRANSACTIONS) {
                  return false; // prevents overflow
              }

        transactions[transactionCount].accountId = id;
        transactions[transactionCount].amount = amount;

        transactions[transactionCount].description = new char[strlen(desc) + 1];
        strcpy(transactions[transactionCount].description, desc);

        transactionCount++;
        return true;
    }

    ~Account() {
        // Trash transaction descriptions at the end
        if (transactions != nullptr) {
            for (int k = 0; k < transactionCount && k < MAX_TRANSACTIONS; k++) {
                delete[] transactions[k].description;
            }
            delete[] transactions;
        }
    }
};

// Bank class declaration
class Bank {
private:
    Account* accounts;
    int accountCount;

public:
    Account* current_account;

    Bank();
    ~Bank();
    bool login(int id);
    void logout();
    bool deposit(double amount);
    bool withdraw(double amount);
};

#endif // TYPES_H

Erase the Destructor content:

	// Keep destructor empty to demonstrate leak for Task 4
	~Account() {
	    // intentionally leaking transactions[*].description and transactions array
	}

Lazy allocation: don’t allocate transactions in the constructors

Allocate it only when the account is actually used (first transaction).

This makes MAX_TRANSACTIONS = 100 safe because only the logged-in account allocates the 100-slot array.

Modify types.h like this

  1. Constructors: don’t allocate transactions
Account() {
    id = -1;
    balance = 0;
    active = false;
    transactionCount = 0;
    transactions = nullptr;   // <-- important
}

Account(int i): id(i) {
    active = true;
    balance = 0;
    transactionCount = 0;
    transactions = nullptr;   // <-- important
}
  1. In addTransaction, allocate on first use
bool addTransaction(double amount, const char* desc) {
    if (transactions == nullptr) {
        transactions = new Transaction[MAX_TRANSACTIONS];
        for (int k = 0; k < MAX_TRANSACTIONS; k++) {
            transactions[k].accountId = -1;
            transactions[k].amount = 0.0;
            transactions[k].description = nullptr;
        }
    }

    if (transactionCount >= MAX_TRANSACTIONS) {
        return false;
    }

    transactions[transactionCount].accountId = id;
    transactions[transactionCount].amount = amount;

    transactions[transactionCount].description = new char[strlen(desc) + 1];
    strcpy(transactions[transactionCount].description, desc);

    transactionCount++;
    return true;
}

Test:

(gdb) print transactions[0].description
$10 = 0x55555556d0b0 "deposit"
(gdb) print transactions[1].description
$11 = 0x55555556d0d0 "deposit"
(gdb) print transactions[2].description
$12 = 0x55555556d0f0 "deposit"
(gdb) p/x &transactions[0]
$13 = 0x55555556c6f0
(gdb) p/x &transactions[0].description
$14 = 0x55555556c700
(gdb) p/x &transactions[1].description
$15 = 0x55555556c718
(gdb) p/x &transactions[2].description
$16 = 0x55555556c730
(gdb) x/32x &transactions[0]
0x55555556c6f0: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x55555556c6f8: 0x00    0x00    0x00    0x00    0x00    0x00    0x59    0x40
0x55555556c700: 0xb0    0xd0    0x56    0x55    0x55    0x55    0x00    0x00
0x55555556c708: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
(gdb)

Repair the destructor:

~Account() {
    if (transactions != nullptr) {
        // Free each allocated description string
        for (int k = 0; k < transactionCount && k < MAX_TRANSACTIONS; k++) {
            delete[] transactions[k].description;
            transactions[k].description = nullptr;
        }

        // Free the transactions array
        delete[] transactions;
        transactions = nullptr;
    }
}

Task 5: Double Free in Logout

The program crashes during logout operations.

Track logout operations:

(gdb) break Bank::logout
(gdb) run

Examine current_account pointer:

(gdb) print current_account
(gdb) x/x current_account
  1. Fix the code, recompile it, and test the flow again.

So in logout(), doing:

delete current_account;

is invalid for two reasons:

  1. It’s not heap memory (new), so delete is undefined behavior.
  2. Even in the “good” case where current_account points into accounts[], that memory is owned by the accounts array (new[]), and you must not delete individual elements.

Change Bank::logout() to only clear the pointer:

void Bank::logout() {
    current_account = nullptr;
}