Wiki

Clone wiki

minlibd / arm-exceptions

This page is about exceptions in arm and gcc/gdc. NOTE: This is an early draft and may be outdated or contain errors. If you find something to correct, please use the bug tracker.

Arm has provided a recommendation for all compiler vendors how to handle exceptions. The original document is here: http://infocenter.arm.com/help/topic/com.arm.doc.ihi0038a/IHI0038A_ehabi.pdf There is also info of exception handling in gcc here: http://gcc.gnu.org/ml/gcc/2002-07/msg00391.html

Information how to use exceptions in d can be found here: http://ddili.org/ders/d.en/exceptions.html

Gdc for arm uses a method called dwarf exceptions. Compiler generated debug info is used to generate information for exception handling. Another method is called setjmp/longjmp. That is used in some other systems but it is not described here.

How exceptions work

When a function is called, the return address is saved in stack. Usually those registers that will be modified in the called function are also saved. Then the stack pointer may be moved to reserve space for local variables. At the end of the function the stack pointer and registers are restored and finally the return address is taken to the program counter and the program continues at this address.

The try block and the throw statement are totally separated from each other. In try block there is no info where the throw statements are. A throw statement has no idea where a catch block is. When an exception is thrown, no code after the throw statement is executed. Instead the program returns immediately to the first try block that is found. All code in all calling functions is skipped. The processor state and stack should still be restored to what they were when the function was called in try block. This is called unwinding. It is done separately for each function that has been called until a try block is found. Then the program jumps to the catch block. There may be several catch blocks ant the compiler translates these to a switch statement. The exception type is tested in case statements. If the exception is not matching any of them the default is to throw the exception again. If no matching catch is found anywhere, abort() function is called.

How unwinding works

In throw statement the first thing is to construct the exception object and its pointer is saved. Then begins the unwind phase. If there is any cleanup code for the current function, it is executed first. Cleanup code contains the scope(exit) and scope(failure) blocks. QUESTION: is there something else automatically than those scope blocks?

After executing this code, the stack is unwinded. The saved registers are popped out of the stack and the return address is fetched. No return value for the function is generated. The program does not jump to the return address. The unwind procedure is repeated instead: cleanup code and stack unwinding. This continues until there is a try marker in the stack telling that we are now in try block. QUESTION: what is the code of the marker? Is it put to stack at the beginning of try block or at the beginning of possible throwing statement?

When the marker is found the unwind process stops and the program jumps to the catch block. The pointer of the exception is fetched and the type of e tion is compared with the type of each catch block. When a match is found that catch block is executed. After catch block the finally block is executed if it exists. QUESTION: where is the address to the catch block? Is it saved with the marker or is it in some jump table?

If there is no match in the catch block, the exception is thrown again and the whole process starts at the beginning.

Unwinding in ARM

The unwinding info is put in two sections on object file: .ARM.extab and .ARM.exidx. Extab contains unwind instructions and exidx maps functions in the program to correct unwinding sequence. These sections contain offsets to code and they have to be in the same output section than code and their position to code may not change. The linker must provide variables _exidx_start and _exidx_end (with 2 leading underscores) that mark the beginning and end of the exidx table.

In these tables, the addresses are offsets from this point in the table to the target address. The type is 31 bit signed integer. the 32th bit(bit 31, msb) is a control bit. When used in calculations, bit 31 must be ignored and the 31 bit integer must be sign extended to 32 bit.

Exidx table contains two word elements, one element for each function in the code. (actually they talk anout code blocks, so they may also be other piece of code). First word contains offset from this place to the beginning of the corresponding code block (in int31 format). The second word is the action word. These elemnts must be in the same order than their code blocks are in memory.

When a function is called, the current value of the program counter is saved into stack. For each function to unwind, the unwind code picks the saved program counter from stack and compares it to the addresses that are calculated from exidx. If the value is greater than the address of this element and less than the address of the next element, the program has been in this function and the action for this function is taken. Because the addresses are ordered, a fast binary search algorithm can be used.

The action word may get 3 different values:

  • if msb is 0, the word contains an int31 offset to the unwind instructions in the extab table.

  • if msb is 1, the unwind instructions are so short that they have been included in this word.

  • code 0x00000001 means this function can not be unwound.

The unwind instructions blocks in extab may be in any order and have any length. The words may have two meanings: if msb is 0, this word is a int31 offset to a code block to execute. If the bit is 1, this word contains unwind code. The unwind instructions are like "restore these registers" or "increment stack pointer by this amount" The actual codes can be found in the arm document.

The general, language independent unwind code is in libgcc. Throw calls funtion _Unwind_RaiseException. This code then calls language dependent code that is named "personality routine". In gdc, this code is in libdruntime/gcc/deh.d. This code then may call helper functions in libgcc.

Unwinding is done with a virtual register set (vrs) that is a table in memory. At the beginning the current values of processor registers are copied to virtual registers. Values are popped from stack to these registers according to the unwind instructions. When the unwind is finished vrs contains values that the registers had in try block. Then the virtual registers are copied to the actual registers and then a jump to the beginning of the catch code is executed.

The actual process is done in two steps. First the instructions are just scanned without executing any operations. The second stage scans everything again and really does the unwinding. The reason is that if unwinding fails for some reason, the program might be left to undefined state. This way it is possible to return with an error code and the throw routine may be able for example to print an error message. One possible failure is missing marker: there is no try block and unwinding would go over top of stack. Another case is a funtion that is marked to be not unwindable.

Updated