自己動手編寫一個Linux調試器系列之1 準備工作 by lantie@15PB
我想每個人都會編寫不止一個 hello world
程序, 并且使用調試器來調試這些程序(如果你沒有,那放下你手上的活兒,來學習使用調試器吧)。然而,盡管調試器使用如此廣泛,但卻沒有很多資料可以告訴我們它的工作原理,以及如何編寫一個調試器。特別是與編程時的其他工具技術(如編譯器)比起來。在這個系列的文章中,我們將會學習調試器的原理并編寫一個調試器去調試Linux程序。
我們將支持以下功能:
- 啟動、停止并繼續執行
- 設置各種斷點
- 內存地址
- 源代碼行
- 函數入口處
- 讀取和寫入寄存器和內存
- 單步跟蹤
- 指令
- 單步步入
- 單步跳過
- 單步步過
- 打印當前源碼位置
- 打印棧回溯信息
- 打印簡單的值信息
最后我還會概述如何將以下功能添加到編寫的調試器中:
- 遠程調試
- 共享庫和動態加載的支持
- 表達式求值
- 多線程調試的支持
我將使用C和C++來編寫這個項目,但這個項目同樣也適用于編譯成機器代碼和輸出標準的DWARF調試信息的編程語言。(如果你不知道這是什么,不要擔心,馬上就會清楚了)
此外, 我們的主要目的是在大多數情況下,使程序都能正常運行,因此健壯的錯誤處理會使編寫變得更簡單。
系列索引
- 準備工作
- 斷點
- 寄存器和內存
- ELF文件和調試信息
- 源碼和信號
- 源碼級單步
- 源碼級斷點
- 堆棧解除
- 處理變量
- 高級主題
開始設置
在我們開始討論之前,讓我們先建立環境。在本教程中,我們將使用兩個依賴項:
-
Linenoise
用于處理我們的命令行輸入 -
libelfin
用于解析調試信息。
你可以使用比較傳統的libdwarf
而不是libelfin
,但是其接口遠沒有那么好,libelfin
還提供了一個基本完整的DWARF表達式求值工具,如果您想要讀取變量的話,這將節省您很多時間。請務必您使用我的libelfin
的fbreg
分支,因為它為x86上的讀取變量提供了一些額外的支持。
一旦你在系統中安裝了這些工具,或者在你的系統上編譯了相關的依賴項,就可以開始了。我只是將它們與我的CMake文件中的其他代碼一起編譯。
啟動程序
在我們調試一個程序時,首先我們需要先系統一個要調試的程序。我們可以使用經典的 fork/exec 模式。
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "Program name not specified";
return -1;
}
auto prog = argv[1];
auto pid = fork();
if (pid == 0) {
// 我們在子進程中
// 執行要調試的程序
}
else if (pid >= 1) {
// 我們在父進程中
// 執行調試器
}
我們調用fork
會使我們的程序分為兩個進程,如果我們在子進程中fork
返回0,如果我們在父進程中,則返回子進程的進程ID。
如果我們在子進程中,我們想用我們要調試的程序替換當前正在執行的程序,從而達到調試程序的目的。
ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
execl(prog, prog, nullptr);
這里我們第一次使用ptrace
,它將在編寫調試器時成為我們最好的朋友。ptrace
允許我們通過讀取寄存器,讀取內存,單步執行等來觀察和控制另一個進程的執行。
這個API非常難看,它是一個單一的函數,其中提了一些枚舉值可以使用,還有一些參數可以根據你提供的值使用或是忽略。函數的簽名如下所示:
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);
-
request
是我們對想要去跟蹤的進程能做什么。 -
pid
是跟蹤進程的進程id。 -
addr
是內存地址,這是用在跟蹤時一些調用指定地址。 -
data
是某些請求特定的資源。 - 返回值通常會提供錯誤信息,因此需要在編寫代碼時對返回值進行檢查,更多信息可以查閱man手冊。
在上面的代碼中 request
的值是PTRACE_TRACEME
時 表面這個進程應該允許其父進程跟蹤它,所有其他參數可以被忽略,因為API設計的參數就不太重要。
下一步,我們調用 execl
,這是許多 exec
的類似函數的其中一個。我們執行給定的程序,通過它的名稱作為命令行參數和一個nullptr
終止參數列表。如果你愿意,你可以將nullptr
替換為你的程序所需的任何其他參數。
在我們完成這項工作之后,我們完成了子進程,我們將讓它繼續運行,直到我們完成它為止。
添加調試器循環
現在我們已經啟動了子進程,我們希望能夠與它進行交互。 為此,我們將創建一個debugger
類,為其提供一個用于監聽用戶輸入的循環,并從我們的main函數中父進程的fork
之后開始。
else if (pid >= 1) {
//parent
debugger dbg{prog, pid};
dbg.run();
}
class debugger {
public:
debugger (std::string prog_name, pid_t pid)
: m_prog_name{std::move(prog_name)}, m_pid{pid} {}
void run();
private:
std::string m_prog_name;
pid_t m_pid;
};
在我們的run
函數中,我們需要等待子進程完成啟動,然后繼續從linenoise
獲取輸入,直到得到一個EOF(ctrl + d)。
void debugger::run() {
int wait_status;
auto options = 0;
waitpid(m_pid, &wait_status, options);
char* line = nullptr;
while((line = linenoise("minidbg> ")) != nullptr) {
handle_command(line);
linenoiseHistoryAdd(line);
linenoiseFree(line);
}
}
當跟蹤進程啟動時,將發送一個SIGTRAP
信號,它是一個跟蹤或斷點陷阱。 我們可以等到這個信號使用 waitpid
函數發送。
在我們知道這個進程已經準備好進行調試之后,我們會監聽用戶的輸入。linenoise
函數自動顯示和處理用戶輸入的提示。 這意味著我們得到一個很好的命令行與歷史和導航命令,而不需要做太多的工作。 當我們得到輸入時,我們給一個handle_command
函數給出這個命令,我們將很快寫入,然后我們將這個命令添加到linenoise
歷史中并釋放資源。
處理輸入
我們的命令將遵循與gdb
和lldb
類似的格式。 要繼續該程序,用戶將鍵入continue
或cont
或甚至c
。 如果他們想在地址上設置一個斷點,它們會寫入break 0xDEADBEEF
,其中0xDEADBEEF是十六進制格式的所需地址。 我們添加對這些命令的支持。
void debugger::handle_command(const std::string& line) {
auto args = split(line,' ');
auto command = args[0];
if (is_prefix(command, "continue")) {
continue_execution();
}
else {
std::cerr << "Unknown command\n";
}
}
split
和is_prefix
是一些小的幫助函數:
std::vector<std::string> split(const std::string &s, char delimiter) {
std::vector<std::string> out{};
std::stringstream ss {s};
std::string item;
while (std::getline(ss,item,delimiter)) {
out.push_back(item);
}
return out;
}
bool is_prefix(const std::string& s, const std::string& of) {
if (s.size() > of.size()) return false;
return std::equal(s.begin(), s.end(), of.begin());
}
我們將在debugger
類中添加continue_execution。
void debugger::continue_execution() {
ptrace(PTRACE_CONT, m_pid, nullptr, nullptr);
int wait_status;
auto options = 0;
waitpid(m_pid, &wait_status, options);
}
現在我們的continue_execution
函數只是使用ptrace
來告訴進程繼續,然后調用waitpid
直到它發出信號。
完成準備工作
現在,你應該可以編譯一些C或C++程序,通過自己寫的調試器
運行它,看到它停止輸入,并能夠從調試器繼續執行。 在下一部分中,我們將學習如何讓我們的調試器設置斷點。 如果遇到任何問題,請在評論中通知我!
你可以在這里找到這篇文章的代碼。
說明
原文來自:https://blog.tartanllama.xyz/writing-a-linux-debugger-setup/
翻譯來自:lantie@15PB, 15PB信息安全教育,主頁:http://www.15pb.com.cn
運行截圖
使用Clion編寫的代碼,在控制臺中的運行結果