如何强制链接到旧版的libc `fcntl`而不是`fcntl64`?

15

看起来GLIBC 2.28 (于2018年8月发布)对fcntl做出了相当激进的更改。在<fcntl.h>中,定义已更改为不再是外部函数,而是一个到fcntl64的#define

结果是,如果您在使用此glibc的系统上编译代码--如果它使用fcntl() 任何时候--生成的二进制文件将无法在2018年8月之前的系统上执行。这影响了相当多的应用程序... fcntl()的手册页面显示它是一小部分子函数的入口点:

https://linux.die.net/man/2/fcntl

如果您能告诉链接器您需要哪个特定版本的GLIBC函数,那将是很好的。但我找到的最接近的是另一篇帖子中描述的这个技巧:链接到.so文件中较旧符号版本的答案。这有点复杂。没有一个带有va_list的vffcntl来调用变参函数fcntl。在这种情况下,您无法转发变参函数的调用。当你有稳定的代码并且依赖性非常低时,如果在当前Ubuntu上构建它,然后可执行文件拒绝在仅发布一年前(几乎到日期)的另一个Ubuntu上运行,那就让人失望了。对此,有什么解决方法吗?
2个回答

13
对此有何救济措施?
GLIBC没有一种定义“#define USE_FCNTL_NOT_FCNTL64”的方法,这说明了很多问题。无论对错如何,大多数操作系统和工具链制造商似乎已经决定,从新版本的系统针对旧版本的二进制文件不是高优先级。
最简单的方法是保留一个虚拟机,用于构建项目的最旧版本的操作系统和工具链。每当您认为该二进制文件将在旧系统上运行时使用它来生成二进制文件。
但是...
如果您相信您的用途属于不受偏移量大小更改影响的fcntl() 调用子集(也就是说,您不使用字节范围锁定)
或者愿意审核代码以使用向后兼容的结构定义
并且不害怕巫术
...那么请继续阅读。
变量名不同,而fcntl是一种变参函数,没有接受va_list的vffcntl。在这种情况下,您无法转发变参函数的调用。
因此,要应用提到的包装技巧,您必须逐行查看fcntl() 的接口文档,并解包变参函数,然后使用新的变参调用调用包装版本。

幸运的是,这不是一个很难的情况(fcntl函数需要0或1个参数,并有文档类型)。为了尝试帮助其他人节省一些麻烦,这里提供了相关代码。请确保将--wrap=fcntl64传递给链接器(如果不直接调用ld,则为-Wl,--wrap=fcntl64):

asm (".symver fcntl64, fcntl@GLIBC_2.2.5");

extern "C" int __wrap_fcntl64(int fd, int cmd, ...)
{
    int result;
    va_list va;
    va_start(va, cmd);

    switch (cmd) {
      //
      // File descriptor flags
      //
      case F_GETFD: goto takes_void;
      case F_SETFD: goto takes_int;

      // File status flags
      //
      case F_GETFL: goto takes_void;
      case F_SETFL: goto takes_int;

      // File byte range locking, not held across fork() or clone()
      //
      case F_SETLK: goto takes_flock_ptr_INCOMPATIBLE;
      case F_SETLKW: goto takes_flock_ptr_INCOMPATIBLE;
      case F_GETLK: goto takes_flock_ptr_INCOMPATIBLE;

      // File byte range locking, held across fork()/clone() -- Not POSIX
      //
      case F_OFD_SETLK: goto takes_flock_ptr_INCOMPATIBLE;
      case F_OFD_SETLKW: goto takes_flock_ptr_INCOMPATIBLE;
      case F_OFD_GETLK: goto takes_flock_ptr_INCOMPATIBLE;

      // Managing I/O availability signals
      //
      case F_GETOWN: goto takes_void;
      case F_SETOWN: goto takes_int;
      case F_GETOWN_EX: goto takes_f_owner_ex_ptr;
      case F_SETOWN_EX: goto takes_f_owner_ex_ptr;
      case F_GETSIG: goto takes_void;
      case F_SETSIG: goto takes_int;

      // Notified when process tries to open or truncate file (Linux 2.4+)
      //
      case F_SETLEASE: goto takes_int;
      case F_GETLEASE: goto takes_void;

      // File and directory change notification
      //
      case F_NOTIFY: goto takes_int;

      // Changing pipe capacity (Linux 2.6.35+)
      //
      case F_SETPIPE_SZ: goto takes_int;
      case F_GETPIPE_SZ: goto takes_void;

      // File sealing (Linux 3.17+)
      //
      case F_ADD_SEALS: goto takes_int;
      case F_GET_SEALS: goto takes_void;

      // File read/write hints (Linux 4.13+)
      //
      case F_GET_RW_HINT: goto takes_uint64_t_ptr;
      case F_SET_RW_HINT: goto takes_uint64_t_ptr;
      case F_GET_FILE_RW_HINT: goto takes_uint64_t_ptr;
      case F_SET_FILE_RW_HINT: goto takes_uint64_t_ptr;

      default:
        fprintf(stderr, "fcntl64 workaround got unknown F_XXX constant")
    }

  takes_void:
    va_end(va);
    return fcntl64(fd, cmd);

  takes_int:
    result = fcntl64(fd, cmd, va_arg(va, int));
    va_end(va);
    return result;

  takes_flock_ptr_INCOMPATIBLE:
    //
    // !!! This is the breaking case: the size of the flock
    // structure changed to accommodate larger files.  If you
    // need this, you'll have to define a compatibility struct
    // with the older glibc and make your own entry point using it,
    // then call fcntl64() with it directly (bear in mind that has
    // been remapped to the old fcntl())
    // 
    fprintf(stderr, "fcntl64 hack can't use glibc flock directly");
    exit(1);

  takes_f_owner_ex_ptr:
    result = fcntl64(fd, cmd, va_arg(va, struct f_owner_ex*));
    va_end(va);
    return result;

  takes_uint64_t_ptr:
    result = fcntl64(fd, cmd, va_arg(va, uint64_t*));
    va_end(va);
    return result;
}

请注意,根据实际构建的版本,如果它们不可用,则可能必须#ifdef掉其中一些标志部分。
这影响相当多的应用程序... fcntl()的手册页面显示它是小宇宙子函数的入口点
...这应该对人们来说是一个教训:避免通过变参滥用创建这样的“厨房水槽”函数。

如果有人想要讨论这篇文章的动机,可以参考这个 Discourse 帖子,它讨论了二进制可传递性在当今世界中的作用。链接为:https://forum.rebol.info/t/dynamic-linking-static-linking-and-the-march-of-progress/1231。 - HostileFork says dont trust SE
@AndrewHenle 重新排列和编辑以防止实际使用锁定时的不兼容性...(我没有这样做)。 - HostileFork says dont trust SE
1
“从新版本的系统针对旧版本的二进制文件并不是一个高优先级的任务。”-- 这不是“不太重要”,而是一个明确的非目标。 - Employed Russian
@HostileFork 希望在旧版本的操作系统、库甚至CPU上运行新版本构建的东西是根本有缺陷的。没有任何东西可以保证向前兼容。"未来不会出现任何不能在此库/操作系统上运行的东西"? 没有人能做出这样的承诺,你也不能依赖它。 - Andrew Henle
1
@AndrewHenle “未来不会出现任何不能在这个库/操作系统上运行的东西”?没有人能做出这样的承诺,你也不能依赖它。”=> 这是明显错误的 - 如果一个人可以使用旧版本的操作系统/工具链来获得结果,新版本的工具链可能也有一个开关来获得该结果。不久之前,发布一个编译器的版本,无法构建可在被认为是“最新”的系统上运行的二进制文件,被认为是不可接受的。我看到许多人似乎已经放弃了这个目标。 - HostileFork says dont trust SE
显示剩余3条评论

4
如何强制链接到旧的libc fcntl而不是fcntl64
编译时针对旧版本的libc进行编译即可。
因为glibc不支持向前兼容, 它只支持向后兼容
GNU C库旨在成为一个向后兼容、便携和高性能的ISO C库。它旨在遵循所有相关标准,包括ISO C11、POSIX.1-2008和IEEE 754-2008。
如果没有任何向前兼容的保证,你就不知道还有什么其他问题会出现

此外,如果可能的话,尽量使用普通软件包构建过程。在大多数发行版中,构建软件包的步骤通常包括安装该发行版中使用的库的版本,这样,如果您的构建过程运行测试,就可以及早发现任何问题。 - XANi
如果包含如何操作的说明,这个答案会更好。只需将旧的 glibc.so 复制到 /some/random/path 并告诉链接器首先查找那里是否足够吗? - MSalters
@MSalters 确实如此,但编译和链接使用除任何系统上安装的标准库之外的一组库,需要完整且正确的答案,几乎需要一本书。 - Andrew Henle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接