【问题标题】:Malloc altering behavior of uninitialized variable in separate function?Malloc 在单独的函数中改变未初始化变量的行为?
【发布时间】:2016-04-12 00:12:43
【问题描述】:

这是我的编程语言概念/实现课程的问题。给定以下C代码sn-p:

void foo()  
{ 
    int i; 
    printf("%d ", i++); 
} 
void main()  
{ 
    int j; 
    for (j = 1; j <= 10; j++)  
        foo(); 
}

foo 中的局部变量 i 从未初始化,但在大多数系统上其行为类似于静态变量。这意味着程序将打印 0 1 2 3 4 5 6 7 8 9。我理解它为什么这样做(i 的内存位置永远不会改变),但作业中的问题要求修改代码(不改变 foo)以改变这种行为。我想出了一个可行的解决方案,让程序打印十个0,但我不知道它是否是“正确”的解决方案,老实说我不知道​​为什么它有效。

这是我的解决方案:

void main()  
{ 
    int j; 
    void* some_ptr = NULL;
    for (j = 1; j <= 10; j++)
    {
        some_ptr = malloc(sizeof(void*));
        foo();
        free(some_ptr);
    }
}

我最初的想法是 i 没有改变位置,因为在 foo 的调用周围没有发生其他内存操作,所以分配一个变量应该会破坏它,但是由于 some_ptr 在堆中分配并且 i 在堆栈上,所以 some_ptr 的分配不应该对 ?我的想法是编译器正在玩一些优化该子例程调用的游戏,有人可以澄清一下吗?

【问题讨论】:

  • 你被问到的确切问题是什么? foo() 中的 i 无法从其他地方初始化,除非通过某种软糖使未定义的行为看起来像您认为它应该具有。在 C 中,i 不会在 任何 系统上初始化,当然也不会在 大多数 上初始化。
  • @WeatherVane 问题的确切措辞是:考虑以下 C 程序:[代码块如上] 子例程 foo 中的局部变量 i 从未初始化。然而,在许多系统上,变量 i 似乎在对 foo 的调用之间“记住”了它的值,并且程序将打印 0 1 2 3 4 5 6 7 8 9。 (a) 建议对此行为的解释。 (b) 更改上面的代码(不修改函数 foo)来改变这种行为。
  • 我的测试不打印 09。这是未定义的行为。因此,您所能做的就是将其更改为另一种未定义的行为。它与malloc 无关,与在调用foo 时覆盖存储i 的堆栈位置有关。
  • 这就是我所害怕的,对于家庭作业来说,这不是一个构思非常好的问题。对于未定义的行为,我只在我的机器上使用 gcc 对其进行了测试,它确实打印了 0-9。无论如何,谢谢你

标签: c memory-management malloc programming-languages compiler-optimization


【解决方案1】:

不可能有“正确”的解决方案。但是可能有一类解决方案适用于特定的 CPU 架构、ABI、编译器和编译器选项。

将代码更改为这样的代码将产生改变堆栈上方的内存的效果,该效果应该会以目标方式影响许多(如果不是大多数)环境。

void foo()  
{ 
    int i; 
    printf("%d ", i++); 
} 
void main()  
{ 
    int j;
    int a [2];

    for (j = 1; j <= 10; j++)
    {
        foo();
        a [-5] = j * 100;
    }
}

输出(Linux 上的 gcc x64):

0 100 200 300 400 500 600 700 800 900 

a[-5] 是用于开销和跨越两个函数的变量的堆栈字数。有返回地址,保存的堆栈链接值等。当 foo() 写入 a[-5] 时,堆栈可能看起来像这样:

i
saved stack link
return address
main's j
(must be something else)
main's a[]

我在第二次尝试时猜到了 -5。 -4 是我的第一个猜测。

【讨论】:

  • 这是一个非常好的答案,知道“正确”答案几乎取决于解释,我感觉好多了。顺便说一句,你对数组所做的事情非常酷。
  • 什么是可移植的未定义行为? a [-5] 也是未定义的行为。一个UB影响另一个UB,只能教OP如何不编码。
  • @WeatherVane:这个问题不是关于正确的编码方法,而是更多关于这样的事情怎么会发生?它可能由一个错误的人发生指针使用、数组边界错误或堆栈损坏。由于许多实现都以相同的方式使用堆栈,因此这种技术有点可移植,尽管我没有这么说。假设你想编写一个病毒:你会使用什么样的非未定义行为? :-)
  • 没错,您没有使用“便携”这个词,但这就是“应该影响许多(如果不是大多数)环境”的意思。
【解决方案2】:

当您从main() 调用foo() 时,(未初始化的)变量i 被分配到内存地址。在原始代码中,它恰好为零(在您的机器上,使用您的编译器,以及您选择的编译选项,您的环境设置,以及给定的当前月相——当其中任何一个或无数其他因素,变化)。

通过在调用foo() 之前调用另一个函数,您允许另一个函数用不同的值覆盖foo() 将用于i 的内存位置。不保证会改变;运气不好,你可以用另一个零替换零。

您也许可以使用其他功能:

static void bar(void)
{
    int j;
    for (j = 10; j < 20; j++)
        printf("%d\n", j);
}

在调用foo() 之前调用它会改变i 中的值。致电malloc() 也会改变一些事情。调用几乎任何函数都可能会改变它。

但是,必须(重新)强调原始代码充满了未定义的行为,并且调用其他函数并不会减少未定义的代码。任何事情都有可能发生,而且是有效的。

【讨论】:

  • 这很有意义。我有同样的想法,但不是bar中的循环,我只有int j = 5;。然后程序打印所有 5 个。
【解决方案3】:

foo 中的变量 i 只是未初始化,未初始化的值在进入块时具有不确定的值。您看到它打印某些值的方式完全是巧合,要编写符合标准的 C,您永远不应该依赖这种行为。在使用它之前,您应该始终初始化自动变量。

来自 c11std 6.2.4p6:

对于这样一个没有可变长度数组类型的对象,它的生命周期从进入与其关联的块开始,直到该块的执行以任何方式结束。 (进入封闭的块或调用函数会暂停,但不会结束当前块的执行。)如果递归地进入块,则每次都会创建对象的新实例。对象的初始值是不确定的。如果为对象指定了初始化,则在执行块时每次到达声明或复合文字时都会执行它;否则,每次达到声明时,值都会变得不确定。

【讨论】:

  • 问题不在于代码是否符合标准 C,而且从各方面来说,它都是糟糕的代码。我正在学习的课程是编译器设计的先驱,并且在设计编译器时必须处理上述代码完全合法的事实。我了解良好的编程标准,不欣赏这种傲慢的回应。
  • 我觉得奇怪的是,如果一个“goto”从一个变量使用之后的一个点跳到它被声明之前的一个点,并且第二个“goto”跳过它的声明,这个值仍然是定义的,但是如果在执行顺序中遇到声明,则该值在那时变得不确定。我想知道这是故意还是疏忽?
【解决方案4】:

未初始化的值似乎保留了过去调用的值的原因是它在堆栈上,并且每次调用函数时堆栈指针恰好具有相同的值。

您的代码可能更改值的原因是您开始调用其他函数:mallocfree。它们的内部堆栈变量使用与foo() 中的i 相同的位置。

至于优化,像这样的小程序有完全消失的危险。 GCC 或 Clang 可能 决定,由于使用未初始化的变量是未定义的行为,编译器有权完全删除代码。或者它可能会将i 放入设置为零的寄存器中。然后决定所有 printf 调用输出为零。然后确定您的整个程序只是一个 puts("0000000000") 调用。

【讨论】:

    猜你喜欢
    • 2016-09-23
    • 1970-01-01
    • 2021-09-07
    • 2019-07-26
    • 2014-05-19
    • 1970-01-01
    • 1970-01-01
    • 2021-07-08
    • 2022-11-30
    相关资源
    最近更新 更多