3

当我通过 Jim Trevor为 PL 课程编写的“ Cyclone: A safe dialect of C ”时,我正在尝试掌握 C 和 Cyclone。Trevor 给出了一个不安全的 go-to 语句的例子:

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。C 不会为第一个块创建一个新的堆栈帧(如 Trevor 所述):因此将 0xBAD 写入内存中的新帧(而不是 Z 引用的内存位置)?

4

4 回答 4

5

我不明白为什么 go to 声明在这里是一个问题。

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

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

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

在第二个块的开始处, int * y 被填入 Z 的地址。

这就是我想说的。 goto L绕过它。

我不明白为什么 Trevor 的论文暗示 Z 和 X 都以某种方式引用 0xBAD

我认为 Trevor 在这里暗示了第二个潜在问题,尽管我不确定有多少编译器(如果有的话)会真正展示它。当使用 离开块时goto,堆栈指针(例如ESP在 x86 上)理论上可能不会递减。通过跳过 的初始化y,堆栈指针也可能不会增加。因此,如果编译器使用堆栈指针(而不是帧指针,例如在 x86 上)来引用局部变量,那么EBP理论上这样的编译器可能会误认为xy就好像int* y = 0xBAD发生了一样。

于 2012-11-12T18:26:26.880 回答
1

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

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

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

于 2012-11-12T18:31:44.643 回答
1

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

但是更详细地查看代码,并对它的行为方式做出一些(无根据的)假设:

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

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

我认为这个想法是第一个块创建一个intobject x,并将值分配0xBAD给它。 当将控制权转移出该块x时不再存在- 但该值可能仍然存在于堆栈顶部的上方。goto0xBAD

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

所以赋值*y = 3;尝试将值存储3在内存位置0xBAD

这可能是定义、初始化和使用变量的基本原理x:将特定的垃圾值留在y.

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

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

于 2012-11-12T19:03:56.570 回答
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++ 语言规范说这在所有情况下都是非法的。

于 2012-11-12T18:29:51.060 回答