程序员的自我修养—ELF文件格式
程序编译的基本流程
相信绝大多树科班的人的第一行代码都是下面这个hello world
程序。当我们用window
下的visual studio
, 还是dev
等集成开发环境(IDE
)。都可以通过一个简单的按钮运行起来(程序没有bug
),那么这个按钮的背后究竟做了什么不为人知的工作,就十分值的我们去了解学习。
#include <stdio.h>
int main()
{
printf("hello world \n");
return 0;
}
Linux
系统是目前开发程序开发的最常用的系统。在linux
中开发过程中一般是使用gcc
来编译上述程序。
$ gcc hello.c
$./a.out
hello world
实际上程序从我们编写出来的代码段到计算机可以执行的二进制程序主要经历四个过程,分别是预处理、编译 、汇编和链接等过程,如下图。
最终通过这整一个流程我们就生能在执行机器上面运行的目标文件。好了,回到这一篇文章的正题。目标文件里面存放的是什么东西。它们又是如何进行组织的?下面就是围绕这些问题进行总结解析。
我们知道在在window
系统下不同的文件对应着不同的文件格式。而Linux
目标文件也有其文件格式–ELF格式,其主要是COFF
(common file formt
)格式的变种。在Linux
中主要的ELF文件格式有如下几种:
ELF 文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件 | 包含代码和数据,可以用来链接成可执行文件或共享文件,静态链接库也可以规则此类 | Linux中.o文件 |
可执行文件 | 可以直接执行的程序 | 如:/bin/bash文件 |
共享目标文件 | 一种是链接成目标文件,另外一种是作为进程印象的一部分 | linux 下的.so 文件 |
核心转储文件 | 进程意外终止,会将一些关键的信息存储到该文件中 | Linux下的core dump 文件 |
总的来说,程序源代码在编译以后主要是分为了两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss 段属于程序数据。bss段只是为未初始的全局变量和局部变量预留位置而已,它并没有内容,因此,不占用空间。基本结构如下:
如上图,ELF文件的开头是一个文件头,其主要包含的信息是文件的类型、入口地址、目标硬件、目标操作系统和一个段表信息等信息,段表就是描述文件中各个段的数组。那为啥要将程序划分成不同的段呢,主要集中在一下几点原因:
- 程序和数据的读写权限不一致,映射到内存后就可以防止指令被无意的修改。
- 指令区和数据区的分离有利于提供数据的局部性。
- 当系统中运行多个程序副本的时候,程序只需要一份,而数据可以有多份。
因此,通过一下命令查看对应的可.o
文件section相关的信息
$ objdump -h hello.o
size hello.o //查看代码各个section 的大小
- 代码段
- 数据段和只读数据段
.data 段只保存全局的静态变量和局部的静态变量。.rodata段存放的是只读数据,如字符串常量或者const变量。
objdump -x -s -d hello.o
- bss 段,存放的是未初始化的全局变量和局部静态变量。
- 其他段
- comment 编译器版本信息
- debug 调试信息
- line 行号表
- strtab 字符串表
- shstrtab 段表
- symtab 符号表
- plt 、got 冬天太链接跳转表和全局入口表
- init fini 程序初始化和总结代码段
因此应用程序也可以在文件中新建一个特殊的段来进行使用。为了满足Linux
系统的硬件内存和IO地址布局,GCC
提供了一个扩展机制。使得程序员可以指定变量或者函数所处的段。
__sttribute__ (section("qin")) int global = 42;
__sttribute__ (section("qin")) void global ()
{
}
- ELF文件头
readelf -h hello.o
-
ELF文件中用到很多字符串,比如段名、变量名等。因为字符串的长度往往不是固定的结构所以表示起来是比较困难。一个比较通用的方法是字符串全部集中在一个表中。使用字符串在表中的偏移表示字符串。strtab:字符串表 。shstrtab:段字符串表。
-
符号表
readelf -s helll.o
- extern “C”, C++为了与C兼容,在符号管理上,C++有一个声明或者定义一个C的符号关键字的用法,但是C语言并不兼容这种用法,因此为了兼容两种语言,C++的宏
__cplusplus
编译会默认使用。
#ifdef __cplusplus
extern "C"
{
#endif
int func();
int var;
#ifdef __cpulsplus
}
#endif
-
强符号和弱符号,在程序编写的过程中,我们经常能碰到符号重复的问题。对于C/C++语言编译默认定义的符号都是强符号。针对强弱符号,编译器会根据一下规则进行处理:
- 不允许强符号被多次定义
- 如果一个符号在某个文件定义为强符号,另外一个文件定义为弱符号。则选择强符号。
- 如果多个定义的都是弱符号,那么选择其中占用空间多的那个。
- 在
gcc
中可以使用一下关键字来定制强弱符号和强弱引用。
__attribute__((weak)) weak = 2; //弱符号
__attribute__((weakref)) void foo(); //弱引用
int main ()
{
if(foo)
foo();
}
这中弱符号和弱引用对于库来说十分有用。比如库中定义的弱符号可以被用户定义的强符号覆盖,从而使得程序可以使用自定义版本库函数。