试图理解C指针,块和转到语句的问题

时间:2012-11-12 18:22:01

标签: c

我正在尝试通过Jim Trevor的“Cyclone: A safe dialect of C”为PL课程开始处理C和Cyclone。特雷弗给出了一个不安全的首要陈述的例子:

int z;
{ int x = 0xBAD; goto L; }
{ int *y = &z;
L: *y = 3; // Possible segfault
}

Trevor在上面的代码中解释了安全问题如下:

许多编译器堆栈分配 输入时块的局部变量,和 当块退出时,释放(弹出)存储 (虽然这不是C标准规定的)。 如果以这种方式编译示例,那么何时 程序进入第一个程序段,x的空间在堆栈上分配,并用值初始化 0xBAD。 goto跳转到第二个块的中间,直接转到内容的赋值 指针y。因为y是第一个(唯一的)变量 在第二个块中声明,赋值期望 y位于堆栈的顶部。不幸的是,那是 确切地分配了x的位置,因此程序会尝试 写入位置0xBAD,可能触发一个 分段错误。

我不明白为什么go to声明是一个问题。似乎问题是未初始化的指针Z的不可预测的行为。在第二个块的开始,int * y填充Z的地址.Z未初始化,因此它将填充int* y Z引用的内存区域中堆栈上的位模式。我不明白为什么Trevor的论文暗示Z和X都以某种方式引用0xBAD。不会为第一个块创建一个新的堆栈帧(正如Trevor所描述的那样):因此将0xBAD写入内存中的新帧(而不是Z引用的内存中的位置)?

4 个答案:

答案 0 :(得分:5)

  

我不明白为什么去声明是一个问题。

goto L绕过y的初始化(y不会设置为&z),因此分配给who-know-where-it&#39时出现问题; s-pointing *y

  

似乎这个问题是不可预测的行为   未初始化的指针Z

没有。指针&z实际上是有效的。 intz未初始化,但这并不重要,因为您从未尝试过读取它;你真的试图覆盖它。

  

在第二个块的开头,int * y将填充Z的地址。

这就是重点。 goto L绕过了那个。

  

我不明白为什么特雷弗的论文暗示Z和X不知何故都引用了0xBAD

我认为Trevor在这里暗示了第二个潜在的问题,但我不确定有多少编译器(如果有的话)会实际展示它。当离开具有goto的块时,理论上不会递减堆栈指针(例如x86上的ESP)。通过跳过y的初始化,堆栈指针也可能不会递增。因此,如果编译器使用堆栈指针(而不是帧指针,例如x86上的EBP)引用本地,这样的编译器理论上可能会将x误认为y,就好像{{ 1}}发生了。

答案 1 :(得分:1)

如果删除块并分离出值的初始化和声明,则更容易理解该问题。

int z;
int *y;
goto L;
y = &z;
L: 
*y = 42;

这基本上是原始样本中发生的事情,但更清楚一点。这里行y = &z永远不会被执行,因此y指向一个未定义的位置,因此它的设置是不安全的。

答案 2 :(得分:1)

就语言而言,程序的行为是未定义的。 goto跳过y的初始化;指针对象存在,但它不指向任何已定义的位置。解除引用y具有未定义的行为。

但是更详细地查看代码,并对其行为进行一些(无根据的)假设:

int z;
{ 
    int x = 0xBAD; goto L; 
}   
{ 
    int *y = &z;
    L: *y = 3; // Possible segfault
}

局部变量(通常)在堆栈上分配。当控制到达包含其定义的块的末尾时,每个局部变量都不存在。

我认为,这个想法是第一个块创建了一个int对象x,并为其指定了值0xBAD。当x将控件转移出该块时,goto不再存在 - 但0xBAD值可能仍然存在于堆栈顶部的上方。

goto将控件转移到第二个块。它会跳过y的初始化,但不会跳过它的分配;指针对象y仍然在堆栈上分配,无论控件是直接进入块还是通过goto语句。如果0xBAD值保留在堆栈顶部之上,则可以在同一位置轻松分配y;由于跳过初始化,0xBAD值可以保留在y中(或者更确切地说,构成int 0xBAD表示的位保留在y中并被解释为指针值。)

因此,作业*y = 3;会尝试将值3存储在内存位置0xBAD中。

这可能是定义,初始化和使用变量x的基本原理:在y占用的内存中留下特定的垃圾值。

但实际上 none 我所描述的行为(在第一段之后)是C标准所要求的。并行块中的对象(如示例中的对象)可能存储也可能不存储在同一存储器位置。 x的初始化,甚至它在堆栈上的分配,都可以通过优化编译器轻松消除。并且局部变量甚至不需要在“堆栈”上分配(在由堆栈指针管理的连续内存区域的意义上); C标准甚至没有使用“堆栈”这个词。连续堆栈是实现局部变量所需语义的最自然方式,但并不是必需的。当然,intint*的大小不一样。

底线:执行*y = 3;时,y的值是未初始化的垃圾(我故意避免使用“随机”一词),因此取消引用y的行为是未定义。鉴于某些假设,垃圾可能看起来像0xBAD,但它并不重要。

答案 3 :(得分:0)

正如您所说,问题是变量y可能在初始化之前被访问。您提供的代码段只是解决问题的一种方式。

当我使用-Wall选项与GCC编译时,它会警告warning 'y' is used uninitialized in this function。如果我使用g ++将其编译为C ++代码,那实际上是一个错误:

test.cc:8:3: error: jump to label ‘L’
test.cc:6:25: error:   from here
test.cc:7:10: error:   crosses initialization of ‘int* y’

虽然在这种情况下y是POD类型,但如果它是具有构造函数的类,goto将跳过构造函数。 C ++语言规范说这在所有情况下都是非法的。