HNU计算机系统实验三 前言做得很粗糙可能有很多问题仅供参考(x)手搓最小ELF可执行文件1.实验项目1.1项目名称最小ELF可执行文件1.2实验目的深入理解ELF文件结构掌握Linux系统调用机制理解链接器的工作原理学习汇编语言编程基础体验极致优化的思维方式。1.3实验资源本实验采用个性化设计每位学生的目标返回值即return的数值为其学号的末尾6位或4位数字。这里用的是后两位要不然有前导零本质上只是需要个性化2.实验任务2.1实验任务本实验包含六个阶段阶段一从最简单的C程序开始了解编译器生成的可执行文件大小学习使用基本优化手段减小文件“尺寸”。阶段二将C程序改写为汇编语言 去除C运行时库的依赖 直接使用Linux系统调用退出程序。阶段三理解ELF文件中的节Section 概念 学习如何去除“非必要”内容 使用ld直接链接。阶段四使用 nasm的bin输出格式 手动构建—个完整的ELF可执行文件 精确控制每个字节。阶段五通过让程序头表与ELF头重叠 进一步减小文件大小。 这是ELF优化的关键技巧。阶段六尝试达到ELF可执行文件的绝对最小值。利用Linux加载器的容错性 移除文件末尾的零字节。2.2实验过程1实验资源处理使用命令sudo apt install nasm安装nasm为后续实验做准备2阶段一1编写基础C程序创建一个名为tiny.c的文件填入目标值xx后内容如下/* tiny.c */ int main () { return xx; }执行以下命令编译程序并查看返回值:~/exp3$ gcc -Wall tiny.c:~/exp3$ ./a.out; echo $?目标值:~/exp3$ wc -c a.out15776 a.out由输出结果可得该可执行文件大小为15776字节即约15.4KB从代码层面上无法继续简化因为只能修改return值而该值对文件大小并没有过多影响但使用gcc编译可减小文件大小例如使用指令gcc -Os -s tiny.c编译可发现文件大小为14336字节。2热身演习创建Example.asm文件并尝试编译BITS 32 GLOBAL main SECTION .text main: mov eax, 5 mov ebx, 10 add eax, ebx ret所用指令nasm -f elf32 Example.asmgcc -m32 -Wall -s Example.o -o example./exampleecho $?输出结果为15使用指令时会弹一个警告但不影响3使用strip优化使用 -s 选项移除可执行文件中的部分内容执行命令可得优化前的可执行文件大小为15776字节而优化后为14336字节:~/exp3$ gcc -Wall -s tiny.c:~/exp3$ wc -c a.out14336 a.out4使用编译器优化由图可知使用编译器优化后的大小仍然为14336效果不明显:~/exp3$ gcc -Wall -s -O3 tiny.c:~/exp3$ wc -c a.out14336 a.out5思考①strip命令移除了可执行文件中的符号表和调试信息等非运行必要内容仅保留机器指令和数据段因此可以减少文件大小。②-O3优化程序的计算逻辑而该程序构极其简单没有循环、函数调用或复杂计算因此编译器无法进行有效的高级优化。③一个可执行文件包括代码操作系统运行环境标准库和链接器信息。在这些内容里c代码占比大概在1%-10%。6阶段验证记录原始文件大小15776字节strip后大小14336字节-O3优化后大小 14336字节阶段一解锁密钥为上述三个数字用连字符连接 格式数字1-数字2-数字3密钥15776-14336-143363阶段二1编写汇编程序创建tiny.asm文件BITS 32 GLOBAL _start SECTION .text _start: mov eax, 1 mov ebx, 32 int 0x802编译汇编程序nasm -f elf tiny.asmld -m elf_i386 -s tiny.o./a.out; echo $?输出32wc -c a.out输出4248 a.out3指令优化分析当前程序的机器码大小 尝试使用更短的指令优化BITS 32 GLOBAL _start SECTION .text _start: xor eax, eax inc eax mov bl, 32 int 0x80nasm -f elf tiny.asmld -m elf_i386 -s tiny.o./a.out; echo $?输出32wc -c a.out输出4240 a.out4阶段验证记录优化前文件大小4248优化后文件大小 4240节省的字节数8阶段二解锁密钥为优化后大小×4节省字节数因此密钥169684阶段三1分析当前ELF结构使用readelf查看当前可执行文件的结构与内容(这里偷懒了结果太长只截了关键部分TT)观察下图可知映射表中出现.interp和.dynamic等关键词以及Dynamic section动态段表示当前可执行文件中存在动态链接行为。即使程序未显式调用外部库函数但gcc默认会链接标准库因此链接过程必然发生。尝试在gcc时使用-nostdlib 取消链接标准库和相关启动代码。nasm -f elf32 tiny.asmgcc -m32 -Wall -s -nostdlib tiny.o出现_start未定义报错2直接对_start 编程BITS 32 GLOBAL _start SECTION .text _start: mov eax, 32 retnasm -f elf tiny.asmgcc -m32 -nostdlib tiny.o./a.outSegmentation fault (core dumped)出现段错误因为代码最后用了retret会从栈中取返回地址然后跳转。在普通的C函数里ret的返回地址是调用者压入栈的而裸程序入口_start没有调用者也没有压入返回地址。因此执行ret时CPU取到的是随机或非法地址会出现段错误。在函数调用过程中栈会压入以下参数并由call指令自动压入返回地址函数内部可进一步保存旧基址寄存器及局部变量从而形成完整的栈帧结构。argc参数数量argv命令行参数内容envp环境变量信息使用gdb分析当前栈可得argc 0x00000001,argv[0] 0xffffd1a8, 后面则为envp内容如何从_start退出裸汇编程序_start必须自己调用系统调用退出。用int 0x80指令唤醒内核若要结束该进程 并返回相应的值 需要对寄存器eax赋值为1告诉内核结束进程对ebx赋返回值。因为在x86 Linux中eax 决定调用哪个功能而ebx/ecx/...是功能参数这是内核规定的固定接口。修改代码编译执行后再次查看文件发现修改后的程序仍然不符合预期BITS 32 GLOBAL _start SECTION .text _start: mov eax, 1 mov ebx, 32 int 0x803使用ld直接链接ld -m elf_i386 -s tiny.o可以发现终于没有链接部分了再查看当前的文件大小wc -c a.out4248 a.out进行缩减最后的内容是BITS 32 GLOBAL _start SECTION .text _start: xor eax, eax inc eax mov bl, 32 int 0x80nasm -f elf tiny.asmld -m elf_i386 -s tiny.owc -c a.out4240 a.out4理解大小变化1.gcc链接时默认添加了启动代码、标准库、编译器支持库以及动态链接器等以提供程序入口、运行时初始化、函数支持和正确退出机制从而保证程序能够在操作系统中正常运行。2.ld链接不会自动链接启动代码、标准库和运行时支持仅包含用户提供的目标代码针对按用户要求拼接因此文件更小5阶段验证记录阶段三解锁密钥格式LD-字节数密钥42405阶段四1编写ELF模板;tiny_elf.asm BITS 32 org 0x08048000 ehdr: db 0x7f, ELF, 1, 1, 1, 0 ; e_ident times 8 db 0 dw 2 ; e_type dw 3 ; e_machine dd 1 ; e_version dd _start ; e_entry dd phdr - $$ ; e_phoff dd 0 ; e_shoff dd 0 ; e_flags dw ehdrsize ; e_ehsize dw phdrsize ; e_phentsize dw 1 ; e_phnum dw 0 ; e_shentsize dw 0 ; e_shnum dw 0 ; e_shstrndx ehdrsize equ $ - ehdr phdr: dd 1 ; p_type dd 0 ; p_offset dd $$ ; p_vaddr dd $$ ; p_paddr dd filesize ; p_filesz dd filesize ; p_memsz dd 5 ; p_flags dd 0x1000 ; p_align phdrsize equ $ - phdr _start: mov bl, 32 xor eax, eax inc eax int 0x80 filesize equ $ - $$2编译并测试nasm -f bin -o a.out tiny_elf.asmchmod x a.outwc -c a.out91 a.out3分析文件结构输入指令xxd a.out | head -10验证ELF魔数前4字节 7F 45 4C 464思考ELF头大小 程序头表大小 84字节程序代码大小 7字节文件总大小等于上述之和因为bin没有对齐或链接器干预。5阶段验证记录阶段四解锁密钥为文件大小 × 2 - 程序代码大小即1756阶段五1编写重叠版本原先ehdr的后八字节与phdr前八字节用小端法表示时均为01 00 00 00 00 00 00 00可以重叠;tiny_elf.asm BITS 32 org 0x08048000 ehdr: db 0x7f, ELF, 1, 1, 1, 0 ; e_ident db 0 _start: mov bl, 32 xor eax, eax inc eax int 0x80 ELF头剩余部分 dw 2 ; e_type dw 3 ; e_machine dd 1 ; e_version dd _start ; e_entry dd phdr - $$ ; e_phoff dd 0 ; e_shoff dd 0 ; e_flags dw ehdrsize ; e_ehsize dw phdrsize ; e_phentsize phdr: dd 1 ; p_type dd 0 ; p_offset ehdrsize equ $ - ehdr dd $$ ; p_vaddr dd $$ ; p_paddr dd filesize ; p_filesz dd filesize ; p_memsz dd 5 ; p_flags dd 0x1000 ; p_align phdrsize equ $ - phdr filesize equ $ - $$2编译并验证...省略指令...输出76 a.out3进一步优化org 0x08048000 -org 0x00010000dd 5 ; p_flags -dd 4 ; p_flags4阶段验证记录阶段五解锁密钥为文件大小即765思考在.bin文件里org只是逻辑偏移不是实际虚拟地址Linux不会按这个地址加载因此加载地址可以被改到如此“低”的数字。在.bin文件里操作系统不会解析p_flags因此可以填4.7阶段六1完成终极代码;tiny_elf.asm BITS 32 org 0x00010000 db 0x7f, ELF ; e_ident dd 1 ; p_type dd 0 ; p_offset dd $$ ; p_vaddr dw 2 ; e_type dw 3 ; e_machine dd _start ; e_version ;p_filesz dd _start ; e_entry ;p_memsz dd 4 ; e_phoff;p_flags _start: mov bl, 32 xor eax, eax inc eax int 0x80 db 0 dw 0x34 dw 0x20 db 1 filesize equ $ - $$2编译验证...省略指令...输出45 a.out3预期能够获得最⼩可执⾏⽂件⼤⼩是45字节其构成如下输入指令xxd a.out | head -10魔数依旧存在且占4字节程序头表有28字节代码7字节以及剩余部分5字节和填充的1字节。4))思考题bin输出纯二进制⽂件不会添加任何ELF结构因此e_phnum可以使用1字节并且p_filesz和p_memsz都⽐实际文件大小大也能正常运行db 0x7f,ELF违反了ELF标准标准要求16字节但实际只写4字节e_version / p_filesz和e_entry / p_memsz有字段覆盖情况_start:有头重叠情况e_phoff地址偏低。从一开始的15776字节到现在的45字节一共优化了15731字节压缩比约为99.7%。3总结3.1实验中出现的问题①在本次实验中出现了对nasm以及elf相关指令不熟悉的情况由于不同阶段对实验细节的要求不断提高指令也变得复杂②对elf结构不够熟悉即使参考指南更改代码也会遇到阻碍。3.2心得体会在本次实验中我通过一步步编写最小的ELF文件深入理解了 ELF 文件结构、程序头表、代码段以及裸二进制输出的原理。整个实验过程让我对加载程序、二进制文件格式和汇编编程有了更直观的认识。