重温C语言 - 基本特性的实现
接着上篇继续对C语言做一些探索,这篇主要分析C语言中一些基本特性的实现。包括全局变量,静态变量,以及他们是否初始化以及链接属性做一些解析
##一、全局变量 全局变量有初始化或未初始化之分,初始化了的全局变量保存在data段,未初始化全局变量保存在BSS段, data段和bss段都是程序的数据段。
对于初始化的全局变量,看下面程序:
int global1 = 100;
int main()
{
global1 = 101;
extern int global2;
global2 = 201;
return 0;
}
int global2 = 200;
对比汇编代码来分析:
.file "demo.c"
.globl global1 ;声明全局符号 global1
.data ;位于数据段
.align 4
.type global1, @object
.size global1, 4 ;空间大小字节数
global1: ;符号global1定义
.long 100 ;初始值
.text
.globl main ;声明全局符号main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
subl %eax, %esp
movl $101, global1 ;通过符号global1访问全局变量global1
movl $201, global2 ;通过符号global2访问全局变量global2
movl $0, %eax
leave
ret
.size main, .-main
.globl global2 ;全局符号global2
.data ;位于数据段
.align 4
.type global2, @object
.size global2, 4 ;字节数
global2: ;符号global2定义
.long 200 ;初始值
.section .note.GNU-stack,"",@progbits
.ident "GCC: (GNU) 3.4.4 20050721 (Red Hat 3.4.4-2)"
对于未初始化的全局变量:
int global1;
int main()
{
global1 = 101;
extern int global2;
global2 = 201;
return 0;
}
int global2;
生成汇编代码:
.file "demo.c"
.text ;注意这里没有数据段,并没有为未初始化的全局变量分配空间,而是运行时在内存映像中分配
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
subl %eax, %esp
movl $101, global1 ;通过符号访问全局变量,这个符号可以在之后,或其他文件中定义
movl $201, global2
movl $0, %eax
leave
ret
.size main, .-main
.comm global1,4,4 ;标明这是个未初始化全局变量,声明空间大小,调入内存时在bss段分配空间
.comm global2,4,4
.section .note.GNU-stack,"",@progbits
.ident "GCC: (GNU) 3.4.4 20050721 (Red Hat 3.4.4-2)"
可以得出结论:全局变量独立于函数存在,所有全局变量都可以通过符号访问(当然这里有链接属性,下文会说到), 并且在运行期,其地址不变。
##二、跨编译单元如何链接 看下面这个程序链接出错,找不符号a,print, 但生成汇编代码并没有问题。这是因为编译的时候只是把符号 地址标记记录下来,等到链接的时候会去所有的编译单元中去找符号的定义,如果符号定义了才会变成具体 的地址。如果链接的时候所有符号地址都有定义,那么生成可执行文件。如果有不确定地址的符号,则链接出错。
#include<stdio.h>
int main()
{
extern int a;
print("a = %d\n", a);
return 0;
}
编译出错信息如下:
$ gcc demo.c
/tmp/cc6toKHG.o(.text+0x21): In function `main':
: undefined reference to `a'
/tmp/cc6toKHG.o(.text+0x2b): In function `main':
: undefined reference to `print'
collect2: ld returned 1 exit status
但是可以正确的生成汇编代码:
.file "demo.c"
.section .rodata
.LC0: ;字符串常量通过符号.LC0访问,放在代码段,只读区域
.string "a = %d\n"
.text
.globl main ;全局符号main,编译后会进入符号表
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
subl %eax, %esp
subl $8, %esp
pushl a ;通过符号a访问全局变量,这里a并没有定义,所以链接当然出错了
pushl $.LC0
call print ;通过符号print访问全局函数,这里print并没有定义
addl $16, %esp
movl $0, %eax
leave
ret
.size main, .-main
.section .note.GNU-stack,"",@progbits
.ident "GCC: (GNU) 3.4.4 20050721 (Red Hat 3.4.4-2)"
##三、static全局变量 这里说下全局变量的链接属性,全局变量的默认是extern的,可以参照第一节对全局变量的描述。全局变量 最终存放在数据段,整个程序的所有文件都能访问,如果加上static则表明值能被当前文件访问。这就是所谓 全局变量的static链接属性。
对于初始化了的static全局变量
static int a = 10;
int main()
{
a = 20;
return 0;
}
对应的汇编代码:
.file "demo.c"
.data ;数据段
.align 4
.type a, @object ;数据段为a分配空间
.size a, 4
a: ;符号a的定义,没有global表明是局部不会进入符号表
.long 10 ;初始值
.text
.globl main
.type main, @function
main:
pushl %ebp
;.....
对比一下,初始化了的extern全局变量
int a = 10;
int main()
{
a = 20;
return 0;
}
汇编代码:
.file "demo.c"
.globl a ;声明全局符号a,编译后a会进入符号表
.data ;数据段
.align 4
.type a, @object ;数据段为a分配空间
.size a, 4
a: ;符号a 的定义
.long 10 ;初始值
.text
.globl main
.type main, @function
main:
pushl %ebp
;......
那么对于未初始化的static全局变量呢?
static int a;
int main()
{
a = 20;
return 0;
}
汇编代码
.file "demo.c"
.text ;未初始化全局变量编译后,并不分配空间,而是运行时bss分配
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
subl %eax, %esp
movl $20, a
movl $0, %eax
leave
ret
.size main, .-main
.local a ;声明a 为局部符号,不进入符号表
.comm a,4,4 ;未初始化符号a,以及所需空间
.section .note.GNU-stack,"",@progbits
.ident "GCC: (GNU) 3.4.4 20050721 (Red Hat 3.4.4-2)"
和extern的未初始化全局变量对比下
int a;
int main()
{
a = 20;
return 0;
}
汇编代码:
.file "demo.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
subl %eax, %esp
movl $20, a
movl $0, %eax
leave
ret
.size main, .-main
.comm a,4,4 ;未初始全局变量a,空间大小,没有local声明,进入符号表
.section .note.GNU-stack,"",@progbits
.ident "GCC: (GNU) 3.4.4 20050721 (Red Hat 3.4.4-2)"
由此可见,static全局变量和extern全局变量存储方式基本一样,最大区别是extern的会进入符号表,其他编译 单元可以链接, 而statc则是本文件的一个符号,本文件的所有函数都可以访问。
##四、static局部变量 static局部变量具备外部变量的生存期,但作用域却和局部变量一样,离开函数就不能访问。 对于初始化的static局部变量
#include<stdio.h>
int fun()
{
static int a = 10;
return (++a);
}
int main()
{
printf("a = %d\n",fun());
printf("a = %d\n",fun());
}
汇编代码:
.file "demo.c"
.data ;数据段
.align 4
.type a.0, @object ;符号a.0在.data 段分配空间
.size a.0, 4
a.0: ;符号a.0定义,没有global不进入符号表
.long 10 ;初始值
.text
.globl fun
.type fun, @function
fun:
pushl %ebp
movl %esp, %ebp
incl a.0 ;通过符号a.0访问static局部变量a,这样只用该函数知道a.0,其他地方通过a访问不到
movl a.0, %eax ;返回值实际上方到寄存器eax,所以函数返回后,可以取到返回值
leave
ret
.size fun, .-fun
.section .rodata
.LC0:
.string "a = %d\n"
.text
.globl main
.type main, @function
main:
;.....
对于未初始化的全局变量:
#include<stdio.h>
int fun()
{
static int a;
return (++a);
}
int main()
{
printf("a = %d\n",fun());
printf("a = %d\n",fun());
}
汇编代码:
.file "demo.c"
.local a.0 ;声明 a.0为local 不进入符号表
.comm a.0,4,4 ;未初始化的符号 a.0,空间大小
.text
.globl fun
.type fun, @function
fun:
pushl %ebp
movl %esp, %ebp
incl a.0 ;通过符号a.0访问static局部变量a,这样只用该函数知道a.0,其他地方通过a访问不到
movl a.0, %eax
leave
ret
.size fun, .-fun
.section .rodata
.LC0:
.string "a = %d\n"
.text
.globl main
.type main, @function
main:
;......
通过以上说明,实际上还是把static局部变量放在数据段存储(要么怎么可能在程序运行期间地址不变呢), static局部变量的存储和static全局变量的存储基本差不多。唯一的区别是符号名编译器会动点手脚 (这样出了函数就访问不了了),同时候多个函数中定义同名的static局部变量,实际上是不同的 符号名,大家互补干涉了。
Simple is beautiful博客,转载请注明出处