访问绑定的未定义行为之外的全局数组?

时间:2014-10-17 14:09:45

标签: c undefined-behavior

我今天刚上课了 - 阅读C代码和输入,如果程序实际运行,所需答案就是屏幕上显示的内容。其中一个问题是a[4][4]作为全局变量,并且在该程序的某个点上,它尝试访问a[27][27],所以我回答了类似“Accessing an array outside its bounds is an undefined behavior”的内容,但老师说{ {1}}的值为a[27][27]

之后,我尝试some code检查“所有未初始化的golbal变量是否设置为0”是否为真。好吧,这似乎是真的。

现在我的问题是:

  • 似乎已经清除了一些额外的内存并保留了代码运行。保留多少内存?为什么编译器会保留比应该更多的内存,它的用途是什么?
  • 对于所有环境,0a[27][27]吗?

编辑:

在该代码中,0是声明的唯一全局变量,a[4][4]中还有一些本地变量。

我在DevC ++中再次尝试that code。所有这些都是main()。但在VSE中并非如此,其中大多数值为0,但有些值具有Vyktor指出的随机值。

8 个答案:

答案 0 :(得分:50)

你是对的:它是未定义的行为,你无法计算它总是产生0

至于为什么在这种情况下你看到零:现代操作系统将内存分配给相对粗粒度的块中的进程,这些块称为比单个变量大得多的页面(在x86上至少为4KB)。如果您有一个全局变量,它将位于页面的某个位置。假设a类型为int[][]int s为系统上的四个字节,a[27][27]将位于距a开头约500个字节的位置。因此,只要a靠近页面的开头,访问a[27][27]将由实际内存支持并读取它不会导致页面错误/访问冲突。

当然,你不能指望这一点。例如,如果a前面有大约4KB的其他全局变量,则a[27][27]将不会被内存支持,当您尝试读取它时,您的进程将崩溃。

即使进程没有崩溃,您也不能指望获取值0。如果您在现代多用户操作系统上有一个非常简单的程序,除了分配此变量并打印该值之外什么都不做,您可能会看到0。在将内存移交给进程时,操作系统将内存内容设置为某些良性值(通常为全零),以便来自一个进程或用户的敏感数据不会泄漏到另一个进程或用户。

但是,没有一般保证您读取的任意内存为零。你可以在一个没有在分配时初始化内存的平台上运行你的程序,你会看到它上次使用时发生的任何值。

此外,如果a后面跟着足够的其他全局变量,这些变量被初始化为非零值,那么访问a[27][27]会显示出任何值。

答案 1 :(得分:28)

访问数组越界是未定义的行为,这意味着结果是不可预测的,因此a[27][27] 0的{​​{1}}结果根本不可靠。

如果我们使用clang

-fsanitize=undefined会非常清楚地告诉您

runtime error: index 27 out of bounds for type 'int [4][4]'

一旦你有未定义的行为,编译器就可以真正做任何事情,我们甚至看到gcc基于未定义行为的优化turned a finite loop into an infinite loop的示例。如果检测到未定义的行为,clanggcc在某些情况下都可以generate and undefined instruction opcode

为什么它是未定义的行为,Why is out-of-bounds pointer arithmetic undefined behaviour?提供了很好的原因摘要。例如,结果指针可能不是有效地址,指针现在可以指向已分配的内存页面,您可以使用内存映射硬件而不是RAM等...

存储静态变量的段很可能比你正在分配的数组要大得多,或者你正在踩踏的段虽然恰好被归零,所以你在这种情况下只是幸运但又完全不可靠的行为。您的page size is 4ka[27][27]的访问权限很可能在该范围内,这可能就是您没有看到分段错误的原因。

标准是什么

draft C99 standard告诉我们这是6.5.6 添加运算符部分中未定义的行为,它涵盖了数组访问归结的指针算法。它说:

  

添加或减去具有整数类型的表达式时   从指针开始,结果具有指针操作数的类型。如果   指针操作数指向数组对象的元素和数组   足够大,结果指向一个偏离的元素   原始元素使得下标的差异   结果和原始数组元素等于整数表达式。

     

[...]

     

如果指针操作数和结果都指向了元素   相同的数组对象,或者超过数组对象的最后一个元素,   评估不得产生溢出;否则,行为   未定义。如果结果指向一个过去的最后一个元素   数组对象,不得用作一元*的操作数   被评估的运算符。

并且未定义行为的标准定义告诉我们标准对行为没有要求,并且注意到可能的行为是不可预测的:

  

行为,使用不可移植或错误的程序构造或   错误的数据,本国际标准没有规定   要求

     

注意可能的未定义行为包括忽略这种情况   完全无法预测的结果,[...]

答案 2 :(得分:11)

以下是标准的引用,它指定了什么是未定义的行为。

  

J.2未定义的行为

     
      
  • 数组下标超出范围,即使某个对象显然可以使用   给定下标(如左边的表达式a [1] [7]给出声明int   a [4] [5])(6.5.6)。

  •   
  • 将指针加到或减去数组对象和数组对象   整数类型产生的结果指向数组对象之外并用作   被评估的一元*运算符的操作数(6.5.6)。

  •   

在您的情况下,您的数组下标完全在数组之外。取决于该值将为零是完全不可靠的。

此外,整个计划的行为仍然存在问题。

答案 3 :(得分:8)

如果只是从visual studio 2012运行你的代码并获得这样的结果(每次运行时都不同):

Address of a: 00FB8130
Address of a[4][4]: 00FB8180
Address of a[27][27]: 00FB834C
Value of a[27][27]: 0
Address of a[1000][1000]: 00FBCF50
Value of a[1000][1000]: <<< Unhandled exception at 0x00FB3D8F in GlobalArray.exe:
                            0xC0000005: Access violation reading location 0x00FBCF50.

当您查看 Modules 窗口时,您会看到您的应用程序模块内存范围是00FA0000-00FBC000。除非你CRT Checks打开没有将控制你在你的记忆中做什么(只要你不违反{{3} })。

所以你0 a[27][27]纯粹偶然得到了00FB8130。当您从位置a0x00FB8130 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8180 01 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 ................ 0x00FB8190 c0 90 45 00 b0 e9 45 00 00 00 00 00 00 00 00 00 À.E.°éE......... 0x00FB81A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB81B0 00 00 00 00 80 5c af 0f 00 00 00 00 00 00 00 00 ....€\¯......... 0x00FB81C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ .......... 0x00FB8330 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x00FB8340 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ <<<< 0x00FB8350 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ .......... ^^ ^^ ^^ ^^ )打开内存视图时,您可能会看到如下内容:

0

您的编译器可能会因为它使用内存的方式而获得a[6][0]代码,但只需几个字节就可以找到另一个变量。

例如,上面显示的内存0x00FB8190指向地址4559040,其中包含整数值{{1}}。

答案 4 :(得分:6)

然后让你的老师解释一下。

我不知道这是否适用于您的系统但是在使用非零字节的数组a之后使用blatting内存进行播放会为a[27][27]提供不同的结果。

在我的系统上,当我打印a[27][27]的内容时,它是0xFFFFFFFF。即转换为无符号的-1是以二进制补码设置的所有位。

#include <stdio.h>
#include <string.h>

#define printer(expr) { printf(#expr" = %u\n", expr); }

   unsigned int d[8096];
   int a[4][4];  /* assuming an int is 4 bytes, next 4 x 4 x 4 bytes will be initialised to zero */
   unsigned int b[8096];
   unsigned int c[8096];


int main() {

   /* make sure next bytes do not contain zero'd bytes */
   memset(b, -1, 8096*4);
   memset(c, -1, 8096*4);
   memset(d, -1, 8096*4);

   /* lets check normal access */
   printer(a[0][0]);
   printer(a[3][3]);

   /* Now we disrepect the machine - undefined behaviour shall result */
   printer(a[27][27]);

   return 0;
}

这是我的输出:

a[0][0] = 0
a[3][3] = 0
a[27][27] = 4294967295

我在评论中看到了在Visual Studio中查看内存。最简单的方法是在代码中的某处添加断点(暂停执行)然后进入Debug ... windows ... Memory菜单,选择例如Memory 1.然后找到数组的内存地址{{1} }。在我的案例中,地址是a。所以你在地址fiend中输入0x0130EFC0并按Enter键。这显示了该位置的内存。

就我而言。

0x0130EFC0

数组的过程为零,其字节大小为4 x 4 x 0x0130EFC0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .................................. 0x0130EFE2 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff ff ff ..............................ÿÿÿÿ 0x0130F004 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 0x0130F026 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 0x0130F048 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ 一个int(在我的情况下为4)= 64字节。地址0x0130EFC0的字节数均为0xFF(来自b,c或d内容)。

请注意:

sizeof

这是您看到的所有0x130EFC0 + 64 = 0x130EFC0 + 0x40 = 130F000 个字节的开头。可能是数组ff

答案 5 :(得分:5)

对于常见的编译器,访问超出其边界的数组只能在非常特殊的情况下提供可预测的结果,您不应该依赖它。示例:

int a[4][4];
int b[4][4];

如果没有对齐问题,并且您既不要求进行积极优化也不要进行清理检查,a[6][1]实际上应该是b[2][1]。但请不要在生产代码中这样做!

答案 6 :(得分:5)

特定的系统上,您的老师可能是正确的 - 这可能是您的特定编译器和操作系统的行为方式。

通用系统上(即没有“内幕”知识),那么你的回答是正确的:这是UB。

答案 7 :(得分:1)

首先C语言没有边界检查。实际上它几乎没有检查任何东西。这是C的喜悦和厄运。

现在回到这个问题,如果你溢出内存并不意味着你触发了段错误。 让我们仔细看看它的工作原理。

启动程序或输入子程序时,处理器会在堆栈中保存函数结束时返回的地址。

堆栈已在进程内存分配期间从操作系统初始化,并获得了一系列合法内存,您可以根据需要进行读取或写入,而不仅仅是存储返回地址。

编译器用于创建本地(自动)变量的常用做法是在堆栈上保留一些空间,并将该空间用于变量。查看以下众所周知的32位汇编程序序列,命名为序言,您可以在任何函数中找到:

push ebp      ;save register on the stack
mov ebp,esp   ;get actual stack address
sub esp,4     ;displace the stack of 4 bytes that will be used to store a 4 chars array

考虑到堆栈在数据的反方向上增长,内存的布局是:

0x0.....1C   [Parameters (if any)]    ;former function
0x0.....18   [Return Address]
0x0.....14   EBP
0x0.....10   0x0......x               ;Local DWORD parameter
0x0.....0C   [Parameters (if any)]    ;our function
0x0.....08   [Return Address]
0x0.....04   EBP
0x0.....00   0, 'c', 'b', 'a'    ;our string of 3 chars plus final nul

这称为堆栈帧。

现在考虑从0x0 .... 0开始到0x ... 3结束的四个字节的字符串。如果我们在数组中写入超过3个字符,我们将按顺序替换:保存的EBP副本,返回地址,参数,先前函数的局部变量,然后是EBP,返回地址等。

我们得到的最大的景观效果是,在函数返回时,CPU尝试跳回到生成 segfault 的错误地址。如果局部变量之一是指针,则可以实现相同的行为,在这种情况下,我们将尝试读取或写入错误的位置,再次触发段错误。

当不能发生段错误时: 当bloated变量不在堆栈上,或者你有很多局部变量,你覆盖它们而不触及返回地址(并且它们不是指针)。 另一种情况是处理器在局部变量和返回地址之间保留一个保护空间,在这种情况下,缓冲器溢出不会到达地址。 另一种可能性是随机访问数组元素,在这种情况下,超大数组可能会超出堆栈空间并溢出其他数据,但幸运的是我们还没有触及那些被映射到保存返回地址的元素(everythibng可能发生.. )。

什么时候我们可以让segfault膨胀变量不在堆栈上? 当溢出数组绑定或指针时。

我希望这些是有用的信息......