背景
国科础石操作系统团队在开发础光智能操作系统的过程中,需要分析glibc启动过程中的异常信息,在此过程中探索出一条快速调试glibc流程的方法。
由于glibc启动代码复杂,printf、ptrace等辅助调试手段还不能正常使用,给分析过程带来困难。本文探索的方法避免了对printf、ptrace的依赖。
glibc 简介
glibc是Linux系统中常用的C运行时库,它是GNU项目的一部分,是一组函数和子例程的集合,为Linux操作系统上的C程序提供了基本的运行时支持。
glibc提供了Linux系统所需的底层功能和工具,包括内存管理、线程支持、网络编程、文件系统访问、数学计算、时间和日期处理、本地化支持等等。它还提供了标准的C库函数,如字符串操作、输入输出、数据结构操作等等。
glibc还提供了一些高级功能,例如动态内存管理、线程安全、多语言支持、安全性等等。它提供了一些重要的头文件和宏定义,例如stdio.h、stdlib.h、string.h、time.h等等。
glibc还提供了一些调试和性能分析工具,例如gdb调试器和strace系统调用跟踪器等。
总之,glibc是Linux系统中最重要的C运行时库之一,提供了许多基本和高级功能,为开发人员提供了强大的工具和支持,使得他们能够更加轻松地编写高质量、高效、可靠的C程序。
glibc是什么?
举个简单的例子来解释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; }
当我们编写一个c程序时,在 glibc 的帮助下会给我们一种错觉 : 当我们运行编译出来的二进制文件,操作系统直接运行到 main 函数,然后执行由提供的函数或我们自己编写的逻辑代码,在上述例子中,我们使用了libc提供的 "printf" 打印函数。我们自己编写了一个求和的逻辑代码。那么glibc真的就是提供一些函数接口的库么?
其实对于操作系统而言,它会都不"认识"main函数。而一个进程的执行也并非由 main 函数开始的。在链接时,链接器会设置函数入口,而该可执行程序入口不是 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 读取该可执行文件的头部信息,可以看到 "Entry point address: 0x400580",表明可执行程序的入口地址是 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
我们通过 readelf -s 指令查看该二进制的符号表,可以看到, elf 执行的第一个"函数"是 _start,而不是 main。可执行文件执行到main函数之前,其实 glibc 偷偷加了一些代码。这部分代码笼统地讲其实就是做了一些进程环境设置的工作,让编写c代码的程序员可以避免每次都要编写重复的进程的环境设置!glibc真切地做到了做好事不留名:)但是今天我们提供一种方式,让大家都能看到glibc做的好事~
glibc 开发者如何调试 glibc?
在 glibc 中,一些地方调用c库函数会出现问题,特别是 _start -> main 这段代码,由于进程环境未初始化,导致大多数的 glibc 的函数运行的前提无法保证,于是绝大多数 glibc 的函数无法在这段代码内运行,这导致对glibc的观察可谓是困难重重,如何提供一种简单通用且可靠的调试方法一直是业界的难题。
我们在 glibc 入口函数找到了一些代码,并调用自定义函数dl_debug_printf来进行调试输出:
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 应该怎么用?它依赖什么?有什么限制?要深入分析会很麻烦,而且在使用中很大概率会因为不够了解其原理而导致遇到各种坑。我们何不另辟蹊径,自己制造出一种可靠的调试方式?
上述问题都能得以解决!
另辟蹊径
在 glibc 中添加一个调试函数 dbg_printf, 该调试函数依赖我们"新增"的系统调用,并且该系统调用仅仅通过 printk 打印的方式将传入的参数打印到 printk 环形缓冲区中。再通过 dmesg 来取数据。
如果真正地新增系统调用,则会导致需要重新编译内核,不够通用。我们采用了 tracepoint hook 点,依赖寄存器读取修改的方式,支持以驱动的方法实现一个系统调用。
本方法的要点在于:
(1) 新添加的dbg_printf不依赖于标准C库的任何系统调用,实现了一份完全干净的字符串格式化方法。
(2) 实现一个内核模块,在内核模块中 实现一个tracepoint hook,该 tracepoint hook会监控sys_enter事件,这样就可以拦截系统调用,而不必通过修改Linux源代码的方式,来扩展新的系统调用。
我们做了什么
该项目一共包含三个主体 : glibc, debug_printf 驱动, 一个简单的测试程序 test.c。
glibc
我们对glibc添加了一个补丁,该补丁在 make devel 时打到 glibc 源码中。
这个补丁添加了 dbg_printf 调试函数的实现
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)
这个补丁调用 dbg_printf 调试函数,打印该进程收到的参数。
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 驱动
利用 tracepoint sys_enter hook 点,伪造一个不存在的系统调用。
test.c
一个普通的c程序,该程序会被链接到我们编译的glibc上,因此我们在 glibc 上的改动(打印参数),会在运行该程序时执行。
#includeint main (void) { printf("Hello, glibcdbg "); return 0; }
遇到的问题
我们在 glibc 中使用 dbg_printf 时调用 vsnprintf 与 syscall 函数时,居然出现了堆栈错误,后续将其换成了自己实现的 dbg_vsnprintf 和 syscall_intface2。
实验环境
glibc的编译与链接存在着许多坑,为避免读者再次趟坑,我们提供了docker编译环境,避免环境问题导致实验失败。
推荐实验环境
推荐使用 ubuntu 18.04 x86_64 架构环境。
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
准备环境依赖
该项目需要依赖基本的编译工具
sudo apt install gcc make git -y
该项目依赖docker,所以第一步需要先安装docker(docker需要内核版本较高,最低内核版本 linux 3.10),如若已安装可跳过。
sudo curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
拉取项目
gitclonegit@gitee.com:kernelsoft/glibcdbg.git
构建编译环境 : 这步骤主要是下载glibc代码,打上我们的补丁以及构建 docker image。
make devel
编译 : 这步骤主要是编译驱动模块/测试小程序/glibc
make build
安装驱动 : 该步骤仅安装驱动模块
make install
运行测试案例并输出 : 运行测试小程序然后使用 dmesg 获取我们使用 printk 输出在内核的信息
make run
卸载驱动 : 该步骤仅卸载驱动模块
make uninstall
清理环境 : 恢复到初始项目状态。
make distclean
审核编辑:刘清
-
Linux系统
+关注
关注
4文章
591浏览量
27346 -
调试器
+关注
关注
1文章
300浏览量
23685 -
GNU
+关注
关注
0文章
143浏览量
17475 -
GDB调试
+关注
关注
0文章
24浏览量
1437
原文标题:硬核:如何调试glibc
文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论