自己動(dòng)手編寫(xiě)一個(gè)Linux調(diào)試器系列之1 準(zhǔn)備工作 by lantie@15PB
我想每個(gè)人都會(huì)編寫(xiě)不止一個(gè) hello world
程序, 并且使用調(diào)試器來(lái)調(diào)試這些程序(如果你沒(méi)有慨代,那放下你手上的活兒僻族,來(lái)學(xué)習(xí)使用調(diào)試器吧)堤如。然而撤逢,盡管調(diào)試器使用如此廣泛,但卻沒(méi)有很多資料可以告訴我們它的工作原理寓辱,以及如何編寫(xiě)一個(gè)調(diào)試器艘绍。特別是與編程時(shí)的其他工具技術(shù)(如編譯器)比起來(lái)。在這個(gè)系列的文章中秫筏,我們將會(huì)學(xué)習(xí)調(diào)試器的原理并編寫(xiě)一個(gè)調(diào)試器去調(diào)試Linux程序诱鞠。
我們將支持以下功能:
- 啟動(dòng)、停止并繼續(xù)執(zhí)行
- 設(shè)置各種斷點(diǎn)
- 內(nèi)存地址
- 源代碼行
- 函數(shù)入口處
- 讀取和寫(xiě)入寄存器和內(nèi)存
- 單步跟蹤
- 指令
- 單步步入
- 單步跳過(guò)
- 單步步過(guò)
- 打印當(dāng)前源碼位置
- 打印椪饩矗回溯信息
- 打印簡(jiǎn)單的值信息
最后我還會(huì)概述如何將以下功能添加到編寫(xiě)的調(diào)試器中:
- 遠(yuǎn)程調(diào)試
- 共享庫(kù)和動(dòng)態(tài)加載的支持
- 表達(dá)式求值
- 多線程調(diào)試的支持
我將使用C和C++來(lái)編寫(xiě)這個(gè)項(xiàng)目航夺,但這個(gè)項(xiàng)目同樣也適用于編譯成機(jī)器代碼和輸出標(biāo)準(zhǔn)的DWARF調(diào)試信息的編程語(yǔ)言。(如果你不知道這是什么崔涂,不要擔(dān)心阳掐,馬上就會(huì)清楚了)
此外, 我們的主要目的是在大多數(shù)情況下冷蚂,使程序都能正常運(yùn)行缭保,因此健壯的錯(cuò)誤處理會(huì)使編寫(xiě)變得更簡(jiǎn)單。
系列索引
- 準(zhǔn)備工作
- 斷點(diǎn)
- 寄存器和內(nèi)存
- ELF文件和調(diào)試信息
- 源碼和信號(hào)
- 源碼級(jí)單步
- 源碼級(jí)斷點(diǎn)
- 堆棧解除
- 處理變量
- 高級(jí)主題
開(kāi)始設(shè)置
在我們開(kāi)始討論之前蝙茶,讓我們先建立環(huán)境艺骂。在本教程中,我們將使用兩個(gè)依賴(lài)項(xiàng):
-
Linenoise
用于處理我們的命令行輸入 -
libelfin
用于解析調(diào)試信息隆夯。
你可以使用比較傳統(tǒng)的libdwarf
而不是libelfin
钳恕,但是其接口遠(yuǎn)沒(méi)有那么好,libelfin
還提供了一個(gè)基本完整的DWARF表達(dá)式求值工具蹄衷,如果您想要讀取變量的話忧额,這將節(jié)省您很多時(shí)間。請(qǐng)務(wù)必您使用我的libelfin
的fbreg
分支愧口,因?yàn)樗鼮閤86上的讀取變量提供了一些額外的支持睦番。
一旦你在系統(tǒng)中安裝了這些工具,或者在你的系統(tǒng)上編譯了相關(guān)的依賴(lài)項(xiàng)调卑,就可以開(kāi)始了抡砂。我只是將它們與我的CMake文件中的其他代碼一起編譯大咱。
啟動(dòng)程序
在我們調(diào)試一個(gè)程序時(shí)恬涧,首先我們需要先系統(tǒng)一個(gè)要調(diào)試的程序。我們可以使用經(jīng)典的 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) {
// 我們?cè)谧舆M(jìn)程中
// 執(zhí)行要調(diào)試的程序
}
else if (pid >= 1) {
// 我們?cè)诟高M(jìn)程中
// 執(zhí)行調(diào)試器
}
我們調(diào)用fork
會(huì)使我們的程序分為兩個(gè)進(jìn)程溯捆,如果我們?cè)谧舆M(jìn)程中fork
返回0,如果我們?cè)诟高M(jìn)程中,則返回子進(jìn)程的進(jìn)程ID提揍。
如果我們?cè)谧舆M(jìn)程中啤月,我們想用我們要調(diào)試的程序替換當(dāng)前正在執(zhí)行的程序,從而達(dá)到調(diào)試程序的目的劳跃。
ptrace(PTRACE_TRACEME, 0, nullptr, nullptr);
execl(prog, prog, nullptr);
這里我們第一次使用ptrace
谎仲,它將在編寫(xiě)調(diào)試器時(shí)成為我們最好的朋友。ptrace
允許我們通過(guò)讀取寄存器刨仑,讀取內(nèi)存郑诺,單步執(zhí)行等來(lái)觀察和控制另一個(gè)進(jìn)程的執(zhí)行。
這個(gè)API非常難看杉武,它是一個(gè)單一的函數(shù)辙诞,其中提了一些枚舉值可以使用,還有一些參數(shù)可以根據(jù)你提供的值使用或是忽略轻抱。函數(shù)的簽名如下所示:
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);
-
request
是我們對(duì)想要去跟蹤的進(jìn)程能做什么飞涂。 -
pid
是跟蹤進(jìn)程的進(jìn)程id。 -
addr
是內(nèi)存地址祈搜,這是用在跟蹤時(shí)一些調(diào)用指定地址较店。 -
data
是某些請(qǐng)求特定的資源。 - 返回值通常會(huì)提供錯(cuò)誤信息容燕,因此需要在編寫(xiě)代碼時(shí)對(duì)返回值進(jìn)行檢查泽西,更多信息可以查閱man手冊(cè)。
在上面的代碼中 request
的值是PTRACE_TRACEME
時(shí) 表面這個(gè)進(jìn)程應(yīng)該允許其父進(jìn)程跟蹤它缰趋,所有其他參數(shù)可以被忽略捧杉,因?yàn)锳PI設(shè)計(jì)的參數(shù)就不太重要。
下一步秘血,我們調(diào)用 execl
味抖,這是許多 exec
的類(lèi)似函數(shù)的其中一個(gè)。我們執(zhí)行給定的程序灰粮,通過(guò)它的名稱(chēng)作為命令行參數(shù)和一個(gè)nullptr
終止參數(shù)列表仔涩。如果你愿意,你可以將nullptr
替換為你的程序所需的任何其他參數(shù)粘舟。
在我們完成這項(xiàng)工作之后熔脂,我們完成了子進(jìn)程,我們將讓它繼續(xù)運(yùn)行柑肴,直到我們完成它為止霞揉。
添加調(diào)試器循環(huán)
現(xiàn)在我們已經(jīng)啟動(dòng)了子進(jìn)程,我們希望能夠與它進(jìn)行交互晰骑。 為此适秩,我們將創(chuàng)建一個(gè)debugger
類(lèi),為其提供一個(gè)用于監(jiān)聽(tīng)用戶輸入的循環(huán),并從我們的main函數(shù)中父進(jìn)程的fork
之后開(kāi)始秽荞。
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
函數(shù)中骤公,我們需要等待子進(jìn)程完成啟動(dòng),然后繼續(xù)從linenoise
獲取輸入扬跋,直到得到一個(gè)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);
}
}
當(dāng)跟蹤進(jìn)程啟動(dòng)時(shí),將發(fā)送一個(gè)SIGTRAP
信號(hào)钦听,它是一個(gè)跟蹤或斷點(diǎn)陷阱趁猴。 我們可以等到這個(gè)信號(hào)使用 waitpid
函數(shù)發(fā)送。
在我們知道這個(gè)進(jìn)程已經(jīng)準(zhǔn)備好進(jìn)行調(diào)試之后彪见,我們會(huì)監(jiān)聽(tīng)用戶的輸入儡司。linenoise
函數(shù)自動(dòng)顯示和處理用戶輸入的提示。 這意味著我們得到一個(gè)很好的命令行與歷史和導(dǎo)航命令余指,而不需要做太多的工作捕犬。 當(dāng)我們得到輸入時(shí),我們給一個(gè)handle_command
函數(shù)給出這個(gè)命令酵镜,我們將很快寫(xiě)入碉碉,然后我們將這個(gè)命令添加到linenoise
歷史中并釋放資源。
處理輸入
我們的命令將遵循與gdb
和lldb
類(lèi)似的格式淮韭。 要繼續(xù)該程序垢粮,用戶將鍵入continue
或cont
或甚至c
。 如果他們想在地址上設(shè)置一個(gè)斷點(diǎn)靠粪,它們會(huì)寫(xiě)入break 0xDEADBEEF
蜡吧,其中0xDEADBEEF是十六進(jìn)制格式的所需地址。 我們添加對(duì)這些命令的支持占键。
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
是一些小的幫助函數(shù):
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
類(lèi)中添加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);
}
現(xiàn)在我們的continue_execution
函數(shù)只是使用ptrace
來(lái)告訴進(jìn)程繼續(xù),然后調(diào)用waitpid
直到它發(fā)出信號(hào)畔乙。
完成準(zhǔn)備工作
現(xiàn)在君仆,你應(yīng)該可以編譯一些C或C++程序,通過(guò)自己寫(xiě)的調(diào)試器
運(yùn)行它牲距,看到它停止輸入返咱,并能夠從調(diào)試器繼續(xù)執(zhí)行。 在下一部分中牍鞠,我們將學(xué)習(xí)如何讓我們的調(diào)試器設(shè)置斷點(diǎn)咖摹。 如果遇到任何問(wèn)題,請(qǐng)?jiān)谠u(píng)論中通知我皮服!
你可以在這里找到這篇文章的代碼楞艾。
說(shuō)明
原文來(lái)自:https://blog.tartanllama.xyz/writing-a-linux-debugger-setup/
翻譯來(lái)自:lantie@15PB, 15PB信息安全教育,主頁(yè):http://www.15pb.com.cn
運(yùn)行截圖
使用Clion編寫(xiě)的代碼龄广,在控制臺(tái)中的運(yùn)行結(jié)果