浅谈GNU LIBC的版本间的变化

发布于:2024-06-17 ⋅ 阅读:(14) ⋅ 点赞:(0)

多线程调试的意外发现

昨天笔者在协助朋友调试一个多线程文件传输的应用时(传输代码不依赖开源库),发现会多次打开同一个文件。这样产生的一个结果是文件描述符泄露,应用运行一段时间后,就不能再创建新的文件描述符了。因不了解代码,笔者在目标设备上安装了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:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
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 <adhemerval.zanella@linaro.org>
Date:   Tue Jan 19 09:18:46 2021 -0300

    nptl: Move fork into libc

    This is part of the libpthread removal project:

       <https://sourceware.org/ml/libc-alpha/2019-10/msg00080.html>

    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 <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

#ifndef HAVE_PTHREAD_H
#define HAVE_PTHREAD_H 0
#endif

#if HAVE_PTHREAD_H
#include <pthread.h>
#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);
}