我在为 Debian 打包 drat-trim 项目时,发现生成的可执行文件居然依赖 glibc >= 2.39
,而我打包的另一个纯 C 项目 kissat 则只依赖 glibc >= 2.34
。明明都只是用了简单的 C 标准库,怎么会有这样的差别呢?
用 readelf
检查后,能看到 drat-trim
的可执行文件依赖了以下的符号:
Symbol table '.dynsym' contains 31 entries:
Num: Value Size Type Bind Vis Ndx Name
7: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __isoc23_fscanf@GLIBC_2.38 (4)
19: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __isoc23_strtol@GLIBC_2.38 (4)
从符号名可以看出,这两个函数应该在 C23 中定义发生了变动,因此 GLIBC 提供了新版的符号,这也可以查阅到相关的变更说明。
然而问题是,明明我用了 -std=c++99
编译,怎么会依赖上如此新版的符号呢?经过对 glibc 源码的阅读,我终于发现了元凶:我在编译时为了使用 POSIX 扩展的标准库函数,定义了 _GNU_SOURCE
特性宏。
问题复现
上述现象用以下的简单测试就可以复现(测试环境为 gcc 14, glibc 2.40):
/tmp# cat test.c
#include <stdio.h>
int main(int argc) {
return fscanf(stdin, "%d", &argc);
}
# w/ _GNU_SOURCE
/tmp# gcc -std=c99 -D_GNU_SOURCE -o test test.c
/tmp# nm test | grep 'fscanf'
U __isoc23_fscanf@GLIBC_2.38
# w/o _GNU_SOURCE
/tmp# gcc -std=c99 -o test test.c
/tmp# nm test | grep 'fscanf'
U __isoc99_fscanf@GLIBC_2.7
可以看到只要定义了 _GNU_SOURCE
,就会导致 fscanf
变成 GLIBC_2.38
版本的符号。
问题溯源
阅读 glibc 2.40 源码中实际声明 fscanf
函数的头文件 libio/stdio.h
,其中涉及 fscanf
的部分,去除了一些无关语句后可以等价为:
# if __GLIBC_USE (C23_STRTOL)
extern int __isoc23_fscanf (FILE *__restrict __stream,
const char *__restrict __format, ...) __wur
__nonnull ((1));
# define fscanf __isoc23_fscanf
# else
extern int __isoc99_fscanf (FILE *__restrict __stream,
const char *__restrict __format, ...) __wur
__nonnull ((1));
# define fscanf __isoc99_fscanf
# endif
再跟踪 C23_STRTOL
,可以从 include/features.h
中看到一些关键的定义链条:
/* If _GNU_SOURCE was defined by the user, turn on all the other features. */
#ifdef _GNU_SOURCE
# undef _ISOC95_SOURCE
# define _ISOC95_SOURCE 1
# undef _ISOC99_SOURCE
# define _ISOC99_SOURCE 1
# undef _ISOC11_SOURCE
# define _ISOC11_SOURCE 1
# undef _ISOC23_SOURCE
# define _ISOC23_SOURCE 1
# undef _POSIX_SOURCE
# define _POSIX_SOURCE 1
# undef _POSIX_C_SOURCE
# define _POSIX_C_SOURCE 200809L
# undef _XOPEN_SOURCE
# define _XOPEN_SOURCE 700
# undef _XOPEN_SOURCE_EXTENDED
# define _XOPEN_SOURCE_EXTENDED 1
# undef _LARGEFILE64_SOURCE
# define _LARGEFILE64_SOURCE 1
# undef _DEFAULT_SOURCE
# define _DEFAULT_SOURCE 1
# undef _ATFILE_SOURCE
# define _ATFILE_SOURCE 1
# undef _DYNAMIC_STACK_SIZE_SOURCE
# define _DYNAMIC_STACK_SIZE_SOURCE 1
#endif
/* This is to enable the ISO C23 extension. */
#if (defined _ISOC23_SOURCE \
|| (defined __STDC_VERSION__ && __STDC_VERSION__ > 201710L))
# define __GLIBC_USE_ISOC23 1
#else
# define __GLIBC_USE_ISOC23 0
#endif
...
#if __GLIBC_USE (ISOC23)
# define __GLIBC_USE_C23_STRTOL 1
#else
# define __GLIBC_USE_C23_STRTOL 0
#endif
也就是说,定义 _GNU_SOURCE
导致了 _ISOC23_SOURCE
被定义,最终导致 glibc 使用了新的符号版本。
进一步阅读 features.h
上面关于这些特性宏的说明,有:
__STRICT_ANSI__ ISO Standard C.
_ISOC99_SOURCE Extensions to ISO C89 from ISO C99.
_ISOC11_SOURCE Extensions to ISO C99 from ISO C11.
_ISOC23_SOURCE Extensions to ISO C99 from ISO C23.
_ISOC2X_SOURCE Old name for _ISOC23_SOURCE.
__STDC_WANT_LIB_EXT2__
Extensions to ISO C99 from TR 27431-2:2010.
__STDC_WANT_IEC_60559_BFP_EXT__
Extensions to ISO C11 from TS 18661-1:2014.
__STDC_WANT_IEC_60559_FUNCS_EXT__
Extensions to ISO C11 from TS 18661-4:2015.
__STDC_WANT_IEC_60559_TYPES_EXT__
Extensions to ISO C11 from TS 18661-3:2015.
__STDC_WANT_IEC_60559_EXT__
ISO C23 interfaces defined only in Annex F.
_POSIX_SOURCE IEEE Std 1003.1.
_POSIX_C_SOURCE If ==1, like _POSIX_SOURCE; if >=2 add IEEE Std 1003.2;
if >=199309L, add IEEE Std 1003.1b-1993;
if >=199506L, add IEEE Std 1003.1c-1995;
if >=200112L, all of IEEE 1003.1-2004
if >=200809L, all of IEEE 1003.1-2008
_XOPEN_SOURCE Includes POSIX and XPG things. Set to 500 if
Single Unix conformance is wanted, to 600 for the
sixth revision, to 700 for the seventh revision.
_XOPEN_SOURCE_EXTENDED XPG things and X/Open Unix extensions.
_LARGEFILE_SOURCE Some more functions for correct standard I/O.
_LARGEFILE64_SOURCE Additional functionality from LFS for large files.
_FILE_OFFSET_BITS=N Select default filesystem interface.
_ATFILE_SOURCE Additional *at interfaces.
_DYNAMIC_STACK_SIZE_SOURCE Select correct (but non compile-time constant)
MINSIGSTKSZ, SIGSTKSZ and PTHREAD_STACK_MIN.
_GNU_SOURCE All of the above, plus GNU extensions.
_DEFAULT_SOURCE The default set of features (taking precedence over
__STRICT_ANSI__).
_FORTIFY_SOURCE Add security hardening to many library functions.
Set to 1, 2 or 3; 3 performs stricter checks than 2, which
performs stricter checks than 1.
_REENTRANT, _THREAD_SAFE
Obsolete; equivalent to _POSIX_C_SOURCE=199506L.
可以看到定义 _GNU_SOURCE
会开启文件中所有的特性,也就是无条件启用了当前 glibc 支持的所有 C 特性和扩展,因此导致 glibc 选择了更新的实现 __isoc23_fscanf
。
这和我一贯的印象(以及若干 TUNA 群友的印象)并不一致。我们都觉得,顾名思义,它之应该启用标准库中的部分 GNU 扩展函数,而具体的语言级别、标准库行为都应该由编译器选项来统一控制。万万没想到,有这样的一些宏,能让 glibc 无视编译器的语言版本限制,影响自身的具体行为。
我对 glibc 的此行为感到有些困惑,因为很多时候用户不会预期(也不期望) glibc 因此使用最新的特性和符号。如此实现会不必要地破坏部分代码的 ABI 兼容性,甚至很可能影响程序的可观测行为。
解决方案
- 如果你不需要更好的向后 ABI 兼容性,也不在乎可能新标准库可能有变化的行为,那么这不是什么问题,只管用就可以了;
- 否则,似乎并没有什么好办法。在可能的情况下(比如只是要用一些 POSIX 的库函数),尽量少使用
_GNU_SOURCE
,而是用_DEFAULT_SOURCE
等不会影响 glibc 语言等级的开关代替。