环境
MIPS eCos
目标
根据线程stack内容,解析出从线程入口函数到最后一个函数(当前函数或线程切换时所在函数)的调用关系。
简单的说,我们希望可以自己实现一个针对MIPS架构back trace(用过GNU GDB的都知道)。
分析
首先可以肯定的是:既然子函数可以根据其自身栈帧(stack frame)信息正确返回到其父函数,那么我们也可以根据.text内容和相应线程的stack解析出相应的函数调用关系。
打断函数执行并切换出去——调用子函数
既然是根据stack frame和.text进行解析,我们需要先看下这两者的内部结构。
可以看到:每个栈帧中都会首先存储其自身返回地址(叶子函数除外),启用fp的情况下会紧接着存储栈底地址。由于每个函数的返回地址必定位于其父函数的代码段中,借助.elf文件的反汇编代码可以看到对应地址在哪个函数中。所以我们的问题就转换为:依次获取每个栈帧中存储的ra值。由于栈帧的最底部就是存储ra的地方,则问题进一步转换为获得每个栈帧的栈底地址。
以下内容分两种情况讨论。
1)使用fp/s8存储栈底地址情况下
当前栈帧的栈底是容易得知的:当前函数正在执行,其fp寄存器中的值就是当前的栈底地址,记为$fp,当前函数的返回地址就是$ra;若当前分析的函数不在执行,则其相应的寄存器值会保存在该函数所在线程的context中,可以从线程context保存处得到其值。
根据当前栈底地址得到其父函数栈帧的栈底地址为*($fp - 8),返回地址为*($fp - 4),这样以此类推就可以获得整个线程栈的函数调用关系。
2)没有存储栈底地址的情况下
可以注意到由c代码翻译得到的MIPS汇编代码中,函数的入口指令是addiu sp, sp, -NUM0这种形式,其中NUM0是指为该函数分配的堆栈大小,且函数体内不再有对sp赋值的操作(函数返回前恢复sp除外)。MIPS是定长指令(32 bytes),只要在代码段中依次向上找到这条指令就可以知道该函数的栈帧大小,再根据栈顶地址就可以得到栈底地址。
值得说明的是,
l 搜索的这条指令特征是高16位为0x27bd,低16位为NUM0;同时还可以注意到函数入口的第二条指令是sw ra, NUM0-4(sp),即高16为是0xafbf,只要你愿意,你可以通过“找到这两条指令在一起”进行判断是否找到了该函数的入口地址。但是目前的实现中我只搜索了第一条指令。
l 当前函数的栈顶地址容易得知(参考上述1)),在知道当前pc值后搜索到当前函数的入口指令,继而得到栈帧大小,加上栈底地址得到当前函数的栈底地址。以此类推,就可以得到整个线程栈的函数调用关系。
需要说明的是,当前pc如何获得?如果当前函数不在执行,则其线程在context switch时会将之保存为ra所在处;如果当前函数正在执行,则可以使用一个小技巧:调用一个函数并让它把ra值赋给传递给该函数的参数。
打断函数执行并将之切换出去——中断/异常
在eCos中,异常/中断(后面的分析不区分这两者,统称为异常)的处理是直接用汇编写的,所以它没有遵守上述情况2中描述的指令使用规则(似乎没有哪个OS会不用汇编去实现异常处理吧)。幸运的是,它只在异常处理入口处例外,它(vector.S)开辟一段栈空间(用于保存中断现场)后,又开始调用一些C/C++函数,在处理完DSR(unlock_inner函数)后有机会进行一次线程切换,当然这些调用开销包括最初的栈空间开辟都是放在线程栈空间中的(独立中断栈暂不考虑)。所以分析一个线程的栈空间时发现其位于异常处理中是很正常的。多说一句,eCos中的线程切换只会发生在两种情况下:异常处理DSR后、线程阻塞(wait semaphore, sleep等),具体负责切换工作的是unlock_inner函数。
所以,如果线程切换发生在异常处理中,那么向上回溯到异常处理入口处将无法继续向上回溯。具体的说,异常处理时的栈操作是:addiu sp, sp, -NUM0(保存中断现场),… addiu sp, sp, -NUM1(进行正常的函数调用:interrupt_end),这不像上述描述的正常情况,两者中间(称为过渡区)没有函数调用。
所以我在处理这部分时,将判断ra是否处于该过渡区中,如果是就从中断现场恢复被中断切换出的函数sp, ra寄存器值等。
需要说明的是:这种情况比较特殊,里面许多数值比如中断现场中的sp值存储地址都是根据异常处理的不同而变化,如果哪天有人修改了异常处理的实现,那么我这段代码也将随之失效。请谨记。
代码实现说明
更多细节请参考sal_os.c中SAL_OSTravThread函数,它会遍历所有线程栈的call stack,打印出每个函数调用的$ra值,其中核心函数是mips_bt。
目前我的实现基于上述第二种情况,事实上我首先实现的是第一种情况,但是不知道为何fp中的值始终错误。我没有继续深究错误缘由,是我对上述情况1的理解错误还是代码实现有误?但这个函数的实现我还保留着(注释掉的mips_bt函数),有兴趣的可以继续改进它。
目前,这个函数在多数情况下可以正常工作,但是也偶尔出现不能正常工作的情况,这是由于第二种情况的依赖没有得到保证,比如eCos中有些C++函数没有遵循上述依赖。
更多细节参考source code:
/* * Author: Randy * Create: 2013-03-11 */ #include <cyg/kernel/kapi.h> #include <cyg/hal/hal_cache.h> #include <stdio.h> #include <stdlib.h> static int mips_bt(unsigned int pc, unsigned int sp, void *pEntryPt) { //#define BT_DEBUG #define TARGET_INS 0x27bd int ret = -1; extern unsigned int _ftext; extern unsigned int _etext; unsigned int text_start = (unsigned int)&_ftext; unsigned int text_end = (unsigned int)&_etext; unsigned int cur_ra; unsigned int cur_sp = sp; unsigned int cur_pc = pc; short tmp_data = 0; unsigned int cur_sf_size = 0; int enter_excep_cnt = 0; unsigned int normal_func_sp = 0;//the sp of the normal function call stack, which is the excepiton process's stack bottom unsigned int normal_func_epc = 0; unsigned int normal_func_ra = -1; bool need_check_ra = false; if((unsigned int)pc < (unsigned int)text_start || (unsigned int)pc > (unsigned int)text_end) { INF("PC value is not in .text segment!\n"); return ret; } INF("PC:0x%x, SP:0x%x, ThreadEntryFunction:0x%x\n", pc, sp, (unsigned int)pEntryPt); while(((unsigned int)cur_pc > (unsigned int)text_start) && ((unsigned int)cur_pc < (unsigned int)text_end)) { if((((*(unsigned int *)cur_pc) >> 16) & 0xffff) == TARGET_INS) { tmp_data = (short)((*(unsigned int *)cur_pc) & 0xffff); #ifdef BT_DEBUG INF("addiu sp, sp, %d=0x%x\n", tmp_data, tmp_data); #endif if(tmp_data > 0) { cur_pc -= 4; #ifdef BT_DEBUG INF("warning: check here, maybe wrong!\n"); #endif continue; } cur_sf_size = (unsigned int)(-tmp_data); /* TEMP test for avoiding exception process */ if((32 == cur_sf_size) && (cur_pc > (unsigned int)__default_interrupt_vsr) && (cur_pc < ((unsigned int)__default_interrupt_vsr + 0x1b0))) { if(enter_excep_cnt >= 1) { INF("thread info shows that: enter exception more than ONE time, there must be somthing wrong, in parsing or ...\n"); return -1; } INF("\n ***** REAL Thread Stack Info Begins: *****\n"); cur_sp += cur_sf_size; normal_func_sp = *(unsigned int *)(cur_sp + 116); normal_func_ra = *(unsigned int *)(cur_sp + 124); normal_func_epc = *(unsigned int *)(cur_sp + 144); //lw k0,144(sp) => return PC <<==>> while 124(sp) => ra cur_sp = normal_func_sp; cur_pc = normal_func_epc; INF("exception stack base: 0x%x\n", cur_sp); INF("In normal function stack, sp=0x%x, ra=0x%x, pc=0x%x\n", normal_func_sp, normal_func_ra, normal_func_epc); INF("Address:0x%x in one function..\n", cur_pc); need_check_ra = true; enter_excep_cnt++; continue; } /* TEMP test end */ cur_sp += cur_sf_size;/* cur_sp is now the fp of current stack frame */ #ifdef BT_DEBUG INF("current SP:0x%x\n", cur_sp); #endif cur_ra = *(unsigned int *)(cur_sp - 4); /**/ if(cur_ra == normal_func_ra) { INF("ra value is equeal between exception's stack and thread's stack, this proves exception happened after operation of saving ra\n"); need_check_ra = false; } else if((true == need_check_ra) && (cur_ra != normal_func_ra)) { cur_ra = normal_func_ra; need_check_ra = false; } cur_pc = cur_ra - 8; /* ra = pc + 8 when call child function */ INF("Address:0x%x in one function..\n", cur_pc); } else { cur_pc -= 4; } if(cur_pc == (unsigned int)pEntryPt) { INF("this thread's call stack has been parsed completely!\n"); return 0; } } return -1; } /* //this function(using fp/s8 register) should work well, however, it always got the wrong fp value, so... static int mips_bt(LPD_U32* stack_bot, LPD_U32 stacksize_used, LPD_U32 cur_ravalue, LPD_U32 cur_fpvalue, LPD_U32 cur_spvalue) { extern unsigned int _ftext; extern unsigned int _etext; unsigned int text_start = (unsigned int)&_ftext; unsigned int text_end = (unsigned int)&_etext; int ret = -1; int stack_callayer_cnt = 1; LPD_U32* cur_fpaddr = (LPD_U32*)cur_fpvalue; LPD_U32* cur_raaddr = (LPD_U32*)cur_ravalue; LPD_U32* cur_spaddr = (LPD_U32*)cur_spvalue; LPD_U32* next_fpaddr = NULL; LPD_U32* next_raaddr = NULL; LPD_U32 cur_stackframe_size = (LPD_U32)cur_fpaddr - (LPD_U32)cur_spaddr; LPD_U32 stacksize_left = stacksize_used; if(NULL == stack_bot || stacksize_used <= 0) return ret; INF("latest stack framesize is: %d bytes.\n", cur_stackframe_size); INF("\ncurrent stack bottom address: 0x%x\n", (LPD_U32)cur_fpaddr); INF("somwhere in caller: 0x%x\n", (LPD_U32)cur_raaddr); stacksize_left -= cur_stackframe_size; while(stacksize_left > 0) { next_fpaddr = (LPD_U32*)*(cur_fpaddr -2); next_raaddr = (LPD_U32*)*(cur_fpaddr -1); if((unsigned int)next_raaddr < text_start || (unsigned int)next_raaddr > text_end) { INF("========>>>>>>>>>> Warning: Ra is not in .text section!!!!\n"); } INF("\n current stack bottom address: 0x%x\n", (LPD_U32)next_fpaddr); INF("somwhere in caller: 0x%x\n", (LPD_U32)next_raaddr); cur_stackframe_size =(LPD_U32)next_fpaddr - (LPD_U32)cur_fpaddr; INF("current stack frame size is: %d bytes..\n", cur_stackframe_size); stacksize_left -= cur_stackframe_size; stack_callayer_cnt++; cur_fpaddr = next_fpaddr; } INF("stack calling layer count is: %d\n", stack_callayer_cnt); return 0; } */ static void Get_PC(unsigned int *pPC) { __asm__ volatile( "move %0, $31\n" : "=r"(*pPC)); return; } SAL_OSErrorCode_e SAL_OSDumpThreadInfo(SAL_OSThread_t ThreadID) { LPD_U8 *Stack; LPD_U32 *sp , * lsp, *thread_sp; LPD_U32 StackSize ; cyg_thread_info ThreadInfo; LPD_U8 *pData = (LPD_U8 *)ThreadID; LPD_U32 *pThreadEntryPt = NULL; extern unsigned int _ftext; extern unsigned int _etext; LPD_U32 thread_ra; LPD_U32 fpvalue; int ret = -1; Stack = (LPD_U8 *)cyg_thread_get_stack_base(ThreadID); StackSize = cyg_thread_get_stack_size(ThreadID); cyg_thread_get_info(ThreadID, cyg_thread_get_id(ThreadID), &ThreadInfo); sp = (LPD_U32 *)((pData[12] << 24) | (pData[13] << 16) | (pData[14] << 8) | (pData[15])); pThreadEntryPt = (LPD_U32 *)((pData[16] << 24) | (pData[17] << 16) | (pData[18] << 8) | (pData[19])); /* if current thread handle is equal to "ThreadID" */ if(ThreadID == cyg_thread_self()) { printf("The thread will be parsed is the CURRENT thread\n"); Get_PC(&thread_ra); __asm__ volatile("move %0, $29\n" : "=r"((LPD_U32)thread_sp)); } else { LPD_U8 switch_context_regs_size = 168; thread_sp = (LPD_U32 *)((LPD_U32)sp + switch_context_regs_size); INF("thread sp address: 0x%x, context sp address: 0x%x\n", (LPD_U32)thread_sp, (LPD_U32)sp); lsp = (LPD_U32 *)(Stack + StackSize); LPD_U32 stack_base; stack_base = ThreadInfo.stack_base; LPD_U32 stack_used; stack_used = (ThreadInfo.stack_used == 0)?((LPD_U32)lsp - (LPD_U32)sp):ThreadInfo.stack_used;//statck_used==0 means this variable is disable via ecos INF("stack_base = 0x%x, stack_used = 0x%x", stack_base, stack_used); INF("text start address: 0x%x, text end address: 0x%x", (unsigned int)&_ftext, (unsigned int)&_etext); thread_ra = *(sp + 31); fpvalue = *(sp + 30); INF("Thread Ra register value is: 0x%x & fp value is: 0x%x", thread_ra, fpvalue); } /*int i = 0; for(i = 0; i < 37; i++) { INF("sp = 0x%x, (r%d) = 0x%x\n", (LPD_U32)(sp + i), i, *(sp + i)); }*/ INF(">>>>>>> thread stack info <<<<<<<<\n"); ret = mips_bt(thread_ra, (LPD_U32)thread_sp, (void *)pThreadEntryPt); if(-1 == ret) { printf("\nWARNING: this thread's stack may be dirty...\n"); } //__asm__("sdbbp"); return SAL_OS_OK; } int SAL_OSTravThread(void) { cyg_handle_t thread = 0; cyg_uint16 id = 0; int i = 0; printf("==============>>>>>>>>>>Travse thread BEGINS:<<<<<<<<<<<<<<============\n"); while( cyg_thread_get_next( &thread, &id ) ) { cyg_thread_info info; if( !cyg_thread_get_info( thread, id, &info ) ) break; i++; printf("The %dth thread is parsed as follows:\n", i); printf("ID: 0x%04x name: %10s pri: %d state:%d\n",info.id, info.name?info.name:"----", info.set_pri, info.state ); SAL_OSDumpThreadInfo(thread); printf("The %dth thread parsed end..\n", i); } printf("==============>>>>>>>>>>Travse thread END:<<<<<<<<<<<<<<============\n"); return 0; }