文档章节

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

谁染霜林醉
 谁染霜林醉
发布于 2014/08/20 22:56
字数 2908
阅读 152
收藏 0

【说明】本文章从本人的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文件的格式就到一段落了。接下来会来分析链接问题。


© 著作权归作者所有

谁染霜林醉
粉丝 3
博文 15
码字总数 18432
作品 0
济南
程序员
私信 提问
程序员的自我修养--链接、装载与库笔记:目标文件里有什么

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/fengbingchun/article/details/88932028 编译器编译源代码后生成的文件叫做目标文件。目标文件从结构上讲,它...

fengbingchun
03/31
0
0
[Linux]vmlinux , vmlinux.bin

vmlinux 是ELF格式的一个kernel file,编译好后一半包含调试信息。 vm表示virtual memory vmlinux.bin 是进行如下操作得来,除二进制内容一无所有,而vmlinx是elf格式的文件里面包含了elf头部...

清水湾2012
2013/09/24
1K
0
GCC编译的背后( 预处理和编译 汇编和链接 )

by falcon 2008-02-22 平时在Linux下写代码,直接用"gcc -o out in.c"就把代码编译好了,但是这后面到底做了什么事情呢?如果学习过编译原理则不难理解,一般高级语言程序编译的过程莫过于:...

AlphaJay
2010/05/18
2.7K
1
Linux ELF文件学习(1)

ELF头文件学习 ELF文件原名Executable and Linking Format,译为“可执行可连接格式”。 ELF规范中把ELF文件宽泛的称为“目标文件”,这与我们平时的理解不同。一般的,我们把编译但没有链接...

hzzhengyx
2017/12/13
0
0
ARM 之一 ELF文件、镜像(Image)文件、可执行文件、对象文件 详解

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 https://blog.csdn.net/ZCShouCSDN/article/details/100048461 ELF 文件规范   ELF(Execu...

ZCShouEXP
08/24
0
0

没有更多内容

加载失败,请刷新页面

加载更多

springboot初探---spring-boot-starter-web究竟干了啥

上一篇已经简单介绍了启动类的部分,这一篇主要讨论一下springboot引入的哪些依赖 我们都知道想用springboot做一个web应用,首先要做的是引入相关依赖,两步操作: 1、添加spring-boot-start...

计算机狼
34分钟前
5
0
基于Rocket.chat搭建内网聊天系统(使用docker,本机不需要安装meteor)

您可能不希望使用标准的Docker命令,而是希望对部署进行更多的自动化管理。这就是使用Docker-compose可能会派上用场的地方。 确保您已安装Docker和Docker-compose并且可以正常运行。 docker...

吴伟祥
36分钟前
6
0
conda 更新源

更新conda 源为阿里源 conda config --add channels http://mirrors.aliyun.com/pypi/simple conda config --set show_channel_urls yes 阿里云: http://mirrors.aliyun.com/pypi/simple/ 豆......

Mr_Tea伯奕
37分钟前
4
0
java 泛型使用

每次写泛型方法都翻下百度,还是自己记录下把。 1、定义一个泛型方法,使用传入参数类型来传递泛型。这种用法在封装json序列化工具类应该会用到。 List<xxx> aa = getList(xxx.class);pr...

朝如青丝暮成雪
41分钟前
6
0
深入了解Java模板引擎Freemarker

前言 常用的Java模板引擎包括:JSP、Freemarker、Thymeleaf、Velocity,从Github上查阅到这几款主流的模板引擎的性能的对比,总体上看,JSP、Freemarker、Thymeleaf、Velocity在性能上差别不...

code-ortaerc
42分钟前
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部