【问题标题】:What is the calling convention that clang uses?clang 使用的调用约定是什么?
【发布时间】:2019-06-22 20:01:55
【问题描述】:

clang 编译器使用的默认调用约定是什么?我注意到当我返回一个本地指针时,引用并没有丢失

#include <stdio.h>

char *retx(void) {
      char buf[4] = "buf";
      return buf;
}

int main(void) {
    char *p1 = retx();
    puts(p1);
    return 0;
}

【问题讨论】:

  • 未定义的行为有时会起作用。调用约定取决于目标。 (例如 x86-64 System V、Windows x64、i386 System V for 32-bit code on Linux、AArch64 的标准约定、PowerPC 的标准约定等)
  • 它也发生在其他编译器上。并不意味着你可以使用它。无论如何,clang 可能会警告你
  • 什么目标?什么编译器选项?
  • @YuriAlbuquerque:它是否“有效”的“决定”/“运气”是在编译时做出的,而不是在运行时。用同一个编译器多次编译/运行同一个源代码不会告诉你什么。
  • 为了测试它,在char *p1 = retx();puts(p1);之间再放一个子程序调用(带有一些局部变量),看看局部变量空间是否会被覆盖,因为局部变量被保存在堆栈上.轻松检查。

标签: c assembly undefined-behavior calling-convention


【解决方案1】:

这是未定义的行为。它可能会起作用,也可能不会,这取决于编译器在为某些特定目标编译时碰巧选择了什么。它实际上是un定义的,而不是“保证会破坏”;这就是重点。编译器在生成代码时可以完全忽略 UB 的可能性,而不是使用额外的指令来确保 UB 中断。 (如果需要,请使用 -fsanitize=undefined 编译)。

了解究竟发生了什么需要查看 asm,而不仅仅是尝试运行它。

warning: address of stack memory associated with local variable 'buf' returned [-Wreturn-stack-address]
      return buf;
             ^~~

即使没有 -Wall 启用,Clang 也会打印此警告。 正是因为它不是合法的 C,无论您的目标是什么 asm 调用约定。


Clang 使用为其编译的目标的 C 调用约定1。同一个 ISA 上的不同操作系统可以有不同的约定,尽管在 x86 之外大多数 ISA 只有一个主要的调用约定。 x86 已经存在了很长时间,以至于最初的调用约定(没有寄存器参数的堆栈参数)效率低下,因此各种 32 位约定不断发展。微软选择了与其他所有人不同的 64 位约定。于是就有了 x86-64 System V、Windows x64、i386 System V for 32-bit x86、AArch64 的标准约定、PowerPC 的标准约定等等。


我用 clang 测试了几次,每次显示字符串时

它是否“有效”的“决定”/“运气”是在编译时做出的,而不是在运行时。用同一个编译器多次编译/运行同一个源代码不会告诉你什么。

查看生成的 asm 以找出 char buf[4] 的结束位置。


我的猜测:也许您使用的是 Windows x64。与大多数调用约定相比,在那里发生工作更合理,您希望buf[4] 最终位于main 中的堆栈指针下方,因此callputsputs 本身将是很可能会覆盖它。

如果您在禁用优化的情况下在 Windows x64 上编译,retx() 的本地 char buf[4] 可能会放置在它拥有的影子空间中。然后调用者以相同的堆栈对齐方式调用puts(),因此retx 的影子空间变为puts 的影子空间。

如果puts 碰巧没有写入它的影子空间,那么retx 存储的内存中的数据仍然存在。例如也许puts 是一个包装函数,它依次调用另一个函数,而无需先为自己初始化一堆本地函数。但不是尾调用,所以它分配了新的影子空间。

(但这不是clang8.0在禁用优化的实践中所做的。看起来buf[4]将被放置在RSP下方并被踩到那里,使用__attribute__((ms_abi))从Linux clang获取Windows x64代码生成: https://godbolt.org/z/2VszYg)

但也有可能在堆栈参数约定中,在调用之前保留填充以将堆栈指针对齐 16。 (例如,用于 32 位 x86 的 Linux 上的现代 i386 System V)。 puts() 有一个 arg 但 retx() 没有,所以可能 buf[4] 最终在内存中,调用者在为 puts 推送指针 arg 之前“分配”为填充。

当然这是不安全的,因为在没有红区的调用约定中,数据会暂时位于堆栈指针下方。 (只有少数 ABI / 调用约定有红色区域:堆栈指针下方的内存,保证不会被目标进程中的信号处理程序、异常处理程序或调试器调用函数异步破坏。)


我想知道启用优化是否会使其内联并碰巧起作用。但是没有,我在 Windows x64 上测试过:https://godbolt.org/z/k3xGe4clang 和 MSVC 都优化了 "buf\0" 的任何存储到内存中。相反,他们只是将 puts 传递给一些未初始化的堆栈内存的指针。

由于启用优化而中断的代码几乎总是 UB。


脚注 1:x86-64 System V 除外,其中 clang 使用了一个额外的未记录的调用约定“功能”:窄整数类型作为寄存器中的函数参数被假定为符号扩展为 32 位。 gcc 和 clang 在调用时都会这样做,但 ICC 不会,因此从 ICC 编译的代码调用 clang 函数会导致损坏。见Is a sign or zero extension required when adding a 32bit offset to a pointer for the x86-64 ABI?

【讨论】:

  • 顺便说一句,我使用的是 linux,目标是 x86-64 架构
  • @YuriAlbuquerque:很幸运,call puts 没有碰巧覆盖堆栈上的buf,位于其返回地址下方 12 到 9 个字节处。也许包装函数会进行一次推送,然后保留更多填充。 godbolt.org/z/94l245
  • -pedantic:返回本地地址在 C 中是合法的,因为当 return 语句分配返回值时,指针仍然是合法的。 UB 来自尝试以任何方式使用返回值。可以编写一个返回本地指针的正确程序,但这并不是说它在任何方面都有用。
【解决方案2】:

C11 草案 N1570 的附录 L 承认某些情况(即“非关键的未定义行为”),其中标准没有施加特定行为要求,而是使用非零定义 __STDC_ANALYZABLE__ value 应该提供一些保证,以及其他情况(“关键的未定义行为”),实现通常不保证任何事情。尝试访问超过其生命周期的对象将属于后一类。

虽然没有什么可以阻止实现提供超出标准要求的行为保证,即使对于关键未定义行为也是如此,并且某些任务会要求实现这样做(例如,许多嵌入式系统任务要求程序取消引用指向其目标的地址的指针不满足“对象”的定义),访问超过其生命周期的自动变量是一种行为,很少有实现会提供任何保证,除了可能保证读取任意 RAM 地址不会产生未指定值之外的副作用。

即使保证自动对象在堆栈上的布局方式的实现也很少保证在函数返回和调用者的下一个操作之间不会覆盖保存它们的存储空间。除非中断被禁用,否则中断处理可能会覆盖已由不再处于活动堆栈帧中的自动对象使用的任何存储空间。

虽然许多实现可以配置为对标准没有要求的操作行为提供有用的保证,但我想不出任何可以配置为提供足够保证以使上述代码可用的实现。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-28
    • 2019-07-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多