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

華為雲開發者社群發表於2021-01-29
摘要:本文將會和讀者分享LiteOS 5.0版本中Cortex-M架構的backtrace軟體原理及實現,供大家參考和學習交流。

原理介紹

彙編指令的執行流程

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

圖 1 彙編指令的執行順序

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

記憶體中棧的佈局

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

圖 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所示。

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

圖 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暫存器。

實現思路

根據函式呼叫執行流程的原理,當程式跳入異常時,傳入當前位置sp指標,通過對sp指標進行迴圈自增訪問操作獲取棧中的內容,sp指向棧頂,迴圈自增的邊界即任務棧的棧底,由於Cortex-M使用的thum-2指令集,彙編指令長度為2位元組,因此可通過判斷棧中的資料是否兩位元組對齊及位於程式碼段區間內篩選出當前棧中的彙編指令地址。並通過判斷上一條是否為bl指令或blx指令(b、bx指令不將lr暫存器入棧,不對其進行處理)對上一條指令進行計算。跳轉指令的機器碼構成如下圖4所示:

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

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

如果為bl指令地址(特徵碼0xf000),通過該地址中儲存的機器碼計算出偏移地址(原理見下圖5),從而獲得跳轉指令目標函式入口地址,如果為blx指令(這裡為blx 暫存器n指令,其特徵碼0x4700),由於目標偏移地址儲存在暫存器中,無法通過機器碼計算偏移地址,則需要根據被呼叫幀儲存的lr地址推算其所在的函式入口地址,直到入口處的push指令。

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

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

設計實現分析

LiteOS在執行過程中出現異常時,會自動轉入異常處理函式。LiteOS提供了backtrace函式用於跟蹤函式的堆疊資訊,通過系統註冊的異常處理函式來呼叫backtrace函式實現系統異常時自動列印函式的呼叫棧。

設計思路

由於Cortex-M架構無fp暫存器,sp暫存器分為msp暫存器(用於主棧)和psp暫存器(用於任務棧),因此只能通過彙編指令機器碼計算及lr地址自增查詢函式入口處的push指令特徵碼計算函式入口。

詳細設計

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

圖 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

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

圖 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值和右圖中的函式入口地址一致。

任務中觸發異常:

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

圖 8 backtrace任務演示效果

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

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

圖 9 backtrace中斷演示效果

初始化函式中觸發異常:

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

圖 10 backtrace初始化演示效果

結語

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

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

如果您對backtrace有其它疑問或需求,可以在公眾號留言或者在社群參與討論:https://gitee.com/LiteOS/LiteOS/issues。

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

 

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

相關文章