无法理解linux中函数调用的简单c代码的输出

时间:2013-01-25 14:19:12

标签: c linux gcc compiler-construction

当我尝试理解函数调用时,我编写了一个简单的代码。但我无法理解它的输出。

#include <stdio.h>

int* foo(int n)
{
    int *p = &n;
    return p;
}

int f(int m)
{
    int n = 1;
    return 999;
}

int main(int argc, char *argv[])
{
    int num = 1;
    int *p = foo(num);
    int q = f(999);
    printf("[%d]\n[%d]\n", *p, q);
    /* printf("[%d]\n", *q); */
}

输出:

[999]
[999]

为什么*p是999?

然后我修改了我的代码,如下所示:

#include <stdio.h>

int* foo(int n)
{
    int *p = &n;
    return p;
}

int f()
{
    int n = 1;
    return 999;
}

int main(int argc, char *argv[])
{
    int num = 1;
    int *p = foo(num);
    int q = f();
    printf("[%d]\n[%d]\n", *p, q);
    /* printf("[%d]\n", *q); */
}

输出:

[1]
[999]

为什么*p是1?我在Linux中,使用gcc但是Clang得到了相同的输出。

6 个答案:

答案 0 :(得分:4)

除了你的代码提出了未定义的行为,因为你正在返回一个指向堆栈变量的指针这一事实,你要求通过改变f()的签名来改变行为的原因。

原因

原因在于编译器为函数构建堆栈帧的方式。假设编译器正在为foo()构建堆栈帧,如下所示:

Address Contents  
0x199   local variable p
0x200   Saved register A that gets overwritten in this function
0x201   parameter n
0x202   return value
0x203   return address

对于f(int m),堆栈看起来很安静:

Address Contents  
0x199   local variable n
0x200   Saved register A that gets overwritten in this function
0x201   parameter m
0x202   return value
0x203   return address

现在,如果在foo中返回指向'n'的指针会发生什么?结果指针将为0x201。返回foo后,堆栈顶部位于0x204。内存保持不变,您仍然可以读取值“1”。这有效直到调用另一个函数(在你的情况下为'f')。调用f后,位置0x201将被参数m的值覆盖。

如果您访问此位置(并使用printf语句),则会显示“999”。如果在调用f()之前复制了此位置的值,则会找到值“1”。

坚持我们的例子,f()的堆栈框架看起来像这样,因为没有指定参数:

Address Contents  
0x200   local variable n
0x201   Saved register A that gets overwritten in this function
0x202   return value
0x203   return address

当您使用'1'初始化局部变量时,您可以在调用f()后在位置0x200处读取'1'。如果您现在从位置0x201读取值,您将获得已保存寄存器的内容。

其他一些陈述

  • 至关重要的是要理解上述说明是为了向您展示观察观察事物的方法。
  • 真正的行为取决于您使用的工具链和所谓的呼叫对话。
  • 人们很容易想象,有时很难预测会发生什么。释放后访问内存是一种安静的类似情况。这就是为什么它总是无法预测会发生什么。
  • 此行为甚至可以随着更改优化级别而改变。例如。我可以想象,如果你打开例如-O3,观察将会有所不同,因为未使用的变量n将不再出现在二进制文件中。
  • 了解了背后的机制,为什么对从foo检索到的地址的写访问可能导致严重问题应该是可以理解的。

勇敢地试图通过实验证明这种解释

首先,重要的是要看到上面的解释不依赖于真正的堆栈框架布局。我刚刚介绍了布局,以便使插图易于理解。

如果你想在你自己的机器上测试行为,我建议你带上你最喜欢的调试器,看一下局部变量和参数放置的地址,看看到底发生了什么。请记住:更改f的签名会更改堆栈中的信息。所以唯一真正的“便携式”测试是改变f()的参数并观察值p指向的输出。

在调用f(void)的情况下,放在堆栈上的信息大不相同,并且在位置p处写入的值不再依赖于参数或本地。它还可以依赖于main函数的堆栈变量。

在我的机器上,例如,再现显示您在第二个变体中读取的'1'来自保存用于存储'1'到“num”的寄存器,因为它似乎用于加载n。< / p>

我希望这会给你一些见解。如果您有其他问题,请发表评论。 (我知道这有点奇怪)

答案 1 :(得分:2)

您正在调用未定义的行为。您不能返回局部变量的地址(在本例中为参数int n),并期望它稍后有用。

答案 2 :(得分:2)

一个局部变量,例如代码中的n

int* foo(int n)
{
    int *p = &n;
    return p;
}
{p <1}}函数完成后,

“消失”。

您无法使用它,因为访问该变量可能会给您带来不可预测的结果。不过你可以写这样的东西:

foo

因为您的变量int* foo(int* n) { *n = 999; return p; } int main(int argc, char *argv[]) { int num = 1; int *p = foo(&num); printf("[%d]\n", *p); } 在打印时仍然存在。

答案 3 :(得分:0)

在你的第一个样本中,当你做

int num = 1;
int *p = foo(num);

其中foo()

int* foo(int n)
{
    int *p = &n;
    return p;
}

当传递来自num的变量main()时,它会按值传递给foo。换句话说,在堆栈上创建了一个名为num的变量n的副本。 numn都具有相同的值,但它们是不同的变量,因此会有不同的地址。

当您从p返回foo()时,main()获取的地址值与num <{1}}中的main()地址不同/ p>

同样的解释适用于您修改的程序。

让我们看另一个例子来澄清:

int i = 2;

int * foo()
{
return &i;
}

int main() {

i = 1;
int *p = foo();
return 0;

}

在这种情况下,i在堆上声明,同一imain()foo()中都被引用。相同的地址和相同的值。

让我们看看第三个例子:

int i = 2;

int * foo(int i)
{
return &i;
}

int main() {

int i = 1;
int *p = foo(i);
return 0;

}

此处,即使存在全局i,它也会被i中的局部变量main()隐藏,这就是传递给foo()的内容。因此,从&i返回的foo,即pmain()的值,将与main()中声明的变量i的地址不同。

希望这澄清了变量范围和值传递,

答案 4 :(得分:0)

没有汇编程序输出并不容易,但这是我的猜测:

当地人和参数在筹码上被收录。因此,当调用foo时,它将返回堆栈中第一个参数的地址。

在第一个示例中,您将一个参数传递给第二个函数,该函数也将被推送到堆栈中,正好是p指向的位置。因此它会覆盖*p

的值

在第二个示例中,第二个调用中未触及堆栈。旧值(num)仍在那里。

答案 5 :(得分:0)

这种未定义的行为是由于堆栈的参与

int *p = foo(num);
int q = f(999);

在第一种情况下,当您说&num时,它实际上将地址存储在存储num的堆栈中。然后foo(num)完成它的执行,f(999)开始执行参数999.由于使用相同的堆栈,堆栈中存储num的相同位置现在具有参数999.并且我们知道堆栈是连续的

这是打印999的原因。实际上两者都试图打印堆栈中相同位置的内容。

而在第二种情况下,num不会被覆盖,因为没有参数传递给f() 因此,这会按预期打印。