如何强制gcc在PIC代码中直接调用函数?

8

考虑以下函数:

extern void test1(void);
extern void test2(void) {
    test1();
}

在amd64 Linux上,如果没有使用-fpic,gcc生成的代码如下:

test2:
    jmp test1

当我使用-fpic编译时,gcc会显式地通过PLT进行调用以启用符号重定位:
test2:
    jmp test1@PLT

对于位置无关代码,这并不是必需的,如果我不想支持,可以省略。如果必要,链接器仍会将跳转目标重写为PLT符号。

如何在不更改源代码且不使编译后的代码不适用于共享库的情况下,使函数调用直接到达它们的目标,而不是显式地通过PLT进行?


1
你是指在你的库/可执行文件内部的调用吗?这应该可以通过某种方式实现,可能是通过定义一个私有别名或其他方法。但是对于只与动态链接的库中的函数进行调用,我不确定运行时链接器是否能够解析这样的引用。 - Peter Cordes
@PeterCordes 如果没有使用-fpic选项,链接器会自动将对符号的引用重写为PLT引用(如果适当/必需)。这就是我想要的行为,即编译器生成对所有函数的普通调用,而链接器重写去往另一个共享对象的调用。 - fuz
1
-fno-semantic-interposition 这个选项是否符合您的需求?另请参阅 https://dev59.com/QpHea4cB1Zd3GeqPtcHI。这个问题是否与其中任何一个重复? - Peter Cordes
@PeterCordes 看起来没有什么区别,但我会记住这个选项。 - fuz
未来的读者请参阅 https://dev59.com/HWgv5IYBdhLWcg3wF9BP,了解如何在跨共享对象边界调用时避免使用 PLT(过程链接表)。 (例如从程序调用库) - Peter Cordes
2个回答

3
如果您将test1()声明为隐藏的(__attribute__((__visibility__("hidden")))),则跳转将是直接的。
现在,test1()可能没有在其源翻译单元中定义为隐藏,但我认为除了C语言保证&test1 == &test1可能会在运行时对您产生影响之外,不应该有任何伤害,如果其中一个指针是通过隐藏引用获得的,而另一个是通过公共引用获得的(公共引用可能已经通过预加载或在查找范围中位于当前DSO之前的DSO进行了插入,而隐藏引用(导致直接跳转)有效地防止任何形式的插入)。
更合适的方法是为test1()定义两个名称 - 一个公共名称和一个私有/隐藏名称。
在gcc和clang中,可以使用一些别名技巧来完成这项工作,这只能在定义符号的翻译单元中完成。
宏可以使其更加美观:
#define PRIVATE __attribute__((__visibility__("hidden")))
#define PUBLIC __attribute__((__visibility__("default")))
#define PRIVATE_ALIAS(Alias,OfWhat) \
    extern __typeof(OfWhat) Alias __attribute((__alias__(#OfWhat), \
                                 __visibility__("hidden")))

#if HERE
PUBLIC void test1(void) { }
PRIVATE_ALIAS(test1__,test1);
#else
PUBLIC void test1(void);
PRIVATE void test1__(void);
#endif

void call_test1(void) { test1(); }
void call_test1__(void) { test1__(); }

void call_ext0(void) { void ext0(void); ext0(); }
void call_ext1(void) { PRIVATE void ext1(void); ext1(); }

上述代码编译为 (-O3, x86-64):

call_test1:
        jmp     test1@PLT
call_test1__:
        jmp     test1__
call_ext0:
        jmp     ext0@PLT
call_ext1:
        jmp     ext1

在-O3开启的情况下,将HERE=1定义为内联测试1调用,因为它很小且本地化。示例请参见https://godbolt.org/g/eZvmp7

“打破了C语言的保证” - 是哪个? - yugr
1
@yugr 感谢您的评论。我不知道为什么最后一段会有那样的内容。我刚试了一下,-fno-semantic-interposition 似乎不能消除实际函数调用中的 PLT 间接寻址。我现在已经将其删除。我相信其余部分应该是没问题的,特别是关于为同一对象同时设置隐藏和可见别名的部分。 - Petr Skocik

3
如果您无法更改源代码,您可以使用-Bsymbolic链接器标志:
创建共享库时,将对全局符号的绑定引用到共享库中的定义(如果有)。通常,程序链接到共享库时可以覆盖共享库中的定义。此选项仅在支持共享库的ELF平台上具有意义。
但请注意,如果库的某些部分依赖于符号插入,则会导致破坏。我建议隐藏不需要导出的函数(通过使用__attribute__((visibility("hidden")))进行注释)或通过hidden aliases调用它们(专门设计为以受控方式进行PLT-less intra-library calls)。

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