现在的位置: 首页 > 综合 > 正文

局部变量

2019年03月10日 ⁄ 综合 ⁄ 共 2123字 ⁄ 字号 评论关闭

局部变量

  在接下来的几篇文章中,我将利用"火眼金睛"来分析一个个的C程序,为大家揭开 C 语言的奥秘。

怪题

  有一天,一个朋友发现一段奇怪的 C 程序:

int i = 3;
int ans = (++i)+(++i)+(++i);

  书上说答案是 18,我还以为是 4+5+6=15 呢。

验证

  然后我就想着验证一下结果到底是怎样的,我写了如下的测试程序(inc.c):

#include <stdio.h>

int main()
{
    int i = 3;
    int ans = (++i)+(++i)+(++i);

    printf("%d\n",ans);
    return 0;
}

  在 linux 中编译、运行,结果如下:

[lqy@localhost temp]$ gcc -o inc inc.c
[lqy@localhost temp]$ ./inc
16
[lqy@localhost temp]$ 

  既然又出现了一个令人匪夷所思的 16!

揭秘

  好吧,先不管 18 了,看看这个 16 是怎么来的:

gcc -S -o inc.s inc.c

  生成了汇编源文件 inc.s,现在我们只关心其中 13~20 行:

   .file    "inc.c"
    .section    .rodata
.LC0:
    .string "%d\n"
    .text
.globl main
    .type   main, @function
main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $32, %esp
    movl    $3, 28(%esp)
    addl    $1, 28(%esp)
    addl    $1, 28(%esp)
    movl    28(%esp), %eax
    addl    %eax, %eax
    addl    $1, 28(%esp)
    addl    28(%esp), %eax
    movl    %eax, 24(%esp)
    movl    $.LC0, %eax
    movl    24(%esp), %edx
    movl    %edx, 4(%esp)
    movl    %eax, (%esp)
    call    printf
    movl    $0, %eax
    leave
    ret
    .size   main, .-main
    .ident  "GCC: (GNU) 4.5.1 20100924 (Red Hat 4.5.1-4)"
    .section    .note.GNU-stack,"",@progbits

  现在我我不得不解释一下这种汇编的格式了,它是 AT&T 格式的 x86 汇编,我们在 windows 上见到的一般是 Intel 格式的汇编, AT&T 汇编与 Intel 格式的汇编有些差异,不过还是很好理解的。

  首先是寄存器的命名格式不同,在 Intel 格式的寄存器前加了个 %:eax 变成了 %eax
然后就是双元指令的操作数的传递方向与 Intel 的刚好相反: mov eax,ebx(相当于 eax=ebx;) 变成了 movl %ebx,%eax (左向右传值)
还有其他一些差别,不过还是容易看明白的。

  给 13~20 行加点注释吧:

movl    $3, 28(%esp)    # i = 3;
addl    $1, 28(%esp)    # ++i; // 4
addl    $1, 28(%esp)    # ++i; // 5
movl    28(%esp), %eax  # eax = i;
addl    %eax, %eax      # eax = eax + eax; // 10
addl    $1, 28(%esp)    # ++i; // 6
addl    28(%esp), %eax  # eax = eax + i; // 16
movl    %eax, 24(%esp)  # ans = eax;

  然后,我们就知道了 16 是怎么来的了。

  为什么我就肯定 28(%esp) 就是变量 i 呢?因为只有它被写入了 3,没有别的内存被写入 3,而且从之后操作它的各条指令也可以确定它就是 C 代码中的变量 i(好悲催啊,堂堂一个局部变量变成了无名无姓的相对寄存器寻址的一块内存!)。类似的,局部变量 ans 变成了 24(%esp),可以推理得出:局部变量最后都变成了相对 esp 寻址的内存块(之后的篇章会看到这个推论还不是很正确)。

延伸

  同样的程序,在 VC 上编译运行, Debug 模式的运行结果是 16,Release 模式的运行结果是 18;而在 Visual Studio 2010 中 Debug 和 Release 模式下都是 18。

  VC 和 VS 也可以看反汇编代码,在调试过程中遇到断点中断的时候, VC 使用 Alt + 8 打开反汇编窗体, VS 右击 C 源代码编辑区选择"转到反汇编",打开反汇编窗体。

  为什么 Intel 自己造的 CPU,AT&T 还出一套与 Intel 不同格式的汇编语言呢?没法子,AT&T 也不是好惹的,人家做了两样东西至今影响全世界:Unix、C语言。

小结

  从这个例子中我们应该吸取经验: 被实施递增(递减)操作的变量不应该在表达式中多次出现,否则结果就不受我们控制了,而是被编译器自由发挥:

  C 标准规定:两个序列点之间,程序执行的顺序可以是任意的。这样做给了编译器优化的空间。

  如果想得到结果 15 的话,程序可以改成这样:

#include <stdio.h>

int main()
{
    int i = 3;
    int ans = (i+2)*3;
    i += 3;

    printf("%d\n",ans);
    return 0;
}

  这个程序不论在 windows 下还是在 linux 下,不论是 Release 还是 Debug,结果一定是 15。

  关于 解剖 C 语言,才出了两篇,已经能够为我们解惑了,悟空很有潜力啊!元芳,你怎么看?

抱歉!评论已关闭.