系统规格:
$ 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
延迟绑定允许在尽可能晚的时间进行绑定。然而,
ifunc
属性的解析器并不真正与链接/绑定相关,但或多或少为函数的用户提供了便利(除此之外它还有一些含义)。
正如文档所解释的,以这种方式标记的函数被隐式更改为间接函数调用。指向实际实现的(隐藏)函数指针由解析器提供。并且解析器在调用
main
之前被调用,即严格来说在 C 应用程序代码开始之前。这是保证在所有情况下调用
main
时的合规行为所必需的。
您期望的是解析器在最晚可能的时间被调用,但这要么需要更改运行时代码,具体取决于捕获所有可能情况的流程(包括获取指向运行时解析函数的指针或条件),或者每次在调用实际函数之前调用解析器。要详细理解这一点,还请记住二进制文件不再是 C 语言。第一次也可以从内联汇编调用包装的函数,因此几乎没有办法绕过第二个选项,因为编译器生成的代码无法意识到这些。类似的情况也适用于其他链接库。
我认为应该清楚的是,每次调用解析器都会增加相当多的开销,并且显然与整个概念的主要意图相矛盾。
ifunc
机制的存在正是为了避免这种情况。它还允许优化,例如编译器直接插入间接调用而不将函数指针设为全局。所以,如果你想在
main
之后解决,你需要使用专用的函数指针提供自己的机制。在这种情况下,您当然有责任自己解析指针,并通过指针(全局指针)调用函数或使用带有静态指针的包装函数(当然,但不是函数本地的)。
main
中的某些内容。无论如何,这都是一个坏主意 - 至少对于标准函数(
memcpy()
/
memclr()
,例如甚至可能由启动代码调用)。对于用户功能,我在上面给了您另一种方法。
这个错误报告,它进一步阐明了它,并且还指出了调用其他函数的问题。因为在 main()
之前,C 环境可能无法完全运行。对于 stdio 来说尤其如此。因此,您的
printf()
通话本身也可能是一个问题。
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
),更不用说惰性绑定机制了。