无法使用具有惰性绑定的 IFUNC 解析器

问题描述 投票:0回答:2

系统规格:

$ uname -a
Linux 5.4.0-66-generic #74-Ubuntu SMP Wed Jan 27
22:54:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
$ gcc -v
...
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04) 
$ inxi
CPU: Quad Core Intel Core i7-1065G7 (-MT MCP-) speed/min/max: 1002/400/3900 MHz 
Kernel: 5.4.0-66-generic x86_64 Up: 6d 7h 59m Mem: 4831.2/7730.1 MiB (62.5%) 
Storage: 476.94 GiB (28.7% used) Procs: 349 Shell: bash 5.0.17 inxi: 3.0.38 

我尝试将 IFUNC 与惰性绑定一起使用,但无论我做什么,解析器都会在调用 main 之前保持运行 - 这意味着解析器在运行时之前运行。

目标ELF中没有BIND_NOW符号。

LD_BIND_NOW 未设置(我尝试在将其绑定到 0 时执行相同操作)。

我在文档中看不到任何关于为什么会发生这种情况的原因。

main.c:

#include <stdio.h>
#include <stdlib.h>


int foo_1() { return 1; }
int foo_2() { return 2; }
int foo_3() { return 3; }

extern int foo(void);
int foo(void) __attribute__((ifunc ("resolve_foo")));


int main() {
    printf("main started\n");
    printf("foo() = %d\n", foo());
    return 0;
}


static void *resolve_foo(void) {
    int res = 0;//atoi(getenv("FOO")); 
    printf("resolver started\n");
    if (res == 1)
        return foo_1;
    if (res == 2)
        return foo_2;
    return foo_3;
    
}

执行:

$ gcc -zlazy -o t main.c
$ ./t
resolver started
main started
foo() = 3
c
2个回答
1
投票

延迟绑定允许在尽可能晚的时间进行绑定。然而,

ifunc
属性的解析器并不真正与链接/绑定相关,但或多或少为函数的用户提供了便利(除此之外它还有一些含义)。

正如文档所解释的,以这种方式标记的函数被隐式更改为间接函数调用。指向实际实现的(隐藏)函数指针由解析器提供。并且解析器在调用

main
之前被调用,即严格来说在 C 应用程序代码开始之前。这是保证在所有情况下
调用 
main 时的合规行为所必需的。

您期望的是解析器在最晚可能的时间被调用,但这要么需要更改运行时代码,具体取决于捕获所有可能情况的流程(包括获取指向运行时解析函数的指针或条件),或者每次在调用实际函数之前调用解析器。

要详细理解这一点,还请记住二进制文件不再是 C 语言。第一次也可以从内联汇编调用包装的函数,因此几乎没有办法绕过第二个选项,因为编译器生成的代码无法意识到这些。类似的情况也适用于其他链接库。

我认为应该清楚的是,每次调用解析器都会增加相当多的开销,并且显然与整个概念的主要意图相矛盾。

ifunc

机制的存在正是为了避免这种情况。它还允许优化,例如编译器直接插入间接调用而不将函数指针设为全局。

所以,如果你想在

main

之后解决,你需要使用专用的函数指针提供自己的机制。在这种情况下,您当然有责任自己解析指针,并通过指针(全局指针)调用函数或使用带有静态指针的包装函数(当然,但不是函数本地的)。


这么说,我想知道你的实际问题是什么。我怀疑您希望您的解析器依赖于

main

 中的某些内容。无论如何,这都是一个坏主意 - 至少对于标准函数(
memcpy()
/
memclr()
,例如甚至可能由启动代码调用)。对于用户功能,我在上面给了您另一种方法。


更新: 我刚刚发现了关于 ifunc 的

这个错误报告,它进一步阐明了它,并且还指出了调用其他函数的问题。因为在 main()

 之前,C 环境可能无法完全运行。对于 stdio 来说尤其如此。因此,您的 
printf()
 通话本身也可能是一个问题。


0
投票
我认为在谈论惰性绑定时关键点是使用动态链接。可以重新组织代码以使

foo()

 从共享库动态链接。那么惰性绑定就可以工作了。

foo.c:

#include <stdio.h> #include <stdlib.h> int foo_1() { return 1; } int foo_2() { return 2; } int foo_3() { return 3; } int foo(void) __attribute__((ifunc ("resolve_foo"))); static void *resolve_foo(void) { int res = 0;//atoi(getenv("FOO")); printf("resolver started\n"); if (res == 1) return foo_1; if (res == 2) return foo_2; return foo_3; }
main.c:

#include <stdio.h> extern int foo(); int main() { printf("main started\n"); printf("foo() = %d\n", foo()); return 0; }
编译并运行:

$ gcc -fPIC -c -g foo.c main.c $ gcc -shared foo.o -o libfoo.so $ gcc main.o -o main.shared -L. -lfoo -Wl,-rpath,`pwd`,-z,lazy $ ./main.shared main started resolver started foo() = 3
对于

静态链接来说,ifunc解析器的执行时间是在main()

函数之前完成的,或者更准确地说,它是在
_start()
函数中完成各种流程准备的。通过检查 
resolve_xxx()
 的断点,可以使用 GDB 轻松跟踪这一点。

-zlazy

 是一个 gcc 链接选项,用于影响 
PLT(过程链接表)函数的绑定行为。据我所知,只有从共享对象中动态链接才能为ELF文件生成PLT结构。你原来的代码是静态链接风格的,所以foo函数不会有PLT项(通过检查汇编代码中是否存在相应的符号foo@plt
),更不用说惰性绑定机制了。

© www.soinside.com 2019 - 2024. All rights reserved.