为什么要更改数组大小值来操纵十进制字节?

Why changing array size value manipulating bytes of decimal?

这是来自 CMU 计算机系统课程。在以下示例中:

 typedef struct { 
  int a[2]; 
  double d; } struct_t; 

double fun(int i) { 
volatile struct_t s;
  s.d = 3.14; 
  s.a[i] = 1073741824; /* Possibly out of bounds */ 
  return s.d; } 
fun(0)  ➙ 3.14 
fun(1)  ➙ 3.14 
fun(2)  ➙ 3.1399998664856 
fun(3)  ➙ 2.00000061035156 
fun(4)  ➙ 3.14 
fun(6)  ➙ Segmentation fault 

教授解释说访问 fun(2) 会操纵 double d 的字节。但是,我没有得到:(a)为什么这会操纵从 fun(2) 开始的双字节,(b)每个字节操纵如何与 fun(2) ➙ 3.1399998664856fun(3) ➙ 2.00000061035156 等值相关,直到 fun(6),以及 (c) 为什么它恰好在 fun (6) 达到临界状态?有关我的问题的更多参考,请参阅 here 幻灯片编号 8 和 9。此外,幻灯片上有一个我不理解的解释图。如果您能花点时间解释一下,我们将不胜感激。

幻灯片 9 上的图表表示调用 fun 时的本地内存。每行代表 4 个字节(从右到左列出),内存地址越往下越小。如果您要以这种格式列出地址 0、1、2,...,它将如下所示:

|...
+--+--+--+--+
|11|10| 9| 8|
+--+--+--+--+
| 7| 6| 5| 4|
+--+--+--+--+
| 3| 2| 1| 0|
+--+--+--+--+

幻灯片 9 上的图表显示了 sstruct_t 类型的变量)在内存中的布局方式。系统使用 4 字节 ints 和 8 字节 doubles。因此 s.a[0] 占用 4 个字节(图中第 0 行),s.a[1] 另外 4 个字节(第 1 行),s.d 8 个字节(第 2 行和第 3 行)。

函数访问s.a[i]。编译器将其转换为采用 s.a 起始地址并向其添加 i*4 字节以到达所选元素的代码。在图中,这对应于从第 0 行开始向上 i 行。只要 i 实际上是数组中的有效索引(在示例中:01,因为 a 只有 2 个元素,这就可以正常工作)。

但是如果 i 更大,那么代码最终会访问内存的其他部分。 s.a[2](图中第 2 行)指的是 s.d 的一部分的内存,因此覆盖它会破坏存储在那里的值(s.a[3] 也是如此)。确切的结果值取决于所使用的浮点格式的内部结构(可能是 IEEE 754)。 (我对此不熟悉,所以我不知道这些位是如何被解释为 3.1399998664856。)

s.a[4] 显然并不重要,因为覆盖它没有任何可见的效果。但是覆盖 s.a[6] 崩溃了,这表明我们破坏了一些重要的东西。那可能是 return 地址,即告诉 fun 完成后跳转到哪里的保存位置。通过覆盖它,我们 fun 跳转到无效内存。

要确认这一点(并找出为什么它的索引 6 特别会破坏事物),您必须查看编译器生成的代码。没有通用的答案,因为它取决于所讨论的编译器、优化级别、它运行的系统等。

但是,在 C 语言中越界写入本地数组会在某些时候破坏 return 地址是很常见的。这是因为编译器几乎普遍使用堆栈实现函数调用和本地 ("automatic") 存储,因此堆栈包含本地变量和 return 地址混合。