从静态链接的角度看global、static与声明

    就像搭积木,链接将不同的软件模块组装成一个完整的可运行的程序。对于连接器来说,软件模块的基本单位是目标文件,来自于某个源文件。a.c要想引用一个外部的函数或者变量,只需进行相应的声明就可以。目标文件以段(section)为单位将程序组织起来。链接大体上可以分为两步:
    1. 同名段合并。典型的合并就是两个.text段(代码段)的合并。
    2. 符号重定位。符号是个很宽泛的概念,目标文件中的符号包括变量名,函数名,段名(如.text, .data),文件名等等。目标文件利用符号表保存所有符号,符号表项含有很多内容,包括名称,值,大小,类型等等。函数符号和变量符号的值就是函数的入口地址或变量的虚拟地址。静态链接最关键的,最核心的操作就是重定位符号的位置,然后根据新的符号位置修正代码中对符号的引用。


    从链接的角度来看,所谓全局变量指那些所有目标文件可视的变量。譬如,源文件a.c声明了一个全局变量global_N,那么利用extern关键字,源文件b.c就可以引用global_N。源文件b.c对global_N的修改可以被a.c察觉,反之亦然。所谓静态变量指那些只能在本目标文件中可视的变量。譬如,源文件a.c和源文件b.c都声明了一个静态变量static_M,那么它们可以引用各自的static_M,互不影响,即使这个静态变量的名字是一样的。如果考虑到变量的存储空间分配,global和static关键字的作用就能更加清晰。由global和static关键字修饰的变量都会在ELF/PE文件中分配存储空间。存储空间分配的细节很啰嗦,因为未初始化的变量会被分配到.bss段,而初始化的变量会被分配到.data段,而且未初始化的变量空间分配问题还牵扯到common段,强弱符号,实在是琐碎!!!无论如何,只看最后可执行ELF/PE的话,由global和static关键字修饰的变量都会在硬盘中被分配空间。而两个.c文件中同名的static变量,比如static_M,在链接过程中会被修饰为不同的符号名,譬如static_M.1和static_M.2。因此,虽然从源码上看,两个.c文件都引用了static_M,但是从ELF/PE上看,它们引用的是不同的符号。总之,global有两个作用,一是将一个变量的可(被)视范围拓展到所有参与链接的目标文件,二是在ELF/PE的数据段为该变量分配存储空间。static有两个作用,一是将一个变量的可(被)视范围限定在本目标文件内部,二是在...(同global)分配存储空间。


    静态链接也解释了函数声明(function declaration)的必要性。如前所述,我们在某一个源文件中引用的函数并不一定在本文件中定义。所谓定义,指的是给出该函数的输入参数,输出参数和函数主体。试想,我们在a.c中使用如下语句引用func:
              result = func(1,2);
而func()的定义在b.c中,那么问题来了: 编译器应该如何传递参数1和2呢?大部分高级程序语言采用压栈方式传递参数,那么为了传递1和2,编译器大体上会将这个语句翻译成两个push和一个call,但是压栈的操作数却取决于常量1和2占用的字节数。大部分情况下,一个int类型的数据占用4字节,而long long类型(如果编译器支持的话)占用8个字节。所以,当面对上述语句的时候,编译器无法判断应该push多少数据到栈里面。函数声明的存在是为了告知编译器一个函数的参数传递细节,这些细节将参与到编译过程中。在Ubuntu 14.04.1下,使用gcc version 4.8.2编译下面两个源文件,得到两个目标文件,通过比较这两个目标文件,可以看到声明是如何影响编译的。汇编中没有显式的使用push,而隐式的利用esp完成压栈操作。
--source1.c-------------------------------------------
int func(long long , long long);
void main()
{
    func(1,2);
}
--source2.c-------------------------------------------
int func(int, int);
void main()
{
    func(1,2);
}
--source1.o中func的调用部分---------------------------
   9:   c7 44 24 08 02 00 00 movl   $0x2,0x8(%esp)
  10: 00 
  11: c7 44 24 0c 00 00 00 movl   $0x0,0xc(%esp)
  18: 00 
  19: c7 04 24 01 00 00 00 movl   $0x1,(%esp)
  20: c7 44 24 04 00 00 00 movl   $0x0,0x4(%esp)
  27: 00 
  28: e8 fc ff ff ff       call   29 <main+0x29>
--source2.o中func的调用部分---------------------------
   9: c7 44 24 04 02 00 00 movl   $0x2,0x4(%esp)
  10: 00 
  11: c7 04 24 01 00 00 00 movl   $0x1,(%esp)
  18: e8 fc ff ff ff       call   19 <main+0x19>
--我是分界线------------------------------------------
从编译结果也可以看到,声明和函数定义的不一致并不会导致编译的失败,甚至不会导致链接的失败,只会导致程序执行时的异常。总之,声明(declaration)真正声明(delcare)的是接口,是函数(function),也就是386手册中所谓的过程(procedure)的接口。这也解释了为什么声明可以只指出输入,输出参数的类型(int func(int, int);),而没必要指明形参名(int func(int a, int b);)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值