ARM Cortex-M çekirdeklerinde Hata Analizi (Fault Exception Analysis)

Serbay Özkan
6 min readJun 13, 2021

Bu yazıyı okumadan önce konunun daha iyi anlaşılması için öncelikle aşağıdaki yazıyı okumanızı tavsiye ederim.

ARM Cortex-M çekirdeklerinde Fault Exception olarak ele alınan hata türleri aşağıdaki gibidir.

  • HardFault
  • MemManage Fault
  • UsageFault
  • BusFault

Bu hatalar oluştuğu zaman çekirdek, vektör tablosuna yerleştirilmiş hata işleyicilerini (fault handler) çağırarak hatanın servis edilmesini sağlar.

Hardfault her zaman varsayılan olarak aktif durumdadır ve sabitlenmiş bir önceliğe (hatalar arasında en yüksek öncelik) sahiptir. Diğer hataların ise öncelikleri değiştirilebilmektedir. Reset sürecinden sonra HardFault dışındaki bütün hatalar varsayılan olarak devre dışıdır ve daha sonra System Control Block (SCB) kullanılarak aktif hale getirilebilmektedirler.

Öncelik Yükselmesi (Priority Escalation)

Öncelik ve maske registerları çekirdeğin exception’u servis edip etmemesine ve exception işleyicisinin başka bir exception tarafından bölünüp bölünmemesine karar vermektedir.

Bazı durumlarda ayarlanabilir önceliğe sahip bir hata HardFault olarak ele alınabilir. Buna priority escalation denilmektedir ve o hata HardFault’a yükselmiş (Escalation to Hardfault) hata olarak tanımlanmaktadır. Hardfault’a yükselme durumu aşağıdaki durumlarda meydana gelmektedir.

  • Hata işleyicisi servis edilirken tekrardan aynı hataya sebebiyet verilmesi. Hata işleyicisi kendi kendini bölemediği (preempt) için bu hata HardFault’a yükseltilir.
  • Hata işleyicisi servis edilirken kendisiyle aynı veya daha düşük öncelikte bir hata oluşturması. Bu durumda yeni oluşan hata o anda servis edilen hatayı bölemeyeceği için hata HardFault’a yükseltilir.
  • Exception işleyicisi servis edilirken kendisiyle aynı veya daha düşük öncelikte bir hata oluşturması.
  • Hata oluştuğunda o hatanın servis edilmesi için gerekli işleyicinin aktif edilmemesi

ARM Cortex-M çekirdeklerinde oluşan hata çeşidine bağlı olarak hata nedenlerini gösteren güzel bir tablo aşağıda yer almaktadır. Hata analizi yaparken bu tablodan oldukça faydalanılmaktadır.

ARM Cortex-M çekirdeğine sahip bir mikrodenetleyici ile çalışıyorken genelde kolay yazılım geliştirme süreci oluşturmak amacıyla hazır araçlarla donanmış bir IDE (Code Composer Studio, Keil uVision, CodeWarrior, IAR, STM32CubeIDE …) kullanırız. Bu IDE’lerin bazılarının içerisinde aşağıdaki görsellerde de belirtilen hata analizi için gömülü araçlar bulunmaktadır ve bu araçlar sayesinde çekirdek seviyesinde bir hata gerçekleştiği zaman hatanın türü ve hatanın gerçekleşmeden önceki yerinin tahmin edilmesi veya direk belirlenmesi için gerekli stack frame bilgileri sağlanmaktadır.

STM32CubeIDE Fault Analyzer
Keil uVision Fault Report

Peki bu araçların yaptığı gibi biz de çekirdek hatalarının türünü, nedenini ve hata gerçekleşmeden önceki stack frame’ini nasıl elde edebiliriz?

Bir hata gerçekleştiğinde çekirdek tarafından SHCSR (System Handler Control and State Register) register’i otomatik olarak güncellenmektedir. Bu register ile bir hata servis edildiğinde o hatanın türünü anlayabilmemiz mümkündür.

SHCSR *(ARM Cortex M33 çekirdeğine özel olarak bu bit anlamlandırması değişmektedir ve ek olarak Secure Fault bitleri gelmektedir.)

Sonu ENA ile biten bitler ilgili exception’un aktif edilmesi (enable) için kullanılmaktadır. Sonu PENDED ile biten bitler ise exception’un hem pending durumunu belirtmekte hem de pending durumunun set/reset edilmesine imkan vermektedir. Yani bu bitleri kullanarak tabloda pending bitleri belirtilmiş exceptionları yazılımsal olarak tetikleyebilmemiz mümkündür. Örneğin aşağıda yazdığım kod parçası ile ARM Cortex M3/M4/M7/M33 tabanlı herhangi bir mikrodenetleyicide UsageFault exception’unu manuel olarak tetikleyebilirsiniz.

Sonu ACT ile biten bitler de hangi exception’un aktif olduğunu belirtmektedir. Yani bir hata gerçekleştiği zaman hatanın türünü aslında bu bitler sayesinde anlamaktayız.

SHCSR register’da yer alan sonu ACT ile biten bitlere bakarak hatanın türünü anlayabiliyoruz dedik peki hatanın nedenini nasıl anlayabiliriz?

Sistemde bir exception oluştuğunda çekirdek sadece SHCSR register’ini otomatik olarak güncellemez. Bunun yanında hangi hata meydana gelmişse o hata türüne ait özel registerlarını da değiştirir. Hatanın türünü artık bildiğimiz için o hata türüne ait register’a bakarak hatanın sebebini anlayabilmemiz mümkündür.

  • MMFSR (MemManage Fault Status Register)
  • BFSR (BusFault Status Register)
  • UFSR (UsageFault Status Register)
  • HFSR (HardFault Register)

Bu registerların her bitinde ilgili hata türüne ait hata nedeni bilgileri yer almaktadır. Hata oluştuktan sonra bu bitlerin hangisi setlenmiş ise hatanın nedenini setlenmiş bitin anlamına bakarak anlayabilmemiz mümkündür.

Örnek olarak UsageFault Status register’ina baktığımızda aşağıdaki gibi bir bit/hata anlamdırmasının olduğunu görmekteyiz.

Sonuç olarak bir hata oluştuğunda hem SHCSR hem de hata status registerlarını parse eden bir yazılım yazmamız halinde hatanın hem türünü hem de nedenini anlayabilmemiz mümkündür.

Hatanın türünü ve nedenini nasıl bulacağımızı öğrendik ancak bu hata ayıklama süreçleri için yeterli bir bilgi değil. Hatanın nerede oluştuğunu da bilmeliyiz ki hatanın düzeltilmesi noktasında doğru bir yaklaşım sergileyebilelim. Hatanın nerede oluştuğunu anlayabilmemiz için exception servis edilmeden önce en son nerede olduğumuzu bilmemiz gerekiyor. Bunun için de çekirdek tarafından exception servis edildiğinde otomatik olarak stack’e atılan (stacking) çekirdek registerlarına (stack frame) ulaşmamız gerekmektedir. Böylelikle bu registerları kullanarak hata oluşmadan önce en son hangi çağrı yapılmış stack aktivitesi ne olmuş gibi sorulara yanıt bulabiliriz.

Bir önceki yazımda da belirttiğim üzere bir exception oluştuğunda çekirdek tarafından aşağıdaki registerlar otomatik olarak stack’e atılmaktadır. Böylelikle exception servis edildikten sonra geri dönülmesi gerekiyor ise exception öncesi en son durum bilgileri bu sayede stack’den pop edilerek (unstacking) geri yüklenmektedir.

R0, R1, R2, R3 registerları fonksiyon çağrılarında fonksiyon argümanlarını tutmak için kullanılır. 4'ten fazla fonksiyon argümanı olması durumunda da geri kalan argümanlar stack’de tutulur. R12 register’i ise Intra Procedure Call Stretch Register olarak kullanılmaktadır bir nevi geçici (temporary) register olarak görev yapar. LR (Link Register), PC (Program Counter) ve xPSR (Program Status Register) olarak görev yapmaktadır.

Stack’den yukarıda belirtilen registerları okuyabilmemiz için stack işaretçisinin exception’a girmeden önceki değerini bilmemiz gerekmektedir. Stack işaretçisinin değerine normal C fonksiyonu veya naked (pure) assembly bir fonksiyon içerisinde inline assembly ile MSP register’ını okuyarak elde etmek mümkündür. Eğer normal C fonksiyonu kullanılırsa function prologue/epilogue sürecinde stack işaretçisi üzerinde işlem yapıldığı için exception işleyicimiz normal C fonksiyonu olarak çağrıldığında stack işaretçisi orjinal değerinde olmayacaktır. Bunu önlemek için naked assembly fonksiyon kullanılmaktadır. Function prologue/epilogue süreci naked fonksiyonlar için devre dışı kalmaktadır. Böylelikle önce MSP değerini alıp daha sonradan da asıl çağırmak istediğimiz pure C hata işleyici fonksiyonunu çağırabiliriz.

MSP değeri okunduğu zaman artık elimizde exception oluştuğunda çekirdek tarafından otomatik olarak gerçekleştirilen stacking işlemi sonucunda oluşan stack frame bulunmaktadır. Stack işaretçisi en son R0’ı göstermektedir ve stack işaretçisinin değeri sırayla yükseltilerek (Full Descending) bütün stack frame’in okunabilmesi mümkündür.

Aşağıdaki örnekte 0’a bölme ile kasti olarak UsageFault hatası oluşturulmuştur ve hatanın türü, nedeni ve hata oluşmadan önceki stack frame yapısı yazdırılarak hatayı bulmak için gerekli bütün analiz sağlanmıştır.

Output:UsageFault: DIVBYZERO
R0 : 0x20000000
R1 : 0x20000064
R2 : 0x00000000
R3 : 0x0000001e
R12 : 0x00000000
LR : 0x080005bf
PC : 0x0800029e
xPSR: 0x61000000

Çıktıdan da görüldüğü üzere UsageFault nedenimiz 0’a bölmeden kaynaklıdır. Nerede 0’a bölme yaptığımızı anlamak için PC register’ina bakmamız bize nokta atışı sorunu yakalamamızda yardımcı olacaktır. PC register’ini disassembly edilmiş kodda aradığımızda aşağıdaki gibi hatanın direk yerini bulabiliriz.

/08000280 <main>:
/* Private Function Declerations */
static void printUsageFaultCause(tUsageFault *UsageFault);
/* Main */
int main(void)
{
8000280: b480 push {r7}
8000282: b085 sub sp, #20
8000284: af00 add r7, sp, #0
// Enable UsageFault
volatile uint32_t *SHCSR = (volatile uint32_t *)(SHCSR_ADRESSS);
8000286: 4b08 ldr r3, [pc, #32] ; (80002a8 <main+0x28>)
8000288: 60fb str r3, [r7, #12]
*SHCSR |= (1 << SHCSR_USGFAULTENA_BIT);
800028a: 68fb ldr r3, [r7, #12]
800028c: 681b ldr r3, [r3, #0]
800028e: f443 2280 orr.w r2, r3, #262144 ; 0x40000
8000292: 68fb ldr r3, [r7, #12]
8000294: 601a str r2, [r3, #0]
// Divide by zero error
int val1, val2 = 30;
8000296: 231e movs r3, #30
8000298: 60bb str r3, [r7, #8]
val1 = val2 / 0;
800029a: 68bb ldr r3, [r7, #8]
800029c: 2200 movs r2, #0
800029e: fb93 f3f2 sdiv r3, r3, r2
80002a2: 607b str r3, [r7, #4]
for(;;){
80002a4: e7fe b.n 80002a4 <main+0x24>
80002a6: bf00 nop
80002a8: e000ed24 .word 0xe000ed24
080002ac <_UsageFault_Handler>:
}
}

Diğer yazılarımda görüşmek üzere…

Referanslar ve Faydalı Kaynaklar

--

--