百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

为可执行文件“减肥”(一) 可执行的文件为

bigegpt 2024-09-27 00:28 3 浏览

转自《泰晓科技》为可执行文件“减肥”(一)

1. 前言

本文从减少可执行文件大小的角度分析了 ELF 文件,期间通过经典的 ”Hello World” 实例逐步演示如何通过各种常用工具来分析 ELF 文件,并逐步精简代码。由于行文较长,本公众号将用一个系列四个篇幅呈现给泰晓的读者。

为了能够尽量减少可执行文件的大小,我们必须了解可执行文件的格式,以及链接生成可执行文件时的后台细节(即最终到底有哪些内容被链接到了目标代码中)。通过选择合适的可执行文件格式并剔除对可执行文件的最终运行没有影响的内容,就可以实现目标代码的裁减。因此,通过探索减少可执行文件大小的方法,就相当于实践性地去探索了可执行文件的格式以及链接过程的细节。

当然,算法的优化和编程语言的选择可能对目标文件的大小有很大的影响,在本文最后我们会探求一个打印 “Hello World” 的可执行文件能够小到什么样的地步。

2. 可执行文件格式的选取

可执行文件格式的选择要满足的一个基本条件是:目标系统支持该可执行文件格式,UNIX 平台下有三种可执行文件格式,这三种格式实际上代表着可执行文件的一个发展过程:

  • a.out
  • 非常紧凑,只包含了程序运行所必须的信息(文本、数据、BSS),而且每个 section 的顺序是固定的。
  • coff
  • 虽然引入了一个节区表以支持更多节区信息,从而提高了可扩展性,但是这种文件格式的重定位在链接时就已经完成,因此不支持动态链接(不过扩展的coff支持)。
  • elf
  • 不仅支持动态链接,而且有很好的扩展性。它可以描述可重定位文件、可执行文件和可共享文件(动态链接库)三类文件。

下面来看看 ELF 文件的结构图:

1文件头部(ELF Header)
2程序头部表(Program Header Table)
3节区1(Section1)
4节区2(Section2)
5节区3(Section3)
6...
7节区头部(Section Header Table)

无论是文件头部、程序头部表、节区头部表还是各个节区,都是通过特定的结构体(struct) 描述的,这些结构在 elf.h 文件中定义。文件头部用于描述整个文件的类型、大小、运行平台、程序入口、程序头部表和节区头部表等信息。例如,我们可以通过文件头部查看该 ELF 文件的类型。

 1$ cat hello.c #典型的hello, world程序
 2#include <stdio.h>
 3
 4int main(void)
 5{
 6 printf("hello, world!n");
 7 return 0;
 8}
 9$ gcc -c hello.c #编译,产生可重定向的目标代码
10$ readelf -h hello.o | grep Type #通过readelf查看文件头部找出该类型
11 Type: REL (Relocatable file)
12$ gcc -o hello hello.o #生成可执行文件
13$ readelf -h hello | grep Type
14 Type: EXEC (Executable file)
15$ gcc -fpic -shared -W1,-soname,libhello.so.0 -o libhello.so.0.0 hello.o #生成共享库
16$ readelf -h libhello.so.0.0 | grep Type
17 Type: DYN (Shared object file)

那节区头部表(将简称节区表)和程序头部表有什么用呢?实际上前者只对可重定向文件有用,而后者只对可执行文件和可共享文件有用。

节区表是用来描述各节区的,包括各节区的名字、大小、类型、虚拟内存中的位置、相对文件头的位置等,这样所有节区都通过节区表给描述了,这样连接器就可以根据文件头部表和节区表的描述信息对各种输入的可重定位文件进行合适的链接,包括节区的合并与重组、符号的重定位(确认符号在虚拟内存中的地址)等,把各个可重定向输入文件链接成一个可执行文件(或者是可共享文件)。如果可执行文件中使用了动态连接库,那么将包含一些用于动态符号链接的节区。我们可以通过 readelf -S(或objdump -h)查看节区表信息。

 1$ readelf -S hello #可执行文件、可共享库、可重定位文件默认都生成有节区表
 2...
 3Section Headers:
 4 [Nr] Name Type Addr Off Size ES Flg Lk Inf Al
 5 [ 0] NULL 00000000 000000 000000 00 0 0 0
 6 [ 1] .interp PROGBITS 08048114 000114 000013 00 A 0 0 1
 7 [ 2] .note.ABI-tag NOTE 08048128 000128 000020 00 A 0 0 4
 8 [ 3] .hash HASH 08048148 000148 000028 04 A 5 0 4
 9...
10 [ 7] .gnu.version VERSYM 0804822a 00022a 00000a 02 A 5 0 2
11...
12 [11] .init PROGBITS 08048274 000274 000030 00 AX 0 0 4
13...
14 [13] .text PROGBITS 080482f0 0002f0 000148 00 AX 0 0 16
15 [14] .fini PROGBITS 08048438 000438 00001c 00 AX 0 0 4
16...

三种类型文件的节区可能不一样,但是有几个节区,例如 .text, .data, .bss 是必须的,特别是 .text,因为这个节区包含了代码。如果一个程序使用了动态链接库(引用了动态连接库中的某个函数),那么需要 .interp 节区以便告知系统使用什么动态连接器程序来进行动态符号链接,进行某些符号地址的重定位。通常,.rel.text 节区只有可重定向文件有,用于链接时对代码区进行重定向,而 .hash, .plt, .got 等节区则只有可执行文件(或可共享库)有,这些节区对程序的运行特别重要。还有一些节区,可能仅仅是用于注释,比如 .comment,这些对程序的运行似乎没有影响,是可有可无的,不过有些节区虽然对程序的运行没有用处,但是却可以用来辅助对程序进行调试或者对程序运行效率有影响。

虽然三类文件都必须包含某些节区,但是节区表对可重定位文件来说才是必须的,而程序的执行却不需要节区表,只需要程序头部表以便知道如何加载和执行文件。不过如果需要对可执行文件或者动态连接库进行调试,那么节区表却是必要的,否则调试器将不知道如何工作。下面来介绍程序头部表,它可通过 readelf -l(或 objdump -p)查看。

 1$ readelf -l hello.o #对于可重定向文件,gcc没有产生程序头部,因为它对可重定向文件没用
 2
 3There are no program headers in this file.
 4$ readelf -l hello #而可执行文件和可共享文件都有程序头部
 5...
 6Program Headers:
 7 Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
 8 PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
 9 INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1
10 [Requesting program interpreter: /lib/ld-linux.so.2]
11 LOAD 0x000000 0x08048000 0x08048000 0x00470 0x00470 R E 0x1000
12 LOAD 0x000470 0x08049470 0x08049470 0x0010c 0x00110 RW 0x1000
13 DYNAMIC 0x000484 0x08049484 0x08049484 0x000d0 0x000d0 RW 0x4
14 NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4
15 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
16
17 Section to Segment mapping:
18 Segment Sections...
19 00
20 01 .interp
21 02 .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
22 03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
23 04 .dynamic
24 05 .note.ABI-tag
25 06
26$ readelf -l libhello.so.0.0 #节区和上面类似,这里省略

从上面可看出程序头部表描述了一些段(Segment),这些段对应着一个或者多个节区,上面的 readelf -l 很好地显示了各个段与节区的映射。这些段描述了段的名字、类型、大小、第一个字节在文件中的位置、将占用的虚拟内存大小、在虚拟内存中的位置等。这样系统程序解释器将知道如何把可执行文件加载到内存中以及进行动态链接等动作。

该可执行文件包含7个段,PHDR 指程序头部,INTERP 正好对应 .interp 节区,两个 LOAD 段包含程序的代码和数据部分,分别包含有 .text 和 .data,.bss 节区,DYNAMIC 段包含 .daynamic,这个节区可能包含动态连接库的搜索路径、可重定位表的地址等信息,它们用于动态连接器。NOTE 和 GNU_STACK 段貌似作用不大,只是保存了一些辅助信息。因此,对于一个不使用动态连接库的程序来说,可能只包含 LOAD 段,如果一个程序没有数据,那么只有一个 LOAD 段就可以了。

总结一下,Linux 虽然支持很多种可执行文件格式,但是目前 ELF 较通用,所以选择 ELF 作为我们的讨论对象。通过上面对 ELF 文件分析发现一个可执行的文件可能包含一些对它的运行没用的信息,比如节区表、一些用于调试、注释的节区。如果能够删除这些信息就可以减少可执行文件的大小,而且不会影响可执行文件的正常运行。

3. 链接优化

从上面的讨论中已经接触了动态连接库。ELF 中引入动态连接库后极大地方便了公共函数的共享,节约了磁盘和内存空间,因为不再需要把那些公共函数的代码链接到可执行文件,这将减少了可执行文件的大小。

与此同时,静态链接可能会引入一些对代码的运行可能并非必须的内容。你可以从《GCC编译的背后(第二部分:汇编和链接)》 了解到 GCC 链接的细节。从那篇 Blog中似乎可以得出这样的结论:仅仅从是否影响一个 C 语言程序运行的角度上说,GCC默认链接到可执行文件的几个可重定位文件(crt1.o, rti.o, crtbegin.o, crtend.o, crtn.o)并不是必须的,不过值得注意的是,如果没有链接那些文件但在程序末尾使用了 return 语句,main 函数将无法返回,因此需要替换为 _exit 调用;另外,既然程序在进入 main 之前有一个入口,那么 main 入口就不是必须的。因此,如果不采用默认链接也可以减少可执行文件的大小。

相关推荐

C#.NET Autofac 详解(c# autoit)

简介Autofac是一个成熟的、功能丰富的.NET依赖注入(DI)容器。相比于内置容器,它额外提供:模块化注册、装饰器(Decorator)、拦截器(Interceptor)、强o的属性/方法注...

webapi 全流程(webapi怎么部署)

C#中的WebAPIMinimalApi没有控制器,普通api有控制器,MinimalApi是直达型,精简了很多中间代码,广泛适用于微服务架构MinimalApi一切都在组控制台应用程序类【Progr...

.NET外挂系列:3. 了解 harmony 中灵活的纯手工注入方式

一:背景1.讲故事上一篇我们讲到了注解特性,harmony在内部提供了20个HarmonyPatch重载方法尽可能的让大家满足业务开发,那时候我也说了,特性虽然简单粗暴,但只能解决95%...

C# 使用SemanticKernel调用本地大模型deepseek

一、先使用ollama部署好deepseek大模型。具体部署请看前面的头条使用ollama进行本地化部署deepseek大模型二、创建一个空的控制台dotnetnewconsole//添加依赖...

C#.NET 中间件详解(.net core中间件use和run)

简介中间件(Middleware)是ASP.NETCore的核心组件,用于处理HTTP请求和响应的管道机制。它是基于管道模型的轻量级、模块化设计,允许开发者在请求处理过程中插入自定义逻辑。...

IoC 自动注入:让依赖注册不再重复劳动

在ASP.NETCore中,IoC(控制反转)功能通过依赖注入(DI)实现。ASP.NETCore有一个内置的依赖注入容器,可以自动完成依赖注入。我们可以结合反射、特性或程序集扫描来实现自动...

C#.NET 依赖注入详解(c#依赖注入的三种方式)

简介在C#.NET中,依赖注入(DependencyInjection,简称DI)是一种设计模式,用于实现控制反转(InversionofControl,IoC),以降低代码耦合、提高可...

C#从零开始实现一个特性的自动注入功能

在现代软件开发中,依赖注入(DependencyInjection,DI)是实现松耦合、模块化和可测试代码的一个重要实践。C#提供了优秀的DI容器,如ASP.NETCore中自带的Micr...

C#.NET 仓储模式详解(c#仓库货物管理系统)

简介仓储模式(RepositoryPattern)是一种数据访问抽象模式,它在领域模型和数据访问层之间创建了一个隔离层,使得领域模型无需直接与数据访问逻辑交互。仓储模式的核心思想是将数据访问逻辑封装...

C#.NET 泛型详解(c# 泛型 滥用)

简介泛型(Generics)是指在类型或方法定义时使用类型参数,以实现类型安全、可重用和高性能的数据结构与算法为什么需要泛型类型安全防止“装箱/拆箱”带来的性能损耗,并在编译时检测类型错误。可重用同一...

数据分析-相关性分析(相关性 分析)

相关性分析是一种统计方法,用于衡量两个或多个变量之间的关系强度和方向。它通过计算相关系数来量化变量间的线性关系,从而帮助理解变量之间的相互影响。相关性分析常用于数据探索和假设检验,是数据分析和统计建模...

geom_smooth()函数-R语言ggplot2快速入门18

在每节,先运行以下这几行程序。library(ggplot2)library(ggpubr)library(ggtext)#用于个性化图表library(dplyr)#用于数据处理p...

规范申报易错要素解析(规范申报易错要素解析)

为什么要规范申报?规范申报是以满足海关监管、征税、统计等工作为目的,纳税义务人及其代理人依法向海关如实申报的行为,也是海关审接单环节依法监管的重要工作。企业申报的内容须符合《中华人民共和国海关进出口货...

「Eurora」海关编码归类 全球海关编码查询 关务服务

  海关编码是什么?  海关编码即HS编码,为编码协调制度的简称。  其全称为《商品名称及编码协调制度的国际公约》(InternationalConventionforHarmonizedCo...

9月1日起,河南省税务部门对豆制品加工业试行新政7类豆制品均适用投入产出法

全媒体记者杨晓川报道9月2日,记者从税务部门获悉,为减轻纳税人税收负担,完善农产品增值税进项税额抵扣机制,根据相关规定,结合我省实际情况,经广泛调查研究和征求意见,从9月1日起,我省税务部门对豆制品...