硬核:如何调试glibc

  • 前言
对于GNU工具链开发者而言,为了获取到一些动态重定位、函数符号解析的信息,开发者通常需要对Glibc中的动态链接器程序进行调试,一般会利用gdb来进行调试,但Glibc不仅包含字符串、文件目录、动态链接器的功能实现,而且为C程序提供了运行环境,这也使一些常规的调试手段不能正常使用,给分析C库带来一定的困难,为了解决这一难题,本文将探索一种新的方法,用于快速调试Glibc。
  • 背景

国科础石操作系统团队在开发础光智能操作系统的过程中,需要分析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 应该怎么用?它依赖什么?有什么限制?要深入分析会很麻烦,而且在使用中很大概率会因为不够了解其原理而导致遇到各种坑。我们何不另辟蹊径,自己制造出一种可靠的调试方式?

上述问题都能得以解决!

  • 另辟蹊径
  1. 在 glibc 中添加一个调试函数 dbg_printf, 该调试函数依赖我们"新增"的系统调用,并且该系统调用仅仅通过 printk 打印的方式将传入的参数打印到 printk 环形缓冲区中。再通过 dmesg 来取数据。
  2. 如果真正地新增系统调用,则会导致需要重新编译内核,不够通用。我们采用了 tracepoint hook 点,依赖寄存器读取修改的方式,支持以驱动的方法实现一个系统调用。

本方法的要点在于:

(1) 新添加的dbg_printf不依赖于标准C库的任何系统调用,实现了一份完全干净的字符串格式化方法。

(2) 实现一个内核模块,在内核模块中 实现一个tracepoint hook,该 tracepoint hook会监控sys_enter事件,这样就可以拦截系统调用,而不必通过修改Linux源代码的方式,来扩展新的系统调用。

  • 我们做了什么

该项目一共包含三个主体 : glibc, debug_printf 驱动, 一个简单的测试程序 test.c。

  • glibc

我们对glibc添加了一个补丁,该补丁在 make devel 时打到 glibc 源码中。

  1. 这个补丁添加了 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)

  1. 这个补丁调用 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编译环境,避免环境问题导致实验失败。

  1. 推荐实验环境

推荐使用 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

  1. 准备环境依赖

该项目需要依赖基本的编译工具

sudoapt install gcc make git -y

该项目依赖docker,所以第一步需要先安装docker(docker需要内核版本较高,最低内核版本 linux 3.10),如若已安装可跳过。

sudocurl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

  1. 拉取项目

gitclone git @gitee.com:kernelsoft/glibcdbg.git

  1. 构建编译环境 : 这步骤主要是下载glibc代码,打上我们的补丁以及构建 docker image。

makedevel

  1. 编译 : 这步骤主要是编译驱动模块/测试小程序/glibc

makebuild

  1. 安装驱动 : 该步骤仅安装驱动模块

makeinstall

  1. 运行测试案例并输出 : 运行测试小程序然后使用 dmesg 获取我们使用 printk 输出在内核的信息

makerun

  1. 卸载驱动 : 该步骤仅卸载驱动模块

makeuninstall

  1. 清理环境 : 恢复到初始项目状态。

makedistclean

END

欢迎关注下方公众号,获取更多精品技术文章

如需技术交流也可以发送邮箱或添加管理员微信

返回搜狐,查看更多

责任编辑:

平台声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。
阅读 ()