The ABI for the ARM Architecture specifies a standard format for exception unwind information. This information is used when an exception is thrown to determine where control should be transferred. In particular, the unwind information is used to determine which function called the function that threw the exception, and which function called that one, and so forth. This information is also used to restore the values of callee-saved registers in the function catching the exception.
If you are writing functions in assembly code, and those functions call other functions that throw exceptions, you must use assembly pseudo ops to ensure that appropriate exception unwind information is generated. Otherwise, if one of the functions called by your assembly code throws an exception, the run-time library will be unable to unwind the stack through your assembly code and your program will not behave correctly.
To illustrate the use of these pseudo ops, we will examine the code that G++ generates for the following C++ input:
void callee (int *); int caller () { int i; callee (&i); return i; }
This example does not show how to throw or catch an exception from assembly code. That is a much more complex operation and should always be done in a high-level language, such as C++, that directly supports exceptions.
The code generated by one particular version of G++ when compiling the example above is:
_Z6callerv: .fnstart .LFB2: @ Function supports interworking. @ args = 0, pretend = 0, frame = 8 @ frame_needed = 1, uses_anonymous_args = 0 stmfd sp!, {fp, lr} .save {fp, lr} .LCFI0: .setfp fp, sp, #4 add fp, sp, #4 .LCFI1: .pad #8 sub sp, sp, #8 .LCFI2: sub r3, fp, #8 mov r0, r3 bl _Z6calleePi ldr r3, [fp, #-8] mov r0, r3 sub sp, fp, #4 ldmfd sp!, {fp, lr} bx lr .LFE2: .fnend
Of course, the sequence of instructions varies based on the options you pass to GCC and on the version of GCC in use. The exact instructions are not important since we are focusing on the pseudo ops that are used to generate unwind information.
An important assumption made by the unwinder is that the stack frame
does not change during the body of the function. In particular, since
we assume that the assembly code does not itself throw an exception,
the only point where an exception can be thrown is from a call, such
as the bl
instruction above. At each call site, the same saved
registers (including lr
, which indicates the return address)
must be located in the same locations relative to the frame pointer.
The .fnstart
(see .fnstart pseudo op) pseudo
op appears immediately before the first instruction of the function
while the .fnend
(see .fnend pseudo op) pseudo
op appears immediately after the last instruction of the function.
These pseudo ops specify the range of the function.
Only the order of the other pseudos ops (e.g., .setfp
or
.pad
) matters; their exact locations are irrelevant. In the
example above, the compiler emits the pseudo ops with particular
instructions. That makes it easier to understand the code, but it is
not required for correctness. It would work just as well to emit all
of the pseudo ops other than .fnend
in the same order, but
immediately after .fnstart
.
The .save
(see .save pseudo op) pseudo op
indicates registers that have been saved to the stack so that they can
be restored before the function returns. The argument to the
.save
pseudo op is a list of registers to save. If a register
is “callee-saved” (as specified by the ABI) and is modified by the
function you are writing, then your code must save the value before it
is modified and restore the original value before the function
returns. If an exception is thrown, the run-time library restores the
values of these registers from their locations on the stack before
returning control to the exception handler. (Of course, if an
exception is not thrown, the function that contains the .save
pseudo op restores these registers in the function epilogue, as is
done with the ldmfd
instruction above.)
You do not have to save callee-saved registers at the very beginning
of the function and you do not need to use the .save
pseudo op
immediately following the point at which the registers are saved.
However, if you modify a callee-saved register, you must save it on
the stack before modifying it and before calling any functions which
might throw an exception. And, you must use the .save
pseudo
op to indicate that you have done so.
The .pad
(see .pad) pseudo op indicates a
modification of the stack pointer that does not save any registers.
The argument is the number of bytes (in decimal) that are subtracted
from the stack pointer. (On ARM CPUs, the stack grows downwards, so
subtracting from the stack pointer increases the size of the stack.)
The .setfp
(see .setfp pseudo op) pseudo op
indicates the register that contains the frame pointer. The first
argument is the register that is set, which is typically fp
.
The second argument indicates the register from which the frame
pointer takes its value. The third argument, if present, is the value
(in decimal) added to the register specified by the second argument to
compute the value of the frame pointer. You should not modify the
frame pointer in the body of the function.
If you do not use a frame pointer, then you should not use the
.setfp
pseudo op. If you do not use a frame pointer, then you
should avoid modifying the stack pointer outside of the function
prologue. Otherwise, the run-time library will be unable to find
saved registers when it is unwinding the stack.
The pseudo ops described above are sufficient for writing assembly code that calls functions which may throw exceptions. If you need to know more about the object-file format used to represent unwind information, you may consult the Exception Handling ABI for the ARM Architecture available from http://infocenter.arm.com.