glibc 的特性(features)宏与其对 ABI 兼容性的影响

 

我在为 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 语言等级的开关代替。