- 前言
- 背景
国科础石操作系统团队在开发础光智能操作系统的过程中,需要分析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大概做了什么 :
# include<stdio.h>
intsum( inta, intb) { returna + b; }
intmain( void) { inta = 35; intb = 24; printf( "%d + %d = %d\n", a, b, sum(a, b)); return0; }
当我们编写一个c程序时,在 glibc 的帮助下会给我们一种错觉 :当我们运行编译
出来的二进制文件,操作系统直接运行到main函数,然后执行由提供的函数或我
们自己编写的逻辑代码,在上述例子中,我们使用了libc提供的"printf"打印函数。
我们自己编写了一个求和的逻辑代码。那么libc真的就是提供一些函数接口的
库么?
其实对于操作系统而言,它会都不"认识"main函数。而一个进程的执行也并非由 main 函数开始的。在链接时,链接器会设置函数入口,而该可执行程序入口不是 main。
我们通过 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), intargc, char**argv, #ifdef LIBC_START_MAIN_AUXVEC_ARGElfW(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) ( "\ninitialize program: %s\n\n", 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 ( constchar*fmt, ...) {intret = 0; intlen = 0; charbuf[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);
returnret; }
# undef_IO_printf ldbl_strong_alias (__dbg_printf, dbg_printf)ldbl_strong_alias (__dbg_printf, _IO_dbg_printf)
- 这个补丁调用 dbg_printf 调试函数,打印该进程收到的参数。
voidprint_args( intargc, char**argv ) { inti; dbg_printf( "argc : %d\n", argc); for(i = 0; i < argc; i++) { dbg_printf( "argv[%d] : %s\n", i, argv[i]); }}
LIBC_START_MAIN ( int(*main) ( int, char**, char** MAIN_AUXVEC_DECL), intargc, char**argv, #ifdef LIBC_START_MAIN_AUXVEC_ARGElfW(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 上的改动(打印参数),会在运行该程序时执行。
# include<stdio.h>
intmain( void) { printf( "Hello, glibcdbg\n"); return0; }
- 遇到的问题
我们在 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 15:02:59 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
- 准备环境依赖
该项目需要依赖基本的编译工具
sudoapt install gcc make git -y
该项目依赖docker,所以第一步需要先安装docker(docker需要内核版本较高,最低内核版本 linux 3.10),如若已安装可跳过。
sudocurl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
- 拉取项目
gitclone git @gitee.com:kernelsoft/glibcdbg.git
- 构建编译环境 : 这步骤主要是下载glibc代码,打上我们的补丁以及构建 docker image。
makedevel
- 编译 : 这步骤主要是编译驱动模块/测试小程序/glibc
makebuild
- 安装驱动 : 该步骤仅安装驱动模块
makeinstall
- 运行测试案例并输出 : 运行测试小程序然后使用 dmesg 获取我们使用 printk 输出在内核的信息
makerun
- 卸载驱动 : 该步骤仅卸载驱动模块
makeuninstall
- 清理环境 : 恢复到初始项目状态。
makedistclean
END
欢迎关注下方公众号,获取更多精品技术文章
如需技术交流也可以发送邮箱或添加管理员微信
责任编辑: