Let's take a look at a short piece of code that will always generate an exception.
int a = 100;
int b = 0;
int c = a / b;
The code generated by my compiler looks like this:
int a = 100;
00000000 push ebp
00000001 mov ebp,esp
00000003 sub esp,0Ch
00000006 mov dword ptr [ebp-4],ecx
00000009 cmp dword ptr ds:[00442330h],0
00000010 je 00000017
00000012 call 6EC0403F
00000017 xor edx,edx
00000019 mov dword ptr [ebp-8],edx
0000001c xor edx,edx
0000001e mov dword ptr [ebp-0Ch],edx
00000021 mov dword ptr [ebp-8],64h
int b = 0;
00000028 xor edx,edx
0000002a mov dword ptr [ebp-0Ch],edx
int c = a / b;
0000002d mov eax,dword ptr [ebp-8]
00000030 cdq
00000031 idiv eax,dword ptr [ebp-0Ch]
Nothing in there for exception handling, but an exception is raised, so something behind the scene is dealing with it. Since the exception is raised in the idiv line, we take a look at the documentation for the processor (found here) and we note that is says "Overflow is indicated with the #DE (divide error) exception rather than with the OF (overflow) flag." So the processor itself has exceptions, so it must be the OS that is handling it in this case.
Now we put a try catch in there:
int a = 100;
int b = 0;
try {
int c = a / b;
} catch {
}
Which gives us:
int a = 100;
00000000 push ebp
00000001 mov ebp,esp
00000003 push edi
00000004 push esi
00000005 push ebx
00000006 sub esp,20h
00000009 xor eax,eax
0000000b mov dword ptr [ebp-20h],eax
0000000e mov dword ptr [ebp-1Ch],eax
00000011 mov dword ptr [ebp-18h],eax
00000014 mov dword ptr [ebp-14h],eax
00000017 xor eax,eax
00000019 mov dword ptr [ebp-18h],eax
0000001c mov dword ptr [ebp-24h],ecx
0000001f cmp dword ptr ds:[00A12330h],0
00000026 je 0000002D
00000028 call 6E9543DF
0000002d xor edx,edx
0000002f mov dword ptr [ebp-28h],edx
00000032 xor edx,edx
00000034 mov dword ptr [ebp-2Ch],edx
00000037 mov dword ptr [ebp-28h],64h
int b = 0;
0000003e xor edx,edx
00000040 mov dword ptr [ebp-2Ch],edx
try {
int c = a / b;
00000043 mov eax,dword ptr [ebp-28h]
00000046 cdq
00000047 idiv eax,dword ptr [ebp-2Ch]
0000004a nop
0000004b jmp 00000055
} catch {
0000004d nop
0000004e call 6E6A0218
00000053 jmp 00000055
}
Notice there is a lot more start-up code. I suspect that this code is setting up for telling the OS that if an exception happens, we want to continue execution at 0000004d and that line 00000028 is where we make a call into the OS.
I can't find any documentation of processor exceptions (and I have to admit I haven't looked very hard) so I can't tell you what is actually going on there, but I suspect that when the processor hits an exception, it looks at a certain memory location then runs the code that they memory location points to. The OS would be required to set up its exception handling code and establish the value at that memory location.