测试当前C环境的栈帧增长方向以及传递参数时的压栈顺序

前文链接:上次由于一个很常见的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:\n");
    printf("\tEndian:                   %s\n", Endian());
    printf("\tStackFrameDirection:      %s\n", StackFrameDirection());
    printf("\tArgumentsPushOrder:       %s\n", ArgumentsPushOrder(a_arg(), a_arg()));
    /* Evaluation Order below is determined by Complier and maybe not always same */
    printf("\tArgumentsEvaluationOrder: %s\n", 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\n", i); // "%" PRId32
$ ./a.out
0
$

因为树莓派上的 GCC 的数据模型为 ILP32,
所以 printf("%ld\n", 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 上起作用,这点真不好说。
假如编译器有自己的传参标准的话,那系统调用怎么处理?
编译器肯定要遵循某种操作系统决定的标准。
可能编译器为了优化,会选择在程序内部的调用使用自己的标准?

posted @ 2017-11-08 20:13  xxyyttxx  阅读(433)  评论(4编辑  收藏  举报