Linux下ELF文件的格式(4)>符号

coding

【说明】本文章从本人的CSDN博客搬过来的,因个人感觉CSDN的博客系统太差,so,搬到这里。

这篇主要说明【符号】这一概念。

那么首先从链接开始说起,源文件到可执行文件一般经过4个步骤,预处理,编译,汇编,链接。作为最后一个阶段,链接到底干什么了?

其实前面也说到了,链接就是将多个目标文件,链接成可执行文件。这样就得考虑一件事,假如我在led.c里定义一个变量led1,而我们要在test.c里使用这个变量。

根据前面几篇我们可以知道,led.c和test.c在汇编这一步,会变成led.o和test.o,并且变量led1的运行地址是不存在的,通常为0 。

而程序运行起来,肯定位于真实的地址上【这里的真实也是虚拟地址】,那么就得靠链接器来确定这些变量的真实地址。

同样,我们的外部函数也是需要确定的,通常这些统称为符号。


每一个目标文件都有一个符号表,这个表中记录了目标文件用到的所有符号,每个符号都有一个对应的值,对于变量和函数来讲,这个值就是他们的地址。

除此之外,还有特殊的符号。总的来讲,分为以下几类:

1.定义在本目标文件中的全局符号,可以被其他模块引用。如:function1,main,global_init等

2.定义在其他文件,被本目标引用的符号,也叫外部符号,如:printf等

3.段名,这是由编译器产生的,如.text  .data  等,它的值就是该段的起始地址。

4.局部符号,如static_init,这类符号只在编译器内部可见。调试器可以用来分析程序或崩溃时的核心转储文件,对链接并无多大作用。

5.行号信息。就是目标文件的指令与源码的对应关系。


对于链接器而言,关心的只是外部的链接的符号,而对内部的符号不感兴趣。所以就是上面提到的1和2类的符号。

我们可以用nm命令查看一下目标文件中的符号:

static的变量后面有个数字,过会讲到它是什么意思。

1.ELF文件符号表结构

从/user/include/elf.h中找到:

这个结构看着比前面的要简单些,下面来看看每个成员的含义:

关于符号类型和绑定信息,st_info。它的低4为表示符号类型,高28位表示绑定信息。

符号类型

    STT_NOTYPE 无类型

    STT_OBJECT 数据对象 ,例如变量 数组等

    STT_FUNC 符号是函数或可执行对象

    STT_SECTION 符号是段,这种符号的类型必须是STB_LOCAL的

    STT_FILE 表示该文件对应的源文件名,类型是STB_LOCAL的并且st_shndx一定是SHN_ABS。


绑定信息

    STB_LOCAL 意为本地符号

    STB_GLOBAL 意为全局符号,可被外部引用

    STB_WEAK 弱引用 稍后讲到


关于st_shndx符号所在段,如果符号在本文件中,那么他表示符号所在段,在段表中的下标。以下几个特殊值:

    SHN_ABS 该符号包含一个绝对的值

    SHN_COMMON 符号是一个common类型的。例如未初始化的全局变量。

    SHN_UNDEF 表示符号在本文件中被定义,但是却定义在其他文件中。


关于st_name,因为存储的是符号的名字,所以会存储在字符串表中。这里仅仅是一个索引值,因为这个结构的大小是固定的,而字符串表的大小可变。

关于st_value,前面说到,如果符号是变量或者函数,则该值是它的地址。

    如果st_shndx不为SHN_COMMON,则这个符号就在st_shndx所指的段中st_value的位置上。

    如果st_shndx为SHN_COMMON,则该值表示符号的对齐属性。

    如果文件是可执行文件,则表示符号的虚拟地址,这个值在链接时是很重要的。


现在看一下真实的符号表:

第一列是数组的索引,

第二列是符号值。

第三列是大小。

第4列,第5列为类型和绑定信息。

第6列c/c++未用。

第7列是st_shndx的值。

第8列就是符号的名字。


首先来说,第一个符号,永远是未定义的符号。

main ,function1的st_shndx的为1,1所指的段是代码段,它的st_value值就是他们在代码段内的偏移。

printf,puts 的st_shndx值是UND,原因是这两个符号是定义在其他文件中的,是外部符号。虽然是可执行的,但是在链接时才会将真正的地址确定。

global_init是全局初始化变量,所以它的st_shndx值为3,在数据段。

global_uninit是未初始化的全局变量,类型是SHN_COMMON,理论上会在bss段,但实际上不保存在bss,最终程序运行时才会分配空间。

static_init,static_un_init,两个值被修饰了,加上了一个数字,稍后讲到符号修饰。它们是编译单元内部可见的。

hello.c 的st_shndx是ABS,表示生成这个目标文件的源文件是hello.c。

还有一些没有名字的,类型是STT_SECTION的,其实他的st_shndx值所对应的段,就是他的名字。例如:第2个,他的st_shndx为1,那么他就是.text段,即代码段。

2.特殊符号

使用ld链接器生成的可执行文件,链接器都会生成一些符号供程序使用。简单看几个。

__executable_start 表示程序的起始地址,不是main入口,整个程序开始的地方。

etext,_etext代码段结束的地址。

edata,_edata数据段结束的地址。

end,_end程序的结束地址。

这里所说的地址都是虚拟地址,而非实际的物理地址。这将在后面的链接中讲到。

看一下输出结果:

3.符号修饰与函数签名

为什么要进行符号修饰 ?

原因很简单,我们的源文件声明的符号中可能与另一个源文件中的声明的符号相同,那么在链接他们的目标文件时,符号就重定义了。

假如项目较大,用到了一些库文件,别人已经用了这个符号,就不能再用了。C语言出现时,使用汇编做成的库时,汇编里用过得符号,C里不能再用,

这是一件很痛苦的事。

所以最初的C为了解决这个问题,将源文件中的全局变量和函数经过编译后,自动在名字前面加上下划线。即如果名为 fun,则编译后为 _fun .

这虽然减小了【撞衫】的几率,但是并不靠谱,奇迹仍会经常出现。后来Linux下的编译器已经取消了这个策略。


之后出现的C++功能强大,十分复杂,面对这个问题,c++用命名空间 (namespace)加以解决,这样函数签名就不同了。

来分析如下:

举个例子,箭头处的函数签名是 _ZN2T12T24funcEi

规则:

1.都以_Z开头,如果处于类或者namespace等中,则加N。

2.类名(其他相同)的长度 + 类名(其他相同)。

3.如有类嵌套,重复步骤2.

4.函数名字的长度 + 函数名 

5.加E表示结束这个命名空间

6.最后如果有参数,加上参数,如 int 就是 i。


注意 :返回值不参与函数签名


下面验证一下刚才那个是否正确,可以使用c++filt 命令 :

这样就不会出现随随便便就重定义了。


同样,全局变量和静态变量也是如此。但是有一些地方值得注意

1.全局变量的类型不参与签名过程。

    假如命名空间 T 下有一个变量 stu,则它的签名是 _ZN1T3stuE,所以不论是什么类型,签名都是一样的,对象也不例外。

2.局部静态变量。

    假如在main函数里有一个静态变量 stu ,在func里有一个静态变量stu ,那么他们是怎样签名的呢 ?假设他们的命名空间都是T。

    main 里面的是 _ZN1T4main3stuE ,func里面的是 _ZN1T4func3stuE


4.extern“C”

    

关于它,都知道这是c++兼容C语言用的。但是实际上是怎样做的的呢 ?

c++的函数和变量都是要签名的,但是c的不用,那么 :

extern "C" {

    int func(int a);

    int var;

}

声明了2个符号,func 和 var ,不加签名。"_"也在现在的linux编译器中默认去掉了。

如果单独声明的话 :

extern "C" int var;

举个例子:

输出结果 :

这说明了C语言的符号,不变化,C++的符号会签名。


在C++ 工程中,包含C语言代码是很正常的事,但是C语言的程序,在经过c++编译之后,必然会按照c++的签名规则来处理C语言的符号。这是不可以的,因为C语言的

符号是不处理的,变量var编译之后还是var。经过C++编译器编译之后肯定无法找到。但是C语言不支持extern "C"这样的语法来告诉编译器自己是C语言。怎么办呢 ?


假设我们有一个函数叫 char* copy(int *src,int *des); 这是我们本来要用的C语言函数,但是c++编译器会将他变成 _Z4copyPiPi。显然已经不是我们需要的了。

所以C++编译器会在编译时生成一个宏,叫__cplusplus .因此,我们可以这样 :

#ifdef __cplusplus

extern "C" {

#endif

char* copy(int *src,int *des);

#ifdef __cplusplus

}

#endif

这样,c++编译器就能将他作为C语言的代码来处理了。

5.弱符号与强符号

譬如int global = 10;就可称之为强符号,int global ;就可称之为弱符号。

当两个目标文件链接在一起时,如果两个目标文件里有相同名字的强符号,就会出现符号重定义错误。


我们可以将任意的强符号,定义成弱符号,比如 __attribute__((weak)) int symbal = 100;这样,symbal就是一个弱符号了。

链接的规则:

1.都是强符号,则会报重定义错误。

2.有强有弱,则会选择强符号。

3.都是弱符号,则会选择占空间大的那一个。


弱引用与强引用

如果引用一个符号,链接时找不到报错,则它是强引用。与之相对 的是弱引用,即使找不到,也不会报错。但是如果执行期间使用这个符号,则会运行期报错。

可以使用 __attribute__((weakref)) void func(int size); 来声明一个弱引用。因为链接时并不报错,所以在运行期会出现错误,因此,当我们使用时,应该先判空,

即if(func) func();


这样,我们就可以在设计库时使用这个思想,库里默认实现一个弱引用的函数,当我们定制时,可以将其声明为强引用,这样就可以覆盖掉库里的弱引用,转而调用我们的

自定义函数,实现自定义的功能。


这样,ELF文件的格式就到一段落了。接下来会来分析链接问题。


以上是 Linux下ELF文件的格式(4)>符号 的全部内容, 来源链接: utcz.com/z/509193.html

回到顶部