我能否通过编程方式更改全局偏移表/GOT或过程链接表/PLT?

8

在运行时可以确定某些平台特定功能的可用性,例如SSE或AVX。如果不想为不同的功能编译和发送不同的对象,则这非常有用。

例如,以下代码允许我检查AVX并使用提供cpuid.h头文件的gcc进行编译:

 #include "stdbool.h"
 #include "cpuid.h"

 bool has_avx(void)
 {
     uint32_t eax, ebx, ecx, edx;
     __get_cpuid(1, &eax, &ebx, &ecx, &edx);
    return ecx & bit_AVX;
 }

不要在代码中频繁执行运行时检查(例如上述内容),这样会使代码混乱、速度变慢,同时引入分支结构(尽管可以缓存检查结果以减少开销,但仍会引入分支结构);相反,我考虑使用动态链接器/装载器提供的基础设施。

在ELF平台上,具有外部链接的函数的调用已经是间接的,并通过程序链接表/PLT和全局偏移表/GOT进行。

假设存在两个内部函数,一个是基本的_do_something_basic,总是执行同样的操作,另一个是一种优化过的版本_do_something_avx,它使用AVX。我可以导出一个通用的do_something符号,并将其别名用于基本的加法:

static void _do_something_basic(…) {
    // Basic implementation
}


static void _do_something_avx(…) {
    // Optimized implementation using AVX
}

void do_something() __attribute__((alias("_do_something_basic")));

在我的库或程序的加载期间,我想通过使用has_avx检查AVX的可用性,并根据检查结果将do_something符号指向_do_something_avx。最好的情况是,如果我能够将do_something符号的初始版本指向一个自修改函数,该函数使用has_avx检查AVX的可用性,并用_do_something_basic_do_something_avx替换它自己。

理论上这应该是可能的,但我如何通过编程方式找到PLT/GOT的位置呢?是否有由ELF加载器(例如ld-linux.so.2)提供的ABI/API可用于此?我需要链接脚本来获取PLT/GOT的位置吗?至于安全考虑,如果我获得指向PLT/GOT的指针,我能否写入其中?

也许某个项目已经完成了类似的工作。 我完全意识到解决方案将高度特定于平台,但既然我已经不得不处理低级平台特定细节,例如指令集的特性,那么这没问题。

据我所知,Solaris通过在引导时运行脚本来交换受影响库的硬链接,以使其与硬件匹配,从而解决了这个问题。 - fuz
创建您的库的不同版本,然后使用dlopen加载适当的版本。不必自己操纵PLT。请参见此答案示例 - Jester
另一个你可以考虑的可能性是使用函数指针。如果你采取这种方法,你只需要进行一次检查,构建函数指针表,就不需要在运行时修改 PLT 或 GOT 了。 - David Hoelzer
1
你有权认为这是一个黑客行为,但对我来说,这是一种优雅且正确的解决方案 :) 加载适当的库,符号会自动解析到正确的版本。肯定比手动操纵PLT条目更加优雅,而且它还跨平台和具备未来性。 - Jester
2
GCC 6拥有多版本控制。你正在重新发明轮子。 - MSalters
显示剩余6条评论
3个回答

6

正如其他人建议的那样,您可以使用特定于平台的库版本。或者,如果您愿意坚持使用Linux,您可以使用(相对较新的)IFUNC重定位,它可以完全满足您的需求。

编辑:正如Sebastian所指出的,IFUNCs似乎也受到其他平台(FreeBSD、Android)的支持。但请注意,该功能并不被广泛使用,可能存在一些问题。


“ifunc”正是我所需要的,但这是否真的只适用于Linux?在我看来,它似乎是GNU和ELF特定的,因此它应该也可以在其他一些平台上工作。 - Sebastian Schrader
谢谢,我有不同的印象。我已经更新了答案。 - yugr

1

一个简单的方法是使用自己的函数指针而不是修改 PLT 中的函数指针。

例如:

extern void (*do_something)(...);

void
_do_something(...) {
     if (has_avx()) {
         do_something = _do_something_avx;
     } else { 
         do_something = _do_something_basic;
     }
     do_something(...);
}

void (*do_something)(...) = _do_something;

虽然如果你有很多这样的函数,这种方法可能会很繁琐,但是这种做法不需要任何特殊的编译器或链接器功能。(尽管如果您需要在读写指针不是原子操作的平台上使函数线程安全,则需要以某种方式使它们原子化。但这在x86平台上不是问题。) 如果您确实有许多这些函数,宏或C++模板可以帮助减少输入。


0
为什么不尝试使用gcc选项-mprefergot? 在生成位置无关代码时,使用全局偏移表而不是过程链接表来发出函数调用。 这样你只需要一个GOT跳转。

仅使用GOT而不是PLT + GOT无法解决我的问题,只是将其移动:如何通过编程方式获取GOT地址? - Sebastian Schrader
还有一个选项 -fno-plt。我不知道它是不是同一件事,或者是同一件事的更现代化的名称,但它会内联 call *foo@GOTPCREL(%rip) 而不是 call foo@plt - Peter Cordes

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