lang使用的调用约定是什么?

时间:2019-06-22 20:01:55

标签: c assembly undefined-behavior calling-convention

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

#include <stdio.h>

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

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

2 个答案:

答案 0 :(得分:2)

这是未定义的行为。它可能会起作用,也可能不会,这取决于编译器为某些特定目标进行编译时会选择什么。它实际上是 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使用为其 1 编译的目标的C调用约定。尽管在x86之外,大多数ISA仅具有一个主要的调用约定,但同一ISA上的不同OS可以具有不同的约定。 x86已经存在了很长时间,以至于原始的调用约定(没有寄存器arg的堆栈args)效率低下,因此各种32位约定都在发展。微软选择了与其他所有人不同的64位约定。因此,有用于32位x86的x86-64 System V,Windows x64,i386 System V,AArch64的标准约定,PowerPC的标准约定等。


  

我已经多次使用clang进行了测试,每次显示字符串时都

是否“有效”的“决定” /“运气”是在编译时而不是在运行时做出的。使用相同的编译器多次编译/运行相同的源代码对您没有任何帮助。

查看生成的asm,找出char buf[4]的结尾。


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

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

如果puts发生{em> not 而不是写入其影子空间,则retx存储的内存中的数据仍然存在。例如也许puts是一个包装函数,该包装函数依次调用另一个函数,而无需首先为其本身初始化一堆局部变量。但是没有尾注,因此它分配了新的影子空间。

(但是clang8.0在禁用优化的情况下实际上不是这样做的。看起来buf[4]将被放置在RSP下方并踩到那里,使用__attribute__((ms_abi))来获取Windows x64代码源来自Linux lang:https://godbolt.org/z/2VszYg

但是在堆栈args约定中也有可能,在调用之前留出填充以使堆栈指针与16对齐。 (例如,Linux上用于32位x86的现代i386 System V)。 puts()有一个arg,但是retx()没有,因此,buf[4]最终会在调用方将arg推送给puts之前,被调用者“分配”为填充。 / p>

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


我想知道启用优化是否可以使其内联并碰巧起作用。但是,没有,我针对Windows x64:https://godbolt.org/z/k3xGe4进行了测试。 clang和MSVC都将"buf\0"的所有存储优化到内存中。相反,它们只是传递puts指向一些未初始化堆栈存储器的指针。

在启用优化后中断的代码几乎总是UB。


脚注1:x86-64系统V除外,其中clang使用了调用约定的一个额外的未记录“功能”:寄存器中的窄整数类型作为函数args假定被符号扩展为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?

答案 1 :(得分:1)

C11草案N1570的附录L认识到某些情况(例如,“非关键性未定义行为”),该标准不施加任何特定行为要求,而是实施以__STDC_ANALYZABLE__ -zero值应提供一些保证,以及其他情况(“严重的未定义行为”),在这种情况下,实现通常不提供任何保证。尝试访问超过其生存期的对象将属于后一类。

尽管没有什么可以阻止实现提供超出标准要求的行为保证,甚至对于严重的未定义行为,并且某些任务可能要求实现做到这一点(例如,许多嵌入式系统任务要求程序取消引用指向目标地址没有的指针)不满足“对象”的定义),超过它们的生命期访问自动变量是一种行为,除了可以保证读取任意RAM地址不会产生未指定的值之外,几乎没有任何实现可以保证。 >

即使保证自动对象如何在堆栈上布局的实现,也很少保证在返回函数和调用者执行下一次操作之间不会覆盖保存它们的存储。除非禁用中断,否则中断处理可能会覆盖已不在活动堆栈帧中的自动对象使用的所有存储。

虽然可以将许多实现配置为提供对标准没有要求的动作的行为的有用保证,但是我无法想到可以配置为提供足够保证以使上述代码可用的任何实现。 / p>