HNU计算机系统实验3:保姆级攻略

  • [[#安装nasm|安装nasm]]
  • [[#linux 64位操作系统环境下用gcc编译32位程序|linux 64位操作系统环境下用gcc编译32位程序]]
  • [[#Hello World 的elf文件分析|Hello World 的elf文件分析]]
    • [[#Hello World 的elf文件分析#分析hw.c的elf文件结构|分析hw.c的elf文件结构]]
      • [[#分析hw.c的elf文件结构#ELF Header分析|ELF Header分析]]
      • [[#分析hw.c的elf文件结构#Section Headers 分析|Section Headers 分析]]
      • [[#分析hw.c的elf文件结构#Program Headers分析|Program Headers分析]]
      • [[#分析hw.c的elf文件结构#.dynamic分析|.dynamic分析]]
      • [[#分析hw.c的elf文件结构#重定位表分析|重定位表分析]]
      • [[#分析hw.c的elf文件结构#Q&A 如何通过重定位表进行重定位?|Q&A 如何通过重定位表进行重定位?]]
      • [[#分析hw.c的elf文件结构#符号表分析|符号表分析]]
    • [[#Hello World 的elf文件分析#程序执行流程|程序执行流程]]
    • [[#Hello World 的elf文件分析#文件大小分析|文件大小分析]]
  • [[#开始获取最小ElF文件|开始获取最小ElF文件]]
    • [[#开始获取最小ElF文件#step1|step1]]
    • [[#开始获取最小ElF文件#step2|step2]]
      • [[#step2#手写汇编尝试|手写汇编尝试]]
    • [[#开始获取最小ElF文件#step 3|step 3]]
      • [[#step 3#原始汇编代码(zero)|原始汇编代码(zero)]]
      • [[#step 3#elf文件大小|elf文件大小]]
      • [[#step 3#查看并分析其elf文件|查看并分析其elf文件]]
      • [[#step 3#Q&A:我们并没有明显调用外部库函数,为什么还会出现这些链接部分以及其有什么作用?|Q&A:我们并没有明显调用外部库函数,为什么还会出现这些链接部分以及其有什么作用?]]
      • [[#step 3#尝试取消链接标准库和启动代码|尝试取消链接标准库和启动代码]]
      • [[#step 3#修改后的汇编代码(one)|修改后的汇编代码(one)]]
      • [[#step 3#elf文件大小|elf文件大小]]
    • [[#开始获取最小ElF文件#step 3.5|step 3.5]]
      • [[#step 3.5#根据实验指导缩减代码|根据实验指导缩减代码]]
      • [[#step 3.5#不缩减代码会有什么问题?|不缩减代码会有什么问题?]]
    • [[#开始获取最小ElF文件#step 4|step 4]]
      • [[#step 4#哪些部分对极简elf文件是必要的|哪些部分对极简elf文件是必要的]]
      • [[#step 4#手搓elf文件代码|手搓elf文件代码]]
    • [[#开始获取最小ElF文件#step 5|step 5]]
      • [[#step 5#将_start压入elf_header(one)|将_start压入elf_header(one)]]
        • [[#将_start压入elf_header(one)#此时的elf文件大小|此时的elf文件大小]]
        • [[#将_start压入elf_header(one)#step3.5简化代码的原因|step3.5简化代码的原因]]
      • [[#step 5#将program header压入elf header(two)|将program header压入elf header(two)]]
        • [[#将program header压入elf header(two)#此时的elf文件大小|此时的elf文件大小]]
    • [[#开始获取最小ElF文件#step 6|step 6]]
      • [[#step 6#elf header各部分的内容,作用,大小(这个表有点乱,有缘再整理)|elf header各部分的内容,作用,大小(这个表有点乱,有缘再整理)]]
      • [[#step 6#program header 各部分的内容,作用,大小|program header 各部分的内容,作用,大小]]
      • [[#step 6#重叠elf header 结构与program header 结构(one)|重叠elf header 结构与program header 结构(one)]]
        • [[#重叠elf header 结构与program header 结构(one)#修改后的代码|修改后的代码]]
        • [[#重叠elf header 结构与program header 结构(one)#此时的elf文件大小|此时的elf文件大小]]
      • [[#step 6#将program header和elf header重叠得更多|将program header和elf header重叠得更多]]
        • [[#将program header和elf header重叠得更多#修改后的代码(two)|修改后的代码(two)]]
        • [[#将program header和elf header重叠得更多#elf文件大小|elf文件大小]]
      • [[#step 6#移除文件末尾的0(three)|移除文件末尾的0(three)]]
        • [[#移除文件末尾的0(three)#修改后的代码|修改后的代码]]
        • [[#移除文件末尾的0(three)#elf文件大小|elf文件大小]]
  • [[#正常执行完结撒花❀|正常执行完结撒花❀]]
  • [[#实验过程中出现的问题|实验过程中出现的问题]]
  • [[#体会|体会]]

一.实验任务

安装nasm

linux 64位操作系统环境下用gcc编译32位程序

1
sudo apt-get install lib32gcc-11-dev lib32stdc++6

Hello World 的elf文件分析

分析hw.c的elf文件结构

elf通常的文件结构如下:

ELF Header分析

  • 文件类型:32位ELF(ELF32)
  • 字节序:小端序(little endian)
  • 文件类型:DYN(位置无关可执行文件,PIE)
    gcc中没有使用-static参数,可以看到entry point的入口也不是静态链接时的0x8开头的32位地址
  • 目标架构:Intel 80386(x86 32位)
  • 入口点地址:0x1060(_start函数的地址)
  • 程序头表(program headers)数量:11个
  • 节区头表(section headers)数量:29个

Section Headers 分析

  • .text (0x1060-0x11cd):包含可执行代码,大小为0x16d字节

    • 包含main函数(地址0x118d,大小60字节)
    • 包含_start函数(地址0x1060,大小44字节)
  • .rodata (0x2000):只读数据段,包含”Hello,World!”字符串常量

  • .data (0x4000):初始化的数据段,大小为8字节

  • .bss (0x4008):未初始化的数据段,大小为4字节

  • .dynamic:动态链接信息

  • .plt:过程链接表,用于动态链接

  • .got:全局偏移表,用于动态链接

Program Headers分析


程序头表定义了11个段,主要包括:

  • PHDR:程序头表自身
  • INTERP:指定动态链接器路径(/lib/ld-linux.so.2)
  • 4个LOAD段
    • 第一个LOAD段:只读,包含ELF头和程序头表
    • 第二个LOAD段:可读可执行,包含.text节(代码)
    • 第三个LOAD段:只读,包含.rodata节(常量数据)
  • DYNAMIC:动态链接信息
  • GNU_STACK:定义栈属性(可读可写,不可执行)

.dynamic分析


动态节区主要显示:

  • 依赖的共享库:libc.so.6(glibc标准c库)
  • INIT:起始代码段地址
  • FINI:终止代码段地址
  • STRTAB/SYMTAB:动态字符串表/符号表地址
  • PLTGOT:GOT地址

重定位表分析

  • .rel.dyn(对应.got)处理数据段重定位,类型包括 R_386_RELATIVE(基址重定位)和 R_386_GLOB_DAT(全局符号地址)
  • .rel.plt(对应.got.plt)处理函数跳转重定位(如 printf 和 __libc_start_main),类型为 R_386_JUMP_SLOT(PLT 跳转槽)。

Q&A 如何通过重定位表进行重定位?

  • .rel.dyn
    • R_386_RELATIVE重定位过程:* 修正地址=基址(info)+原始值(offset)
    • R_386_GLOB_DAT重定位过程:初始化got条目,填入符号的绝对地址
  • .rel.plt
    • 1.首次调用时,跳转到plt桩(如printf的libc地址),通过.got.plt条目触发动态连接器解析符号地址
    • 2.地址回填:解析后的函数地址写入.got.plt(如0x3fe8)
    • 3.后续调用:直接跳转到.got.plt缓存的地址,无需再次解析

符号表分析


  • ​.dynsym​​ (8项):
    动态符号表,记录外部依赖符号(如 printf@GLIBC_2.0)。
  • ​.symtab​​ (40项):
    静态符号表,包含本地符号(如 main_start)和调试信息。

程序执行流程

  1. 操作系统加载程序,并将控制权交给动态链接器(/lib/ld-linux.so.2)
  2. 动态链接器解析所有外部符号(如printf)
  3. 执行从_start函数(地址0x1060)开始
  4. _ start调用__libc_start_main,后者设置C运行时环境
  5. _ libc_start_main调用main函数(地址0x118d)
  6. main函数调用printf打印”Hello,World!”
  7. main返回0,程序结束

文件大小分析

尽管源代码只有几行,编译后的ELF文件包含大量节区和信息,这是因为:

  1. C运行时环境:在main函数执行前库和准备安全机制。,需要大量的初始化代码(_start函数和__libc_start_main函数)来设置程序环境、初始化C
  2. 动态链接支持:程序使用了printf这样的外部函数,需要.dynamic、.plt、.got等节区来支持动态链接,以及重定位表来解析外部符号。
  3. 元数据和调试信息:ELF文件包含符号表、字符串表、版本信息和注释信息等元数据,这些对于程序的加载、调试和维护至关重要。
  4. 安全机制:现代编译器会添加位置无关代码(PIE)、堆栈保护和GOT保护等安全特性,这些都增加了文件大小。
  5. 标准库支持:即使只调用一个printf函数,也需要包含支持该函数运行的所有必要信息。

开始获取最小ElF文件

step1

  • 前述仅仅返回你学号(最后两位)的代码,ta的可执行文件有多大
  • 如果要减小可执行代码,其C源码层面还能进一步简化吗?如果不能,为什么?
    该代码仅包含一个main函数和return语句,从语法角度已无法进一步简化。任何修改(如删除return或修改返回值)都会改变程序行为或导致编译错误.
  • 求助于gcc编译器呢?ta有优化选项啊?请尝试,是否缩小了可执行文件?
    • 使用-O3优化选项
    • 剥离符号表:编译时加-s选项移除调试信息

      可以看到减少了很多字节,readelf查看后可以看到
      ==Entry address发生了变化,.symtab和.strstab节消失==

step2

手写汇编尝试

  • 示例代码
  • 运行尝试
1
2
nasm -f elf32 example.asm 
gcc -m32 -Wall -s example.o -o example
  • 通过readelf objdump 查看.o文件,可执行文件的内容
1
2
readelf -a example
objdump -d example.o

step 3

原始汇编代码(zero)

  • asm
  • 汇编(objdump)

elf文件大小

查看并分析其elf文件

  • 其动态链接部分如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Dynamic section at offset 0x2ee4 contains 27 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x1000
0x0000000d (FINI) 0x1188
0x00000019 (INIT_ARRAY) 0x3edc
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x3ee0
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x1ec
0x00000005 (STRTAB) 0x27c
0x00000006 (SYMTAB) 0x20c
0x0000000a (STRSZ) 151 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x3fdc
0x00000002 (PLTRELSZ) 8 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x394
0x00000011 (REL) 0x354
0x00000012 (RELSZ) 64 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x0000001e (FLAGS) BIND_NOW
0x6ffffffb (FLAGS_1) Flags: NOW PIE
0x6ffffffe (VERNEED) 0x324
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x314
0x6ffffffa (RELCOUNT) 4
0x00000000 (NULL) 0x0

Relocation section '.rel.dyn' at offset 0x354 contains 8 entries:
Offset Info Type Sym.Value Sym. Name
00003edc 00000008 R_386_RELATIVE
00003ee0 00000008 R_386_RELATIVE
00003ff8 00000008 R_386_RELATIVE
00004004 00000008 R_386_RELATIVE
00003fec 00000206 R_386_GLOB_DAT 00000000 _ITM_deregisterTM[...]
00003ff0 00000306 R_386_GLOB_DAT 00000000 __cxa_finalize@GLIBC_2.1.3
00003ff4 00000406 R_386_GLOB_DAT 00000000 __gmon_start__
00003ffc 00000506 R_386_GLOB_DAT 00000000 _ITM_registerTMCl[...]

Relocation section '.rel.plt' at offset 0x394 contains 1 entry:
Offset Info Type Sym.Value Sym. Name
00003fe8 00000107 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.34
No processor specific unwind information to decode

==显式依赖:0x00000001 (NEEDED) Shared library: [libc.so.6]==
==隐式依赖:动态链接器,ld-linux.so.2==

Q&A:我们并没有明显调用外部库函数,为什么还会出现这些链接部分以及其有什么作用?

  • 我们可以查看一下08文件的依赖库

    即使什么外部库都没有调用,可执行文件依然链接了三个库:​libc.so.6,ld-linux.so.2,ld-linux.so.2

[!NOTE] 解释

​ ==​libc.so.6==
动态节区中的NEEDED条目(0x00000001)直接声明了依赖GNU C库(glibc)提供标准C函数(如printfmalloc)和程序启动/终止的底层支持(如__libc_start_main
程序入口初始化​​:__libc_start_main.rel.plt中重定位的符号)负责调用main函数前的环境设置(如栈初始化、全局对象构造)
事务内存​​:_ITM_registerTMCloneTable等符号(.rel.dyn R_386_GLOB_DAT类型)由GCC自动插入,用于线程安全操作
==ld-linux.so.2==
虽然未在.dynamic中显式列出,但所有动态链接的ELF文件均隐式依赖动态链接器(通过.interp节指定路径)。它负责加载libc.so.6并处理重定位
工作流程​​:
内核加载可执行文件后,将控制权交给ld-linux.so.2
解析.dynamic节区,加载libc.so.6等库到内存。
重定位符号地址
==linux-gate.so.1==
这是一个虚拟动态共享库[VSDO](vDSO - 维基百科,自由的百科全书 — vDSO - Wikipedia),由Linux内核直接提供,​
​不实际存在于文件系统中​**​。它用于加速系统调用(syscall),避免传统int 0x80中断的性能开销
特点​:
地址固定(如0xf7f08000),映射到每个进程的地址空间。
-提供高效的sysentersyscall指令实现(取决于CPU架构)

所以我们可以知道出现这些链接部分的原因啦

  • gcc默认动态链接libc,即使代码只包含mov和ret,程序入口(_ start)等仍然由libc提供
  • ld-linux.so.2即动态链接器本身也是个共享库,他负责加载所有共享库并处理重定位;
  • 现代工具链默认生成位置无关代码(pie),依赖动态链接器处理基址重定位
  • gcc插入_ITM_符号,需要libc支持

尝试取消链接标准库和启动代码

1
2
gcc -m32 -Wall -s 08.o -
o 08 -nostdlib

报错:

我们需要编写自己的_start入口函数去代替标准库的main调用

报段错误:
查看堆栈顶部:

[!NOTE] 解释
程序启动时,os会将命令行参数和环境变量压入堆栈:
==堆栈顶部 ->argc argv数组 envp数组==
argc是命令行参数个数,argv是输指向参数字符串的指针数组,envp为环境变量指针数组
此时堆栈顶部看似是地址(如果这是作为c函数的话),但其实是argc,在_start中使用ret会错把argc当成返回地址,导致段错误
_ start 不是c函数,而是链接器指定的程序入口点符号,使用-nostdlib编译时,程序跳过了标准启动文件crt1.o,此时需要手动实现_start的逻辑,不能用c函数的方式返回,因为操作系统不会在堆栈顶部设置return address

所以我们应该用系统调用直接终止进程,回忆一下系统调用的原理?


int$0x80软中断陷入内核查找中断向量表,%eax存储了系统调用号,%ebx存储了返回值,

[!NOTE] 解释
sys_exit的系统调用号为1,存在一个约定的地方(eax)让内核读取从而去系统调用表查找对应系统调用函数
前 5 个参数依次存入 ​​EBX、ECX、EDX、ESI、EDI​​,超过 5 个参数时需通过内存块传递,sys_exit 仅需 1 个参数(退出码),因此该参数存入 ​ebx​​ 寄存器
int $0x80 通过软中断触发用户态到内核态的切换,0x80是预设的系统调用中断号,cpu根据该值在中断向量表中找到对应的处理程序入口地址(system_call)

明白原理,让我们手搓一点点汇编

修改后的汇编代码(one)

  • asm
  • 汇编(objdump)

    此时的elf文件大小为:

    查看Elf文件,发现跟链接有关的节还是存在

    非常讨厌,这是因为 gcc 在链接时会增添一些额外信息, 但我们没有使用它的任何附加功能,因此我们可以自己调用链接器 ld
1
ld -m elf_i386 08.o -s -o 08_ld

再次查看elf文件,讨厌的dynamic终于消失了

[!NOTE] ATTENTION
ld 记得加-s哦不然会多出一大串systab出来

elf文件大小


step 3.5

根据实验指导缩减代码

  • asm
  • 汇编(objdump)

不缩减代码会有什么问题?

详见后文:step3.5简化代码的原因


step 4

哪些部分对极简elf文件是必要的

[!NOTE] 解释

  • 必要部分
    • elf header: 文件格式,入口地址
    • program header: 至少需要一个Pt_load段,(VirtAddr->PhysAddr的映射),该段包含.text
    • text节区:包含执行代码,绝对不能少
    • pt_load段,可读可执行,对于.text节(程序代码)
  • 非必要部分:
    • .data .bss .rodata .symtab:我们没有变量(事实什么类型的变量都没有),不需要
    • .rel.txt .rel.data:重定位条目,同理不需要
    • .debug :这个更不必说,省略省略~
    • section header:节头表用来记录每个节的位置,在链接的时候非常重要,但是我们现在要运行Elf,不太需要这东西

所以我们需要:ELF Header、Program Header(至少一个段)、.text 节区。

手搓elf文件代码


报错:

解决:

1
2
nasm -f bin  08.asm -o 08
#使用-f bin 生成原始二进制文件,不管其是否符合elf格式

`### elf文件大小


step 5

将_start压入elf_header(one)

1
times 8 db      0                       ; e_ident[EI_PAD]: 填充字节

把以上填充字节用_start代替,进一步压缩文件

此时的elf文件大小

==执行文件时报错,那肯定是_start出现了什么问题?这个问题其实在step2中埋下了伏笔,让我们在这里一起为step2做出解答==
当我观察elf文件时,我发现phnum居然为0

可是明明在nasm里写的1

我怀疑是否哪里出了错导致我的phnum被覆盖了
非常显然,我应该去查看_start,我发现_start中的机器码一共7字节,而填充字节原本有8位


元凶就是你!
于是我们修改asm代码如下


现在就可以正常运行啦~

[!NOTE] 一些补充
dd 定义32位的数据单元
dw 定义16位的数据单元
db 定义8位的数据单元

step3.5简化代码的原因

汇编转机器码时,xor,inc比mov的指令编码长度要小的多,mov bl.08 比mov eax,08的编码长度要小,这样才能进一步通过叠叠乐将elf文件进一步简化,防止elf文件被覆写。

将program header压入elf header(two)

观察到program header前八个字节和elf header后八个字节长得很像 ,将相同的部分叠在一起,缝合

此时的elf文件大小


step 6

elf header各部分的内容,作用,大小(这个表有点乱,有缘再整理)

内容 作用 大小
e_ident[16] ​ Magic Number​​(0-3字节):固定为0x7F 'E' 'L' 'F',标识ELF文件
​ ​EI_CLASS​​(4字节):文件类型,1表示32位(ELFCLASS32),2表示64位(ELFCLASS64
​EI_DATA​​(5字节):字节序,1为小端(ELFDATA2LSB),2为大端(ELFDATA2MSB
​​EI_VERSION(6字节):ELF版本,固定为1EV_CURRENT
​EI_OSABI​​(7字节):操作系统ABI类型(如Linux为3
​​EI_ABIVERSION​​(8字节):ABI版本(通常为0
​填充字节​​(9-15字节):对齐保留字段,默认用0填充
标识ELF文件的基本属性 16字节
e_type 1:ET_REL:可重定位文件(如.o文件)

2:ET_EXEC:可执行文件

3:ET_DYN:共享库(如.so文件)

4:ET_CORE:核心转储文件
定义文件类型 2字节
e_machine 3:EM_386:Intel x86架构

0x28:EM_AR:ARM架构

0x3E:EM_X86_64:x86-64架构
目标架构标识 2字节
e_version 固定为1 ELF文件版本 4字节
e_entry 程序入口点的虚拟地址(VA) 程序入口点的虚拟地址(VA) 4字节/32位 8字节/64位
e_phoff 程序头表(Program Header Table)在文件中的偏移量 程序头表(Program Header Table)在文件中的偏移量 32位下4字节,64位下8字节
e_shoff 节头表(Section Header Table)在文件中的偏移量 节头表(Section Header Table)在文件中的偏移量 32位下4字节,64位下8字节
e_flags 通常为0 处理器特定标志(如ARM架构的指令集类型)。 4字节
e_ehsize ELF头自身的大小 ELF头自身的大小(32位为52字节,64位为64字节) 2字节
e_phentsize 程序头表中每个条目的大小(默认值为32字节,0x20) 程序头表中每个条目的大小 2字节
e_phnum 程序头表中条目的数量。 程序头表中条目的数量。若为0,表示无程序头表 2字节
e_shentsize 节头表中每个条目的大小(固定为40字节) 节头表中每个条目的大小(固定为40字节) 2字节
e_shnum 节头表中条目的数量。若为0,表示无节头表 节头表中条目的数量 2字节
e_shstrndx 节名称字符串表在节头表中的索引 节名称字符串表在节头表中的索引 2字节

program header 各部分的内容,作用,大小

内容 ​作用​ ​大小(32位)​
​p_type​ 段类型标识符(如PT_LOADPT_DYNAMIC 定义段的用途,如代码加载、动态链接信息、解释器路径等 4字节
​p_offset​ 段在文件中的偏移量 指示段内容在ELF文件中的起始位置 4字节
​p_vaddr 段加载到内存的虚拟地址 指定段在进程虚拟地址空间中的起始位置 4字节
​p_paddr 段加载到物理内存的地址(通常与p_vaddr相同,现代系统忽略) 保留字段,多用于嵌入式系统或无虚拟内存的架构 4字节
​p_filesz​ 段在文件中的大小 确定从文件读取多少字节内容到内存(若为0,表示无文件内容) 4字节
​p_memsz​ 段在内存中的大小 定义段在内存中占用的空间(可能大于p_filesz,多余部分填充零) 4字节
p_flags​ 段权限标志(PF_RPF_WPF_X的组合) 控制内存页的访问权限(如可读、可写、可执行) 4字节
​p_align​ 段在文件和内存中的对齐要求(必须是2的幂,如0x1000) 确保段地址按页对齐,提升内存映射效率 4字节

可以看到,Elf header的后半部分几乎不太重要,让我们对他进行修改,只保留一些必要的信息

重叠elf header 结构与program header 结构(one)

修改后的代码

  • 此时,程序头表的前 20 个字节与 ELF 头的最后 20 个字节重叠,而且还契合得很好。重叠区域内ELF头只有两个部分是重要的:第一个是 e_phnum 字段,它恰好与p_paddr 字段重合,这是程序头表中为数不多的绝对被忽略的字段之一。另 一个是 ==e_phentsize 字段(默认值应为0x20)==,它与 p_vaddr 字段的上半部分重合。这些为我们的程序选择一个非标准加载地址来匹配,上半部分等于 0x0020。
此时的elf文件大小

将program header和elf header重叠得更多

  • p_memsz>= p_filesz
    p_memsz 表 示要为内存段分配多少内存,即它至少需要与 p_filesz 一样大,但如果它更大, 也不会有任何坏处 —— 可以“尸位素餐”。
  • p_flag
    可以将 p_flags 字段中的 可执行位设置为0,因为可读位和可执行位存在着一种微妙的共生关系(任何一个 都会暗示另一个)。
  • 000_

[!NOTE] 一些补充
传统系统中,可读(PF_R)与可执行(PF_X)权限存在隐式关联: 若段标记为 PF_R,某些旧版内核(或未启用严格内存保护的系统)会默认允许代码执行,即使未显式设置 PF_X

修改后的代码(two)

  • 此时e_phoff 变为00000100,这是program header的文件偏移量(0x7F,”ELF”),p_flags相应的变为4(可读和可执行相互暗示)
  • 加载地址更改成一个低得多的数字,这样e_entry的值就会小,p_memsz就会相应的变小(实际上, 对于虚拟内存,这几乎无关紧要,为什么?)
  • 对 p_filesz 为什么要如此更改?因为我们没有在 p_flags 字段中设置写入位,所以 Linux 不允许定义大于 p_filesz 的 p_memsz 值,因为如果这些额外的字节不可写, 它就无法对它们进行零初始化。将 p_filesz 增加到等于 p_memsz。但这会使得其比实际文件大。

[!NOTE] 为什么虚拟内存机制中,p_memsz(程序头表中定义的段在内存中的大小)对虚拟内存无关紧要?
我的理解:p_memsz仅描述段在虚拟空间中的逻辑范围,并不强制要求物理内存立即分配,物理内存的实际分配由操作系统按需完成

elf文件大小

移除文件末尾的0(three)

修改后的代码

elf文件大小

正常执行~完结撒花❀


二. 心得体会

实验过程中出现的问题

  • elf文件对每个部分的规定都非常严格,有自己的字节数,缩小文件的时候我少打了一个0,就导致后面的部分被覆盖了,没办法正常执行
  • 修改elf文件其实是一个偏离Elf文件编写规范的过程(因为我们只追求极致的小),所以要把它强行使用-f bin 生成原始二进制文件,不管其是否符合elf格式
  • 还有一些小问题我写在实验文档穿插的Q&A里啦,往上翻翻

体会

实验三还是相对好做的,主要考察我们对编译链接过程的理解,对elf文件架构的理解以及缩小elf文件时的思考,我们需要思考链接过程能否进行优化,哪些内容是执行文件是必要的,哪些是不必要的,通过精巧的设计将elf文件不同部分重叠,抠字眼的去缩小。
这个实验可以说是对link这一章节的拓展延伸,让我们理论课的理解发生质变,受益匪浅,而且确实好玩~~ 之前做小班课病毒那一次时,我就有尝试对elf文件进行修改,又熟悉又陌生的感觉^-^