linux可执行文件格式

时间:2022-12-29 23:31:22

1. 可执行文件的任务:

  1. 可执 行文件的创建:
    • 编译(compile): 源程序文件被编译成目标文件,
    • 连接(link): 多个目标文件 被连接成一个最终的可执行文件,
  2. 可执行文件的运行: 可执行文件被加载(load)到内存中执行。

2. a.out

assembler and link editor output-汇编器和链接编辑器的输出格式(简述)
a.out 是一种古老的文件格式,简单,紧凑, 但其精华犹在,略作研究

标准 a.out 文件包含 7 个 块,1个头部+6个段
格式如下:

a.out 文件格式
exec header(执行头部,也可理解为文件头部)
text segment(文本段)
data segment(数据段)
text relocations(文本重定位段)
data relocations(数据重定位段)
symbol table(符号表)
string table(字符串表)

头部的数据结构:

struct exec {
unsigned long a_midmag; /* 魔数和其它信息 */
unsigned long a_text; /* 文本段的长度 */
unsigned long a_data; /* 数据段的长度 */
unsigned long a_bss; /* BSS段的长度 */
unsigned long a_syms; /* 符号表的长度 */
unsigned long a_entry; /* 程序进入点 */
unsigned long a_trsize; /* 文本重定位表的长度 */
unsigned long a_drsize; /* 数据重定位表的长度 */
};

头部中有标识本文件类型为a.out 的魔数, 程序入口点. 占8byte
还有6个段的长度,24byte, 共32个bytes.
这6个段的长度,是文件体中各个段的长度, 没有string table表长度,因为它在最后,但添加了BSS 段长度.

我们看到, a.out 格式非常紧凑, 头部只记录了段的长度, 没有记录段的地址. 所以
它要求各个段的排序位置是固定的, 各段的开始地址通过前面段长度累加得到.

由a.out 可以看出可执行文件的基本要素:
1. 代码和数据是必要的。
2. 因为文件会引用外部文件定义的符号(变量和函数),因此重定位信息和符号信息也是需要的
3. ascii 字符串, 把它作为一个单独的表放到最后.

举例: hello.c 程序

printf("hello world\n");
printf 就是一个外部符号,
"hello world" 是只读数据,
这个格式的文件太古老了, 已经难以见到它的身影, 就让它过去吧.

3. ELF

(Executable and Linking Format 可执行和链接格式)

网上内容不少,但我要用最简单的语言和例子来描述清楚问题.
elf文件格式由elf头, program 表头+program, setion 表头 + section 來构成,
但一个实际的elf文件可能并不包含以上所有项,其中elf头位置及大小是固定的.
section 体与 program体可以有交叉.

1. elf文件头

这个文件是对elf文件整体信息的描述,在32位系统下是56的字节,在64位系统下是64个字节。

对于可执行文件来说,文件头的完整描述参考后面描述.
文件头包含的以下信息与进程启动相关

e_entry     程序入口地址
e_phoff program表偏移
e_phnum program 数量

2. program 表

表中每一项叫program 头

typedef struct
{
Elf64_Word p_type;
Elf64_Word p_flags; /* 权限: 6表示可读写,5表示可读可执行*/
Elf64_Off p_offset; /* 段在文件中的偏移*/
Elf64_Addr p_vaddr; /* 虚拟内存地址,??*/
Elf64_Addr p_paddr; /* 物理内存地址,??*/
Elf64_Xword p_filesz; /* file size ,段在文件中的长度*/
Elf64_Xword p_memsz; /* memory size 在内存中的长度,一般和p_filesz的值一样*/
Elf64_Xword p_align; /* 段对齐*/

} Elf64_Phdr;

3. 节表及节头

我这里就忽略了, 它们与运行无关,只与链接有关.

4. 汇编级别的elf64 实例,

用汇编,因为生成的文件小

cat t1.s

.section .data
.global data_item
data_item:
.long 0x12345678,0x90abcdef

.section .text
.global _start
_start:
mov $1,%eax
mov $4,%ebx
int $0x80

as t1.s -o t1.o //编译及链接
ld t1.o -o t1
strip t1 //顺便剔除符号信息:
观察其16进制导出文件: xxd t1
就看到其全貌了.

$ xxd t1
0000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
0000010: 0200 3e00 0100 0000 b000 4000 0000 0000 ..>.......@.....
0000020: 4000 0000 0000 0000 e000 0000 0000 0000 @...............
0000030: 0000 0000 4000 3800 0200 4000 0400 0300 ....@.8...@.....
0000040: 0100 0000 0500 0000 0000 0000 0000 0000 ................
0000050: 0000 4000 0000 0000 0000 4000 0000 0000 ..@.......@.....
0000060: bc00 0000 0000 0000 bc00 0000 0000 0000 ................
0000070: 0000 2000 0000 0000 0100 0000 0600 0000 .. .............
0000080: bc00 0000 0000 0000 bc00 6000 0000 0000 ..........`.....
0000090: bc00 6000 0000 0000 0800 0000 0000 0000 ..`
.............
00000a0: 0800 0000 0000 0000 0000 2000 0000 0000 .......... .....
00000b0: b801 0000 00bb 0400 0000 cd80 7856 3412 ............xV4.
00000c0: efcd ab90 002e 7368 7374 7274 6162 002e ......shstrtab..
00000d0: 7465 7874 002e 6461 7461 0000 0000 0000 text..data......
00000e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000100: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000110: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000120: 0b00 0000 0100 0000 0600 0000 0000 0000 ................
0000130: b000 4000 0000 0000 b000 0000 0000 0000 ..@.............
0000140: 0c00 0000 0000 0000 0000 0000 0000 0000 ................
0000150: 0100 0000 0000 0000 0000 0000 0000 0000 ................
0000160: 1100 0000 0100 0000 0300 0000 0000 0000 ................
0000170: bc00 6000 0000 0000 bc00 0000 0000 0000 ..`.............
0000180: 0800 0000 0000 0000 0000 0000 0000 0000 ................
0000190: 0100 0000 0000 0000 0000 0000 0000 0000 ................
00001a0: 0100 0000 0300 0000 0000 0000 0000 0000 ................
00001b0: 0000 0000 0000 0000 c400 0000 0000 0000 ................
00001c0: 1700 0000 0000 0000 0000 0000 0000 0000 ................
00001d0: 0100 0000 0000 0000 0000 0000 0000 0000 ................

/usr/include/elf.h 中有定义

typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ 16 bytes
Elf64_Half e_type; /* Object file type */ uint16 ->0x2 , 可执行
Elf64_Half e_machine; /* Architecture */ uint16 ->0x3e, x86_64
Elf64_Word e_version; /* Object file version */ uint32 ->0x1
Elf64_Addr e_entry; /* Entry point virtual address */ uint64-> 0x4000b0
Elf64_Off e_phoff; /* Program header table file offset */ uint64-> 0x40
Elf64_Off e_shoff; /* Section header table file offset */ uint64-> 0xe0
Elf64_Word e_flags; /* Processor-specific flags */ uint32-> 0
Elf64_Half e_ehsize; /* ELF header size in bytes */ uint16-> 0x40
Elf64_Half e_phentsize; /* Program header table entry size */ uint16-> 0x38
Elf64_Half e_phnum; /* Program header table entry count */ uint16-> 0x2
Elf64_Half e_shentsize; /* Section header table entry size */ uint16-> 0x40
Elf64_Half e_shnum; /* Section header table entry count */ uint16->0x4
Elf64_Half e_shstrndx; /* Section header string table index */ uint16->0x3
} Elf64_Ehdr;

后面的注释是根据二进制文件手工添加的.

5.用工具解读elf:

  1. $ objdump -x t2
t1:     file format elf64-x86-64
t1
architecture: i386:x86-64, flags 0x00000102:
EXEC_P, D_PAGED
start address 0x00000000004000b0

Program Header:
LOAD off 0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**21
filesz 0x00000000000000bc memsz 0x00000000000000bc flags r-x
LOAD off 0x00000000000000bc vaddr 0x00000000006000bc paddr 0x00000000006000bc align 2**21
filesz 0x0000000000000008 memsz 0x0000000000000008 flags rw-

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000000c 00000000004000b0 00000000004000b0 000000b0 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000008 00000000006000bc 00000000006000bc 000000bc 2**0
CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
no symbols

objdump -> elf header显示比较差, section 才显示2个 , 也许它指的是section映射到segment吧

  1. $ readelf -e t1
    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: 0x4000b0
Start of program headers: 64 (bytes into file)
Start of section headers: 224 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 2
Size of section headers: 64 (bytes)
Number of section headers: 4
Section header string table index: 3

Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 00000000004000b0 000000b0
000000000000000c 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 00000000006000bc 000000bc
0000000000000008 0000000000000000 WA 0 0 1
[ 3] .shstrtab STRTAB 0000000000000000 000000c4
0000000000000017 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000bc 0x00000000000000bc R E 200000
LOAD 0x00000000000000bc 0x00000000006000bc 0x00000000006000bc
0x0000000000000008 0x0000000000000008 RW 200000

Section to Segment mapping:
Segment Sections...
00 .text
01 .data

readelf: 内容显示的比较清晰!

6. 下面继续完成手工阅读elf 文件.

  1. 程序头
    typedef struct
{
Elf64_Word p_type; /* Segment type */ 0x01
Elf64_Word p_flags; /* Segment flags */ 0x05
Elf64_Off p_offset; /* Segment file offset */ 0x0
Elf64_Addr p_vaddr; /* Segment virtual address */ 0x400000
Elf64_Addr p_paddr; /* Segment physical address */0x400000
Elf64_Xword p_filesz; /* Segment size in file */ 0xbc
Elf64_Xword p_memsz; /* Segment size in memory */ 0xbc
Elf64_Xword p_align; /* Segment alignment */ 0x200000
} Elf64_Phdr;

每个program header 占用56 byte, 注释上标记了一个, 跟程序输出可以进行对照!
我们看到:
- 1个可读可运行的段,大小0xbc, 位于文件偏移0, 内存地址0x400000处
- 1个可读可写的段,大小8bytes, 位于文件偏移0xbc, 内存地址0x6000bc处

  1. 节头
    typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */ 0x0b, 0x11,0x01
Elf64_Word sh_type; /* Section type */ 0x1,0x1,0x3
Elf64_Xword sh_flags; /* Section flags */ 0x06,0x3,0x0
Elf64_Addr sh_addr; /* Section virtual addr at execution */ 0x4000b0,0x6000bc,0x0
Elf64_Off sh_offset; /* Section file offset */ 0xb0,0xbc,0xc4
Elf64_Xword sh_size; /* Section size in bytes */ 0x0c,0x03,0x17
Elf64_Word sh_link; /* Link to another section */ 0x0,0x0,0x0
Elf64_Word sh_info; /* Additional section information */ 0x0,0x0,0x0
Elf64_Xword sh_addralign; /* Section alignment */ 0x1,0x1,0x1
Elf64_Xword sh_entsize; /* Entry size if section holds table */ 0x0,0x0,0x0
} Elf64_Shdr;

手工分析, 从0xe0开始, 存在包含4个节头的节表.每个节头0x40个byte, 谁说的? elf header 说的.
第一个节表头项闲置不用, 其它三个已经标注在注释里了. 依次可解释4个section header

这个表跟program 没有关系,不影响程序执行
这4个节(实际是3个,第0个为空项)是说:
- 第1个节,名字叫.text, 可分配可运行(0x6),文件位置0xb0,内存地址0x4000b0,大小0xc,
- 第2个节,名字叫.data,可分配可写(0x3),文件位置0xbc,内存地址0x6000bc,大小0x8,
- 第3个节,名字叫.shstrtab, 未分配内存,文件位置0xc4,内存地址给0(默认),大小0x17, 这个是字符串索引节
至此,这个简单的elf 数据已经被分割完毕!

看起来内容还是不少,几个简单的运行指令竟前呼后涌跟随着不少数据, 这是因为可执行的elf 不是给人看得,是给加载器看的,加载器总是按照固定的套路把数据加载到内存中.