在嵌入式设备开发中。内核为内核模块的函数栈追踪已经提供了非常好的支持,但用户层的函数栈追踪确没有非常好的提供支持。
在网上收集学习函数栈跟踪大部分都是描写叙述INTER体系架构支持栈帧的实现机制。或者直接给出glibc的现成库函数。
但假设开发环境是broadcom相关方案,通常使用的是MIPS32的体系架构,而且C库使用的是更小的uclibc。尽管MIPS32体系架构中也定义了栈帧寄存器s8(类似于Inter体系架构中常见的ebp寄存器),但经过GCC编译器的优化选项控制后,通常在O1以上的优化就已经去除了s8栈帧的使用,所以给函数栈追踪的实现就带来了一点点小麻烦。
通常情况下。函数的返回值还是使用压栈实现,所以仅仅要知道了函数每次调用时,返回值(ra寄存器)与当前栈顶(sp寄存器)的偏移量,就能够实现函数栈追踪,对函数反汇编能够了解到这个偏移量通过sw ra, xxxx(sp)能够得到,还有一个难题是怎么得到函数每次调用时的当前栈顶(sp寄存器),通过函数反汇编能够了解到addiu sp, sp, xxxx指令就是每次给函数分配当前栈帧大小的,所以仅仅要得到这个栈帧大小然后用sp进行差值计算就能够往回推出上一个sp的值了。还剩下最后一个问题,一步步获取上一个函数的栈顶,什么时候结束?答案就是仅仅要栈顶sp的寄存器为0就追踪到头了。通过这些分析我们能够了解到当前实现机制根本没用到程序的栈段内容。对,全然从程序指令段做为线索,获取程序执行时指令一步步找出函数栈的调用。看起来非常酷。但现实也比較残酷,比方我们上面分析说要得到栈顶sp的寄存器为0表示追踪到头,我当前调试环境在__start(能够參考一些链接载入的相关技术资料了解,其他main并非c的起始函数。__start才是c语言的起始函数)函数中使得sp为0的汇编指令为addu ra, zero, zero,我还不清楚其他编译器是否会使用其他指令方法进行设置。所以我们当前实现的这个函数栈追跟踪功能的实现代码并非非常标准的。假设读者有幸參考该代码,请一定要在理解上面描写叙述的原理基础上。另外须要在您自己的编译开发环境进行调试,确保这个追踪功能代码自己不会引发Crash再使用,否则那玩笑就开的大了。
剩下就不多说了。直接付上我当前的开发环境及实现源代码,以及终于在broadcom6838板上执行的測试程序的效果图。
开发环境:
实现源代码:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <signal.h> #include <ucontext.h> #define abs(s) ((s) < 0 ? -(s) : (s)) #define CALL_TRACE_MAX_SIZE (10) #define MIPS_ADDIU_SP_SP_XXXX (0x27bd0000) /* instruction code for addiu sp, sp, xxxx */ #define MIPS_SW_RA_XXXX_SP (0xafbf0000) /* instruction code for sw ra, xxxx(sp) */ #define MIPS_ADDU_RA_ZERO_ZERO (0x0000f821) /* instruction code for addu ra, zero, zero */ void getCodeIn(unsigned long codeAddr, char *pCodeIn, int iCodeInSize, unsigned long *pOffset) { FILE *pFile = NULL; char szLine[1000] = {0}; pFile = fopen("/proc/self/maps", "r"); if ( pFile != NULL ) { while (fgets(szLine, sizeof(szLine), pFile)) { char *pTmp = NULL; char szAddr[500] = {0}; char szCodeIn[500] = {0}; unsigned long begin = 0; unsigned long end = 0; sscanf(szLine, "%s %*s %*s %*s %*s %s", szAddr, szCodeIn); pTmp = strchr(szAddr, '-'); if ( pTmp != NULL ) { *pTmp++ = '