提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言

提示:这里可以添加本文要记录的大概内容:

实验三和以往的实验都不一样,是22级的几位学长写的,这里我找到了“原”网址(点击此处),应该也是几位学长的灵感来源,这个网址里的内容已经写的非常详细了,我的报告仅供参考。验收的话,只要最后的文件大小是45B,逻辑清楚应该没什么问题,我这次的验收等级是A


提示:以下是本篇文章正文内容,下面案例可供参考

报告内容

首先,第一个版本:

/* tiny.c */
int main() { return 12; }

对它正常编译和测试:
在这里插入图片描述

那这个可执行文件有多大呢:
通过wc-ca.out 可以看到:
在这里插入图片描述

为得到最小ELF文件,第一步想到的显然是剥离(符号表)可执行文件:
在这里插入图片描述

但是,仅仅只节约了2kB的空间,远远不够,接下来应该怎么做:
首先,tiny.c 返回12 在汇编语言中,意味着函数应该设置累加器 eax 设置为 12,然后返回:

;tiny.asm:
BITS 32
 GLOBAL main
 SECTION .text
 main:
 mov eax, 12
 ret

同样,进行编译和测试得到:
在这里插入图片描述

发现,又节约了1KB左右!继续:
那么为什么一个简单的文件会有这么大的开销呢:问题在于我们仍然通过使用 main()接口去实现函数。链接器仍在为我们向操作系统添加一个接口,而实际上正是这个接口调用了 main() 函数。但是我们并不需要这个接口,首先,我们仅仅只需要返回12,其次,这种实现方式大大增加了开销。
那么怎么避开它,不使用这种方式呢:首先,了解到,链接器默认使用的实际入口点是名为 _start 的符号。当我们使用 gcc 进行链接时,它会自动包含一个 _start 例程,该例程会完成诸如设置 argc 和 argv 等操作,然后调用 main() 函数。 那么,我们完全可以定义自己的_start 例程!

;tiny.asm
 BITS 32
 GLOBAL _start
 SECTION .text
 _start:
 moveax, 12
 ret

那么会成功编译和测试吗:
在这里插入图片描述

并不行!是为什么:
实际上,问题出在我们把_start当成了一个 C 函数,还试图从它里面返回。实际上,它根本就不是一个函数。它只是目标文件里的一个符号,链接器利用这个符号来定位程序的入口点。当我们的程序被调用时,它是被直接调用的。如果我们查看一下,就会发现栈顶的值是数字 1,这显然不像是个地址。
在这里插入图片描述

用GDB调试可以看到:
在这里插入图片描述

其中:
0x00000001:argc(参数个数为 1,即只有程序名)。
0xffffd2a8:argv[0] 的地址(指向程序名字符串)。
0x00000000:argv[] 的终止符NULL。
0xffffd2d3:envp[0] 的地址(指向第一个环境变量字符串)。
argc, argv, envp 的作用:
(1) argc(Argument Count)
功能:表示命令行参数的个数(包括程序名本身)。
(2) argv(Argument Vector)
功能:指向一个以NULL结尾的字符串数组,每个元素是命令行参数的值。
(3) envp(Environment Vector)
功能:指向一个以 NULL结尾的字符串数组,每个元素是环境变量(格式
为KEY=VALUE)
那么,_start 如何退出呢:
那么就可以使用指导书中的int0x80,去唤醒内核。同时,要对 eax 赋值为 1 (告诉内核结束进程),对 ebx 赋值为返回值:
这是因为:首先,eax 存放系统调用号,告诉内核“要执行什么操作”。eax=1:终
止进程。eax = 3:读取文件。eax = 4:写入文件。
其次,需要参数传递,而其规则是:参数按顺序依次存入ebx,ecx,edx,esi,edi,ebp
寄存器。而此时只有一个返回值需要传递。因此,只用改变ebx寄存器的值。

;tiny.asm
 BITS 32
 GLOBAL _start
 SECTION .text
 _start:
 xor eax,eax
 inc eax
 movbl,12
 int 0x80

编译和测试:
在这里插入图片描述

发现,又小了1KB!
但是,我们的程序里仅仅只有7个字节长啊!为什么开销依旧还是很大!
用objdump-xa.out | less 查看一下:
在这里插入图片描述

我们发现 .test确实只有7个字节,符合我们预期,但是,却有很多其他非必须的节,我们是否可以考虑删去它们呢:
那么就可以使用指导书给出的标准ELF文件模板了:

;tiny.asm
 BITS 32
 org 0x08048000
 ehdr:
 db 0x7F,"ELF",1,1,1,0
 times 8 db 0
 dw2
 dw3
 dd 1
 dd _start
 dd phdr- $$
 dd 0
 dd 0
 dwehdrsize
 dwphdrsize
 dw1
 dw0
 dw0
 dw0
 ehdrsize equ $- ehdr
 phdr:
 dd 1
 dd 0
 dd $$
 dd $$
dd filesize
 dd filesize
 dd 5
 dd 0x1000
 phdrsize equ $- phdr
 _start:
 xor eax,eax
 inc eax
 movbl,12
 int 0x80
 filesize equ $- $$

编译并测试:
在这里插入图片描述

哇哦,是不是非常神奇,现在仅仅只有91字节的大小了!
我们再次用objdump查看一下:
在这里插入图片描述

为什么没有.text这些节了呢:
这是因为我们编写的汇编文件是一个高度优化后的文件,为了减小开销,我们跳过
了标准节的定义,不然,如果添加.text等节,需额外元数据,增大文件。
但是,是不是还可以继续缩小呢:
查找资料会发现:一、ELF 文件的不同部分可以放置在任何位置(不过 ELF 文件头必须位于文件的开头),而且它们甚至可以相互重叠。其次,文件头中的某些字段实际上并未被使用。并且,16 字节标识字段末尾的那串零,它们纯粹是填充内容,是为 ELF 标准将来的扩展预留空间的。所以操作系统根本不应该在意那里到底是什么内容。
在这里插入图片描述

那么,我们完全可以将代码放在 ELF 标头内:

;tiny.asm
 BITS 32
 org 0x08048000
 ehdr:
 db 0x7F,"ELF"
 db 1,1,1,0,0
 _start:
 xor eax,eax
 inc eax
 movbl,12
 int 0x80
 dw2
 dw3
 dd 1
 dd _start
 dd phdr- $$
 dd 0
 dd 0
 dwehdrsize
 dwphdrsize
 dw1
 dw0
 dw0
 dw0
 ehdrsize equ $- ehdr
 phdr:
 dd 1
 dd 0
dd $$
 dd $$
 dd filesize
 dd filesize
 dd 5
 dd 0x1000
 phdrsize equ $- phdr
 filesize equ $- $$

编译并测试:
在这里插入图片描述

再用readelf 查看一下:
在这里插入图片描述

发现:Magic已经去掉了7字节的“无用”0!
还可以继续减小开销吗:
当然,如果我们能对程序头表做同样的事情,使其与 ELF 标头重叠,不就可以继续减小了吗!

;tiny.asm
 BITS 32
 org 0x08048000
 ehdr:
 db 0x7F,"ELF"
 db 1,1,1,0,0
 _start:
 xor eax,eax
 inc eax
movbl,12
 int 0x80
 dw2
 dw3
 dd 1
 dd _start
 dd phdr- $$
 dd 0
 dd 0
 dwehdrsize
 dwphdrsize
 phdr:
 dd 0
 ehdrsize equ $- ehdr
 dd $$
 dd $$
 dd filesize
 dd filesize
 dd 5
 dd 0x1000
 phdrsize equ $- phdr
 filesize equ $- $$

编译并测试:
在这里插入图片描述

又减小了8字节!
用readelf 再查看一下:
在这里插入图片描述

发现程序起点从52—>44 ,正是减小的8字节,符合预期。
继续处理:
我们要清楚ELF哪些是关键的,哪些是不关键的!首先,前四个字节必须包含Magic,否则Linux 根本不会处理这个文件。然而,e_ident字段中的另外三个字节并不会被检查,这意味着我们有足足十二个连续的字节可以随意设置。e_type必须设置为 2,以表示这是一个可执行文件,e_machine必须是 3 ,表示目标为英特尔 386 架构。e_version就像e_ident里的版本号一样,完全被忽略。e_entry必须是有效的,因为它指向程序的起始位置。e_phoff需要包含程序头表在文件中的正确偏移量,e_phnum需要包含该表中条目的正确数量。不过,e_flags 文档说明目前对于Intel 未使用,所以我们可以随意使用它。e_ehsize本应是用来验证 ELF 文件头是否具有预期大小的,但 Linux 并不理会它。e_phentsize同样是用于验证程序头表条目的大小。在旧内核中这个字段不会被检查,但现在需要正确设置。ELF 文件头中其他所有与节头表相关的字段,对于可执行文件来说并不起作用。
那么程序头表条目呢:p_type必须为 1,以将其标记为可加载段。p_offset确实需要是开始加载的正确文件偏移量。同样,p_vaddr需要包含合适的加载地址。文档说明p_paddr字段会被忽略,所以这个字段肯定可以随意使用。p_filesz表示从文件中加载到内存中的字节数,p_memsz 表示内存段需要的大小,所以这些数值应该相对合理。p_flags表示赋予内存段的权限。它必须是可读的(4),否则根本无法使用,而且还必须是可执行的(1),否则我们无法在其中执行代码。其他位可能也可以设置,但至少这两个位是必须的。最后,p_align给出了内存段的对齐要求。这个字段主要用于重定位包含位置无关代码的段(比如共享库),所以对于可执行文件,Linux 会忽略我们在这里设置的任何无用信息。
因此,我们会发现:ELF 文件头中大多数必要字段都在前半部分 —— 后半部分几乎完全可
以随意修改。那么,我们完全可以将这两个结构(ELF 文件头结构和程序头表条目结构)相互嵌入:

;tiny.asm
 BITS 32
 org 0x00200000
 ehdr:
 db 0x7F,"ELF"
 db 1,1,1,0,0
 _start:
 xor eax,eax
 inc eax
 movbl,12
 int 0x80
 dw2
 dw3
 dd 1
 dd _start
 dd phdr- $$
 phdr:
 dd 1
 dd 0
 dd $$
dw1
 dw0
 dd filesize
 dd filesize
 dd 5
 dd 0x1000
 filesize equ $- $$

编译并测试:
在这里插入图片描述

减小了12字节:
再次用readelf 查看:
在这里插入图片描述

发现程序起点从44—>32(0x20) ,正是因为相互嵌入而减小的12字节,符合预期。
Program header 中还有几个字段可以供我们可以进处理。p_memsz表示要为内存段分配多少内存,即它至少需要与 p_filesz 一样大,但如果它更大,也不会有任何坏处 —— 可以“尸位素餐”。同时我们可以将 p_flags 字段中的可执行位设置为 0,因为可读位和可执行位存在着一种微妙的共生关系(任何一个都会暗示另一个)。

;tiny.asm
 BITS 32
 org 0x00100000
 db 0x7F,"ELF"
 dd 1
 dd 0
dd $$
 dw2
 dw3
 dd _start
 dd _start
 dd 4
 _start:
 xor eax,eax
 inc eax
 movbl,12
 int 0x80
 db 0
 dw0x34
 dw0x20
 dw1
 dw0
 dw0
 dw0
 filesize equ $- $$

正如我们之前提到可以这么做的,p_flags字段已从 5 改为 4 。这个 4 也是e_phoff字段的值,e_phoff给出了程序头表在文件中的偏移量,而我们恰好将程序头表放在了这个位置 。程序已被移到 ELF 文件头的更低部分,从e_shoff字段开始,到e_flags字段内结束 。
编译并测试:
在这里插入图片描述

发现,又少了12字节!
再次用readelf 看看:
在这里插入图片描述

发现程序头起点尽然变为了4!
也就是说仅仅只包含了7f454c46 , 而这是ELF文件必须包含的!也就是说程序前的内容已经无法再压缩了!
在这里插入图片描述

那么就到此为止了吗?
当然不,我们会发现文件末尾处还有7个0,而如果文件的大小略小于一个完整的 ELF 文件头,Linux 仍然会处理它,如果我们从文件映像中删掉它们:

;tiny.asm
 BITS 32
 org 0x00010000
 db 0x7F,"ELF"
 dd 1
 dd 0
 dd $$
 dw2
 dw3
 dd _start
 dd _start
 dd 4
 _start:
 xor eax,eax
 inc eax
 movbl,12
 int 0x80
 db 0
 dw0x34
 dw0x20
 db 1
 filesize equ $- $$

编译并测试:
在这里插入图片描述
在这里插入图片描述

发现,确实删除了7字节的‘0’,并且可以正常运行!
45 字节便是我能做到的最小ELF文件了!
PS:由于缺失和重叠的内容太多,这个ELF文件甚至不能通过readelf命令查看了!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐