可执行文件是如何在Linux系统上跑起来的

初学编程时,你是不是很好奇编译好的可执行文件是如何在系统上跑起来的呢?今天就让霞姐带你一起研究一下吧~

一、可执行文件布局

1.ELF格式简介

Linux上可执行文件是ELF(Executable and Linkable Format)格式的。

ELF是 UNIX/Linux 系统的标准二进制文件格式,可用于可执行文件、可链接目标文件(.o),共享库(so)。

ELF首次出现在上世纪90年代初的基于 SVR4 的 Solaris 2.0中;随着处理器的64-bit化,在1997年有了对应的ELF-64 规范;另外因为ELF在Section和Segment的类型上拓展性很大,各大 OS、LLVM等会根据需求拓展功能。

ELF文件的结构如下图所示:

ELF文件内容包括:

• ELF头(ELF Header)

ELF头对所有ELF文件都是必须的。它包含魔数、体系架构、CPU架构、版本、入口点等信息

• 节表(Section header Table)

重定位文件(如编译生成的 .o 文件)必须包含,可加载文件(如可执行文件、共享库)则为可选。

• 程序头表(Program Header Table)

可加载文件中此项必须包含,重定位文件中此项则为可选。

该表用于描述可加载段以及其他数据结构,这些结构是加载程序或动态链接库并为执行做准备时所必需的。

• 节或段的内容

包括可加载数据、重定位信息、字符串表和符号表。

2.可执行文件实例

我们使用readelf和objdump解析一下可执行文件的内容吧!

(1)先用readelf看看头:

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: DYN (Position-Independent Executable file)

Machine: Advanced Micro Devices X86-64

Version: 0x1

Entry point address: 0x1040

Start of program headers: 64 (bytes into file)

Start of section headers: 14016 (bytes into file)

Flags: 0x0

Size of this header: 64 (bytes)

Size of program headers: 56 (bytes)

Number of program headers: 13

Size of section headers: 64 (bytes)

Number of section headers: 29

Section header string table index: 28

Elf Header的各字段告诉了我们这些信息(节选):

字段

字段信息及意义

Magic

7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00前 4 个字节 7f 45 4c 46 是 ELF 文件的标志性 “魔数”(\x7fELF),后续字节包含格式细节:02 表示 64 位文件,01 表示小端字节序等

Class

ELF64该文件是 64 位 ELF 格式(32 位文件会显示 ELF32)。

Data

2's complement, little endian数据存储采用小端字节序(x86/x86_64 架构的默认格式),整数用补码表示。

OS/ABI

UNIX - System V目标操作系统为 UNIX System V 兼容系统(包括 Linux、Solaris 等)。

Type

DYN (Position-Independent Executable file)文件类型是“位置无关可执行文件”(PIE),这类文件与共享库类似,加载时可被映射到内存的任意地址,增强安全性(如对抗缓冲区溢出攻击)。

Machine

Advanced Micro Devices X86-64目标指令集架构是 x86_64(64 位 x86 处理器,如 Intel 酷睿或 AMD Ryzen)。

Entry point address

0x1040程序加载到内存后,CPU 开始执行的起始地址(虚拟地址)为 0x1040。

(2)再用readelf看看程序头表(Program Header Table):

Program Headers:

Type Offset VirtAddr PhysAddr

FileSiz MemSiz Flags Align

PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040

0x00000000000002d8 0x00000000000002d8 R 0x8

INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318

0x000000000000001c 0x000000000000001c R 0x1

[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000

0x00000000000005f0 0x00000000000005f0 R 0x1000

LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000

0x0000000000000175 0x0000000000000175 R E 0x1000

LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000

0x00000000000000d8 0x00000000000000d8 R 0x1000

LOAD 0x0000000000002df0 0x0000000000003df0 0x0000000000003df0

0x0000000000000228 0x0000000000000230 RW 0x1000

DYNAMIC 0x0000000000002e00 0x0000000000003e00 0x0000000000003e00

0x00000000000001c0 0x00000000000001c0 RW 0x8

NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338

0x0000000000000030 0x0000000000000030 R 0x8

NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368

0x0000000000000044 0x0000000000000044 R 0x4

GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338

0x0000000000000030 0x0000000000000030 R 0x8

GNU_EH_FRAME 0x0000000000002004 0x0000000000002004 0x0000000000002004

0x0000000000000034 0x0000000000000034 R 0x4

GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000

0x0000000000000000 0x0000000000000000 RW 0x10

GNU_RELRO 0x0000000000002df0 0x0000000000003df0 0x0000000000003df0

0x0000000000000210 0x0000000000000210 R 0x1

从上述输出可以看出来,程序头表的每一个表项包含以下八项信息:

类型、在文件中的偏移量、加载到内存后的虚拟地址、物理地址、在文件中的大小、加载到内存后的大小、权限、对齐要求。

注意:

a.物理地址大部分情况下不需要关注,只有不支持虚拟内存的场景下需要关注。

b.加载到内存后的大小可能比 FileSiz 大,如.bss 段会动态分配内存;

c.权限:R= 读,W= 写,E= 执行);

下表对上述的程序表头进行了整理:

程序表头类型

说明

PHDR

存储程序头表自身的数据

INTERP

指定动态链接器(程序解释器)的路径,用于加载动态链接的程序。

LOAD

LOAD段是最关键的段类型,OS会将这些段从文件加载到内存,程序运行时的代码和数据都来自这些段。所有 LOAD 段的 Align 都是 0x1000(4096 字节),这是内存页的默认大小,确保段加载到内存页边界。代码段(R E)和数据段(R/RW)严格分离,是操作系统内存保护的基础LOAD 1:文件头+元数据(PHDR、INTERP、NOTE、GNU_PROPERTY)LOAD 2:代码段(.init .plt .text .fini)LOAD 3:只读数据(.rodata、.eh_frame_hdr)LOAD 4:读写数据(.got .data .bss 等)

DYNAMIC

存储动态链接所需的信息,如依赖的共享库列表、重定位表位置、符号表位置等。

NOTE

存放辅助信息,如程序的构建版本、编译器信息、操作系统特定标记等(对程序运行非必需,但工具可能会读取)

GNU_PROPERTY

GNU 扩展的属性信息,如 CPU 特性要求(如是否支持 AVX 指令集)、安全相关标记等。

GNU_EH_FRAME

存放 C++ 异常处理或 C 语言 setjmp/longjmp 所需的框架信息,用于异常发生时定位栈帧和恢复上下文。

GNU_STACK

指定程序栈的权限(RW 表示可读可写,无执行权限),是 Linux 系统的安全机制(防止栈上的代码被执行,对抗缓冲区溢出攻击)。

GNU_RELRO

标记动态链接重定位后应设为只读的区域(如全局偏移表 GOT),防止重定位后的数据被篡改,增强安全性。

(3)最后用objdump捞一个segment看看里面的内容:

段:LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000

0x0000000000000175 0x0000000000000175 R E 0x1000

命令:objdump -d --start-address=0x1000 --stop-address=0x1175 prog

输出:

prog: file format elf64-x86-64

Disassembly of section .init:

0000000000001000 <.init>:

1000: f3 0f 1e fa endbr64

1004: 48 83 ec 08 sub $0x8,%rsp

1008: 48 8b 05 d9 2f 00 00 mov 0x2fd9(%rip),%rax # 3fe8 <__cxa_finalize@plt+0x2fb8>

100f: 48 85 c0 test %rax,%rax

1012: 74 02 je 1016 <__cxa_finalize@plt-0x1a>

1014: ff d0 call *%rax

1016: 48 83 c4 08 add $0x8,%rsp

101a: c3 ret

Disassembly of section .plt:

0000000000001020 <.plt>:

1020: ff 35 a2 2f 00 00 push 0x2fa2(%rip) # 3fc8 <__cxa_finalize@plt+0x2f98>

1026: ff 25 a4 2f 00 00 jmp *0x2fa4(%rip) # 3fd0 <__cxa_finalize@plt+0x2fa0>

102c: 0f 1f 40 00 nopl 0x0(%rax)

二、可执行文件如何加载

当我们在linux的shell上输入可执行文件的名字(比如:aaa),并按回车的时候,接下来发生了什么?

1.shell接收到“./aaa”命令,检查aaa是不是内置的shell命令

2.发现不是,就通过执行execve()函数调用驻留在内存中的加载器(loader)执行

3.加载器将可执行文件中的只读代码段和可读写数据段从硬盘“拷贝”到内存

这时,并没有真正将代码和数据拷贝到内存,而是创建了只读代码段和可读写数据段对应的页表项。在执行代码的过程中发生了缺页异常之后,才真正把它们加载到主存。

4.跳转到由ELF Header的Entry point address指定的可执行文件的第一条指令处执行

比如:本例中,Entry point address是0x1040,0x1040其实是相对于基地址的偏移(相对地址),加上基址(本例中是0x555555554000),那么程序的入口地址是0x555555555040,就是_start函数。

5.上述1~4过程用流程图来表现的话,应该是这样的:

上一篇: 一种蜗牛监测诱捕器.pdf
下一篇: 回顾自然界:不同的植物有不同的“特异功能”,含羞草一碰就收缩

相关推荐

51人品贷电审需要多长时间?迅速理解审批时长,
win10耳机麦克风详细设置教程 win10耳机麦克风用不了怎么办
饬的解释
哪种衣服易褪色?如何防止褪色
【OPSSON(欧博信)手机大全】OPSSON(欧博信)手机报价及图片大全
《逃离塔科夫》ak系列哪个好用?