> 调用栈解析概念:
任意体系结构的CPU,都设计了一套通用寄存器、状态寄存器及其他控制寄存器,用以维系系统的正常运行。函数调用过程中,CPU一般都需要处理几件事情:保存母函数现场(寄存器值),将被调用函数的返回地址存储到相应的寄存器中(MIPS ra寄存器,ARM的LR寄存器),以确保被调用函数执行结束后,系统可重新跳转回正确的位置,并可拥有正确的执行环境继续运行。由于CPU通用寄存器甚至状态寄存器在不同的函数间是复用的,这就决定了在函数调用过程中需要将当前的CPU状态临时保存到一段存储空间里,并在被调用函数返回后从存储空间取出相应的数据恢复CPU状态上下文。
用于存储CPU状态的空间为堆栈stack,函数调用过程中用来保存CPU状态的存储空间就叫调用栈。
ARM体系结构对64位的支持从ARMV8开始,V7及以前的版本并不支持。V8的寄存器及指令集都发生了较大的变化,导致V7之前版本的调用栈解析工具不再可用,需要或从新设计实现一套。
Ø 调用栈Q&A
Q1: 收否所有函数都有调用栈?
并非所有函数都有调用栈,例如层次关系最深的函数,就可以直接使用CPU状态,而不需要关心调用栈。可以把有调用栈的函数称为非叶子函数,而无调用栈的则称为叶子函数
Q2:正常情况下是否需要感知函数调用栈
不需要。调用栈无处不在,但一般情况下程序员不感知其存在。
Q3:调用栈的内存管理是怎样的的
调用栈位于系统的栈section内,一般大多数系统里栈都是低地址方向生长的;每个函数的被调用的时候,系统开辟堆栈;而在函数结束的时候,释放堆栈。
Q4: 调用栈在函数运行过程如何管理?
调用栈的管理主要由编译器及处理器体系结构决定;在编译阶段根据函数临时变量规模、是否调用子函数等因素计算函数调用栈规模。处理器一般有一个SP通用寄存器器指针,用于指向某个函数的栈底(最低地址)。在函数运行过程中,关于堆栈的访问都通过SP指针+-offset的方式来访问堆栈(存储或读取)
Q5: 调用栈的大小是由谁决定的
调用栈的大小由函数的实现决定,如函数临时变量数、函数是否调用子函数等。编译阶段可决定函数调用栈的规模
Q6:调用栈是动态生长的还是静态划分的
从系统内存管理角度来会所,函数调用栈肯定是动态生长的;
而从一个函数的调用栈本身来说,动态或静态两种方式都存在。如MIPS的堆栈一般都在函数入口处一次性开辟:addiu sp sp –x; 而ARM 32的函数堆栈,则在函数内部也是动态生长的;到了ARM 64,函数调用栈又变成静态规划的了。
Q7:为何要进行调用栈解析
系统运行过程中,最直接的数据都保存在堆栈中。通过解析调用栈,对分析系统错误信息、快速定位问题有极大的价值。几乎所有操作系统的异常处理中,除了记录异常状态寄存器和堆栈信息外,都会包含调用栈轨迹分析的结果。最起码,通过调用栈解析得到的轨迹,可知道系统是在哪个环节发生了错误。
Ø 调用栈解析设计问题分解
要弄清楚如何进行调用栈解析函数轨迹的设计,需要对几件事情有基本的概念和了解。
1)函数返回地址的获取
函数返回地址,不外乎可从两个途径获取:返回地址寄存器(ra、LR之类),或堆栈。至于具体从哪个途径获取需要具体问题具体分析。叶子函数相对简单,可直接从寄存器获取,因为叶子函数根本没有堆栈。非叶子函数,则相对复杂一点,两种可能都存在。要看导致系统crash的指令跟LR寄存器堆栈存储指令间的先后关系而定。一般来说,现代体系结构及编译器的设计,都会在函数最开始的几条指令里完成堆栈开辟可函数返回地址的存储,因此取堆栈里的数据八九不离十是正确的。如果要进行指令先后关系的梳理,那么系统设计就会比较复杂。由于ARM64目前看来不涉及这一点,因此本文不会就这种情形详细展开。因此非叶子函数可简单认为,从堆栈获取返回地址即可。
2)跟函数调用栈分析的关键指令有哪些
不外乎以下几条:
堆栈开辟指令->可获取堆栈大小,便于计算上一层函数的栈底指针:SP_Pre = SP_base + Sp_size
返回地址堆栈存储指令->获取函数返回地址在对战的存储位置
这是最基本的两条指令,在某些体系结构里,还需要涉及到函数调用指令,如BL;以获取被调用函数的入口地址。在这种情况下,如果系统里设计一种类似符号表的机制,则不需要分析这类指令,通过查找符号表即可,可极大地降低工作量。
3)如何分析并判断指令
请查看处理器手册中的指令编码格式!
4)指令查找过程中如何防止越界?(指针++--,难免会跑到其他函数地盘去了)
最简单的方式,就是用符号表界定该函数的上界和下界。简单又安全。用指令特点方式虽然也可,但一定的概率会失败,不再展开这一方式。
OK,调用栈解析的设计问题分解至此基本完成,是不是很简单?下文简单附加ARM64调用栈解析的实现流程。
首先,ARM64的函数调用栈特点如下:堆栈静态开辟、堆栈低地址方向生长、非叶子函数入口处会开辟堆栈并保存返回地址、函数调用指令执行时LR寄存器会保存被调用函数的返回地址、函数栈内严格遵循load、store的方式存储及访问堆栈。
调用栈解析处理流程如下:
1)系统crash处理模块提供异常上下文信息
2)从异常指针处开始向前回溯,查找该函数是否有开辟堆3a)无堆栈的叶子函数,直接取LR寄存器值;把当前函数栈底当成上一层函数栈底继续分析
或
3b)获取堆栈大小,计算上一层函数栈底。
4)查找堆栈存储返回地址指令,获取堆栈偏移值;从堆栈得到返回地址
5)重复3-4步骤,直到获取到足够多的信息。