What’s GDB?

GDB, the GNU Debugger, is an efficient debugger for debugging applications in multiple programming languages such as C, C++, and more. It enables programmers to:
- Run Programs Step-by-Step: Execute code line by line to understand its flow.
- Set Breakpoints: Pause execution at specific points to inspect the program’s state.
- Inspect Variables: Examine and modify variable values during execution.
- Analyze Crashes: Identify the cause of program crashes by examining the call stack and memory.
- Modify Execution: Change program behavior on the fly for testing purposes.
For low-level tasks like binary analysis, reverse engineering, or kernel debugging, GDB is required.
Let’s start with the most basic commands:
Command | Alias | Description |
---|---|---|
b | break | Sets a breakpoint at a specific line, function, or memory address. |
r | run | Starts the program execution. |
c | continue | Resumes the program execution. |
n | next | Executes the next line of code without stepping into functions. |
s | step | Steps into the next function call. |
p | print | Prints the value of a variable. |
q | quit | Exits the debugger. |
info | Displays information about the program, breakpoints, and more. | |
x/4xb | Examines memory at a given address, printing 4 bytes in hexadecimal format. | |
catch syscall | Catches system calls made by the program, useful for debugging syscalls. | |
start | Puts a breakpoint at the first line of code that is executed. |
Let’s try to use these commands in a simple C++ program:
#include<iostream>
int main(){
int x = 10;
int y, z;
std::cout<<"Give 2 values: "<<std::endl;
std::cin>>y>>z;
if(y>z){
x = x + y;
}else{
x = x + z;
}
std::cout<<"Value: "<<x<<std::endl;
return 0;
}
Starting the Debugger with gdb
First of all, we need to use the keyword gdb
to start the debugger. Then, we can use various GDB commands to debug the program efficiently.
gdb main
After you run the command, you will see the following output:
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from main...
(No debugging symbols found in main)
(gdb)
Debugging the Program: Exploring Functions
Now that the debugger is up and running, we can leverage the previously mentioned GDB commands to effectively analyze and debug the program.
Investigating Program Functions
To deepen our understanding of how the program works, let’s start by examining the functions used within the code. This step will allow us to uncover any potential issues and optimize our approach for debugging.
i functions
//or
info functions
The output will be:
_init
__cxa_finalize@plt
std::basic_istream<char, std::char_traits<char> >::operator>>(int&)@plt
std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)@plt
std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))@plt
__stack_chk_fail@plt
std::basic_ostream<char, std::char_traits<char> >::operator<<(int)@plt
_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
main
_fini
As you can observe a few functions like istream or ostram are used in the program.
The core functionality lies in the main
function, where the majority of the code relevant to our interest resides.
Thus, we can insert a breakpoint in the main function and execute the program.
break main
#or
b main
Disassembling the Main Function
To examine the low-level assembly instructions, we can use the command disassemble main
. This will display the assembly code corresponding to the main function, giving us insight into how the program is executed at the machine level.
Dump of assembler code for function main:
0x00000000000011c9 <+0>: endbr64
0x00000000000011cd <+4>: push %rbp
0x00000000000011ce <+5>: mov %rsp,%rbp
=> 0x00005555555551d1 <+8>: sub $0x20,%rsp
0x00000000000011d5 <+12>: mov %fs:0x28,%rax
0x00000000000011de <+21>: mov %rax,-0x8(%rbp)
0x00000000000011e2 <+25>: xor %eax,%eax
0x00000000000011e4 <+27>: movl $0xa,-0xc(%rbp)
0x00000000000011eb <+34>: lea 0xe12(%rip),%rax # 0x2004
0x00000000000011f2 <+41>: mov %rax,%rsi
0x00000000000011f5 <+44>: lea 0x2e44(%rip),%rax # 0x4040 <_ZSt4cout@GLIBCXX_3.4>
0x00000000000011fc <+51>: mov %rax,%rdi
0x00000000000011ff <+54>: call 0x10a0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
0x0000000000001204 <+59>: mov 0x2dcd(%rip),%rdx # 0x3fd8
0x000000000000120b <+66>: mov %rdx,%rsi
0x000000000000120e <+69>: mov %rax,%rdi
0x0000000000001211 <+72>: call 0x10b0 <_ZNSolsEPFRSoS_E@plt>
0x0000000000001216 <+77>: lea -0x14(%rbp),%rax
0x000000000000121a <+81>: mov %rax,%rsi
0x000000000000121d <+84>: lea 0x2f3c(%rip),%rax # 0x4160 <_ZSt3cin@GLIBCXX_3.4>
0x0000000000001224 <+91>: mov %rax,%rdi
0x0000000000001227 <+94>: call 0x1090 <_ZNSirsERi@plt>
0x000000000000122c <+99>: mov %rax,%rdx
0x000000000000122f <+102>: lea -0x10(%rbp),%rax
0x0000000000001233 <+106>: mov %rax,%rsi
0x0000000000001236 <+109>: mov %rdx,%rdi
0x0000000000001239 <+112>: call 0x1090 <_ZNSirsERi@plt>
0x000000000000123e <+117>: mov -0x14(%rbp),%edx
0x0000000000001241 <+120>: mov -0x10(%rbp),%eax
0x0000000000001244 <+123>: cmp %eax,%edx
0x0000000000001246 <+125>: jle 0x1250 <main+135>
0x0000000000001248 <+127>: mov -0x14(%rbp),%eax
0x000000000000124b <+130>: add %eax,-0xc(%rbp)
0x000000000000124e <+133>: jmp 0x1256 <main+141>
0x0000000000001250 <+135>: mov -0x10(%rbp),%eax
0x0000000000001253 <+138>: add %eax,-0xc(%rbp)
0x0000000000001256 <+141>: lea 0xdb7(%rip),%rax # 0x2014
0x000000000000125d <+148>: mov %rax,%rsi
0x0000000000001260 <+151>: lea 0x2dd9(%rip),%rax # 0x4040 <_ZSt4cout@GLIBCXX_3.4>
0x0000000000001267 <+158>: mov %rax,%rdi
0x000000000000126a <+161>: call 0x10a0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
0x000000000000126f <+166>: mov %rax,%rdx
0x0000000000001272 <+169>: mov -0xc(%rbp),%eax
0x0000000000001275 <+172>: mov %eax,%esi
0x0000000000001277 <+174>: mov %rdx,%rdi
0x000000000000127a <+177>: call 0x10d0 <_ZNSolsEi@plt>
0x000000000000127f <+182>: mov 0x2d52(%rip),%rdx # 0x3fd8
0x0000000000001286 <+189>: mov %rdx,%rsi
0x0000000000001289 <+192>: mov %rax,%rdi
0x000000000000128c <+195>: call 0x10b0 <_ZNSolsEPFRSoS_E@plt>
0x0000000000001291 <+200>: mov $0x0,%eax
0x0000000000001296 <+205>: mov -0x8(%rbp),%rdx
0x000000000000129a <+209>: sub %fs:0x28,%rdx
0x00000000000012a3 <+218>: je 0x12aa <main+225>
0x00000000000012a5 <+220>: call 0x10c0 <__stack_chk_fail@plt>
0x00000000000012aa <+225>: leave
0x00000000000012ab <+226>: ret

The =>
symbol indicates the instruction that is currently set to be executed.
GDB supports a number of commands to debug programs, and each of them has its own specific purpose. However, for now, let’s learn these simple commands so that we can handle basic things, such as debugging issues in picoCTF. Mastering these simple commands will give you a strong foundation for more complex debugging processes down the line. I will utilize GDB baby step 2 to provide further explanation regarding GDB.
GDB Baby Step 2
In this challenge
, we are given a binary file named debugger0_c
, i used mv debugger0_c d0
to rename the file.
Before starting let’s read the Description:
Can you figure out what is in the eax register at the end of the main function? Put your answer in the picoCTF flag format: picoCTF{n} where n is the contents of the eax register in the decimal number base. If the answer was 0x11 your flag would be picoCTF{17}. Debug this.
WHat do they ask us to do?
- We need to figure out what is in the eax register at the end of the main function.
Let’s analyse the binary file using file
command.
file d0
Output:
d0:
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a10a8fa896351748020d158a4e18bb4be15cd3aa, for GNU/Linux 3.2.0, not stripped
What does this output mean?
- The binary file is a 64-bit ELF executable file for the x86-64 architecture.
- It is dynamically linked and requires the
/lib64/ld-linux-x86-64.so.2
interpreter. - The file was built for GNU/Linux 3.2.0.
Now we’re going to use gdb to debug the binary file.
-> gdb d0
-> info functions
-> disassemble main
Epilogue
0x000000000040110a <+4>: push %rbp
0x000000000040110b <+5>: mov %rsp,%rbp
[Part1]
0x000000000040110e <+8>: mov %edi,-0x14(%rbp)
0x0000000000401111 <+11>: mov %rsi,-0x20(%rbp)
0x0000000000401115 <+15>: movl $0x1e0da,-0x4(%rbp)
0x000000000040111c <+22>: movl $0x25f,-0xc(%rbp)
0x0000000000401123 <+29>: movl $0x0,-0x8(%rbp)
[Part2]
0x000000000040112a <+36>: jmp 0x401136 <main+48>
0x000000000040112c <+38>: mov -0x8(%rbp),%eax
0x000000000040112f <+41>: add %eax,-0x4(%rbp)
0x0000000000401132 <+44>: addl $0x1,-0x8(%rbp)
0x0000000000401136 <+48>: mov -0x8(%rbp),%eax
0x0000000000401139 <+51>: cmp -0xc(%rbp),%eax
0x000000000040113c <+54>: jl 0x40112c <main+38>
0x000000000040113e <+56>: mov -0x4(%rbp),%eax
Prologue
0x0000000000401141 <+59>: pop %rbp
0x0000000000401142 <+60>: ret
Now i splitted the main function into two parts, Part1 and Part2, Prologue and Epilogue.
- Part1: We use this section for declaring the variables and defining their values.
- Part2: This section is used to perform the main operations and calculations.
- Prologue: This part is used to set up the stack frame.
- Epilogue: It is utilized to finish the stack frame and allow the function to return.
Check the next link to learn more about Stack-Heap-Buffer
It would be quite easy at this stage to break the program execution at the end of the main function and look at what is in the eax register. Yet the objective is not only to obtain the eax, but also to comprehend the program and the way it functions.
Prior to diving into the program, it is important to understand that there is a significant distinction between the present assembly code and that provided in earlier blog entries. The earlier blogs were in Intel syntax, but this one is in AT&T syntax.
Feature | AT&T Syntax (Linux) | Intel Syntax (Windows, MASM) |
---|---|---|
Operand Order | OP src, dst (source first) | OP dst, src (destination first) |
Register Prefix | %eax, %rbp | eax, rbp |
Immediate Values | $0x10 (uses $ for constants) | 0x10 |
Memory Access | (%rbp), -4(%rbp) | [rbp], [rbp-4] |
Indirect Memory | mov (%eax), %ebx | mov ebx, [eax] |
Instruction Size | movl, movb, movw (l = 32-bit, W = 16-bit, b = 8-bit) | mov (size inferred) |
Now that we know the basic differences between the two syntaxes, let’s start understanding the program.
Part 1
The first part, which I refer to as the initialization, performs basic mov
instructions.
[rbp-14] = edi
[rbp-20] = rsi
[rbp-4] = 0x1e0da
[rbp-c] = 0x25f
[rbp-8] = 0x0
However, you’ll notice that we use movl
instead of mov
. Let’s take a moment to understand the significance of this instruction.
Definition:
movl
Instruction in x86-64 AssemblyThe
movl
instruction is designed to transfer a 32-bit value (4 bytes) from one location to another in x86-64 assembly. It is part of theMOV
family of instructions, which are responsible for moving data between registers and memory.
movl $0xa, -0x8(%rbp)
movl $0x14, -0x4(%rbp)
In GDB, at least based on my experience, variables are typically stored within the stack frame. As a result, we may not observe the variable x being dereferenced directly in the code. However, we can observe the value of x at** [rbp-8], **as shown in the code above, where the C++ declaration int x = 10; is represented by the value 10 stored at that stack location.
Part 2
Let’s now look at the complex part of the code, where the main operations are performed.
- Remember that in AT&T syntax, the source operand comes first, and the destination operand comes second.
The instruction:
asm
<+36>: jmp 0x401136 <main+48>
It represents a basic jump operation, where the destination is main+48 (or memory address 0x401136).
Next, we have:
<+48>: mov -0x8(%rbp),%eax
This moves the value at [rbp-8] into the eax register. Since the value at [rbp-8] is 0, eax will be set to 0.
This is followed by:
<+48>: cmp -0xc(%rbp),%eax
<+54>: jl 0x40112c <main+38>
Here, we compare the value in eax with [rbp-c]. If eax is less than the value at [rbp-c], a jump will occur to main+38.
You can think of it like this in C++:
if eax < rbp-c{
goto <main+38>
}
This logic ensures the program flow is conditional based on the comparison between eax and the value at [rbp-c].
Let's analyze the main+38 blocK
<+38>: mov -0x8(%rbp),%eax
<+41>: add %eax,-0x4(%rbp)
<+44>: addl $0x1,-0x8(%rbp)
As you might see already mov and add
so we move the value of rbp-8 into eax and add rbp-4 with eax so the new value of rbp-4 will be 0x1e0da then we use addl which is the 32-bit version of the add
instruction in x86-64 assembly. so [rbp-8] + 1 that is the new value of [rbp-8] will be 1
and we return to main+48 and we repeat the same thing again and again until eax > rbp-c.
Now we can do it manually but here the idea is not to do it manually since it is time-consuming but gdb offers us the ability to break and then examine the values in registers.
Example:
<+0>: endbr64
<+4>: push rbp
<+5>: mov rbp,rsp
<+8>: mov DWORD PTR [rbp-0x14],edi
<+11>: mov QWORD PTR [rbp-0x20],rsi
<+15>: mov DWORD PTR [rbp-0x4],0x9fe1a
<+22>: mov eax,DWORD PTR [rbp-0x4]
<+25>: pop rbp
<+26>: ret
That’s a fairly basic example we have this variable rbp-4 we don’t really care what the variable is named but we know what value is in **rbp-4 **and that is 0x9fe1a and then we can figure out what the value of eax is pretty quickly. Let’s say there was a cmp and jmp instructions (loop ideea) maybe 5 loops and the value of eax would be added by 1 it’s easy to say.
To place a break on a memory address we’re going to use :
break *0x40113e
To find out the value stored in registers, we will utilize a straightforward command:
info registers
Output:
rax 0x25f 607
rbx 0x7fffffffe718 140737488348952
rcx 0x7ffff7f90680 140737353680512
rdx 0x7fffffffe728 140737488348968
rsi 0x7fffffffe718 140737488348952
rdi 0x1 1
rbp 0x7fffffffe5f0 0x7fffffffe5f0
rsp 0x7fffffffe5f0 0x7fffffffe5f0
r8 0x4011c0 4198848
r9 0x7ffff7fcae60 140737353920096
r10 0x7fffffffe320 140737488347936
r11 0x203 515
r12 0x1 1
r13 0x0 0
r14 0x7ffff7ffd000 140737354125312
r15 0x0 0
rip 0x40113e 0x40113e <main+56>
eflags 0x246 [ PF ZF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
fs_base 0x7ffff7da5740 140737351669568
gs_base 0x0 0

Wait but there is no eax register. Actually there it is as you may know rax is made up of 2 eax registers sooo what we are going to do rn is to print the value of eax register.
print/x $eax # Print EAX in hexadecimal
print/d $eax # Print EAX in decimal
print/t $eax # Print EAX in binary
When we run it, we notice that the value in eax is 607 which is NOT the answer to the challenge.
The registers RAX and EAX hold identical values. What does this mean? Possibly because that loop will run 607 times, from 0 to 606 Thus, the total amount contributed to RBP-4 is represented by the summation of integers from 0 to 606. So we can have a formula here The sum of the first n integers is given by the formula n*(n+1)/2. So we got 606+607 /2 and we have [res], I’m not gonna leak any results do some simple math after all.
Okay let me explain:
1. What does the program do?
- The program initializes a variable (at address
-0x4(%rbp)
) with the value0x1e0da
. - Then, in a loop, it adds numbers from 0 to 606 (607 iterations) to this variable.
- At the end, the value from the variable is moved to the
eax
register.
2. Why does the sum of numbers from 0 to 606 appear?
- The loop starts with a counter variable (
-0x8(%rbp)
) initialized to 0. - In each iteration, the value of the counter is added to the main variable (
-0x4(%rbp)
), and the counter is incremented by 1. - The loop stops when the counter exceeds 606.
Thus, the loop performs the following sum:
Sum=0+1+2+3+⋯+606;
3. Why do we use the formula n x (n+1) / 2*
The assembly code illustrates a loop that cycles through numbers and sums them up. The result in eax derives from this function, which essentially calculates the sum of an aggregation of numbers.
The formula n x (n+1) / 2 is used here since it is an effective means of calculating the summation of the first n integers, with n being the last number in the series (in this case, 606). Instead of performing each number and adding them separately, the formula calculates the answer directly without the need for a loop.
In the assembly code, the loop is adding numbers to the stack many times and comparing the sum each time. But the formula is doing the same thing more efficiently. The code’s loop is merely computing this sum step by step, and the formula n x (n+1) / 2 is an efficient method of performing those same steps.
Why are the values inside eax
and rax
the same?
In the x86-64 architecture, rax
is a 64-bit register, while eax
represents the lower 32 bits of rax
. This means:
rax
= 64 bits (8 bytes)eax
= 32 bits (4 bytes) — the lower 32 bits ofrax
When a 32-bit value is moved into eax
, it automatically affects the lower 32 bits of rax
. For example:
mov eax, 0x12345678
This instruction results in:
eax = 0x12345678
rax = 0x0000000012345678

As you can see, the lower 32 bits of rax are the same as the value in eax, while the upper 32 bits of rax are zeroed out.
Why are the values the same?
The reason the values in eax and rax are the same is that eax is really the low 32 bits of the 64-bit rax register. When you modify eax, the low 32 bits of rax are modified and the high 32 bits of rax are not (and are typically zeroed out in this case).
At this point, we’ve covered the basics of using GDB for binary analysis.
There are more commands you can explore, such as step
, next
, continue
, and finish
, which will help you navigate through the code while debugging. Here’s a brief overview with basic examples:
next
orn
: Proceed to the next line of execution without stepping into a function call in the current line.
Example:
(gdb) next
This will execute the current line and move to the next line, without stepping into any function calls.
If you are at a call to a function and you invoke next, the debugger will execute the function and proceed to the next line in the current function. The function executes entirely, and you won’t see what goes on within it unless you decide to step into it.
Effect: If you’re at the line:
int result = add(2, 3); // Function call
After using next, the function add(2, 3) will execute, and the debugger will stop at the line following this one, skipping the function’s internal code.
step or s: Step into the function call on the current line, even if there is no breakpoint for that function. Example:
(gdb) step
This will resume execution and stop at the next breakpoint or when the program finishes.
If you’re at a function call and you use step, the debugger will enter the function, allowing you to see the details of what happens inside it line by line. This is useful for inspecting the execution of a function.
Effect: If you’re at the line:
int result = add(2, 3); // Function call
After using step
, the debugger will enter the add
function and stop at the first line of the function, allowing you to see the internal code execution.
finish: Finish the execution of the current function and stop at the next line of the calling function. Example:
(gdb) finish
This will complete the current function’s execution and return to the calling function.
Conditional Breakpoints
You can set a conditional breakpoint in GDB, which will only break when a specific condition is met.
The basic command syntax for a conditional breakpoint is:
(gdb) break <location> if <condition>
For example, if you want to break at line 20 only when the value of eax is equal to 5:
(gdb) break 20 if $eax == 5
This will set a breakpoint on line 20, but it will only be activated if the condition** $eax == 5 is true.**
You also use conditional breakpoints to stop the program at certain locations when a condition is met, giving you more debugging capabilities. But these are merely the basics, and we’ll use them more in the other binary analysis challenges.
As for this blog post, i tried to cover the basics of GDB and how to use it to debug a simple C++ program. I also provided a brief overview of the GDB commands and their functionalities.
References:
Would you like to support me?
If you find my content helpful and would like to support my work, consider visiting my Patreon page.
Your support means the world to me and helps keep this work going!