嵌入式系统程序调用关系分析设计方法*
2010-04-26庄克良高云岭纪向尚
庄克良 高云岭 纪向尚
(海军704厂 青岛 266109)
1 引言
在嵌入式系统开发中,由于硬件资源受到功耗、体积、环境等因素的限制,此时程序的性能分析就显得非常的重要,清晰的函数调用关系以及各函数的具体执行时间,可以有效的弥补硬件资源所带来的缺憾。
嵌入式系统程序函数调用关系设计方法在不改变待分析程序源代码的情况下,充分利用编译器的编译选项,简化插入代码的操作复杂度;然后,根据函数调用的规律进行函数入口地址的逆向追踪,同时结合编译中间文件,成功完成动态执行数据的函数名称分析工作;最后,在开源工具Graphviz的帮助下完成树状图的生成工作。
2 性能分析功能设计和实现原理
通过对程序的动态执行情况进行分析,获取整个程序的函数调用层次关系以及执行时间情况。传统的方法是通过手动在函数的进入处和退出处插入唯一的标记符号来实现。传统方法不仅过程繁琐,而且非常容易出错。
嵌入式系统程序函数调用关系设计方法[4~5]充分利用Microsoft Visual C++Complier的编译选项/Gh和/GH,通过这两个附加钩子函数的编译选项,可以方便的把自定义的_penter和_pexit函数插入到每个函数的头部和尾部。在UEFI的开发框架EDKII中,通过修改编译工具链的局部配置文件,成功地解决编译选项问题[1~3]。
为了捕获并显示函数动态执行的调用关系图,需要充分利用四个主要的元素:MS Visual C++编译工具链、编译中间文件、自行设计的中间处理转化代码和一款开源工具 Graphviz。Microsoft Visual C++Complier编译工具链主要是在程序预处理阶段在函数首尾处插入自定义的钩子函数;然后,对编译中间文件进行分析,由程序的相对地址得到对应的函数名称;接着,通过自行设计的中间转化程序将获得的动态执行情况和函数名称进行映射,对临时数据进行精简处理,输出符合DOT语法规范的输入文件;最后Graphviz软件将输入文件转化为直观的树状结构图。性能分析模块处理流程框图如图1所示。
图1 性能分析模块处理流程框图
3 性能分析功能详细设计
3.1 添加编译选项
Microsoft Visual C++Complier编译工具链是支持钩子函数编译选项的先决条件。通过/Gh和/GH这两个编译选项能够把自定义的_penter和_pexit函数插入到每个函数的头部和尾部。
3.2 获取父函数地址
针对IA-32架构函数调用过程中通过堆栈传参的特点,对切换时堆栈的内容变化情况进行分析。在父函数按照cdecl约定调用子函数的时候,参数首先由右向左压入堆栈,函数本身不清理堆栈,而是由调用者负责清理堆栈。由于参数按照从右向左顺序压栈,最开始的参数在最接近栈顶的位置。当采用不定数个数参数时,第一个参数在栈中的位置肯定能知道,只要不定的参数个数能够根据第一个明确的参数确定下来,就可以使用不定参数。IA-32处理器架构堆栈切换对应关系如图2所示。
图2 IA-32处理器架构堆栈切换对应关系图
3.3 获取动态执行数据
保存动态数据的思想是,在插入函数的入口处将获取的父函数地址记录到文件中,同时压入堆栈;在函数退出时,对堆栈执行出栈操作,并将函数出栈地址记录到文件中。对于第一次插入和最后一次退出的特殊情况进行单独处理,避免程序出现异常情况。
根据这个设计思想,插入函数伪代码为:
3.4 获取动态执行函数名称
函数名在程序中实质上为该函数的函数指针。在程序的最终执行文件中,函数名字符串已经不存在了,取而代之的就是函数地址。因此获取函数名称只有在预处理阶段获得,充分利用编译选项/Zd,即MAP文件,它表示在编译的时候生成行信息。
在MAP文件中,关键信息的默认的基地址为0x00000000。该地址在.EFI程序加载到内存执行时,会根据当前硬件平台的实际加载情况发生相应的变化,但load address与其它地址相对关系不会发生变化。这是通过分析MAP文件获取函数名称的理论基础,同时MAP文件指定了程序的数据段、地址段和堆栈段的开始相对地址。
RVA(Relative Virtual Address):相对虚拟地址,表示程序可执行文件加载到实际执行内存中时,相对于基地址的偏移位置;
Base(Base Address):程序开始加载位置的基地址,这与开始处的perfferred load address相同。
RVA+Base:虚拟地址。
从MAP文件中,可以发现InitializeLs函数在理想虚拟地址0x00000260;MainProc函数在理想虚拟地址为0x00000310;Listing函数的虚拟地址为0x00000ab0;值得一提的是,通过/GH和/Gh插入的钩子函数也在其中,函数头部自动插入的函数名字为__penter,相对虚拟地址为0x000019b0;函数退出时自动插入的函数名字为__pexit,相对虚拟地址为0x00001d90。编译产生MAP中间文件如图3所示。
图3 编译产生MAP中间文件
3.5 精简提取单元
在执行插入钩子函数的应用程序时,会创建一个名为trace.txt的文本文件。该文件中包含了一系列地址信息:每行一个地址,每行都有一个前缀字符。如果前缀是E,那么这个地址就是一个函数的入口地址。如果前缀是一个X字符,那么这个地址就是一个出口地址。
因此,如果在跟踪文件中有一个函数入口地址A紧跟着另外一个函数入口地址B,那么就可以推断是函数A调用了函数B。如果一个函数入口地址A后面跟着一个函数出口地址A,那么就说明这个函数A被调用后就直接返回了。当涉及大量的调用链关系时,就很难分析究竟是谁调用了谁,因此,一种简单的解决方案是维护一个存储函数地址的堆栈。每次在跟踪文件中碰到一个函数入口地址时,就将其压入堆栈。栈顶的地址代表最后一次被调用的函数(也就是当前的活动函数)。如果后面紧接着是另外一个函数入口地址,这说明堆栈中的地址调用了这个刚从跟踪文件处读出的地址。在遇到退出函数时,当前的活动函数就会返回,并释放栈顶元素。这会将当前上下文返到回前一个函数,由此,就可以产生正确的调用链过程。
图4演示了这个原理以及精简数据的方法。在分析跟踪文件中的调用链时,会构建一个二维连通矩阵,用来表示哪个函数调用了其它哪些函数。这个矩阵的行表示调用函数的地址,列表示被调用函数的地址。对于每个调用对来说,行与列的交叉点不断进行累加,就是调用次数。当处理完整个跟踪文件时,其结果是应用程序的整个调用历史的一个非常简明的表示,同时包含了调用的次数。时间信息同样被累加保存在行的函数属性中,表示该函数在整个程序中总共占用的时间。
图4 二维矩阵形式精简数据过程图
3.6 图形化结果输出
图形化结果的输出充分利用了开源工具Graphviz-2.20.3,Graphviz使用DOT(一种图形描述语言)描述图,通过解释工具dot生成图像文件,如图5所示。
图5 程序层次图形文件输出
4 应用前景
嵌入式系统程序函数调用关系设计方法完全通过编译器的编译选项和开源工具实现,设计中充分利用了编译过程中产生的临时数据文件。在分析过程中,对源程序不做任何改动,输出的形式能够直观生动地反映当前程序动态执行过程中的具体调用关系和时间占用比率,对于程序的进一步优化作用是显而易见的。
[1]Microsoft corporation.Visual C++MSDN[EB/OL].http://msdn.microsoft.com/zh-cn/library/c63a9b7h(VS.80).aspx,2008
[2]Framework Open Source Community.EFI_Shell_getting_Started_Guide.Version 0.31[EB/OL].https://efi-shell.tianocore.org/servlets/ProjectDocumentList,2005-06
[3]M.Tim Jones.Visualize function calls with Graphviz[EB/OL].http://www.ibm.com/developerworks/linux/library/l-graphvis,2005-06
[4]杨震伦.嵌入式操作系统及编程[M].北京:清华大学出版社,2009,5
[5]刘亚平.嵌入式系统及应用[M].北京:中国人民大学出版社,2009,1