关于C语言BSS段问题以及变量定义的解答

作者:胡是

地址:www.cnblogs.com/whos/archive/2010/10/20/1856274.html


非常感谢作者的这篇文章解决了我的疑问


弱符号与强符号概念

链接过程实质上就是把不同目标文件粘在一起,对不同目标文件中定义或引用的相同名字进行决议resolve和绑定binding。

符号的分类如下:

  • 定义在本目标文件中的全局符号,可以被其它文件引用。
  • 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(External Symbol), 也就是我们前所谓符号引用。
  • 段名,这种符号通常由编译器产生,它的值就是该段的起始地址。
  • 局部符号,这类符号只在当前编译单元内部可见。局部符号对于链接过程没有作用,链接器往往忽略它们。
  • 行号信息,即目标指令与源代码中代码行的对应关系,它是可选的。

链接关心的是各种全局符号。

readelf -s xxx.o

所有 bind 这一列为 GLOBAL 的为 全局符号。

 

 

特殊符号

  • __executable_start   该符号为程序起始地址,不是入口地址,是程序最开始的地址。
  • __etext, _etext, etext   该符号为代码段结束地址
  • _edata edata   该符号为数据段结束地址
  • _end end   该符号为程序结束地址

以上地址均指的是载入后的虚拟地址。

 

符号修饰和符号签名

Name Decoration   Name Mangling

gcc 编译选项 "-fleading-underscore" 或 "-fno-leading-underscore" 可以打开或者关闭在编译时 C 语言符号前加上下划线。

 

C++符号修饰因编译器不同而区别很大。比如下面一段代码:

int func(int);

float func(float);

class C {

    int func(int);

    class C2 {

        int func(int);

    };

};

 

namespace N {

    int func(int);

    class C {

        int func(int);

    };

}

 

 

在gcc下编译,其得到的修饰后的符号名称为:

函数签名                              修饰后符号名

int func(int)                          _Z4funci

float func(float)                    _Z4funcf

int C::func(int)                     _ZN1C4funcEi

int C::C2::func(int)               _ZN1C2C24funcEi

int N::func(int)                     _ZN1N4funcEi

int N::C::func(int)                _ZN1N1C4funcEi

 

binutils 工具集中的c++filt 可以用于解析被修饰过的名称

如: c++filt _ZN1N1C4funcEi 输出为 N::C::func(int)

 

如果是VC编译上面这段代码得到的名称修饰结果为

函数签名                              修饰后符号名

int func(int)                          ?func@@YAHH@Z

float func(float)                    ?func@@YAMM@Z

int C::func(int)                     ?func@C@@AAEHH@Z

int C::C2::func(int)              ?func@C2@C@@AAEHH@Z

int N::func(int)                    ?func@N@@YAHH@Z

int N::C::func(int)               ?func@C@N@@AAEHH@Z

 

微软提供了一个api将修饰后的名称转换为函数签名,UnDecorateSymbolName().

 

在linux平台上, extern “C” 的作用就是让 gcc 编译 C++文件时,对C++函数或变量不采用C++的方式来进行名称修饰。

 

 

 

弱符号与强符号

对于C++来说,弱符号通常来源于未初始化的全局变量。而默认情况下,编译器将函数和初始化了的全局变量作为强符号


(全局变量是前提,能在程序运行前分配内存的才存在强弱符号一说,这样连接器才有可能裁决到底选择哪一个,要是运行时在栈分配内存的变量,在同一作用域内多次定义总是出现错误!)


可以通过gcc的 __attribute__((weak)) 来定义任何一个强符号为弱符号。

不同的目标文件中不能有同名的强符号,否则不能链接在一起。

如果一个符号在某个目标文件中是强符号,在其它文件中都是弱符号,那么该名称在链接时选择强符号。

如果一个符号在所有的目标文件中都是弱符号,则选择占用空间(字节数)最大的一个。

 

相应的有 弱引用与强引用的概念。

可以将一个外部函数申明为弱引用,比如下面的做法:

__attribute__((weakref)) void foo();

int main()

{

    if(foo)  foo();

}

 

多个符号定义类型不一致及其处理

不一致有三种情况:

  • 两个或两个以上强符号类型不一致;
  • 有一个强符号,其他都是弱符号,出现类型不一致;
  • 两个或者两个以上弱符号类型不一致。

第一种情况,在编译的时候会提示多重定义错误,因为多个同名强符号定义本身就是非法的。

后面两种情况需要链接器(ld)来处理。

编译器把未初始化的全局变量作为弱符号处理。比如在某个.o中定义了一个未初始化的全局变量 global_uninit_var。此时用 readelf -s查看该变量会看到:

st_name = "global_uninit_var"

st_value = 4

st_size = 4

st_info = 0x11 STB_GLOBAL STT_OBJECT

st_other = 0

st_shndx = 0xfff2 SHN_COMMON

发现这个变量是一个 SHN_COMMON类型。这里使用的是一种成为 Common Block的机制,是一种事先声明临时使用空间的机制。

如果在另一个.o文件也定义了相同名字的 global_uninit_var 变量,且未初始化,类型为占8个字节的double,则按照common block的链接规则,在最终链接后的输出文件中,global_uninit_var的大小会以输入文件中占用空间最大的那个为准。在上面这个例子中,global_uninit_var最终所占的空间是8个字节。

COMMON类型的链接规则是针对符号都是弱符号的情况,如果其中有个符号是强符号,其他都是弱符号,则最终输出结果中的符号所占空间与强符号相同。如果链接过程中有弱符号大于强符号,那么ld链接器会报如下警告:

ld: warning: alignment 4 of symbol `global' in a.o is smaller than 8 in b.o

 

正是由于未初始化的全局变量(弱符号)其大小在编译某个目标文件时未可知,所以那时无法为其在 .bss 节区分配空间。在链接过程中,任何一个弱符号的最终大小都可以确定了,所以它可以在最终输出文件的bss段为其分配空间。所以,从最终的输出可执行文件来看,未初始化的全局变量是放在 .bss 节区的。

(这里有点问题:

在最终输出文件这一步并没有给bss段分配空间,只是在记录了bss段所需要的大小,也就是说所谓在可执行文件中,bss段并不占据目标文件的任何空间,如果要说有的话,就是存储bss段大小的存储空间)【在c专家编程中pag117段一节对此有详细描述】

 

GCC 可以使用 -fno-common 使得我们可以不以COMMON 块机制处理未初始化的全局变量。这时,该符号就相当于一个强符号。

int global __attribute__((no_common));

 

当然,如果程序员足够小心,在声明全局变量时记住在该加 “extern” 关键字时加上它,很多的弱符号类型不一致问题可避免。

 

 

 

__END__

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值