Documents/ARM Exceptions
Contents
ARM Exceptions
Context Switch |
---|
The procedure of storing and restoring the status of a CPU is called context switching. |
Microprocessors are able to respond to an asynchronous event with a context switch. Typically an external hardware activates a specific input line. This forces the microprocessor to temporarily interrupt the current program sequence and execute a special handler routine. Such events are called interrupts or, more precisely, hardware interupts. On many platforms the term software interrupt is used for context switches initiated by special instructions.
On ARM processors all these interrupts (including hardware reset) are called exceptions. The architecture supports seven processor modes, six privileged modes called FIQ, IRQ, supervisor, abort, undefined and system mode, and the non-privileged user mode. The current mode may change under software control or when processing an exception. However, the non-privileged user mode can switch to another mode only by generating an exception.
When an exception occurs, the processor saves the current status and the return address, enters a specific mode and possibly disables hardware interrupts. Execution is then forced from a fixed memory address called exception vector.
Many ARM processors can run either in 32-bit ARM state or 16-bit thumb state and it should be noted, that the CPU is switched to ARM state before executing the instruction at the exception vector.
The following table provides an overview of ARM exceptions and how they are processed.
Event | Exception | Priority 1 | Return address | Status | Mode | FIQ | IRQ | Vector 2 | Prefered return instruction |
---|---|---|---|---|---|---|---|---|---|
Reset input de-asserted | Reset | 1 | Not available | Not available | Supervisor | Disabled | Disabled | Base+0 | Not available |
Reading from or writing to invalid address | Data Access Memory Abort (Data Abort) | 2 | R14_abt=PC+8 4 | SPSR_abt=CPSR | Abort | Unchanged | Disabled | Base+16 | SUBS PC,R14_abt,#8 8 |
FIQ input asserted | Fast Interrupt (FIQ) | 3 | R14_fiq=PC+4 5 | SPSR_fiq=CPSR | FIQ | Disabled | Disabled | Base+28 7 | SUBS PC,R14_fiq,#4 |
IRQ input asserted | Normal Interrupt (IRQ) | 4 | R14_irq=PC+4 5 | SPSR_irq=CPSR | IRQ | Unchanged | Disabled | Base+24 | SUBS PC,R14_irq,#4 |
Executing BKPT 3 or instruction at invalid address | Instruction Fetch Memory Abort (Prefetch Abort) | 5 | R14_abt=PC+4 6 | SPSR_abt=CPSR | Abort | Unchanged | Disabled | Base+12 | SUBS PC,R14_abt,#4 |
Executing SWI instruction | Software Interrupt (SWI) | 6 | ARM state: R14_svc=PC+4 Thumb state: R14_svc=PC+2 6 |
SPSR_svc=CPSR | Supervisor | Unchanged | Disabled | Base+8 | MOVS PC,R14_svc |
Executing undefined instruction code | Undefined Instruction | 6 | ARM state: R14_und=PC+4 Thumb state: R14_und=PC+2 6 |
SPSR_und=CPSR | Undefined | Unchanged | Disabled | Base+4 | MOVS PC,R14_und |
Note 1: Priority 1 is highest, 6 is lowest.
Note 2: The normal vector base address is 0x00000000. Some implementations allow the vector base address to be moved to 0xFFFF0000.
Note 3: When the instruction at the breakpoint causes a prefetch abort, then the abort request is handled first. When the abort handler fixes the abort condition and returns to the aborted instruction, then the debug request is handled.
Note 4: PC is the address of the instruction that caused the data abort.
Note 5: PC is the address of the instruction that did not get executed after the interrupt occured.
Note 6: PC is the address of the SWI, BKPT or undefined instruction or the instruction that had the prefetch abort.
Note 7: Intentionally the FIQ vector is placed at the end of the vector table. No additional branch is required. The handler can directly start at this location.
Note 8: This re-executes the aborted instruction. If this is not intended, use SUBS PC,R14_abt,#4 instead.
ARM Exceptions and Nut/OS
Initially designed for AVR microcontrollers, Nut/OS provides handlers for hardware interrupts (IRQ and FIQ exceptions) only. Let's see, what would be the benefit of additional handlers.
The ARM7TDMI used on Ethernut 3, for example, allows to configure (remap) its memory address areas during initialization. On reset, external Flash memory is located at addresses 0x00000000 to 0x000FFFFF and the on-chip RAM is at 0x00300000 to 0x0033FFFF. These locations are then remapped by either
- the boot loader or
- the Nut/OS initialization of a flashed application or
- any programmer utility, like JTAG-O-MAT or OpenOCD.
For Ethernut 3 a typical memory layout is:
- 0x00000000 - 0x0003FFFF RAM
- 0x10000000 - 0x103FFFFF FLASH
- 0x20000000 - 0x200FFFFF Ethernet Controller
- 0x21000000 - 0x210FFFFF CPLD Registers
- 0x22000000 - 0x220FFFFF Expansion Port Memory Bus
If the firmware tries to read from or write to any other, unspecified memory location, the CPU generates a data abort exception. When the CPU tries to read an instruction from an unspecified memory area, a prefetch abort exception is generated. Last not least, trying to execute an invalid instruction code generates an undefined instruction exception.
By default, no routines were available to handle such abort exceptions. As you can imagine, they are most useful for debugging and luckily had been included recently (Nut/OS Version 4.7.5 and above).
Analyzing an Abort Exception
This chapter will explain internal details of abort exception processing and present an example of a custom exception handler. You can skip it, if you simply want to enable the Nut/OS default handler that had been introduced in version 4.7.5.
As stated above, abort exception are not processed in Nut/OS versions older than 4.7.5. This is not fully correct, though. In fact, in case of an exception, Nut/OS enters an endless loop, which actually freezes the system.
Let's examine the following bad Nut/OS application.
#include <stdio.h> #include <io.h> #include <dev/board.h> #include <sys/timer.h> #include <sys/version.h> int main(void) { u_long baud = 115200; int *bad; /* * Register and initialize the DEBUG device as stdout. */ NutRegisterDevice(&DEV_DEBUG, 0, 0); freopen(DEV_DEBUG_NAME, "w", stdout); _ioctl(_fileno(stdout), UART_SETSPEED, &baud); /* * Print a banner, so we can show that we are running. */ printf("\n\nData Abort Sample - Nut/OS %s\n", NutVersionString()); /* * Set a pointer to a bad memory address. */ bad = (u_long *)0x09000000; /* * This will crash. */ *bad = 0x12345678; /* * We will never reach this point. */ puts("Brave new world!"); return 0; }
Compiling this code and uploading it to your ARM based target board should result in the following output on the RS-232/DBGU interface.
Data Abort Sample - Nut/OS 4.0.2.1
As expected, the system freezes and doesn't execute any statement beyond the false pointer usage. If a JTAG adapter (e.g. Turtelizer) is connected, we can use the jtagomat or any similar utility to stop the CPU and query its current program counter value.
$ jtagomat -v HALT Turtelizer 1.2.4 $ jtagomat LOAD PC 1 STDOUT PC 0x00000038
As we can see, the CPU stopped with the program counter pointing to memory address 0x00000038. The last executed instruction was at 0x00000034.
A look to the linker map file of our application shows, that this is the location of several labels.
0x00000034 __xcpt_dummy 0x00000034 __swi 0x00000034 __data_abort 0x00000034 __prefetch_abort 0x00000034 __undef
Let's assume, that our application is running on the Ethernut 3 board, uploaded to internal RAM by the boot loader. The related source code is found in arch/arm/init/crtat91_ram.S. Source files for other target boards are available in the same directory and contain almost the same code. Even if you are not familiar with ARM assembly code, you may recognize the exception vectors, which are all set to a label named __xcpt_dummy, which in turn is a label to an endless loop. The instruction "b" means branch (jump to). Thus, the code at __xcpt_dummy jumps at itself in an endless loop.
.global __vectors __vectors: ldr pc, [pc, #24] /* Reset */ ldr pc, [pc, #24] /* Undefined instruction */ ldr pc, [pc, #24] /* Software interrupt */ ldr pc, [pc, #24] /* Prefetch abort */ ldr pc, [pc, #24] /* Data abort */ ldr pc, [pc, #24] /* Reserved */ /* * On IRQ the PC will be loaded from AIC_IVR, which * provides the address previously set in AIC_SVR. * The interrupt routine will be called in ARM_MODE_IRQ * with IRQ disabled and FIQ unchanged. */ ldr pc, [pc, #-0xF20] /* Interrupt request, auto vectoring. */ ldr pc, [pc, #-0xF20] /* Fast interrupt request, auto vectoring. */ .word _start .word __undef .word __swi .word __prefetch_abort .word __data_abort .weak __undef .set __undef, __xcpt_dummy .weak __swi .set __swi, __xcpt_dummy .weak __prefetch_abort .set __prefetch_abort, __xcpt_dummy .weak __data_abort .set __data_abort, __xcpt_dummy .global __xcpt_dummy __xcpt_dummy: b __xcpt_dummy
You probably will agree, that jumping to an endless loop is not very helpful. We will now add an exemplary data abort handler to our application.
But first lets use the debugger (jtagomat in our case) to retrieve some more useful information from the CPU. The following command queries the contents of the link register.
$ jtagomat LOAD LR 1 STDOUT LR 0x00000560
When checking the table in the first chapter, we can see that the address of the instruction that generated the exception is stored in the link register r14, with an offset of 8. In our case this is address 0x00000560. By consulting the linker map file again, we are able to find out that this is located between labels NutAppMain and NutInit.
0x000004d0 NutAppMain 0x000005dc NutInit
This requires some more explanations. First, C function entries will become labels in ARM assembly code, or more exactly, binary linker code. NutInit is the Nut/OS initialization routine, which is typically linked immediately after the application code. NutAppMain is something Nut/OS specific. It is actually the main() routine, but redefined to NutAppMain in order to fool the compiler and make it believe, that it is nothing special. This is required, because some compiler indeed treat main() very special and in this case may break the Nut/OS multithreading support.
The result of this lengthy explanations: We proofed, that the exception appeared in our main routine.
But I promised to present an exception handler. Here it is.
void __data_abort(void) __attribute__ ((naked)); void __data_abort(void) { puts("Data Abort\n"); for(;;); }
Simply add this routine to the simple application we used above to generate the data abort exception.
Luckily the DEBUG device allows us to use printf and other stdio functions within exception context. Now our application produces the following result.
Data Abort Sample - Nut/OS 4.0.2.1 Data Abort
Obviously it works, but the more sceptical among us may ask, how this can be? What happened to the endless loop at __xcpt_dummy? Well, if you check the assembly code above, you will notice that most exception vectors are defined as weak. That means, that any non weak definition will override that initial one. And that is exactly what our C function __data_abort(void) does: It replaces the weak label of the Nut/OS default handler.
You can imagine, that an exception handler is different from normal C functions. In order to create pure code without any specific C language treatment, we added the "naked" attribute to the function.
Now we have code which informs us that a data abort exception has happened. This is a big advantage compared to our first, silently frozen application. However, it would be helpful to display the program location at which the exception occured. We learned, that the contents of the link register is most valuable. The following enhanced handler will retrieve the contents of this register by using inline assembly code.
void __data_abort(void) __attribute__ ((naked)); void __data_abort(void) { register u_long *lnk_ptr; __asm__ __volatile__ ( "sub lr, lr, #8\n" "mov %0, lr" : "=r" (lnk_ptr) ); /* On data abort exception the LR points to PC+8 */ printf("Data Abort at %p 0x%08lX\n", lnk_ptr, *(lnk_ptr)); for(;;); }
The advanced handler does not only display the location but also the instruction code at that location.
Data Abort Sample - Nut/OS 4.1.4.1 pre Data Abort at 0x558 0xE5823000
We can verify the result by checking the application's listing file, which had been produced by the compiler.
0078 0934A0E3 mov r3, #150994944 @ tmp75, 007c 14300BE5 str r3, [fp, #-20] @ tmp75, bad 0080 14201BE5 ldr r2, [fp, #-20] @ bad, bad 0084 30309FE5 ldr r3, .L2+20 @ tmp77, 0088 003082E5 str r3, [r2, #0] @ tmp77,* bad
Register r3 is loaded with 150994944 (decimal), which is equal to 0x90000000. Obviously this is our bad pointer. The next statement at 0x007C stores this value to the pointer's memory location (fp is the frame pointer register). Then it loads register r2 with the pointer value, loads register r3 with the constant 0x12345678 (stored at label .L2) and finally tries to store the constant in register r3 to the bad location stored in r2. This last instruction initiates the data abort exception. Note, that the compiler listing shows the instruction codes in reversed byte order. Note further, that the memory addresses in the compiler listing are relative. Absolute addresses are calculated by the linker and will be found in the linker map file.
Enabling Nut/OS Abort Exception Handling
Since version 4.7.5 Nut/OS comes with build-in abort exception handling. The original code had been released as part of the LostARM Project under GPL Version 2 and is published for Nut/OS under the BSD license with kind permission from the author, Duane Ellis.
Nut/OS exception handling is not enabled by default. First you need to make sure, that the compiler maintains stack frame pointers. This is required because the Nut/OS exception handler uses them to generate a backtrace. That means, it will print a list of the addresses at which functions (subroutines) had been called. By default, Nut/OS is compiled with option -fomit-frame-pointer
, where frame pointers are not saved in order to reduce memory usage. Choosing arm-gccdbg instead of arm-gcc as a platform in the Configurator will select a different set of compile options, where, among other things, frame pointers are added to the stack. Alternatively you may manually remove the -fomit-frame-pointer
option from Makedefs.arm-gcc and app/Makedefs.arm-gcc.
Next we need to add the exception handler object files to the LIBS list in our application's Makefile. Best add them to the front.
LIBS = $(LIBDIR)/arm-da.o $(LIBDIR)/arm-pfa.o $(LIBDIR)/arm-udf.o \ $(LIBDIR)/nutinit.o -lnutos -lnutdev -lnutarch -lnutcrt
The following exceptions handlers are available:
- arm-da.o
Data abort, initiated when trying to access bad memory locations.
- arm-pfa.o
Prefetch abort, initiated when trying to read the next instruction from a bad memory location.
- arm-swi.o
Software interrupt.
- arm-udf.o
Undefined instruction abort, initiated when trying to execute bad instruction code.
The exception handler will print to stdout. Thus, you need to make sure, that a device has been assigned to stdout and that the related device driver is a so called debug device driver.
Finally you must rebuild Nut/OS and your application. Here is an example of a crashing application. It's basically the same one as presented above, but does several nested function calls to demonstrate backtracing.
#include <stdio.h> #include <io.h> #include <dev/board.h> #include <sys/timer.h> #include <sys/version.h> int global_int; void sub3(void) { int *bad = (int *)0x09000000; printf("Bye bye\n"); *bad = 0x12345678; } void sub2(void) { int *good = &global_int; *good = 2; printf("In sub%d\n", global_int); sub3(); } void sub1(void) { int *good = &global_int; *good = 1; printf("In sub%d\n", global_int); sub2(); } /* * Main application routine. */ int main(void) { u_long baud = 115200; /* * Register and initialize the DEBUG device as stdout. */ NutRegisterDevice(&DEV_DEBUG, 0, 0); freopen(DEV_DEBUG_NAME, "w", stdout); _ioctl(_fileno(stdout), UART_SETSPEED, &baud); /* * Print a banner, so we can show that we are running. */ printf("\n\nData Abort Sample - Nut/OS %s\n", NutVersionString()); sub1(); /* * We will never reach this point. */ puts("Brave new world!"); for (;;) { NutSleep(1000); putchar('.'); } return 0; }
This sample will produce the following output:
Data Abort Sample - Nut/OS 4.7.5.0 In sub1 In sub2 Bye bye Unexpected: DA R0 : 0x00000000 R8 : 0xaa55aa55 R1 : 0x0000000d R9 : 0x55aa55aa R2 : 0x09000000 R10: 0xaa55aa55 R3 : 0x12345678 R11: 0x20000f94 R4 : 0xaa55aa55 R12: 0x20000ee8 R5 : 0x55aa55aa R13: 0x20000f34 R6 : 0xaa55aa55 R14: 0x000002cc R7 : 0x55aa55aa R15: 0x000002d8 PSW: 0x600000df nZCv...FIt sys-mode Backtrace: 0) 0x000002bc 1) 0x000002fc 2) 0x0000034c 3) 0x0000039c
To interpret the backtrace, we look into the linker map file, where we find the start addresses of all public functions:
.text 0x000002ac 0x198 testxcept.o 0x000002ac sub3 0x000002ec sub2 0x0000033c sub1 0x0000038c main
The exception occured at 0x000002bc, which is located in sub3. This was called at 0x000002fc in sub2, which in turn was called at 0x0000034c in sub1, which in turn was called at 0x0000039c in our main routine.
Alternatively you can use the addr2line tool, which is part of the GCC binutils installed with your GCC cross toolchain:
arm-elf-addr2line -f -e example.elf 0x000002bc
Early stdio Initialization
When debugging applications it is sufficient to have stdout available at the beginning of the main routine. We are in trouble, if we modified Nut/OS itself and experience exceptions before main is called. The exception handler itself will crash when trying to send output to a non-exisiting stdout stream. This chapter will provide a solution.
We assume, that the initialization code is running fine. It is typically written in assembly language and hard to debug without a JTAG debugger. At the end of the initialization NutInit() will be called. Here we will setup stdout. For ARM targets it is located in arch/arm/os/nutinit.c.
Open the file in your favorite editor and add the following lines directly before the NutInit() function.
#ifdef EARLY_STDIO_DEV #include <sys/device.h> #include <stdio.h> #include <fcntl.h> struct __iobuf { int iob_fd; uint16_t iob_mode; uint8_t iob_flags; int iob_unget; }; #endif
This part provides all required declarations. At the beginning of NutInit() we can now setup the stdout stream. You may place the following code immediately after all other hardware initialization had been done, typically before NutHeapAdd() is called:
#ifdef EARLY_STDIO_DEV { extern NUTDEVICE EARLY_STDIO_DEV; static struct __iobuf early_stdout; EARLY_STDIO_DEV.dev_init(&EARLY_STDIO_DEV); stdout = &early_stdout; stdout->iob_fd = (int)EARLY_STDIO_DEV.dev_open(&EARLY_STDIO_DEV, "", 0, 0); stdout->iob_mode = _O_WRONLY | _O_CREAT | _O_TRUNC; puts("\nNutInit"); } #endif
You may have noticed, that all code had been enclosed in pre-processor statements, which makes it easier to enable and disable it. To enable early stdio, add
#define EARLY_STDIO_DEV devDebug
at the top of the file or add
HWDEF+=-DEARLY_STDIO_DEV=devDebug
to the UserConf.mk file in your build tree. devDebug is the right device for AT91 family member, which have a dedicated DBGU port. For other device, like the AT91R40008 on Ethernut 3, it is typically replaced by devDebug0.
Finally you need to rebuild the Nut/OS libraries and the application code. If everything works as designed, you should see the following output at your serial debug port immediately after the system is restarted:
NutInit
Early abort exceptions are now reported and you can add additional printf() calls to any part of the system, even inside interrupt routines.
Conclusion
In opposite to standard desktop PCs, embedded systems are quite different and require different actions in case of fatal errors. The demonstrated method of adding a custom abort exception handler allows to re-act on such events in an application conformant way, while the build-in Nut/OS handlers provided additional help during debugging.
Harald Kipp
Castrop-Rauxel, 26th of June 2009
Copyright
Copyright (C) 2008-2009 by Harald Kipp. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.3 or any later version published by the Free Software Foundation.
Document History
Date Change Thanks to 2009/06/26 Corrected R14_abt content on prefetch abort, which is the same in Thumb and ARM state. Stephen M. Rumble Note 3 added to the overview table. Added copyright notice.