Translated tech/20170424 Writing a Linux Debugger Part 5 Source and signals.md

This commit is contained in:
ictlyh 2017-06-09 15:06:52 +08:00
parent ad5d19873c
commit 8da8425fb4
No known key found for this signature in database
GPG Key ID: 6861F1F0C29F118D
2 changed files with 297 additions and 302 deletions

View File

@ -1,302 +0,0 @@
ictlyh Translating
Writing a Linux Debugger Part 5: Source and signals
============================================================
In the the last part we learned about DWARF information and how it can be used to read variables and associate our high-level source code with the machine code which is being executed. In this part well put this into practice by implementing some DWARF primitives which will be used by the rest of our debugger. Well also take this opportunity to get our debugger to print out the current source context when a breakpoint is hit.
* * *
### Series index
These links will go live as the rest of the posts are released.
1. [Setup][1]
2. [Breakpoints][2]
3. [Registers and memory][3]
4. [Elves and dwarves][4]
5. [Source and signals][5]
6. [Source-level stepping][6]
7. Source-level breakpoints
8. Stack unwinding
9. Reading variables
10. Next steps
* * *
### Setting up our DWARF parser
As I noted way back at the start of this series, well be using [`libelfin`][7] to handle our DWARF information. Hopefully you got this set up in the first post, but if not, do so now, and make sure that you use the `fbreg` branch of my fork.
Once you have `libelfin` building, its time to add it to our debugger. The first step is to parse the ELF executable were given and extract the DWARF from it. This is very easy with `libelfin`, just make these changes to `debugger`:
```
class debugger {
public:
debugger (std::string prog_name, pid_t pid)
: m_prog_name{std::move(prog_name)}, m_pid{pid} {
auto fd = open(m_prog_name.c_str(), O_RDONLY);
m_elf = elf::elf{elf::create_mmap_loader(fd)};
m_dwarf = dwarf::dwarf{dwarf::elf::create_loader(m_elf)};
}
//...
private:
//...
dwarf::dwarf m_dwarf;
elf::elf m_elf;
};
```
`open` is used instead of `std::ifstream` because the elf loader needs a UNIX file descriptor to pass to `mmap` so that it can just map the file into memory rather than reading it a bit at a time.
* * *
### Debug information primitives
Next we can implement functions to retrieve line entries and function DIEs from PC values. Well start with `get_function_from_pc`:
```
dwarf::die debugger::get_function_from_pc(uint64_t pc) {
for (auto &cu : m_dwarf.compilation_units()) {
if (die_pc_range(cu.root()).contains(pc)) {
for (const auto& die : cu.root()) {
if (die.tag == dwarf::DW_TAG::subprogram) {
if (die_pc_range(die).contains(pc)) {
return die;
}
}
}
}
}
throw std::out_of_range{"Cannot find function"};
}
```
Here I take a naive approach of just iterating through compilation units until I find one which contains the program counter, then iterating through the children until we find the relevant function (`DW_TAG_subprogram`). As mentioned in the last post, you could handle things like member functions and inlining here if you wanted.
Next is `get_line_entry_from_pc`:
```
dwarf::line_table::iterator debugger::get_line_entry_from_pc(uint64_t pc) {
for (auto &cu : m_dwarf.compilation_units()) {
if (die_pc_range(cu.root()).contains(pc)) {
auto &lt = cu.get_line_table();
auto it = lt.find_address(pc);
if (it == lt.end()) {
throw std::out_of_range{"Cannot find line entry"};
}
else {
return it;
}
}
}
throw std::out_of_range{"Cannot find line entry"};
}
```
Again, we simply find the correct compilation unit, then ask the line table to get us the relevant entry.
* * *
### Printing source
When we hit a breakpoint or step around our code, well want to know where in the source we end up.
```
void debugger::print_source(const std::string& file_name, unsigned line, unsigned n_lines_context) {
std::ifstream file {file_name};
//Work out a window around the desired line
auto start_line = line <= n_lines_context ? 1 : line - n_lines_context;
auto end_line = line + n_lines_context + (line < n_lines_context ? n_lines_context - line : 0) + 1;
char c{};
auto current_line = 1u;
//Skip lines up until start_line
while (current_line != start_line && file.get(c)) {
if (c == '\n') {
++current_line;
}
}
//Output cursor if we're at the current line
std::cout << (current_line==line ? "> " : " ");
//Write lines up until end_line
while (current_line <= end_line && file.get(c)) {
std::cout << c;
if (c == '\n') {
++current_line;
//Output cursor if we're at the current line
std::cout << (current_line==line ? "> " : " ");
}
}
//Write newline and make sure that the stream is flushed properly
std::cout << std::endl;
}
```
Now that we can print out source, well need to hook this into our debugger. A good place to do this is when the debugger gets a signal from a breakpoint or (eventually) single step. While were at this, we might want to add some better signal handling to our debugger.
* * *
### Better signal handling
We want to be able to tell what signal was sent to the process, but we also want to know how it was produced. For example, we want to be able to tell if we just got a `SIGTRAP` because we hit a breakpoint, or if it was because a step completed, or a new thread spawned, etc. Fortunately, `ptrace` comes to our rescue again. One of the possible commands to `ptrace` is `PTRACE_GETSIGINFO`, which will give you information about the last signal which the process was sent. We use it like so:
```
siginfo_t debugger::get_signal_info() {
siginfo_t info;
ptrace(PTRACE_GETSIGINFO, m_pid, nullptr, &info);
return info;
}
```
This gives us a `siginfo_t` object, which provides the following information:
```
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count;
POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
void *si_lower; /* Lower bound when address violation
occurred (since Linux 3.19) */
void *si_upper; /* Upper bound when address violation
occurred (since Linux 3.19) */
int si_pkey; /* Protection key on PTE that caused
fault (since Linux 4.6) */
void *si_call_addr; /* Address of system call instruction
(since Linux 3.5) */
int si_syscall; /* Number of attempted system call
(since Linux 3.5) */
unsigned int si_arch; /* Architecture of attempted system call
(since Linux 3.5) */
}
```
Ill just be using `si_signo` to work out which signal was sent, and `si_code` to get more information about the signal. The best place to put this code is in our `wait_for_signal` function:
```
void debugger::wait_for_signal() {
int wait_status;
auto options = 0;
waitpid(m_pid, &wait_status, options);
auto siginfo = get_signal_info();
switch (siginfo.si_signo) {
case SIGTRAP:
handle_sigtrap(siginfo);
break;
case SIGSEGV:
std::cout << "Yay, segfault. Reason: " << siginfo.si_code << std::endl;
break;
default:
std::cout << "Got signal " << strsignal(siginfo.si_signo) << std::endl;
}
}
```
Now to handle `SIGTRAP`s. It suffices to know that `SI_KERNEL` or `TRAP_BRKPT` will be sent when a breakpoint is hit, and `TRAP_TRACE` will be sent on single step completion:
```
void debugger::handle_sigtrap(siginfo_t info) {
switch (info.si_code) {
//one of these will be set if a breakpoint was hit
case SI_KERNEL:
case TRAP_BRKPT:
{
set_pc(get_pc()-1); //put the pc back where it should be
std::cout << "Hit breakpoint at address 0x" << std::hex << get_pc() << std::endl;
auto line_entry = get_line_entry_from_pc(get_pc());
print_source(line_entry->file->path, line_entry->line);
return;
}
//this will be set if the signal was sent by single stepping
case TRAP_TRACE:
return;
default:
std::cout << "Unknown SIGTRAP code " << info.si_code << std::endl;
return;
}
}
```
There are a bunch of different signals and flavours of signals which you could handle. See `man sigaction` for more information.
Since we now correct the program counter when we get the `SIGTRAP`, we can remove this coded from `step_over_breakpoint`, so it now looks like:
```
void debugger::step_over_breakpoint() {
if (m_breakpoints.count(get_pc())) {
auto& bp = m_breakpoints[get_pc()];
if (bp.is_enabled()) {
bp.disable();
ptrace(PTRACE_SINGLESTEP, m_pid, nullptr, nullptr);
wait_for_signal();
bp.enable();
}
}
}
```
* * *
### Testing it out
Now you should be able to set a breakpoint at some address, run the program and see the source code printed out with the currently executing line marked with a cursor.
Next time well be adding the ability to set source-level breakpoints. In the meantime, you can get the code for this post [here][8].
--------------------------------------------------------------------------------
via: https://blog.tartanllama.xyz/c++/2017/04/24/writing-a-linux-debugger-source-signal/
作者:[TartanLlama ][a]
译者:[译者ID](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]:https://www.twitter.com/TartanLlama
[1]:https://blog.tartanllama.xyz/2017/03/21/writing-a-linux-debugger-setup/
[2]:https://blog.tartanllama.xyz/c++/2017/03/24/writing-a-linux-debugger-breakpoints/
[3]:https://blog.tartanllama.xyz/c++/2017/03/31/writing-a-linux-debugger-registers/
[4]:https://blog.tartanllama.xyz/c++/2017/04/05/writing-a-linux-debugger-elf-dwarf/
[5]:https://blog.tartanllama.xyz/c++/2017/04/24/writing-a-linux-debugger-source-signal/
[6]:https://blog.tartanllama.xyz/c++/2017/05/06/writing-a-linux-debugger-dwarf-step/
[7]:https://github.com/TartanLlama/libelfin/tree/fbreg
[8]:https://github.com/TartanLlama/minidbg/tree/tut_source

View File

@ -0,0 +1,297 @@
开发 Linux 调试器第五部分:源码和信号
============================================================
在上一部分我们学习了关于 DWARF 的信息以及它如何可以被用于读取变量和将被执行的机器码和我们高层次的源码联系起来。在这一部分,我们通过实现一些我们调试器后面会使用的 DWARF 原语将它应用于实际情况。我们也会利用这个机会,使我们的调试器可以在命中一个断点时打印出当前的源码上下文。
* * *
### 系列文章索引
随着后面文章的发布,这些链接会逐渐生效。
1. [启动][1]
2. [断点][2]
3. [寄存器和内存][3]
4. [Elves 和 dwarves][4]
5. [源码和信号][5]
6. [源码级逐步执行][6]
7. 源码级断点
8. 调用栈展开
9. 读取变量
10. 下一步
译者注ELF[Executable and Linkable Format](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format "Executable and Linkable Format") 可执行文件格式DWARF一种广泛使用的调试数据格式参考 [WIKI](https://en.wikipedia.org/wiki/DWARF "DWARF WIKI")
* * *
### 设置我们的 DWARF 解析器
正如我在这系列文章开始时备注的,我们会使用 [`libelfin`][7] 来处理我们的 DWARF 信息。希望你已经在第一部分设置好了这些,如果没有的话,现在做吧,确保你使用我仓库的 `fbreg` 分支。
一旦你构建好了 `libelfin`,就可以把它添加到我们的调试器。第一步是解析我们的 ELF 可执行程序并从中提取 DWARF 信息。使用 `libelfin` 可以轻易实现,只需要对`调试器`作以下更改:
```
class debugger {
public:
debugger (std::string prog_name, pid_t pid)
: m_prog_name{std::move(prog_name)}, m_pid{pid} {
auto fd = open(m_prog_name.c_str(), O_RDONLY);
m_elf = elf::elf{elf::create_mmap_loader(fd)};
m_dwarf = dwarf::dwarf{dwarf::elf::create_loader(m_elf)};
}
//...
private:
//...
dwarf::dwarf m_dwarf;
elf::elf m_elf;
};
```
我们使用了 `open` 而不是 `std::ifstream`,因为 elf 加载器需要传递一个 UNIX 文件描述符给  `mmap`,从而可以将文件映射到内存而不是每次读取一部分。
* * *
### 调试信息原语
下一步我们可以实现从程序计数器的值中提取行条目line entries以及函数 DWARF 信息条目function DIEs的函数。我们从 `get_function_from_pc` 开始:
```
dwarf::die debugger::get_function_from_pc(uint64_t pc) {
for (auto &cu : m_dwarf.compilation_units()) {
if (die_pc_range(cu.root()).contains(pc)) {
for (const auto& die : cu.root()) {
if (die.tag == dwarf::DW_TAG::subprogram) {
if (die_pc_range(die).contains(pc)) {
return die;
}
}
}
}
}
throw std::out_of_range{"Cannot find function"};
}
```
这里我采用了朴素的方法,迭代遍历编译单元直到找到一个包含程序计数器的,然后迭代遍历它的孩子直到我们找到相关函数(`DW_TAG_subprogram`)。正如我在上一篇中提到的,如果你想要的话你可以处理类似成员函数或者内联等情况。
接下来是 `get_line_entry_from_pc`
```
dwarf::line_table::iterator debugger::get_line_entry_from_pc(uint64_t pc) {
for (auto &cu : m_dwarf.compilation_units()) {
if (die_pc_range(cu.root()).contains(pc)) {
auto &lt = cu.get_line_table();
auto it = lt.find_address(pc);
if (it == lt.end()) {
throw std::out_of_range{"Cannot find line entry"};
}
else {
return it;
}
}
}
throw std::out_of_range{"Cannot find line entry"};
}
```
同样,我们可以简单地找到正确的编译单元,然后查询行表获取相关的条目。
* * *
### 打印源码
当我们命中一个断点或者逐步执行我们的代码时,我们会想知道处于源码中的什么位置。
```
void debugger::print_source(const std::string& file_name, unsigned line, unsigned n_lines_context) {
std::ifstream file {file_name};
//获得一个所需行附近的窗口
auto start_line = line <= n_lines_context ? 1 : line - n_lines_context;
auto end_line = line + n_lines_context + (line < n_lines_context ? n_lines_context - line : 0) + 1;
char c{};
auto current_line = 1u;
//跳过 start_line 之前的行
while (current_line != start_line && file.get(c)) {
if (c == '\n') {
++current_line;
}
}
//如果我们在当前行则输出光标
std::cout << (current_line==line ? "> " : " ");
//输出行直到 end_line
while (current_line <= end_line && file.get(c)) {
std::cout << c;
if (c == '\n') {
++current_line;
//如果我们在当前行则输出光标
std::cout << (current_line==line ? "> " : " ");
}
}
//输出换行确保恰当地清空了流
std::cout << std::endl;
}
```
现在我们可以打印出源码了,我们需要将这些通过钩子添加到我们的调试器。一个实现这个的好地方是当调试器从一个断点或者(最终)逐步执行得到一个信号时。到了这里,我们可能想要给我们的调试器添加一些更好的信号处理。
* * *
### 更好的信号处理
我们希望能够得知什么信号被发送给了进程,同样我们也想知道它是如何产生的。例如,我们希望能够得知是否由于命中了一个断点从而获得一个 `SIGTRAP`,还是由于逐步执行完成、或者是产生了一个新线程,等等。幸运的是,我们可以再一次使用 `ptrace`。可以给 `ptrace` 的一个命令是 `PTRACE_GETSIGINFO`,它会给你被发送给进程的最后一个信号的信息。我们类似这样使用它:
```
siginfo_t debugger::get_signal_info() {
siginfo_t info;
ptrace(PTRACE_GETSIGINFO, m_pid, nullptr, &info);
return info;
}
```
这会给我们一个 `siginfo_t` 对象,它能提供以下信息:
```
siginfo_t {
int si_signo; /* 信号编号 */
int si_errno; /* errno 值 */
int si_code; /* 信号代码 */
int si_trapno; /* 导致生成硬件信号的陷阱编号
(大部分架构中都没有使用) */
pid_t si_pid; /* 发送信号的进程 ID */
uid_t si_uid; /* 发送信号进程的用户 ID */
int si_status; /* 退出值或信号 */
clock_t si_utime; /* 消耗的用户时间 */
clock_t si_stime; /* 消耗的系统时间 */
sigval_t si_value; /* 信号值 */
int si_int; /* POSIX.1b 信号 */
void *si_ptr; /* POSIX.1b 信号 */
int si_overrun; /* 计时器 overrun 计数;
POSIX.1b 计时器 */
int si_timerid; /* 计时器 ID; POSIX.1b 计时器 */
void *si_addr; /* 导致错误的内存地址 */
long si_band; /* Band event (在 glibc 2.3.2 和之前版本中是 int 类型) */
int si_fd; /* 文件描述符 */
short si_addr_lsb; /* 地址的最不重要位
(自 Linux 2.6.32) */
void *si_lower; /* 出现地址违规的下限 (自 Linux 3.19) */
void *si_upper; /* 出现地址违规的上限 (自 Linux 3.19) */
int si_pkey; /* PTE 上导致错误的保护键 (自 Linux 4.6) */
void *si_call_addr; /* 系统调用指令的地址
(自 Linux 3.5) */
int si_syscall; /* 系统调用尝试次数
(自 Linux 3.5) */
unsigned int si_arch; /* 尝试系统调用的架构
(自 Linux 3.5) */
}
```
我只需要使用 `si_signo` 就可以找到被发送的信号,使用 `si_code` 来获取更多关于信号的信息。放置这些代码的最好位置是我们的 `wait_for_signal` 函数:
```
void debugger::wait_for_signal() {
int wait_status;
auto options = 0;
waitpid(m_pid, &wait_status, options);
auto siginfo = get_signal_info();
switch (siginfo.si_signo) {
case SIGTRAP:
handle_sigtrap(siginfo);
break;
case SIGSEGV:
std::cout << "Yay, segfault. Reason: " << siginfo.si_code << std::endl;
break;
default:
std::cout << "Got signal " << strsignal(siginfo.si_signo) << std::endl;
}
}
```
现在再来处理 `SIGTRAP`。知道当命中一个断点时会发送 `SI_KERNEL` 或 `TRAP_BRKPT`,而逐步执行结束时会发送 `TRAP_TRACE` 就足够了:
```
void debugger::handle_sigtrap(siginfo_t info) {
switch (info.si_code) {
//如果命中了一个断点其中的一个会被设置
case SI_KERNEL:
case TRAP_BRKPT:
{
set_pc(get_pc()-1); //将程序计数器的值设置为它应该指向的地方
std::cout << "Hit breakpoint at address 0x" << std::hex << get_pc() << std::endl;
auto line_entry = get_line_entry_from_pc(get_pc());
print_source(line_entry->file->path, line_entry->line);
return;
}
//如果信号是由逐步执行发送的,这会被设置
case TRAP_TRACE:
return;
default:
std::cout << "Unknown SIGTRAP code " << info.si_code << std::endl;
return;
}
}
```
这里有一大堆不同风格的信号你可以处理。查看 `man sigaction` 获取更多信息。
由于当我们收到 `SIGTRAP` 信号时我们已经修正了程序计数器的值,我们可以从 `step_over_breakpoint` 中移除这些代码,现在它看起来类似:
```
void debugger::step_over_breakpoint() {
if (m_breakpoints.count(get_pc())) {
auto& bp = m_breakpoints[get_pc()];
if (bp.is_enabled()) {
bp.disable();
ptrace(PTRACE_SINGLESTEP, m_pid, nullptr, nullptr);
wait_for_signal();
bp.enable();
}
}
}
```
* * *
### 测试
现在你应该可以在某个地址设置断点,运行程序然后看到打印出了源码,而且正在被执行的行被光标标记了出来。
后面我们会添加设置源码级别断点的功能。同时,你可以从[这里][8]获取该博文的代码。
--------------------------------------------------------------------------------
via: https://blog.tartanllama.xyz/c++/2017/04/24/writing-a-linux-debugger-source-signal/
作者:[TartanLlama ][a]
译者:[ictlyh](https://github.com/ictlyh)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]:https://www.twitter.com/TartanLlama
[1]:https://blog.tartanllama.xyz/2017/03/21/writing-a-linux-debugger-setup/
[2]:https://blog.tartanllama.xyz/c++/2017/03/24/writing-a-linux-debugger-breakpoints/
[3]:https://blog.tartanllama.xyz/c++/2017/03/31/writing-a-linux-debugger-registers/
[4]:https://blog.tartanllama.xyz/c++/2017/04/05/writing-a-linux-debugger-elf-dwarf/
[5]:https://blog.tartanllama.xyz/c++/2017/04/24/writing-a-linux-debugger-source-signal/
[6]:https://blog.tartanllama.xyz/c++/2017/05/06/writing-a-linux-debugger-dwarf-step/
[7]:https://github.com/TartanLlama/libelfin/tree/fbreg
[8]:https://github.com/TartanLlama/minidbg/tree/tut_source