One, foreword
This article to talk about the famous GDB, its rich family background we do not mention, and its brother GCC is born with a golden key, in the GNU family status is unshakable. Every embedded developer has used GDB to debug a program. If you say you haven’t used GDB, then your development experience has not been rough enough and you need to continue to be bugged.
We all know that when compiling with GCC, you can use the -g option to embed more debugging information in the executable. What debugging information is embedded? How does this debugging information interact with binary instructions? During debugging, how to obtain the context information in the function call stack in the debugging information?
In response to these doubts, Doug uses two articles to thoroughly describe the deepest problems at the bottom, so that you can enjoy watching at once.
The first article, which is the current one, mainly introduces the underlying debugging principles of GDB. Let’s take a look at the mechanism by which GDB controls the execution order of the debugged program.
In the second article, we chose a small, well-integrated LUA language to analyze everything from source code analysis to function call stacks, instruction sets to debugging library modifications.
It’s a lot of content, and it may take a little longer to finish. For your health, reading this while squatting is not recommended.
Second, GDB debugging model
GDB debugging consists of two programs: GDB program and debugged program. According to whether the two programs are running on the same computer, the debugging model of GDB can be divided into two types:
- Local debugging
- Remote debugging
Local debugging: The debugger and the debugger run on the same computer.
Remote debugging: The debugger runs on one computer and the debugger runs on another.
The visual debugger is beside the point; it’s just a shell that encapsulates GDB. We can either use the black terminal window to manually enter debugging commands; You can also choose an integrated development environment (IDE), which has built-in debugger, so you can use various buttons instead of typing debugging commands manually.
Compared to local debugging, remote debugging includes a GdbServer program, which runs on the target machine, either an x86 computer or an ARM board. The red line in the figure indicates that GDB communicates with GdbServer through a network or serial port. GDB Remote Serial Protocol(GDB) : RSP Protocol(RSP) : GDB Remote Serial Protocol(GDB)
We don’t need to worry about the specific format and content of the communication protocol, just know that they are all strings, with a fixed start character (‘$’) and end character (‘#’), and two hexadecimal ASCII characters as the checksum. That’s enough. As for more details, if really idle XX can take a peek, in fact, these agreements, like all kinds of weird rules in society, are a group of experts in the toilet to think out.
In the second LUA article, we’ll implement a similar remote debugging prototype. The communication protocol is also a string, directly after the HTTP protocol to simplify the use of the very clear, convenient.
Three, GDB debugging instruction
For the sake of integrity, here part of the GDB debugging instructions posted, there is a perceptual understanding.
In addition, not all the instructions are listed here. The instructions listed are common and easier to understand. As we walk through LUA, we’ll select some of these instructions to compare in detail, including the underlying implementation mechanism.
Every debug instruction has a number of command options, such as breakpoints related to: set breakpoints, remove breakpoints, conditional breakpoints, temporary disable enable, and so on. The focus of this article is to understand the underlying debugging mechanisms of GDB, so the application layer instructions are not listed. There are many resources available on the network.
The relationship between GDB and the debugger
For the sake of description, write a very simple C program:
#include <stdio.h>
int main(int argc, char *argv[])
{
int a = 1;
int b = 2;
int c = a + b;
printf("c = %d \n", c);
return 0;
}
Copy the code
Compile command:
$ gcc -g test.c -o test
We debug the executable test by typing the command:
$ gdb ./test
The output is as follows:
In the last line, you can see the cursor flashing. This is the GDB program waiting for us to give it a debugging command.
When the black terminal window above executes GDB./test, a lot of complicated things happen in the operating system:
The system first starts the GDB process, which calls the system function fork() to create a child process that does two things:
- Call the system function ptrace(PTRACE_TRACEME, [other arguments]);
- The executable program test is loaded and executed through execc, and the test program starts executing in this child process.
One additional point: sometimes referred to as a program, sometimes as a process. “Program” describes a static concept, which is a bunch of data lying on the hard disk, while “process” describes a dynamic process, which is a task control block (a data structure) in the operating system that is used to manage the process after the program is read and loaded into memory.
It’s the system call ptrace (whose arguments are explained later) that gives GDB its powerful debugging capabilities. The function prototype is:
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
Copy the code
Let’s take a look at the description of this function in man:
Tracer is a debugger and can be understood as a GDB program. Tracee is the debugger and corresponds to the target program test in the diagram. The employer is the employer and the employee is the employee. The employer is the employer and the employee is the employee.
The ptrace system function is a system call provided by the Linux kernel for process tracing through which one process (GDB) can read and write instruction space, data space, stack, and register values from another process (test). In addition, the GDB process takes over all signals from the test process. In other words, all signals sent by the system to the test process are received by the GDB process. In this way, the execution of the test process is controlled by GDB, thus achieving the purpose of debugging.
That is, without GDB debugging, the operating system interacts directly with the target process; If use GDB to debug the program, then the operating system will be sent to the process of target signal GDB intercepted, GDB is determined according to the signal properties: continue to run when the target is to transfer the intercepted signal to target program, so that the target program is under the command from the GDB signal to the corresponding action.
How does GDB debug the service process that has been executed
Is it possible to use GDB to debug a service process that is already in execution? The answer: Yes. This relates to the first parameter of the ptrace system function, which is an enum type value, of which two are important: PTRACE_TRACEME and PTRACE_ATTACH<.
If you want to send a signal to the GDB process, please send it directly to the GDB process. If you want to send a signal to the GDB process, please send it directly to the GDB process. If you want to send a signal to the GDB process, please send it to the GDB process.
To debug an already executed process B, call ptrace(PTRACE_ATTACH,[other parameter]) in the parent process of GDB. At this point, GDB will attach(attach) to the already executed process B, and GDB will adopt it as its own child. The behavior of child process B is equivalent to a PTRACE_TRACEME operation. The GDB process sends a SIGSTO signal to child process B. After receiving the SIGSTOP signal, child process B suspends execution and enters the TASK_STOPED state, indicating that it is ready to be debugged.
So, whether you are debugging a new program or a service program that is already in execution, the final result of the ptrace system call is: GDB program is the parent process, the debug program is a child process, all the signals of the child process are taken over by the parent process GDB, and the parent process GDB can view and modify the internal information of the child process, including: stack, register, etc.
There are a few restrictions about binding: self-binding is not allowed, binding to the same process more than once is not allowed, and binding to process 1 is not allowed.
Six, peeping GDB how to achieve breakpoint instruction
With that said, let’s take a peek inside GDB by setting a break point. Using the same code as above, here is the code again:
#include <stdio.h>
int main(int argc, char *argv[])
{
int a = 1;
int b = 2;
int c = a + b;
printf("c = %d \n", c);
return 0;
}
Copy the code
To see what the compiled disassembly code looks like, compile the instruction:
gcc -S test.c; cat test.S)
Only a portion of the disassembly code is posted here, as long as it illustrates the underlying principles, it will serve our purpose.
After GDB./test is executed, GDB will fork out a child process. This child process will first call ptrace and then run the test program.
We put the source code and assembly code together for easy understanding:
In the debug window, enter the breakpoint instruction “break 5”, while GDB does two things:
- Line 10 assembly code corresponding to line 5 source code is stored in a list of breakpoints.
- At line 10 of assembly code, insert interrupt instruction INT3, that is: line 10 in assembly code is replaced with INT3.
The PC pointer (an internal pointer that points to the line to be executed) in the assembly code executes line 10, but it is INT3, so the operating system sends a SIGTRAP signal to the test process.
At this point, line 10 of assembly code has been executed, and the PC pointer points to line 11.
As mentioned above, any signal sent by the operating system to test is taken over by GDB, which means that GDB first receives the SIGTRAP signal. GDB finds that the current assembly code is executing line 10, so it looks up the list of breakpoints and finds that line 10 is stored, indicating that the breakpoint is set on line 10. So GDB does two more operations:
- Replace line 10 “INT3” in assembly code with the original code in the breakpoint list.
- Set the PC pointer back one step to point to line 10.
GDB then continues to wait for debugging instructions from the user.
At this point, the next instruction to execute is line 10 in assembly code and line 5 in source code. From the debugger’s point of view, the debugger pauses at the breakpoint on line 5, at which point we can continue to enter other debugging commands to debug, such as: view variable values, view stack information, modify local variable values, and so on.
Seven, peeping GDB how to achieve a single step instruction next
Again using the source code and assembly code example, suppose the program stops at line 6 of the source code and line 11 of the assembly code:
Step next in the debug window. Our goal is to execute a line of code, that is, to finish executing line 6 of the source code, and then stop at line 7. When GDB receives the next execution, it will calculate the source code line 7, which should correspond to line 14 of assembly code, so GDB will control the PC pointer in assembly code to execute until the end of line 13, that is, when THE PC points to line 14, it will stop, and then continue to wait for the user to input debugging instructions.
Eight, summary
With the break and next debugging instructions, we have seen how debugging instructions are handled in GDB. Of course, there are many debugging instructions in GDB, including more complex stack information, changing the value of variables, and so on, interested partners can continue to follow.
I’ll discuss this in more detail later when I write a debug library in LUA, which is smaller and simpler. I will also show you how to set PC Pointers in LUA code, so that we have a better understanding of the internal implementation of a programming language, and maybe record a video, so that we can better explain the internal details of LUA.
I will continue to summarize the actual experience in the development process of embedded projects in the public IOT town, I believe you will not be disappointed!