鴻蒙輕內核M核的故障管家:Fault異常處理
摘要:本文先簡單介紹下Fault異常類型,向量表及其代碼,異常處理C語言程序,然後詳細分析下異常處理彙編函數實現代碼。
本文分享自華為雲社區《鴻蒙輕內核M核源碼分析系列十八 Fault異常處理》,作者:zhushy。
Fault異常處理模塊與OpenHarmony LiteOS-M內核芯片架構相關,提供對HardFault、MemManage、BusFault、UsageFault等各種故障異常處理。有關Cortex-M芯片相關的知識不在本文討論,請自行參考《Cortex™-M7 Devices Generic User Guide》等官方資料。本文先簡單介紹下Fault異常類型,向量表及其代碼,異常處理C語言程序,然後詳細分析下異常處理彙編函數實現代碼。文中所涉及的源碼,以OpenHarmony LiteOS-M內核為例,均可以在開源站點//gitee.com/openharmony/kernel_liteos_m 獲取。
1、Fault Type異常類型
如下圖中的Fault類型表格所示,Fault表示各種故障,Handler表示故障處理機制,Bit Name標記故障的寄存器的Bit位,Fault status register故障狀態寄存器。該圖摘自《Cortex™-M7 Devices Generic User Guide》。
2、Vector table向量表
向量表包含棧指針的複位值和開始地址,也叫異常向量。異常可以看作特殊的中斷,異常編號Exception number, 中斷請求號IRQ number,偏移值offset,向量Vector的對應關係如下圖所示,本文主要關注NMI、HardFault、Memory management fault、Bus fault、Usage fault、SVCall等異常。
在中斷初始化時,會初始化該異常向量表,代碼位置kernel\arch\arm\cortex-m7\gcc\los_interrupt.c。⑴處的HalExcNMI,⑵處的HalExcHardFault,⑶處的HalExcMemFault,⑷處的HalExcBusFault,⑸處的HalExcUsageFault,⑹處的HalExcSvcCall這些中斷異常處理函數定義在kernel\arch\arm\cortex-m7\gcc\los_exc.S。本文我們主要分析這些彙編函數的代碼。
⑺處開始的這兩行代碼也比較重要,通過更改系統處理控制與狀態寄存器(System Handler Control and State Register)的bit位來使能相應的異常,通過更改配置與控制寄存器(Configuration and Control Register)的bit位來使能除零異常。
LITE_OS_SEC_TEXT_INIT VOID HalHwiInit(VOID) { #if (LOSCFG_USE_SYSTEM_DEFINED_INTERRUPT == 1) UINT32 index; g_hwiForm[0] = 0; /* [0] Top of Stack */ g_hwiForm[1] = Reset_Handler; /* [1] reset */ for (index = 2; index < OS_VECTOR_CNT; index++) { /* 2: The starting position of the interrupt */ g_hwiForm[index] = (HWI_PROC_FUNC)HalHwiDefaultHandler; } /* Exception handler register */ ⑴ g_hwiForm[NonMaskableInt_IRQn + OS_SYS_VECTOR_CNT] = HalExcNMI; ⑵ g_hwiForm[HARDFAULT_IRQN + OS_SYS_VECTOR_CNT] = HalExcHardFault; ⑶ g_hwiForm[MemoryManagement_IRQn + OS_SYS_VECTOR_CNT] = HalExcMemFault; ⑷ g_hwiForm[BusFault_IRQn + OS_SYS_VECTOR_CNT] = HalExcBusFault; ⑸ g_hwiForm[UsageFault_IRQn + OS_SYS_VECTOR_CNT] = HalExcUsageFault; ⑹ g_hwiForm[SVCall_IRQn + OS_SYS_VECTOR_CNT] = HalExcSvcCall; g_hwiForm[PendSV_IRQn + OS_SYS_VECTOR_CNT] = HalPendSV; g_hwiForm[SysTick_IRQn + OS_SYS_VECTOR_CNT] = SysTick_Handler; /* Interrupt vector table location */ SCB->VTOR = (UINT32)(UINTPTR)g_hwiForm; #endif #if (__CORTEX_M >= 0x03U) /* only for Cortex-M3 and above */ NVIC_SetPriorityGrouping(OS_NVIC_AIRCR_PRIGROUP); #endif /* Enable USGFAULT, BUSFAULT, MEMFAULT */ ⑺ *(volatile UINT32 *)OS_NVIC_SHCSR |= (USGFAULT | BUSFAULT | MEMFAULT); /* Enable DIV 0 and unaligned exception */ *(volatile UINT32 *)OS_NVIC_CCR |= DIV0FAULT; return; }
3、HalExcHandleEntry異常處理C程序入口
HalExcHandleEntry異常處理函數是彙編異常函數跳轉到C語言程序的入口,定義在文件kernel\arch\arm\cortex-m7\gcc\los_interrupt.c,被kernel\arch\arm\cortex-m7\gcc\los_exc.S文件中的彙編函數調用。函數參數由彙編程序中的R0-R3寄存器傳值進來,彙編程序中的寄存器和HalExcHandleEntry函數參數對應關係如下表所示:
下面我們分析下函數的源代碼,⑴處的標籤表示異常類型參數的高16位用於特色的標記,主要用於標記故障地址是否有效、是否故障發生在中斷中,是否支持浮點等。⑵處增加中斷計數和嵌套異常數目。⑶記錄異常類型,⑷處如果記錄了有效的故障地址,則獲取故障地址。⑸處如果當前運行任務存在時,若標記了異常發生在中斷,則記錄中斷號,並記錄異常發生在中斷內,否則記錄任務編號,並記錄異常發生在任務內。如果當前運行任務為空,則異常發生在初始化階段。⑹處如果異常類型里包含支持浮點數的標記,則相應處理下。⑺處輸出異常信息到控制台。
LITE_OS_SEC_TEXT_INIT VOID HalExcHandleEntry(UINT32 excType, UINT32 faultAddr, UINT32 pid, EXC_CONTEXT_S *excBufAddr) { ⑴ UINT16 tmpFlag = (excType >> 16) & OS_NULL_SHORT; /* 16: Get Exception Type */ ⑵ g_intCount++; g_excInfo.nestCnt++; ⑶ g_excInfo.type = excType & OS_NULL_SHORT; ⑷ if (tmpFlag & OS_EXC_FLAG_FAULTADDR_VALID) { g_excInfo.faultAddr = faultAddr; } else { g_excInfo.faultAddr = OS_EXC_IMPRECISE_ACCESS_ADDR; } ⑸ if (g_losTask.runTask != NULL) { if (tmpFlag & OS_EXC_FLAG_IN_HWI) { g_excInfo.phase = OS_EXC_IN_HWI; g_excInfo.thrdPid = pid; } else { g_excInfo.phase = OS_EXC_IN_TASK; g_excInfo.thrdPid = g_losTask.runTask->taskID; } } else { g_excInfo.phase = OS_EXC_IN_INIT; g_excInfo.thrdPid = OS_NULL_INT; } ⑹ if (excType & OS_EXC_FLAG_NO_FLOAT) { g_excInfo.context = (EXC_CONTEXT_S *)((CHAR *)excBufAddr - LOS_OFF_SET_OF(EXC_CONTEXT_S, uwR4)); } else { g_excInfo.context = excBufAddr; } ⑺ OsDoExcHook(EXC_INTERRUPT); OsExcInfoDisplay(&g_excInfo); HalSysExit(); }
4、Los_Exc異常處理彙編函數
上文介紹Vector table向量表時,已經提到了在文件kernel\arch\arm\cortex-m7\gcc\los_exc.S中定義的的異常處理函數,如下。當發生Fault故障異常時,會調度執行這些異常處理函數,本節會詳細分析函數的源代碼來掌握內核如何處理這些發生的異常。這6個函數處理過程類似,我們選擇2個典型的函數進行分析。
.global HalExcNMI .global HalExcHardFault .global HalExcMemFault .global HalExcBusFault .global HalExcUsageFault .global HalExcSvcCall
4.1 HalExcNMI
當發生NMI(Non Maskable Interrupt,不可屏蔽中斷)時,會觸發運行HalExcNMI彙編函數,該函數的執行流程如下圖。下文會結合該流程圖來閱讀函數代碼。
HalExcNMI函數代碼如下,⑴處給R0寄存器賦值OS_EXC_CAUSE_NMI,該值等於16,對應文件kernel\arch\arm\cortex-m7\gcc\los_arch_interrupt.h中的異常類型宏定義OS_EXC_CAUSE_NMI,均為16。該值對應HalExcHandleEntry函數的第一個參數。⑵處設置故障地址,該值對應HalExcHandleEntry函數的第二個參數。⑶處跳轉到函數osExcDispatch繼續執行。
.type HalExcNMI, %function .global HalExcNMI HalExcNMI: .fnstart .cantunwind ⑴ MOV R0, #OS_EXC_CAUSE_NMI ⑵ MOV R1, #0 ⑶ B osExcDispatch .fnend
下面分析的一些函數比較通用,其他異常處理函數也都會調用。
4.1.1 osExcDispatch函數
osExcDispatch函數代碼如下,⑴處加載Interrupt Active Bit Registers中斷活躍位寄存器基地址。中斷活躍位寄存器共有8個,NVIC_IABR0-NVIC_IABR7,每個寄存器包含32位,可以對應32個中斷號,共支持256個中斷。其中,IABR[0]的 bit位0~31 分別對應中斷號0~31;IABR[1]的bit位0~31對應中斷32~63;其他以此類推。⑵處設置循環計數,對應8個寄存器,後文會循環遍歷8個寄存器查詢是否存在活躍的中斷。
.type osExcDispatch, %function .global osExcDispatch osExcDispatch: .fnstart .cantunwind ⑴ LDR R2, =OS_NVIC_ACT_BASE ⑵ MOV R12, #8 // R12 is hwi check loop counter .fnend
4.1.2 _hwiActiveCheck函數
執行完上述osExcDispatch函數代碼後,會繼續執行隨後的函數_hwiActiveCheck的代碼。⑴處讀取活躍位寄存器的數值,然後執行⑵比較寄存器數值與0的大小,如果相等,說明該活躍位寄存器對應的中斷均不活躍,然後跳轉到_hwiActiveCheckNext。如果不等於0,則執行⑶,參數類型的高16位標記為中斷。⑷處代碼根據中斷活躍位計算中斷號,並賦值給寄存器R2,該值對應HalExcHandleEntry函數的第三個參數。具體計算方式為,首先反轉活躍中斷位寄存器數值R3,並保存到R2,然後計算高位0的數量。把計數值R12加1,然後左移5位(等於乘以32),然後加上R2,就是中斷號。
.type _hwiActiveCheck, %function .global _hwiActiveCheck _hwiActiveCheck: .fnstart .cantunwind ⑴ LDR R3, [R2] // R3 store active hwi register when exc ⑵ CMP R3, #0 BEQ _hwiActiveCheckNext // exc occurred in IRQ ⑶ ORR R0, R0, #FLAG_HWI_ACTIVE ⑷ RBIT R2, R3 CLZ R2, R2 AND R12, R12, #1 ADD R2, R2, R12, LSL #5 // calculate R2 (hwi number) as pid .fnend
4.1.3 _ExcInMSP函數和_NoFloatInMsp函數
如果有活躍的中斷,則繼續執行後續的代碼。處理中斷時,使用的主棧處理函數_ExcInMSP。⑴處比較異常返回值和#0XFFFFFFED的大小,如果相等說明支持浮點計算則繼續執行後續代碼,如果不相等則不支持浮點計算,會跳轉到函數_NoFloatInMsp函數。有關異常返回值的更多信息請參考《Cortex™-M7 Devices Generic User Guide》表格Table 2-15 Exception return behavior。
如果支持浮點計算時,執行⑵把棧指針加上104賦值給R3寄存器,然後壓棧,該值對應HalExcHandleEntry函數的第四個參數。104的大小應該來源於結構體EXC_CONTEXT_S。⑶處把寄存器PRIMASK數值複製到R12寄存器,然後把R4-R12寄存器壓棧。⑷處把浮點寄存器壓棧,⑸處跳轉到函數_handleEntry。
當不支持浮點計算時,執行函數_NoFloatInMsp。⑹處把棧指針加上32賦值給R3寄存器,然後壓棧,該值對應HalExcHandleEntry函數的第四個參數。然後把R3壓棧,把寄存器PRIMASK數值複製到R12,然後壓棧R4-R12。和支持浮點時的差別就是,不需要壓棧D8-D15寄存器。⑺處把參數類型高位上加上不支持浮點的標記,然後跳轉到函數_handleEntry。
.type _ExcInMSP, %function .global _ExcInMSP _ExcInMSP: .fnstart .cantunwind ⑴ CMP LR, #0XFFFFFFED BNE _NoFloatInMsp ⑵ ADD R3, R13, #104 PUSH {R3} ⑶ MRS R12, PRIMASK // store message-->exc: disable int? PUSH {R4-R12} // store message-->exc: {R4-R12} #if ((defined(__FPU_PRESENT) && (__FPU_PRESENT == 1U)) && \ (defined(__FPU_USED) && (__FPU_USED == 1U))) ⑷ VPUSH {D8-D15} #endif ⑸ B _handleEntry .fnend .type _NoFloatInMsp, %function .global _NoFloatInMsp _NoFloatInMsp: .fnstart .cantunwind ⑹ ADD R3, R13, #32 PUSH {R3} // save IRQ SP // store message-->exc: MSP(R13) MRS R12, PRIMASK // store message-->exc: disable int? PUSH {R4-R12} // store message-->exc: {R4-R12} ⑺ ORR R0, R0, #FLAG_NO_FLOAT B _handleEntry .fnend
4.1.4 _hwiActiveCheckNext函數
遍歷中斷活躍位寄存器時,如果前一個寄存器沒有活躍的中斷則執行函數_hwiActiveCheckNext判斷下一個寄存器是否有活躍的中斷。⑴處把活躍位寄存器地址偏移4位元組,計數減1,如果還有其他活躍位寄存器,則跳轉到函數_hwiActiveCheck繼續判斷。否則執行後續的代碼,⑵處加載System Handler Control and State Register(縮寫SHCSRS)系統處理控制與狀態寄存器的地址,然後加載半位元組數值。⑶處加載掩碼0xC00,該數值二進制的第10、第11位為1。SHCSRS寄存器的第11位對應SysTick異常活躍位,第10位對應PendSV異常活躍位。⑷處R2、R3進行邏輯與計算,然後把結果與0進行比較,如果結果為0,說明沒有發生ysTick異常或PendSV異常。如果結果為1,說明發生了異常,需要執行⑸跳轉到函數_ExcInMSP繼續執行,上文已分析該函數。⑹處獲取全局變量g_taskScheduled的地址,然後獲取其數值,與1進行比較。如果等於1,說明系統已經開始任務調度,會繼續執行後續的代碼。如果不為1,系統未調度,處於初始化階段,需要跳轉到函數_ExcInMSP繼續執行。
如果系統開始了任務調度,此時使用進程棧PSP,執行⑺,判斷系統是否支持浮點計算。如果支持則繼續執行,否則跳轉到函數_NoFloatInPsp。⑻處開始的代碼和函數_NoFloatInPsp可以對比着閱讀,前者需要壓棧浮點寄存器,後者不需要。⑻處把棧指針複製到R2寄存器,然後把棧指針減去96。⑼處把PSP線程棧指針值賦值給R3寄存器,然後把R3加104賦值給寄存器R12,計算出來的值是任務棧指針,然後進行壓棧。
⑽處複製PRIMASK寄存器數值到R12,然後把寄存器R4-R12壓棧,接着壓棧浮點寄存器D8-D15。⑾處從PSP棧指針開始把R4-R11、D8-D15出棧,然後從R13棧指針開始把D8-D15、R4-R11進行壓棧。⑿處跳轉到函數_handleEntry繼續指向。
.type _hwiActiveCheckNext, %function .global _hwiActiveCheckNext _hwiActiveCheckNext: .fnstart .cantunwind ⑴ ADD R2, R2, #4 // next NVIC ACT ADDR SUBS R12, R12, #1 BNE _hwiActiveCheck /*NMI interrupt exception*/ ⑵ LDR R2, =OS_NVIC_SHCSRS LDRH R2,[R2] ⑶ LDR R3,=OS_NVIC_SHCSR_MASK ⑷ AND R2, R2,R3 CMP R2,#0 ⑸ BNE _ExcInMSP // exc occured in Task or Init or exc // reserved for register info from task stack ⑹ LDR R2, =g_taskScheduled LDR R2, [R2] TST R2, #1 // OS_FLG_BGD_ACTIVE BEQ _ExcInMSP // if exc occurred in Init then branch ⑺ CMP LR, #0xFFFFFFED //auto push floating registers BNE _NoFloatInPsp // exc occurred in Task ⑻ MOV R2, R13 SUB R13, #96 // add 8 Bytes reg(for STMFD) ⑼ MRS R3, PSP ADD R12, R3, #104 PUSH {R12} // save task SP ⑽ MRS R12, PRIMASK PUSH {R4-R12} VPUSH {D8-D15} // copy auto saved task register ⑾ LDMFD R3!, {R4-R11} // R4-R11 store PSP reg(auto push when exc in task) VLDMIA R3!, {D8-D15} VSTMDB R2!, {D8-D15} STMFD R2!, {R4-R11} ⑿ B _handleEntry .fnend .type _NoFloatInPsp, %function .global _NoFloatInPsp _NoFloatInPsp: .fnstart .cantunwind MOV R2, R13 // no auto push floating registers SUB R13, #32 // add 8 Bytes reg(for STMFD) MRS R3, PSP ADD R12, R3, #32 PUSH {R12} // save task SP MRS R12, PRIMASK PUSH {R4-R12} LDMFD R3, {R4-R11} // R4-R11 store PSP reg(auto push when exc in task) STMFD R2!, {R4-R11} ORR R0, R0, #FLAG_NO_FLOAT .fnend
4.1.5 _handleEntry函數
繼續分析函數_handleEntry。代碼很簡單,⑴把棧指針複製給R3,該值對應HalExcHandleEntry函數的第四個參數。⑵處關閉中斷,關閉Fault異常,然後執行⑵跳轉到C語言的函數HalExcHandleEntry。
_handleEntry: .fnstart .cantunwind ⑴ MOV R3, R13 // R13:the 4th param ⑵ CPSID I CPSID F B HalExcHandleEntry NOP .fnend
4.2 HalExcUsageFault
當發生使用異常UsageFault時,會觸發運行HalExcUsageFault彙編函數,該函數的執行流程如下圖。下文會結合該流程圖來閱讀函數代碼。
HalExcUsageFault函數代碼如下,⑴處把可配置故障狀態寄存器Configurable Fault Status Register(CFSR)的地址複製到R0寄存器,然後讀取寄存器值到R0寄存器。⑵處把0x030F賦值給R1寄存器,然後左移16位。UsageFault Status Register使用故障狀態寄存器的有效性如下,即0-3,8-9為有效位,0x030F的二進制對應這些有效位。⑶處進行邏輯與,這樣就計算出實際的使用故障對應的bit位。⑷處把R12賦值為0,然後會繼續執行後續的彙編代碼osExcCommonBMU。
.type HalExcUsageFault, %function .global HalExcUsageFault HalExcUsageFault: .fnstart .cantunwind ⑴ LDR R0, =OS_NVIC_FSR LDR R0, [R0] ⑵ MOVW R1, #0x030F LSL R1, R1, #16 ⑶ AND R0, R0, R1 ⑷ MOV R12, #0 .fnend
4.2.1 g_uwExcTbl數組
在看osExcCommonBMU函數的代碼之前需要了解下g_uwExcTbl數組,g_uwExcTbl數組定義在文件kernel\arch\arm\cortex-m7\gcc\los_interrupt.c,代碼如下。
該數組包含32個元素,每個元素對應CFSR寄存器的一個bit位,元素數值在LiteOS-M中定義為異常類型。比如OS_EXC_UF_DIVBYZERO等於異常類型10,為除零異常。
UINT8 g_uwExcTbl[FAULT_STATUS_REG_BIT] = { 0, 0, 0, 0, 0, 0, OS_EXC_UF_DIVBYZERO, OS_EXC_UF_UNALIGNED, 0, 0, 0, 0, OS_EXC_UF_NOCP, OS_EXC_UF_INVPC, OS_EXC_UF_INVSTATE, OS_EXC_UF_UNDEFINSTR, 0, 0, 0, OS_EXC_BF_STKERR, OS_EXC_BF_UNSTKERR, OS_EXC_BF_IMPRECISERR, OS_EXC_BF_PRECISERR, OS_EXC_BF_IBUSERR, 0, 0, 0, OS_EXC_MF_MSTKERR, OS_EXC_MF_MUNSTKERR, 0, OS_EXC_MF_DACCVIOL, OS_EXC_MF_IACCVIOL };
4.2.2 osExcCommonBMU函數
現在來分析下彙編代碼osExcCommonBMU。⑴處計算出R0數值的高位0的個數,加載數組全局變量g_uwExcTbl地址到R3寄存器,然後執行⑵計算是第幾個數組元素,加載元素值到R0寄存器。⑶處R0與R12進行邏輯或運算,沒有什麼影響。R0對應HalExcHandleEntry函數的第一個參數。後續會繼續執行osExcDispatch函數,前文已經分析過。
.type osExcCommonBMU, %function .global osExcCommonBMU osExcCommonBMU: .fnstart .cantunwind ⑴ CLZ R0, R0 LDR R3, =g_uwExcTbl ⑵ ADD R3, R3, R0 LDRB R0, [R3] ⑶ ORR R0, R0, R12 .fnend
小結
本文介紹了Fault異常類型,向量表及其代碼,異常處理C語言程序,異常處理彙編函數實現代碼。感謝閱讀,如有任何問題、建議,都可以博客下留言給我,謝謝。
參考資料
- Cortex™-M7 Devices Generic User Guide Download