ARM Exceptions
ARM Exceptions
ARM Exceptions
Microprocessors are able to respond to an asynchronous event Context Switch
with a context switch. Typically an external hardware activates a
specific input line. This forces the microprocessor to temporarily The procedure of storing and
interrupt the current program sequence and execute a special restoring the status of a CPU is
handler routine. Such events are called interrupts or, more called context switching.
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.
Prefered return
Event Exception Priority 1 Return address Status Mode FIQ IRQ Vector 2 instruction
Reset Reset 1 Not available Not available Supervisor Disabled Disabled Base+0 Not available
input de-
asserted
Reading Data 2 R14_abt=PC+8 4 SPSR_abt=CPSR Abort Unchanged Disabled Base+16 SUBS PC,R14_abt,#8 8
from or Access
writing to Memory
invalid Abort
address (Data
Abort)
FIQ input Fast 3 R14_fiq=PC+4 5 SPSR_fiq=CPSR FIQ Disabled Disabled Base+28 7 SUBS PC,R14_fiq,#4
asserted Interrupt
(FIQ)
IRQ input Normal 4 R14_irq=PC+4 5 SPSR_irq=CPSR IRQ Unchanged Disabled Base+24 SUBS PC,R14_irq,#4
asserted Interrupt
(IRQ)
Executing Instruction 5 R14_abt=PC+4 6 SPSR_abt=CPSR Abort Unchanged Disabled Base+12 SUBS PC,R14_abt,#4
BKPT 3 or Fetch
instruction Memory
at invalid Abort
address (Prefetch
Abort)
Executing Software 6 ARM state: SPSR_svc=CPSR Supervisor Unchanged Disabled Base+8 MOVS PC,R14_svc
SWI Interrupt R14_svc=PC+4
instruction (SWI) Thumb state:
R14_svc=PC+2 6
Executing Undefined 6 ARM state: SPSR_und=CPSR Undefined Unchanged Disabled Base+4 MOVS PC,R14_und
undefined Instruction R14_und=PC+4
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).
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.
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.
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.
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.
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.
The advanced handler does not only display the location but also the instruction code
at that location.
We can verify the result by checking the application's listing file, which had been
produced by the compiler.
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();
/*
i i i an exception occurs%2C the,memory address called exception vector.
www.ethernut.de/en/documents/arm-exceptions.html#:~:text=When 7/10
2/23/22, 10:43 AM ARM Exceptions
* We will never reach this point.
*/
puts("Brave new world!");
for (;;) {
NutSleep(1000);
putchar('.');
}
return 0;
}
To interpret the backtrace, we look into the linker map file, where we find the start
addresses of all public functions:
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:
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
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.
www.ethernut.de/en/documents/arm-exceptions.html#:~:text=When an exception occurs%2C the,memory address called exception vector. 9/10
2/23/22, 10:43 AM ARM Exceptions
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.