通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

ELF文件装载和符号表解析

通过本文可以学到或者了解什么?

  • 1、helloWorld的执行流程;
  • 2、ELF文件的概念;
  • 3、ELF文件的重要信息分析;
  • 4、Linux如何使用readelf工具分析ELF文件的信息;

文章目录

一、什么是ELF文件

ELF(Executable and Linkable Format):可执行可链接文件格式

ELF文件是在linux下可执行的二进制目标程序,这样说可能不是这么好理解,可以换个角度。

那你知道windwos下可执行的文件的是什么类型的吗?

答案:exe文件
通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)
通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

那么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。这个翻译过程可分为四个阶段完成,如下图所示。执行这四个阶段的程序(预处理器、编译器、汇编器、链接器)一起构成了编译系统。

通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

下面这是分步编译的步骤:

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$ 

通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

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文件的格式

通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

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$ 

通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

2、ELF头解析

前面也简单的说了ELF头部永远位于ELF文件最开始的地方,长度是固定(长度根据ELF32,ELF64文件格式会有区别)

查看ELF文件头部:

使用readelf -h hello得到ELF头部信息:

通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

文本也放上:

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文件也是二进制的,但是魔数是无法匹配的,所以无法运行)

通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

可以重点关注一下关键的信息:

入口点地址:它标志着这个ELF可运行的文件的入口(第一条指令的地址)是在哪里。

程序头起点:标志着头部信息的位置,本身就是ELF头的大小。

通过这两个信息,就可以定位到程序头表节头表

3、节头表解析

节头表的作用是给出一组索引,将ELF格式的文件划分成一个又一个独立功能的节。

可以执行:readelf -S hello命令查看ELF文件的节头表信息,查看内容如下:
通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

上表给出了,整个ELF文件被分成了31个节(从0到30号节);

例如:

.text节:位于第14号节,.text节是用来存放程序代码的地方;

.data节:存储已经初始化的变量的值

.strtab节:存放了ELF文件所有的符号的信息

总结:

文件分为多少个节,每个节(如.text节)在文件中的什么地方

4、程序头表

如果有程序头表,那么程序头表的位置一定是在64之后的,因为在ELF头信息中指出:

程序头起点: 64

我的是64,你们在操作的时候,可能不是这样子。

可以执行:readelf -l hello命令查看ELF文件的程序头表信息,查看内容如下:

通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

为了方便看,放一下截图:

通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

解析:

通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

例如.inerp节,因为这个.inerp节是第01段的内容,则对应的是:
通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

说明:

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

为了养眼:
通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

重新编译: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个符号;

可以看到Namechar_str的这一列:

Num:             Value          Size    Type        Bind          Vis       Ndx        Name 
66:        0000000000201060      50    OBJECT       GLOBAL      DEFAULT     25       char_str

思考一下:为什么Name有char_str这个名字呢?

回过头再来看一下源程序:

通过Linux的ELF文件解开HelloWorld的三生三世之谜(从此,你的青春不迷茫)

可以看到源程序中,定义了两个全局变量,一个为char_str另一个是in_arr。

这下应该就明白了。

char_str对应的:

Value就是0000000000201060,或者说就是这个变量对应的内存的地址;

Size占用的空间就是50个字节;

总结:

某个全局变量或函数(如:main)对应的内存的哪个地址(地址是Value的值给出的)。

上一篇:C#防止程序多次运行


下一篇:C# 实现程序只启动一次(总结)