程序员的自我修养——3、目标文件里有什么

bss段:

  bss段(bss segment)通常是指用来存放程序中未初始化的全局变量和局部静态变量的一块内存区域。

  bss是英文Block Started by Symbol的简称。

  bss段属于静态内存分配。 

  

data段:

  数据段(data segment)通常是指用来存放程序中已初始化的全局变量和局部静态变量的一块内存区域。

  数据段属于静态内存分配。 

  

text段:

  代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。

  这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架构也允许代码段为可写,即允许修改程序)。

  在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。 

  

堆(heap):

  堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。

  当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);

  当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。

  

栈(stack)

   栈又称堆栈,是用户存放程序临时创建的局部变量,

  也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。

  除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。

  由于栈的先进先出(FIFO)特点,所以栈特别方便用来保存/恢复调用现场。

  从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。 




3.5 链接的接口——符号

链接过程的本质就是要把多个不同的目标文件之间相互“粘”到一起。

在链接中,目标文件之间的相互拼合实际上是目标文件之间对地址的引用。即对函数和变量的地址的引用。


我们将函数和变量统称为符号(Symbol),函数名和变量名就是符号名(Symbol Name)。


每个目标文件都会有一个相应的符号表(Symbol Table)。每个定义的符号有一个对应的值,符号值(Symbol Value)。即变量的地址。


符号类型:

全局符号(global_init_var,main,func1)、外部符号(printf)、段名(.text,.data)、局部符号(static_var)、行号信息。


3.5.1  ELF符号表结构

ELF文件中的符号表往往是文件中的一个段,段名是“.symtab”。符号表的结构如下(32位)

typedef struct {
    Elf32_Word st_name;
    Elf32_Addr st_value;
    Elf32_Word st_size;
    unsigned char st_info;
    unsigned char st_other;
    Elf32_Half st_shndx;
} Elf32_Sym;

成员定义如下表:



符号类型和绑定信息(st_info):该成员低4位表示符号类型,高28位表示符号绑定信息。如下表:


符号所在段(st_shndx):

1、如果符号定义在本目标文件中,那么这个成员表示符号所在的段的段表中的下标。

2、如果符号不是定义在本目标文件中,或者对于有些特殊符号,st_shndx的值有些特殊。



符号值(st_value):

1、在目标文件中,如果符号的定义并且符号不是“COMMON块”类型的(即st_shndx不为SHN_COMMON,具体要根据COMMON块来分析)。

        则表示该符号在段中的偏移

2、在目标文件中,如果是“COMMON块”类型的,

        则表示该符号的对齐属性

3、在可执行文件中,st_value表示符号的虚拟地址。这个虚拟地址对于动态链接器来说十分有用。


3.5.2 特殊符号

这些符号并没有在程序中定义,但是可以直接声明并且引用它,称之为特殊符号

1、__executable_start该符号为程序起始地址注意,不是入口地址,是程序的最开始的地址。

2、__etext 或 _etext 或 etext,该符号为代码段结束地址,即代码段最末尾的地址。

3、_edata 或 edata,该符号为数据段结束地址,即数据段最末尾的地址。

4、_end 或 end,该符号为程序结束地址。

5、以上地址都为程序被装载时的虚拟地址。


3.5.3 符号修饰和函数签名

GCC编译器可以通过参数 “-fleading-underscore” 或者 “-fno-leading-underscore” 来打开和关闭是否在C语言符号前加上下划线。

C++符号修饰(Name Decoration)或符号改编(Name Mangling):

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);
    };
};


针对如上代码,有6个func。我们引入一个术语: 函数签名(Function Signature)

函数签名包含了一个函数的信息,包括它的函数名、它的参数类型、它所在的类、命名空间、以及其他信息

上面6个函数在GCC编译器下,相应的修饰后名称如下:


GCC的基本C++名称修饰方法如下:所有的符号都以"_Z"开头对于嵌套的名字(在名称空间或在类里面的),后面紧跟"N",然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以"E"结尾。比如N::C::func经过名称修饰以后就是_ZN1N1C4funcE。对于一个函数来说,它的参数列表紧跟在"E"后面,对于int类型来说,就是字母"i"。所以整个N::C::func(int)函数签名经过修饰为_ZN1N1C4funcEi。

签名和名称修饰机制不光被使用到函数上, C++中的全局变量和静态变量也有同样的机制。对于全局变量来说,它跟函数一样都是一个全局可见的名称,它也遵循上面的名称修饰机制,比如一个名称空间foo中的全局变量bar,它修饰后的名字为:_ZN3foo3barE。 值得注意的是,变量的类型并没有被加入到修饰后名称中,所以不论这个变量是整形还是浮点型甚至是一个全局对象,它的名称都是一样的。

名称修饰机制也被用来防止静态变量的名字冲突。比如main()函数里面有一个静态变量叫foo,而func()函数里面也有一个静态变量叫foo。为了区分这两个变量,GCC会将它们的符号名分别修饰成两个不同的名字_ZZ4mainE3foo和_ZZ4funcvE3foo,这样就区分了这两个变量。


3.5.4 extern “C”

C++为了与C兼容,在符号管理上,C++有一个用来声明或定义一个C的符号的 extern “C” 关键字用法。

#ifdef __cplusplus
extern "C" {
#endif

...

#ifdef __cplusplus
}
#endif


3.5.5 弱符号和强符号

我们经常在编程中碰到一种情况叫符号重复定义。多个目标文件中含有相同名字全局符号的定义。

对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号(Strong Symbol),未初始化的全局变量为弱符号(Weak Symbol)。


我们可以通过 GCC 的 “__attribute__((weak))” 来定义任何一个强符号为弱符号

强符号和弱符号都是针对定义来说的,不是针对符号的引用

extern int ext;

int weak;
int strong = 1;

__attribute__((weak)) weak2 = 2;

int main()
{
    return 0;
}

针对上面这段程序,weak 和weak2 是弱符号,stong 和 main 是强符号。而 ext 非强符号也非弱符号,它是一个外部变量的引用。


规则如下:

  • 规则1:不允许强符号被多次定义(即不同的目标文件中不能有同名的强符号);如果有多个强符号定义,则链接器报符号重复定义错误。
  • 规则2:如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
  • 规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。比如目标文件A定义全局变量global为int型,占4个字节;目标文件B定义global为double型,占8个字节,那么目标文件A和B链接后,符号global占8个字节(尽量不要使用多个不同类型的弱符号,否则容易导致很难发现的程序错误)。

弱引用和强引用 

目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用(Strong Reference)。与之相对应还有一种弱引用(Weak Reference),在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。


在GCC中,我们可以通过使用"__attribute__((weakref))"这个扩展关键字来声明对一个外部函数的引用为弱引用。

__attribute__ ((weakref)) void foo();

int main()
{
    foo();
}

当main函数试图调用foo函数时,foo函数的地址为0,于是发生了非法地址访问的错误。一个改进的例子是:

__attribute__ ((weakref)) void foo();

int main()
{
    if (foo)
        foo();
}


这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数;或者程序可以对某些扩展功能模块的引用定义为弱引用,当我们将扩展模块与程序链接在一起时,功能模块就可以正常使用;如果我们去掉了某些功能模块,那么程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更加容易裁剪和组合。

在Linux程序的设计中,如果一个程序被设计成可以支持单线程或多线程的模式,就可以通过弱引用的方法来判断当前的程序是链接到了单线程的Glibc库还是多线程的Glibc库(是否在编译时有-lpthread选项),从而执行单线程版本的程序或多线程版本的程序。我们可以在程序中定义一个pthread_create函数的弱引用,然后程序在运行时动态判断是否链接到pthread库从而决定执行多线程版本还是单线程版本:
#include <stdio.h>
#include <pthread.h>

int pthread_create(pthread_t *, const pthread_attr_t *, void* (*)(void*), void*)
__attribute__ ((weak));

int main()
{
    if (pthread_create) {
        printf("This is multi-thread version!\n");
        // ...
    }
    else {
        printf("This is single-thread version!\n");
        // ...
    }
}

执行结果:

$ gcc pthread.c -o pt
$ ./pt
This is single-thread version!
$ gcc pthread.c -lpthread -o pt
$ ./pt
This is multi-thread version!

3.5.6 调试信息

GCC -g 加上调试信息。我们可以通过readelf工具可以看到。当然,我们也可以使用strip去掉调试信息。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值