Here is a simple code:
bool extenrnal_variable; int f(...) { if (extenrnal_variable) throw 0; return 42; } int g() { return f(1, 2, 3); }
I added extenrnal_variable to prevent the compiler from optimizing all branches. f has ... to prevent inlining.
When compiling with:
$ clang++ -S -O3 -m32 -o - eh.cpp | c++filt
it produces the following code for g() (the rest is omitted):
g(): ## @_Z1gv .cfi_startproc ## BB#0: pushl %ebp Ltmp9: .cfi_def_cfa_offset 8 Ltmp10: .cfi_offset %ebp, -8 movl %esp, %ebp Ltmp11: .cfi_def_cfa_register %ebp subl $24, %esp movl $3, 8(%esp) movl $2, 4(%esp) movl $1, (%esp) calll f(...) movl $42, %eax addl $24, %esp popl %ebp ret .cfi_endproc
All of these .cfi_* directives exist to expand the stack if an exception is thrown. All of them were compiled into the FDE block (frame description record) and saved under the name g().eh __Z1gv.eh ( __Z1gv.eh ). These directives determine where the CPU registers are stored on the stack. When an exception is thrown and the stack is unwound, the code in the function should not be executed (with the exception of locale destructors), but the registers that were saved earlier must be restored. These tables store exactly this information.
These tables can be reset using the dwarfdump tool:
$ dwarfdump --eh-frame --english eh.o | c++filt
Output:
0x00000018: FDE length: 0x00000018 CIE_pointer: 0x00000000 start_addr: 0x00000000 f(...) range_size: 0x0000004d (end_addr = 0x0000004d) Instructions: 0x00000000: CFA=esp+4 eip=[esp] 0x00000001: CFA=esp+8 ebp=[esp] eip=[esp+4] 0x00000003: CFA=ebp+8 ebp=[ebp] eip=[ebp+4] 0x00000007: CFA=ebp+8 ebp=[ebp] esi=[ebp-4] eip=[ebp+4] 0x00000034: FDE length: 0x00000018 CIE_pointer: 0x00000000 start_addr: 0x00000050 g() range_size: 0x0000002c (end_addr = 0x0000007c) Instructions: 0x00000050: CFA=esp+4 eip=[esp] 0x00000051: CFA=esp+8 ebp=[esp] eip=[esp+4] 0x00000053: CFA=ebp+8 ebp=[ebp] eip=[ebp+4]
Here you can find out about the format of this block. Here are a few more and several alternative more compact ways of presenting the same information. Basically this block describes which registers and where to pop out of the stack during stack unloading.
To see the original contents of these characters, you can list all the characters with their offsets:
$ nm -n eh.o 00000000 T __Z1fz U __ZTIi U ___cxa_allocate_exception U ___cxa_throw 00000050 T __Z1gv 000000a8 s EH_frame0 000000c0 S __Z1fz.eh 000000dc S __Z1gv.eh 000000f8 S _extenrnal_variable
And then unload the section (__TEXT,__eh_frame) :
$ otool -s __TEXT __eh_frame eh.o eh.o: Contents of (__TEXT,__eh_frame) section 000000a8 14 00 00 00 00 00 00 00 01 7a 52 00 01 7c 08 01 000000b8 10 0c 05 04 88 01 00 00 18 00 00 00 1c 00 00 00 000000c8 38 ff ff ff 4d 00 00 00 00 41 0e 08 84 02 42 0d 000000d8 04 44 86 03 18 00 00 00 38 00 00 00 6c ff ff ff 000000e8 2c 00 00 00 00 41 0e 08 84 02 42 0d 04 00 00 00
By matching offsets, you can see how each character is encoded.
When there are local variables, they must be destroyed during the expansion of the stack. To do this, usually more code is built into the functions themselves, and several additional large tables are created. You can study this yourself by adding a local variable with a non-trivial destructor in g , compiling and looking at the output of the assembly.
Further reading