最后更新2021/12/14
注:以下及以后本系列都是个人对相关技术在此时此刻的理解和研究,会根据学习深入,不断修正,但过去历史文章不见得会被(及时)修改订正,见谅。当然欢迎读者批评指正,本人虚心接受,但什么时候把文章修改正确,不好说。
============================================
qemu-linux-use利用linux binfmt执行机制(或者说代码)实现了对ELF的解析和加载。
判断文件类型的机制是统一的,binfmt提供了两个方案:扩展名和MAGIC魔数。扩展名很简单,文件命名就代表了自身格式,例如exe, com;魔数其实跟文件名扩展名方案没啥大区别,只是将类型放到了文件头,确切来说,是最开始的若干个字节,例如0x1df是xcoff32格式,'ELF’是elf格式。至于为啥有的是hex数,有的是字母?其实,本质上都是hex,只是这个hex对应到ascii码表恰好是有意义的字母而已。起名(魔数)的这些人也不是那么无趣或计算机呆子,自然会想一些有意义的数字以便让后人容易识别。
可以对比一下,扩展名在不打开文件就能看到,放在文件meta data里,文件复制的时候很容易被修改,一般是给“人”看的,不是给计算机用的。如果是魔数,则包含在文件数据之内,更改就不太容易,同样,读取也更麻烦,一般是给计算机处理用的。基于此点,对于二进制文件,魔数会优先于扩展名,或者更看重魔数;而文本文件类的,则更在意扩展名,因为文本文件的直接对象是人,而非计算机。一个普通文件,通常扩展名,魔数兼备,这样就皆大欢喜。如果不方便如此,那就最好采用适合它的直接使用者的一种方案。如果都没有呢?也不是不可以,那就要使用者“强迫”把它作为某种文件类型去解析、打开。
例如shell文件,一般起名xxx.ksh, xxx.sh等等,这就是扩展名方案。同时,也有魔数方案,就是我们熟知又大都不知所以的在shell文件第一行写上:#!/bin/ksh,这个东东其实就是魔数。由于设计者优势,魔数方案的创立者unix大神们总要有特权,先把方便实用的字母表达占据上。这个魔数一语三关:
- #! 表示我是个shell脚本
- #通常表示后面的是非执行信息,这样程序在解释执行的时候,就能自动跳过,处理方便
- /bin/ksh表示了解释本文件的shell是哪个。最终用一个解释器入口,就能自动处理任何shell
由于历史发展久了,shell也烂了大街,现在已经没人注意这个#!魔数了,没有它,其实OS省缺找到了对应的shell,然后执行,也都没啥事,隐含着,计算机帮你设定了省缺shell,省缺执行等等。
对任何可执行文件,Linux采用了统一的读magic和文件头(magic在文件头结构里面)方案,用一个bprm结构,把文件前128字节都读到buf里面,然后根据定义的魔数去判断是否付合,判断相符之后,就会转交给相应挂接的加载、解释程序。qemu中由linux_load/loader_exec()完成判断,然后去call load_xxx_binary()
- ELF magic:0x7f 0x45 0x4c 0x46 —> 0x7f ‘ELF’
- xcoff magic: 0x01 0xdf —> 32bit xcoff
- xcoff magic: 0x01 0xf7 —> 64bit xcoff
====================================
只检查magic就OK么?不,还有更多与ELF有关的参数要验证,都在_check_ident()里面,再次检查magic,类型(用于判断elf,目标cpu版本等等。
return (ehdr->e_ident[EI_MAG0] == ELFMAG0
&& ehdr->e_ident[EI_MAG1] == ELFMAG1
&& ehdr->e_ident[EI_MAG2] == ELFMAG2
&& ehdr->e_ident[EI_MAG3] == ELFMAG3
&& ehdr->e_ident[EI_CLASS] == ELF_CLASS
&& ehdr->e_ident[EI_DATA] == ELF_DATA
&& ehdr->e_ident[EI_VERSION] == EV_CURRENT);
前4个字节是ELF的Magic Number,固定为7f 45 4c 46。
第5个字节指明ELF文件是32位还是64位的。
第6个字节指明了数据的编码方式,即我们通常说的little endian或是big endian。little endian小端低位字节在前,或者说低位字节在低位地址,比如0x7f454c46,存储顺序就是46 4c 45 7f。big endian大端高位字节在前,就是高位字节在低位地址,比如0x7f454c46,在文件中的存储顺序是7f 45 4c 46。
第7个字节指明了ELF header的版本号,目前值都是1。
第8-16个字节,都填充为0。
xcoff对应检查就比较简单,因为没有大小端标记,其源自AIX,都是大端的;xcoff用了两个magic标记32位还是64位格式,因此也不存在额外检测(到这个阶段,早就定好了是32位还是64位);最后版本,也没有。xcoff有一个类似作用的 f_timdat, f_flags。
f_timdat 是日期和时间戳,但一般都不用,总是0;
f_flags包含的内容比较多,其功能与ELF的ELF_CLASS差别比较大,暂且不分析;
基于这个分析,xcoff没有必要去分析ident(因为没这个结构),因此直接删掉、跳过:elf_check_ident(ehdr)。
由于ELF存在大端和小端两种,因此elf_check_ident被执行了两次,中间由bswap_ehdr做了交换,以便大小端不对而导致判断错误。都删掉!我删除,我快乐,我擅长。
由于bprm结构只预读了文件头128byte,而再下一步,需要分析更多的文件头数据,这些数据可能已经在128byte中,也许不够,要预先读进来,因此需要先计算空间要多大,读进来多少。
同理xcoff也要这么做,但有点不同,很多数据都是在auxiliary head里面,虽然auxiliary长度是变化的,但大概应该在128字节之内,所以计算上要根据filehdr和auxhdr两个结构计算总共需要多少空间。计算方案是(当前全部需求):
filehdr + auxhdr + sectionhdr * number of sections
由于预读的128字节可能已经包括filehdr + auxhdr +,因此做一些判断,还差多少,如果已经够了,那就不用申请空间,如果不够,那就只申请后面section header的空间(filehdr + auxhdr肯定小于128byte的);
其实,我们知道filehdr是20bytes,auxhdr是72bytes(均为32位,64位以后再考虑),因此就不用判断了,直接计算为section header准备空间,并读进来。
顺便说一句,由于有大小端问题,读进来的数据都要先经过swap_xxxs()走一遍,将其中必要的数据进行大小端转换,变得与目标机一致;有s结尾的指更改目的地址的数据,没s的,返回变化后的数据。tls看起来是和32位,64位有关,如果已知32位还是64位并且不想随宿主机自动变化,则可以直接用定义好的。
今天就到这里吧,收工。明天看map memory。
晚上睡不着,又加了会班,多看了两眼,发现此处ELF和XCOFF差别还是很大,例如xcoff没那么多section,map也相对简单一点,大部分为ELF做的map操作都要删掉。其实,代码变化越大,无论增删,都变得复杂了,看来memory map不是一两天能搞定的。再接再厉,自我催眠一下。