Linux可执行文件中有什么?

2020-10-29 12:05:16

自从我还是个孩子的时候就发现可执行文件只是文件之后,我就一直对它们着迷。如果您将.exe重命名为其他名称,您可以在记事本中打开它!如果您将其他文件重命名为.exe,您会看到一个整齐的错误对话框。

显然,这些文件有些不同。从记事本上看,他们大多是胡言乱语,但在混乱中必须有秩序。12岁的我知道这一点,尽管他不太知道如何或在哪里挖掘才能让这一切变得有意义。

所以,这个系列是献给我过去的自己。在这本书中,我们将试图理解Linux可执行文件是如何组织的,它们是如何执行的,以及如何编写一个程序,将可执行文件从链接器上取下并压缩--这仅仅是因为我们可以做到这一点。

由于上一个大型系列(自己制作ping)都是关于Windows的,所以这一个系列将重点关注64位Linux。

在本系列的整个过程中,我们肯定会想要发出我们自己的ELF文件,但是-就像我们在处理以太网、IPv4和ICMP时所做的那样,我们首先要拿到一个格式良好、工作正常的Linux可执行文件,然后用各种棍子戳它。

ELF代表可执行和可链接格式。它第一次发布是在1983年,作为SysV4的一部分,今天它仍然在Linux上使用,尽管增加了新的部分。

我不得不回到阅读文件的艰难道路上来--第2部分是为了快速复习一下--Netwide汇编程序--所以如果你不得不这么做,我也不会责怪你。

无论如何,这里是简短的版本:这里是一些要打印到标准输出的代码。你好,后面跟一个换行符:

;在`hello.asm`global_start节.text_start:mov rdi,1;stdout FD mov rsi,msg mov rdx,9;8个字符+换行符mov rax,1;写入syscall syscall xor rdi,rdi;返回代码0 mov rax,60;退出syscall syscall节.datamsg:db";hi";,10。

$NASM-f elf64 hello.asm$ld hello.o-o hello$file hellohello:ELF 64位LSB可执行文件,x86-64,版本1(SYSV),静态链接,而不是剥离$。/hellohi在那里。

现在,我们的可执行文件按原样大约是8.68 KiB。如果我们在它上面使用gzip-9,我们可以很容易地把它降到372B,所以我有点好奇,老实说,我想看看里面有什么。

这个系列不是关于哀叹过去的美好时光,那时所有的东西都可以放在软盘上。这不是关于现代软件是如何臃肿和缓慢的,也不是要谈到开发人员是多么懒惰,只要有人尝试一下,事情就不会那么难了。

我们在这里不是在开发一些实用的东西,我们纯粹是为了学习东西而选择一个挑战。

如果你需要一大片怀旧之情,那么你可以在几乎所有的互联网上模糊地查看一下。

如果我们查看hello的十六进制转储,我们会看到早期的ELF字符串,后面跟着一组二进制数据:

$xxd<;hello|head00000000:7f45 4c46 0201 0100 0000 0000 0000.ELF.00000010:0200 3e00 0100 0000 0010 4000 0000 0000 0000.>;00000020:4000 0000 0000 0000 3821 0000 [email protected]:0000 0000 4000 3800 0300 4000 0600 [email protected]:0100 0000 0400 0000 0000 0000.00000050:0000 4000 0000 0000 0000 4000 0000 0000 [email protected]:e800。00000070:00100 0000 0000 0000 0100 0000 0500 0000...00000080:0010 0000 0000 0000 0010 4000 0000 0000.00000090:0010 4000 0000 0000 2500 0000 0000 0000

但是我们可以非常容易地看到,文件的大部分由空(零)字节组成:

$xxd<;HELLO|Tail-60|head00001f00:0000 0000 0000.00001f10:0000 0000 0000.00001f20:0000 0000 0000.00001f30:0000 0000 0000.00001f40:0000 0000 0000。00001f50:0000 0000 0000.00001f60:0000 0000 0000.00001f70:0000 0000 0000.00001f70:0000 0000 0000.00001f50:0000 0000 0000.00001f70:0000 0000 0000。00001f90:0000 0000 0000.。

我们还可以找到一个包含一堆名字的部分,也许它们有某种意义?

$xxd<;hello|ail-32|head000020c0:0920 4000 0000 0000 0000。@.000020d0:2b00 0000 1000 0200 1020 4000 0000 0000+.。@...000020e0:0000 0000 0000 0068 656c 6c6f 2d6f.hello-o000020f0:7269 6769 6e61 6c2e 6173 6d00 6d73 6700 riginal.asm.msg.00002100:5f5f 6273 735f 7374 6172 7400 5f65 6461__bss_start._eda00002110:7461 005f 656e 6400 002e 7379 6d74 6162._end...symtab00002120:002e 73274 6162 2e 73274str..sht00002130:600162 e 7465 780074 2e 64761.。00002150:0000 0000 0000。

现在,有一整套工具可以让我们在舒适的终端上戳到这个ELF文件。哦,是的。他们中的许多人。一目了然的工具和工具。但我们今天不会使用它们。今天不行,伙计。

今天,我们自己解析事物。使用的是我们在“制造我们自己的ping”系列中使用的Nom板条箱,并配备了它,我愿意分析几乎任何东西-即使是PSD。

不过,要做到这一点,我们需要一些指导。ELF的维基百科页面还算不错,但它也不是最好的概述--部分原因是它为32位ELF而烦恼,我们在本系列的整个篇幅中都会很方便地忘记这一点。

我意识到要接受的东西太多了--很多东西现在还说不通呢!。

在我们开始编写任何代码之前,让我们手工做一些基本的探索。根据该图,在文件中的偏移量62处,有一个带有节名称的条目索引。对于我们的hello可执行文件,这是...。

$#-s=查找,-l=长度$xxd-s 62-l 2。/hello0000003e:0500.。

字节05和00-现在,我们正在处理一个小端文件,这意味着0x0005,也就是5。因此,表中的第五节标题包含节名称。

在这一点上,我们不知道段是什么,但我认为可以安全地说,文件被分成几个段,并且它们的开始和大小存储在这些段标题中。

$#-g=组大小,-e=小端$xxd-s 40-l 8-g 8-e./hello00000028:0000000000002140。

即使xxd本身不支持十六进制记数法,我们也可以用$((Expr))!

根据ELF上的Wikipedia页面,每个区段标题都包含存储区段数据的文件中的偏移量……。偏移量为0x18!

$xxd-s$((0x2140+0x40*5+0x18))-l 8-g 8-e./hello00002298:0000000000002118。!.

这意味着包含节名称的节的数据应该是0x2118。让我们来看看吧:

$xxd-s$((0x2118))./hello|head-400002118:002e 7379 6d74 6162 002e 7374 7274 6162.symtab..strtab00002128:002e 7368 7374 7274 6162 002e 7465 7874.shstrtab..text00002138:002e 6461 7461 0000 0000 0000数据.00002148:0000 0000 0000.。

手工浏览文件很有趣,我们学会了使用xxd来做这件事(如果没有图形十六进制查看器/编辑器,这在核冬天肯定会派上用场),但是我们现在可能想要开始编写一个真正的解析器了。

$Cargo new--lib Delf$cd Delf$Cargo将NOM添加到依赖项中添加NOM v5.1.2。

//在`delf/src/parse.rs`pub type input<;';a&>;=&;&39;a[U8];;pub type result<;a,O>;=nom::IResult<;input<;,O,nom::error::VerboseError<;input<;';a>;>;;

就像我前面说的,我们不会费心使用大端ELF或32位ELF,所以我们可以硬编码一些值。让我们开始吧!

//在`delf/src/lib.rs`#[Derive(Debug)]pub struct File{//we';ll add field as we go}Impl File{const Magic:&;&39;static[U8]=&;[0x7f,0x45,0x4c,0x46];pub FN parse(i:parse::input)->;parse::result<;self>;{Use Nom::{Bytes::Complete::{Tag,Take},Error::Context,Sequence::tuple,};let(i,_)=tuple((//-Context((//-Context(";Magic";,Tag(";Class";,Tag(&;[0x2]),Context(";Endianness";,标记(&;[0x1]),上下文(";版本";,标记(&;[0x1])),上下文(";操作系统ABI&34;,标记(&;[0x0])),//-上下文(";填充";,Take(8_Usize)),))(I)?;好的((我,自己{}))}}。

这看起来很合理。让我们再做一个板条箱来测试Delf的板条箱。我们将把它命名为Elk&34;,用于Executable&;Linker Kit";

//在`src/elk/main.rs`中使用std::{env,error::error,fs};fn main()->;result<;(),Box<;dyn error>;>;{let input_path=env::args()。第n(1)。预期(";用法:ELK文件";);让INPUT=fs::Read(&;INPUT_PATH)?;Delf::File::Parse(&;INPUT[.。]))。Map_err(|e|format!(";{:?}";,e))?;println!(";输入为支持的ELF文件!";);确定(())}。

让我们从类型开始-在Rust中声明一个枚举就足够简单了。我们会想要派生一些有用的特性-Debug for Print,Clone and Copy,这样它就有了Copy语义(而不是传递所有权),PartialEq和Eq来比较Type值是否相等。

//在`delf/src/lib.rs`#[Derate(Debug,Clone,Copy,PartialEq,Eq)]pub枚举类型{None,Rel,Exec,Dyn,Core,}中。

但是,当读取ELF文件时,我们不会得到Type-我们会得到u16。同样,当我们写出ELF文件时,我们也需要u16。

//在`delf/src/lib.rs`实施类型{to_u16(&;self)->;u16{Match Self{Self::None=>;0,Self::Rel=>;1,Self::Exec=>;2,Self::Dyn=>;3,//etc}}。

或者,我们可以将类型枚举的表示形式设置为u16-然后我们将免费获得:

//在`delf/src/lib.rs`中#[Derate(Debug,Clone,Copy,PartialEq,Eq)]#[repr(U16)]pub枚举类型{None=0x0,rel=0x1,Exec=0x2,dyn=0x3,Core=0x4,}。

现在,我们可以使用as运算符将Type强制转换为u16-let swrite快速测试来验证我们的假设:

$#t==测试,--lib==仅库单元测试(不是文档测试)$Cargo t--库已完成测试[未优化+调试信息]运行target/debug/deps/delf-d6fdd5529c793a0brunning 1测试测试的0.02s目标::TYPE_TO_u16...。OK测试结果:OK。1通过;0失败;0忽略;0测量;0过滤掉。

好极了!现在有了另一种方式的小问题-将u16转换为Type。当然,这里的问题是并不是所有的u16值都是有效的Type值。

事实上,如果我们自己从_u16开始写,我们将不得不正面处理这个问题:

//在`delf/src/lib.rs`实施类型{pub fn from_u16(x:u16)->;self{Match x{0=>;self::None,1=>;self::rel,2=>;self::exec,3=>;self::dyn,4=>;self::core,}。

#b=构建,-q=安静$CAROR b-qerror[E0004]:非穷举模式:`5u16..=std::u16::max`未覆盖-->;src/lib.rs:17:15|17|match x{|^pattern`5u16..=std::u16::max`未覆盖|=help:确保正在处理所有可能的情况,可能是通过添加通配符或更多匹配臂。

//在`delf/src/lib.rs`impl Type{pub fn from_u16(x:u16)->;option<;self>;{Match x{0=>;Some(Self::None),1=>;Some(Self::Rel),2=>;Some(Self::Exec),3=>;Some(Self::Dyn),4=>;Some(Self::Core),_=>;Some(Self::Core),_=>;无,}#[cfg(Test)]mod test{//省略:以前的测试#[test]fn type_from_u16(){assert_eq!(Super::type::from_u16(0x3),ome(Super::type::dyn));assert_eq!(Super::type::from_u16(0xf00d),None);}}。

$Cargo t--lib已在0.02s内完成测试[未优化+调试信息]目标,运行类型2测试测试::target/debug/deps/delf-d6fdd5529c793a0brunning_to_u16...。正常测试测试::type_from_u16...。OK测试结果:OK。2通过;0失败;0忽略;0测量;0过滤掉。

...但是这有点单调乏味。当我们第一次定义枚举时,我们已经指定了Type<;->;u16映射。我们为什么要重复我们自己呢?

事实证明..。眼睛会惊奇地亮起来,有一个板条箱可以放这个!

//在`delf/src/lib.rs`中使用Deriate_try_from_primitive::TryFromPrimitive;#[Deriate(Debug,Clone,Copy,PartialEq,Eq,TryFromPrimitive)]#[repr(U16)]pub枚举类型{None=0x0,rel=0x1,Exec=0x2,dyn=0x3,Core=0x4,}#[Deriate(Debug,Clone,Copy,PartialEq,Eq,TryFromPrimitive)]#[repr(U16)]pub enum Machine{X86=0x03,X86_64=0x3e,}#[CFG(Test)]mod test{Use Super::Machine;Use STD::Convert::TryFromPrimitive)]#[repr(U16)]pub enum Machine{X86=0x03,X86_64=0x3e,}#[CFG(Test)]mod test{USE SUPER::MACHINE;USE STD::CONVERT::TryFromPrimitive。#[test]fn try_enum(){assert_eq!(MACHINE::x86_64 as u16,0x3E);assert_eq!(MACHINE::TRY_FROM(0x3E),OK(MACHINE::x86_64));ASSERT_eq!(MACHINE::TRY_FROM(0xFA),Err(0xFA));}}。

在本文的前一个版本中,我们使用了Derate-try-from-primitive v0.1.0,它返回一个选项<;T>;。但是,从那时起,发布了从原语派生-尝试-版本1.0.0,它返回一个结果<;T,E>;,因为它实现了标准的TryFrom接口。

$Cargo t已在0.01s内完成测试[未优化+调试信息]目标,运行/home/amos/ftl/elf-series/target/debug/deps/delf-2d44b198a598eda8running 1测试测试::Try_enums...。OK测试结果:OK。1通过;0失败;0忽略;0测量;0过滤出单据-测试脱机测试0测试结果:OK。0通过;0失败;0被忽略;0被测量;0被过滤掉。

现在,我们终于可以解析类型和机器了。首先让我们将它们添加到文件结构中:

$Cargo b-qerror:预期标识符,找到关键字`type`-->;src/lib.rs:24:5|24|type:type,|^预期标识符,找到关键字|help:您可以转义保留关键字以将其用作标识符|24|r#type:type,|^。

它是!。我们可以改用类型--但就这一次,让我们按照编译器的建议,改用转义形式。

//在`delf/src/lib.rs`中//new!Use std::Convert::TryFrom;Iml File{//省略:魔术常数pub FN parse(i:parse::input)->;parse::result<;self>;{use nom::{bytes::Complete::{Tag,Take},Error::Context,Sequence::Tuple,Combinator::Map,Number::Complete::LE_u16,};//省略:解析魔术等let(i,(r#type,machine))=tuple((context(";Type";,map(le_u16,|x|Type::try_from(X)。展开()),上下文(";Machine";,map(le_u16,|x|Machine::try_from(X)。Unwork(),)(I)?;设res=self{机器,r#type};OK((i,res))}}

为了尝试这一点,我们将切换回我们的二进制机箱elk一秒钟,并打印File结构,现在它有了字段!

//在`elk/src/main.rs`fn main()->;result<;()中,Box<;dyn error>;>;{let input_path=env::args()。第n(1)。预期(";用法:ELK文件";);let input=fs::read(&;input_path)?;let(_,file)=Delf::file::parse(&;input[.。]))。Map_err(|e|format!(";{:?}";,e))?;//new!Println!(";{:#?}";,file);确定(())}。

$./target/debug/elk/usr/lib32/libc.soError:";错误(VerboseError{错误:[47,42,32,71,78,85,32,108,100,32,115,99,114,105,112,116,10,32,32,32,85,115,101,32,116,104,101,32,115,104,97,114,101,100,32,108,105,98,114,97,114,121,44,32,98,117,116,32,115,111,109,101,32,102,117,110,99,116,105,111,110,115、32、97、114、101、32、111、110、108、121、32、105、110、10、32、32、116、104、101、32、115、116、97、116、105、99、32、108、105、98、114、97、114、121、44、32、115、111、32、116、114、121、32、116、104、97、116、32、115、101、99、111、110、100、97、97、114、105、108、121、46、32、32、42、47、10。79,85,84,80,85,84,95,70,79,82,77,65,84,40,101,108,102,51,50,45,105,51,56,54,41,10,71,82,79,85,80,32,40,32,47,117,115,114,47,108,105,98,51,50,47,108,105,98,99,46,115,111,46,54,32,47,117,115,114,47,108,105,98,50,108,105,98,99,46,115,111,46,54,32,47,117,115,114,47,108,105,98,51,50,47,47。108、105、98、99、95、110、111、110、115、104、97、114、101、100、46、97、32、32、65、83、95、78、69、69、68、32、40、32、47、117、115、114、47、108、105、98、51、50、47、108、100、45、108、105、110、117、120、46、115、111、46、50、32、41、32、41、10]、nom(标签),([47,42,32、71、78、85、32、108、100、32、115、99、114、105、112、116、10、32、32、32、85、115、101、32、116、104、101、32、32、115、104、97、114、101、100、32、108、105、98、114、97、114、121、44、32、98、117、116、32、115、111、109、101、32、102、117、110、99、116、105、111、110、115、32、97、114、101、32、111、115、32、97、114、101、32、111、115、32、97、114、101、32、111、110、99、116、105、111、110、115、32、97、114、101、32、111。110、108、121、32、105、110、10、32、32、32、116、104、101、32、115、116、97、116、105、99、32、108、105、98、114、97、114、121、44、32、115、111、32、116、114、121、32、116、104、97、116、32、115、101、99、111、110、100、97、114、105、108、121、46、32、32、42、47、10、79、85、84、80、85、95。70,79,82,77,65,84,40,101,108,102,51,50,45,105,51,56,54,41,10,71,82,79,85,80,32,40,32,47,117,115,114,47,108,105,98,51,50,47,108,105,98,51,50,47,108,105,98,99,46,115,111,46,54,32,47,117,115,114,47,108,105,98,51,50,47,108,105,98,99,95,110,111,110、115、104、97、114、101、100、46、97、32、32、65、83、95、78、69、69、68、69、68、32、40、32、47、117、115、114、47、108、105、98、51、50、47、108、100、45、108、105、110、117、120、46、115、111、46、50、32、41、32、41、41、41、10]、上下文(\";魔术\";))]})";

//在`delf/src/lib.rs`pub struct HexDump<;';a>;(&;#39;a[U8]);使用std::fmt;Iml<;';a>;fmt::debug for HexDump<;';a>;{fn fmt(&;self,f:&;mut fmt::Formatter)->;fmt::result{for&;x in self。0。ITER()。拿(20){写!(F,";{:02x}";,x)?;}OK(())}}。

NOM::ERR::错误是可以恢复的。也许另一个分支可以工作(如果我们连续尝试几个解析器),或者我们只需要获得更多的输入。

Nom::Err::失败是无法恢复的-我们已经尝试了所有的解析器,更多的输入是没有帮助的,只是有些地方是完全错误的。

在任何一种情况下,它们只包装一个NOM::Error::VerboseError,它本身可以包含多个Nom::Error::ErrorKind,以及相关的输入片。

我知道,我知道--那太多了!但多亏了模式匹配,它并不是那么糟糕。

我们可以在短短几行内制作出更好的错误打印机。我们将直接在Delf中添加它,这样我们就不会在ELK中使用NOM类型:

//在`delf/src/lib.rs`impl File{pub FN parse(i:parse::input)->;parse::result<;self>;{let Original_i=i;use nom::{error::{ErrorKind,ParseError,VerboseError},number::Complete::le_u16,err,};let(i,x)=le_u16(I)?;匹配self::try_from(X){OK(Res)=>;OK((i,res)),Err(_)=>;Err(Err::Failure(VerboseError::FROM_ERROR_KIND(Original_I,ErrorKind::alt,),}。

//在`elk/src/main.rs`fn main()->;result<;()中,Box<;dyn error>;>;{//省略:抓取第一个参数let file=Match Delf::File::parse_or_print_error(&;input[.。])){Some(F)=>;f,None=>;std::process::exit(1),};println!(";{:#?}";,file);}。

$Cargo b-q$./target/debug/elk/usr/lib。

.