(大連醫科大學附屬第二醫院 信息中心,大連 116021)
嵌入式系統遠程調試工具在嵌入式開發過程中起著至關重要的作用,它直接影響開發效率。國外在嵌入式調試器方面一直領先,國內普遍采用國外的工具,價格比較昂貴,而適配自主可控的國產芯片的調試系統相對缺乏,這就對嵌入式開發環境提出了新的要求[1]。嵌入式軟件開發中的遠程調試主要有硬件調試和軟件調試兩大類,硬件調試雖然實時性強,但價格高昂,通用性差,操作復雜,這給開發者應用開發過程帶來極大不便; 軟件調試由于其成本低,通用性強,因而得到廣泛的應用[2]。這種方式通常需要在目標系統中駐留代理軟件,宿主機調試器與目標機代理軟件通信,調試代理實現對被調試程序的控制[3]。
本文硬件選擇了Arm架構平臺,設計并實現了一款基于搶占式實時操作系統T-kernel的軟件遠程調試工具,T-kernel是構筑于T-Engine之上的標準化開源實時操作系統內核,由TRON(the real-time operating nucleus) 發展而來[4],而TRON是日本東京大學坂村健博士于1984年提出的計算機操作系統規范,T-kernel由于其實時性及開源性,已經被安裝到了全球約40億臺家用電子產品當中,占全球微處理器操作系統市場約60%[5]的份額。同時Arm架構的微處理器更是占據了當今移動終端核心的半壁江山。因此基于兩者結合的遠程調試工具的設計顯得更具有通用性的意義。
宿主機方面采用Linux系統,使用Arm-elf-gdb打開經過Arm交叉編譯器編譯出來的帶有調試信息的T-kernel系統應用程序文件。宿主機端作為請求方,通過通用串行接口UART與目標機相連,向目標機遠程發送調試命令,并接收相應的反饋,實現交互。整體結構概要設計如圖1所示。

圖1 整體結構概要設計
目標機中T-kernel系統下建立調試命令服務端監聽任務,實時監聽來自宿主機通過串口發送來的調試命令,一旦接收到調試請求命令,監聽任務即陷入Arm exception系統狀態,通過T-monitor中事先注冊好的異常處理程序接管系統執行,進行相應任務的解析及具體調試命令的執行,最后將結果通過串口反饋給宿主機,完成整體調試通信。
目標機中T-kernel系統在啟動之初,需要將Arm exception注冊進T-monitor中,T-monitor是T-kernel的啟動部分,啟動系統并創建調試監聽任務,這里需要注意的是要將該監聽任務的優先級適當調高,以便使遠程調試命令及時得到服務。任務創建成功后即可實時監聽串口發來的數據字符,根據RSP(Remote Serial Protocol)協議規則進行字符解析,當解析出完整的“$

圖2 目標機執行流程
本文設計的監聽任務只負責解析一個遠程調試啟動命令。當宿主機由串口虛擬終端ttyS1發送啟動調試命令“target remote/dev/ttyS1”時,監聽任務可以接收到“$qSupported:qRelocInsn+#9a”字符信息,即表示遠程調試已由宿主機發起,此時目標機應該進入調試模式。這里利用Arm處理器的未定義指令異常模式,在監聽任務中通過內嵌預設好的Arm未定義異常指令,使系統陷入啟動之初注冊入T-monitor中的undefined exception程序。至此系統進入異常處理模式,并在此模式下通過串口與上位機進行通信,接收并進一步解析開發者的調試命令,將不同的命令信息反饋給上位機,完成對特定內存、特定寄存器的存取察看工作,完成斷點插入和刪除等一系列工作以達到開發調試的目的。

圖3 Debug task結構設計
調試入口的Debug task主要完成兩個工作:一是負責利用串口與宿主機進行通信,二是接收到調試啟動命令后跳轉到未定義異常入口。因為T-kernel是搶占式內核,任務調度由優先級決定,高優先級任務具有絕對優勢,因此,為了使啟動調試命令能夠及時得到響應,Debug task的優先級被設計成高于一般業務task,T-kernel系統優先級數值越小級別越高,這里設置為2,因為通信數據量小,故棧空間設置為4 096。在操作系統啟動之初,Debug task就得到CPU使用權,并開始監聽串口,如果串口沒有數據,則會因為等待資源而被系統掛起并讓出CPU使用權,當宿主機啟動調試,數據發送到下位機串口會引起中斷而喚醒該任務,從而解析命令并順利產生異常,進入T-monitor中預先注冊好的異常處理程序中。Debug task結構設計如圖3所示。
該注冊函數處于T-kernel系統的T-monitor中,異常入口處首先將寄存器r8~r11全部入棧,這些寄存器后續需要使用,因此先入棧保存,然后將PC寄存器地址減8字節,并將內容取出進行判斷,即為發生異常時的地址內容。此處注意,本文使用的Arm處理器采用了五級流水線機制,當第三階段PC寄存器值取指時,第一階段的指令執行,所以PC值為當前執行指令地址值加8個字節,也就是說,對Arm指令集來說,PC指向當前指令的下兩條指令的地址,因此想要得到異常發生時的地址內容,必須要PC地址減去8個字節,此處具體還要看使用的是芯片的哪種設計方式,注意其差異。

圖4 Bootloader中注冊程序流程
同時,這里還可以采用另外一種方案,即取出R14寄存器減4字節的地址內容,因為在Arm體系結構中,R14即為鏈接寄存器,就是用來存儲子程序的返回地址,因此它之前的地址內容即為產生異常的指令。取出該值后進行判斷,來區分產生異常的指令是否為預設的異常指令或是其它的未定義指令異常情況,以進入不同的處理分支。如果是預設的異常指令,則繼續將r0~r7通用寄存器內容保存到預設好的全局變量中,然后取出spsr,spsr寄存器用于保存cpsr的狀態,根據此寄存器的最低5位可以判定出發生異常前的系統模式,因為Arm處理的不同工作模式下,寄存器R8~R14是與模式相關的,具有非通用性,所以在保存它們之前需要判斷出發生異常時的工作模式,并切換到該模式下,將R8~R14這些寄存器內容取出并保存到預設的全局變量結構體中,以便異常返回后恢復異常發生時的處理器工作狀態。Bootloader中注冊程序流程如圖4所示。
調試命令實現流程如圖5所示。

圖5 調試命令實現流程
進入異常結構后,對于上位機的諸多調試命令,最終經過RSP協議均被解析為圖5中的一系列命令。‘g’命令讀取寄存器,將r0~R15包括fps和cpsr均打包發送回主機端。‘G’命令將接收到的將要修改的數據回寫到相應寄存器中;‘m/M’分別對指定內存地址進行讀/寫操作;‘c’命令用于使停止在某異常處理狀態的程序繼續執行;‘s’命令用于系統進行單步執行操作。單步執行的處理,實際上就是向下一地址插入斷點命令的過程,即插入一條未定義指令,該指令就是Debug task中啟動調試時預設的內嵌指令。程序維護一個數據結構,分別用于存儲斷點地址、斷點內容以及斷點標志。每次進入exception handler之后,首先判斷之前是否存在已經運行過的斷點,如果有,則將其地址中的內容恢復,然后再根據異常發生時保存的PC寄存器內容判斷程序將要運行的下一個地址,并將其地址及內容取出保存,替換為異常指令,最后返回程序繼續運行,這樣程序運行到下一地址時即再次陷入異常,從而達到單步調試的目的。因此,不管在宿主機端一次插入多少個斷點,實際在目標機端都是一次只恢復一個斷點,再插入下一個,直到程序運行結束。宿主機只是維護了一個斷點集合,只要這個斷點集合不被刪除,程序就會循環不斷地進入異常,完成開發者的調試工作。
對于單步執行的斷點插入工作,比較復雜的問題是下一地址的獲取方法,Arm指令的一般編碼格式如下:

3128272524 212019 1615 12 110CondOpcodeSRnRdShifter_operand
其中:Cond: 指令執行的條件編碼; Opcode: 指令操作編符碼;S: 決定指令的操作是否影響CPSR值;Rn: 包含第一個操作數的寄存器編碼;Rd: 目標寄存器編碼;Shifter_operand: 表示第二個操作數[7]。
Arm指令大致格式如此,但不同的指令有其差別,經過對與跳轉有關指令尋址方式進行分析,當程序進入異常時,可以通過取出 PC寄存器內容的bit[27:25]三位進行過濾,并根據其差異對照Arm指令集中與跳轉有關的指令編碼結構,可以判斷出指令集中可能產生跳轉的命令,再由特定的指令字段中找到目標地址寄存器,從而定位實際地址,或由Rd、Rn、Shifter_operand中的一個或幾個組合而生成,具體取決于不同指令的編碼。另外還需要額外對條件域進行判斷,Cond域中的條件編碼數據決定指令的跳轉邏輯。BX/B/BL/BLX均為跳轉地址。B/BL跳轉指令結構如下:

31 28 27262524230Cond1 0 1LSigned_immed_24
在判斷Cond域的前提下,該指令的目標跳轉地址是將指令中的24位帶符號的立即數擴展為32位,注意是帶符號位進行擴展,然后將這個32位立即左移兩位,將得到的值再加到PC寄存器中即可。
BLX指令有兩種結構,其一如下所示:

31 28 2726252423011111 0 1HSigned_immed_24
該指令的目標地址同樣需要擴展并左移兩位,但結果需要再加上H位值左移一位后的數值,再加到PC寄存器中。值得注意的是需要對該指令的最高4位進行二次確認是否為全1,以此來區分B/BL指令。
對于BLX的第二種指令結構,如下所示:

31 28 27 2019 8 7 4 30Cond00010010應為00011Rm
該指令的Rm為寄存器號,在這個寄存器中即保存著跳轉的目標地址。在此之前除對bit[27:25]確認為全0后,還需要進一步確認bit[24:20],以確定是否為該指令。
BX的指令格式與BLX2類似,如下所示:

31 2827 2019 8 7 4 3 0Cond00010010應為00001Rm
需要對bit[5]進行判斷以進行區分,Rm寄存器中保存著跳轉的目標地址。
對于內存讀取指令LDR、LDM,當PC寄存器作為其目標地址時,指令從內存中讀取的字數據將被當作目標地址值,指令執行后程序將從目標地址處開始執行,也起到了跳轉程序的作用。LDR指令格式如下所示:

31 28272625 24232221 201916 15 12110Cond00IPU0W1RnRdAddress
對于第一操作數,當PC作為基址寄存器Rn時,內存基地址為當前指令地址加8字節的偏移量。對于第二操作數,首先需要判斷bit[25]即I位,如果為1,Address則由索引寄存器Rm,即bit[3:0]與其它移位值共同組合而成;I位如果為0,Address整體則為Rn的偏移值offset_12。無論對于索引寄存器Rm中存儲的值,還是直接偏移值offset_12,Rn對其偏移的方向都由bit[23]決定,當U位為1時,加上偏移量,當U位為0時,則要減去偏移量,同時偏移量的生成也有需要Rm經過移位操作后得到的結果。
LDR指對單個內存單元向單個寄存器傳送數據,而LDM則可以完成批量內存數據到一組寄存器的數據傳輸工作。LDM指令格式如下所示:

31 2827 2524 23 22 21 2019 1615 0Cond100PUSWLRnRegisterlist
Register list分別對應r0~r15寄存器,其中編號低的寄存器對應內存區域的低地址,編號高的對應高地址。Rn中存放連續內存區域的最低地址值。與LDR類似,也有向上偏移和向下偏移之分。這里不再贅述。
最后一類為數據處理指令,第一操作數Rn的獲取方式與LDR指令獲取方式相同,第二操作數需要首先判斷bit[25],如果為1,那么指令的操作數為直接的立即數;如果為0,則需要索引寄存器Rm參與,即偏移值由Rm寄存器中存儲的指定操作數經過相應的移位操作獲得。
本文介紹了關于搶占式實時嵌入式系統的遠程調試功能的設計思想,并解析了實現過程,詳述了調試任務的設計及調試功能的實現方式。宿主機采用了安裝在VMware8.0上的centOS 7.0操作系統以及Arm_elf_gdb工具共同完成,目標機Arm芯片采用CXD3175。圖6、圖7為部分已經驗證過的可支持的命令以及節選的調試日志。由于內容較多,故不全部列舉。

圖6 可支持的調試命令節選

圖7 驗證日志節選

參考文獻
[1] 余梓奇,章建雄,馬鵬,等.基于OpenOCD和DAP的嵌入式遠程調試系統研究與設計[J].電子設計工程,2017,25(11):149-153.
[2] 趙俊濤,詹瑾瑜.嵌入式內核遠程調試系統的研究與應用[J].計算機應用與軟件, 2015,32(8):211-214.
[3] 殷紹劍,雷航,詹瑾瑜.嵌入式遠程調試原理研究與實現[J].計算機應用與軟件,2014(6):240-241.
[4] 坂村健.源碼開放的嵌入式實時操作系統T-Kernel[M].北京:北京航空航天大學出版社,2005.
[5] 李傳煌,王偉明.T-Kernel任務調度的實時性分析[J].計算機工程,2006,32(16):58.
[6] Bill Gatliff.Embedding with GNU:the gdb Remote serial Protocol[J].Embedded System Programming,1999(11):108-113.
[7] 杜春雷.ARM體系結構與編程[M].北京:清華大學出版社,2003.
[8] 蔣龍.基于GDB的嵌入式多任務調試器的設計實現與集成[D].杭州:浙江大學,2014.
[9] 殷紹劍.嵌入式多線程遠程調試器研究與實現[D].成都:電子科技大學,2013.
任玉帥(工程師),主要研究方向為嵌入式軟件程序設計。