嵌入式系统那些事-arm运行时文件解读

0、背景

        前面笔者从arm的指令集开始,依次解读了文件视角下的arm汇编文件、arm elf文件,其目的是帮助读者系统地了解arm体系结构下指令转换和执行过程。本文是文件视角主题下的最后一篇文章,将对arm运行时的文件进行解读,即被加载后的代码如何被一步一步执行的。这个部分的核心是函数调用栈,尽管系统已经帮我们做了这件事,但是了解其内部的机制和原理,对我们来说仍然是很有裨益的,特别是解决一些踩内存的问题,可以让我们抽丝剥茧,找到问题根源,而不是靠猜测碰运气。本文将详述函数调用栈的基本原理,并在最后一个小节汇总我们几篇文章综述整个arm文件的生成、加载和执行流程。

1、运行时文件结构

        与前面几篇文章类似,上来我们先看一下arm的运行时文件包含哪些内容,此处笔者将其总结如下图所示。图中运行时文件是整个arm执行流程的最后一步,在上一篇文章【1】中生成的elf文件此时已被加载到了进程虚拟地址空间的只读代码段部分,下一步就会被调用到栈区域执行。在此处我们可以停下来想一下,为何要用栈这种数据结构,程序执行的最小单元函数是如何被映射到栈中的(栈映射),函数之间的调用过程是怎样的(函数调用栈),函数是如何入栈和出栈的。本文将围绕这些问题一一解答。        

         在开始解读函数调用栈之前,让我们先来回顾一下上一篇文章【1】的进程空间的布局,笔者重新拿过来进行二次加工,如下图所示。图中自下而上依次是保留段,只读代码段和可读可写段,后面这两个段【1】中详细介绍了内容的加载过程,此处不再介绍,此时代码是静态的,还未被执行。红色的箭头指示,这部分的静态代码会被加载到栈区被执行,到了这一步代码才算真正地跑起来。来到图中的左边,白色的箭头指示地址是由高到低变化的,内核空间和栈空间被放到高地址处,笔者将其圈到了一起,这一部分是由系统管理和维护的;而低地址处除了前面两个段(只读代码段和可读写数据段)外,还有堆空间,这三个部分笔者也圈到了一起,表示是程序员可以看到和管理的部分。在进程的设计中将这两大部分分离开,可以让我们更加方便的访问内存空间,同时可以对内存做保护。

2、函数调用栈

2.1 函数调用数据结构和栈映射

        回到我们的主题函数调用栈,大家想过没有,为何要使用栈这种数据结构,为何不用队列或者其他的数据结构来实现函数的调用执行。笔者将函数的调用过程总结在如下图左边的部分,三个函数,A调用B,B调用C,我们发现实际上首先被执行的应该是最后被调用的函数C,只有函数C被执行完,拿到了结果,函数B才能执行,函数A也是一样的,这样的一种调用和执行的过程有没有很像一种“后进先出”的结构,而这就是栈这种结构的特点。在调用时,函数依次入栈,当到达最后一个函数的时候,再依次从栈顶执行。

         函数的调用关系我们已经清楚了,可以用栈存储,那函数的基本构成元素,在栈中是如何被映射的呢?如下图所示,同一个函数的不同元素,在映射到栈中时被分成了两拨,一拨是在上一个函数栈帧中,映射的元素包括函数名(函数入口地址)、输入参数和输出参数;另一拨在当前函数栈帧中,主要包括函数的局部变量、下一调用函数输入参数、调用的函数和下一条执行的表达式。前一拨是用来调用寻址的,后一拨是来执行,然后调用另外的函数。函数名对应的地址在elf文件中已经被分配好了,而函数中的其他元素都是在执行时才被加载到栈中的。

         看完上面的介绍,似乎还少了一个关键的元素,就是当执行完当前被调用的函数后怎么返回到上一个函数继续往下执行?如下图所示,图中函数被依次调用,入栈,当执行完成后就会依次出栈,返回到该函数被调用的位置继续执行下一条指令。没错我们需要将被调用函数的返回地址记录到栈中,只有这样才能找到回去的路。

        至此,在一个函数调用过程中采用的数据结构,函数每个元素的作用,以及在过程中需要保存哪些临时变量(如返回地址),就已经介绍完毕。下面我们把本节开始举得例子,函数ABC之间的调用,用我们刚才分析的结构总结一下,如下图所示,三个函数依次入栈,按照上面介绍的映射关系,将函数的各个元素放到对应的位置,并且定义了每个函数的返回地址。函数C在栈顶,将被第一个执行,而后返回到调用的B函数的位置,同时进行弹栈,函数B执行完后也有同样的过程。

2.2 入栈和出栈过程

        这一小节,我们进一步抽象一下函数调用的入栈和出栈的过程。从角色上看分为两个,一个是调用者,一个是被调用者,这两个角色分别有自己的栈区域,按照调用的先后顺序,调用者在栈底位置,被调用者在栈顶位置。在前面的进程空间中我们知道,栈底在高地址的位置,栈顶在低地址的位置,栈是往低地址方向增长的。每个函数都有自己的栈底即EBP指示的位置,可以通过偏移访问栈帧中的所有元素,栈顶的位置在被调用者的地方,即ESP。下面的两张图描述了整个入栈和出栈的过程。

 

 3、端到端函数调用运行基本过程

        函数调用栈的原理理清以后,笔者将文件视角下的arm系列串起来,从源代码文件到编译后的elf文件,再到加载到进程空间的对应的段,最后从对应的段调用到栈空间执行,这样一个全流程总结下来,以一小段代码的为例,梳理出如下图所示的流程。图中的源代码部分调用函数就是main,被调用函数是TEST_HelloWorld,这个函数就是打印helloworld。main函数中还定义了两个局部变量,TEST_HelloWorld调用完后还跑了一个while循环。

        上面的源代码文件编译链接成elf文件后,两个函数被分配了地址,同时还有一个动态链接的_start函数被链接进来,这个elf文件可以被加载到进程空间中,按照【1】中介绍的,被暂时放置到只读代码段,进程运行时,_start函数被优先加载到栈空间中,然后由其调用main函数,再由main函数调用TEST_HelloWorld,按照本节介绍的方式入栈和出栈。这样一个完整的执行过程希望对读者有所启发

4、小结

        本文是文件视角的arm的终结篇,笔者在梳理这个系列的4篇文章时也收获很大,希望对读者也有所启发,能够真正看到这样的一个系统是如何运作的。后续笔者将继续从其他视角再深入探讨arm,敬请期待。

【参考文献】

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

猜你喜欢

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