【问题标题】:What is "namespace cleanliness", and how does glibc achieve it?什么是“命名空间清洁度”,glibc 是如何实现的?
【发布时间】:2019-08-30 20:58:12
【问题描述】:

我最近从this answer by @zwol看到了这段话:

read 上的 __libc_ 前缀是因为在 C 库中 read 实际上有三个不同的名称:read__read__libc_read。这是实现“命名空间清洁”的一种技巧,如果您打算实现一个成熟且完全符合标准的 C 库,您只需要担心这一点。简短的版本是C库中有很多函数需要调用read,但是其中一些不能使用nameread来调用它,因为C程序在技术上是允许的自己定义一个名为 read 的函数。

你们中的一些人可能知道,我是setting out to implement my own full-fledged and fully standards-compliant C library,所以我想了解更多详情。

什么是“命名空间清洁度”,glibc 是如何实现的?

【问题讨论】:

  • 我想为这个问题写一个答案,但是现在是星期五下午 5 点,我不知道我是否有时间给予它应有的关注,嗯,几个星期。现在,我将向您指出section 7.1.3 of C2011,并建议您特别考虑“没有保留其他标识符(用于实现)”的含义。

标签: c posix language-lawyer libc


【解决方案1】:

首先,注意标识符read 根本没有被ISO C 保留。一个严格符合 ISO C 程序可以有一个名为read 的外部变量或函数。然而,POSIX 有一个名为read 的函数。那么我们如何才能拥有一个带有read 的POSIX 平台,同时允许C 程序呢?毕竟freadfgets 可能使用read;他们不会坏吗?

一种方法是将所有 POSIX 内容拆分到单独的库中:用户必须链接 -lio 或其他任何东西才能获得 readwrite 以及其他功能(然后拥有 freadgetc使用一些替代读取功能,因此即使没有-lio,它们也可以工作。

glibc 中的方法不是使用像 read 这样的符号,而是在保留的命名空间中使用像 __libc_read 这样的替代名称来避免碍事。 read 对 POSIX 程序的可用性是通过将 read 设为 __libc_read弱别名 来实现的。对read 进行外部引用但未定义它的程序将到达与__libc_read 别名的弱符号read。定义了read 的程序将覆盖弱符号,并且它们对read 的引用都将转到该覆盖。

重要的是这对__libc_read 没有影响。此外,需要使用read函数的库本身调用其内部的__libc_read名称,不受程序影响。

所以所有这些加起来就是一种清洁。在具有许多组件的情况下,这不是一种可行的命名空间清洁的一般形式,但它适用于两方情况,我们唯一的要求是分离“系统库”和“用户应用程序”。

【讨论】:

  • @JL2210 这可能是目前的情况,但我似乎记得在 glibc 的历史中并不总是如此;无论如何,这只是一个细节。弱别名read 和像__read__libc_read 这样的单个内部函数足以带来命名空间分离。任何进一步拆分为__read__libc_read 都必须满足一些额外的要求。 (比如允许程序在需要时实际上覆盖库函数。)
  • __libc_read 不仅仅是一个存根,它与__readread 做同样的事情。 (JL2210 可能会感到困惑,因为在链接的问题中,有人引用了 glibc 的 所有三个 的存根实现中的代码。)我将写一个补充答案来解释为什么 glibc 需要 __libc_read__read.
【解决方案2】:

好的,首先是关于标准规定的 C 语言的一些基础知识。为了您可以编写 C 应用程序而不必担心您使用的某些标识符可能与标准库实现中使用的外部标识符或标准头文件内部使用的宏、声明等冲突,语言标准拆分了可能的标识符到为实现保留的名称空间和为应用程序保留的名称空间中。相关文字为:

7.1.3 保留标识符

每个标头声明或定义其相关子条款中列出的所有标识符,并可选地声明或定义其相关未来库方向子条款中列出的标识符和始终保留用于任何用途或用作文件范围标识符的标识符。

  • 以下划线和大写字母或另一个下划线开头的所有标识符始终保留用于任何用途。
  • 以下划线开头的所有标识符始终保留用作普通和标记名称空间中具有文件范围的标识符。
  • 以下任何子条款(包括未来的库方向)中的每个宏名称都保留用于指定使用,如果包含任何相关的头文件;除非另有明确说明(参见 7.1.4)。
  • 以下任何子条款(包括未来的库方向)和 errno 中的所有具有外部链接的标识符始终保留用作具有外部链接的标识符。184)
  • 以下任何子条款(包括未来的库方向)中列出的每个具有文件范围的标识符都保留用作宏名称和在同一名称空间中作为具有文件范围的标识符,如果包括其任何关联的头文件.

没有保留其他标识符。如果程序在保留标识符的上下文中声明或定义标识符(7.1.4 允许的除外),或将保留标识符定义为宏名称,则行为未定义。

这里的重点是我的。例如,标识符 read 为所有上下文中的应用程序保留(“没有其他...”),但标识符 __read 为所有上下文中的实现保留(要点 1)。

现在,POSIX 定义了许多不属于标准 C 语言的接口,而 libc 实现可能还有很多未被任何标准涵盖的接口。到目前为止没关系,假设工具(链接器)正确处理它。如果应用程序不包含<unistd.h>(超出语言标准的范围),它可以安全地将标识符read 用于它想要的任何目的,即使libc 包含一个名为read 的标识符,也不会出现任何问题。

问题在于,用于类 unix 系统的 libc 还需要使用函数 read 来实现基本 C 语言标准库的部分内容,例如 fgetc (以及基于它构建的所有其他 stdio 函数)。这是一个问题,因为现在您可以拥有一个严格符合的 C 程序,例如:

#include <stdio.h>
#include <stdlib.h>
void read()
{
    abort();
}
int main()
{
    getchar();
    return 0;
}

并且,如果 libc 的 stdio 实现调用 read 作为其后端,它将最终调用应用程序的函数(更不用说,使用错误的签名,这可能会因其他原因而中断/崩溃),从而产生错误的行为一个简单的、严格遵守的程序。

这里的解决方案是让 libc 有一个名为 __read(或您喜欢的保留命名空间中的任何其他名称)的内部函数,可以调用它来实现 stdio,并让公共 read 函数调用它(或,为它做一个弱别名,这是一种更高效、更灵活的机制,可以用传统的unix链接器语义来实现同样的事情;注意有一些命名空间问题比read更复杂,没有弱别名就无法解决)。

【讨论】:

  • 啊,糟了。我希望避免弱别名。这有我的赞成票,但我要等“几个星期”看看@zwol 是否能回答。
  • @JL2210:你可以通过完全独立的标准库版本来避免它们,你可以为“除了普通 C”等链接,但这是一个非常极端的措施。拒绝使用它们(或其他可以代替它们起作用的扩展,例如在链接时设置函数指针的全局 ctor)也排除了任何有效的静态链接。
  • 好吧,我想那时需要它们。我想我会开始努力的。
【解决方案3】:

Kaz 和 R.. 解释了为什么 C 库通常需要为诸如 read 之类的函数提供 两个 名称,这些函数由应用程序和其他函数调用C 库。其中一个名称将是正式的记录名称(例如read),其中一个名称将带有前缀,使其成为为实现保留的名称(例如__read)。

GNU C 库的某些函数有三个名称:正式名称 (read) 加上两个不同的保留名称(例如 __read__libc_read)。这不是因为 C 标准的任何要求;从一些频繁使用的内部代码路径中挤出一点额外性能是一种技巧。

GNU libc 的编译代码,在磁盘上,被分成几个共享对象libc.so.6ld.so.1libpthread.so.0libm.so.6libdl.so.2 等(确切的名称可能因底层 CPU 和操作系统而异)。每个共享对象中的函数往往需要调用同一个共享对象中定义的其他函数;不太常见的是,它们需要调用在不同共享对象中定义的函数。

如果被调用者的名称​​隐藏,则单个共享对象中的函数调用会更有效——只有同一共享对象中的调用者才能使用。这是因为globally visible names can be interposed。假设主可执行文件和共享对象都定义了名称__read。将使用哪一个? ELF 规范说主可执行文件中的定义获胜,并且 allanywhere 对该名称的调用必须解析为该定义。 (ELF 规范与语言无关,没有使用 C 标准对保留和非保留标识符的区别。)

插入是通过procedure linkage table 发送对全局可见符号的所有调用来实现的,这涉及额外的间接层和运行时变量的最终目的地。另一方面,可以直接调用隐藏符号。

readlibc.so.6 中定义。被libc.so.6内的其他函数调用;它也被其他共享对象中的函数调用,这些共享对象也是 GNU libc 的一部分;最后由应用程序调用。因此,它被赋予了三个名称:

  • __libc_readlibc.so.6 内的呼叫者使用的隐藏名称。 (nm --dynamic /lib/libc.so.6 | grep read 不会显示此名称。)
  • __read,一个可见的保留名称,供来自libpthread.so.0 和 glibc 其他组件的调用者使用。
  • read,一个可见的普通名称,由应用程序的调用者使用。

有时隐藏名称有一个__libc 前缀,而可见的实现名称只有两个下划线;有时情况正好相反。这并不意味着什么。这是因为 GNU libc 自 1990 年代以来一直在不断发展,其开发人员已经多次改变了对内部约定的看法,但并不总是费心修复所有旧式代码以匹配新约定(有时兼容性要求意味着我们无法修复旧代码,甚至)。

【讨论】:

  • 这有点疯狂。既然只有一个内部函数,为什么还要麻烦拥有两个内部函数?
  • 这个答案的全部意义在于解释为什么 glibc 有两个内部名称(它们不是单独的函数)。你能更详细地解释一下为什么你还不清楚吗?
  • 我对“如果被调用者的名字是hidden,单个共享对象中的[f]函数调用效率更高”有点困惑。这是为什么呢?
  • @JL2210 我现在已经添加了一些关于此的文本。
猜你喜欢
  • 2010-10-10
  • 2011-03-23
  • 2011-04-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多