玩Linux的人应该明白ELF文件是一种文件格式,就好比.txt,.doc等一样,只是这个文件是按照特定信息排列组成,同样在windows上也存在一种格式,它叫PE,老的叫dos。下面我就来看看ELF文件里面到底有什么, 以hello.c为例子。
点击(此处)折叠或打开
-
#include <stdio.h>
-
-
int main (int argc, char *argv[])
-
{
-
printf ("Hello World\n");
-
-
return 0;
-
}
图 1
执行:make hello 生成hello可执行文件(ELF格式).
首先,我们可以通过readelf -h hello,来获取hello这个elf文件的头部信息(该信息放置在hello文件的头部,大小为64字节):
点击(此处)折叠或打开
-
$ readelf -h hello
-
ELF Header:
-
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
-
Class: ELF64
-
Data: 2's complement, little endian
-
Version: 1 (current)
-
OS/ABI: UNIX - System V
-
ABI Version: 0
-
Type: EXEC (Executable file)
-
Machine: Advanced Micro Devices X86-64
-
Version: 0x1
-
Entry point address: 0x400430
-
Start of program headers: 64 (bytes into file)
-
Start of section headers: 6696 (bytes into file)
-
Flags: 0x0
-
Size of this header: 64 (bytes)
-
Size of program headers: 56 (bytes)
-
Number of program headers: 9
-
Size of section headers: 64 (bytes)
-
Number of section headers: 31
-
Section header string table index: 28
图 2
当我们在shell命令行敲击./hello的时候,内核是怎么识别这是一个可执行文件(
elf文件)的呢?其实,在shell进行运行的时候,busybox会将"./hello"当成execve系统函数参数进行传参即:execve("./hello", ["./hello"]) 。当Linux发生系统调用并陷入内核后,Linux内核将这个hello文件的从0开始的前面几个字节与.ELF(
7f 45 4c 46)比较,如果一样,则是elf文件,如果不一样,则与"#!"进行比较,以此来查看是否为需要第三方解释的脚本语言(#!后面跟的是解释器的路径),如#!/bin/sh或者#!/usr/bin/python。
上面提到的7f 45 4c 46就是一个elf文件的magic,一种固定elf标志即文件的识别码,其对应的ascii码是
.ELF, 可以通过readelf -h hello获取到,如图2。
由图2可知,这个ELF文件支持的平台为X86-64,版本是0x1,入口地址是0x400430,段的起始地址是文件偏移6696字节处,Program Header在文件偏移64字节处即elf头信息之后。当前elf header占大小64字节,有9个program header和31个段等等,具体elf header格式,可以参考elf.h头文件。
温馨小提示:这里我们普及一下什么是section。我们常听说的section有代码段(TEXT段),BSS段,数据段(DATA段),只读数据段(RODATA段),HEAP, STACK等。在C语言中,.TEXT段主要用于存放函数对应的机器码的(也就是可执行程序的可运行部分),BSS段主要存放全局未初始化变量的(数据部分),DATA段主要存放全局并且已经初始化过的变量的(注意:初始化成0/NULL的全局变量会被认为是未初始化而放置到BSS段,因为BSS段的数据默认就是0/NULL),RODATA段用于存放const类型的变量,HEAP主要是分配内存使用,而STACK主要是用于函数调用和函数局部变量使用。
在讲ELF各个部分内容之前,我们先看看ELF文件的整体结构:
-
+-----------------------+ hello文件偏移 0bytes
-
| |
-
| ELF Header |
-
+-----------------------+ hello文件偏移 64bytes
-
| |
-
| Program Header |
-
+-----------------------+ hello文件偏移 568bytes (64+56 * 9)
-
| interpreter | (动态解释器位置)
-
+-----------------------+
-
| ....... |
-
+-----------------------+ hello文件偏移 3600bytes
-
| text | (代码段位置)
-
+-----------------------+
-
| ro data | (只读数据段位置)
-
+-----------------------+
-
| ....... | (其它段)
-
+-----------------------+
-
| data | (数据段)
-
+-----------------------+ hello文件偏移 6696bytes
-
| Section Header | (段表位置)
-
+-----------------------+ hello文件尾部 8680 bytes
-
$ ls -l hello
-
-rwxr-xr-x 1 cliu4 users 8680 9月 30 14:31 hello
图 3
如图3,图为hello文件的整个布局图,从图中可以看出,hello可执行文件的大小为8680字节,其中0-64字节存放的是elf文件的头部信息,即elf header(这个结构体可以通过查看elf.h获取),64字节之后存放的是program header,在之后放的是动态解释器(interpreter),而section header则是放到文件末尾的即6696字节处,中间放到程序段即section。而当我们知道了elf头部信息的作用就是指明当前elf文件的版本号,运行环境和其他段的位置,个数以及大小后,那么紧接着elf header的program header到底又有什么用呢?首先我们看看program header里面到底有什么,如下图:
点击(此处)折叠或打开
-
$ readelf -l hello
-
-
Elf file type is EXEC (Executable file)
-
Entry point 0x400430
-
There are 9 program headers, starting at offset 64
-
-
Program Headers:
-
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
-
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 0x00000000000001f8 0x00000000000001f8 RE 8
-
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238 0x000000000000001c 0x000000000000001c R 1
-
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
-
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000 0x0000000000000794 0x0000000000000794 RE 200000
-
LOAD 0x0000000000000e00 0x0000000000600e00 0x0000000000600e00 0x0000000000000238 0x0000000000000240 RW 200000
-
DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28 0x00000000000001d0 0x00000000000001d0 RW 8
-
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 0x0000000000000044 0x0000000000000044 R 4
-
GNU_EH_FRAME 0x0000000000000618 0x0000000000400618 0x0000000000400618 0x0000000000000044 0x0000000000000044 R 4
-
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 10
-
GNU_RELRO 0x0000000000000e00 0x0000000000600e00 0x0000000000600e00 0x0000000000000200 0x0000000000000200 R 1
-
-
Section to Segment mapping:
-
Segment Sections...
-
00
-
01 .interp
-
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
-
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
-
04 .dynamic
-
05 .note.ABI-tag .note.gnu.build-id
-
06 .eh_frame_hdr
-
07
-
08 .init_array .fini_array .jcr .dynamic .got
图 4
如图4,program headers包含:1、PHDR(program header起始位置0x40,大小0x1f8);2、INTERP(动态解释器,其实就是一个字符串,记录了动态解释器的位置:/lib64/ld-linux-x86-64.so.2);3、LOAD;4、LOAD;5、DYNAMIC;6、NOTE等。从第20行"Section to Segment mapping"下面可以看到编号00,01,02,03....08,这些编号对应的是program header对应的表,例如:00对应的是PHDR,01对应的是INTERP,02对应的是LOAD等。因此,可以知道01对应的program header INTERP部分记录的是.interp段信息;而其他的与运行相关的记录在02/03对应的LOAD结构当中。那么这个program header到底有什么用呢?其实,这个program header仔细的朋友应该能够发现每个program header都记录了对应所在的文件偏移和内存的虚拟地址和物理地址,当系统调用execve陷入到内核,并且发现这个hello文件为elf文件的时候,Linux内核就会根据program header当中的提示信息来对hello进行内存映射,给hello开辟自己的虚拟地址空间(如LOAD部分虚拟地址是0x400000即图4标红部分),并且通过INTERP获取到当前elf文件的动态解释器路径
:/lib64/ld-linux-x86-64.so.2,还可以通过DYNAMIC部分获取到当前hello文件依赖的动态库信息,知道哪些动态库需要引用(readelf -d hello)。因此,我可以简单的理解为:program header信息 就是为Linux内核提供应用程序加载相关的信息,方便内核加载可执行程序。
如图3,经过了program header,挨着它的便是动态解释器(.interpreter)的位置了,这个地方比较简单,就是一个字符串,指明了动态解释器的路径。这个动态解释器
/lib64/ld-linux-x86-64.so.2看起来是一个动态库,其实不是,它是一个可用自我重定位的可执行程序,其主要作用就是帮助hello进行代码重定位。
如图3,在动态解释器后的具体段,将在section headers之后描述。
如图3,在文件的末尾,即6696偏移到8680偏移的位置处,存放的是section header。可用通过readelf -S hello获取到。下图是section headers结构的信息:
点击(此处)折叠或打开
-
There are 31 section headers, starting at offset 0x1a28:
-
-
Section Headers:
-
[Nr] Name Type Address Offset Size EntSize Flags Link Info Align
-
[ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0
-
[ 1] .interp PROGBITS 0000000000400238 00000238 000000000000001c 0000000000000000 A 0 0 1
-
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254 0000000000000020 0000000000000000 A 0 0 4
-
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274 0000000000000024 0000000000000000 A 0 0 4
-
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298 000000000000001c 0000000000000000 A 5 0 8
-
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8 0000000000000060 0000000000000018 A 6 1 8
-
[ 6] .dynstr STRTAB 0000000000400318 00000318 000000000000003d 0000000000000000 A 0 0 1
-
[ 7] .gnu.version VERSYM 0000000000400356 00000356 0000000000000008 0000000000000002 A 5 0 2
-
[ 8] .gnu.version_r VERNEED 0000000000400360 00000360 0000000000000020 0000000000000000 A 6 1 8
-
[ 9] .rela.dyn RELA 0000000000400380 00000380 0000000000000018 0000000000000018 A 5 0 8
-
[10] .rela.plt RELA 0000000000400398 00000398 0000000000000030 0000000000000018 AI 5 24 8
-
[11] .init PROGBITS 00000000004003c8 000003c8 000000000000001a 0000000000000000 AX 0 0 4
-
[12] .plt PROGBITS 00000000004003f0 000003f0 0000000000000030 0000000000000010 AX 0 0 16
-
[13] .plt.got PROGBITS 0000000000400420 00000420 0000000000000008 0000000000000000 AX 0 0 8
-
[14] .text PROGBITS 0000000000400430 00000430 00000000000001b2 0000000000000000 AX 0 0 16
-
[15] .fini PROGBITS 00000000004005e4 000005e4 0000000000000009 0000000000000000 AX 0 0 4
-
[16] .rodata PROGBITS 00000000004005f0 000005f0 0000000000000027 0000000000000000 A 0 0 4
-
[17] .eh_frame_hdr PROGBITS 0000000000400618 00000618 0000000000000044 0000000000000000 A 0 0 4
-
[18] .eh_frame PROGBITS 0000000000400660 00000660 0000000000000134 0000000000000000 A 0 0 8
-
[19] .init_array INIT_ARRAY 0000000000600e00 00000e00 0000000000000010 0000000000000000 WA 0 0 8
-
[20] .fini_array FINI_ARRAY 0000000000600e10 00000e10 0000000000000010 0000000000000000 WA 0 0 8
-
[21] .jcr PROGBITS 0000000000600e20 00000e20 0000000000000008 0000000000000000 WA 0 0 8
-
[22] .dynamic DYNAMIC 0000000000600e28 00000e28 00000000000001d0 0000000000000010 WA 6 0 8
-
[23] .got PROGBITS 0000000000600ff8 00000ff8 0000000000000008 0000000000000008 WA 0 0 8
-
[24] .got.plt PROGBITS 0000000000601000 00001000 0000000000000028 0000000000000008 WA 0 0 8
-
[25] .data PROGBITS 0000000000601028 00001028 0000000000000010 0000000000000000 WA 0 0 8
-
[26] .bss NOBITS 0000000000601038 00001038 0000000000000008 0000000000000000 WA 0 0 1
-
[27] .comment PROGBITS 0000000000000000 00001038 0000000000000035 0000000000000001 MS 0 0 1
-
[28] .shstrtab STRTAB 0000000000000000 00001915 000000000000010c 0000000000000000 0 0 1
-
[29] .symtab SYMTAB 0000000000000000 00001070 0000000000000678 0000000000000018 30 49 8
-
[30] .strtab STRTAB 0000000000000000 000016e8 000000000000022d 0000000000000000 0 0 1
图 5
图5是hello文件的section header, 这个信息描述了这个hello文件中到底存在有多少section,这里我将比较重要的section用紫色表示。其中,.interp, .text, .rodata, .data, .bss等几个常用段,这里就不在进行介绍(通常.rodata,.data,和.text是挨着的,是为了节约存储空间,因为.bss不会占用存储空间)。.interp的具体实现将在以后的文章中加以描述。.note开头和.gnu开头的段基本上都是存放与调试相关的信息。
.dynsym和.dynstr: 这两个段为动态符号段和动态字符串符号段,里面存放的是hello程序需要的外部符号信息,用于动态链接。
-
$readelf --dyn-syms hello
-
-
Symbol table '.dynsym' contains 4 entries:
-
Num: Value Size Type Bind Vis Ndx Name
-
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
-
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
-
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
-
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
.rela.dyn和.rela.plt:这两个段是重定位符号表,主要用于指明需要动态解释器(interpreter)帮忙重定位的符号。
-
$readelf -r hello
-
-
Relocation section '.rela.dyn' at offset 0x380 contains 1 entries:
-
Offset Info Type Sym. Value Sym. Name + Addend
-
000000600ff8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
-
-
Relocation section '.rela.plt' at offset 0x398 contains 2 entries:
-
Offset Info Type Sym. Value Sym. Name + Addend
-
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
-
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
注:在Linux当中,并不是所有符号需要在启动的时候,由动态解释器(interpreter)重定位。事实上,为了加快应用程序的启动速度,Linux引入了延迟加载功能,即PLT。所谓的延迟加载是指在启动的过程中,不需要对所有符号进行重定位,只有在需要该符号的时候,才对其进行重定位,通常Linux将函数作为PLT部分。
注:其中R_X86_64_GLOB_DAT 和R_X86_64_JUMP_SLO 为重定向类型,具体参考重定向。
.init: 与应用初始化相关,在进入main函数之前会涉及到,通常位于PLT代码的前面。
-
Disassembly of section .init: 只是一个过渡函数,由__libc_csu_init调用
-
-
00000000004003c8 <_init>:
-
4003c8: 48 83 ec 08 sub $0x8,%rsp
-
4003cc: 48 8b 05 25 0c 20 00 mov 0x200c25(%rip),%rax # 600ff8 <_DYNAMIC+0x1d0>
-
4003d3: 48 85 c0 test %rax,%rax
-
4003d6: 74 05 je 4003dd <_init+0x15>
-
4003d8: e8 43 00 00 00 callq 400420 <__libc_start_main@plt+0x10>
-
4003dd: 48 83 c4 08 add $0x8,%rsp
-
4003e1: c3 retq
.plt和.plt.got: .plt用于存放PLT跳板,如puts@GLIBC_2.2.5的实现(以后讲解具体怎么跳);下图为.plt段和.got.plt段的具体内容,其中.plt段内部是跳板函数的实现(汇编语句),.got.plt则只是数据的实现:
-
Contents of section .got.plt: .got.plt的内容,601020->0x400416, 601018->0x400406(标红部分,为数据,PLT第一次会访问这里,之后这里会被覆盖成真正的函数地址)
601000 280e6000 00000000 00000000 00000000 (.`.............
601010 00000000 00000000 06044000 00000000 ..........@.....
601020 16044000 00000000
-
Disassembly of section .plt:
-
-
00000000004003f0 <puts@plt-0x10>:
-
4003f0: ff 35 12 0c 20 00 pushq 0x200c12(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
-
4003f6: ff 25 14 0c 20 00 jmpq *0x200c14(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
-
4003fc: 0f 1f 40 00 nopl 0x0(%rax)
-
-
0000000000400400 <puts@plt>: 这是一个跳板函数,当第一次调用的时候,GOT+0x8指向400406,然后进行函数重定位 ,第二次调用的时候,指向重定位后的地址
-
400400: ff 25 12 0c 20 00 jmpq *0x200c12(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
-
400406: 68 00 00 00 00 pushq $0x0
-
40040b: e9 e0 ff ff ff jmpq 4003f0 <_init+0x28>
-
-
0000000000400410 <__libc_start_main@plt>:跳板函数:当第一次调用的时候,GOT+0x8指向400416,然后进行函数重定位 ,第二次调用的时候,指向重定位后的地址
-
400410: ff 25 0a 0c 20 00 jmpq *0x200c0a(%rip) # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
-
400416: 68 01 00 00 00 pushq $0x1
-
40041b: e9 d0 ff ff ff jmpq 4003f0 <_init+0x28>
-
-
Disassembly of section .plt.got:
-
-
0000000000400420 <.plt.got>:
-
400420: ff 25 d2 0b 20 00 jmpq *0x200bd2(%rip) # 600ff8 <_DYNAMIC+0x1d0>
-
400426: 66 90 xchg %ax,%ax
如上图,如果我调用puts函数,则是通过callq 400400 来调用的,即首先调用的是puts@plt函数,而这个函数就是在.plt段中实现的跳板函数,由它来完成最后的延迟重定向。
.fini: 与应用退出有关,在main函数退出可能会涉及到,通常为空。
.init_array和.fini_array: 类似于构造函数和析构函数,挂在.init_array段里面的函数,在main函数运行前运行;挂在.fini_array段里面的函数在main函数退出后运行。
.got和.got.plt: 又名全局偏移表,用于存放全局符号地址,其中plt属于一种特殊的GOT表。.got.plt主要用于存放动态函数地址,此表和PLT搭配使用(以后会讲解)。
.symtab和.strtab: hello程序内部能提供的符号表和一些符号信息,这个段通常用于动态库需要,应用程序对此无用。
-
$readelf -s hello
-
-
Symbol table '.dynsym' contains 4 entries:
-
Num: Value Size Type Bind Vis Ndx Name
-
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
-
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
-
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
-
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
-
-
Symbol table '.symtab' contains 69 entries:
-
Num: Value Size Type Bind Vis Ndx Name
-
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
-
1: 0000000000400238 0 SECTION LOCAL DEFAULT 1
-
2: 0000000000400254 0 SECTION LOCAL DEFAULT 2
-
3: 0000000000400274 0 SECTION LOCAL DEFAULT 3
-
4: 0000000000400298 0 SECTION LOCAL DEFAULT 4
-
5: 00000000004002b8 0 SECTION LOCAL DEFAULT 5
-
限于篇幅,此处省略部分符号
-
55: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC
-
56: 0000000000601028 0 NOTYPE GLOBAL DEFAULT 25 __data_start
-
57: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
-
58: 0000000000601030 0 OBJECT GLOBAL HIDDEN 25 __dso_handle
-
59: 00000000004005f0 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
-
60: 0000000000400570 101 FUNC GLOBAL DEFAULT 14 __libc_csu_init
-
61: 0000000000601040 0 NOTYPE GLOBAL DEFAULT 26 _end
-
62: 0000000000400430 42 FUNC GLOBAL DEFAULT 14 _start
-
63: 0000000000601038 0 NOTYPE GLOBAL DEFAULT 26 __bss_start
-
64: 0000000000400548 32 FUNC GLOBAL DEFAULT 14 main
-
65: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses
-
66: 0000000000601038 0 OBJECT GLOBAL HIDDEN 25 __TMC_END__
-
67: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
-
68: 00000000004003c8 0 FUNC GLOBAL DEFAULT 11 _init
-
如上图,符号表可以看出一个符号是否为全局符号,是否为弱符号,是否为局部符号,符号是函数还是变量等等。其中UND表示未定义,需要外部重定位的,GLOBAL为全局,LOCAL为局部,WEAK为弱符号,FUNC为函数,OBJECT为变量,NOTYPE为需要外部重定位的。例如:
64行:
64: 0000000000400548 32 FUNC GLOBAL DEFAULT 14 main
函数名字main函数,全局符号,函数,地址0x400548等。
另外,我们可以通过objdump -s hello获取整个hello文件各个section对应的内容,如下:
通过上图,再结合各个段的具体信息,可以做进一步分析。上图的具体格式为:
Contents of section 段名:
地址 内容1 内容2 内容3 内容4 ASCII字符
地址 内容1 内容2 内容3 内容4 ASCII字符
地址 内容1 内容2 内容3 内容4 ASCII字符
...........................
elf文件的大致粗略信息就是如此,如果有需要可以进行更详细的分析,这篇到这里为止,下一篇,主要分析execve系统调用里面到底完成了什么。
阅读(3774) | 评论(6) | 转发(4) |