C++ Exceptions in Ten Minutes
I got a bit frustrated with some exception-related C++ stuff I was doing this
weekend, and set out to learn how exceptions work. Here’s a write-up of what I
found. Firstly, I’ll list the source and gcc -S
output for this source. Read
the C++ source and understand it, but you don’t need to understand the
assembly in one go. It’s there as a reference.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdexcept>
int check_divisibility(int i) {
if (i % 2) {
throw std::runtime_error("uh oh");
}
return 0;
}
int main(int argc, char** argv) {
(void)argv;
try {
check_divisibility(argc);
} catch (const std::runtime_error& e) {
return -12;
}
return 4;
}
And the assembly for this program:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
.file "tmp.cpp"
# GNU C++14 (GCC) version 7.1.1 20170621 (x86_64-pc-linux-gnu)
# compiled by GNU C version 7.1.1 20170621, GMP version 6.1.2, MPFR version 3.1.5-p2, MPC version 1.0.3, isl version isl-0.18-GMP
# GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
# options passed: -D_GNU_SOURCE tmp.cpp -mtune=generic -march=x86-64
# -auxbase-strip tmp.S -O2 -Wall -Werror -Wextra -std=c++1z -fverbose-asm
# options enabled: -faggressive-loop-optimizations -falign-labels
# -fasynchronous-unwind-tables -fauto-inc-dec -fbranch-count-reg
# -fcaller-saves -fchkp-check-incomplete-type -fchkp-check-read
# -fchkp-check-write -fchkp-instrument-calls -fchkp-narrow-bounds
# -fchkp-optimize -fchkp-store-bounds -fchkp-use-static-bounds
# -fchkp-use-static-const-bounds -fchkp-use-wrappers -fcode-hoisting
# -fcombine-stack-adjustments -fcommon -fcompare-elim -fcprop-registers
# -fcrossjumping -fcse-follow-jumps -fdefer-pop
# -fdelete-null-pointer-checks -fdevirtualize -fdevirtualize-speculatively
# -fdwarf2-cfi-asm -fearly-inlining -feliminate-unused-debug-types
# -fexceptions -fexpensive-optimizations -fforward-propagate
# -ffp-int-builtin-inexact -ffunction-cse -fgcse -fgcse-lm -fgnu-runtime
# -fgnu-unique -fguess-branch-probability -fhoist-adjacent-loads -fident
# -fif-conversion -fif-conversion2 -findirect-inlining -finline
# -finline-atomics -finline-functions-called-once -finline-small-functions
# -fipa-bit-cp -fipa-cp -fipa-icf -fipa-icf-functions -fipa-icf-variables
# -fipa-profile -fipa-pure-const -fipa-ra -fipa-reference -fipa-sra
# -fipa-vrp -fira-hoist-pressure -fira-share-save-slots
# -fira-share-spill-slots -fisolate-erroneous-paths-dereference -fivopts
# -fkeep-static-consts -fleading-underscore -flifetime-dse -flra-remat
# -flto-odr-type-merging -fmath-errno -fmerge-constants
# -fmerge-debug-strings -fmove-loop-invariants -fomit-frame-pointer
# -foptimize-sibling-calls -foptimize-strlen -fpartial-inlining -fpeephole
# -fpeephole2 -fplt -fprefetch-loop-arrays -free -freg-struct-return
# -freorder-blocks -freorder-functions -frerun-cse-after-loop
# -fsched-critical-path-heuristic -fsched-dep-count-heuristic
# -fsched-group-heuristic -fsched-interblock -fsched-last-insn-heuristic
# -fsched-rank-heuristic -fsched-spec -fsched-spec-insn-heuristic
# -fsched-stalled-insns-dep -fschedule-fusion -fschedule-insns2
# -fsemantic-interposition -fshow-column -fshrink-wrap
# -fshrink-wrap-separate -fsigned-zeros -fsplit-ivs-in-unroller
# -fsplit-wide-types -fssa-backprop -fssa-phiopt -fstdarg-opt
# -fstore-merging -fstrict-aliasing -fstrict-overflow
# -fstrict-volatile-bitfields -fsync-libcalls -fthread-jumps
# -ftoplevel-reorder -ftrapping-math -ftree-bit-ccp -ftree-builtin-call-dce
# -ftree-ccp -ftree-ch -ftree-coalesce-vars -ftree-copy-prop -ftree-cselim
# -ftree-dce -ftree-dominator-opts -ftree-dse -ftree-forwprop -ftree-fre
# -ftree-loop-if-convert -ftree-loop-im -ftree-loop-ivcanon
# -ftree-loop-optimize -ftree-parallelize-loops= -ftree-phiprop -ftree-pre
# -ftree-pta -ftree-reassoc -ftree-scev-cprop -ftree-sink -ftree-slsr
# -ftree-sra -ftree-switch-conversion -ftree-tail-merge -ftree-ter
# -ftree-vrp -funit-at-a-time -funwind-tables -fverbose-asm
# -fzero-initialized-in-bss -m128bit-long-double -m64 -m80387
# -malign-stringops -mavx256-split-unaligned-load
# -mavx256-split-unaligned-store -mfancy-math-387 -mfp-ret-in-387 -mfxsr
# -mglibc -mieee-fp -mlong-double-80 -mmmx -mno-sse4 -mpush-args -mred-zone
# -msse -msse2 -mstv -mtls-direct-seg-refs -mvzeroupper
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "uh oh"
.text
.p2align 4,,15
.globl _Z18check_divisibilityi
.type _Z18check_divisibilityi, @function
_Z18check_divisibilityi:
.LFB1302:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA1302
# tmp.cpp:4: if (i % 2) {
andl $1, %edi #, i
jne .L10 #,
# tmp.cpp:9: }
xorl %eax, %eax #
ret
.L10:
# tmp.cpp:3: int check_divisibility(int i) {
pushq %rbp #
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
pushq %rbx #
.cfi_def_cfa_offset 24
.cfi_offset 3, -24
# tmp.cpp:5: throw std::runtime_error("uh oh");
movl $16, %edi #,
# tmp.cpp:3: int check_divisibility(int i) {
subq $8, %rsp #,
.cfi_def_cfa_offset 32
# tmp.cpp:5: throw std::runtime_error("uh oh");
call __cxa_allocate_exception #
movl $.LC0, %esi #,
movq %rax, %rdi # _6,
movq %rax, %rbx #, _6
.LEHB0:
call _ZNSt13runtime_errorC1EPKc #
.LEHE0:
# tmp.cpp:5: throw std::runtime_error("uh oh");
movl $_ZNSt13runtime_errorD1Ev, %edx #,
movl $_ZTISt13runtime_error, %esi #,
movq %rbx, %rdi # _6,
.LEHB1:
call __cxa_throw #
.L4:
movq %rax, %rbp #, tmp96
# tmp.cpp:5: throw std::runtime_error("uh oh");
movq %rbx, %rdi # _6,
call __cxa_free_exception #
movq %rbp, %rdi # tmp96,
call _Unwind_Resume #
.LEHE1:
.cfi_endproc
.LFE1302:
.globl __gxx_personality_v0
.section .gcc_except_table,"a",@progbits
.LLSDA1302:
.byte 0xff
.byte 0xff
.byte 0x1
.uleb128 .LLSDACSE1302-.LLSDACSB1302
.LLSDACSB1302:
.uleb128 .LEHB0-.LFB1302
.uleb128 .LEHE0-.LEHB0
.uleb128 .L4-.LFB1302
.uleb128 0
.uleb128 .LEHB1-.LFB1302
.uleb128 .LEHE1-.LEHB1
.uleb128 0
.uleb128 0
.LLSDACSE1302:
.text
.size _Z18check_divisibilityi, .-_Z18check_divisibilityi
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB1303:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA1303
subq $8, %rsp #,
.cfi_def_cfa_offset 16
.LEHB2:
# tmp.cpp:15: check_divisibility(argc);
call _Z18check_divisibilityi #
.LEHE2:
# tmp.cpp:20: return 4;
movl $4, %eax #, <retval>
.L11:
# tmp.cpp:21: }
addq $8, %rsp #,
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.L16:
.cfi_restore_state
subq $1, %rdx #, tmp94
movq %rax, %rdi #, tmp95
je .L14 #,
.LEHB3:
call _Unwind_Resume #
.LEHE3:
.L14:
# tmp.cpp:16: } catch (const std::runtime_error& e) {
call __cxa_begin_catch #
call __cxa_end_catch #
# tmp.cpp:17: return -12;
movl $-12, %eax #, <retval>
jmp .L11 #
.cfi_endproc
.LFE1303:
.section .gcc_except_table
.align 4
.LLSDA1303:
.byte 0xff
.byte 0x3
.uleb128 .LLSDATT1303-.LLSDATTD1303
.LLSDATTD1303:
.byte 0x1
.uleb128 .LLSDACSE1303-.LLSDACSB1303
.LLSDACSB1303:
.uleb128 .LEHB2-.LFB1303
.uleb128 .LEHE2-.LEHB2
.uleb128 .L16-.LFB1303
.uleb128 0x1
.uleb128 .LEHB3-.LFB1303
.uleb128 .LEHE3-.LEHB3
.uleb128 0
.uleb128 0
.LLSDACSE1303:
.byte 0x1
.byte 0
.align 4
.long _ZTISt13runtime_error
.LLSDATT1303:
.section .text.startup
.size main, .-main
.ident "GCC: (GNU) 7.1.1 20170621"
.section .note.GNU-stack,"",@progbits
We’re going to start by taking a look at the language-specific data area (LSDA)
towards the end of the assembly (starting at the
.gcc_except_table
). Understanding this region is important to understanding
the control flow when an exception is caught and thrown.
This is the header for the LSDA. Not too much interesting happening here:
This is also not that interesting, other than the third field which specifies the total length of the call site table.
This describes the entries in the type table. The most interesting bit is the second field, which is the offset of the type table. The first byte is simply the encoding.
Now, we get the meat of it - the call site table. A definition to note is
“landing pad” this is the code in the catch portion of a try/catch sequence.
The first entry here (.uleb128 .LEHB2-.LFB1303
) is the start of the
instructions for the first call site. The location is specified in terms of the
byte offset from the landing pad base, which in practice is the start of the
LSDA (in this example, the label is LFB1303). Look at where the LEHB2 and
LFB1303 labels are, and it should be clear what this is doing.
The second field here (.uleb128 .LEHE2-.LEHB2
) is the length of the
instructions for the current call site. Check where these labels appear in the
full disassembly - there’s only one instruction for the current call site (call
_Z18check_divisibilityi
), and so these labels are on either side of that
instruction.
The third field here (.uleb128 .L16-.LFB1303
) is a pointer to the landing pad
for this sequence of instructions. Once again, this is an offset from the
landing pad base. As an exercise, check the location of L16 and LFB1303, which
should help build a mental model of what this field means.
The next field (.uleb128 0x1
) encode the action to take. There might not be an
action to take, in which case the field is zero. If there is an action to take,
the field will be 1 + an offset into the action table (below). In this case,
we’re taking the first action in the action table.
The next entry in the site table is pretty similar to the first one (check out the location of those LEHB3 and LEHE3 labels to see what it’s doing). Interestingly, this function has no landing pad (the third field is 0) - this makes sense, since this is here to handle the case where another exception occurs while unwinding - and you aren’t allowed to throw again while that’s happening, so it makes sense there isn’t a “catch” here.
Finally, the action table - this action table is pretty boring and contains only a single action. The first byte is a type filter (which we’ll cover shortly), and the second is the pointer (again, a byte offset) to the next action in the action table. We have zero here, which means this is the last action.
Back to the type filter - the 0x1 here corresponds to an index in the types table. In this case, it’s a negatvie index - the value 0x1 means the value one before the base of the types table.
Here, the types table is extremely simple, and it’s clear that this entry means
we’re catching a std::runtime_error
. The TI
in that entry stands for “Type
Info”. This is what allows us to catch different types of error at runtime, and
it’s worth adding a second catch clause for a different type and looking at what
happens to this table. A null entry (.long 0
) in the types tables is a
catch-all handler, and again, it’s worth experimenting with: add a catch (...)
clause and see what happens.
The types table, described earlier:
So, now that we understand how the exceptions in our code map onto the assembly,
let’s run a program and see if we can figure out what’s going on. Running this
under GDB, we see that once check_divisibility
is called, we end up jumping
to L10, and allocating a std::runtime_error
. But while calling __cxa_throw
,
we jump to L16. So, __cxa_throw
is doing the heavy lifting of inspecting the data structures outlined
above. This is not a blog post about GCC’s internals, but you can probably look
through the code starting with the above function. To cut to the chase, the
function that does the actual jump is uw_install_context
.
Now that we’re back in main
, we continue happily until we jump to L14. That’s
right, this is the catch
clause of our try
- but what are those
__cxa_begin_catch
and __cxa_end_catch
functions doing? They’re basically
bookkeeping, with __cxa_begin_catch
,
in our case, simply setting some bookkeeping state, and __cxa_end_catch
checking that state. They can do more, but our use case is simple and the code
is easy enough to read if you’re curious.
With that sorted, we set our return value to -12, and jump back to L11, our common code for returning whatever value we’ve put on the stack - and we’re done.