Documents/ARM Exceptions

From Nutwiki
Jump to: navigation, search

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.