重温C语言 - 基本特性的实现

来源:互联网 发布:mac版unturned怎么汉化 编辑:程序博客网 时间:2024/05/17 01:00

接着上篇继续对C语言做一些探索,这篇主要分析C语言中一些基本特性的实现。包括全局变量,静态变量,以及他们是否初始化以及链接属性做一些解析

一、全局变量

全局变量有初始化或未初始化之分,初始化了的全局变量保存在data段,未初始化全局变量保存在BSS段, data段和bss段都是程序的数据段。

对于初始化的全局变量,看下面程序:

  1. int global1 = 100;
  2. int main()
  3. {
  4. global1 = 101;
  5. extern int global2;
  6. global2 = 201;
  7. return 0;
  8. }
  9. int global2 = 200;

对比汇编代码来分析:

  1. .file "demo.c"
  2. .globl global1 ;声明全局符号 global1
  3. .data ;位于数据段
  4. .align 4
  5. .type global1, @object
  6. .size global1, 4 ;空间大小字节数
  7. global1: ;符号global1定义
  8. .long 100 ;初始值
  9. .text
  10. .globl main ;声明全局符号main
  11. .type main, @function
  12. main:
  13. pushl %ebp
  14. movl %esp, %ebp
  15. subl $8, %esp
  16. andl $-16, %esp
  17. movl $0, %eax
  18. addl $15, %eax
  19. addl $15, %eax
  20. shrl $4, %eax
  21. sall $4, %eax
  22. subl %eax, %esp
  23. movl $101, global1 ;通过符号global1访问全局变量global1
  24. movl $201, global2 ;通过符号global2访问全局变量global2
  25. movl $0, %eax
  26. leave
  27. ret
  28. .size main, .-main
  29. .globl global2 ;全局符号global2
  30. .data ;位于数据段
  31. .align 4
  32. .type global2, @object
  33. .size global2, 4 ;字节数
  34. global2: ;符号global2定义
  35. .long 200 ;初始值
  36. .section .note.GNU-stack,"",@progbits
  37. .ident "GCC: (GNU) 3.4.4 20050721 (Red Hat 3.4.4-2)"

对于未初始化的全局变量:

  1. int global1;
  2. int main()
  3. {
  4. global1 = 101;
  5. extern int global2;
  6. global2 = 201;
  7. return 0;
  8. }
  9. int global2;

生成汇编代码:

  1. .file "demo.c"
  2. .text ;注意这里没有数据段,并没有为未初始化的全局变量分配空间,而是运行时在内存映像中分配
  3. .globl main
  4. .type main, @function
  5. main:
  6. pushl %ebp
  7. movl %esp, %ebp
  8. subl $8, %esp
  9. andl $-16, %esp
  10. movl $0, %eax
  11. addl $15, %eax
  12. addl $15, %eax
  13. shrl $4, %eax
  14. sall $4, %eax
  15. subl %eax, %esp
  16. movl $101, global1 ;通过符号访问全局变量,这个符号可以在之后,或其他文件中定义
  17. movl $201, global2
  18. movl $0, %eax
  19. leave
  20. ret
  21. .size main, .-main
  22. .comm global1,4,4 ;标明这是个未初始化全局变量,声明空间大小,调入内存时在bss段分配空间
  23. .comm global2,4,4
  24. .section .note.GNU-stack,"",@progbits
  25. .ident "GCC: (GNU) 3.4.4 20050721 (Red Hat 3.4.4-2)"

可以得出结论:全局变量独立于函数存在,所有全局变量都可以通过符号访问(当然这里有链接属性,下文会说到), 并且在运行期,其地址不变。

二、跨编译单元如何链接

看下面这个程序链接出错,找不符号a,print, 但生成汇编代码并没有问题。这是因为编译的时候只是把符号 地址标记记录下来,等到链接的时候会去所有的编译单元中去找符号的定义,如果符号定义了才会变成具体 的地址。如果链接的时候所有符号地址都有定义,那么生成可执行文件。如果有不确定地址的符号,则链接出错。

  1. #include<stdio.h>
  2. int main()
  3. {
  4. extern int a;
  5. print("a = %d\n", a);
  6. return 0;
  7. }

编译出错信息如下:

  1. $ gcc demo.c
  2. /tmp/cc6toKHG.o(.text+0x21): In function `main':
  3. : undefined reference to `a'
  4. /tmp/cc6toKHG.o(.text+0x2b): In function `main':
  5. : undefined reference to `print'
  6. collect2: ld returned 1 exit status

但是可以正确的生成汇编代码:

  1. .file "demo.c"
  2. .section .rodata
  3. .LC0: ;字符串常量通过符号.LC0访问,放在代码段,只读区域
  4. .string "a = %d\n"
  5. .text
  6. .globl main ;全局符号main,编译后会进入符号表
  7. .type main, @function
  8. main:
  9. pushl %ebp
  10. movl %esp, %ebp
  11. subl $8, %esp
  12. andl $-16, %esp
  13. movl $0, %eax
  14. addl $15, %eax
  15. addl $15, %eax
  16. shrl $4, %eax
  17. sall $4, %eax
  18. subl %eax, %esp
  19. subl $8, %esp
  20. pushl a ;通过符号a访问全局变量,这里a并没有定义,所以链接当然出错了
  21. pushl $.LC0
  22. call print ;通过符号print访问全局函数,这里print并没有定义
  23. addl $16, %esp
  24. movl $0, %eax
  25. leave
  26. ret
  27. .size main, .-main
  28. .section .note.GNU-stack,"",@progbits
  29. .ident "GCC: (GNU) 3.4.4 20050721 (Red Hat 3.4.4-2)"

三、static全局变量

这里说下全局变量的链接属性,全局变量的默认是extern的,可以参照第一节对全局变量的描述。全局变量 最终存放在数据段,整个程序的所有文件都能访问,如果加上static则表明值能被当前文件访问。这就是所谓 全局变量的static链接属性。

对于初始化了的static全局变量

  1. static int a = 10;
  2. int main()
  3. {
  4. a = 20;
  5. return 0;
  6. }

对应的汇编代码:

  1. .file "demo.c"
  2. .data ;数据段
  3. .align 4
  4. .type a, @object ;数据段为a分配空间
  5. .size a, 4
  6. a: ;符号a的定义,没有global表明是局部不会进入符号表
  7. .long 10 ;初始值
  8. .text
  9. .globl main
  10. .type main, @function
  11. main:
  12. pushl %ebp
  13. ;.....

对比一下,初始化了的extern全局变量

  1. int a = 10;
  2. int main()
  3. {
  4. a = 20;
  5. return 0;
  6. }

汇编代码:

  1. .file "demo.c"
  2. .globl a ;声明全局符号a,编译后a会进入符号表
  3. .data ;数据段
  4. .align 4
  5. .type a, @object ;数据段为a分配空间
  6. .size a, 4
  7. a: ;符号a 的定义
  8. .long 10 ;初始值
  9. .text
  10. .globl main
  11. .type main, @function
  12. main:
  13. pushl %ebp
  14. ;......

那么对于未初始化的static全局变量呢?

  1. static int a;
  2. int main()
  3. {
  4. a = 20;
  5. return 0;
  6. }

汇编代码

  1. .file "demo.c"
  2. .text ;未初始化全局变量编译后,并不分配空间,而是运行时bss分配
  3. .globl main
  4. .type main, @function
  5. main:
  6. pushl %ebp
  7. movl %esp, %ebp
  8. subl $8, %esp
  9. andl $-16, %esp
  10. movl $0, %eax
  11. addl $15, %eax
  12. addl $15, %eax
  13. shrl $4, %eax
  14. sall $4, %eax
  15. subl %eax, %esp
  16. movl $20, a
  17. movl $0, %eax
  18. leave
  19. ret
  20. .size main, .-main
  21. .local a ;声明a 为局部符号,不进入符号表
  22. .comm a,4,4 ;未初始化符号a,以及所需空间
  23. .section .note.GNU-stack,"",@progbits
  24. .ident "GCC: (GNU) 3.4.4 20050721 (Red Hat 3.4.4-2)"

和extern的未初始化全局变量对比下

  1. int a;
  2. int main()
  3. {
  4. a = 20;
  5. return 0;
  6. }

汇编代码:

  1. .file "demo.c"
  2. .text
  3. .globl main
  4. .type main, @function
  5. main:
  6. pushl %ebp
  7. movl %esp, %ebp
  8. subl $8, %esp
  9. andl $-16, %esp
  10. movl $0, %eax
  11. addl $15, %eax
  12. addl $15, %eax
  13. shrl $4, %eax
  14. sall $4, %eax
  15. subl %eax, %esp
  16. movl $20, a
  17. movl $0, %eax
  18. leave
  19. ret
  20. .size main, .-main
  21. .comm a,4,4 ;未初始全局变量a,空间大小,没有local声明,进入符号表
  22. .section .note.GNU-stack,"",@progbits
  23. .ident "GCC: (GNU) 3.4.4 20050721 (Red Hat 3.4.4-2)"

由此可见,static全局变量和extern全局变量存储方式基本一样,最大区别是extern的会进入符号表,其他编译 单元可以链接, 而statc则是本文件的一个符号,本文件的所有函数都可以访问。

四、static局部变量

static局部变量具备外部变量的生存期,但作用域却和局部变量一样,离开函数就不能访问。 对于初始化的static局部变量

  1. #include<stdio.h>
  2. int fun()
  3. {
  4. static int a = 10;
  5. return (++a);
  6. }
  7. int main()
  8. {
  9. printf("a = %d\n",fun());
  10. printf("a = %d\n",fun());
  11. }

汇编代码:

  1. .file "demo.c"
  2. .data ;数据段
  3. .align 4
  4. .type a.0, @object ;符号a.0在.data 段分配空间
  5. .size a.0, 4
  6. a.0: ;符号a.0定义,没有global不进入符号表
  7. .long 10 ;初始值
  8. .text
  9. .globl fun
  10. .type fun, @function
  11. fun:
  12. pushl %ebp
  13. movl %esp, %ebp
  14. incl a.0 ;通过符号a.0访问static局部变量a,这样只用该函数知道a.0,其他地方通过a访问不到
  15. movl a.0, %eax ;返回值实际上方到寄存器eax,所以函数返回后,可以取到返回值
  16. leave
  17. ret
  18. .size fun, .-fun
  19. .section .rodata
  20. .LC0:
  21. .string "a = %d\n"
  22. .text
  23. .globl main
  24. .type main, @function
  25. main:
  26. ;.....

对于未初始化的全局变量:

  1. #include<stdio.h>
  2. int fun()
  3. {
  4. static int a;
  5. return (++a);
  6. }
  7. int main()
  8. {
  9. printf("a = %d\n",fun());
  10. printf("a = %d\n",fun());
  11. }

汇编代码:

  1. .file "demo.c"
  2. .local a.0 ;声明 a.0local 不进入符号表
  3. .comm a.0,4,4 ;未初始化的符号 a.0,空间大小
  4. .text
  5. .globl fun
  6. .type fun, @function
  7. fun:
  8. pushl %ebp
  9. movl %esp, %ebp
  10. incl a.0 ;通过符号a.0访问static局部变量a,这样只用该函数知道a.0,其他地方通过a访问不到
  11. movl a.0, %eax
  12. leave
  13. ret
  14. .size fun, .-fun
  15. .section .rodata
  16. .LC0:
  17. .string "a = %d\n"
  18. .text
  19. .globl main
  20. .type main, @function
  21. main:
  22. ;......

通过以上说明,实际上还是把static局部变量放在数据段存储(要么怎么可能在程序运行期间地址不变呢), static局部变量的存储和static全局变量的存储基本差不多。唯一的区别是符号名编译器会动点手脚 (这样出了函数就访问不了了),同时候多个函数中定义同名的static局部变量,实际上是不同的 符号名,大家互补干涉了。