在我上一篇随笔中,我讨论了一个由于数组越界导致程序陷入死循环的情况及其原因。不过,其中还是有些疑问:如果变量跟数组处于同一数据段时(或是栈,或是非初始化数据段等等),它们在内存中是怎样安排的?于是,我重新又小实验了一下。对于代码:

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

使用gcc -c生成目标文件后利用objdump -d反汇编一下,生成如下:

   0:   push   %rbp
   1:   mov    %rsp,%rbp
   4:   movl   $0x0,-0x4(%rbp)
   b:   jmp    1e <foo+0x1e>
   d:   mov    -0x4(%rbp),%eax
  10:   cltq   
  12:   movl   $0x0,-0x20(%rbp,%rax,4)
  1a:   addl   $0x1,-0x4(%rbp)
  1e:   cmpl   $0x4,-0x4(%rbp)
  22:   jle    d <foo+0xd>
  24:   pop    %rbp
  25:   retq   
                           

观察一下此汇编代码知:对i赋值的语句是 movl $0x0,-0x4(%rbp) ,对数组a元素赋值的语句是movl $0x0,-0x20(%rbp,%rax,4) 。其中$0x0对应整数0自然不必多说,而%rbp则代表栈底地址寄存器,%rax我觉得应该是寄存器%eax的最低32位(我是64位机子,%eax也应是64位的),结合前面的代码知道这里就代理着i的值。有此认识后,变量i的地址 -0x4(%rbp) 意义也就清晰了,它位于栈底!而数组i的地址 -0x20(%rbp,%rax,4) 可解析为:%rbp-0x20+%rax*4,也就是说,数组a的地址是 -0x20(%rbp)。(注:第一眼看到这我就懵了,这样的话a[4]不正代表着i吗?因为(20-4)/4=4。后来突然意识到,0x20是16进制数。。。)于是乎,栈的分布图如下:

-0x4 i
-0x8  
-0xc  
-0x10  
-0x14 a[3]
-0x18 a[2]
-0x1c a[1]
-0x20 a[0]

哈,奇怪吧。变量i跟数组a之间竟然存在一段空白!那么,这段空白里面竟然存在有什么数据呢?我一开始也不明白,还以为是APUE里面说的函数调用时所需要保存的信息。后来觉得这还是说不通,因为这些信息应该位于i的上面才是。看着这张表,我想起了struct结构体的变量对齐问题,觉得这两者之中貌似有些类似啊。于是我往函数里面又增加了一个变量,果断不是所料,新增加的变量被放入-0x8的位置!那么,如果这段空白已经被填满了,再加一个变量情况又会变成什么样呢?答案是:栈的空间又增加了0x10的大小,然后,栈会变成这个样子:

-0x4 i
-0x8 j
-0xc k
-0x10 l
-0x14 m
-0x18  
-0x1c  
-0x20  
-0x24 a[3]
-0x28 a[2]
-0x2c a[1]
-0x30 a[0]

 (可能你还会想那么变量i,j,k...是怎么安排的,虽然我没做过验证,但我想应该跟他们声明的顺序以及有没有被初始化有关)

  至此,为什么foo函数在我这不会发生死循环的原因应该算是清楚了。而对于把数组a跟变量i放在foo函数外面的情况,其反汇编出来的代码是:

   0:   push   %rbp
   1:   mov    %rsp,%rbp
   4:   movl   $0x0,0x0(%rip) 
   e:   jmp    32 <foo+0x32>
  10:   mov    0x0(%rip),%eax     
  16:   cltq   
  18:   movl   $0x0,0x0(,%rax,4)
  
  23:   mov    0x0(%rip),%eax      
  29:   add    $0x1,%eax
  2c:   mov    %eax,0x0(%rip)     
  32:   mov    0x0(%rip),%eax       
  38:   cmp    $0x4,%eax
  3b:   jle    10 <foo+0x10>
  3d:   pop    %rbp
  3e:   retq   

有能力的同学自己分析下看看了,我汇编基础不大好,所以还是有些不明白%rip是什么玩意。不过,实验中我曾经打印过a跟i的地址,发现i总会比a大0x10,故a[4]的地址即是i的地址。

  为什么编译器会这样安排呢?我认为,跟结构体的内存对齐是同个道理。具体可参考:http://zhangyu.blog.51cto.com/197148/673792

  最后的最后,不妨再给出一段代码,让大家猜猜输出结果会是什么 。(实践出真知,不妨运行一下,看与你想的一不一样)

#include <stdio.h>

int glob_v1;
int glob_v2;
int glob_array[4];

int glob_v1_with_value = 1;
int glob_v2_with_value = 2;
int glob_array_with_value[4] = {1, 2, 3, 4};

int main(int argc, const char *argv[])
{
    int loc_v1;
    int loc_v2;
    int loc_array[4];

    static int sloc_v1;
    static int sloc_v2;
    static int sloc_array[4];

    printf("glob_v1:\t\t0X%08x\n", &glob_v1);
    printf("glob_v2:\t\t0X%08x\n", &glob_v2);
    printf("glob_array:\t\t0X%08x\n", glob_array);

    printf("glob_v1_with_value:\t0X%08x\n", &glob_v1_with_value);
    printf("glob_v2_with_value:\t0X%08x\n", &glob_v2_with_value);
    printf("glob_array_with_value:\t0X%08x\n", glob_array_with_value);

    printf("loc_v1:\t\t\t0X%08x\n", &loc_v1);
    printf("loc_v2:\t\t\t0X%08x\n", &loc_v2);
    printf("loc_array:\t\t0X%08x\n", &loc_array);

    printf("sloc_v1:\t\t0X%08x\n", &sloc_v1);
    printf("sloc_v2:\t\t0X%08x\n", &sloc_v2);
    printf("sloc_array:\t\t0X%08x\n", &sloc_array);

    return 0;
}