• 浅谈GNU LIBC的版本间的变化


    多线程调试的意外发现

    昨天笔者在协助朋友调试一个多线程文件传输的应用时(传输代码不依赖开源库),发现会多次打开同一个文件。这样产生的一个结果是文件描述符泄露,应用运行一段时间后,就不能再创建新的文件描述符了。因不了解代码,笔者在目标设备上安装了eBPF调试工具bpftrace,之后使用以下脚本调试该应用,查看该应用打开文件的调用栈:

    #!/usr/bin/bpftrace
    
    uprobe:/lib/x86_64-linux-gnu/libc-2.27.so:openat,
    uprobe:/lib/x86_64-linux-gnu/libc-2.27.so:open / pid == $1 / {
    	printf("[GLIBC6] PID: %d, comm: %s, open(%s)", pid, comm, str(arg0));
    	print(ustack);
    }
    

    注意,目标设备使用的glibc动态库为ARM64平台的libc-2.27.so,这里笔者把问题简化,在Ubuntu-18.04系统上复现了该问题。因目标设备上的glibc动态库不带有调试信息,于是笔者没有使用bpftraceSyscall-Tracepoint调试方法,因为这样可能不能获得正确的应用调用栈回溯。笔者编写了一个简单的演示代码,稍后会贴出相应代码。bpftrace期望的结果为:

    root@vmware:~/trace# bpftrace ./glibc-open.bt 5534
    Attaching 2 probes...
    [GLIBC6] PID: 5534, comm: test-open, open(/proc/uptime)
            __open64+0
            open_test_l1+9
            open_test_l2+9
            open_test_l3+10
            main+187
            __libc_start_main+231
            0x2ee258d4c544155
    

    但始终没得到以上结果;bpftrace输出的结果为空。以上为笔者把演示应用中的多线程功能禁用后得到的调试结果;这说明多线程干扰了我们的调试过程。

    带有重复符号表的libpthread.so动态库

    在笔者之前动态链接器的分析文章中提到,动态链接器/lib64/ld-linux-x86-64.so.2也提供了malloc/free等标准函数的符号:

    root@vmware:~/trace# nm -D --defined-only /lib64/ld-linux-x86-64.so.2 | grep -e malloc -e free
    0000000000017df0 T _dl_exception_free
    000000000001b800 W free
    000000000001b690 W malloc
    root@vmware:~/trace# nm -D --defined-only /lib/x86_64-linux-gnu/libc.so.6 | grep -e malloc -e free
    0000000000097910 T cfree
    0000000000097910 T free
    0000000000108820 T freeaddrinfo
    ...
    0000000000097020 T malloc
    

    于是笔者很容易地联想到,是不是多线程库libpthread也提供了一些open之类系统调用?以下结果可以确认:

    root@vmware:~/trace# nm -D --defined-only /lib/x86_64-linux-gnu/libpthread.so.0 | grep -e open -e write
    0000000000011dd0 W open
    0000000000011dd0 T __open
    0000000000011dd0 W open64
    0000000000011dd0 T __open64
    00000000000120f0 W pwrite
    00000000000120f0 W pwrite64
    

    由此可以确认,文件传输应用因存在多线程库的依赖,调用到了libpthread.so动态库中的open系统调用函数。那么笔者改进的bpftrace脚本可以检测到文件的打开:

    #!/usr/bin/bpftrace
    
    uprobe:/lib/x86_64-linux-gnu/libc-2.27.so:openat,
    uprobe:/lib/x86_64-linux-gnu/libc-2.27.so:open / pid == $1 / {
    	printf("[GLIBC6] PID: %d, comm: %s, open(%s)", pid, comm, str(arg0));
    	print(ustack);
    }
    
    uprobe:/lib/x86_64-linux-gnu/libpthread-2.27.so:open / pid == $1 / {
    	printf("[THREAD] PID: %d, comm: %s, open(%s)", pid, comm, str(arg0));
    	print(ustack);
    }
    

    调试结果如下:

    root@vmware:~/trace# bpftrace open.bt 5591
    Attaching 3 probes...
    [THREAD] PID: 5591, comm: test-open.threa, open(/proc/uptime)
            open+0
            open_test_l1+9
            open_test_l2+9
            open_test_l3+10
            main+220
            __libc_start_main+231
            0x1226258d4c544155
    

    新版本glibc库的变化

    以上调试结果笔者使用的系统为Ubuntu-22.04,始终未能复现该问题。最终确认新系统的libpthread.so库不存在重复的符号导出:

    yejq@ubuntu:~$ /lib/x86_64-linux-gnu/libc.so.6
    GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.8) stable release version 2.35.
    Copyright (C) 2022 Free Software Foundation, Inc.
    This is free software; see the source for copying conditions.
    There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
    PARTICULAR PURPOSE.
    Compiled by GNU CC version 11.4.0.
    libc ABIs: UNIQUE IFUNC ABSOLUTE
    For bug reporting instructions, please see:
    .
    yejq@ubuntu:~$ nm -D --defined-only /lib/x86_64-linux-gnu/libpthread.so.0 | grep -e open -e write
    yejq@ubuntu:~$
    

    从此可以看出不同版本的glibc确实出现了一些变化;实际上,新版本glibc中的libpthread.so库仅是一个空壳,相关的线程操作相关函数已由libc.so库提供:

    root@ubuntu:~# lsb_release -a
    No LSB modules are available.
    Distributor ID: Ubuntu
    Description:    Ubuntu 22.04.4 LTS
    Release:        22.04
    Codename:       jammy
    root@ubuntu:~# nm -D --defined-only /lib/x86_64-linux-gnu/libpthread.so.0 | grep -e pthread_create
    root@ubuntu:~# nm -D --defined-only /lib/x86_64-linux-gnu/libc.so.6 | grep -e pthread_create
    0000000000094c40 T pthread_create@GLIBC_2.2.5
    0000000000094c40 T pthread_create@@GLIBC_2.34
    

    此外,笔者还注意到,动态链接库libdl中的函数也同样由libc.so.6提供了:

    root@ubuntu:~# nm -D --defined-only /lib/x86_64-linux-gnu/libdl.so.2
    0000000000000000 A GLIBC_2.2.5
    0000000000000000 A GLIBC_2.3.3
    0000000000000000 A GLIBC_2.3.4
    0000000000001100 T __libdl_version_placeholder@GLIBC_2.2.5
    0000000000001100 T __libdl_version_placeholder@GLIBC_2.3.4
    0000000000001100 T __libdl_version_placeholder@GLIBC_2.3.3
    root@ubuntu:~# nm -D --defined-only /lib/x86_64-linux-gnu/libc.so.6 | grep -e dlopen -e dlclose
    000000000008fe30 T dlclose@GLIBC_2.2.5
    000000000008fe30 T dlclose@@GLIBC_2.34
    0000000000090680 T dlopen@GLIBC_2.2.5
    0000000000090680 T dlopen@@GLIBC_2.34
    

    GLIBC的线程库变化

    笔者通过查看glibc的代码仓库历史发现,libpthread库提供的API的大量移动自2021年1月份开始,后续开发人员几个月的陆续修改渐渐把libpthread库提供的函数移动到libc.so动态库中。存在变动的glibc版本从2.34开始:

    commit 7384193b71a1720a381b7150ed44e07b13af45d5
    Author: Adhemerval Zanella 
    Date:   Tue Jan 19 09:18:46 2021 -0300
    
        nptl: Move fork into libc
    
        This is part of the libpthread removal project:
    
           
    
        Checked on x86_64-linux-gnu.
    

    上面的链接有详细的讨论;在2019年研发人员的在邮件中有这么一句话:

    We only need one implementation.  The indirection from libc to libpthread is completely unnecessary.
    

    了解Linux内核及对线程的支持的人可能知道,早期的多线程支持是由LinuxThread实现的,后来POSIX实义了通了的多线程调用接口POSIX Thread,于是就存在两个不同的动态库提供线程的接口API:

           Over time, two threading implementations have been provided by
           the GNU C library on Linux:
    
           LinuxThreads
                  This is the original Pthreads implementation.  Since glibc
                  2.4, this implementation is no longer supported.
    
           NPTL (Native POSIX Threads Library)
                  This is the modern Pthreads implementation.  By comparison
                  with LinuxThreads, NPTL provides closer conformance to the
                  requirements of the POSIX.1 specification and better
                  performance when creating large numbers of threads.  NPTL
                  is available since glibc 2.3.2, and requires features that
                  are present in the Linux 2.6 kernel。
    

    但随着Posix Thread的标准化及广泛应用,glibc库中逐渐去掉了LinuxThread的相关代码;但NPTL的实现仍保留在单独的动态库libpthread中。而现在最新的glibc库提供的libpthread库仅是一个placeholder。其一个好处是,简化C/C++代码的链接,可以不再加入-lpthread链接选项,从而可避免很多的链接报错。

    笔者在系统开发过程中也注意到,使用clock_gettime系统调用,早期的glibc要求可执行文件链接到librt.so库,而最近几年遇到的开发环境已不再有这个链接选项的要求。这些确实都是开源社区缓慢而又可见的改进。

    相关调试代码

    笔者为了追踪这个问题,编写了一个简单的演示代码open-test.c。通过一个宏可以禁用是否使用libpthread提供的函数;两次分别的编译操作如下:

    gcc -Wall -fPIC -DHAVE_PTHREAD_H=0 -O1 -D_GNU_SOURCE -ggdb -fno-omit-frame-pointer -o test-open open-test.c
    gcc -Wall -fPIC -DHAVE_PTHREAD_H=1 -O1 -D_GNU_SOURCE -ggdb -fno-omit-frame-pointer -o test-open.thread open-test.c -lpthread
    

    最后,笔者编写的演示代码open-test.c内容如下:

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #ifndef HAVE_PTHREAD_H
    #define HAVE_PTHREAD_H 0
    #endif
    
    #if HAVE_PTHREAD_H
    #include 
    #endif
    
    #define _NO_INL_ __attribute__((__noinline__))
    static int open_test_l0(const char * arg) _NO_INL_;
    static int open_test_l1(const char * arg) _NO_INL_;
    static int open_test_l2(const char * arg) _NO_INL_;
    static int open_test_l3(const char * arg) _NO_INL_;
    
    int main(int argc, char *argv[])
    {
    	int i, ret;
    
    	fprintf(stdout, "PID: %ld\n", (long) getpid());
    #if HAVE_PTHREAD_H
    	fprintf(stdout, "Current pthread ID: %ld\n", (long) pthread_self());
    #endif
    	fprintf(stdout, "Press any key to continue...\n");
    	fflush(stdout);
    	(void) getchar();
    
    	for (i = 1; i < argc; ++i) {
    		const char *argp;
    
    		argp = argv[i];
    		if (argp == NULL || argp[0] == '\0') {
    			fprintf(stderr, "Error, invalid argument at %d\n", i);
    			fflush(stderr);
    		}
    
    		ret = open_test_l3(argp);
    		fprintf(stderr, "open(%s) has returned: %d\n", argp, ret);
    		fflush(stderr);
    	}
    
    	fprintf(stdout, "Press any key to exit...\n");
    	fflush(stdout);
    	(void) getchar();
    	return 0;
    }
    
    int open_test_l0(const char * arg)
    {
    	int fd;
    	fd = open(arg, O_RDONLY);
    	if (fd >= 0)
    		close(fd);
    	return fd;
    }
    
    int open_test_l1(const char * arg)
    {
    	int ret;
    	ret = open_test_l0(arg);
    	return ret;
    }
    
    int open_test_l2(const char * arg)
    {
    	return open_test_l1(arg);
    }
    
    int open_test_l3(const char * arg)
    {
    	return open_test_l2(arg);
    }
    
  • 相关阅读:
    Style样式设置器
    “百模大战”大模型哪家强?开源的全面评测来了!
    k8s上部署Harbor通过Nginx-Ingress域名访问
    Abbkine ExKine 总蛋白提取试剂盒实验建议&FAQ
    Shell Script注释和debug
    GOPS·2023上海站 | 提前剧透!阿里、腾讯、字节、擎创等专家齐聚上海,共话互联网运维
    【Linux】第十一章 进程信号(概念+产生信号+阻塞信号+捕捉信号)
    map容器使用及员工分组实例
    【AI视野·今日Robot 机器人论文速览 第三十九期】Fri, 22 Sep 2023
    Python入门学习篇(一)——注释&变量&输入输出
  • 原文地址:https://blog.csdn.net/yeholmes/article/details/139582403