我正在尝试通过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引用的内存中的位置)?
答案 0 :(得分:5)
我不明白为什么去声明是一个问题。
goto L
绕过y
的初始化(y
不会设置为&z
),因此分配给who-know-where-it&#39时出现问题; s-pointing *y
。
似乎这个问题是不可预测的行为 未初始化的指针Z
没有。指针&z
实际上是有效的。 int
值z
未初始化,但这并不重要,因为您从未尝试过读取它;你真的试图覆盖它。
在第二个块的开头,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标准甚至没有使用“堆栈”这个词。连续堆栈是实现局部变量所需语义的最自然方式,但并不是必需的。当然,int
和int*
的大小不一样。
底线:执行*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 ++语言规范说这在所有情况下都是非法的。