Ch20.9: Legacy Exceptions Cannot Exist Here
1. The Feature
P1105R0 §4.1 is the section that started the modern freestanding work, and its first sentence is the argument: kernel and embedded environments cannot universally afford exceptions, and throwing one "requires a heap allocation on the Itanium ABI, and a large stack allocation on the Microsoft ABI, neither of which are suitable" in a kernel.
19.3 taught the mechanism and 19.8 measured it. This page is the freestanding question only: what does it need from the environment, and what do you do when that environment is the kernel itself?
2. Measured: Eleven Symbols
#include <stdexcept>
void fail(int x) noexcept(false) { if (x < 0) { throw ::std::runtime_error("negative"); } }
int main() { try { fail(-1); } catch (::std::runtime_error const &) { return 0; } return 1; }
$ g++ -std=c++26 -O2 -c legacy.cc && nm -u legacy.o
U _Unwind_Resume
U _ZNSt12__cow_stringC2EPKc
U _ZNSt13runtime_errorD1Ev
U _ZTISt13runtime_error
U _ZTVSt13runtime_error
U __cxa_allocate_exception
U __cxa_begin_catch
U __cxa_end_catch
U __cxa_free_exception
U __cxa_throw
U __gxx_personality_v0
Eleven, and every one of them is a component of an operating system facility:
-
__cxa_allocate_exception— a heap. On a target with no heap, this function cannot be written. -
_Unwind_Resume— an unwinder, a stack walker that understands the target's frame layout. -
__gxx_personality_v0— the per-frame callback that decides whether this frame catches the exception. It reads the language exception table,.gcc_except_table, for every function on the way out. -
_ZTISt13runtime_error,_ZTVSt13runtime_error— RTTI and vtables, needed to match a handler against the thrown type (20.8). -
The frame tables themselves,
.eh_frame, which must be registered with a loader so the unwinder can find them.
There is no subset of that you can provide yourself. Writing them means writing a runtime library, compiled for your target, ABI-compatible with the compiler's expectations, and then maintaining it against every compiler release. This is the load that 20.3 described as making C++03's freestanding mode a lie.
3. In a Kernel There Is Nobody to Ask
On a hosted system the unwinder comes from libgcc_s.so, the frame tables are
registered by ld.so, and the heap is brk/mmap. A
kernel is the layer those would have been built on. If it wants legacy exceptions
it must implement an unwinder over its own interrupt frames, an exception-table registry
for its own loadable modules, and a heap that is reentrant enough to be called from a
faulting context — and then it is maintaining a private C++ runtime inside the kernel,
which is the definition of the problem.
There is a second reason that has nothing to do with machinery: an exception thrown in kernel context resumes in some frame. Which frame? A process that has already been killed? An interrupt handler with a clobbered register state? Legacy exceptions assume the unwound-to frame is a function call in a healthy thread, and in a kernel that assumption is simply false. This is what "especially broken for kernels" means (20.4).
4. On a GPU There Is No Version of It at All
The kernel at least could build the runtime at enormous cost. Device code cannot: every GPU programming model requires exceptions to be off, there is no unwinder in the device runtime, no dynamic loader, no exception tables, and no place to store a thrown object — a thread that throws has nowhere to go, because the other threads in the warp and block are not part of its call stack.
| Environment | What legacy exceptions need | What exists there |
|---|---|---|
| Freestanding / bare metal | Unwinder, frame tables, heap, RTTI, loader | Nothing unless you bring it |
| Kernel | All of the above, plus a definable resume frame | The kernel itself, which cannot assume any of it |
| GPU device code | All of the above | Not part of the model at all; exceptions are off by requirement |
| WebAssembly | Unwinding as a target feature | Not in the base target; proposals add it to the engine, not to your binary |
Any -fno-exceptions build |
— | error: exception handling disabled, use '-fexceptions' to enable |
Five rows, and in four of them the answer is "this mechanism is unavailable". A language whose only error channel is this one is a language that cannot report an error on four of those targets — and the workaround, an error code threaded by hand through every function, is the thing C++ users were told they had escaped.
5. So They Are Banned, Not Discouraged
This is why 19.7 does not say
"prefer herbceptions"; it says add -fno-exceptions to the build so that a
legacy throw is a compile error. On a freestanding target, a build that has
not banned them is a build that links on your desktop and fails on the board, which is the
worst of both worlds.
The next page is the other half: the mechanism that does work here, with one linker symbol instead of eleven.