Exception Handling (Part 2 of 2)
Sorry it took so long to get to part 2.
First up is the unwind info (I think it's the .XDATA, but don't quote me). Basically there is a set of opcode sequences that are allowed in the prologue and epilogue, and there is some info in the describes what is happening in the prologue. The OS can read that data (the unwind info) and figure out what registers were saved, the frame size, if there's a frame pointer register, and how big the prologue is, etc. Each processor defines different things to store in the unwind info.
Second is the exception info (I think it's the .PDATA). This describes a bunch or ranges: the range of protected code (usually called a try body in high-level languages like C# or C++), the protector (the catch or finally body), a filter (like in VB.NET or SEH where you can have actual code executed to determine is a catch body is appropriate). There is also usually a spot reserved int he PDATA for language specific stuff to be passed to the personality routine.
So what happens when an exception is thrown? Well on x86 the OS just walks the 'linked-list' of exception information stored at FS:[0], and calls the personality routine or filters (I'm a little fuzzy here) to see if they want to 'catch' the exception. On IA64 and AMD64 the OS has to walk the .PDATA. Once the a handler is found the first pass has ended and a second pass begins. All stack frames between where the exception occurred and the handler are unwound. This is a joint effort between the OS and the personality routine. The personality routine is responsible for running things like destructors and finally clauses at each stack level (like when you have nested try/finally blocks). The OS does the physical unwinding by changing the stack pointer, frame pointer, instruction pointer, and restoring saved registers, accordingly. I have no clue how this works on x86, but for AMD64 and IA64, the OS reads the XDATA and figures out what to do (generally this is just undoing the prologue).
So the real point of all this was to see the differences. In the code, for x86 the entry of a try body results in pushing a new exception record pointer into FS:[0]. This is generally just 2 opcodes: one to store the existing pointer into the new record, and one change FS:[0] to point to the new record. The end of the try body is indicated by restoring FS:[0] to it's original value (popping the head off of the linked list).
For AMD64 and IA64 there is nothing in the code that indicates the start or end of a try body. This seems like it would be better. For the general case I think it is. The ugly part comes into play when the try body doesn't exit normally (you have a goto or leave from inside the try body to someplace other than the end of the try/catch or try/finally block). For x86 this just means you pop the exception record in more than one place, but for AMD64 and IA64 this means extra PDATA (and possibly XDATA). Since everything is range based, and there's no way to encode something like “everything in this range except the 5 sub-ranges”, you end up breaking the range into a bunch of smaller ranges and duplicating all the handler info.
Comments
- Anonymous
July 05, 2004
Well, I suppose this is even more reason not use use exceptions for flow control...
JIT programming sounds fun with such interesting problems as this :)