Back to blog
Mar 02, 2025
17 min read

GDB Basics

The GNU Debugger

What’s GDB?

Flag

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:

CommandAliasDescription
bbreakSets a breakpoint at a specific line, function, or memory address.
rrunStarts the program execution.
ccontinueResumes the program execution.
nnextExecutes the next line of code without stepping into functions.
sstepSteps into the next function call.
pprintPrints the value of a variable.
qquitExits the debugger.
infoDisplays information about the program, breakpoints, and more.
x/4xbExamines memory at a given address, printing 4 bytes in hexadecimal format.
catch syscallCatches system calls made by the program, useful for debugging syscalls.
startPuts 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
Flag

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.

FeatureAT&T Syntax (Linux)Intel Syntax (Windows, MASM)
Operand OrderOP src, dst (source first)OP dst, src (destination first)
Register Prefix%eax, %rbpeax, rbp
Immediate Values$0x10 (uses $ for constants)0x10
Memory Access(%rbp), -4(%rbp)[rbp], [rbp-4]
Indirect Memorymov (%eax), %ebxmov ebx, [eax]
Instruction Sizemovl, 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 Assembly

The 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 the MOV 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

Flag

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 value 0x1e0da.
  • 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 of rax

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

Flag

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 or n: 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!