LiteOS調測利器:backtrace函數原理知多少

摘要:本文將會和讀者分享LiteOS 5.0版本中Cortex-M架構的backtrace軟件原理及實現,供你們參考和學習交流。

原理介紹

彙編指令的執行流程

圖 1 彙編指令的執行順序git

上圖1所示,ARM的彙編指令執行分三步:取值(fetch)、譯指(decode)、執行(execute),按照流水線的方式執行,即當運行指令節拍m時,pc會指向n+2彙編指令地址進行取指令操做,同時會將n+1處彙編指令翻譯成對應機器碼,並執行指令n。架構

內存中棧的佈局

圖 2 棧在內存中的佈局框架

LiteOS Cortex-M架構的棧佈局如上圖2,棧區間在內存中位於最末端,程序運行時從內存末端(棧頂)開始進行遞減壓棧。LiteOS的內存末端爲主棧空間(msp_stack),LiteOS進入任務前的初始化過程及中斷函數調用過程的棧數據保存在此區間內,主棧地址空間往下爲任務棧空間(psp_stack),任務棧空間在每一個任務被建立時指定,多個任務棧空間依次排列。一個任務中可能包含多個函數,每一個函數都有本身的棧空間,稱爲棧幀。調用函數時,會建立子函數的棧幀,同時將函數入參、局部變量、寄存器入棧。棧幀從高地址向低地址生長。函數

寄存器數據入棧流程

ARM爲了維護棧中的數據設計了兩個寄存器,分別爲fp寄存器(framepointer,幀指針寄存器)和sp寄存器(stack pointer,堆棧寄存器)。fp指向當前函數的父函數的棧幀起始地址, sp指向當前函數的棧頂。經過對sp寄存器的地址進行偏移訪問能夠獲得棧中的數據內容,經過訪問fp寄存器地址能夠獲得上一棧幀的起始位置,進而計算出函數的返回地址。因爲Cortex-M沒有fp寄存器,若想得到函數入口地址只能經過sp地址偏移找到lr寄存器(link register,連接寄存器,指向當前函數的返回地址),並結合函數入口的push指令計算得出。lr寄存器會在每次函數調用時壓入棧中,用以返回到函數調用前的位置繼續執行。函數調用執行流程引用自Joseph Yiu的《Cortex-M3 權威指南》,以下圖3所示。工具

圖 3 函數調用執行流程佈局

如函數調用執行流程所示,程序進入一個子函數後,一般都會使用push指令先將寄存器的值壓入棧中,執行完業務邏輯後再使用pop指令將棧中保存的寄存器數據出棧並按順序存入對應的寄存器。當程序執行bl跳轉指令時,pc中的值爲bl指令後的第二條指令的地址,減去一條彙編指令的長度後爲bl後第一條指令的地址,即lr值。程序在進入Fx1前,bl或blx指令會將此lr值保存到lr寄存器,並在進入Fx1函數時將其壓入棧中。例若有以下彙編指令:學習

800780e:  6078        str  r0, [r7, #4]
8007810:  f7ff ffe0   bl  80077d4 <test_div>
8007814:  f7f9 fe68   bl  80014e8 <OsTickStart>

當程序執行到地址0x8007810時,在bl指令跳轉到函數test_div以前,bl指令會將此時的pc地址(0x8007818)減去一條彙編指令的長度(這裏爲4),將計算獲得的值0x8007814(本條指令僅執行到譯指,還沒有完成所有執行過程,返回後需從新取指)保存到lr寄存器。fetch

實現思路

根據函數調用執行流程的原理,當程序跳入異常時,傳入當前位置sp指針,經過對sp指針進行循環自增訪問操做獲取棧中的內容,sp指向棧頂,循環自增的邊界即任務棧的棧底,因爲Cortex-M使用的thum-2指令集,彙編指令長度爲2字節,所以可經過判斷棧中的數據是否兩字節對齊及位於代碼段區間內篩選出當前棧中的彙編指令地址。並經過判斷上一條是否爲bl指令或blx指令(b、bx指令不將lr寄存器入棧,不對其進行處理)對上一條指令進行計算。跳轉指令的機器碼構成以下圖4所示:優化

圖 4 thum跳轉指令機器碼構成ui

若是爲bl指令地址(特徵碼0xf000),經過該地址中存儲的機器碼計算出偏移地址(原理見下圖5),從而得到跳轉指令目標函數入口地址,若是爲blx指令(這裏爲blx 寄存器n指令,其特徵碼0x4700),因爲目標偏移地址保存在寄存器中,沒法經過機器碼計算偏移地址,則須要根據被調用幀保存的lr地址推算其所在的函數入口地址,直到入口處的push指令。

圖 5 bl指令偏移地址計算規則

設計實現分析

LiteOS在運行過程當中出現異常時,會自動轉入異常處理函數。LiteOS提供了backtrace函數用於跟蹤函數的堆棧信息,經過系統註冊的異常處理函數來調用backtrace函數實現系統異常時自動打印函數的調用棧。

設計思路

因爲Cortex-M架構無fp寄存器,sp寄存器分爲msp寄存器(用於主棧)和psp寄存器(用於任務棧),所以只能經過彙編指令機器碼計算及lr地址自增查找函數入口處的push指令特徵碼計算函數入口。

詳細設計

圖 6 backtrace代碼框架

當調用Cortex-M架構的ArchBackTrace接口時,該函數會經過ArchGetSp獲取當前sp指針,若是在初始化或中斷過程發生異常,sp指向msp,在任務中發生異常,sp指向psp。將獲取的sp指針傳入BackTraceWithSp進行調用棧分析,該函數經過FindSuitableStack函數進行棧邊界確認,找到合適的任務棧邊界或主棧(未區分中斷棧及初始化棧)邊界。再經過邊界值控制循環查找次數,從而確保將對應棧空間內全部棧幀的lr地址過濾出來。最後將lr地址傳入CalculateTargetAddress函數計算出lr前一條指令(即跳轉指令)要跳轉到的函數入口地址。

代碼路徑

以上代碼在LiteOS 5.0版本中已經發布,核心代碼路徑以下:

https://gitee.com/LiteOS/LiteOS/blob/master/arch/arm/cortex_m/src/fault.c

Backtrace效果演示

  • 演示demo

圖 7 除0錯誤用例函數

演示demo設計了一個會致使除0錯誤的函數(如上圖圖7),分別在初始化、中斷、任務三個場景下調用該函數,將會觸發異常並打印相應的信息,觀察相應的fp(此處指函數入口地址,非棧幀寄存器的值)地址是否與實際代碼的反彙編地址一致。

能夠經過menuconfig菜單使能backtrace功能,菜單項爲:Debug--> Enable Backtrace。同時爲避免編譯優化形成的影響,還需配置編譯優化選項爲不優化:Compiler--> Optimize Option --> Optimize None。

  • 演示效果

下面所示圖中,左圖爲異常接管打印的日誌,右圖爲反彙編代碼。能夠看到左圖中出現異常的pc指令值,對應於右圖中的彙編代碼爲sdiv r3, r2, r3,即爲test_div函數中的int z = a / b代碼行。左圖中打印的backtrace信息,其fp值和右圖中的函數入口地址一致。

任務中觸發異常:

圖 8 backtrace任務演示效果

中斷處理函數中觸發異常:

圖 9 backtrace中斷演示效果

初始化函數中觸發異常:

圖 10 backtrace初始化演示效果

結語

程序異常或崩潰時,經過backtrace能夠快速定位到問題代碼的程序段,是代碼調試的必備利器。當與其它工具深度結合時,如與LiteOS的LMS結合時,會碰撞出更奇妙的火花,甚至能夠不用分析彙編代碼,直接跳轉到出問題的C代碼行。

對於其它架構,如LiteOS Cortex-A的backtrace實現會有差別,讀者能夠參考arch目錄下其它架構的backtrace相應實現。

若是您對backtrace有其它疑問或需求,能夠在公衆號留言或者在社區參與討論:https://gitee.com/LiteOS/LiteOS/issues。

本文分享自華爲雲社區《LiteOS調測利器之backtrace原理剖析》,原文做者:風清揚。

 

點擊關注,第一時間瞭解華爲雲新鮮技術~

相關文章
相關標籤/搜索