ELF文件装载和符号表解析
通过本文可以学到或者了解什么?
- 1、helloWorld的执行流程;
- 2、ELF文件的概念;
- 3、ELF文件的重要信息分析;
- 4、Linux如何使用readelf工具分析ELF文件的信息;
文章目录
一、什么是ELF文件
ELF(Executable and Linkable Format):可执行可链接文件格式
ELF文件是在linux下可执行的二进制目标程序,这样说可能不是这么好理解,可以换个角度。
那你知道windwos下可执行的文件的是什么类型的吗?
答案:exe
文件
那么Linux下的二进制可执行文件ELF,就相当于是windows下的exe可执行文件。
这下懂了吧。
二、如何得到ELF文件
如果想搞清楚ELF文件怎么来的,就有必要从HelloWorld开始了
(一)深度解析HelloWorld在Linux内核中的运行过程及原理
想彻底搞明白怎么运行的,并不是这么容易的事,你可能要学习几本书加起来的厚度的知识,也许你学到老都不可能学完,本篇不会长篇大论,会以最好理解的方式来进行书写,目的能倒背如流的让大家说出来HelloWorld在系统的内部到底怎么执行的
。
也许你是做Java开发的,也许你是做C#、C++、C、Python、php,也许你是使用go语言做开发的。
但是你真的知道你最初写HelloWorld时的运行原理吗?
如果Java面试官问你,你可能会说:经过编写---编译---解释就可以运行成功了
。
如果你真是想这么回答,请继续往下看,那么这篇文章一定会明白到底是怎么运行的。
不过本片采用C语言的HelloWorld,并且在Linux的系统下进行。
看到C语言不要慌
,请继续看。
开始
例如,我们写了下面这段代码,写完之后,你肯定要运行才能看到效果。
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
你会执行:gcc -o hello hello.c
这个命令来编译C语言程序
zhenghui@pc:~/Desktop/code$ ls
hello.c
zhenghui@pc:~/Desktop/code$
zhenghui@pc:~/Desktop/code$
zhenghui@pc:~/Desktop/code$ gcc -o hello hello.c
zhenghui@pc:~/Desktop/code$ ls
hello hello.c
zhenghui@pc:~/Desktop/code$
最终才能运行
zhenghui@pc:~/Desktop/code$ ./hello
Hello world!
zhenghui@pc:~/Desktop/code$
那么执行:gcc -o hello hello.c
这个命令的背后发生了什么?是不是更让你好奇,在此你可以打开脑洞使劲想,但是就是想不出,哈哈,继续看。
GCC编译器驱动程序读取源程序文件hello.c,并把它翻译成一个可执行目标文件hello。这个翻译过程可分为四个阶段完成,如下图所示。执行这四个阶段的程序(预处理器、编译器、汇编器、链接器)一起构成了编译系统。
下面这是分步编译的步骤:
zhenghui@pc:~/Desktop/code$ ls
hello.c
zhenghui@pc:~/Desktop/code$
zhenghui@pc:~/Desktop/code$ gcc -E hello.c -o hello.i
zhenghui@pc:~/Desktop/code$
zhenghui@pc:~/Desktop/code$ ls
hello.c hello.i
zhenghui@pc:~/Desktop/code$
zhenghui@pc:~/Desktop/code$ gcc -S hello.i -o hello.s
zhenghui@pc:~/Desktop/code$
zhenghui@pc:~/Desktop/code$ ls
hello.c hello.i hello.s
zhenghui@pc:~/Desktop/code$
zhenghui@pc:~/Desktop/code$ gcc -c hello.s -o hello.o
zhenghui@pc:~/Desktop/code$
zhenghui@pc:~/Desktop/code$ ls
hello.c hello.i hello.o hello.s
zhenghui@pc:~/Desktop/code$
zhenghui@pc:~/Desktop/code$ gcc hello.o -o hello
zhenghui@pc:~/Desktop/code$ ls
hello hello.c hello.i hello.o hello.s
zhenghui@pc:~/Desktop/code$
zhenghui@pc:~/Desktop/code$ ./hello
Hello world!
zhenghui@pc:~/Desktop/code$
gcc命令选项总结:
选项 | 含义 |
---|---|
-E(大写) | 只进行预处理 |
-S(大写) | 只进行预处理和编译 |
-c(小写) | 只进行预处理、编译和汇编 |
-o(小写) file | 指定生成的输出文件名为 file |
gcc命令各个选项产生的文件总结:
文件后缀 | 含义 |
---|---|
.c | C 语言文件 |
.i | 预处理后的 C 语言文件 |
.s | 编译后的汇编文件 |
.o | 编译后的目标文件 |
支持巩固:
-
预处理阶段
:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。例如hello.c的第一行#include <stdio.h>
命令告诉预处理器读取系统头文件stdio.h
的内容,并把它直接插入程序文本中。结果就得到了另一个完整的C程序,通常以.i
做为文件扩展名。 -
编译阶段
:编译器(ccl)将文本文件hello.i
翻译成文本文件hello.s
,它包括一个汇编语言程序。该程序包含函数main的定义,如下所示:
00000000000006b0 <main>:
6b0: 55 push %rbp
6b1: 48 89 e5 mov %rsp,%rbp
6b4: 48 8d 3d 99 00 00 00 lea 0x99(%rip),%rdi # 754 <_IO_stdin_used+0x4>
6bb: e8 a0 fe ff ff callq 560 <puts@plt>
6c0: b8 00 00 00 00 mov $0x0,%eax
6c5: 5d pop %rbp
6c6: c3 retq
6c7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
6ce: 00 00
-
汇编阶段
:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标语言程序
的格式,并将结果保存在hello.o中。 -
链接阶段
:C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到最终可执行程序中去。链接器(ld)就负责处理这种合并,最终得到hello文件,它是一个可执行目标文件(或称为可执行文件),可以被加载到内存中,由系统执行。
三、ELF文件格式剖析
(一)ELF文件格式概念介绍
不管在什么平台,什么操作系统下,不管是什么文件,都是有文件结构的,只是文件结构不同罢了。例如下面这个图,就是表达的ELF文件的格式
。
ELF文件是一个二进制文件,一串01
组成的数据,存储在硬盘上面。
从文件的开头到文件的末尾被分成了很多个区域,比较重要的:
首先有一个固定长度的
ELF头
,位于ELF文件的最开始的地方。跟着ELF头后面是
程序头表
(可选),有的文件有,有的文件没有。只有在可执行文件中是存在的,在可重定位文件就没有。
节头表
位于ELF文件的末尾,节头表
存储着很多索引,索引到ELF文件的中间。通过索引,就可以把ELF文件的中间划成一个又一个的区域,称作为
节
。
.text节
:存储可执行文件的代码的部分;.rodata节
:存放该程序只读数据的部分;.data节
:存放已经初始化的全局变量的数据;.bss节
:存放未初始化全局变量数据,其实啥也没有;.symtab节
:符号表;.strtab节
:字符串表。
(二)使用工具查看ELF文件,并进一步分析
1、readelf 命令介绍
在linux中比较方便,提供了readelf
命令工具可以很方便的查看ELF文件的各种信息。
readelf
用法:
用法:readelf <选项> elf文件
readelf
常用选项:
-a
:查看所有
elf文件信息
-h
:查看elf文件头
部信息
-l
:查看elf文件程序头
信息
-S
:查看elf文件节头表
-s
:查看elf文件符号表
信息
其它选项:
-a
:–all 显示全部信息,等价于 -h -l -S -s -r -d -V -A -I
-h
:–file-header 显示elf文件开始的文件头信息.
-l
:–program-headers ;–segments 显示程序头(段头)信息(如果有的话)。
-S
:–section-headers ;–sections 显示节头信息(如果有的话)。
-g
:–section-groups 显示节组信息(如果有的话)。
-t
:–section-details 显示节的详细信息(-S的)。
-s
:–syms ;–symbols 显示符号表段中的项(如果有的话)。
-e
:–headers 显示全部头信息,等价于: -h -l -S
-n
:–notes 显示note段(内核注释)的信息。
-r
:–relocs 显示可重定位段的信息。
-u
:–unwind 显示unwind段信息。当前只支持IA64 ELF的unwind段信息。
-d
:–dynamic 显示动态段的信息。
-V
:–version-info 显示版本段的信息。
-A
:–arch-specific 显示CPU构架信息。
-D
:–use-dynamic 使用动态段中的符号表显示符号,而不是使用符号段。
-x
:–hex-dump= 以16进制方式显示指定段内内容。number指定段表中段的索引,或字符串指定文件中的段名。
-w[liaprmfFsoR]
或者
-debugdump[=line,=info,=abbrev,=pubnames,=aranges, =macro,=frames,=frames-interp,=str,=loc,=Ranges]
显示调试段中指定的内容。
-I
:–histogram 显示符号的时候,显示bucket list长度的柱状图。
-v
:–version 显示readelf的版本信息。
-H
:–help 显示readelf所支持的命令行选项。
-W
:–wide 宽行输出。
由于之前已经通过gcc
命令进行了编译操作,所以已经得到了ELF可执行文件hello
,下面以这个文件为例来查看细节。
zhenghui@pc:~/Desktop/code$ ls
hello hello.c hello.i hello.o hello.s
zhenghui@pc:~/Desktop/code$
2、ELF头解析
前面也简单的说了
ELF头部永远位于ELF文件最开始的地方,长度是固定(长度根据ELF32,ELF64文件格式会有区别)
查看ELF文件头部:
使用readelf -h hello
得到ELF头部信息:
文本也放上:
zhenghui@pc:~/Desktop/code$ readelf -h hello
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: DYN (共享目标文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x580
程序头起点: 64 (bytes into file)
Start of section headers: 6656 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 56 (字节)
Number of program headers: 9
节头大小: 64 (字节)
节头数量: 31
字符串表索引节头: 30
zhenghui@pc:~/Desktop/code$
解析:
Magic
称为魔数,相当于是标识符,标识这个文件是属于什么格式;可以看做为一个暗号,程序在运行的时候,就先看Magic如果暗号对接成功,那么就可以顺利执行。Linux在运行ELF文件时,先去对一下Magic,如果不匹配说明不是可运行的ELF文件,那么就会报错,不会运行成功。(例如:把windows下的exe文件拿到linux下运行,肯定是不行的,虽然exe文件也是二进制的,但是魔数是无法匹配的,所以无法运行)
可以重点关注一下关键的信息:
入口点地址
:它标志着这个ELF可运行的文件的入口(第一条指令的地址)是在哪里。
程序头起点
:标志着头部信息的位置,本身就是ELF头的大小。
通过这两个信息,就可以定位到程序头表
和节头表
。
3、节头表解析
节头表的作用是给出一组索引,将ELF格式的文件划分成一个又一个独立功能的节。
可以执行:readelf -S hello
命令查看ELF文件的节头表信息,查看内容如下:
上表给出了,整个ELF文件被分成了31个节(从0到30号节);
例如:
.text节
:位于第14号节,.text
节是用来存放程序代码的地方;
.data节
:存储已经初始化的变量的值
.strtab节
:存放了ELF文件所有的符号的信息
总结:
文件分为多少个节,每个节(如.text节)在文件中的什么地方
4、程序头表
如果有程序头表,那么程序头表的位置一定是在64之后的,因为在ELF头信息中指出:
程序头起点: 64
我的是64,你们在操作的时候,可能不是这样子。
可以执行:readelf -l hello
命令查看ELF文件的程序头表信息,查看内容如下:
为了方便看,放一下截图:
解析:
例如.inerp节
,因为这个.inerp节
是第01段的内容,则对应的是:
说明:
它的Type是INTERP
是在ELF文件的Offset开始,FileSiz这么多个字节,要被搬到内存中的VirtAddr开始MemSiz这个区域里。
总结:
程序头表
指出文件中的哪一个部分(节),应该搬到内存中的哪个位置(段)去。
5、符号表+字符串表
如果有程序头表,那么程序头表的位置一定是在64之后的,因为在ELF头信息中指出:
程序头起点: 64
我的是64,你们在操作的时候,可能不是这样子。
为了好演示效果,现在打开hello.c文件加上两个全局变量:
#include<stdio.h>
int in_arr[5] = {1,2,3,4,5};
char char_str[50] = "zhenghui";
int main()
{
printf("Hello world!\n");
printf("%s\n",char_str);
}
为了养眼:
重新编译:gcc -o hello hello.c
然后执行:readelf -s hello
命令查看ELF文件的符号表信息,查看内容如下(内容太多,我给出了部分符号表的内容):
zhenghui@pc:~/Desktop/test$ readelf -s hello
Symbol table '.dynsym' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
7: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)
Symbol table '.symtab' contains 70 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
...
49: 0000000000201020 0 NOTYPE WEAK DEFAULT 25 data_start
...
62: 00000000000006b0 35 FUNC GLOBAL DEFAULT 14 main
...
66: 0000000000201060 50 OBJECT GLOBAL DEFAULT 25 char_str
...
69: 0000000000201040 20 OBJECT GLOBAL DEFAULT 25 in_arr
zhenghui@pc:~/Desktop/test$
分析:
可以看到
.symtab
一共70个符号;可以看到
Name
为char_str
的这一列:
Num: Value Size Type Bind Vis Ndx Name 66: 0000000000201060 50 OBJECT GLOBAL DEFAULT 25 char_str
思考一下:为什么
Name
列有char_str
这个名字呢?回过头再来看一下源程序:
可以看到源程序中,定义了两个全局变量,一个为char_str另一个是in_arr。
这下应该就明白了。
char_str
对应的:
Value
就是0000000000201060
,或者说就是这个变量对应的内存的地址;
Size
占用的空间就是50
个字节;
总结:
某个全局变量或函数(如:
main
)对应的内存的哪个地址(地址是Value的值给出的)。