mips體系堆棧回溯分析與實現

  在上家公司做 linux 驅動開發主要基於mips架構,在內核下有一個好玩的函數 dump_stack, 只要調用到這個函數就能把函數的調用棧給打印出來,十分方便,不再需要kgdb斷點了。
  現在做電視機頂盒開發,各種軟硬件平臺 arm mips linux ecos 都有,有時候想要獲得一個函數從哪裏調用來的單憑靜態分析根本不行,如果動態運行那就要在每個調用前面加入打印,修改量巨大而且容易出錯。於是仿照 linux 內核代碼 arch/mips/oprofile/backtrace.c 實現了mips架構下的棧回溯功能。

mips棧幀原理
  Call stack 是指存放某個程序的正在運行的函數的信息的棧。Call stack 由 stack frames 組成,每個 stack frame 對應於一個未完成運行的函數。
  在當今流行的計算機體系架構中,大部分計算機的參數傳遞,局部變量的分配和釋放都是通過操縱程序棧來實現的。棧用來傳遞函數參數,存儲返回值信息,保存寄存器以供恢復調用前處理機狀態。 關於棧可見以前的文章: cdecl、stdcall、fastcall函數調用約定區別
  每次調用一個函數,都要爲該次調用的函數實例分配棧空間。爲單個函數分配的那部分棧空間就叫做 stack frame,也就是說,stack frame 這個說法主要是爲了描述函數調用關係的。

Stack frame 具體從2點來闡述
  第一,它使調用者和被調用者達成某種約定。這個約定定義了函數調用時函數參數的傳遞方式,函數返回值的返回方式,寄存器如何在調用者和被調用者之間進行共享;
  第二,它定義了被調用者如何使用它自己的 stack frame 來完成局部變量的存儲和使用。
mips 棧幀

  上圖描述的是一種典型的(MIPS O32)嵌入式芯片的 stack frame 組織方式。在這張圖中,計算機的棧空間採用的是向下增長的方式(intel 是確定向下增長的,arm可以配置棧增長方向,mips 是可配置還是隻能向下增長?),SP(stack pointer) 就是當前函數的棧指針,它指向的是棧底的位置。Current Frame 所示即爲當前函數(被調用者)的 frame ,Caller’s Frame 是當前函數的調用者的 frame 。
  在沒有 BP(base pointer) 寄存器的目標架構中,進入一個函數時需要將當前棧指針向下移動 n 字節,這個大小爲n字節的存儲空間就是此函數的 stack frame 的存儲區域。此後棧指針便不再移動(在linux 內核代碼 TODO 裏面寫着要加上在函數內部調整棧的考慮 – 雖然這通常不會發生),只能在函數返回時再將棧指針加上這個偏移量恢復棧現場。由於不能隨便移動棧指針,所以寄存器壓棧和出棧都必須指定偏移量,這與 x86 架構的計算機對棧的使用方式有着明顯的不同。
  RISC計算機一般藉助於一個返回地址寄存器 RA(return address) 來實現函數的返回。幾乎在每個函數調用中都會使用到這個寄存器,所以在很多情況下 RA 寄存器會被保存在堆棧上以避免被後面的函數調用修改,當函數需要返回時,從堆棧上取回 RA 然後跳轉。移動 SP 和保存寄存器的動作一般處在函數的開頭,叫做 function prologue;
  注意如果當前函數是葉子函數(不存在對其它函數的調用,就不保存ra寄存器,反之就保存)。恢復這些寄存器狀態的動作一般放在函數的最後,叫做 function epilogue。關於這些動作可以從IDA 反彙編的結果看出來:
ida mips反彙編
通過上面分析就有思路了:
首先獲取當前的棧指針 sp,和指令指針 pc (也叫做 IP)
在mips下sp容易獲取 已經約定$29 寄存器作爲棧指針,所以可用如下內嵌彙編獲取sp:

__asm__ volatile ("move %0, $29" : "=r"(reg));

  MIPS沒有記錄當前PC地址的寄存器,就是說不能像ARM那樣讀PC寄存器。MIPS使用ra保存函數返回地址,利用這個特性可以獲取到當前的PC。 比如:

#pragma GCC push_options
#pragma GCC optimize ("O0")
static  unsigned int /*__attribute__((optimize("O0")))*/ * __getpc(void) 
{
    unsigned int *rtaddr;

    __asm__ volatile ("move %0, $31" : "=r"(rtaddr));

    return rtaddr;
}
#pragma GCC pop_options

對應的彙編代碼是:

  注意到上面代碼中有 push_options 這是爲了防止編譯器偷偷把我們的代碼給優化成 inline 了,那樣就無法獲取pc了。在要獲取pc的地方調用就可以了:

pc      = __getpc();

得到sp 和pc之後剩下的就開始回溯了,具體參考下面函數實現(linux 內核未修改代碼,只加入註釋):

/*
 * TODO for userspace stack unwinding:
 * - handle cases where the stack is adjusted inside a function
 *     (generally doesn't happen)
 * - find optimal value for max_instr_check
 * - try to find a way to handle leaf functions
 */

static inline int unwind_user_frame(struct stackframe *old_frame,
                    const unsigned int max_instr_check)     //// max_instr_check 函數最大可能的代碼長度
{
    struct stackframe new_frame = *old_frame;
    off_t ra_offset = 0;
    size_t stack_size = 0;
    unsigned long addr;

    if (old_frame->pc == 0 || old_frame->sp == 0 || old_frame->ra == 0)
        return -9;

    for (addr = new_frame.pc; (addr + max_instr_check > new_frame.pc)   /// 上面通過 __getpc 獲取的pc指針
        && (!ra_offset || !stack_size); --addr) {
        union mips_instruction ip;

        if (get_mem(addr, (unsigned long *) &ip))           /// 取出一條指令  ip 
            return -11;

        if (is_sp_move_ins(&ip)) {                  /// 這條指令是不是 addiu $sp,imme 形式的
            int stack_adjustment = ip.i_format.simmediate;      /// 如果是求出 imme 立即數,那麼本函數棧大小也就知道了
            if (stack_adjustment > 0)
                /* This marks the end of the previous function,
                   which means we overran. */
                break;
            stack_size = (unsigned long) stack_adjustment;
        } else if (is_ra_save_ins(&ip)) {               /// 是不是  sw / sd $ra, offset($sp) 類似的指令
            int ra_slot = ip.i_format.simmediate;           /// 如果是獲取 offset
            if (ra_slot < 0)
                /* This shouldn't happen. */
                break;
            ra_offset = ra_slot;
        } else if (is_end_of_function_marker(&ip))
            break;
    }

    if (!ra_offset || !stack_size)
        return -1;

    if (ra_offset) {
        new_frame.ra = old_frame->sp + ra_offset;           /// 根據上面的 offset 和當前函數的sp指針得到存放 ra 數值的地址
        if (get_mem(new_frame.ra, &(new_frame.ra)))         /// 獲取ra 的數值,也就是 jal func 這條指令所在的地址
            return -13;
    }

    if (stack_size) {
        new_frame.sp = old_frame->sp + stack_size;          /// 上個函數的棧指針 貌似這裏這麼代碼是錯誤的,反正我按我的方式改了能正確運行,
        if (get_mem(new_frame.sp, &(new_frame.sp)))         /// 這裏這麼寫我也不懂。
            return -14;
    }

    if (new_frame.sp > old_frame->sp)
        return -2;

    new_frame.pc = old_frame->ra;
    *old_frame = new_frame;

    return 0;
}

  上面代碼修改之後可以獲取 上一層函數的 pc (jal func :調用本函數的指令不就是上層函數的某一條指令? ra 的值並不是jal func 的地址,而是jal func的地址 +8 ,傳說中的 跳轉延時槽)指針和sp 。
  依此循環調用上面的函數一層層向上回溯,即可。

參考文章:
mips體系堆棧回溯分析與實現
MIPS 架構上函數調用過程中的堆棧和棧幀

在我的dropbox下載完成的代碼:
https://www.dropbox.com/sh/uqgvjs1ybu118e1/AADMzHpDbvgtN8QSAPtA0lJ1a?dl=0

碼字不容易,覺得好請打賞下:
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章