0% found this document useful (0 votes)
4K views10 pages

ARM Exceptions

When an exception occurs on an ARM processor, the processor saves the current status and return address, enters a specific mode, and executes code from a fixed memory address called the exception vector. There are several types of exceptions including hardware interrupts, software interrupts, and memory access violations. The ARM architecture supports seven privilege modes that determine which instructions can be executed when handling different exceptions. Custom exception handlers can be used for debugging by analyzing the cause of exceptions like data aborts when invalid memory is accessed.

Uploaded by

Amina Amen
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4K views10 pages

ARM Exceptions

When an exception occurs on an ARM processor, the processor saves the current status and return address, enters a specific mode, and executes code from a fixed memory address called the exception vector. There are several types of exceptions including hardware interrupts, software interrupts, and memory access violations. The ARM architecture supports seven privilege modes that determine which instructions can be executed when handling different exceptions. Custom exception handlers can be used for debugging by analyzing the cause of exceptions like data aborts when invalid memory is accessed.

Uploaded by

Amina Amen
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 10

2/23/22, 10:43 AM ARM Exceptions

Hardware Firmware Tools Download Community


Search | Legals | Deutsch

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

www.ethernut.de/en/documents/arm-exceptions.html#:~:text=When an exception occurs%2C the,memory address called exception vector. 1/10


2/23/22, 10:43 AM ARM Exceptions
instruction Thumb state:
code R14_und=PC+2 6

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


www.ethernut.de/en/documents/arm-exceptions.html#:~:text=When an exception occurs%2C the,memory address called exception vector. 2/10
2/23/22, 10:43 AM ARM Exceptions

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

www.ethernut.de/en/documents/arm-exceptions.html#:~:text=When an exception occurs%2C the,memory address called exception vector. 3/10


2/23/22, 10:43 AM ARM Exceptions

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

www.ethernut.de/en/documents/arm-exceptions.html#:~:text=When an exception occurs%2C the,memory address called exception vector. 4/10


2/23/22, 10:43 AM ARM Exceptions

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"
% ( )
www.ethernut.de/en/documents/arm-exceptions.html#:~:text=When an exception occurs%2C the,memory address called exception vector. 5/10
2/23/22, 10:43 AM ARM Exceptions
"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.

www.ethernut.de/en/documents/arm-exceptions.html#:~:text=When an exception occurs%2C the,memory address called exception vector. 6/10


2/23/22, 10:43 AM ARM Exceptions
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();
/*
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;
}

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.

www.ethernut.de/en/documents/arm-exceptions.html#:~:text=When an exception occurs%2C the,memory address called exception vector. 8/10


2/23/22, 10:43 AM ARM Exceptions

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.
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.

www.ethernut.de/en/documents/arm-exceptions.html#:~:text=When an exception occurs%2C the,memory address called exception vector. 10/10

You might also like