【问题标题】:Why can't C functions be name-mangled?为什么不能对 C 函数进行名称修改?
【发布时间】:2016-08-05 22:23:37
【问题描述】:

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

我知道 C++ 编译器在编译函数时,会给函数一个特殊的名称,主要是因为我们可以在 C++ 中重载同名的函数,这些函数必须在编译时解析。在 C 中,函数的名称将保持不变,或者可能在其前面加上一个 _。

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

【问题讨论】:

  • C 不需要 修改名称,因为它没有函数重载。
  • 如果 C++ 编译器破坏了函数名,如何将 C 库与 C++ 代码链接?
  • " 我回答说是在 C++ 代码中使用 C 函数,因为 C 不使用名称修饰。" - 我认为情况正好相反。 Extern "C" 使 C++ 函数在 C 编译器中可用。 source
  • @Engineer999:如果你用 C++ 编译器编译 C 的子集,它也是 C++,函数名确实会被破坏。但是,如果您希望能够链接使用不同编译器创建的二进制文件,则不需要名称修改。
  • C 确实会混淆名称。通常,重整的名称是函数的名称,前面有一个下划线。有时它是函数名后跟一个下划线。 extern "C" 说要以与“C”编译器相同的方式修改名称。

标签: c++ c name-mangling extern-c


【解决方案1】:

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

首先,C 是第一位的。因此,C 所做的就是“默认”。它不会破坏名称,因为它不会。函数名是函数名。全局就是全局,依此类推。

然后 C++ 出现了。 C++ 希望能够使用与 C 相同的链接器,并能够与用 C 编写的代码进行链接。但是 C++ 不能让 C 的“修改”(或缺少)保持原样。查看以下示例:

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

在 C++ 中,这些是不同的函数,具有不同的主体。如果它们都没有被破坏,则两者都将被称为“函数”(或“_function”),并且链接器将抱怨符号的重新定义。 C++ 解决方案是将参数类型转换为函数名称。因此,一种称为_function_int,另一种称为_function_void(不是实际的重整方案),避免了冲突。

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

为避免该错误,在函数的声明期间,我们告诉编译器它是一个旨在与 C 编译器链接或由 C 编译器编译的函数:

extern "C" int function(int a);

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

【讨论】:

  • @ShacharShamesh:我在别处问过这个问题,但是,在 C++ 编译库中进行链接呢?当编译器单步执行并编译调用 C++ 编译库中的函数之一的代码时,它如何知道仅在看到函数的声明或函数调用时要修改或赋予函数的名称?如何知道它是在哪里定义的,它被命名为其他东西?那么C++中一定有标准的名字修饰方法吗?
  • 每个编译器都有自己的特殊方式。如果您使用相同的编译器编译所有内容,那没关系。但是,如果您尝试使用使用 Borland 编译器编译的库,以及使用 Microsoft 编译器构建的程序,那么……祝你好运;你会需要它:)
  • @Engineer999 有没有想过为什么没有可移植 C++ 库之类的东西,但它们要么准确指定您必须使用的编译器(和标准库)的版本(和标志),要么只导出一个C API?你去吧。 C++ 几乎是有史以来最不可移植的语言,而 C 恰恰相反。在这方面有一些努力,但现在如果你想要真正便携的东西,你会坚持使用 C。
  • @Voo 好吧,理论上你应该能够通过遵守标准来编写可移植代码,例如-std=c++11,并避免使用超出标准的任何内容。这与声明 Java 版本相同(尽管较新的 Java 版本是向后兼容的)。人们使用编译器特定的扩展和平台相关的代码并不是标准错误。另一方面,你不能责怪他们,因为标准中缺少很多东西(尤其是 IO,比如套接字)。该委员会似乎正在慢慢赶上这一点。如果我遗漏了什么,请纠正我。
  • @mucaho:您说的是源代码可移植性/兼容性。即API。 Voo 正在讨论 binary 兼容性,无需重新编译。这需要ABI 兼容性。 C++ 编译器会定期在版本之间更改其 ABI。 (例如 g++ 甚至没有尝试拥有一个稳定的 ABI。我认为他们不会仅仅为了好玩而破坏 ABI,但他们不会避免在需要获得一些东西并且没有其他好方法时需要更改 ABI 的更改去做。)。
【解决方案2】:

一般来说,并不是他们“不能”,他们不是

如果您想调用 C 库中名为 foo(int x, const char *y) 的函数,那么让您的 C++ 编译器将其修改为 foo_I_cCP()(或其他任何东西,只是在这里当场编造一个修改方案)是不好的,因为它可以。

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

请记住,上述 C 函数可能位于您没有源代码的库中,您所拥有的只是预编译的二进制文件和标头。所以你的 C++ 编译器不能做“它自己的事情”,毕竟它不能改变库中的内容。

【讨论】:

  • 这是我缺少的部分。为什么 C++ 编译器在看到它的声明或看到它被调用时会破坏函数名。当它看到它们的实现时,它不只是破坏函数名称吗?这对我来说更有意义
  • @Engineer999:你怎么能有一个名字作为定义而另一个名字作为声明呢? “你可以调用一个名为 Brian 的函数。” “好的,我给布赖恩打电话。” “抱歉,没有名为 Brian 的函数。” 原来它叫 Graham。
  • C++ 编译库中的链接怎么样?当编译器单步执行并编译调用 C++ 编译库中的某个函数的代码时,它如何仅看到函数的声明或函数调用就知道要修改或赋予函数的名称?
  • @Engineer999 两者必须就相同的修改达成一致。所以他们看到了头文件(记住,原生 DLL 中的元数据非常少——头就是元数据),然后说“啊,对,Brian 应该真的是 Graham”。如果这不起作用(例如,使用两个不兼容的修饰方案),您将无法获得正确的链接并且您的应用程序将失败。 C++ 有很多这样的不兼容性。在实践中,您必须显式使用重整名称并禁用重整(例如,您告诉您的代码执行 Graham,而不是 Brian)。在实际实践中...extern "C" :)
  • @Engineer999 我可能是错的,但您是否有使用 Visual Basic、C# 或 Java(甚至在一定程度上是 Pascal/Delphi)等语言的经验?这些使互操作看起来非常简单。在 C 尤其是 C++ 中,它不是。您需要遵守大量调用约定,您需要知道谁负责哪些内存,并且您必须拥有告诉您函数声明的头文件,因为 DLL 本身不包含足够的信息 - 特别是在纯C。如果没有头文件,一般需要反编译DLL才能使用。
【解决方案3】:

允许 C++ 编译器也破坏 C 函数有什么问题?

它们不再是 C 函数了。

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

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

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

【讨论】:

  • +int(PI/3),但持保留态度:我会非常谨慎地谈论“C++ ABI”... AFAIK,在定义 C++ 时有尝试 ABI,但没有真正的 de facto / de jure 标准 - 正如isocpp.org/files/papers/n4028.pdf 所说(我完全同意),引用,具有讽刺意味的是,C++ 实际上一直支持一种使用稳定二进制 ABI 发布 API 的方式——通过外部“C”求助于 C++ 的 C 子集。C++ Itanium ABI 就是这样 - some C++ ABI for Itanium... 如 stackoverflow.com/questions/7492180/c-abi-issues-list 所述
  • @vaxquis:是的,不是“C++ 的 ABI”,而是“C++ ABI”,就像我有一个不能在每个房子上都使用的“房子钥匙”一样。猜猜它可能会更清楚,尽管我试图从“The C++ ABI in use by your system这个短语开始尽可能清楚地说明它。为了简洁起见,我在后面的话语中删除了澄清词,但我会接受一个减少混淆的编辑!
  • AIUI C abi 往往是平台的属性,而 C++ ABI 往往是单个编译器的属性,甚至通常是单个编译器版本的属性。因此,如果您想在使用不同供应商工具构建的模块之间进行链接,则必须使用 C abi 作为接口。
  • “名称重整的函数将不再是 C 函数”的说法被夸大了——如果已知重整的名称,则完全可以从普通的 C 中调用名称重整的函数。名称的更改并不会使其与 C ABI 的依从性降低,即不会使其不再是 C 函数。反过来说更有意义——C++ 代码不能在不声明“C”的情况下调用 C 函数,因为它会在尝试链接到被调用者时进行名称修改。
  • @PeterA.Schneider:是的,标题短语被夸大了。 答案的其余部分包含相关的事实细节。
【解决方案4】:

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

所以前提是有缺陷的。

【讨论】:

  • 这不是真正的名称修改。它只是一个供应商特定的命名(或名称修饰)约定,以防止可执行文件链接到使用具有不同调用约定的函数构建的 DLL 的问题。
  • 在前面加上 _ 怎么样?
  • @Peter:字面意思是一样的。
  • @Frankie_C:任何 C 标准都没有指定“调用者清理堆栈”:从语言的角度来看,这两种调用约定都不比另一种更标准。
  • 从 MSVC 的角度来看,“标准调用约定”正是您从 /Gd, /Gr, /Gv, /Gz 中选择的。 (也就是说,除非函数声明明确指定调用约定,否则使用标准调用约定。)。您正在考虑__cdecl,这是默认的标准调用约定。
【解决方案5】:

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

在大多数平台上,都有一个规范——通常称为 ABI [应用程序二进制接口],它描述了编译器必须做什么才能生成具有特定名称的函数,该函数接受某些特定类型的参数并返回某个值特定类型。在某些情况下,ABI 可能会定义多个“调用约定”;此类系统的编译器通常提供一种方法来指示应为特定函数使用哪种调用约定。例如,在 Macintosh 上,大多数 Toolbox 例程都使用 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 和 C++,那么很难理解为什么要这样做。要理解,我们必须将事物放在静态链接的旧方式的上下文中。静态链接对于 Windows 程序员来说似乎很原始,但这是 C 不能修改名称的主要原因。
  • @user34660: 不是qutie。这就是 C 不能强制要求存在其实现需要修改可导出名称或允许存在多个由次要特征区分的同名符号的特性的原因。
  • 我们是否知道有人试图“强制”这些东西,或者这些东西是 C++ 之前 C 可用的扩展?
  • @user34660:关于“静态链接对 Windows 程序员来说似乎很原始......”,但对于使用 Linux 的人来说,动态链接有时似乎是一个主要的 PITA,当安装程序 X(可能用 C++ 编写)时意味着必须跟踪并安装您系统上已有不同版本的库的特定版本。
  • @jamesqf,是的,Unix 在 Windows 之前没有动态链接。我对 Unix/Linux 中的动态链接知之甚少,但听起来它不像一般操作系统中那样无缝。
【解决方案6】:

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

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

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

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

最初的 Windows 3.1 ABI 基于 Pascal。因此,它使用了 Pascal ABI(从左到右顺序的参数,被调用者弹出)。由于参数编号的任何不匹配都可能导致堆栈损坏,因此形成了一种修改方案。每个函数名称都带有一个数字,表示其参数的大小(以字节为单位)。因此,在 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。

【讨论】:

    【解决方案7】:

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

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

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

    【讨论】:

      【解决方案8】:

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

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

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

      【讨论】:

        【解决方案9】:

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

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

        【讨论】:

        • 您的意思是“允许在 link 时间检查它们的类型”。类型在编译时被检查,但与未修改的名称链接无法检查不同编译单元中使用的声明是否一致。如果他们不同意,那是你的构建系统从根本上被破坏了,需要修复。
        猜你喜欢
        • 1970-01-01
        • 2010-11-30
        • 1970-01-01
        • 2018-02-04
        • 1970-01-01
        • 2017-07-19
        • 1970-01-01
        • 2013-09-25
        • 1970-01-01
        相关资源
        最近更新 更多