Work / Virtual Machine / Dev Blog

2011-12-15 02:11:47

Exception Handling

Exception Handling is supported. Each frame on the stack stores a pointer to the handler to be called if an exception occurs within that frame. This pointer is stored on the stack directly below the local variables and directly above the base pointer of the previous frame.

When transferring control to a vm program, the 'host' calling code pushes null onto the stack in place of what would otherwise be a return address for eip. When returning from a symbol, if the return address is null, the program will return control to the caller. There are initially no stack frames present on the stack. As a result there are no exception handlers present by default. When a frame is created, the handler pointer is initialised to null. It is up to the code in the context of that frame to setup the handler for that frame. This is done through the sexh (set exception handler) instruction. If an exception occurs and no handlers have been setup, the exception will be throw back up into the host. Handlers themselves can of course throw as well.

When an exception occurs, the handler pointer of the current frame is checked. If it is null, the stack is unwound to the previous frame, and the handler pointer for that frame is then checked. This continues either until the stack is entirely depleted of frames, or until a handler pointer is found that is not null.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void divideNumbers()
{
    int c = divideByZero(1, 0);
}

int divideByZero(int a, int b)
{
    int c = 0;
    try
    {
        c = a / b;
        return c;
    }
    catch(...)
    {
        return -1;
    }
}

If a valid handler pointer is found, the stack is unwound to the frame that declared that handler, and eip is set to the handler address. Noteably, the handler pointer for the frame that declared the handler (the frame that the stack is now unwound to) is set to null. This is important, because if the handler itself threw, the process of finding a valid handler pointer would immediately return the address of the handler that just threw the exception. Setting the pointer to null ensures the next handler up in the chain is used instead.

This makes sense in that when an exception occurs it is usually to signify an issue with the current context from which execution cannot continue (an exceptional state). The handler, through unwinding of the stack, assumes the context that contains the handler - this includes local variables. As a result, the handler has direct access to the local variables of the frame that threw the exception. This can be used to inspect values and clean-up any partial initialisation that may have taken place.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
divideNumbers:
    entr    1
    pshi    1
    pshi    0
    call    divideByZero
    popr    0,  2
    retn

divideByZero:
    nvar    0,  a
    nvar    1,  b
    nvar    2,  c

    entf    3,  2

    movi    c,  0

    sexh    .exceptionHandler

    mov c,  a
    div c,  b

    lret    c

.exceptionHandler:
    lrti    -1

A handler can exit in one of two ways; it can either throw another exception, which would travel up the frame chain for the next available handler, or it can simply return to the calling code and execution continues as normal. Note that the handler must respect the stack in terms of return parameters that would have been pushed if an exception had not occured. Failure to adhere to this will result in stack corruption as the caller attempts to pop return values that arent there.

The runtime determines if the stack is depleted of frames by testing the proposed values of eip, esp and ebp with the address of the stack segment. If they are less (i.e. point to memory outside of the stack segment), there are no more stack frames to read.

Frames can be created at any point; they are not tied to calling and return instructions. As a result, the value immediately below the base pointer of the previous frame may not be the address to return execution to. This implies potentially exceptional code can be wrapped within the context of a symbol call by a frame of its own, without requiring execution to jump back out to the caller of the symbol (by keeping it in the called 'sub-routine').