Linux下ELF文件类型分为以下几种:
1、可重定位文件,例如SimpleSection.o;
2、可执行文件,例如/bin/bash;
3、共享目标文件,例如/lib/libc.so。
在Linux 可重定位文件 ELF结构一文中,我们已经分析了可重定位文件ELF结构。本文分析可执行文件的ELF结构。
首先附上源代码:
SectionMapping.c
#include <stdlib.h>
int main()
{
while(1)
{
sleep(1000);
}
return 0;
}
使用命令gcc -staticSectionMapping.c -oSectionMapping.elf,静态链接为可执行文件。
接着使用命令readelf -S SectionMapping.elf得到Section Table。如下:
There are 33 section headers, starting at offset 0xc3878:
Section Headers:
[Nr] Name Type AddressOffset
Size EntSizeFlags Link Info Align
[ 0]NULL 0000000000000000 00000000
0000000000000000 00000000000000000 0 0
[ 1] .note.ABI-tag NOTE 0000000000400190 00000190
0000000000000020 0000000000000000 A 0 0 4
[ 2] .note.gnu.build-i NOTE 00000000004001b0 000001b0
0000000000000024 0000000000000000 A 0 0 4
[ 3] .rela.pltRELA 00000000004001d8 000001d8
0000000000000120 0000000000000018 A 0 5 8
[ 4] .init PROGBITS00000000004002f8 000002f8
0000000000000018 0000000000000000 AX 0 0 4
[ 5] .plt PROGBITS0000000000400310 00000310
00000000000000c0 0000000000000000 AX 0 0 16
[ 6] .text PROGBITS00000000004003d0 000003d0
0000000000094988 0000000000000000 AX 0 0 16
[ 7] __libc_thread_fre PROGBITS0000000000494d60 00094d60
00000000000000a8 0000000000000000 AX 0 0 16
[ 8] __libc_freeres_fn PROGBITS0000000000494e10 00094e10
000000000000181c 0000000000000000 AX 0 0 16
[ 9] .fini PROGBITS000000000049662c 0009662c
000000000000000e 0000000000000000 AX 0 0 4
[10] .rodataPROGBITS0000000000496640 00096640
000000000001d344 0000000000000000 A 0 0 32
[11] __libc_thread_sub PROGBITS00000000004b3988 000b3988
0000000000000008 0000000000000000 A 0 0 8
[12] __libc_subfreeres PROGBITS00000000004b3990 000b3990
0000000000000058 0000000000000000 A 0 0 8
[13] __libc_atexit PROGBITS00000000004b39e8 000b39e8
0000000000000008 0000000000000000 A 0 0 8
[14] .eh_framePROGBITS00000000004b39f0 000b39f0
000000000000d4c4 0000000000000000 A 0 0 8
[15] .gcc_except_table PROGBITS00000000004c0eb4 000c0eb4
0000000000000172 0000000000000000 A 0 0 1
[16] .tdataPROGBITS00000000006c1ef0 000c1ef0
0000000000000020 0000000000000000 WAT 0 0 16
[17] .tbss NOBITS00000000006c1f10 000c1f10
0000000000000038 0000000000000000 WAT 0 0 16
[18] .init_array INIT_ARRAY 00000000006c1f10 000c1f10
0000000000000008 0000000000000000 WA 0 0 8
[19] .fini_array FINI_ARRAY 00000000006c1f18 000c1f18
0000000000000008 0000000000000000 WA 0 0 8
[20] .ctorsPROGBITS00000000006c1f20 000c1f20
0000000000000010 0000000000000000 WA 0 0 8
[21] .dtorsPROGBITS00000000006c1f30 000c1f30
0000000000000010 0000000000000000 WA 0 0 8
[22] .jcr PROGBITS00000000006c1f40 000c1f40
0000000000000008 0000000000000000 WA 0 0 8
[23] .data.rel.ro PROGBITS00000000006c1f50 000c1f50
0000000000000080 0000000000000000 WA 0 0 16
[24] .got PROGBITS00000000006c1fd0 000c1fd0
0000000000000010 0000000000000008 WA 0 0 8
[25] .got.pltPROGBITS00000000006c1fe8 000c1fe8
0000000000000078 0000000000000008 WA 0 0 8
[26] .data PROGBITS00000000006c2060 000c2060
0000000000001690 0000000000000000 WA 0 0 32
[27] .bss NOBITS00000000006c3700 000c36f0
0000000000002ba8 0000000000000000 WA 0 0 32
[28] __libc_freeres_pt NOBITS00000000006c62b0 000c36f0
0000000000000048 0000000000000000 WA 0 0 16
[29] .commentPROGBITS0000000000000000 000c36f0
000000000000002a 0000000000000001 MS 0 0 1
[30] .shstrtabSTRTAB0000000000000000 000c371a
000000000000015b 00000000000000000 0 1
[31] .symtabSYMTAB0000000000000000 000c40b8
000000000000c168 000000000000001832 870 8
[32] .strtabSTRTAB0000000000000000 000d0220
0000000000007a26 00000000000000000 0 1
表 1
这个可执行文件共有33个Section。
接着我们使用readelf -hSectionMapping.elf,读取elf可执行文件头部信息。如下图:
图 1
可以对比,Linux 可重定位文件 ELF结构,这里多了program header。
Entry point address:程序的入口地址是0x401058,使用objdump -dSectionMapping.elf | less,可以查看到程序的入口地址是<_start>。如下图:
图 2
Start of program headers:program headers的偏移,由于头文件大小为64,所以program headers紧挨着头文件存放。
Size of program headers:program headers的大小。为56个字节。
Number of section headers:program headers的数量。为6个。
在表1中,第一个section在文件中的偏移是0x190,头文件大小为64 + program header大小为56 * program header数量6 = 400 = 0x190。
然后,我们使用命令readelf -lSectionMapping.elf,我们会得到program header部分。如下图:
图 3
从图中可见,分为6个Segment。注意表1中每个段叫Section。
Offset:这个Segment在文件中偏移。
VirtAddr:这个Segment在虚拟地址的偏移。
FileSiz:在ELF文件中所占的长度。
MemSiz:在进程虚拟空间所占的长度。
我们发现第二个Segment,MemSiz > FileSiz,表示在内存中分配的空间大小超过文件实际大小。超过的部分全部初始化为0,作为BSS段。因为数据段和BSS段的唯一区别是,数据段从文件中初始化内容,BSS段内容全部初始化为0。
我们主要关心前两个Segment,第一个是代码段,虚拟地址从0x00400000到0x004c1026。文件偏移从0x00000000到0x000c1026。
第二个是数据段,虚拟地址为从0x006c1ef0到0x006c1ef0+0x4408=0x6c62f8。文件偏移从0x000c1ef0到0x000c1ef0+0x1800=0x000C36f0。
结合表1和两个Segment的文件偏移,可以得出:
第一个Segment从第0个Section到第15个Section。(0x00000000-0x000c1026)
[Nr] Name Type AddressOffset
Size EntSizeFlags Link Info Align
[ 0]NULL 0000000000000000 00000000
0000000000000000 00000000000000000 0 0
[ 1] .note.ABI-tag NOTE 0000000000400190 00000190
0000000000000020 0000000000000000 A 0 0 4
[ 2] .note.gnu.build-i NOTE 00000000004001b0 000001b0
0000000000000024 0000000000000000 A 0 0 4
[ 3] .rela.pltRELA 00000000004001d8 000001d8
0000000000000120 0000000000000018 A 0 5 8
[ 4] .init PROGBITS00000000004002f8 000002f8
0000000000000018 0000000000000000 AX 0 0 4
[ 5] .plt PROGBITS0000000000400310 00000310
00000000000000c0 0000000000000000 AX 0 0 16
[ 6] .text PROGBITS00000000004003d0 000003d0
0000000000094988 0000000000000000 AX 0 0 16
[ 7] __libc_thread_fre PROGBITS0000000000494d60 00094d60
00000000000000a8 0000000000000000 AX 0 0 16
[ 8] __libc_freeres_fn PROGBITS0000000000494e10 00094e10
000000000000181c 0000000000000000 AX 0 0 16
[ 9] .fini PROGBITS000000000049662c 0009662c
000000000000000e 0000000000000000 AX 0 0 4
[10] .rodataPROGBITS0000000000496640 00096640
000000000001d344 0000000000000000 A 0 0 32
[11] __libc_thread_sub PROGBITS00000000004b3988 000b3988
0000000000000008 0000000000000000 A 0 0 8
[12] __libc_subfreeres PROGBITS00000000004b3990 000b3990
0000000000000058 0000000000000000 A 0 0 8
[13] __libc_atexit PROGBITS00000000004b39e8 000b39e8
0000000000000008 0000000000000000 A 0 0 8
[14] .eh_framePROGBITS00000000004b39f0 000b39f0
000000000000d4c4 0000000000000000 A 0 0 8
[15] .gcc_except_table PROGBITS00000000004c0eb4 000c0eb4
0000000000000172 0000000000000000 A 0 0 1
第二个Segment从第16个Section到26个Section。(0x000c1ef0-0x000C36f0)
[16] .tdataPROGBITS00000000006c1ef0 000c1ef0
0000000000000020 0000000000000000 WAT 0 0 16
[17] .tbss NOBITS00000000006c1f10 000c1f10
0000000000000038 0000000000000000 WAT 0 0 16
[18] .init_array INIT_ARRAY 00000000006c1f10 000c1f10
0000000000000008 0000000000000000 WA 0 0 8
[19] .fini_array FINI_ARRAY 00000000006c1f18 000c1f18
0000000000000008 0000000000000000 WA 0 0 8
[20] .ctorsPROGBITS00000000006c1f20 000c1f20
0000000000000010 0000000000000000 WA 0 0 8
[21] .dtorsPROGBITS00000000006c1f30 000c1f30
0000000000000010 0000000000000000 WA 0 0 8
[22] .jcr PROGBITS00000000006c1f40 000c1f40
0000000000000008 0000000000000000 WA 0 0 8
[23] .data.rel.ro PROGBITS00000000006c1f50 000c1f50
0000000000000080 0000000000000000 WA 0 0 16
[24] .got PROGBITS00000000006c1fd0 000c1fd0
0000000000000010 0000000000000008 WA 0 0 8
[25] .got.pltPROGBITS00000000006c1fe8 000c1fe8
0000000000000078 0000000000000008 WA 0 0 8
[26] .data PROGBITS00000000006c2060 000c2060
0000000000001690 0000000000000000 WA 0 0 32
以上分析的都是静态状态下的程序,下面我们看看动态下的进程的空间是怎么分配的。
首先使用命令,./SectionMapping.elf &,输出如下:
然后使用命令:cat /proc/2184/maps,输出如下:
图 4
静态时,我们计算出的两个Segment的虚拟空间的偏移分别为:
第一个是代码段,虚拟地址从0x00400000到0x004c1026。在图4中,因为要页面对齐,所以分配了0x400000到0x4c2000。
第二个是数据段,虚拟地址为从0x006c1ef0到0x006c1ef0+0x4408=0x6c62f8。在图4中,因为要页面对齐,所以分配了0x6c1000到0x6c4000。注意,0x6c62f8大于0x6c4000,具体原因以后再分析。
第三个紧接着是堆。用于动态分配内存。
第四个是栈。用于存放局部变量。
整体的结构如下图:
程序运行的过程:建立虚拟空间(分配一个页目录)-> 建立虚拟空间与可执行文件映射(页目录项指向磁盘的程序) -> 跳到程序入口 -> 缺页异常-> 在内存中寻找空闲页,将对应的页换入 -> 建立映射 -> 开始执行。
原文:/jltxgcy/article/details/39233689