为什么要使用函数调用而不是变量地址来检测堆栈增长方向?

8

我看到了关于检测堆栈增长的不同回答,我了解到在现代架构中,堆栈可能会随机增长,可能会在堆上创建等等。

然而,在这个经典的面试问题中,我想要理解为什么人们使用函数调用而不是比较同一函数中的2个本地变量。我认为肯定有某些特殊的原因,但作为一个非C/低级开发者[Java :)],我只是猜测。

这是我尝试过的代码:

void sub (int *a)  {
    int b;
    int c;
    printf ("a:%d\n", a);
    printf ("b:%d\n", &b);
    printf ("c:%d\n", &c);
    if (&b > a) {
        printf ("Stack grows up.\n");
    } else {
        printf ("Stack grows down.\n");
    }
}

int main (void) {
    int a;
    int b;
    sub (&a);
    printf ("\nHere we go again!!\n");
    if (&b > &a)  {
        printf ("Stack grows up.\n");
    } else  {
        printf ("Stack grows down.\n");
    }
    return 0;
}

我还找到了一篇文章,试图优化这个解决方案,但我也不理解:http://www.devx.com/tips/Tip/37412 另外,在不同的回复和其他主题中,似乎问题本身是错误/不相关的。作为面试问题,除非有人调查答案,否则它可能会强化错误的假设!
谢谢!

请参见此回答。我已经详细回答了您的问题。 - Megharaj
5个回答

6

你无法完全控制编译器选择分配本地变量的顺序。但是,你可以合理地控制将调用哪些函数以及以什么顺序进行调用。


像这样做 int a = 0; int b = &a; 不是会强制规定顺序吗?(我取变量地址是为了避免被优化掉的可能性。) - RedX
@RedX:完全不是这样。初始化的顺序与在堆栈上分配空间的相对位置无关。 - R.. GitHub STOP HELPING ICE
同时,即使取a的地址,如果你从未使用该地址,它也可能被优化掉。顺便说一下,int b = &a;是无效的C代码,无法编译。你需要进行强制类型转换。 - R.. GitHub STOP HELPING ICE

5

声明变量的顺序将被放置在堆栈上是未定义的,但在由函数调用的函数中,内部函数调用的参数必然比外部函数的参数后推到堆栈上。


4

一个堆栈帧中,编译器可以自由地按照其认为合适的顺序排序本地变量,因此代码:

int i;
double j;

变量可能在j之前或之后出现i。只要编译器生成了正确的代码来访问变量,它可以放在任何地方。

实际上,除非您使用取地址运算符&(或者必须获取地址),否则变量可能永远不会在堆栈上。它可能在调用期间存储在寄存器中。

然而,栈帧本身的放置顺序是受限制的,因为如果它们的顺序错乱,函数返回将无法正常工作(委婉地说)。


当然,栈增长的方向仅在非常有限的情况下有用。绝大多数代码都不应该关心它。如果您对不同的架构以及它们如何处理堆栈感兴趣,请参阅此答案


您是在说我的示例中,由于我没有使用“&”,它可能不在堆栈上。隐式“&”的含义是什么? - codeObserver
是的,它可能根本不在堆栈上,而是放入寄存器中。 - paxdiablo

4
编译器可以并且确实会重新排列栈帧中的变量:
#include <stdio.h>

int main ()
{
    char c1;
    int a;
    char c2;
    int b;

    printf("%p %p %p %p\n", &c1, &a, &c2, &b);
    return 0;
}

打印

0x7ffff62acc1f 0x7ffff62acc18 0x7ffff62acc1e 0x7ffff62acc14

这里是在64位Linux上使用gcc 4.4.3编译的。c2已经移动到了c1旁边。


+1 证明一下。好奇你是怎么确定这个会完成的?所有相似数据类型的文字都被分组在一起进行优化吗? - codeObserver
1
@p1. 我不确定是否已经完成,但我花了足够的时间重新排列结构体字段以减少内存消耗,以知道如果您想遵守对齐约束并最小化堆栈帧的大小,则必须进行重新排序。 - AProgrammer

3
问题在于当您执行以下操作时:

void test(void)
{
    int a;
    int b;
    if (&a < &b)
        ...

你得到的结果与堆栈增长方向无关。唯一知道堆栈增长方向的方法是通过创建新的帧(frame)。编译器可以自由地将变量a放在变量b上面或下面,因为这取决于编译器。不同的编译器可能会给出不同的结果。
但是,如果你调用另一个函数,那么该函数的局部变量必须在新的堆栈帧中,即在调用者的变量增长方向中。

最后一句话/段落并不正确。编译器可以内联函数,这种情况下仍然存在上述问题。确保编译器必须创建一个新的堆栈帧实际上非常困难。我能想到的最好方法是使用函数指针,通过异或其字节与从库调用获取的必定为零的值来破坏函数指针的表示方式,但编译器无法证明它将为零。 - R.. GitHub STOP HELPING ICE
@R 正确,但是防止编译器内联函数很容易。使函数递归是一种方法,使用函数属性是另一种方法,将它们定义在单独的翻译单元中是第三种方法。(这些方法并不总是适用于所有编译器,但仍然有用。) - Dietrich Epp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接