背景
國(guó)科礎(chǔ)石操作系統(tǒng)團(tuán)隊(duì)在開(kāi)發(fā)礎(chǔ)光智能操作系統(tǒng)的過(guò)程中,需要分析glibc啟動(dòng)過(guò)程中的異常信息,在此過(guò)程中探索出一條快速調(diào)試glibc流程的方法。
由于glibc啟動(dòng)代碼復(fù)雜,printf、ptrace等輔助調(diào)試手段還不能正常使用,給分析過(guò)程帶來(lái)困難。本文探索的方法避免了對(duì)printf、ptrace的依賴(lài)。
glibc 簡(jiǎn)介
glibc是Linux系統(tǒng)中常用的C運(yùn)行時(shí)庫(kù),它是GNU項(xiàng)目的一部分,是一組函數(shù)和子例程的集合,為L(zhǎng)inux操作系統(tǒng)上的C程序提供了基本的運(yùn)行時(shí)支持。
glibc提供了Linux系統(tǒng)所需的底層功能和工具,包括內(nèi)存管理、線程支持、網(wǎng)絡(luò)編程、文件系統(tǒng)訪問(wèn)、數(shù)學(xué)計(jì)算、時(shí)間和日期處理、本地化支持等等。它還提供了標(biāo)準(zhǔn)的C庫(kù)函數(shù),如字符串操作、輸入輸出、數(shù)據(jù)結(jié)構(gòu)操作等等。
glibc還提供了一些高級(jí)功能,例如動(dòng)態(tài)內(nèi)存管理、線程安全、多語(yǔ)言支持、安全性等等。它提供了一些重要的頭文件和宏定義,例如stdio.h、stdlib.h、string.h、time.h等等。
glibc還提供了一些調(diào)試和性能分析工具,例如gdb調(diào)試器和strace系統(tǒng)調(diào)用跟蹤器等。
總之,glibc是Linux系統(tǒng)中最重要的C運(yùn)行時(shí)庫(kù)之一,提供了許多基本和高級(jí)功能,為開(kāi)發(fā)人員提供了強(qiáng)大的工具和支持,使得他們能夠更加輕松地編寫(xiě)高質(zhì)量、高效、可靠的C程序。
glibc是什么?
舉個(gè)簡(jiǎn)單的例子來(lái)解釋glibc大概做了什么 :
#includeint sum (int a, int b) { return a + b; } int main (void) { int a = 35; int b = 24; printf("%d + %d = %d ", a, b, sum(a, b)); return 0; }
當(dāng)我們編寫(xiě)一個(gè)c程序時(shí),在 glibc 的幫助下會(huì)給我們一種錯(cuò)覺(jué) : 當(dāng)我們運(yùn)行編譯出來(lái)的二進(jìn)制文件,操作系統(tǒng)直接運(yùn)行到 main 函數(shù),然后執(zhí)行由提供的函數(shù)或我們自己編寫(xiě)的邏輯代碼,在上述例子中,我們使用了libc提供的 "printf" 打印函數(shù)。我們自己編寫(xiě)了一個(gè)求和的邏輯代碼。那么glibc真的就是提供一些函數(shù)接口的庫(kù)么?
其實(shí)對(duì)于操作系統(tǒng)而言,它會(huì)都不"認(rèn)識(shí)"main函數(shù)。而一個(gè)進(jìn)程的執(zhí)行也并非由 main 函數(shù)開(kāi)始的。在鏈接時(shí),鏈接器會(huì)設(shè)置函數(shù)入口,而該可執(zhí)行程序入口不是 main。
[vizdl@localhost glibc_debug]# readelf -h build/crt.elf ELF Header: Magic: 7f 45 4c 46 02 01 01 03 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - GNU ABI Version: 0 Type: EXEC (Executable file) Machine: AArch64 Version: 0x1 Entry point address: 0x400580 Start of program headers: 64 (bytes into file) Start of section headers: 634584 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 6 Size of section headers: 64 (bytes) Number of section headers: 28 Section header string table index: 27
在這里我將上述代碼編譯鏈接后,使用 readelf -h 讀取該可執(zhí)行文件的頭部信息,可以看到 "Entry point address: 0x400580",表明可執(zhí)行程序的入口地址是 0x400580。
[vizdl@localhost glibc_debug]# readelf -s build/crt.elf | grep 400580 29: 0000000000400580 0 NOTYPE LOCAL DEFAULT 6 $x 2471: 0000000000400580 60 FUNC GLOBAL HIDDEN 6 _start
我們通過(guò) readelf -s 指令查看該二進(jìn)制的符號(hào)表,可以看到, elf 執(zhí)行的第一個(gè)"函數(shù)"是 _start,而不是 main??蓤?zhí)行文件執(zhí)行到main函數(shù)之前,其實(shí) glibc 偷偷加了一些代碼。這部分代碼籠統(tǒng)地講其實(shí)就是做了一些進(jìn)程環(huán)境設(shè)置的工作,讓編寫(xiě)c代碼的程序員可以避免每次都要編寫(xiě)重復(fù)的進(jìn)程的環(huán)境設(shè)置!glibc真切地做到了做好事不留名:)但是今天我們提供一種方式,讓大家都能看到glibc做的好事~
glibc 開(kāi)發(fā)者如何調(diào)試 glibc?
在 glibc 中,一些地方調(diào)用c庫(kù)函數(shù)會(huì)出現(xiàn)問(wèn)題,特別是 _start -> main 這段代碼,由于進(jìn)程環(huán)境未初始化,導(dǎo)致大多數(shù)的 glibc 的函數(shù)運(yùn)行的前提無(wú)法保證,于是絕大多數(shù) glibc 的函數(shù)無(wú)法在這段代碼內(nèi)運(yùn)行,這導(dǎo)致對(duì)glibc的觀察可謂是困難重重,如何提供一種簡(jiǎn)單通用且可靠的調(diào)試方法一直是業(yè)界的難題。
我們?cè)?glibc 入口函數(shù)找到了一些代碼,并調(diào)用自定義函數(shù)dl_debug_printf來(lái)進(jìn)行調(diào)試輸出:
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL), int argc, char **argv, #ifdef LIBC_START_MAIN_AUXVEC_ARG ElfW(auxv_t) *auxvec, #endif __typeof (main) init, void (*fini) (void), void (*rtld_fini) (void), void *stack_end) { ... if (__builtin_expect (GLRO(dl_debug_mask) & DL_DEBUG_IMPCALLS, 0)) GLRO(dl_debug_printf) (" initialize program: %s ", argv[0]); ... }
但是 dl_debug_printf 應(yīng)該怎么用?它依賴(lài)什么?有什么限制?要深入分析會(huì)很麻煩,而且在使用中很大概率會(huì)因?yàn)椴粔蛄私馄湓矶鴮?dǎo)致遇到各種坑。我們何不另辟蹊徑,自己制造出一種可靠的調(diào)試方式?
上述問(wèn)題都能得以解決!
另辟蹊徑
在 glibc 中添加一個(gè)調(diào)試函數(shù) dbg_printf, 該調(diào)試函數(shù)依賴(lài)我們"新增"的系統(tǒng)調(diào)用,并且該系統(tǒng)調(diào)用僅僅通過(guò) printk 打印的方式將傳入的參數(shù)打印到 printk 環(huán)形緩沖區(qū)中。再通過(guò) dmesg 來(lái)取數(shù)據(jù)。
如果真正地新增系統(tǒng)調(diào)用,則會(huì)導(dǎo)致需要重新編譯內(nèi)核,不夠通用。我們采用了 tracepoint hook 點(diǎn),依賴(lài)寄存器讀取修改的方式,支持以驅(qū)動(dòng)的方法實(shí)現(xiàn)一個(gè)系統(tǒng)調(diào)用。
本方法的要點(diǎn)在于:
(1) 新添加的dbg_printf不依賴(lài)于標(biāo)準(zhǔn)C庫(kù)的任何系統(tǒng)調(diào)用,實(shí)現(xiàn)了一份完全干凈的字符串格式化方法。
(2) 實(shí)現(xiàn)一個(gè)內(nèi)核模塊,在內(nèi)核模塊中 實(shí)現(xiàn)一個(gè)tracepoint hook,該 tracepoint hook會(huì)監(jiān)控sys_enter事件,這樣就可以攔截系統(tǒng)調(diào)用,而不必通過(guò)修改Linux源代碼的方式,來(lái)擴(kuò)展新的系統(tǒng)調(diào)用。
我們做了什么
該項(xiàng)目一共包含三個(gè)主體 : glibc, debug_printf 驅(qū)動(dòng), 一個(gè)簡(jiǎn)單的測(cè)試程序 test.c。
glibc
我們對(duì)glibc添加了一個(gè)補(bǔ)丁,該補(bǔ)丁在 make devel 時(shí)打到 glibc 源碼中。
這個(gè)補(bǔ)丁添加了 dbg_printf 調(diào)試函數(shù)的實(shí)現(xiàn)
int __dbg_printf (const char *fmt, ...) { int ret = 0; int len = 0; char buf[buffsize]; va_list ap; memset(buf, 0, buffsize); va_start(ap, fmt); len = dbg_vsnprintf(buf, buffsize, fmt, ap); buf[len] = 0; va_end(ap); ret = syscall_intface2(__NR_dbg, (long)buf, len + 1); return ret; } #undef _IO_printf ldbl_strong_alias (__dbg_printf, dbg_printf) ldbl_strong_alias (__dbg_printf, _IO_dbg_printf)
這個(gè)補(bǔ)丁調(diào)用 dbg_printf 調(diào)試函數(shù),打印該進(jìn)程收到的參數(shù)。
void print_args (int argc, char **argv) { int i; dbg_printf("argc : %d ", argc); for (i = 0; i < argc; i++) { dbg_printf("argv[%d] : %s ", i, argv[i]); } } LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL), int argc, char **argv, #ifdef LIBC_START_MAIN_AUXVEC_ARG ElfW(auxv_t) *auxvec, #endif __typeof (main) init, void (*fini) (void), void (*rtld_fini) (void), void *stack_end) { ... /* Perform IREL{,A} relocations. */ ARCH_SETUP_IREL (); /* print argc and argv */ print_args(argc, argv); /* The stack guard goes into the TCB, so initialize it early. */ ARCH_SETUP_TLS (); ... }
debug_printf 驅(qū)動(dòng)
利用 tracepoint sys_enter hook 點(diǎn),偽造一個(gè)不存在的系統(tǒng)調(diào)用。
test.c
一個(gè)普通的c程序,該程序會(huì)被鏈接到我們編譯的glibc上,因此我們?cè)?glibc 上的改動(dòng)(打印參數(shù)),會(huì)在運(yùn)行該程序時(shí)執(zhí)行。
#includeint main (void) { printf("Hello, glibcdbg "); return 0; }
遇到的問(wèn)題
我們?cè)?glibc 中使用 dbg_printf 時(shí)調(diào)用 vsnprintf 與 syscall 函數(shù)時(shí),居然出現(xiàn)了堆棧錯(cuò)誤,后續(xù)將其換成了自己實(shí)現(xiàn)的 dbg_vsnprintf 和 syscall_intface2。
實(shí)驗(yàn)環(huán)境
glibc的編譯與鏈接存在著許多坑,為避免讀者再次趟坑,我們提供了docker編譯環(huán)境,避免環(huán)境問(wèn)題導(dǎo)致實(shí)驗(yàn)失敗。
推薦實(shí)驗(yàn)環(huán)境
推薦使用 ubuntu 18.04 x86_64 架構(gòu)環(huán)境。
vizdl@ubuntu:~/glibcdbg$ uname -a Linux ubuntu 5.4.0-146-generic #163~18.04.1-Ubuntu SMP Mon Mar 20 1559 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
準(zhǔn)備環(huán)境依賴(lài)
該項(xiàng)目需要依賴(lài)基本的編譯工具
sudo apt install gcc make git -y
該項(xiàng)目依賴(lài)docker,所以第一步需要先安裝docker(docker需要內(nèi)核版本較高,最低內(nèi)核版本 linux 3.10),如若已安裝可跳過(guò)。
sudo curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
拉取項(xiàng)目
gitclonegit@gitee.com:kernelsoft/glibcdbg.git
構(gòu)建編譯環(huán)境 : 這步驟主要是下載glibc代碼,打上我們的補(bǔ)丁以及構(gòu)建 docker image。
make devel
編譯 : 這步驟主要是編譯驅(qū)動(dòng)模塊/測(cè)試小程序/glibc
make build
安裝驅(qū)動(dòng) : 該步驟僅安裝驅(qū)動(dòng)模塊
make install
運(yùn)行測(cè)試案例并輸出 : 運(yùn)行測(cè)試小程序然后使用 dmesg 獲取我們使用 printk 輸出在內(nèi)核的信息
make run
卸載驅(qū)動(dòng) : 該步驟僅卸載驅(qū)動(dòng)模塊
make uninstall
清理環(huán)境 : 恢復(fù)到初始項(xiàng)目狀態(tài)。
make distclean
審核編輯:劉清
-
Linux系統(tǒng)
+關(guān)注
關(guān)注
4文章
593瀏覽量
27403 -
調(diào)試器
+關(guān)注
關(guān)注
1文章
305瀏覽量
23742 -
GNU
+關(guān)注
關(guān)注
0文章
143瀏覽量
17494 -
GDB調(diào)試
+關(guān)注
關(guān)注
0文章
24瀏覽量
1447
原文標(biāo)題:硬核:如何調(diào)試glibc
文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論