"namespace cleanliness"是什么,glibc是怎么实现的?

What is "namespace cleanliness", and how does glibc achieve it?

我最近从 看到这段话:

The __libc_ prefix on read is because there are actually three different names for read in the C library: read, __read, and __libc_read. This is a hack to achieve "namespace cleanliness", which you only need to worry about if you ever set out to implement a full-fledged and fully standards compliant C library. The short version is that there are many functions in the C library that need to call read, but some of them cannot use the name read to call it, because a C program is technically allowed to define a function named read itself.

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

什么是"namespace cleanliness",glibc是如何实现的?

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

7.1.3 Reserved identifiers

Each header declares or defines all identifiers listed in its associated subclause, and optionally declares or defines identifiers listed in its associated future library directions subclause and identifiers which are always reserved either for any use or for use as file scope identifiers.

  • All identifiers that begin with an underscore and either an uppercase letter or another underscore are always reserved for any use.
  • All identifiers that begin with an underscore are always reserved for use as identifiers with file scope in both the ordinary and tag name spaces.
  • Each macro name in any of the following subclauses (including the future library directions) is reserved for use as specified if any of its associated headers is included; unless explicitly stated otherwise (see 7.1.4).
  • All identifiers with external linkage in any of the following subclauses (including the future library directions) and errno are always reserved for use as identifiers with external linkage.184)
  • Each identifier with file scope listed in any of the following subclauses (including the future library directions) is reserved for use as a macro name and as an identifier with file scope in the same name space if any of its associated headers is included.

No other identifiers are reserved. If the program declares or defines an identifier in a context in which it is reserved (other than as allowed by 7.1.4), or defines a reserved identifier as a macro name, the behavior is undefined.

这里的重点是我的。例如,标识符 read 为所有上下文中的应用程序保留 ("no other..."),但标识符 __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 作为其后端,它将最终调用应用程序的函数(更不用说,带有错误的签名,这可能 break/crash 由于其他原因), 为一个简单的、严格遵守的程序产生错误的行为。

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

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

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

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

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

所以所有这些加起来就是一种清洁。它不是在具有许多组件的情况下可行的命名空间清洁的一般形式,但它适用于两方情况,我们唯一的要求是将 "the system library" 和 "the user application".

分开

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 和 OS 而异)。每个共享对象中的函数通常需要调用同一共享对象中定义的其他函数;不太常见的是,他们需要调用在不同共享对象中定义的函数。

如果被调用者的名字是 hidden,则单个共享对象内的函数调用会更有效——仅供同一共享对象内的调用者使用。这是因为 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 年代以来一直在不断发展,其开发人员多次改变了对内部约定的看法,但并不总是费心修复所有旧式代码以匹配新约定(有时兼容性要求意味着我们不能修复旧代码,甚至)。