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]
/CSCE-313/Lecture/Visual%20Aids/part-1-task-1.png)
Task 2: State monitoring
- 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"
/CSCE-313/Labs/Visual%20Aids/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
/CSCE-313/Labs/Visual%20Aids/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
/CSCE-313/Labs/Visual%20Aids/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
/CSCE-313/Lecture/Visual%20Aids/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
- Note: The backtrace command displays the list of active function calls (stack frames) that led to the current point of execution, allowing you to trace the path the program took to get there. You can view this with and without local variables.
/CSCE-313/Lecture/Visual%20Aids/part-1-task-4.png)
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
/CSCE-313/Labs/Visual%20Aids/part-1-task-5.png)
Part 2: Advanced Debugging Tasks
Task 1: Uninitialized Transaction Array
- Start GDB and run the program:
gdb ./banking-system
(gdb) run
-
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.
-
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
/CSCE-313/Labs/Visual%20Aids/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
-
Bug 1: transactions is never allocated
Account()sets transactions = nullptr;Account(int i)does not set transactions at alladdTransaction()assumes transactions points to an array- So the first deposit tries to write to
transactions[0]and crashes (since it is uninitialized).
-
Bug 2: No bounds check on transactionCount
- You set
MAX_TRANSACTIONS = 2, butaddTransaction()never checks iftransactionCountis already 2. - Even after you allocate, a 3rd transaction would write out of bounds.
- You set
- 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.
- Set breakpoints and run:
(gdb) break Bank::login
- Try logging in with ID 1, continue and stop at the breakpoint multiple times, then examine the call stack:
(gdb) backtrace full
- 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);
- You typed user ID 1
- It activates account 1, then calls login(2), which activates 2, calls login(3), etc…
- That keeps going until it hits MAX_ACCOUNTS, and you’ve set MAX_ACCOUNTS = 10,000,000, so it can recurse millions of times → stack/chaos.
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).
-
Attempt to deposit money multiple times. Eventually, the program would crash.
-
Set a breakpoint and run:
(gdb) break Account::addTransaction
(gdb) run
(gdb) print transactionCount
(gdb) print MAX_TRANSACTIONS
(gdb) continue
- Monitor transactionCount during each iteration until the program crashes, determine the error, and resolve it.
/CSCE-313/Lecture/Visual%20Aids/part-2-task-3.png)
- 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;
}
- This will reflect that the deposit fails
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.
- Modify the MAX_TRANSACTIONS = 100;
- Set breakpoints to track memory allocation:
(gdb) break Account::addTransaction
(gdb) run
- 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
/CSCE-313/Labs/Visual%20Aids/part-2-task-4-1.png)
- 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]
/CSCE-313/Labs/Visual%20Aids/part-2-task-4-2.png)
- 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
- 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
}
- 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)
- Each transaction allocates a new heap string:
transactions[0].description = 0x...d0b0transactions[1].description = 0x...d0d0transactions[2].description = 0x...d0f0
- The addresses increase in a steady pattern → repeated allocations.
- Your
x/32x &transactions[0]dump clearly contains the pointer bytes fortransactions[0].descriptionat 0x...c700 (you can see b0 d0 56 55 55 55 = little-endian of 0x55555556d0b0).
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
- Fix the code, recompile it, and test the flow again.
So in logout(), doing:
delete current_account;
is invalid for two reasons:
- It’s not heap memory (new), so delete is undefined behavior.
- 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;
}
- That's it
- No
delete, since the destructor is already taking charge of this.- accounts is allocated once in Bank::Bank() via new Account[MAX_ACCOUNTS]
- Those accounts are freed once in Bank::~Bank() via delete[] accounts
- current_account is just a non-owning pointer to one of the accounts (or should be)
- So logout just “forgets” which account is logged in.