ELF符号:GLOBAL+HIDDEN

ELF符号:GLOBAL+HIDDEN

基于例如。这个 Oracle 描述

STB_GLOBAL 全局符号。这些符号对所有目标文件都可见被合并。一个文件对全局符号的定义将满足另一个文件对同一全局符号的未定义引用。 ...

STV_HIDDEN 当前组件中定义的符号如果其名称被隐藏对其他组件不可见。这样的符号必然受到保护。该属性用于控制组件的外部接口。如果将其地址传递到外部,则由此类符号命名的对象仍可以从另一个组件引用。
可重定位对象中包含的隐藏符号被删除或转换为 STB_LOCAL 绑定当可重定位目标文件包含在可执行文件或共享目标文件中时,由链接编辑器执行。

然而,查看readelf -s一些简单的测试程序(在x86-64 Linux上用GCC编译),有一些全局隐藏符号:

FUNC    GLOBAL HIDDEN    16 _fini
OBJECT  GLOBAL HIDDEN    25 __dso_handle
OBJECT  GLOBAL HIDDEN    25 __TMC_END__

根据上面的描述,这应该是荒谬的,也是根本不允许的。

该组合有哪些特性(可见性、插入能力……)?

答案1

我现在有点明白了。

GLOBAL+HIDDEN 通常发生在.o隐藏函数的未链接文件(包括静态库)中(而单元局部静态函数已经是 LOCAL+DEFAULT)。然后在链接期间,GLOBAL+HIDDEN 转换为 LOCAL+DEFAULT,始终用于正常功能。

创建共享库时,问题中的 GCC/lib 内部也会发生这种情况。但是,对于非库可执行文件,这些内部未完全转换,出于可能纯粹是方便的原因而保持全局+隐藏。由于它不是链接到任何地方的库,因此不会造成任何损害。

或许可以概括一下普通用户定义函数对其他人也有帮助...这是关于 Linux 行为的;其他系统可能有点不同。

需要考虑什么(正常功能)

让我们构建一个二进制文件mybin。它有三个文件fila.cfilb.c并且filc.c.在其中一个代码文件中有一个函数func1

mybin可能成为静态库(仅编译.o和存档)、可运行程序(也链接)或共享库(也链接)。

func1代码中可以是

  • 一个普通的函数
  • 虚弱的 (__attribute__((weak))
  • 隐藏,意味着在mybin使用共享库的程序中可用,但在使用共享库的程序中不可用(如果是一个)( __attribute__ ((__visibility__ ("hidden"))))
  • 静态,仅.c在定义它的文件中可用(static在 C 中)
  • (以及受保护的和内部的,但这些并不是很重要)

这没有考虑优化(编译时和/或 LTO(链接时优化)),这可能会完全删除符号。

编译步骤(至.o

fila.c包含 的文件中func1,该函数显然对所有类型(正常/弱/隐藏/静态)都是可见且可用的。

  • 一个普通的函数:
    • filb.c对其他单元也可见filc.c(也对静态库,如果有的话)
    • 所有单元中只能有一个具有该名称的普通/强函数,否则链接器稍后会抱怨(这一个可执行文件/sharedlib/staticlib)(除了像链接器选项这样奇怪的东西muldefs)。
    • 在生成的.o文件中,函数是GLOBAL+DEFAULT
  • 功能较弱
    • 其他单位也可见
    • 除了一个正常/强函数之外,周围还可以存在多个弱函数。在这种情况下,稍后在连接时,强的用于所有单位,弱的消失。
    • 如果只有弱函数而没有具有该名称的强函数,则第一个弱函数获胜(即编译器命令行中具有此类弱函数的第一个文件)
    • 在生成的.o文件中,函数为WEAK+DEFAULT
  • 隐藏功能
    • 变成 GLOBAL+HIDDEN (仅在文件中.o
    • 否则表现得像普通函数
  • 弱+隐藏函数
    • 变得弱+隐藏
    • 否则表现得像弱函数
  • 静态函数
    • 变成本地+默认
    • 对其他单位不可见;使用它们会导致链接器稍后抱怨,因为跨单元使用时所有 LOCAL 都会被忽略
    • 几个单元可能有自己的具有该名称的静态函数,并且它们都可以不同
  • .o文件中不存在 LOCAL+HIDDEN

编译时链接

如果创建静态库,则不会发生链接。

对于共享库和可执行文件:

  • 任何本地的(即静态函数)都被完全忽略,对其他单元不可见,并且不会导出为可用的共享库函数。
  • 如上所述,GLOBAL/WEAK+DEFAULT 可用,对于共享库,它们被输入到可用函数列表中(动力)具有相同的类型。
  • GLOBAL/WEAK+HIDDEN 可用于如上所述的相同二进制文件的单元,但在链接期间它们的类型更改为 LOCAL+DEFAULT(如链接之前的静态函数)。对于共享库及其可用函数列表(动力),这意味着要么根本不输入它们,要么在运行时它们被忽略。

被调用的函数名称通常必须存在,要么在刚刚创建的二进制文件中,要么在链接器命令行中指定的任何共享库中(并且作为 GLOBAL/WEAK+DEFAULT)。
如前所述,任何完全链接的二进制文件(共享库或可执行文件)不会有多个同名的 GLOBAL/WEAK+DEFAULT 函数,最多只有 1 个。每个二进制文件一个。但是,不同的二进制文件仍然可能存在重叠,即两个共享库(或可执行文件与共享库)可能都具有func1.这不会导致错误;查找如下面的插入部分所述。

如果在链接期间,所使用的函数名称未在任何地方实现(既不是可执行文件也没有任何使用共享库),这通常会导致错误。但是,如果定义(在.h文件等中,没有代码)被标记为 WEAK,则它的链接不会出现错误,并且在运行时如果找不到该定义,则计算结果为空指针(可以使用 检查if)。在运行时,可能会发现它是因为共享库同时发生了更改,或者LD_PRELOAD添加了更多库。

运行时干预和优化

函数调用可能会生成二进制指令

  • 不可插入:这些调用跳转到存在于同一二进制文件(同一共享库或同一可执行文件,可能在不同.o单元中,甚至在同一单元中)中存在的函数的某个预先计算的(绝对/相对)地址。这也使得诸如函数内联之类的优化成为可能。
  • interposable:在运行时,运行时链接器ld.so会被询问具有该名称的函数在哪里,并且在不同的程序运行中可能会产生不同的结果。这也意味着没有函数内联,并且性能可能会稍差一些。然而,它使得重写函数成为可能;见下文。

二进制文件之间的调用(可执行文件→共享库,或共享库1→共享库2)始终是可插入的。
在同一个二进制文件中,两个“自己的”函数之间的调用:

  • 本地函数(静态、隐藏)永远不可插入。
  • 对于 GLOBAL/WEAK 函数,它取决于创建此二进制文件时的编译器/链接器选项(例如,fPIC fPIE fno-semantic-interposition -Bsymbolic -rdynamic 等)

如果正在运行的程序中的调用要求ld.so搜索func1

  • 始终首先检查程序的可执行二进制文件。
  • 如果在主 exe 中找不到,则LD_PRELOAD考虑共享库(如果有),
  • 然后,按照链接期间指定的顺序(命令行中的顺序)检查程序使用的所有共享库。
    仅考虑 GLOBAL/WEAK+DEFAULT 函数。这个阶段,GLOBAL和WEAK已经没有区别了;较早检查位置中的 WEAK 胜过较晚库中的非弱函数。 (很久以前就存在历史差异,但在 Linux 上它们早已不复存在。)

这意味着,例如:

  • 二进制文件之间的函数调用始终可以被LD_PRELOAD库覆盖。如果主可执行文件想要func1从某个共享库调用,并且预加载的库具有不同的func1,则预加载的库获胜。共享库之间的调用类似;总是首先搜索预加载的。
  • 主可执行文件中存在的函数(不在共享库中)永远不能被覆盖,甚至不能被LD_PRELOAD.因此,在编译程序期间,是否编译对程序内函数的调用进行询问实际上并不重要ld.so,因为无论如何程序本身总是获胜。 (编译模式对于共享库确实很重要。)
  • sharedlib2→sharedlib3之间的调用可以被早期的sharedlib1甚至主可执行文件覆盖,这样sharedlib2就会调用它们而不是sharedlib3。
  • 根据编译方式,对sharedlib2 中的全局函数的调用可以由主可执行文件和早期库覆盖,也可以进行更优化但不会注意到覆盖。
  • 为了防止在没有编译器标志的情况下覆盖库中的特定函数,从而实现更好的优化,甚至更快的链接,可以在代码中将其声明为隐藏(甚至静态)。然而,这也意味着不能从主可执行文件中使用该函数(特别是如果不被覆盖)。一种中间方法,在没有特定函数的编译器选项的情况下也可以工作,是将函数声明为隐藏,但添加全局别名。其内部的库调用优化的非重写隐藏函数,外部的所有内容都使用别名(可能被其他库等重写)

关于可执行文件的注释动力

所有应覆盖其他二进制文件中可覆盖调用的函数都需要在动力他们自己的二进制文件的一部分。对于共享库,所有 GLOBAL/WEAK 函数都会自动存在,但对于主可执行文件来说不一定如此。

readelf -s可以显示列表的内容。在我对 GCC 的测试中,如果链接器在编译时发现某些共享库也有一个具有该名称的函数(与代码内容无关),则似乎会添加函数,但如果该函数看起来是唯一的,则不会添加函数(因为那样它就不会覆盖任何东西)。

在某些情况下这可能是一个问题。例如,如果在编译过程中只有主可执行文件有一个func1,但后来使用的共享库之一被更改为func1也有一个(无需重新编译主程序),则主程序func1无法覆盖该库之一。

为了避免这种情况,请始终列出所有函数,请使用-rdynamic(仅在主可执行文件上;它在库上无用)。

最后,刚才提到的PROTECTED模式有什么问题呢?

原则上,库函数上的 GLOBAL+PROTECTED 应该像上面提到的别名隐藏函数一样工作:该库有一个隐藏(LOCAL+DEFAULT)函数,该函数在库本身内使用并进行优化,但不可能覆盖它,加上可以从外部使用(和覆盖)的公共别名。

使用 PROTECTED,无需设置 HIDDEN 和创建别名。

然而,存在两个问题:

  • 由于细节ld.so和 C ABI 要求(相同函数的相同地址),PROTECTED 比 HIDDEN+alias 慢。
  • 存在一些错误,例如 GCC 19520

相关内容