前文链接:上次由于一个很常见的printf-bug(下文有提及)引发了我对栈的思考,并写下了一点总结。这次就尝试对不同的C环境进行实践,检测其传递参数的一些性质。
这是今天写的检查C环境的一段程序、能够判断环境的大小端、栈帧增长方向、传递参数时的压栈顺序、以及参数的求值顺序。
代码如下:
#include <stdio.h>
#include <assert.h>
#include <inttypes.h>
typedef const char *string_literal;
string_literal Endian() {
union {
uint16_t u16;
uint8_t u8; /* if FF small endian */
} u = {.u16 = 0x00FF};
return u.u8 ? "Small Endian" : "Big Endian";
}
enum {H2L, L2H} SD;
string_literal StackFrameDirection()
{
static string_literal *addr;
string_literal rtn;
return !addr ? addr = &rtn, rtn = StackFrameDirection(), addr = NULL, rtn
: &rtn < addr ? SD = H2L , "High -> Low" : (SD = L2H, "Low -> High");
}
enum {R2L, L2R} APO;
string_literal ArgumentsPushOrder(int a, int b)
{
(void)StackFrameDirection();
return (APO = !!SD ^ (&a > &b) ? L2R : R2L) ? "Left -> Right" : "Right -> Left";
}
string_literal ArgumentsEvaluationOrder(int a, int b)
{
return a < b ? "Left -> Right" : "Right -> Left";
}
int a_arg() {
static int cnt;
return ++cnt;
}
int main()
{
printf("In this C implementation:
");
printf(" Endian: %s
", Endian());
printf(" StackFrameDirection: %s
", StackFrameDirection());
printf(" ArgumentsPushOrder: %s
", ArgumentsPushOrder(a_arg(), a_arg()));
/* Evaluation Order below is determined by Complier and maybe not always same */
printf(" ArgumentsEvaluationOrder: %s
", ArgumentsEvaluationOrder(a_arg(), a_arg()));
return 0;
}
我在macOS(intel)上以及树莓派OS(ARM Cortex-A)上都是这个结果:
In this C implementation:
Endian: Small Endian
StackFrameDirection: High -> Low
ArgumentsPushOrder: Left -> Right
ArgumentsEvaluationOrder: Left -> Right
在某咸鱼的 win10(intel) mingw 上的结果:
In this C implementation:
Endian: Small Endian
StackFrameDirection: High -> Low
ArgumentsPushOrder: Right -> Left
ArgumentsEvaluationOrder: Right -> Left
!!只有压栈顺序不一样。
Win下的压栈顺序和 WIN32 缓冲区溢出的知识相互照应了。
树莓派的压栈顺序又和学 ARM 的 ATPCS 相互照应了。
所以上次在树莓派(ILP32)上的异常结果的具体原因可以尝试分析一下了:
int64_t i = 1;
printf("%ld
", i); // "%" PRId32
$ ./a.out
0
$
因为树莓派上的 GCC 的数据模型为 ILP32,
所以 printf("%ld
", i);
可以简化成 F(P32, LL64);
;
假设 P32 为 0xFFFFFCD0 , LL64 为 1 即 0x0000000000000001;
因为参数从左边开始压入栈中,且为小端模式,树莓派的栈是从高地址端向低地址端增长,
所以传递参数的时候字节的压栈顺序是 FF FF FC D0 00 00 00 00 00 00 00 01;
按照 C 传递参数以及可变参数 stdarg.h 的原理,printf 会根据 P32 的内容,把更低地址的四个字节00 00 00 00
理解成 long 并输出,所以最后输出了0。
思考:前文检测的规律是有标准的吗?那又是谁制定的呢?
嗯,ATPCS 是否会在 Linux 上起作用,这点真不好说。
假如编译器有自己的传参标准的话,那系统调用怎么处理?
编译器肯定要遵循某种操作系统决定的标准。
可能编译器为了优化,会选择在程序内部的调用使用自己的标准?