在上一部分我們學習了關于 dwarf 的資訊,以及它如何被用于讀取變量和将被執行的機器碼與我們的進階語言的源碼聯系起來。在這一部分,我們将進入實踐,實作一些我們調試器後面會使用的 dwarf 原語。我們也會利用這個機會,使我們的調試器可以在命中一個斷點時列印出目前的源碼上下文。
系列文章索引
随着後面文章的釋出,這些連結會逐漸生效。
<a href="http://os.51cto.com/art/201706/543147.htm">準備環境</a>
<a href="http://os.51cto.com/art/201706/543616.htm">斷點</a>
<a href="http://os.51cto.com/art/201707/544074.htm">寄存器和記憶體</a>
<a href="http://os.51cto.com/art/201707/545736.htm">elves 和 dwarves</a>
<a href="https://blog.tartanllama.xyz/c++/2017/04/24/writing-a-linux-debugger-source-signal/">源碼和信号</a>
<a href="https://blog.tartanllama.xyz/c++/2017/05/06/writing-a-linux-debugger-dwarf-step/">源碼級逐漸執行</a>
源碼級斷點
調用棧展開
讀取變量
下一步
設定我們的 dwarf 解析器
正如我在這系列文章開始時備注的,我們會使用 libelfin 來處理我們的 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 entry)以及函數 dwarf 資訊條目(function die)的函數。我們從 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) {
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;
//如果我們在目前行則輸出光标
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; /* 系統調用嘗試次數
unsigned int si_arch; /* 嘗試系統調用的架構
我隻需要使用 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;
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:
std::cout << "unknown sigtrap code " << info.si_code << std::endl;
這裡有一大堆不同風格的信号你可以處理。檢視 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();
測試
現在你應該可以在某個位址設定斷點,運作程式然後看到列印出了源碼,而且正在被執行的行被光标标記了出來。
後面我們會添加設定源碼級别斷點的功能。同時,你可以從這裡擷取該博文的代碼。
作者:simon brand
來源:51cto