MiHoYo C++ Second Interview: How to Use GDB to Debug Unsigned Executable Programs?

MiHoYo C++ Second Interview: How to Use GDB to Debug Unsigned Executable Programs?

In the long journey of software development, debugging is undoubtedly one of the most critical weapons in a developer’s arsenal. Debugging unsigned executable programs plays an indispensable role in many scenarios. In software development, unsigned executable programs often arise from specific compilation options or the use of third-party libraries. Imagine you are involved in a large project where part of the code is provided by a third party. When the program encounters an exception, without a symbol table, you feel as if you are in a dark forest, where every function call and every variable change becomes difficult to trace. However, by debugging unsigned executable programs, you can delve into this darkness, gradually uncovering the mysteries of program execution, identifying the root cause of issues, and bringing the code back on track.

Now, turning our attention to the field of security analysis, unsigned executable programs are often closely associated with malware and reverse engineering. When security researchers face an unsigned malicious program, debugging becomes a key means to analyze its behavior and understand its attack mechanisms. Through debugging, they can observe the program’s actions during execution, analyze how it steals information and spreads, and subsequently devise effective defense strategies to protect systems and users. Given the importance of debugging unsigned executable programs, how can we use GDB, this powerful debugging tool, to accomplish this task? Next, let us step into the fascinating world of GDB debugging for unsigned executable programs.

1. Interview Question Interpretation

Question: How to use GDB to debug executable programs without debugging information?

Interpretation: When using GDB to debug executable programs without debugging information, although you cannot directly use variable names and function names as symbol information, you can still perform basic debugging operations. First, ensure that the GDB debugging tool is installed. Then, start GDB and load the executable program. In GDB, you can use the <span>break</span> command to set breakpoints (based on addresses or function offsets), the <span>run</span> command to run the program, and the <span>next</span> and <span>step</span> commands for single-step execution, and the <span>print</span> command to view memory or register contents. Although it is inconvenient, through careful observation and memory analysis, you can still trace program execution and locate issues.

Here are the steps and examples:

(1) Use gcc or clang, and add the <span>-g</span> option when compiling the program to generate debugging information.

gcc -g -o program program.c

(2) Use GDB to debug your program:

gdb program

(3) In GDB, you can set breakpoints, view variable values, single-step execution, etc.

For example, set a breakpoint:

(gdb) break 10

Start execution:

(gdb) run

Single-step execution:

(gdb) step

View variable value:

(gdb) print variable_name

Please note that without debugging information, GDB’s functionality will be limited, such as being unable to single-step, print variable values, etc.

2. GDB Basic Introduction

2.1 What is GDB

GDB, or GNU Debugger, is a powerful program debugging tool released by the GNU open-source organization, regarded as a pillar in the debugging field. Since its inception in 1986, it has continuously evolved and improved, becoming the preferred choice for many developers when debugging programs.

  • GDB official website:https://www.gnu.org/software/gdb/(https://www.gnu.org/software/gdb/)
  • Programming languages applicable to GDB: Ada / C / C++ / Objective-C / Pascal, etc.
  • GDB’s working modes: local debugging and remote debugging.

GDB is like a super detective in the world of programs, supporting various programming languages such as C, C++, Fortran, Ada, Objective-C, Go, and D. It can delve into the internals of programs, helping developers clearly understand the program’s running state through operations like setting breakpoints, single-stepping, viewing variable values, and observing memory, accurately locating errors in the program and uncovering bugs hidden deep within the code. Whether for desktop applications, server-side services, or embedded system development, GDB, with its powerful features and flexible interaction methods, provides developers with an unparalleled debugging experience. Its emergence has greatly improved the efficiency and quality of software development, allowing developers to better combat bugs.

2.2 Installation and Startup of GDB

The installation methods for GDB vary across different systems. In Linux systems, if you are using a Debian or Ubuntu-based distribution, installing GDB is as simple as picking an item off the supermarket shelf; just enter “sudo apt -y install gdb” in the terminal, and the system will automatically complete the installation. For users of RedHat, CentOS, Fedora, and other RedHat-based distributions, executing “sudo yum -y install gdb” in the terminal will easily bring GDB into your system.

On macOS, you can use the powerful package manager Homebrew to install GDB. First, ensure that you have installed Homebrew, then enter “brew install gdb” in the terminal and wait for the installation to complete. However, due to macOS’s system security settings, you may need to perform some additional configurations when using GDB, such as granting GDB relevant permissions in system preferences.

For Windows systems, although GDB is not a built-in tool, you can install it through MinGW or Cygwin. For example, with MinGW, you need to download the MinGW installation package, select the GDB component during installation, and after installation, add the MinGW installation path to the system environment variables, allowing you to conveniently use GDB in the command prompt.

Once you have successfully installed GDB, starting it is also straightforward. The most common method is to directly enter “gdb” in the terminal or command prompt. If you have multiple versions of GDB installed on your system, you can also specify the version number to start a specific version. If you want to debug a specific executable program, such as one named “test”, just enter “gdb test” to start GDB and load that program for debugging. Additionally, you can pass some parameters when starting GDB to meet different debugging needs, such as “gdb –args test arg1 arg2”, which allows you to pass “arg1” and “arg2” as parameters when debugging the “test” program.

(1) GDB Startup Process

  1. gdb -v to check if it is installed successfully; if not, install it (make sure the compiler is installed, such as gcc).
  2. Start GDB
  3. gdb test_file.exe to start GDB debugging, directly specifying the executable file name to debug
  4. Directly enter gdb to start, and after entering GDB, use the command file test_file.exe to specify the file name
  5. If the target executable file requires input parameters (such as argv[] receiving parameters), you can specify parameters in three ways:
  6. When starting GDB, gdb –args text_file.exe
  7. After entering GDB, run set args param_1
  8. After entering GDB debugging, run param_1 or start para_1

(2) GDB Functions

  • Start the program, allowing it to run according to user-defined requirements.
  • Allows the debugged program to stop at user-specified breakpoints (breakpoints can be conditional expressions).
  • When the program is stopped, you can check what is happening in the program at that moment. For example, you can print the value of variables.
  • Dynamically change the execution environment of the program.

(3) Using GDB

① Run the program

run (r) to run the program; if you want to add parameters, it is run arg1 arg2 ...

② View source code

list (l): view the last ten lines of source code
list fun: view the source code of the fun function
list file:fun: view the source code of the fun function in the file

③ Set breakpoints and watchpoints

break line_number/fun to set a breakpoint.
break file:line_number/fun to set a breakpoint.
break if<condition>: program stops when the condition is met.
info break (abbreviated: i b): view breakpoints.
watch expr: program stops when the value of expr changes.
delete n: delete the breakpoint.

④ Single-step debugging

continue (c): run to the next breakpoint.
step (s): single-step trace, enter the function, similar to step in in VC.
next (n): single-step trace, do not enter the function, similar to step out in VC.
finish: run the program until the current function returns, printing the stack address and return value and parameter values when the function returns.
until: when tired of single-stepping through a loop, this command can run the program until exiting the loop.

⑤ View runtime data

print (p): view runtime variables and expressions.
type: view types.
print array: print all elements of the array.
print *array@len: view dynamic memory. len is the number of elements in the array.
print x=5: change runtime data.

2.3 Differences Between Regular GDB Debugging and Unsigned Program Debugging

(1) Review of Symbolic Debugging Process

In the regular process of symbolic debugging, when we have an executable program containing a symbol table, it is like having a detailed map where every detail is clearly visible. Suppose we are debugging a C language program; first, we compile it using the gcc command with the “-g” option, such as “gcc -g -o my_program my_program.c”, so that the generated executable file contains rich symbol information.

When we start GDB and load this executable program, we can easily set breakpoints by line number or function name. For example, if we know that a key logic in the program is at line 50 of the “my_program.c” file, we can enter “break my_program.c:50” in GDB to set a breakpoint precisely at that line. If we want to set a breakpoint at the entry of a function, such as the “calculate_result” function, we just need to enter “break calculate_result”.

After setting the breakpoints, we use the “run” command to run the program, and the program will pause execution at the breakpoints we set. At this point, we can use the “print” command to view the value of variables, such as “print variable_name”, to clearly understand the state of the variable at that moment. We can also use the “next” command to single-step execute the next line of code or use the “step” command to enter the function and explore the execution details of the function. Through these operations, we can gradually troubleshoot issues in the program and find the root of errors.

(2) Challenges in Debugging Unsigned Programs

However, when faced with unsigned executable programs, the situation becomes much more complicated, like wandering in an unfamiliar city without a map. Due to the lack of a symbol table, we cannot directly set breakpoints by line number or function name. Imagine the same C language program; if the “-g” option was not used during compilation, the generated unsigned executable program is like a black box, making it difficult to peek into its internal structure and logic.

We can no longer enter “break my_program.c:50” or “break calculate_result” to set breakpoints because GDB cannot recognize these line numbers and function names. When viewing variable values, it also becomes challenging due to the lack of symbol information, making it impossible to directly view their values by variable names. This requires us to adopt some special methods and techniques to overcome these difficulties and achieve effective debugging of unsigned executable programs.

2.4 Core File Debugging

(1) Core File

When a program crashes, a file called <span>core</span> is generally generated. The core file records the memory image of the program at the time of the crash and includes debugging information; the process of generating the core file is called <span>core dump</span>. The system does not generate this file by default.

(2) Setting Up Core File Generation

  • <span>ulimit -c: check core-dump status.</span>
  • <span>ulimit -c xxxx: set the size of the core file.</span>
  • <span>ulimit -c unlimited: unlimited size for core files.</span>

(3) GDB Debugging Core Files

After setting ulimit -c xxxx, if the program encounters a segmentation fault again, a core file will be generated. Use gdb core to debug the core file, and use the bt command to print the stack trace information.

3. Common GDB Commands

GDB is a commonly used tool for debugging programs in C, C++, and other programming languages, equipped with rich debugging functions such as setting breakpoints, single-stepping, and viewing variables. Here are some common commands:

3.1 Startup and Exit Commands

  • gdb <program>: start GDB and load the executable file named <program>. Additionally, you can use gdb <program> core to debug the associated core file or gdb <program> <PID> to debug a specified process.
  • q: quit, used to exit the GDB debugging environment.

3.2 Viewing Source Code

  • 1 or list: by default, display 10 lines of program source code.
  • 1<line number>: display 10 lines of code centered around the specified line number.
  • 1<function name>: display the source code of the corresponding function.

3.3 Breakpoint Debugging

(1) Setting Breakpoints:

  • a, break + [source code line number][source code function name][memory address]
  • b, break … if condition … can be any of the above parameters, condition is the condition. For example, in a loop, you can set break … if i = 100 to set the loop count.

(2) Deleting Breakpoints

  • (gdb) clear location: the location parameter is usually the line number of a certain line of code or a specific function name. When the location parameter is a function name, it indicates deleting all breakpoints at the entrance of that function.
  • (gdb) delete [breakpoints] [num]: the breakpoints parameter is optional, and the num parameter is the specified breakpoint number, which can delete a specific breakpoint rather than all.

(3) Disabling Breakpoints

disable [breakpoints] [num…]: the breakpoints parameter is optional; num… indicates multiple parameters, each of which is the number of the breakpoint to be disabled. If num… is specified, the disable command will disable the specified numbered breakpoints; otherwise, if num… is not set, disable will disable all breakpoints in the current program.

(4) Activating Breakpoints

  • enable [breakpoints] [num…] activates multiple breakpoints specified by num…, and if num… is not set, it activates all disabled breakpoints.
  • enable [breakpoints] once num… temporarily activates multiple breakpoints numbered num…, but the breakpoints can only be used once and will automatically return to the disabled state afterward.
  • enable [breakpoints] count num… temporarily activates multiple breakpoints numbered num…, and the breakpoints can be used count times before entering the disabled state.
  • enable [breakpoints] delete num… activates multiple breakpoints numbered num…, but the breakpoints can only be used once and will be permanently deleted afterward.

break(b): sets a normal breakpoint, and there are two forms of setting breakpoints

(gdb) break location // b location, location represents the position to set the breakpoint

MiHoYo C++ Second Interview: How to Use GDB to Debug Unsigned Executable Programs?
(gdb) break ... if cond // b .. if cond, indicates that if cond condition is true, set a breakpoint at "..."

By using the condition command to set conditional expressions for different types of breakpoints, the corresponding breakpoint will only trigger and pause the program when the condition expression is satisfied (value is True).

tbreak: the tbreak command can be seen as another version of the break command; tbreak and break commands are very similar in usage and function, with the only difference being that the breakpoint set using the tbreak command will only take effect once, and after the program pauses, that breakpoint will automatically disappear.

rbreak: unlike break and tbreak commands, the rbreak command targets functions in C and C++ programs, setting a breakpoint at the beginning of the specified function.

(gdb) tbreak regex
  • regex represents a regular expression, which will set a breakpoint at the beginning of the matched function.
  • The breakpoint set by the tbreak command has the same effect as the break command and will persist, not automatically disappearing.

watch: this command sets a watchpoint, which can monitor the value of a variable or expression. The program will only stop running when the monitored variable (expression) value changes.

(gdb) watch cond
  • cond represents the variable or expression to be monitored.
  • rwatch command: the program will stop running whenever the target variable (expression) value is read;
  • awatch command: the program will stop running whenever the target variable (expression) value is read or changed.

catch: the purpose of the catch breakpoint is to monitor the occurrence of a certain event in the program, such as when a certain exception occurs, or when a dynamic library is loaded, etc. Once the target event occurs, the program will stop executing.

(5) Observing Breakpoints:

  • a, watch + [variable][expression] when the value of the variable or expression changes, the program will stop.
  • b, rwatch + [variable][expression] when the variable or expression is read, the program will stop.
  • c, awatch + [variable][expression] when the variable or expression is read or written, the program will stop.

(6) Setting Catchpoints:

catch + event when the event occurs, the program will stop.

event can be one of the following:

  • a, throw an exception thrown by C++ (throw is a keyword).
  • b, catch an exception caught by C++ (catch is a keyword).
  • c, exec when the system call exec is invoked (exec is a keyword, currently this function is only useful under HP-UX).
  • d, fork when the system call fork is invoked (fork is a keyword, currently this function is only useful under HP-UX).
  • e, vfork when the system call vfork is invoked (vfork is a keyword, currently this function is only useful under HP-UX).
  • f, load or load when loading a shared library (dynamic link library) (load is a keyword, currently this function is only useful under HP-UX).
  • g, unload or unload when unloading a shared library (dynamic link library) (unload is a keyword, currently this function is only useful under HP-UX).

(7) Capturing Signals:

handle + [argu] + signals

signals: are signals defined by Linux/Unix, SIGINT represents the interrupt character signal, which is the signal of Ctrl+C, SIGBUS represents the signal of hardware failure; SIGCHLD represents the signal of child process state change; SIGKILL represents the signal to terminate the program, etc.

argu:

  • nostop when the debugged program receives a signal, GDB will not stop the program’s execution but will print a message informing you that this signal has been received.
  • stop when the debugged program receives a signal, GDB will stop your program.
  • print when the debugged program receives a signal, GDB will display a message.
  • noprint when the debugged program receives a signal, GDB will not inform you of the received signal.
  • pass or noignore when the debugged program receives a signal, GDB does not handle the signal. This means that GDB will pass this signal to the debugged program for handling.
  • nopass or ignore when the debugged program receives a signal, GDB will not allow the debugged program to handle this signal.

(8) Thread Interruption:

break [linespec] thread [threadno] [if ...]
  • linespec is the line number where the breakpoint is set in the source code. For example: test.c:12 indicates setting a breakpoint at line 12 in the file test.c.
  • threadno is the thread ID. It is assigned by GDB, and you can view the thread information of the running program by entering info threads.
  • if … sets the interruption condition.

View information:

① View data:

  • print variable to view the variable
  • print *array@len to view the array (array is an array pointer, len is the required data length)

You can set the output format by adding parameters:

/ display the variable in hexadecimal format.
/d display the variable in decimal format.
/u display the unsigned integer in hexadecimal format.
/o display the variable in octal format.
/t display the variable in binary format.
/a display the variable in hexadecimal format.
/c display the variable in character format.
/f display the variable in floating-point format.

② View memory

examine /n f u + memory address (pointer variable)

  • n indicates the length of memory to display
  • f indicates the output format (see above)
  • u indicates the byte size specification (b for single byte; h for double byte; w for four bytes; g for eight bytes; default is four bytes)
For example: x /10cw pFilePath (pFilePath is a string pointer, pointer occupies 4 bytes)
     x is the abbreviation for the examine command.

③ View stack information

backtrace [-n][n]

  • n indicates only print the stack information of the top n layers.
  • -n indicates only print the stack information of the bottom n layers.
  • Without parameters, it indicates print all stack information.

3.4 Single-Step Debugging

run(r)

continue(c)
next(n)

Command format: (gdb) next count: count indicates how many lines of code to single-step execute, default is 1 line

The biggest feature is that when encountering a statement that includes a function call, regardless of how many lines of code are included in the function, the next instruction will execute it all at once. In other words, for the called function, the next command will only treat it as one line of code.

step(s)

  • (gdb) step count: the parameter count indicates how many lines to execute at once, default is 1 line.
  • Generally, the step command and next command have the same function, both single-step execute the program. The difference is that when the step command executes a line of code that includes a function, it will enter the function and stop at the first line of code in the function.

until(u)

(gdb) until: the unparameterized until command can quickly run the current loop and stop at the end of the loop. Note that the until command does not always work this way; it will only take effect when executing to the end of the loop (the last line of code); otherwise, the until command will function the same as the next command, just single-step executing the program.

(gdb) until location: the parameter location is the line number of a certain line of code

View variable values

print(p)

  • p num_1: the parameter num_1 is used to refer to the target variable or expression to be viewed or modified
  • Its function is to output or modify the value of the specified variable or expression during the GDB debugging process.

display

  • (gdb) display expr
  • (gdb) display/fmt expr
  • expr represents the target variable or expression to be viewed; the fmt parameter is used to specify the output format of the variable or expression
  • (gdb) undisplay num…
  • (gdb) delete display num…
  • The num… parameter represents the number of the target variable or expression, and the number can be multiple.
  • (gdb) disable display num…
  • Disable the automatically displayed variables or expressions that are currently active in the display list.
  • (gdb) enable display num…
  • Activate the currently disabled variables or expressions.
  • Like the print command, the display command is also used to view the value of a variable or expression during the debugging phase.
  • The difference is that when using the display command to view the value of a variable or expression, GDB will automatically print it every time the program pauses execution (for example, during single-step execution), while the print command will not.

GDB handle command: Signal Handling

→(gdb) handle signal mode, where the signal parameter indicates the target signal to be set, which is usually the full name of a signal (SIGINT) or the abbreviation (the part after removing ‘SIG’, such as INT); if you want to specify all signals, you can use all.The mode parameter is used to specify how GDB handles the target signal, and its value can be one of the following:

  • ostop: when the signal occurs, GDB will not pause the program, and it can continue executing, but will print a prompt message informing us that the signal has occurred;
  • stop: when the signal occurs, GDB will pause the program execution.
  • noprint: when the signal occurs, GDB will not print any prompt information;
  • print: when the signal occurs, GDB will print necessary prompt information;
  • nopass (or ignore): GDB captures the target signal while not allowing the program to handle that signal;
  • pass (or noignore): GDB debugging captures the target signal while also allowing the program to handle that signal automatically.

GDB frame and backtrace commands: View Stack Information

(gdb) frame spec this command can select the stack frame specified by the spec parameter as the current stack frame. The spec parameter can be specified in three common ways:

  • Specify by the stack frame number. 0 corresponds to the current called function’s stack frame number, and the maximum numbered stack frame usually corresponds to the main() function;
  • Specify by the stack frame address. The stack frame address can be seen in the information printed by the info frame command (which will be discussed later);
  • Specify by the function name. Note that if it is a recursive function, the specified one is the stack frame with the smallest number.

(gdb) info frame we can view the information stored in the current stack frame

This command will print the following information about the current stack frame:

  • The current stack frame number and the address of the stack frame;
  • The storage address of the function corresponding to the current stack frame and the address of the code stored when the function was called;
  • The caller of the current function and the address of the corresponding stack frame;
  • The programming language used for this stack frame;
  • The storage address and value of the function parameters;
  • The storage address of local variables in the function;
  • The register variables stored in the stack frame, such as the instruction register (represented by rip in a 64-bit environment, eip in a 32-bit environment), stack base pointer register (rbp in a 64-bit environment, ebp in a 32-bit environment), etc.

In addition, you can also use the <span>info args </span> command to view the values of the parameters of the current function; use the <span>info locals </span> command to view the values of local variables in the current function.

(gdb) backtrace [-full] [n] is used to print the information of all stack frames in the current debugging environment

Among them, the parameters enclosed in [ ] are optional, and their meanings are:

n: an integer value, when it is a positive integer, it indicates printing the information of the innermost n stack frames; when n is a negative integer, it indicates printing the information of the outermost n stack frames;

-full: while printing stack frame information, also print the values of local variables.

GDB Edit and Search Source Code

GDB edit command: Edit File

  • (gdb) edit [location]
  • (gdb) edit [filename]:[location]

location indicates the position in the program. This command activates the specified position in the file for editing.

If you encounter the error “bash: /bin/ex: No such file or directory”, because GDB’s default editor is ex, you need to specify an editor, such as export EDITOR=/usr/bin/vim or export EDITOR=/usr/bin/vi.

GDB search command: Search File

search <regexp>
reverse-search <regexp>

The first command format indicates searching backward from the start of the current line, while the latter indicates searching forward from the current line. Here, regexp is a regular expression that describes a pattern for string matching, which can be used to check whether a string contains a certain substring, replace matched substrings, or extract substrings that meet certain conditions. Many programming languages support the use of regular expressions.

Note: GDB commands can also utilize the help command to view more specific usage instructions, such as entering help breakpoints to obtain more information about breakpoint setting commands.

4. GDB Debugging Program Usage

Generally speaking, GDB mainly helps you accomplish the following four aspects:

  • 1. Start your program, allowing it to run according to your custom requirements.

  • 2. Allow the debugged program to stop at the breakpoints you specify (breakpoints can be conditional expressions).

  • 3. When the program is stopped, you can check what is happening in your program at that moment.

  • 4. Dynamically change your program’s execution environment.

From the above, it seems that GDB is no different from general debugging tools, basically accomplishing these functions, but in detail, you will find the power of GDB as a debugging tool. While many may be accustomed to graphical debugging tools, sometimes command-line debugging tools have functionalities that graphical tools cannot achieve. Let us take a look at them one by one.

A debugging example:

Source program: tst.c

1 #include <stdio.h>
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i<n; i++)
7 {
8 sum+=i;
9 }
10 return sum;
11 }
12
13
14 main()
15 {
16 int i;
17 long result = 0;
18 for(i=1; i<=100; i++)
19 {
20 result += i;
21 }
22
23 printf("result[1-100] = %d /n", result );
24 printf("result[1-250] = %d /n", func(250) );
25 }

Compile to generate the executable file: (under Linux)

hchen/test> cc -g tst.c -o tst

Using GDB for debugging:

hchen/test> gdb tst <---------- Start GDB
GNU gdb 5.1.1
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386-SUSE-linux"...
(gdb) l <-------------------- l command is equivalent to list, listing the source code from the first line.
1 #include <stdio.h>
2
3 int func(int n)
4 {
5 int sum=0,i;
6 for(i=0; i<n; i++)
7 {
8 sum+=i;
9 }
10 return sum;
11 }
12
13
14 main()
15 {
16 int i;
17 long result = 0;
18 for(i=1; i<=100; i++)
19 {
20 result += i;
(gdb) <-------------------- Pressing enter repeats the last command
21 }
22
23 printf("result[1-100] = %d /n", result );
24 printf("result[1-250] = %d /n", func(250) );
25 }

(gdb) break 16 <——————– Set a breakpoint at line 16 of the source program.

Breakpoint 1 at 0x8048496: file tst.c, line 16.

(gdb) break func <——————– Set a breakpoint at the entry of the func() function.

Breakpoint 2 at 0x8048456: file tst.c, line 5.

(gdb) info break <——————– View breakpoint information.

Num Type Disp Enb Address What
1 breakpoint keep y 0x08048496 in main at tst.c:16
2 breakpoint keep y 0x08048456 in func at tst.c:5

(gdb) r <——————— Run the program, run command abbreviation

Starting program: /home/hchen/test/tst

Breakpoint 1, main () at tst.c:17 <---------- Paused at the breakpoint.
17 long result = 0;
(gdb) n <--------------------- Execute the next statement, next command abbreviation.

(gdb) n

18 for(i=1; i<=100; i++)

(gdb) n

20 result += i;

(gdb) n

18 for(i=1; i<=100; i++)

(gdb) n

20 result += i;

(gdb) c <——————— Continue running the program, continue command abbreviation.

Continuing.
result[1-100] = 5050 <---------- Program output.

(gdb) break 2, func (n=250) at tst.c:5

5 int sum=0,i;

(gdb) n

6 for(i=1; i<=n; i++)

(gdb) p i <——————— Print the value of variable i, print command abbreviation.

$1 = 134513808

(gdb) n

8 sum+=i;

(gdb) n

6 for(i=1; i<=n; i++)

(gdb) p sum

$2 = 1

(gdb) n

8 sum+=i;

(gdb) p i

$3 = 2

(gdb) n

6 for(i=1; i<=n; i++)

(gdb) p sum

$4 = 3

(gdb) n

6 for(i=1; i<=n; i++)

(gdb) bt <——————— View function stack.

#0 func (n=250) at tst.c:5
#1 0x080484e4 in main () at tst.c:24
#2 0x400409ed in __libc_start_main () from /lib/libc.so.6

(gdb) finish <——————— Exit the function.

Run till exit from #0 func (n=250) at tst.c:5
0x080484e4 in main () at tst.c:24
24 printf("result[1-250] = %d /n", func(250) );
Value returned is $6 = 31375

(gdb) c <——————— Continue running.

Continuing.
result[1-250] = 31375 <---------- Program output.

Program exited with code 027. <——– Program exits, debugging ends.

(gdb) q <——————— Exit gdb.

hchen/test>

Alright, with the above intuitive understanding, let us systematically recognize GDB.

Basic GDB commands:

Common GDB Commands	Format	Meaning	Abbreviation
list	List [start, end]	List the code of the file	l
prit	Print variable name	Print variable content	p
break	Break [line number or function name]	Set a breakpoint	b
continue	Continue [start, end]	Continue running	c
info	Info variable name	List information	i
next	Next	Next line	n
step	Step	Enter function (step in)	S
display	Display variable name	Display parameter	 
file	File filename (can be absolute or relative path)	Load file	 
run	Run args	Run program	r

5. Debugging Unsigned Executable Programs with GDB

5.1 Preparation Work

First, let us take a simple C language program as an example. Suppose we have the following code saved in a file named example.c:

#include <stdio.h>

int main() {
    int a = 10;
    int b = 20;
    int sum = a + b;
    printf("The sum is: %d\n", sum);
    return 0;
}

When compiling, we do not use the -g parameter, directly using gcc example.c -o example to compile, thus generating example as an unsigned executable file.

5.2 Disassembling to Obtain Key Information

Next, use GDB to load this unsigned executable program, enter “gdb example” to start GDB. Then, we will disassemble the key functions in the program, taking the main function as an example, and enter the command “disassemble main” in GDB.

Executing this command will yield disassembled code similar to the following:

Dump of assembler code for function main:
   0x0804841d <+0>:     push   %ebp
   0x0804841e <+1>:     mov    %esp,%ebp
   0x08048420 <+3>:     and    $0xfffffff0,%esp
   0x08048423 <+6>:     sub    $0x20,%esp
   0x08048426 <+9>:     movl   $0xa,0x1c(%esp)
   0x0804842e <+17>:    movl   $0x14,0x18(%esp)
   0x08048436 <+25>:    mov    0x18(%esp),%eax
   0x0804843a <+29>:    mov    0x1c(%esp),%edx
   0x0804843e <+33>:    add    %edx,%eax
   0x08048440 <+35>:    mov    %eax,0x14(%esp)
   0x08048444 <+39>:    mov    0x14(%esp),%eax
   0x08048448 <+43>:    mov    %eax,0x4(%esp)
   0x0804844c <+47>:    movl   $0x8048510,(%esp)
   0x08048453 <+54>:    call   0x8048300 <printf@plt>
   0x08048458 <+59>:    mov    $0x0,%eax
   0x0804845d <+64>:    leave
   0x0804845e <+65>:    ret
End of assembler dump.

In this disassembled code, the leftmost column is the memory address of the instruction, for example, 0x0804841d is the address where the function starts. The middle column is the machine code of the instruction, represented in hexadecimal. The rightmost column is the corresponding assembly instruction, such as “push %ebp”, which serves to push the value of the ebp register onto the stack, typically used to save the stack frame information before a function call. By analyzing these assembly instructions, we can understand the execution flow of the function, such as where to start initializing variables, where to perform addition, and where to call the printf function, etc.

5.3 Setting Breakpoints Based on Addresses

Based on the addresses obtained from disassembly, we can use the “break * address” command to set breakpoints. For example, if we want to set a breakpoint before the printf function call, we can see from the disassembled code that the call 0x8048300 <printf@plt> line’s address is 0x08048453, so we enter “break *0x08048453” in GDB.

We can also set breakpoints at other key locations, such as at the instruction for addition. Suppose the address of the addition instruction is 0x0804843e, we can enter “break *0x0804843e” to set a breakpoint. By setting breakpoints at different key locations, we can pause the program when it reaches these points to observe the program’s running state.

5.4 Common Commands During Debugging

After setting the breakpoints, we can use the “run” command (abbreviated as “r”) to run the program, which will pause at the first breakpoint. When the program is paused at the breakpoint, if we want to continue executing to the next breakpoint, we can use the “continue” command (abbreviated as “c”).

Although it is challenging to directly view variable values in unsigned programs, in certain specific scenarios, we can assist in judgment by viewing memory values. For example, if we know the address of the variable sum (assumed to be obtained through analyzing the assembly code and stack frame information), we can use the “x /nfu address” command to view the value in memory. Here, “x” indicates viewing memory, “n” indicates the number of units to display, “f” indicates the display format (such as “x” for hexadecimal, “d” for decimal), and “u” indicates the size of each unit (such as “b” for byte, “w” for word, “g” for double word). For example, “x /1dw 0x memory address” can view the value of one word (4 bytes) at the specified memory address in decimal format, allowing us to infer whether the value of the variable sum is correct.

Additionally, we can use the “next” command (abbreviated as “n”) to single-step execute the next instruction without entering the function; using the “step” command (abbreviated as “s”) will single-step execute the next instruction and enter the function. These commands are very useful during debugging, helping us gradually troubleshoot issues in the program.

5.5 Common Problems and Solutions During Debugging

(1) Invalid Breakpoint Settings

When using GDB to debug unsigned executable programs, invalid breakpoint settings are a common and tricky issue. This can be caused by various reasons.

First, address errors are a common cause. In unsigned programs, we rely on the addresses obtained from disassembly to set breakpoints, but if there is a deviation in the disassembly process, or if we misunderstand and misuse the addresses, it will lead to breakpoints being set in the wrong position, thus failing to take effect. For example, in the disassembled code, the instruction address may change due to code optimization, relocation during the linking process, etc. If we do not update the address in time, the breakpoint will be ineffective.

Program optimization may also lead to instruction offsets, causing breakpoints to be invalid. When the program is compiled with optimization options enabled, the compiler performs a series of optimizations on the code, such as removing redundant code, merging instructions, rearranging instruction order, etc. These optimizations may cause the addresses obtained from disassembly based on unoptimized code to be inconsistent with the actual executing code addresses, leading to breakpoints not hitting. For instance, a breakpoint originally set at a certain line of code may become ineffective if that line of code is deleted or moved to another location after optimization.

To troubleshoot and resolve the issue of invalid breakpoint settings, we can take the following approaches. First, carefully check the disassembled code to confirm the accuracy of the addresses. We can disassemble multiple times and compare the disassembly results under different conditions to ensure that the addresses are correct. At the same time, check the compilation options of the program to see if optimization options are enabled; if so, we can try disabling optimization and recompiling the program, then set breakpoints again for debugging.

If the breakpoints are still ineffective, we can use the “info breakpoints” command to view detailed information about the breakpoints, including the breakpoint number, location, status, etc. By examining this information, we can determine whether the breakpoints are set correctly and whether there are conflicts or other issues. Additionally, we can try setting breakpoints at different locations and observe the program’s response to gradually narrow down the problem.

(2) Difficulty Viewing Variable Values

In unsigned programs, due to the lack of symbol information, directly viewing variable values becomes extremely challenging. However, we can utilize memory viewing commands to indirectly obtain relevant information about variable values.

The “x /nfu address” command in GDB is a powerful memory viewing tool. Here, “n” indicates the number of units to display, “f” indicates the display format (such as “x” for hexadecimal, “d” for decimal, “u” for unsigned decimal, etc.), and “u” indicates the size of each unit (such as “b” for byte, “w” for word, “g” for double word). For example, “x /1dw 0x memory address” can view the value of one word (4 bytes) at the specified memory address in decimal format.

When using this command, we need to infer variable values based on disassembly information and program logic. First, by analyzing the disassembled code, determine the storage location of the variable in memory. For example, in the assembly code, we can see the assignment operations of the variable and the memory read/write instructions related to the variable, allowing us to roughly determine the memory area where the variable is located.

Then, combine the program logic to judge the type and possible value range of the variable. For instance, if we know that a certain variable is used to store an integer, we can interpret the data in memory based on the storage method and size of integers. If the variable is an array, we can view the specific element’s value in the array using the memory viewing command based on the array’s definition and index.

In practical operations, we can also set multiple breakpoints to view memory values at different stages of program execution, observing changes in variable values to better understand the program’s running logic and determine whether variable values meet expectations. For example, by setting breakpoints before and after variable assignments and checking memory values, we can verify whether the variable has been correctly assigned.

6. GDB Advanced Features

6.1 Backtrace

During program debugging, understanding the order of function calls and the contextual relationships between each layer of calls is crucial. Sometimes, when a program encounters an error, we may not know which function call chain the error originated from; this is where the backtrace feature comes into play. GDB provides the backtrace command, abbreviated as bt, to display the current call stack information.

When the program runs into an exception or pauses at a breakpoint, entering the bt command will list each stack frame from shallow to deep, with each stack frame containing key information such as function name, source file name, line number, and parameter values. For example, we have a program containing multiple function calls:

#include <stdio.h>

void function_c(int num) {
    int result = num * 2;
    printf("Function C: result = %d\n", result);
}

void function_b(int num) {
    function_c(num + 1);
}

void function_a() {
    int num = 5;
    function_b(num);
}

int main() {
    function_a();
    return 0;
}

When debugging this program in GDB, if the program pauses inside the function_c function, entering the bt command may yield the following output:

(gdb) bt
#0  function_c (num=6) at test.c:5
#1  0x000000000040056d in function_b (num=5) at test.c:9
#2  0x0000000000400588 in function_a () at test.c:13
#3  0x00000000004005a4 in main () at test.c:17

From the output, we can clearly see the order of function calls: main calls function_a, function_a calls function_b, and function_b calls function_c, and we can also see the parameter values passed to each function during the calls. This is very helpful for quickly locating where the problem occurred; for example, if a division by zero error occurs in function_c, we can use the backtrace information to trace back how the parameters passed to function_c were calculated, thus finding the root of the problem.

6.2 Dynamic Memory Detection

Memory leaks, illegal accesses, and other memory issues are hidden killers of program robustness; they can lead to performance degradation or even crashes after the program runs for a while. Although there are specialized memory analysis tools like Valgrind, GDB itself also has certain memory detection capabilities, especially when combined with the heap plugin, which can perform preliminary checks on the program’s heap memory usage.

First, we need to obtain and load the heap plugin, assuming the plugin file is gdbheap.py, use the following command to load the plugin:

(gdb) source /path/to/gdbheap.py

Then, we can attach GDB to the running process (assuming the process ID is <pid>) and use the commands provided by the plugin to view the heap memory allocation status:

(gdb) attach <pid>
(gdb) monitor heap

After executing the above commands, GDB will display relevant information about the heap memory, such as the number of memory blocks, sizes, allocation status, etc. By observing this information, we can identify potential memory issues. For example, if we find that a large number of small memory blocks are allocated and not released for a long time, there may be a risk of memory leaks; if we see abnormal allocation and release order of memory blocks, there may be illegal memory access issues.

Here is a simple example demonstrating how to use GDB and the heap plugin to detect memory issues:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr1 = (int *)malloc(10 * sizeof(int));
    int *ptr2 = (int *)malloc(20 * sizeof(int));
    free(ptr1);
    // Intentionally not freeing ptr2, causing a memory leak
    return 0;
}

After running the program, using GDB and the heap plugin for detection, by analyzing the heap memory information output by the plugin, we may discover that the memory pointed to by ptr2 has not been released, thus locating the memory leak issue.

6.3 Conditional Breakpoints and Watchpoints

Conditional Breakpoints: In some complex programs, we may not want the program to pause at every breakpoint but rather only when specific conditions are met; this is where conditional breakpoints come into play. For example, in a program processing an array, we suspect that an array out-of-bounds issue occurs when the array index i exceeds the array size; we can set the following conditional breakpoint:

(gdb) break array_processing_function if i >= array_size

Thus, the program will only pause at array_processing_function when i is greater than or equal to array_size, greatly improving debugging efficiency and avoiding frequent pauses at irrelevant breakpoints, allowing us to capture the moment the problem occurs more precisely.

Watchpoints: Watchpoints are used to monitor changes in variable values. When the observed variable is modified, GDB will automatically pause the program, which is particularly useful for tracking difficult-to-reproduce intermittent issues. For example, in a multithreaded program, if a global variable’s value is unexpectedly modified, but we are unsure which thread modified it and under what circumstances, we can set a watchpoint for this global variable:

(gdb) watch global_variable

When the value of global_variable changes, the program will immediately pause; at this point, we can check the current thread status, call stack, and other information to determine how the variable was modified, thus finding the root of the problem. Additionally, we can set read watchpoints (rwatch) and read/write watchpoints (awatch); rwatch pauses the program when the target variable’s value is read, while awatch pauses the program when the target variable’s value is read or modified, allowing us to choose the appropriate type of watchpoint based on specific debugging needs.

6.4 Remote Debugging Technology

In actual development, we often encounter the need to debug programs deployed on remote servers or embedded devices. GDB supports remote debugging over the network, greatly simplifying the complexity of cross-device debugging.

The basic principle of remote debugging is to run the GDB server (gdbserver) on the remote device and connect the local GDB client to the server. The specific operation steps are as follows:

(1) On the Remote Device: First, ensure that gdbserver is installed on the remote device; you can check if it is installed by using the gdbserver –version command. Then, start gdbserver and specify the program to debug and the listening port, for example:

gdbserver :<port> /path/to/remote_program

Where <port> is an unused port number that can be specified arbitrarily based on the actual situation, and /path/to/remote_program is the path of the program to be debugged. After successful startup, gdbserver will listen on the specified port, waiting for the local GDB client to connect.

(2) On the Local GDB Client: Start GDB locally and load a local copy of the executable file that matches the remote program (ensure it is compiled with debugging information), then use the target remote command to connect to the remote gdbserver:

gdb ./local_program
(gdb) target remote <remote_host>:<port>

<remote_host> is the IP address or hostname of the remote device, and <port> is the port number specified when starting gdbserver on the remote device. After a successful connection, you can use various debugging commands in the local GDB client as if debugging a local program, such as setting breakpoints, single-stepping, viewing variable values, etc. GDB will communicate with the remote gdbserver over the network to achieve debugging of the remote program.

For example, when developing an embedded system program, we can run gdbserver on the development board (remote device) and use the GDB client on the local PC for debugging. This way, we can conveniently debug programs running on remote embedded devices in the local environment, improving development efficiency.

Leave a Comment