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


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

原理介紹

匯編指令的執行流程

圖 1 匯編指令的執行順序

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

實現思路

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

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

如果為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原理剖析》,原文作者:風清揚。

 

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM