clang 使用的调用约定是什么?
What is the calling convention that clang uses?
clang 编译器使用的默认调用约定是什么?我注意到当我 return 一个本地指针时,引用并没有丢失
#include <stdio.h>
char *retx(void) {
char buf[4] = "buf";
return buf;
}
int main(void) {
char *p1 = retx();
puts(p1);
return 0;
}
这是未定义的行为。它可能会工作,也可能不会,这取决于编译器在为某个特定目标编译时碰巧选择了什么。字面上是un定义的,不是"guaranteed to break";这就是重点。编译器在生成代码时可以完全忽略 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 调用约定。同一个 ISA 上的不同操作系统可以有不同的约定,尽管在 x86 之外大多数 ISA 只有一个主要的调用约定。 x86 已经存在了很长时间,以至于最初的调用约定(没有寄存器参数的堆栈参数)效率低下,因此出现了各种 32 位约定。微软选择了与其他人不同的 64 位约定。所以有 x86-64 System V, Windows x64, i386 System V for 32-bit x86, AArch64's standard convention, PowerPC's standard convention 等等
I have tested with clang several times and every time I displayed the string
是否"works"的"decision"/"luck"是编译时做的,不是运行时做的。使用同一个编译器多次编译/运行同一个源代码不会告诉你任何事情。
查看生成的 asm 以找出 char buf[4]
结束的位置。
我的猜测:也许您使用的是 Windows x64。碰巧在那里工作比大多数调用约定更合理,你期望 buf[4]
在 main
中的堆栈指针下方结束,所以 call
到 puts
,和 puts
本身,很可能会覆盖它。
如果您在 Windows x64 上编译并禁用优化,retx()
的本地 char buf[4]
可能会被放置在它拥有的影子 space 中。然后调用者以相同的堆栈对齐方式调用 puts()
,因此 retx
的影子 space 变成 puts
的影子 space.
并且如果puts
发生而不是写入它的影子space,那么retx
存储在内存中的数据仍然存在。例如也许 puts
是一个包装函数,它反过来调用另一个函数,而不是先为自己初始化一堆局部变量。但不是尾调用,所以它分配了新的影子 space.
(但这不是 clang8.0 在禁用优化的情况下在实践中所做的。看起来 buf[4]
将被放置在 RSP 下面并被踩到那里,使用 __attribute__((ms_abi))
得到 Windows x64 code-gen 来自 Linux clang: https://godbolt.org/z/2VszYg)
但在 stack-args 约定中也有可能在调用之前留下填充以将堆栈指针对齐 16。 (例如,现代 i386 System V on Linux for 32-bit x86)。 puts()
有一个 arg 但 retx()
没有,所以也许 buf[4]
在内存中结束,调用者 "allocates" 在为 puts
推送指针 arg 之前作为填充.
当然这是不安全的,因为在没有 red-zone 的调用约定中,数据将暂时位于堆栈指针下方。 (只有少数 ABI/调用约定有红色区域:堆栈指针下方的内存保证不会被目标进程中的信号处理程序、异常处理程序或调试器调用函数异步破坏。)
我想知道启用优化是否会使它内联并碰巧起作用。但是不,我测试了 Windows x64: https://godbolt.org/z/k3xGe4。 clang 和 MSVC 都将 "buf[=33=]"
的任何存储优化到内存 中。相反,他们只是传递 puts
指向一些未初始化堆栈内存的指针。
启用优化后中断的代码几乎总是 UB。
脚注 1:除了 x86-64 System V,clang 使用额外的 un-documented "feature" 调用约定:窄整数类型作为寄存器中的函数参数被假定为 sign-extended 到 32 位。 gcc 和 clang 在调用时都会这样做,但 ICC 不会,因此从 ICC-compiled 代码调用 clang 函数可能会导致中断。参见
C11 草案 N1570 的附件 L 承认某些情况(即 "non-critical Undefined Behavior"),在这些情况下,标准没有强加 特定的 行为要求,但实现定义 __STDC_ANALYZABLE__
non-zero 值应该提供一些保证,而在其他情况下 ("critical Undefined Behavior") 实现通常不保证任何东西。尝试访问超过其生命周期的对象将属于后一类。
虽然没有什么可以阻止实现提供超出标准要求的行为保证,即使对于关键未定义行为,但某些任务会要求实现这样做(例如,许多嵌入式系统任务要求程序取消引用指向其地址的指针targets no not satisfy the definition for "objects"), accessing automatic variables past their lifetime is a behavior which several implementations would provide any guarantees beyond properly guarantee that reading an arbitrary RAM address would have no side-effects beyond yield一个未指定的值。
即使保证自动对象在堆栈上的布局方式的实现也很少保证保存它们的存储不会在函数返回和调用者的下一个操作之间被覆盖。除非中断被禁用,否则中断处理可能会覆盖已被不再处于实时堆栈帧中的自动对象使用的任何存储。
虽然许多实现可以配置为对标准没有强加要求的操作的行为提供有用的保证,但我想不出任何可以配置为提供足够保证以使上述代码可用的实现.
clang 编译器使用的默认调用约定是什么?我注意到当我 return 一个本地指针时,引用并没有丢失
#include <stdio.h>
char *retx(void) {
char buf[4] = "buf";
return buf;
}
int main(void) {
char *p1 = retx();
puts(p1);
return 0;
}
这是未定义的行为。它可能会工作,也可能不会,这取决于编译器在为某个特定目标编译时碰巧选择了什么。字面上是un定义的,不是"guaranteed to break";这就是重点。编译器在生成代码时可以完全忽略 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 调用约定。同一个 ISA 上的不同操作系统可以有不同的约定,尽管在 x86 之外大多数 ISA 只有一个主要的调用约定。 x86 已经存在了很长时间,以至于最初的调用约定(没有寄存器参数的堆栈参数)效率低下,因此出现了各种 32 位约定。微软选择了与其他人不同的 64 位约定。所以有 x86-64 System V, Windows x64, i386 System V for 32-bit x86, AArch64's standard convention, PowerPC's standard convention 等等
I have tested with clang several times and every time I displayed the string
是否"works"的"decision"/"luck"是编译时做的,不是运行时做的。使用同一个编译器多次编译/运行同一个源代码不会告诉你任何事情。
查看生成的 asm 以找出 char buf[4]
结束的位置。
我的猜测:也许您使用的是 Windows x64。碰巧在那里工作比大多数调用约定更合理,你期望 buf[4]
在 main
中的堆栈指针下方结束,所以 call
到 puts
,和 puts
本身,很可能会覆盖它。
如果您在 Windows x64 上编译并禁用优化,retx()
的本地 char buf[4]
可能会被放置在它拥有的影子 space 中。然后调用者以相同的堆栈对齐方式调用 puts()
,因此 retx
的影子 space 变成 puts
的影子 space.
并且如果puts
发生而不是写入它的影子space,那么retx
存储在内存中的数据仍然存在。例如也许 puts
是一个包装函数,它反过来调用另一个函数,而不是先为自己初始化一堆局部变量。但不是尾调用,所以它分配了新的影子 space.
(但这不是 clang8.0 在禁用优化的情况下在实践中所做的。看起来 buf[4]
将被放置在 RSP 下面并被踩到那里,使用 __attribute__((ms_abi))
得到 Windows x64 code-gen 来自 Linux clang: https://godbolt.org/z/2VszYg)
但在 stack-args 约定中也有可能在调用之前留下填充以将堆栈指针对齐 16。 (例如,现代 i386 System V on Linux for 32-bit x86)。 puts()
有一个 arg 但 retx()
没有,所以也许 buf[4]
在内存中结束,调用者 "allocates" 在为 puts
推送指针 arg 之前作为填充.
当然这是不安全的,因为在没有 red-zone 的调用约定中,数据将暂时位于堆栈指针下方。 (只有少数 ABI/调用约定有红色区域:堆栈指针下方的内存保证不会被目标进程中的信号处理程序、异常处理程序或调试器调用函数异步破坏。)
我想知道启用优化是否会使它内联并碰巧起作用。但是不,我测试了 Windows x64: https://godbolt.org/z/k3xGe4。 clang 和 MSVC 都将 "buf[=33=]"
的任何存储优化到内存 中。相反,他们只是传递 puts
指向一些未初始化堆栈内存的指针。
启用优化后中断的代码几乎总是 UB。
脚注 1:除了 x86-64 System V,clang 使用额外的 un-documented "feature" 调用约定:窄整数类型作为寄存器中的函数参数被假定为 sign-extended 到 32 位。 gcc 和 clang 在调用时都会这样做,但 ICC 不会,因此从 ICC-compiled 代码调用 clang 函数可能会导致中断。参见
C11 草案 N1570 的附件 L 承认某些情况(即 "non-critical Undefined Behavior"),在这些情况下,标准没有强加 特定的 行为要求,但实现定义 __STDC_ANALYZABLE__
non-zero 值应该提供一些保证,而在其他情况下 ("critical Undefined Behavior") 实现通常不保证任何东西。尝试访问超过其生命周期的对象将属于后一类。
虽然没有什么可以阻止实现提供超出标准要求的行为保证,即使对于关键未定义行为,但某些任务会要求实现这样做(例如,许多嵌入式系统任务要求程序取消引用指向其地址的指针targets no not satisfy the definition for "objects"), accessing automatic variables past their lifetime is a behavior which several implementations would provide any guarantees beyond properly guarantee that reading an arbitrary RAM address would have no side-effects beyond yield一个未指定的值。
即使保证自动对象在堆栈上的布局方式的实现也很少保证保存它们的存储不会在函数返回和调用者的下一个操作之间被覆盖。除非中断被禁用,否则中断处理可能会覆盖已被不再处于实时堆栈帧中的自动对象使用的任何存储。
虽然许多实现可以配置为对标准没有强加要求的操作的行为提供有用的保证,但我想不出任何可以配置为提供足够保证以使上述代码可用的实现.