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:

.LFE1303:
	.section	.gcc_except_table
	.align 4

This is also not that interesting, other than the third field which specifies the total length of the call site table.

.LLSDA1303:
	.byte	0xff
	.byte	0x3
	.uleb128 .LLSDATT1303-.LLSDATTD1303

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.

.LLSDATTD1303:
	.byte	0x1
	.uleb128 .LLSDACSE1303-.LLSDACSB1303

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.

.LLSDACSB1303:
	.uleb128 .LEHB2-.LFB1303
	.uleb128 .LEHE2-.LEHB2
	.uleb128 .L16-.LFB1303
	.uleb128 0x1
	.uleb128 .LEHB3-.LFB1303
	.uleb128 .LEHE3-.LEHB3
	.uleb128 0
	.uleb128 0

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.

	.uleb128 .LEHB3-.LFB1303
	.uleb128 .LEHE3-.LEHB3
	.uleb128 0
	.uleb128 0

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.

.LLSDACSE1303:
	.byte	0x1
	.byte	0

The types table, described earlier:

	.long	_ZTISt13runtime_error

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.