为什么不能对 C 函数进行名称修改?

Why can't C functions be name-mangled?

最近面试了一个问题,问的是extern "C"在C++代码中有什么用。我回答说是在 C++ 代码中使用 C 函数,因为 C 不使用名称修饰。有人问我为什么 C 不使用名称修改,老实说我无法回答。

我了解到,当C++编译器编译函数时,它给函数起了一个特殊的名字,主要是因为我们在C++中可以有同名的重载函数,必须在编译时解析。在 C 中,函数的名称将保持不变,或者可能在它之前加上一个 _。

我的问题是:允许 C++ 编译器也破坏 C 函数有什么问题?我会假设编译器给它们起什么名字并不重要。我们在 C 和 C++ 中以相同的方式调用函数。

MSVC 事实上 确实 破坏了 C 名称,尽管是以一种简单的方式。它有时会附加 @4 或另一个小数字。这与调用约定和堆栈清理的需要有关。

所以前提是有缺陷的。

不是他们"can't",他们不是,总的来说。

如果你想调用一个名为 foo(int x, const char *y) 的 C 库中的函数,让你的 C++ 编译器将其分解为 foo_I_cCP() 是没有好处的(或者其他什么,只是在点这里)只是因为它可以。

该名称无法解析,该函数在 C 中且其名称不依赖于其参数类型列表。所以 C++ 编译器必须知道这一点,并将该函数标记为 C 以避免进行处理。

请记住,上述 C 函数可能位于您没有其源代码的库中,您所拥有的只是 pre-compiled 二进制文件和 header。所以你的 C++ 编译器不能做 "it's own thing",毕竟它不能改变库中的内容。

C++ 希望能够与链接到它或它链接到的 C 代码互操作。

C 需要非名称损坏的函数名称。

如果C++对其进行了破坏,它就找不到从C导出的非破坏函数,或者C也找不到C++导出的函数。 C 链接器必须获得它自己期望的名称,因为它不知道它来自或去往 C++。

C++ 编译器使用名称修饰,以便为重载函数提供唯一的符号名称,否则其签名将相同。它基本上也对参数类型进行编码,这允许在基于函数的级别上进行多态性。

C 不需要这个,因为它不允许函数重载。

请注意,名称修改是一个(但肯定不是唯一!)不能依赖 'C++ ABI' 的原因之一。

what's wrong with allowing the C++ compiler to mangle C functions also?

它们将不再是 C 函数。

一个函数不仅仅是一个签名和一个定义;函数的工作方式在很大程度上取决于调用约定等因素。指定在您的平台上使用的 "Application Binary Interface" 描述了系统如何相互通信。您的系统使用的 C++ ABI 指定了一个名称修饰方案,以便该系统上的程序知道如何调用库中的函数等。 (阅读 C++ Itanium ABI 以获得一个很好的例子。您会很快明白为什么它是必要的。)

这同样适用于您系统上的 C ABI。一些 C ABI 实际上有一个名称修改方案(例如 Visual Studio),所以这与 "turning off name mangling" 无关,更多的是关于从 C++ ABI 切换到 C ABI,用于某些功能。我们将 C 函数标记为 C 函数,C ABI(而不是 C++ ABI)与其相关。声明必须与定义相匹配(无论是在同一个项目中还是在某些第三方库中),否则声明毫无意义。 否则,您的系统根本不知道如何locate/invoke这些功能。

至于为什么平台不将 C 和 C++ ABI 定义为相同并删除这个 "problem",这部分是历史原因 — 原始的 C ABI 不足以用于具有名称空间的 C++ , 类 和运算符重载,所有这些都需要以某种方式以计算机友好的方式在符号名称中表示——但也有人可能会争辩说,让 C 程序现在遵守 C++ 对 C 社区来说是不公平的,为了其他一些想要互操作性的人,它不得不忍受一个复杂得多的 ABI。

上面已经回答了,但我会尝试把事情放在上下文中。

首先,C在前。因此,C 所做的是某种程度上的 "default"。它不会破坏名称,因为它不会。函数名是函数名。全局就是全局,以此类推。

然后 C++ 出现了。 C++ 希望能够使用与 C 相同的 linker,并且能够 link 使用用 C 编写的代码。但是 C++ 不能离开 C "mangling"(或者,缺少那里的)原样。查看以下示例:

int function(int a);
int function();

在 C++ 中,这些是不同的函数,具有不同的主体。如果它们中的 none 被破坏,两者都将被调用 "function"(或“_function”),并且 linker 会抱怨符号的重新定义。 C++ 解决方案是将参数类型打乱为函数名称。因此,一个称为 _function_int,另一个称为 _function_void(不是实际的 mangling 方案),避免了冲突。

现在我们遇到了一个问题。如果 int function(int a) 是在 C 模块中定义的,而我们只是在 C++ 代码中获取它的 header(即声明)并使用它,编译器将生成一条指令给 linker导入 _function_int。当函数被定义时,在 C 模块中,它并没有被调用。它被称为 _function。这将导致 linker 错误。

为避免该错误,在函数的 声明 期间,我们告诉编译器它是一个设计用于 link 编辑或编译的函数, C 编译器:

extern "C" int function(int a);

C++ 编译器现在知道导入 _function 而不是 _function_int,一切正常。

部分用 C 语言编写,部分用其他语言(通常是汇编语言,但有时是 Pascal、FORTRAN 或其他语言)编写的程序是很常见的。程序包含由不同的人编写的不同组件也很常见,他们可能没有所有的源代码。

在大多数平台上,都有一个规范——通常称为 ABI [应用程序二进制接口],它描述了编译器必须做什么才能生成具有特定名称的函数,该函数接受某些特定类型的参数和 returns 某种特定类型的值。在某些情况下,一个 ABI 可能会定义多个 "calling convention";此类系统的编译器通常提供一种方法来指示应为特定函数使用哪种调用约定。例如,在 Macintosh 上,大多数工具箱例程使用 Pascal 调用约定,因此 "LineTo" 之类的原型将类似于:

/* Note that there are no underscores before the "pascal" keyword because
   the Toolbox was written in the early 1980s, before the Standard and its
   underscore convention were published */
pascal void LineTo(short x, short y);

如果一个项目中的所有代码都是使用同一个编译器编译的,它 编译器为每个函数导出的名称无关紧要,但在 在许多情况下,C 代码有必要调用已编写的函数 使用其他工具编译,不能用当前编译器重新编译 [而且很可能甚至不在 C 中]。能够定义链接器名称 因此对于此类功能的使用至关重要。

我将添加另一个答案,以解决发生的一些切题讨论。

C ABI(应用程序二进制接口)最初调用以相反的顺序在堆栈上传递参数(即 - 从右向左推送),调用者还释放堆栈存储。现代 ABI 实际上使用寄存器来传递参数,但是许多处理考虑都可以追溯到原始的堆栈参数传递。

相比之下,原始的 Pascal ABI 将参数从左推到右,而被调用者必须弹出参数。原始的 C ABI 在两个要点上优于原始的 Pascal ABI。参数推送顺序意味着第一个参数的堆栈偏移量始终是已知的,允许具有未知数量参数的函数,其中早期参数控制有多少其他参数(ala printf)。

C ABI 优越的第二种方式是在调用者和被调用者不同意有多少参数的情况下的行为。在 C 的情况下,只要您实际上不访问最后一个参数之后的参数,就不会发生任何不良情况。在 Pascal 中,错误数量的参数从堆栈中弹出,整个堆栈已损坏。

最初的 Windows 3.1 ABI 是基于 Pascal 的。因此,它使用了 Pascal ABI(从左到右的参数,被调用者弹出)。由于参数数量的任何不匹配都可能导致堆栈损坏,因此形成了一个 mangling 方案。每个函数名称都用一个数字表示其参数的大小(以字节为单位)。因此,在 16 位机器上,以下函数(C 语法):

int function(int a)

被损坏为 function@2,因为 int 是两个字节宽。这样做是为了如果声明和定义不匹配,链接器将无法找到该函数,而不是在 运行 时破坏堆栈。相反,如果程序链接,那么您可以确定在调用结束时从堆栈中弹出正确数量的字节。

32 位 Windows 及以后使用 stdcall ABI。它类似于 Pascal ABI,除了推送顺序与 C 中一样,从右到左。与 Pascal ABI 一样,名称修改将参数字节大小修改为函数名称以避免堆栈损坏。

与此处其他地方的声明不同,C ABI 不会破坏函数名称,即使在 Visual Studio 上也是如此。相反,用 stdcall ABI 规范修饰的修饰函数并不是 VS 独有的。 GCC 也支持此 ABI,即使是为 Linux 编译时也是如此。 Wine 广泛使用它,它使用自己的加载器允许 运行 将 Linux 编译的二进制文件链接到 Windows 编译的 DLL。

修改 C 函数和变量的名称将允许在 link 时检查它们的类型。目前,所有(?)C 实现都允许您在一个文件中定义一个变量,并在另一个文件中将其作为函数调用。或者您可以声明一个带有错误签名的函数(例如 void fopen(double) 然后调用它。

我在 1991 年通过使用 mangling 提出 a scheme for the type-safe linkage of C variables and functions。该方案从未被采纳,因为正如其他人在这里指出的那样,这会破坏向后兼容性。