系統(tǒng)調(diào)用追蹤器與實(shí)驗(yàn)報(bào)告實(shí)戰(zhàn))
如果你這學(xué)期的操作系統(tǒng)課也布置了“追蹤系統(tǒng)調(diào)用”這個(gè)作業(yè)大概率是這樣一個(gè)場(chǎng)景裝好 Ubuntu 虛擬機(jī)打開(kāi)終端對(duì)著 strace 的輸出一頭霧水屏幕滾過(guò)去幾十行 read、write、mmap數(shù)據(jù)是有了但不知道該怎么組織成一份能過(guò)審的實(shí)驗(yàn)報(bào)告。這篇文章就是圍繞這個(gè)作業(yè)寫(xiě)的我會(huì)把“系統(tǒng)調(diào)用到底怎么追”這件事拆開(kāi)講清楚順便把我自己踩過(guò)的坑、被老師追問(wèn)過(guò)的點(diǎn)、以及作業(yè)報(bào)告該怎么寫(xiě)一次性交代完。適合你的情況有幾種一是剛開(kāi)始接觸 Linux 系統(tǒng)調(diào)用連 strace 都沒(méi)用過(guò)二是作業(yè)要求寫(xiě)一個(gè)自定義的追蹤工具卡在了 ptrace 上;三是已經(jīng)在命令行跑通了但實(shí)驗(yàn)報(bào)告不知道怎么寫(xiě)才能拿高分。這篇按從易到難的路線推進(jìn)先講原理和 strace 的使用再給一份可以直接編譯運(yùn)行的 C 語(yǔ)言追蹤器最后補(bǔ)充作業(yè)報(bào)告的寫(xiě)法與常見(jiàn)故障排查。1. 作業(yè)背后的底層邏輯為什么操作系統(tǒng)課要讓你追蹤系統(tǒng)調(diào)用1.1 系統(tǒng)調(diào)用是什么程序與內(nèi)核之間的“官方接口”系統(tǒng)調(diào)用system call是用戶態(tài)程序請(qǐng)求內(nèi)核服務(wù)的唯一合法入口。你可能寫(xiě)過(guò)無(wú)數(shù)個(gè) printf但 printf 本身不是系統(tǒng)調(diào)用它最終會(huì)通過(guò) write 這個(gè)系統(tǒng)調(diào)用把數(shù)據(jù)交給內(nèi)核由內(nèi)核驅(qū)動(dòng)終端或文件系統(tǒng)完成實(shí)際輸出。這個(gè)過(guò)程可以理解為應(yīng)用程序是顧客內(nèi)核是營(yíng)業(yè)大廳系統(tǒng)調(diào)用就是那個(gè)唯一開(kāi)放的窗口——你想操作文件、創(chuàng)建進(jìn)程、申請(qǐng)內(nèi)存、讀取鍵盤(pán)全部要在這個(gè)窗口遞單子。以 x86_64 Linux 為例系統(tǒng)調(diào)用的具體流程是程序把系統(tǒng)調(diào)用號(hào)放入 rax 寄存器參數(shù)依次放入 rdi、rsi、rdx、r10、r8、r9然后執(zhí)行 syscall 指令。執(zhí)行這條指令時(shí)CPU 會(huì)從用戶態(tài)切換到內(nèi)核態(tài)根據(jù)系統(tǒng)調(diào)用號(hào)在 sys_call_table 中查到對(duì)應(yīng)的內(nèi)核函數(shù)執(zhí)行完成后把返回值放入 rax再切換回用戶態(tài)。你根本不需要記住每個(gè)細(xì)節(jié)但腦里要有這個(gè)畫(huà)面一次普通的文件讀操作背后就包含了一次陷入內(nèi)核、一次內(nèi)核態(tài)處理、一次返回用戶態(tài)的完整時(shí)空穿梭。1.2 為什么作業(yè)要選“追蹤”作為切入點(diǎn)這是整個(gè)作業(yè)最有價(jià)值的地方。一個(gè)程序從啟動(dòng)到退出會(huì)經(jīng)歷數(shù)十個(gè)甚至上百個(gè)系統(tǒng)調(diào)用追蹤這些調(diào)用本質(zhì)上就是在給程序做“X光透視”。你不光能看到程序調(diào)用了幾次 read、write還能看出程序是怎么加載動(dòng)態(tài)庫(kù)的、是怎么申請(qǐng)內(nèi)存的、是怎么創(chuàng)建子進(jìn)程的。很多同學(xué)做完作業(yè)只會(huì)交一張 strace 截圖但實(shí)際這個(gè)作業(yè)的隱藏考點(diǎn)有三個(gè)第一你是否理解用戶態(tài)與內(nèi)核態(tài)的切換代價(jià)——每次系統(tǒng)調(diào)用都是一次寶貴的狀態(tài)切換頻繁調(diào)用會(huì)拖慢性能第二你是否能從追蹤結(jié)果反向推斷程序的啟動(dòng)流程——比如 execve 之后緊跟一堆 openat 和 mmap那是在找動(dòng)態(tài)鏈接庫(kù)第三你是否知道庫(kù)函數(shù)和系統(tǒng)調(diào)用的區(qū)別——printf 不是系統(tǒng)調(diào)用write 才是這個(gè)區(qū)分比想象中重要。實(shí)驗(yàn)課老師考的不是你會(huì)不會(huì)敲 strace 命令而是你有沒(méi)有真正理解系統(tǒng)調(diào)用在整個(gè)操作系統(tǒng)中的作用。1.3 這個(gè)作業(yè)的擴(kuò)展方向從追蹤到攔截再到自定義行為基礎(chǔ)作業(yè)是“追蹤”但如果你想拿更高分通常會(huì)延伸出兩條支線。一條是過(guò)濾統(tǒng)計(jì)把大量追蹤數(shù)據(jù)按系統(tǒng)調(diào)用類(lèi)型聚合分析哪個(gè)調(diào)用最頻繁進(jìn)而提出優(yōu)化方案。另一條是“攔截修改”不只是看還要在系統(tǒng)調(diào)用發(fā)生前后插入自己的邏輯比如打印額外參數(shù)、修改返回值。后者已經(jīng)具備“逆向工程”“沙箱”“調(diào)試器”的基本形態(tài)很多安全工具的核心原理也在這。所以這個(gè)作業(yè)絕不是一個(gè)孤立的命令練習(xí)它是通往進(jìn)程控制、性能剖析和工具開(kāi)發(fā)的起點(diǎn)。2. 環(huán)境準(zhǔn)備Ubuntu 虛擬機(jī)與基礎(chǔ)工具鏈2.1 虛擬機(jī)方案選擇VMware 還是 VirtualBox絕大多數(shù)課程作業(yè)要求在 Linux 環(huán)境完成如果你本身不是 Linux 主力機(jī)最省事的方案就是虛擬機(jī)。我在開(kāi)始做之前選型對(duì)比過(guò)幾套方案VMware Workstation 是中文界面友好對(duì)新手比較友好VirtualBox 是開(kāi)源的沒(méi)有授權(quán)困擾但性能上同一臺(tái)機(jī)器差距其實(shí)不大特別對(duì)于這種純 CPU 密集型的小實(shí)驗(yàn)差別可以忽略。如果是 Windows 11 系統(tǒng)注意在“Windows 功能”里關(guān)閉 Hyper-V否則 VMware 加載 Ubuntu 時(shí)容易報(bào)“VMware Workstation 與 Device/Credential Guard 不兼容”的錯(cuò)誤。Ubuntu 版本建議選擇 22.04 LTS 或 24.04 LTS不要選 26.04 之類(lèi)的 development 分支實(shí)驗(yàn)環(huán)境穩(wěn)定性第一。安裝時(shí)虛擬機(jī)內(nèi)存給到 4GB 以上硬盤(pán) 40GB 以上CPU 至少雙核。如果你的機(jī)器配置一般這個(gè)環(huán)境也足夠用了。2.2 安裝必要的工具鏈與追蹤工具開(kāi)機(jī)進(jìn)入 Ubuntu 后第一件事是把編譯工具鏈和追蹤工具裝齊。打開(kāi)終端執(zhí)行以下命令sudo apt update sudo apt install build-essential gcc gdb strace linux-tools-generic -ybuild-essential 包含 gcc、make 等編譯工具strace 是后面要用的第一級(jí)追蹤工具linux-tools-generic 提供部分性能追蹤工具不是必需但建議安裝。裝完之后用版本號(hào)驗(yàn)證環(huán)境uname -a gcc --version strace -V如果這幾條命令都能正常輸出說(shuō)明你的 Ubuntu 環(huán)境已經(jīng)就緒。整個(gè)過(guò)程一般不超過(guò) 5 分鐘但很多同學(xué)在安裝 headless 版本時(shí)反而卡在沒(méi)裝 gcc 這種基礎(chǔ)問(wèn)題上再?gòu)?qiáng)調(diào)一次先 build-essential再談其他。2.3 驗(yàn)證追蹤環(huán)境是否可用第一個(gè)最小實(shí)驗(yàn)環(huán)境搭建完不要急著跑復(fù)雜程序先用系統(tǒng)里最基礎(chǔ)的命令驗(yàn)證追蹤功能是否正常。比如追蹤 pwdstrace -o /tmp/pwd.trace pwd cat /tmp/pwd.trace這個(gè)命令會(huì)列出執(zhí)行 pwd 期間的所有系統(tǒng)調(diào)用。正常情況下你能看到 openat、fstat、getcwd、write、close 等調(diào)用??吹竭@些輸出說(shuō)明你的系統(tǒng)、內(nèi)核和追蹤工具都正常工作。這一步的價(jià)值在于把“環(huán)境問(wèn)題”和“作業(yè)問(wèn)題”隔離開(kāi)后面再出問(wèn)題就大概率是你自己的程序和邏輯問(wèn)題了。3. 最省力的追蹤路徑strace 命令速成與實(shí)驗(yàn)數(shù)據(jù)獲取3.1 strace 的基本用法與輸出解讀strace 是 Linux 自帶的高位格式化的系統(tǒng)調(diào)用追蹤器它的原理是內(nèi)嵌在 ptrace 之上的一個(gè)封裝我們第四部分會(huì)自己實(shí)現(xiàn)一個(gè)簡(jiǎn)化版現(xiàn)在先把它當(dāng)黑盒用。基本格式是strace -o 輸出文件 目標(biāo)程序最簡(jiǎn)單的使用場(chǎng)景是追蹤 lsstrace -o /tmp/ls.trace ls head -30 /tmp/ls.trace你會(huì)看到類(lèi)似下面的輸出execve(/usr/bin/ls, [ls], 0x7ffd...) 0 brk(NULL) 0x5577... openat(AT_FDCWD, /etc/ld.so.cache, O_RDONLY|O_CLOEXEC) 3 fstat(3, {st_mode...}) 0 mmap(NULL, 132096, PROT_READ, MAP_PRIVATE, 3, 0) 0x7f... close(3) 0每一行的結(jié)構(gòu)是系統(tǒng)調(diào)用名(參數(shù)列表) 返回值。比如 openat 返回 3說(shuō)明內(nèi)核分配了一個(gè)新的文件描述符 3 給這個(gè)打開(kāi)的文件。如果你看到返回負(fù)數(shù)比如 -1那往往表示調(diào)用失敗后面括號(hào)里會(huì)帶一個(gè) errno 碼。3.2 三個(gè)最常用的追蹤選項(xiàng)過(guò)濾、跟隨子進(jìn)程、統(tǒng)計(jì)純基礎(chǔ)實(shí)驗(yàn)只需要 strace 默認(rèn)行為就足夠但如果你的作業(yè)要求分析“某個(gè)程序整個(gè)生命周期”的全部行為很可能用到以下幾個(gè)選項(xiàng)。我把它們整理成一個(gè)速查表方便你直接抄作業(yè)選項(xiàng)作用使用示例說(shuō)明-f同時(shí)追蹤子進(jìn)程strace -f -o out.txt ./prog不使用時(shí)只追蹤主進(jìn)程-e trace指定追蹤的調(diào)用集合strace -e traceopen,read,write ./prog讓輸出聚焦減少噪音-c按系統(tǒng)調(diào)用匯總統(tǒng)計(jì)strace -c ./prog生成調(diào)用次數(shù)、耗時(shí)統(tǒng)計(jì)表-o輸出到文件strace -o log.txt ./prog防止追蹤輸出與程序輸出混合-T顯示每次調(diào)用耗時(shí)strace -T ./prog用于分析性能瓶頸我自己的使用習(xí)慣是第一遍用strace -f -o trace.txt ./program全量采集第二遍用strace -c做統(tǒng)計(jì)如果作業(yè)要求分析某個(gè)特定行為比如文件訪問(wèn)再用-e traceopen,openat做定向追蹤。三步走下來(lái)實(shí)驗(yàn)數(shù)據(jù)基本上就齊了。3.3 數(shù)據(jù)怎么變成實(shí)驗(yàn)報(bào)告內(nèi)容追蹤完成后作業(yè)報(bào)告最核心的“實(shí)驗(yàn)結(jié)果”部分一般就是兩張表加一張圖。第一張是系統(tǒng)調(diào)用頻率統(tǒng)計(jì)表用strace -c的輸出往上貼第二張是程序關(guān)鍵行為路徑從全量 trace 中篩選出有代表性的關(guān)鍵調(diào)用按時(shí)間順序排列說(shuō)明每個(gè)調(diào)用在完成什么動(dòng)作。如果還需要圖可以把strace -c的結(jié)果手動(dòng)錄入 Excel 或 gnuplot畫(huà)一個(gè)柱狀圖或者餅圖。這里提醒一下實(shí)驗(yàn)報(bào)告不是流水賬不要全量貼 trace 日志。老師想知道的是你有沒(méi)有看懂?dāng)?shù)據(jù)比如追蹤ls時(shí)你會(huì)發(fā)現(xiàn)調(diào)用次數(shù)最多的是mmap和openat這時(shí)候你就應(yīng)該在報(bào)告里解釋程序啟動(dòng)需要加載動(dòng)態(tài)鏈接庫(kù)ld.so而加載庫(kù)涉及文件打開(kāi)、內(nèi)存映射這正好印證了程序動(dòng)態(tài)鏈接的原理。這種從數(shù)據(jù)到原理的對(duì)應(yīng)關(guān)系才是高分的分水嶺。4. 硬核路線用 ptrace 手寫(xiě)一個(gè)系統(tǒng)調(diào)用追蹤器4.1 ptrace 原理系統(tǒng)調(diào)用追蹤的“地基”如果你以為操作系統(tǒng)作業(yè)只是敲敲 strace 命令那很可能低估了課程設(shè)計(jì)要求。不少學(xué)校明確要求“實(shí)現(xiàn)一個(gè)簡(jiǎn)單的系統(tǒng)調(diào)用追蹤器”或者追問(wèn)“strace 底層是怎么做到的”。答案核心就是 ptrace 系統(tǒng)調(diào)用。ptrace 提供了一種機(jī)制允許一個(gè)進(jìn)程觀察和控制另一個(gè)進(jìn)程的執(zhí)行并讀取被觀察進(jìn)程的寄存器與內(nèi)存。最常見(jiàn)的使用模式是父進(jìn)程 fork 一個(gè)子進(jìn)程子進(jìn)程調(diào)用 ptrace(PTRACE_TRACEME) 聲明自己愿意被父進(jìn)程追蹤然后執(zhí)行目標(biāo)程序父進(jìn)程用 waitpid 等待子進(jìn)程因各種事件陷入停止再用 PTRACE_GETREGS 讀取它的寄存器從而實(shí)現(xiàn)“看到每一次系統(tǒng)調(diào)用的進(jìn)出瞬間”。這種模式正是調(diào)試器 GDB、系統(tǒng)調(diào)用探查工具 strace 的共同基礎(chǔ)。4.2 追蹤器的核心工作流程fork、exec、wait 與事件判斷手寫(xiě)追蹤器的總體邏輯分為四步創(chuàng)建子進(jìn)程并在子進(jìn)程中先調(diào)用ptrace(PTRACE_TRACEME)再execvp執(zhí)行目標(biāo)程序。父進(jìn)程waitpid等待子進(jìn)程停止。每次子進(jìn)程因系統(tǒng)調(diào)用而停止時(shí)父進(jìn)程用PTRACE_GETREGS讀取寄存器。通過(guò)寄存器中的orig_rax獲取系統(tǒng)調(diào)用號(hào)通過(guò)rax獲取返回值打印輸出后讓子進(jìn)程繼續(xù)。關(guān)鍵在于ptrace 下每次系統(tǒng)調(diào)用會(huì)觸發(fā)兩次停止事件第一次是在系統(tǒng)調(diào)用進(jìn)入內(nèi)褲之前syscall-enter-stop第二次是在系統(tǒng)調(diào)用返回用戶態(tài)之前syscall-exit-stop。所以父進(jìn)程必須記住當(dāng)前是“進(jìn)入”還是“退出”狀態(tài)。一個(gè)實(shí)用的判斷方法是用一個(gè)布爾變量交替切換。當(dāng)進(jìn)入事件發(fā)生時(shí)讀取orig_rax得到系統(tǒng)調(diào)用號(hào)當(dāng)退出事件發(fā)生時(shí)讀取rax得到返回值。4.3 可直接編譯運(yùn)行的 C 語(yǔ)言追蹤器代碼下面這份代碼我在 Ubuntu 22.04 上測(cè)試過(guò)可以原樣保存為tracer.c并使用gcc -o tracer tracer.c編譯。代碼用 x86_64 Linux 的寄存器命名如果你用的是 32 位系統(tǒng)需要換成 eax/ebx 那一套但今天絕大多數(shù)課程環(huán)境都是 64 位。#include stdio.h #include stdlib.h #include unistd.h #include sys/ptrace.h #include sys/wait.h #include sys/user.h #include signal.h const char *syscall_name(long nr) { switch (nr) { case 0: return read; case 1: return write; case 2: return open; case 3: return close; case 9: return mmap; case 10: return mprotect; case 12: return brk; case 39: return getpid; case 56: return clone; case 59: return execve; case 60: return exit; case 79: return getcwd; case 158: return arch_prctl; case 257: return openat; default: return unknown; } } int main(int argc, char *argv[]) { if (argc 2) { fprintf(stderr, 用法: %s program [args...]\n, argv[0]); return 1; } pid_t pid fork(); if (pid 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execvp(argv[1], argv[1]); perror(execvp); exit(1); } int status; waitpid(pid, status, 0); int in_syscall 1; while (1) { if (ptrace(PTRACE_SYSCALL, pid, NULL, NULL) -1) { break; } if (waitpid(pid, status, 0) -1) break; if (WIFEXITED(status) || WIFSIGNALED(status)) break; if (!WIFSTOPPED(status)) continue; struct user_regs_struct regs; if (ptrace(PTRACE_GETREGS, pid, NULL, regs) -1) continue; if (in_syscall) { long nr regs.orig_rax; printf([進(jìn)入] 調(diào)用號(hào) %ld (%s)\n, nr, syscall_name(nr)); } else { long nr regs.orig_rax; long ret regs.rax; printf([退出] %s 返回值 %ld\n, syscall_name(nr), ret); } in_syscall !in_syscall; } return 0; }代碼邏輯不復(fù)雜但有三個(gè)容易寫(xiě)錯(cuò)的地方需要特別留意。第一是子進(jìn)程必須在 execvp 之前調(diào)用 ptrace(PTRACE_TRACEME)不然后續(xù)追蹤不到任何事件。第二是父進(jìn)程第一輪 waitpid 拿到的是“子進(jìn)程因 exec 而停止”的事件此時(shí)子進(jìn)程還沒(méi)進(jìn)入第一個(gè)系統(tǒng)調(diào)用所以 in_syscall 變量要初始化為 1。第三是 PTRACE_SYSCALL 每次只能推進(jìn)一次事件你想持續(xù)追蹤就得不斷設(shè)置新事件并用 waitpid 配合。4.4 運(yùn)行追蹤器并解讀結(jié)果編譯并運(yùn)行g(shù)cc -o tracer tracer.c ./tracer pwd運(yùn)行后會(huì)看到類(lèi)似下面的輸出[進(jìn)入] 調(diào)用號(hào) 12 (brk) [退出] brk 返回值 94709773307904 [進(jìn)入] 調(diào)用號(hào) 257 (openat) [退出] openat 返回值 3 [進(jìn)入] 調(diào)用號(hào) 1 (write) [退出] write 返回值 8 ......這個(gè)結(jié)果和 strace 輸出雖然排版不同但信息本質(zhì)一致進(jìn)入了哪個(gè)系統(tǒng)調(diào)用帶什么返回值。如果作業(yè)要求顯示參數(shù)比如打開(kāi)的文件名你還需要使用 ptrace 的 PTRACE_PEEKDATA 或 process_vm_readv 去讀取被追蹤進(jìn)程的內(nèi)存地址。這里先不展開(kāi)因?yàn)槎鄶?shù)基礎(chǔ)作業(yè)只要求調(diào)用名和返回值如果你想追加參數(shù)顯示可以在進(jìn)入系統(tǒng)調(diào)用時(shí)拿到 rdi/rsi/rdx 等寄存器再用 PTRACE_PEEKDATA 按字節(jié)拼接字符串。思路不難但是是個(gè)相對(duì)獨(dú)立的小工程。5. 操作系統(tǒng)實(shí)驗(yàn)報(bào)告怎么寫(xiě)從數(shù)據(jù)到高分結(jié)論5.1 報(bào)告結(jié)構(gòu)邏輯完整比花哨重要很多同學(xué)實(shí)驗(yàn)做的很認(rèn)真但報(bào)告拿不到高分主要問(wèn)題是結(jié)構(gòu)混亂。這里提供一個(gè)通用框架實(shí)驗(yàn)?zāi)康?、?shí)驗(yàn)環(huán)境、實(shí)驗(yàn)原理、實(shí)驗(yàn)步驟、實(shí)驗(yàn)結(jié)果與分析、問(wèn)題與心得、結(jié)論與參考。實(shí)驗(yàn)?zāi)康牟灰蠖盏木渥又苯訉?xiě)“本實(shí)驗(yàn)要求追蹤一個(gè) Linux 程序執(zhí)行時(shí)的全部系統(tǒng)調(diào)用并通過(guò)數(shù)據(jù)分析其行為”實(shí)驗(yàn)原理部分寫(xiě)清楚系統(tǒng)調(diào)用機(jī)制和 strace/ptrace 的原理不要寫(xiě)流水賬實(shí)驗(yàn)結(jié)果是圖表和關(guān)鍵輸出分析部分是重點(diǎn)至少占全報(bào)告的三分之一篇幅。5.2 數(shù)據(jù)呈現(xiàn)的三個(gè)技巧表格化、聚焦化、原理解釋第一數(shù)據(jù)要表格化不要丟大段 terminal 日志。把strace -c的結(jié)果整理成調(diào)用次數(shù)、耗時(shí)、占比的表格一眼就能看出系統(tǒng)調(diào)用的分布。第二結(jié)果要聚焦化從追蹤數(shù)據(jù)里挑出三到五個(gè)代表性系統(tǒng)調(diào)用比如 execve 啟動(dòng)進(jìn)程、openat 打開(kāi)庫(kù)文件、mmap 映射內(nèi)存、write 輸出結(jié)果逐個(gè)解釋它們?cè)诔绦蛏芷诶锏慕巧皇前褞资姓{(diào)用全貼進(jìn)去。第三結(jié)論必須有原理解釋。比如用你手寫(xiě)追蹤器去追蹤一個(gè)空的 C 程序你一定會(huì)發(fā)現(xiàn) brk 和 mmap 出現(xiàn)在比較靠前的位置這就是 C 運(yùn)行時(shí)初始化堆內(nèi)存和加載動(dòng)態(tài)鏈接器的過(guò)程把它和課堂上的“程序加載與執(zhí)行”知識(shí)點(diǎn)對(duì)應(yīng)起來(lái)老師一看就知道你理解了。5.3 心得與加分項(xiàng)把“踩坑”變成亮點(diǎn)一份有價(jià)值的報(bào)告還要有“問(wèn)題與心得”板塊。這里面最好的素材就是你在這份作業(yè)中真實(shí)遇到的問(wèn)題比如虛擬機(jī)里 Ubuntu 沒(méi)有安裝 build-essential 導(dǎo)致的編譯失敗或 ptrace 追蹤 shell 腳本時(shí)報(bào)參數(shù)錯(cuò)誤把這些寫(xiě)進(jìn)去并說(shuō)明你是如何排查和解決的這屬于加分項(xiàng)。另外一個(gè)非常加分的點(diǎn)是把你的追蹤器功能擴(kuò)展一下。比如在代碼中加入統(tǒng)計(jì)接口統(tǒng)計(jì)每個(gè)系統(tǒng)調(diào)用出現(xiàn)的次數(shù)或者加入時(shí)間戳測(cè)量每次系統(tǒng)調(diào)用耗時(shí)。這兩項(xiàng)幾乎不需要額外引入復(fù)雜機(jī)制只需要在 while 循環(huán)加一個(gè)計(jì)數(shù)器和gettimeofday調(diào)用但寫(xiě)進(jìn)報(bào)告之后你的作業(yè)就從“實(shí)現(xiàn)了”變成了“實(shí)現(xiàn)并分析了”深度完全不是一個(gè)等級(jí)。6. 常見(jiàn)問(wèn)題排查與避坑實(shí)錄6.1 問(wèn)題速查表高頻故障與解決方案問(wèn)題現(xiàn)象可能原因解決方法虛擬機(jī)啟動(dòng)提示與 Hyper-V 不兼容Windows 11 開(kāi)啟了 Hyper-V關(guān)閉 Hyper-V 或改用 VirtualBoxUbuntu 中提示gcc: command not found未安裝編譯工具鏈安裝 build-essentialstrace 輸出全是ENOENT錯(cuò)誤程序默認(rèn)路徑不完整檢查工作目錄使用絕對(duì)路徑執(zhí)行目標(biāo)程序tracer 運(yùn)行后沒(méi)有輸出任何調(diào)用忘記 PTRACE_TRACEME 或 execvp 失敗了代碼里加入 perror 打印錯(cuò)誤tracer 輸出大量的 interrupted system call目標(biāo)程序收到信號(hào)忽略該錯(cuò)誤碼或讓子進(jìn)程先屏蔽 SIGINTptrace 報(bào)Operation not permitted目標(biāo)程序是 setuid 程序換一個(gè)普通程序進(jìn)行追蹤不建議做復(fù)雜繞過(guò)操作追蹤 shell 腳本無(wú)效strace 默認(rèn)只追蹤 sh 進(jìn)程用-f追蹤子進(jìn)程6.2 我踩過(guò)的三個(gè)大坑與應(yīng)對(duì)方法第一個(gè)坑是子進(jìn)程行為不可控。早期我在子進(jìn)程里先打印再用 execvp結(jié)果追蹤到的全是 printf 的 write 調(diào)用真正的目標(biāo)程序反而沒(méi)跑起來(lái)。這里的關(guān)鍵是子進(jìn)程除 ptrace 和 exec 之外不要讓任何其他邏輯運(yùn)行一切調(diào)試信息都放在父進(jìn)程里打印。第二個(gè)坑是寄存器讀取失敗。當(dāng)時(shí)我在 32 位虛擬機(jī)上跑 64 位程序直接導(dǎo)致 PTRACE_GETREGS 返回錯(cuò)誤。后面統(tǒng)一改用 64 位 Ubuntu 系統(tǒng)并且用 uname -m 確認(rèn)環(huán)境這個(gè)問(wèn)題再?zèng)]出現(xiàn)。所以你在動(dòng)手前先確認(rèn)“追蹤器位數(shù)”和“目標(biāo)程序位數(shù)”一致。第三個(gè)坑是 PTRACE_SYSCALL 與信號(hào)處理混在一起。子進(jìn)程運(yùn)行到一半收到 SIGINT 時(shí)父進(jìn)程 waitpid 返回的狀態(tài)會(huì)混合系統(tǒng)調(diào)用停止事件和信號(hào)停止事件如果不判斷信號(hào)會(huì)導(dǎo)致后續(xù)追蹤事件錯(cuò)亂。解決辦法是遇到WIFSTOPPED時(shí)進(jìn)一步檢查status 8的值區(qū)分是 SIGTRAP系統(tǒng)調(diào)用事件還是其他信號(hào)再將其他信號(hào)轉(zhuǎn)發(fā)給子進(jìn)程。這個(gè)屬于進(jìn)階處理但遇到一次之后你就會(huì)記住。6.3 后續(xù)還能怎么擴(kuò)展從作業(yè)到真正工具這個(gè)作業(yè)做完如果你還有興趣可以在此基礎(chǔ)上做幾件很有價(jià)值的事。一個(gè)是實(shí)現(xiàn)“系統(tǒng)調(diào)用參數(shù)解析”用PTRACE_PEEKDATA去讀被追蹤進(jìn)程的內(nèi)存把字符串參數(shù)真實(shí)顯示出來(lái)這就非常接近 strace 的日常行為了。另一個(gè)是做成“系統(tǒng)調(diào)用性能分析器”統(tǒng)計(jì)每個(gè)系統(tǒng)調(diào)用耗時(shí)與頻次明顯是給服務(wù)端性能優(yōu)化做準(zhǔn)備的。還有一種是結(jié)合 eBPF 跟蹤在 Linux 5.x 內(nèi)核上使用 BPF Type Format 獲取調(diào)用堆棧不過(guò)這個(gè)已經(jīng)跳出本科生作業(yè)的普遍范圍了。我個(gè)人在實(shí)際操作中的體會(huì)是這個(gè)作業(yè)最好的學(xué)習(xí)方式就是先不管三七二十一寫(xiě)一個(gè)最簡(jiǎn)陋的 ptrace 追蹤器能打印一次系統(tǒng)調(diào)用就算成功。然后再跑一個(gè)真實(shí)程序看到屏幕上那些熟悉的名字——write、openat、mmap 一個(gè)接一個(gè)閃現(xiàn)的時(shí)候你對(duì)“操作系統(tǒng)到底在干什么”的理解會(huì)在那一刻變得非常具體。這也是我仍然推薦你親手做一遍而不只是拿 strace 結(jié)果敷衍的原因。伸出手去敲代碼遠(yuǎn)比你想象的更有收獲。