嵌入式系统那些事-文件视角下的arm elf解读

0 背景

        这篇文章笔者继续文件视角下的arm elf的解读,elf(Executable Linkable Format)是在linux下使用的一种可执行文件格式,我们的arm架构上通常都是部署的linux系统,因此elf文件也是跟arm架构强相关的一种文件格式。通过对其进行解读,可以帮助我们了解这样的一个文件是如何生成的,内部的结构是怎样的,如何在嵌入式的arm设备上加载运行起来,从而加深对整个系统运行机制的理解。当我们遇到一些系统级的问题,如系统的性能、应用程序的编排、组件的聚合等问题,就可以结合本文的elf运作机制去分析,相信会让你更清楚地看到全貌,解决起问题来也更加得心应手。

1 elf文件的生成

        如下图所示是elf在arm文件视角下所处的位置,从图中可以看出elf的上一级文件是汇编文件,通过编译链接后就可以生成elf文件,下一级文件是运行时文件,elf可执行文件是可以直接加载运行的。来到elf这个主题内部,我们将elf所涉及的机制梳理成图中的5个模块,分别是elf是怎么生成的、elf文件的结构是怎样的、生成的elf可执行文件怎么加载、elf文件都包含哪些类型以及elf的应用场景。显然前4个模块是elf应用的基础,也是本文我们重点解读的方向。

         从上面的纵览图,我们已经知道elf有不同的文件类型,分别是目标文件、静态或者动态的链接文件、可执行文件。前两类文件属于中间的文件,在我们日常的工作中,可能接触静态或者动态的链接文件比较多,当我们在完成一个应用特性的时候,就要考虑将这个特性以何种形式提供出来,是直接提供可执行的文件,还是作为一个更高阶的应用程序的一个组件存在。从架构的角度看,过多的可执行文件虽然会带来一定程度的解耦和灵活性,但是维护的成本却也随之水涨船高,因此我们通过会考虑将某些特性作为lib组件或者.so组件提供。那这些文件是怎么生成的呢?

        如下图所示总结了从一个c代码实例生成可执行elf文件的全流程。这个过程对使用更高级语言的同学来说已经由IDE集成开发工具完成了,不需要再去关注,但是作为嵌入式开发的同学,特别是要理解arm的运行机制的同学,就需要了解这个过程。图中的代码A实现了main函数,在其中会调用代码B的打印helloworld的函数,还实现了输入一个应用程序名,然后创建一个进程运行这个应用程序的功能。

        首先A和B这两个c文件会进行预编译,这里典型的就是我们定义的宏就会被展开,并且将包含的文件,如头文件插入到.i文件中;然后进行编译,编译的过程中会对文件进行词法分析、语法分析和语义分析,保证我们写的代码符合语法规则,如果代码中有语法错误也会报出来,最终这一步会生成汇编.s文件;汇编文件再通过汇编的方式生成目标.o文件,我们在看编译生成的中间文件中经常会看到每个c文件都会对应一个.o的目标文件,通过一定的规则,组合成静态或者动态的链接文件,再加入运行时库,就可以将这些目标文件组合成可执行的elf文件。这个组合的的规则是怎样的呢?

        如下图所示是目标文件和运行时库组合成一个可执行的elf文件的过程。组合的基本规则是将具有相同属性的部分放到一起,比如说可执行elf的代码段,是由目标文件A和目标文件B,再加上运行时库的代码段(.text section)组合而成,依次类推,数据段、bss段、只读数据段等也分别由各目标文件的对应部分组合而成,只不过还会增加一些段,比如使用的c库,所有的符号构成的符号表等。

         上面将具有相同属性的段组合到一起的好处是,可以节省内存空间中对页的开销。举个例子,假设目标文件A的代码段大小为5000 byte,内存一个页的大小为4096字节,那需要两个页去存储代码段,另外一个目标文件B的大小为512字节,加起来就需要三个页存储,但是如果将两个目标文件的代码段合到一起,就只需要两个页,很显然这极大地节省了空间访问,还有一个好处,将相同属性的段放到一起,也方便对其进行访问。至此一个elf文件的生成过程我们已经梳理完成,接下来,我们拆开看看elf文件内部的构造是怎样的。

扫描二维码关注公众号,回复: 14699288 查看本文章

2 elf文件结构解读

        如下图所示,是我们举例的文件通过readelf命令读取的elf文件结构,在目标文件链接和加载elf文件时,会按照这张图提供的信息进行检查、组合、加载和运行。图中的elf文件结构从链接视图和加载视图两个角度进行分解,链接视图下会将代码和数据生成不同的section,每个section会有对应的地址,最终生成一张符号表symtab,其中包含了所有符号和对应的地址映射。加载视图则将section中具有相同访问属性的段如只读的、可读可执行、可读可写的组合到一起,生成segment,便于操作系统加载运行。最后包含的section信息和segment信息会被打包在elf头中,再加上魔术字、文件类型(可执行或者可重定位)、数据是大端还是小端、系统架构是arm还是x86、入口点地址等信息,就可以构成一个完整的elf文件。

       图中的elf头信息,是文件链接后生成的,在文件被加载的过程中用来做校验。首先可以用来确定是否是elf文件,在magic中,开头的4个字符7f 45 4c 46分别代表del e l f,通过判断可以确定加载器,校验文件是否正确;其次文件的一些属性信息,如数据大小端,系统架构,类型等信息可以进一步确定文件运行时环境是否满足;最后上面的文件正确、环境满足后,就可以按照segment和section的信息,将文件加载到环境中,并跳转到入口点地址,也就是代码段的地址处开始运行。

       图中的链接视图,文件的section(主要包含代码段、数据段、bss段)、.dynsym(库函数)、.rela.plt(库函数可重定位,主要生成库加载函数地址)、.rela.dyn(同rela.plt)、dynamic section(动态链接库节点)这5个部分构成.symtab(符号表,包含所有符号和偏移地址)表。这张链接视图将所有的符号和地址信息生成在一张表中,便于后续进行加载。

        图中的加载视图,会按照section到segment的映射,将不同访问属性的section放到对应的段中。比如.text .rodata .init等具有只读或者可读可执行的section被放到了segment的LOAD字段;.data .bss等具有可读可写的section被放到了segment的另一个LOAD字段;其他的section也根据不同的访问属性被放到segment的9个段中。笔者按照地址信息将这9个段进行排列,可以很明显的看出各个段之间的关系,比如具有跟代码段相同的访问属性的段PHDR、INTERP、NOTE、GNU_EH_FRAME,被放到了一个地址范围内;跟数据段相同的访问属性GNU_RELRO、DYNAMIC也被放到了一个地址范围内。这样的映射方式非常方便直接将文件映射到内存不同的页,下一节我们将介绍这个加载映射过程。

3 elf文件加载过程

        elf文件是如何加载到内存中并运行的呢?如下图所示,是在一个bash进程下创建elf进程和加载elf文件到进程虚拟地址空间的全流程。通过对这个过程的了解,可以帮助我们理解系统的运行机制,知道一个文件是如何被加载到内存,然后由CPU调用运行的,这也是我们这篇文章的根本目的,通过对elf文件的解读和加载运行,深入理解arm架构。

        图中在bash命令行下运行一个test.elf文件,bash进程首先会创建一个test.elf的进程,生成pid,下面就是加载这个文件到进程空间,运行该文件。接下来bash进程继续调用接口execve,系统也切换到内核态;接着调用sys_execve,对execve传进来的参数进行检查,如检查运行的文件名是否为空,检查通过后,就可以调用do_execve。在这个函数中,会首先找到test.elf文件,然后读取文件的头128个字节,基于魔术字判断该文件是否为elf文件,如果是,则使用加载函数将elf中的segment映射到test.elf进程空间中。

        加载函数进来先是校验elf的头部,获取文件的运行信息,判断是否满足加载条件,如果满足,就会依次将elf文件中代码段的LOAD和数据段的LOAD映射到进程地址空间的只读段和可读可写段,最后将dynamic段中的fini地址设置到EDX寄存器中,入口地址设置到EIP寄存器,依次返回调用栈,从内核态切换回用户态后,就可以进入入口地址执行test.elf文件。

4 小结

        至此,本文对elf的解读完成,这篇文章从elf文件的生成,内部结构和加载三个角度进行梳理,以期帮助读者理清整个流程,加深对arm架构和系统的理解。后续笔者将从进程的视角解读arm架构下的运行时文件,敬请期待。

猜你喜欢

转载自blog.csdn.net/linus_ben/article/details/124550999
今日推荐