前两天在朋友的QQ空间上看到一个函数,如下:

void foo()
{  
    int a[4];
    int i;
    for (i = 0; i <= 4; i++) {
        a[i] = 0;  
    } 
}

朋友说这个函数里会陷入死循环。我看了下,除了数组越界问题,其它没看出有什么问题。后来果断敲代码用GCC编译运行一下,发现并不像他说得那样。于是我找他讨论,他说没道理啊,在他那确实陷入死循环了。我想这应该是跟编译环境有关,可是,在什么情况下它会陷入死循环呢?我很疑惑,在听到朋友说到程序是用堆栈存放数据的,我突然想起APUE上面讲过的程序存储空间布局。于是,立马翻到那一章,看到这个表:

 
参数及环境变量
 
未初始化数据段
初始化数据段
正文

这个表每个区域的地址从底部到顶部递增。正文存放机器指令部分,初始化数据段存放程序中明确赋初值的全局变量,未初始化数据段(bss段)存放未明确赋值的全局变量,栈存放自动变量及每次函数调用时所需保存的信息,堆存放动态分配的数据。

看到这表,突然,脑海里顿时有点思路了。

  在朋友的机器上,foo函数的变量i存放于栈底(请注意栈底的地址最高),而数组a则紧跟其后(准确来说是元素a[3]),如下:

i
a[3]
a[2]
a[1]
a[0]

而for循环中,i=4时程序尝试访问a[4],此处地址刚好就是变量i的地址!于是乎,i的值就被赋予了0。经过i++后其值变成了1但仍满足 i<=4的条件,新的循环便又开始了。这就是为什么在朋友的机器上会发生死循环的原因。但是,很不幸,这种情况在我的机器上没有发生。刚开始我以为在我这里i位于数组a的上面,后来使用a[-1]尝试给i赋值,失败了;后来以来是编译器优化的原因,于是尝试用-O0选项关闭优化后再编译一次,还是失败了……现在暂时不知道原因。

  不过,我在实验过程中又发现一些有趣的情况,我将变量i跟数组a放到函数外面时,我发现程序陷入死循环了,情况跟上面的类似。为了验证我的猜想,我特地修改下代码以便查看i的值:

int i;
int a[4];
void foo() { for (i = 0; i <= 4; i++) { printf("%d\n", i); if (i == 4) sleep(1); a[i] = 0; } }

编译后运行结果:

 着实,i的值被修改了。

  代码经这样修改后,数组a跟变量i存储在未初始化数据段中且i紧跟着a之后——此处还有一些不明白的地方,变量跟数组究竟是按什么标准进行存储的?明显不是按声明的顺序,因为数组a在变量i之后声明;那是不是跟声明顺序相反呢?为此我尝试在i跟a之间加入另外一个变量j,但是i的值还是被修改了,因此似乎也不是跟声明顺序相反。

  如果在声明的时候给i赋值了呢,情况又会有什么不一样?如果你理解了上面的程序存储布局表,你就会知道,此时i存储在初始化数据段,而初始化数据段是位于未初始化数据段的前面的(即位于低地址处),于是a数组之后再也不是i了,i值也不会被意外修改。

  那究竟有什么办法避免这种死循环呢?有个小技巧,可以在GCC编译的时候加入优化选项-O2。这样i会被放到CPU的寄存器中,每次程序都会从寄存器中而不是内存访问i的值。像a[4]=0这样间接对i进行修改的只能发生在内存上,循环就自然而然会结束了。不过,这样的修改其实还是很投机取巧的,也不值得推荐的。正统的做法当然是,避免你的数组越界访问。

  经过这一番折腾后,有两个感悟:一是数组越界所会发生的行为有时真令人想不到;二是,APUE确实是个好东西~