如何调试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 


int 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 上的改动(打印参数),会在运行该程序时执行。

 

#include 


int 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

 

拉取项目

 

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

 

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

 

make devel

 

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

 

make build

 

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

 

make install

 

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

 

make run

 

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

 

make uninstall

 

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

 

make distclean

 





审核编辑:刘清

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分