Mittwoch, 30. März 2011

Compiler internals - What happens with try/catch/throw in MSVC

Hello everyone

After i have seen that a lot of people are thinking that try - catch is a concept which is completely analyzed during compile time and therefore wont have big impact at runtime i thought i might shed some light on how the Microsoft compiler (cl.exe) is acting with try, catch and throw.

First off we will have a look on how a throw statement is interpreted by the compiler. Lets have a look at the following code:
int main()
{
 try
 {
  throw 2;
 }
 catch(...)
 {
 }
}

For now we are only interested in the throw 2. When the compiler hits the throw statement it actually has no clue if the exception its now converting is handled by an exception handler (and it doesnt care). The throw statement will be converted into a call to _CxxThrowException (exported by MSVCR100.dll (or any other version)). That function is a built in function in the compiler. You can call it yourself if you like ;). The first parameter of that function is a pointer to the object thrown. Therefore it gets clear, that the code above definitely expands to the following:
int main()
{
 try
 {
  int throwObj = 2;
  throw throwObj;
 }
 catch(...)
 {
 }
}

The second parameter of _CxxThrowException holds a pointer to a _ThrowInfo object. _ThrowInfo is also a built in type of the compiler. Its a struct holding various information about the type of exception that was thrown. It looks like that:
typedef const struct _s__ThrowInfo
{
 unsigned int attributes;
 _PMFN pmfnUnwind;
 int (__cdecl*pForwardCompat)(...);
 _CatchableTypeArray *pCatachableTypeArray;
} _ThrowInfo;

Here the important thing is the _CatchableTypeArray. It holds a set of runtime type informations of the types that are catchable within this throw. In our case thats pretty simple. The only catchable type is typeid(int). Lets say you have a class derived from std::exception called my_exception. If you now throw an object of type my_exception you will have two entries in pCatchableTypeArray. One of them is typeid(my_exception) and the other is typeid(std::exception).

The compiler now fills the _ThrowInfo object as a global variable (and all the other objects needed). In the above case this is done the following way:
_TypeDescriptor tDescInt = typeid(int);

_CatchableType tcatchInt = 
{
 0,
 &tDescInt,
 0,
 0,
 0,
 0,
 NULL,
};

_CatchableTypeArray tcatchArrInt = 
{
 1,
 &tcatchInt,
};

_ThrowInfo tiMain1 = 
{
 0,
 NULL,
 NULL,
 &tcatchArrInt
};

You see that thats pretty a lot of information stored just for the throw 2. So finally Our above code expands to:
_TypeDescriptor tDescInt = typeid(int);

_CatchableType tcatchInt = 
{
 0,
 &tDescInt,
 0,
 0,
 0,
 0,
 NULL,
};

_CatchableTypeArray tcatchArrInt = 
{
 1,
 &tcatchInt,
};

_ThrowInfo tiMain1 = 
{
 0,
 NULL,
 NULL,
 &tcatchArrInt
};

int main()
{
 try
 {
  int throwObj = 2;
  _CxxThrowException(&throwObj, &tiMain1);
 }
 catch(...)
 {
 }
}

Inside _CxxThrowException now the following happens: RaiseException is called. But first the neccessary parameters are created. The exception code for an exception thrown by _CxxThrowException is 0xE06D7363. It also passes 3 parameters to RaiseException. A magic number, the pointer to the object thrown and the pointer to the _ThrowInfo. Resulting in the following pseudo code:
__declspec(noreturn) void __stdcall __CxxThrowException(void* pObj, _ThrowInfo* pInfo)
{
 struct { unsigned int magic; void* object, _ThrowInfo* info } Params;
 Params throwParams = 
 {
  0x19930520,
  pObj,
  pInfo
 }

 RaiseException(0xE06D7363, 1, 3, (const ULONG_PTR*)&throwParams);
}

Now we basically know how throw is handled by the compiler and we also see that in the end what you will notice is something like if you have encountered an access violation as they are also invoked by RaiseException.

Ok, if we now go further and inspect the try and catch there should be a bell ringing like crazy and it should be yelling "Wait!! You say that the throw gets transformed into a call to RaiseException like its for access violations, 0 divides and so on?! But they cannot be catched with try-catch!". And yes, you are right, they cant and thats way try - catch in fact gets transformed to a __try __except but in a special form. In code it would look somehow like that (its not real code, just theory):
unsigned long __stdcall mainHandler1(LPEXCEPTION_POINTERS info)
{
 if(info->ExceptionRecord->ExceptionCode != 0xE06D7363)
  return EXCEPTION_CONTINUE_SEARCH;

 if(WeHaveAHandlerForThisTypeSomeWhere(info->ExceptionRecord))
  return EXCEPTION_EXECUTE_HANDLER;

 return EXCEPTON_CONTINUE_SEARCH;
}

/* The stuff with _ThrowInfo comes here, omitted for readability */

int main()
{
 __try
 {
  int throwObj = 2;
  _CxxThrowException(&throwObj, &tiMain1);
 }
 __except(mainHandler1(GetExceptionInfo())
 {
 }
}

But thats not all! Somewhere we need to store which types of exceptions we can catch using our catch-statement. In fact the catch(int) gets transformed into an own function (actually only a function chunk where the runtime jumps using jmp not a real function called with call) which looks like that (now its really pseudocode because i cannot really translate it to C as it misses some information which would blow up the whole thing)
_s_FuncInfo* info = mainCatchBlockInfo1;
__asm { mov eax, info } // Its used for the following function as argument and passed through eax
goto CxxFrameHandler3;

The _s_FuncInfo is now again a structure that is built in to the compiler. It would make the article to big to explain everything like i did for the _ThrowInfo. In short it holds information for every type that can be caught in the current block. This consists (beneath other stuff) of runtime type information for every type and for each of them also the address of the actual code that is inside the catch-block.

Ok, now what is CxxFrameHandler3 doing? This is pretty simple:
1. It rejects exceptions that dont have 0xE06D7363 as code (which stands for C++ exceptions).
2. It searches through the _s_FuncInfo structure to find a type witch matches with one of the types it gets from the exception objects _CatchableTypeArray.
3. If it gets a match it indicates that there is a handler read
4. If there is not match it instructs the OS to search in the next frame

To finish the catch-part all we now need is the actual handler code. This code also is transformed into a function chunk (not a complete function). It actually is transformed into the chunk that ends a function. In code it would look like that:
// execute handler code
return addressWhereToContinueAfterCatch;

The operating system gets the address where it should jump to when it has set up again the original context and performs that jump. An example:
catch(...)
{
}

MessageBox(0, L"Ello!", L"", MB_OK);

Gets translated into the following assembler code:
.text:00401088 $LN16:
.text:00401088                 mov     eax, offset $LN9
.text:0040108D                 retn
.text:0040108E ; ---------------------------------------------------------------------------
.text:0040108E
.text:0040108E $LN9:                                   ; DATA XREF: _main:$LN16 o
.text:0040108E                 push    0               ; uType
.text:00401090                 push    offset Caption  ; lpCaption
.text:00401095                 push    offset Text     ; "Ello!"
.text:0040109A                 push    0               ; hWnd
.text:0040109C                 call    ds:__imp__MessageBoxW@16 ; MessageBoxW(x,x,x,x)

You see that it returns $LN9 in eax which is the address of the call to MessageBox. And $LN16 is the address of the catch block which is referenced in the _s_FuncInfo somewhere.

All that remains now is the try part. Here its no longer the compiler that can "decide" how to do things because now its the operating system that says how it works.

Inside the Thread Information Block the first field (fs:[0]) holds a pointer to a linked list of exception handlers (in our case its the address of the part where it goes to CxxFrameHandler3). Now what try does is it adds the catch-block to the linked list. After the RaiseException call we arrive in the function KiUserExceptionDispatcher. This function does a lot of work but in the end the important thing is that it loads the current linked list from the TIB using FS:[0] and loops through it to find a handler that says that it could handle the exception and calls its handler. If you want to browse through the currently attached handlers you do the following:
struct LinkedExceptionFrame
{
 LinkedExceptionFrame* pPrevious;
 void* pFunction;
};

LinkedExceptionFrame* pCur = NULL;
__asm
{
 mov eax, fs:[0]
 mov pCur, eax
}
while((DWORD)pCur != 0xFFFFFFFF)
{
 std::cout << pCur->pFunction << std::endl;
 pCur = pCur->pPrevious;
}

Now we have all the basic concepts we need to understand that try/catch/throw is not as trivial as most people think and that most things are actually handled at runtime (though a huge amount of additional data and function overhead is made to catch the correct type of exception). There is way more we could talk about (for example: What if we have parts of our frame protected by try-catch and others not or if we even have more than one try-catch-block and so on. But i think so far the most important things are said!

Some tips if you like to browse through it using a disassembler and a debugger:
Use Release build but disable any kind of code optimization. So you dont have all the register checks at the beginning and the end of function calls but your code is not getting rearranged by the optimizer so you can better compare it to the source. And its a good thing to disable Dynamic Base (ASLR) in the linker options (under Advanced).

So far
Yanick

2 Kommentare:

  1. Hi, cool article! I think i'll stick on internals a bit more, looks very interesting!

    AntwortenLöschen
  2. Yes, thats true. For me its one of the most interesting fields. And it also helps when you are developing other stuff. You know what happens to the code and therefore you can better estimate what to change to improve the code.

    AntwortenLöschen