Estell Shrinks ARM Firmware as C++ Exceptions Beat Error Return Codes

C++ Exceptions for Smaller Firmware - Khalil Estell - CppCon 2024 — CppCon

CppCon

Table-based C++ exceptions function as binary code compression on microcontrollers because stripping distributed error-check branches outpaces unwind metadata overhead once a firmware project surpasses 550 fallible operations.

AI-assisted summary of C++ Exceptions for Smaller Firmware - Khalil Estell - CppCon 2024 — original source. Check the original for full context.

Source published:

Key ideas

  • Embedded C++ dogma routinely bans exceptions due to perceived binary bloat, yet default GCC runtime bloat (~150 KB) is primarily caused by verbose terminate handlers that can be reduced to ~13 KB by overriding weak symbols. (17:22)
  • Code-size modeling demonstrates that table-based exceptions function as code compression, breaking even against error-return checks (such as std::expected) at roughly 4 to 7.5 fallible calls per function. (67:19)
  • Dynamic memory allocation and RTTI bloat can be completely avoided by overriding __cxa_allocate_exception with static storage and compiling with -fno-rtti, which retains type info only for explicitly thrown types. (19:05)
  • A customized ARM unwinder cuts GCC libunwind runtime size down to ~4.5 KB while reducing unwinding latency from a 27x penalty over std::expected down to 4.77x at a 6-frame depth. (71:05)

Detailed notes

## Background Published on YouTube by CppCon on December 6, 2024, this presentation features Khalil Estell, an ISO C++ standards committee member with extensive production firmware experience at Google. The talk explores how C++ exceptions—long avoided in embedded development—can be properly configured to reduce firmware binary sizes compared to value-based error handling.

## TL;DR Khalil Estell challenges the long-standing embedded systems dogma of compiling with `-fno-exceptions` by demonstrating that C++ exceptions can serve as an effective form of code compression. While trivial programs experience an initial runtime overhead, real-world firmware with extensive error-checking boilerplate (`std::expected` or return codes) scales poorly because distributed check branches add code at every call site. By dissecting the ARM Exception Handling ABI, `.ARM.exidx`, `.ARM.extab`, and eliminating bloat through custom weak symbol overrides like `__cxa_allocate_exception`, Estell reveals that exceptions beat return codes in code density across larger codebases. He also introduces an ELF-inspecting static analysis tool, Exception Insights, to resolve the safety and visibility concerns traditionally associated with exceptions in bare-metal environments.

## Atmosphere & style The presentation is energetic, irreverent, and deeply technical, blending self-deprecating humor with razor-sharp disassembly analysis. Estell commands the stage like an enthusiastic hacker-educator, playfully bantering with audience members, reacting to the dread of assembly math, and poking fun at modern language rivalries with Rust. The talk transitions seamlessly from high-level architectural frustrations down to single-byte LEB128 encodings, creating an atmosphere of an eye-opening deep dive that dismantles established programming dogma.

## Bulletin - [17:22](https://www.youtube.com/watch?v=bY2FlayomlE&t=1042s) Embedded C++ dogma routinely bans exceptions due to perceived binary bloat, yet default GCC runtime bloat (~150 KB) is primarily caused by verbose terminate handlers that can be reduced to ~13 KB by overriding weak symbols. - [01:07:19](https://www.youtube.com/watch?v=bY2FlayomlE&t=4039s) Code-size modeling demonstrates that table-based exceptions function as code compression, breaking even against error-return checks (such as std::expected) at roughly 4 to 7.5 fallible calls per function. - [19:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=1145s) Dynamic memory allocation and RTTI bloat can be completely avoided by overriding __cxa_allocate_exception with static storage and compiling with -fno-rtti, which retains type info only for explicitly thrown types. - [01:11:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=4265s) A customized ARM unwinder cuts GCC libunwind runtime size down to ~4.5 KB while reducing unwinding latency from a 27x penalty over std::expected down to 4.77x at a 6-frame depth.

## Key takeaways - [01:04](https://www.youtube.com/watch?v=bY2FlayomlE&t=64s) Bare-metal microcontroller environments operate under severe flash and RAM constraints, routinely prohibiting dynamic heap allocation and large runtime libraries. - [04:46](https://www.youtube.com/watch?v=bY2FlayomlE&t=286s) Abstracting fallible hardware peripherals (like I2C IO expanders) behind clean, infallible void interfaces creates architectural conflicts that error-code propagation handles poorly. - [10:53](https://www.youtube.com/watch?v=bY2FlayomlE&t=653s) Propagating error values across codebases via std::expected or result types introduces widespread boilerplate that tempts developers to silently discard errors. - [17:42](https://www.youtube.com/watch?v=bY2FlayomlE&t=1062s) The massive binary bloat observed when first enabling exceptions on ARM is caused by formatting utilities in __gnu_cxx::__verbose_terminate_handler, which can be eliminated with a trivial weak symbol override. - [19:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=1145s) Overriding __cxa_allocate_exception allows exception payloads and headers to reside in deterministic, pre-allocated static storage, removing malloc requirements. - [20:01](https://www.youtube.com/watch?v=bY2FlayomlE&t=1201s) Compiling with -fno-rtti does not trigger undefined behavior with exceptions; it safely removes type information for all types except those explicitly thrown or caught. - [24:49](https://www.youtube.com/watch?v=bY2FlayomlE&t=1489s) Error-as-value propagation imposes a distributed tax scaling with call sites (N), whereas exception tables scale primarily with non-leaf function definitions (M). - [27:22](https://www.youtube.com/watch?v=bY2FlayomlE&t=1642s) In controlled benchmarks, exception-based code crossed over to become smaller than result-based code at approximately 550 checked call sites. - [37:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=2248s) ARM Exception Index (.ARM.exidx) and Exception Table (.ARM.extab) structures allow trivially unwindable functions to encode return adjustments in as few as 8 bytes. - [50:23](https://www.youtube.com/watch?v=bY2FlayomlE&t=3023s) Catch matching under the Itanium ABI relies on fast pointer equality between type descriptors rather than runtime string comparisons. - [01:02:39](https://www.youtube.com/watch?v=bY2FlayomlE&t=3759s) In real-world firmware, functions requiring catch blocks or complex automatic destructors are rare, meaning most functions pay only the minimal 8-byte unwind table cost. - [01:08:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=4108s) Because exceptions eliminate branch instructions and epilogue handling from call sites, table-based unwinding acts effectively as binary code compression. - [01:11:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=4265s) GCC's ARM unwinder has suffered from decades of stagnation, but streamlined implementations significantly reduce unwinding overhead and runtime size. - [01:12:25](https://www.youtube.com/watch?v=bY2FlayomlE&t=4345s) The 'Exception Insights' binary analysis tool resolves the invisibility problem of exceptions by deducing throw sites, catch targets, and noexcept violations directly from compiled ELF binaries.

## Detailed walkthrough ### [00:08](https://www.youtube.com/watch?v=bY2FlayomlE&t=8s) Introduction and Embedded Background

Khalil Estell introduces himself and his goal for the session: demonstrating how developers can actually make their binaries smaller by embracing C++ exceptions, running directly counter to prevailing embedded dogma. Estell graduated from San Jose State University in 2017 and is known online by his handle across various platforms. Before his current work, he was an engineer at Google working on consumer hardware. On the original Google Pixel Watch, he served as the directly responsible engineer across several core hardware domains—including haptics, the bootloader, the speaker, and the microphone—before transitioning to the Google Pixel Buds team to focus on low-level code optimization to reclaim enough flash space for new features and bug fixes. In addition to serving on the ISO C++ standards committee, he continues to volunteer at SJSU, primarily mentoring their student robotics team.

Estell emphasizes his deep background in resource-constrained systems to frame the talk's thesis: > “I was the directly responsible engineer for the haptics and the bootloader and the speaker and the microphone and eventually when I got tired of that I moved over to the Google Pixel Buds and worked on code optimization so we could fit more features and bug fixes into the device.”

[01:04](https://www.youtube.com/watch?v=bY2FlayomlE&t=64s) Bare-Metal Reality: No OS, Limited Flash, and Restricted RAM

The motivation for this research originated six years prior while Estell was teaching Computer Engineering 146, an introductory embedded systems course at SJSU. For the class, he authored an educational C++ firmware framework built around the C++ Core Guidelines and designed a custom development board for students. A central lesson for engineers entering bare-metal development is the complete absence of an operating system. Without an underlying OS, developers do not get standard POSIX utilities: there is no `fopen`, no filesystem, no network stack, no serial terminal to receive standard output, no system clock, and no threading primitives. Any of these capabilities must either be hand-rolled or supplied by external libraries, but external libraries run immediately into brutal hardware storage walls.

A real-time search on electronic distributor DigiKey illustrates the steep memory cliff in the microcontroller market. Flash memory and RAM distributions show that the vast majority of available microcontrollers sit far below desktop-scale capacities:

| Resource Metric | Below Threshold | Above Threshold | Dominant Tier | |:--- |:--- |:--- |:--- | | **Flash Memory** (1 MB threshold) | ~33,000 devices | ~5,000 devices | < 1 MB | | **SRAM** (100 KB threshold) | ~31,000 devices | ~7,000 devices | < 100 KB |

To avoid out-of-memory crashes and heap fragmentation across these tiny memories, firmware guidelines routinely ban dynamic allocation primitives such as `malloc`, `free`, `calloc`, `new`, and `delete`. This restriction also disqualifies standard heap-allocating STL containers. While embedded practitioners often argue that allocation during boot is acceptable so long as memory is never reallocated or freed at runtime, general-purpose containers remain problematic. Developers turn instead to static-storage libraries like the Embedded Template Library (ETL), or the newly standardized `std::inplace_vector` voted into C++26 at the St. Louis meeting, which provides fixed-capacity contiguous storage alongside safety-checking APIs like `try_push_back`.

> “The first thing that I teach a lot of my students is that when you're developing for microcontrollers and doing bare-metal development you don't have an operating system that means you don't have access to fopen, you don't have a file system, you don't have networking, you don't have a terminal to print to, you don't even have a clock or even threads.”

[03:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=208s) Why Firmware Developers Reject C++ Exceptions

Given these physical constraints, firmware teams almost universally compile with `-fno-exceptions`. Estell catalogs the litany of common objections raised against exception handling in embedded engineering: - **Heap Allocation Requirements:** Standard exceptions are widely assumed to require `malloc` to instantiate exception objects, introducing the risk of unbounded heap usage during fault conditions. - **Binary Bloat and Transitive Dependencies:** There is a persistent belief that enabling exceptions drags in heavyweight runtime overhead. One developer warned Estell that flipping the switch pulls in the entire standard library, including `std::string` and `std::vector`. - **RTTI Overhead:** Exception matching relies on runtime type information (RTTI), adding metadata tables that consume precious flash. - **Table and Runtime Complexity:** Exception unwinding relies on `.eh_frame` exception tables and personality routines that demand significant flash footprint. - **Non-Determinism and Latency:** The unwinding process requires binary searching through tables, calculating frame offsets, and evaluating landing pads, making worst-case execution time hard to bound for real-time safety.

Estell notes that while these concerns are pervasive, many of them stem from misunderstandings or historical implementation details that can be engineered around.

> “I had one individual tell me that if you use exceptions it brings in the entire STL, all string, vector, everything that get brought into your binary, can't handle that, it's too much memory being used up.”

[04:46](https://www.youtube.com/watch?v=bY2FlayomlE&t=286s) An Infallible Abstraction Meets a Fallible Bus: The IO Expander Problem

To demonstrate how error handling challenges manifest in firmware design, Estell recalls drafting basic hardware driver abstractions for his students. The simplest abstraction is an `output_pin`, which controls a physical microcontroller pin by toggling its electrical state between low voltage (ground, 0V) and high voltage (VCC). With an LED connected between the pin and ground, pulling the pin high establishes a potential difference, driving current and illuminating the LED. Estell modeled this with a minimalist interface: a single member function `void level(bool)`, where `true` maps to high and `false` maps to low. Because setting an on-chip register bit is practically infallible, a `void` return type felt natural.

The interface breaks down when the pin is not an on-chip GPIO, but located across a serial communication bus such as I2C (Inter-Integrated Circuit). I2C uses two open-drain physical lines—serial clock (SCL) and serial data (SDA)—to connect a bus controller to multiple peripheral devices:

```mermaid flowchart TD accTitle: Infallible Output Pin Interface Over a Fallible I2C Bus accDescr: Flowchart showing an application invoking a void level function which translates into an I2C transaction that can fail due to NACK or bus errors.

subgraph Host["Microcontroller Host"] App["Application Logic"] Pin["output_pin Interface<br/>void level(bool)"] I2CDrv["I2C Controller Driver"] App --> Pin Pin --> I2CDrv end

subgraph Bus["I2C Communication Bus"] Start["Start Signal + Address + R/W"] Ack{"Device ACK?"} Data["Write Data Byte + ACK"] Start --> Ack Ack -->|"ACK"| Data end

subgraph Peripheral["Off-Chip Hardware"] Expander["I2C IO Expander"] LED["Physical Pin Output (LED)"] Expander --> LED end

I2CDrv --> Start Data --> Expander Ack -.->|"NACK (Fault)"| Err["I2C Error State"] ```

I2C operations are fundamentally fallible. The controller issues a start condition, broadcasts a 7-bit device address with a read/write bit, and waits for the target device to pull SDA low for an acknowledge (ACK) bit. If the target is absent or unresponsive, a non-acknowledge (NACK) occurs. While a missing device may be benign in hot-swappable modular systems, losing communication with a flight-controller IMU on a drone causes catastrophic failure. Furthermore, bus lines can experience physical IO errors due to electromagnetic interference (EMI) or rogue peripherals locking the lines.

When an engineer runs out of native GPIOs and adds an external I2C-driven IO expander, an architectural conflict arises: controlling the pin now requires an I2C transaction that can fail with NACKs or IO errors, yet the existing `output_pin` interface is modeled as an infallible `void level(bool)`.

> “But here comes the rub: the I2C side is a fallible interface and the output pin is an infallible interface, at least the way that I wrote it prior.”

[08:50](https://www.youtube.com/watch?v=bY2FlayomlE&t=530s) The Pain of In-Band Error Reporting and Moving to `std::expected`

When exceptions are turned off, handling failure in embedded firmware becomes messy very quickly. If a function cannot return an error code directly, developers are forced into bad patterns: setting an internal state variable on the object or setting a global `errno`-style flag, requiring the caller to check a separate status function. “Setting up `errno` is not a good option—we all kind of figured that part out—and I don't think that using an internal variable and having to call an out-of-line function is any better than `errno`.”

```mermaid flowchart TD accTitle: Hardware Failure Severity Spectrum accDescr: Spectrum of embedded failure modes from mild LED indicators up to critical emergency stop relays. subgraph NonCritical["Mild Impact"] LED["Status LED fails to light"] end subgraph MediumCritical["System Impact"] CEL["Check-engine light fails"] Heat["Heating element fails during chemical reaction"] end subgraph HighCritical["Critical Impact"] Relay["Emergency cut-off relay fails to trip"] end LED --> CEL --> Heat --> Relay ```

Khalil Estell considered whether this is genuinely a serious problem by walking through what his students might actually control with a GPIO output pin: - **Toggling a generic indicator LED:** If it fails to turn on, the user might be mildly annoyed, but it is rarely catastrophic. - **A check-engine warning light:** If an alert LED fails to light up when an engine is failing, the consequences escalate immediately. - **A heating element:** If that pin drives a heater regulating a chemical reaction, failing to switch or report failure can ruin a batch or cause dangerous runaway conditions. - **An emergency cut-off relay:** Even if the software cannot physically recover when a hardware fault prevents a relay from tripping, “at the very least we can tell you that something went wrong when you tried to attempt this action.”

Recognizing that silent failures are unacceptable, Estell decided to migrate all his APIs to functional error handling using `std::expected`. He defined a custom type alias—`status`—where the expected value type is `void` and the error type is a custom error enumeration. For functions producing values, the return type becomes a `result<T>`, holding either the requested data (such as a device version number) or an error created via `std::unexpected` when a precondition is violated.

[10:53](https://www.youtube.com/watch?v=bY2FlayomlE&t=653s) Syntactic Noise, Propagation Virality, and Student Backlash

Adopting `std::expected` fundamentally transformed what user code looked like. An operation as simple as blinking an LED—setting a pin high, delaying, setting it low, and delaying again—is clean and readable when using exceptions or assuming an infallible world. With `std::expected`, however, every single call must be followed by an explicit check on the returned status; if an error is present, the function must unpack it and return an unexpected value upward.

```mermaid flowchart LR accTitle: Macro-based Error Propagation Flow accDescr: Demonstrates how sj_check intercepts failures and returns them early out of the current function. Call["Invoke fallible API"] --> Check{"Result ok?"} Check -- "Success" --> Unpack["Extract value / continue happy path"] Check -- "Failure" --> EarlyRet["Early return std::unexpected(err)"] ```

To reduce the mountain of boilerplate, Estell looked to paper P2616R1, which proposed an error propagation operator using `??` (very similar to the `?` operator in Rust) to automatically propagate errors up the call stack. He even emailed the author of the proposal, though he never heard back—a fact he now views as a blessing in disguise, noting that had it been accepted, “it would have caused me to not go and do the research that I did currently.” In the absence of native language support, he wrote a macro named `SJ_CHECK` to evaluate the expression and early-return any failure.

Yet the problem went deeper than syntax. Because hardware protocols like I²C are inherently fallible, and virtually anything—from motor controllers to graphical displays—can be connected over an I²C bus, almost every interface had to be marked fallible. This triggered an explosion of error types across the codebase. Over two and a half years of forcing this paradigm on students, the cracks showed: - **Discarding errors:** Students hated writing repetitive `if` statements so intensely that they began casting error returns to `(void)` to bypass them. Estell recalled spending a long debugging session with a student who was stuck for a week, only to discover that her code ignored an address failure on an I²C device that proper error propagation would have highlighted immediately. - **Pervasive virality:** Students grew frustrated that fallibility infected every layer: “Man, I got to make every single thing return back status or results every single time; if I want to call anything that could potentially be fallible, I got to put it everywhere—it's everywhere and it touches everything.” - **Visual disruption:** Macros obscured the primary execution path. Developers had to mentally block out lines filled with macro wrappers just to see the happy path before turning their attention to the sad path.

[14:11](https://www.youtube.com/watch?v=bY2FlayomlE&t=851s) The Shift to Re-examining C++ Exceptions

Inevitably, students asked the obvious question: “Why not just use C++ exceptions? They're built into the language, they're meant for error handling, wouldn't that get rid of a lot of the problems that we're dealing with?”

For years, Estell gave them the standard embedded dogma. He would present diagrams, cite articles, and switch compiler flags from `-fno-exceptions` to `-fexceptions` to watch the resulting binary explode in size, asking them rhetorically why anyone would want such bloat. Most students accepted this, but some pressed deeper: - Why are exceptions non-deterministic? - Why do they have to be slow? - Why do they bloat the binary so severely? - Why do they require dynamic heap allocation?

Answering those questions made Estell increasingly uneasy. “I did not like the idea that there was that I was telling my students this information about why you shouldn't use something that I don't know how it works—it's a black box to me, I just know what I've been told from other people.” Feeling guilty about propagating dogma without firsthand technical proof, he decided to give exceptions a fair, rigorous evaluation on bare-metal ARM microcontrollers.

[15:24](https://www.youtube.com/watch?v=bY2FlayomlE&t=924s) Enabling Exceptions on Bare-Metal ARM and the Bloat Shock

Getting C++ exceptions to actually execute on ARM hardware is not straightforward:

```mermaid flowchart TD accTitle: Steps and Roadblocks Enabling ARM Exceptions accDescr: Sequence showing the barriers encountered when turning on exceptions in an ARM toolchain. subgraph Obstacle1["Barrier 1: Toolchain Defaults"] A["Default GNU ARM toolchain"] --> B["Throws trigger std::terminate immediately"] end subgraph Solution1["Recompilation & Runtime"] B --> C["Rebuild runtime libraries with -fexceptions"] C --> D["Implement sbrk stub for malloc backing"] end subgraph Obstacle2["Barrier 2: Massive Code Size"] D --> E["Throw & Catch successfully executes"] E --> F["Binary balloons from 2.6 KB to ~150 KB"] end ```

The first obstacle is that the precompiled GNU ARM toolchains disable exceptions by default in the runtime libraries. If code attempts to throw an exception, it immediately calls `std::terminate`, regardless of how many valid `try`/`catch` blocks are in place. Worse, the toolchain still links in substantial runtime machinery needed for exceptions, causing binary bloat without providing functional error handling.

To get around this, Estell had to download the runtime source, locate where `-fno-exceptions` was defined, flip it to `-fexceptions`, and recompile the toolchain runtime libraries. Additionally, because the standard exception runtime allocates exception objects dynamically, he had to implement `_sbrk`, the low-level system call stub that supplies raw heap memory pages to `malloc`.

Once those obstacles were cleared, a minimal program—a `main()` calling a `[[gnu::noinline]]` `start()` function that throws an object and lands in a `catch` block—finally worked. But the cost was staggering: - The minimal binary jumped from **2.6 KB** to approximately **150,000 bytes** (~150 KB). - On a microcontroller with only 512 KB (half a megabyte) of total flash memory, the exception machinery alone consumed roughly **30%** of the entire device capacity just to execute a basic throw and catch.

Refusing to accept this overhead at face value, Estell set out to eliminate the bloat. He disassembled the generated ELF binary to inspect the assembly instructions, hunting for symbols inserted by the compiler and runtime libraries. Among the unexpected symbols dominating the output, one immediately stood out: `__gnu_cxx::__verbose_terminate_handler` and a symbol called `Global string callback`.

[17:42](https://www.youtube.com/watch?v=bY2FlayomlE&t=1062s) Eliminating Binary Bloat with Weak Symbols

While inspecting the binary bloat caused by enabling exceptions, Khalil Estell discovered unexpected standard library components embedded in his build: string adapters, `append_num`, `sprintf`, and `strlen`. Tracing these symbols led to `__gnu_cxx::__verbose_terminate_handler()`, the routine responsible for printing human-readable panic messages such as `"terminate called after throwing an instance of my_error"`. The verbose formatting and string manipulation required for this output were the direct cause of the massive binary size.

```mermaid flowchart TD accTitle: Replacing Default Verbose Terminate Handler accDescr: GCC default verbose terminate pulls in heavy string dependencies, but overriding it with a custom weak symbol reduces binary size significantly.

subgraph Default ["Default GCC Behavior (~150 KB)"] A["Default __verbose_terminate_handler()"] --> B["String Adapters & Sprintf"] B --> C["Heavy I/O & Formatting Bloat"] end

subgraph Custom ["Overridden Implementation (~13 KB)"] D["User Definition (Weak Symbol Override)"] --> E["Empty while(1) Infinite Loop"] E --> F["91% Flash Savings"] end ```

To eliminate this overhead, Estell took advantage of weak symbols in GCC and Clang. A weak symbol allows developers to define a variable or function with the same name that overrides the default implementation provided by the standard runtime libraries without causing linker collisions. By replacing the verbose terminate handler with a custom function containing a trivial infinite `while` loop, the compiler omitted the formatting dependencies entirely.

- Overriding the terminate handler reduced the overall binary footprint by 91%, shrinking the image from 150 KB down to approximately 13 KB. - Estell remarked on the result: “In five simple lines you can eliminate all of this... 150k nah it's not okay but 13 kilobytes I can do a lot with that.”

---

[19:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=1145s) Overriding Dynamic Allocation via `__cxa_allocate_exception`

Despite the massive space savings, a significant architectural obstacle remained: C++ exceptions depend on dynamic memory allocation, pulling in `sbrk` and `malloc`. Estell noted the pedagogical dilemma this created: “It feels weird to teach my students not to use dynamic memory allocation and then my mechanism that is in their software development kit uses dynamic memory allocation.”

Investigating the throw assembly revealed that the runtime relies on `__cxa_allocate_exception` to provision storage for in-flight exception objects. Because this runtime function is also declared as a weak symbol, it can be replaced with an application-defined allocator. Overriding this entry point allowed Estell to supply a custom, fixed pre-allocated buffer with known bounds, bypassing the general-purpose heap entirely and giving embedded developers deterministic control over the memory overhead.

---

[20:01](https://www.youtube.com/watch?v=bY2FlayomlE&t=1201s) The Myth of RTTI Bloat and `-fno-rtti`

The next major hurdle was Run-Time Type Information (RTTI). While RTTI overhead was manageable in small experiments, Estell was concerned that adding more types and abstractions to larger applications would cause RTTI metadata to scale uncontrollably. To test what happens under `-fno-rtti`, Estell presented the audience with three possible compiler reactions when combining `-fno-rtti` with active exception handling:

1. The compiler throws a hard compilation error because exceptions require RTTI. 2. The compiler strips all RTTI metadata except for types specifically involved in exception handling. 3. The compiler silently strips RTTI metadata, resulting in undefined behavior when `throw` is called.

The majority of the audience voted for option 3, assuming C++ would quietly break at runtime. Estell joked about their cynicism: “Do all are you guys all Rustaceans? Are you guys all infiltrated CppCon? Because here's the thing, why do you guys all assume that C++ defaults to UB? No, the answer is number two.”

Compiling with `-fno-rtti` selectively strips type descriptors for every object in the program except those explicitly thrown or caught. Furthermore, the true memory layout cost of RTTI descriptors is far smaller than commonly assumed:

| Class Inheritance Hierarchy | Descriptor Size (Pointers) | |:--- |:--- | | Standalone object (no inheritance) | 2 pointers | | Single inheritance (inherits 1 parent) | 3 pointers | | Multiple inheritance (inherits $N$ parents) | $3 + N$ pointers |

Because embedded applications throw relatively few unique exception types, RTTI costs proved practically negligible. Estell noted that while the runtime once dragged in `iostream`, `locale`, and basic string machinery totaling 150 KB, modern ARM toolchains have improved significantly, dropping default costs down to around 84 KB to 87 KB before manual pruning.

---

[23:04](https://www.youtube.com/watch?v=bY2FlayomlE&t=1384s) Real-World Embedded Results: The MP3 Player Surprise

Confident that heap allocation and RTTI were manageable, Estell migrated the `sjsu-dev2` educational framework to C++ exceptions, removing the legacy error macros. Concurrently, he maintained an alternative pull request refactoring the same codebase to functional result types so he could directly compare real-world size and usability metrics.

```mermaid flowchart LR accTitle: Codebase Transition to Exceptions vs Result Types accDescr: Small demos gained size with exceptions, but complex projects like an MP3 player shrank from 56 KB to the 40s KB range.

subgraph Evaluation ["Architectural Paths Tested"] A["Legacy Macro Codebase"] --> B["C++ Exceptions Engine"] A --> C["Functional Result Types PR"] end

subgraph Impact ["Binary Size Trajectory"] B --> D["Tiny Demos (Blinky): Size Increased"] B --> E["Complex Applications (MP3 Player): Size Decreased (56 KB down to 40s KB)"] end ```

The results showed a clear split based on project size: - **Trivial projects** (such as LED blinkers and "Hello World" demos) grew larger with exceptions due to the fixed baseline cost of the unwinding machinery. - **Complex applications** saw their binary footprints contract noticeably. A student final project—an MP3 player targeting a microcontroller with 64 KB of flash—shrank from approximately 56 KB under macro/error return values down to the 40 KB range under C++ exceptions.

Reflecting on the findings, Estell wrote: “It was decided to transition to exception handling rather than error return types for large enough projects exceptions are small in code size and faster when exceptions are not being thrown.” However, he sat on the discovery for years, unsure if anyone would care and half-jokingly admitting he feared blowback from online communities: “I was also afraid of the Rust community coming after me, because if I showed anything positive for C++ I was certain they'd come after me and I'd get just completely washed on any social media.”

---

[24:49](https://www.youtube.com/watch?v=bY2FlayomlE&t=1489s) The Mathematical Hypothesis: Result Types vs. Exceptions

The silence ended at CppCon 2023 during an ISO C++ committee gathering. Patrice Roy gave a presentation on SG14 activities, debating whether `std::pmr::vector` could be safely used in embedded systems given that `push_back` lacks an in-band failure channel and cannot notify developers of allocation failures without panicking or terminating. Estell mentioned that embedded systems could resolve this by turning on exceptions to achieve smaller binary footprints. Roy challenged the assertion: “Hold on, hold on, that goes against all of common knowledge, what do you mean?” Roy then asked for concrete numbers and invited Estell to present his findings at the Wednesday ISO working meeting.

To demonstrate why exceptions outperform result types as programs grow, Estell formalized a cost model for binary size:

- **Result Types Cost Formula:** $$\text{Cost} = A \cdot N$$ Where $A$ is the average size overhead (in bytes) of branches and status checks required per function call, and $N$ is the total count of checked function calls across the codebase. - **Exception Handling Cost Formula:** $$\text{Cost} = B \cdot M + E$$ Where $E$ represents the static footprint of the `libunwind` engine, $B$ is the average exception metadata overhead per function, and $M$ is the total count of non-leaf functions in the binary.

Because any non-trivial application invokes many more checked calls ($N$) than it has distinct non-leaf functions ($M$), the linear growth of error propagation sites ($A \cdot N$) will eventually outpace the fixed overhead of the unwinding engine ($E$) alongside metadata tables ($B \cdot M$), making exceptions the more compact solution at scale.

[27:22](https://www.youtube.com/watch?v=bY2FlayomlE&t=1642s) Code Size Crossing Point: Exceptions vs. Result Types

To rigorously test the binary size trade-off between exceptions and value-based error handling (such as `std::expected` or result types), Khalil Estell devised an automated code-generation experiment. Because firmware often contains extensive error propagation rather than local recovery, he generated large batches of isomorphic C++ code across two files: `except.cpp` and `result.cpp`. Both files performed the exact same operations—randomly defining classes with trivial and non-trivial destructors, instantiating objects, invoking APIs, and calling sequences of functions—diverging solely in how they propagated errors. Estell purposefully isolated error propagation: “From my experience, just to be real, I see more error propagation code than I see error handling code.” To prevent GCC and Clang from aggressively optimizing away the code or inlining everything across a single translation unit, volatile variables and side effects were added to force realistic code generation.

```mermaid flowchart LR accTitle: Binary Size Comparison Model accDescr: Graph showing where exceptions become smaller than return-based error checking. subgraph Growth ["Binary Size Growth"] R["Result / Expected Code<br/>Linear growth per check<br/>(+16.5 B/check)"] E["Exception Code<br/>Flat metadata tables<br/>(~867 B fixed runtime overhead)"] end Cross{"Crossing Point<br/>(~550 checks)"} R --> Cross E --> Cross Cross --> Win["Exceptions achieve smaller footprint"] ```

When compiling down the generated files and tracking binary growth against the number of checks, the data revealed a clear crossover point: - The baseline runtime overhead of the custom exception mechanism started at approximately 867 bytes, which closely mirrors the footprint of a minimal `libunwind`. - Result-based code grew significantly faster with each check, calculated by Google Sheets at approximately 16.5 bytes per check (`-16.5 * X` in the delta equation). - The crossover point occurred at roughly 550 error checks. Beyond this threshold, exception-based code resulted in a smaller overall binary size than return-value propagation. - Estell observed that 550 checks across an entire firmware application is remarkably small, noting: “I've definitely made single classes with up to 50 checks on its own.”

[29:43](https://www.youtube.com/watch?v=bY2FlayomlE&t=1783s) Demystifying Exceptions: Function Classifications

Estell emphasized that showing aggregate size graphs is insufficient to overcome long-held industry skepticism: “None of those graphs were meant to convince you at all. I don't think they're very convincing. I think they're there to show you something. If I want to convince you, I need to show you how exceptions work from throw to catch.” To do so, he restricted the scope to table-based unwinding models (explicitly omitting nested exceptions and alternative schemes like setjmp/longjmp), stating flatly: “To be honest, I think that table-based exceptions are the best.”

```mermaid flowchart TD accTitle: Exception Handling Function Classifications accDescr: Three categories of functions encountered during table-based exception unwinding. subgraph Types ["Function Classifications"] direction TB L["Leaf Functions<br/>• Do not call other functions<br/>• No destructors<br/>• No try/catch blocks<br/>• Never unwind"] T["Transparent / Trivial Functions<br/>• Call other functions<br/>• No destructors<br/>• No try/catch blocks<br/>• Frame skipped during unwind"] R["Reentrant Functions<br/>• Call other functions<br/>• Non-trivial destructors OR try/catch<br/>• Must be re-entered to clean up or handle"] end ```

To demonstrate the runtime mechanics, Estell uses a minimal sample program where function `foo` calls `bar` within a `try`/`catch` block, `bar` instantiates a destructible object and calls `baz`, and `baz` executes a `throw`: - **Leaf functions**: Do not invoke any other functions, contain no destructors, and have no `try`/`catch` blocks. Because they do not participate in error unwinding, they are irrelevant to exception runtime costs. - **Trivial / Transparent functions**: Call other functions but contain no local destructors and no `try`/`catch` handlers. In the example, `baz` is transparent; the unwinder simply steps through its frame without running cleanup code. - **Reentrant functions**: Call other functions and contain either active local destructors or `try`/`catch` blocks (or both). These functions must be re-entered during unwinding to execute cleanup routines or transfer control to a catch handler. In the sample code, both `foo` (handler) and `bar` (destructor) are reentrant.

[31:14](https://www.youtube.com/watch?v=bY2FlayomlE&t=1874s) ARM Calling Conventions and Register Preservation

Understanding exception unwinding requires understanding the target calling convention. On 32-bit ARM architectures: - Registers `r0` through `r3` serve as scratch registers, function argument inputs, and return values. - Registers `r4` through `r11` are callee-preserved (non-volatile) registers; any function that overwrites them must restore their exact initial state before returning to its caller. - `SP` (Stack Pointer / `r13`) tracks the top of the active stack frame. - `LR` (Link Register / `r14`) holds the return address where execution resumes once a function completes. - `PC` (Program Counter / `r15`) points to the instruction currently being executed.

In standard ARM assembly, a function prologue typically executes `push {r4, lr}`. This preserves the caller's `r4` value and saves the return address onto the stack. After the function performs its computations (often placing a return value in `r0` or using `r4` for scratch calculations), the epilogue executes `pop {r4, pc}`. Popping the link register directly into the program counter performs an immediate branch back to the caller while restoring preserved registers in a single instruction.

[32:50](https://www.youtube.com/watch?v=bY2FlayomlE&t=1970s) Throw Allocation and the Exception Header

When execution reaches a `throw` statement under the Itanium C++ ABI, the compiler emits a call to `__cxa_allocate_exception` passing the byte size of the exception object. Rather than allocating only the payload object, `__cxa_allocate_exception` allocates sufficient space for the user payload preceded immediately by an internal control structure called the exception header.

```mermaid flowchart LR accTitle: Exception Memory Allocation Layout accDescr: Memory layout showing the exception header placed directly before the thrown object. subgraph AllocatedBlock ["Single Memory Allocation"] direction LR subgraph Header ["Exception Header"] CPU["Virtual CPU Context<br/>(Preserved Registers)"] TI["std::type_info*"] DTOR["Destructor fn*"] Cache["Unwind Cache"] end subgraph Payload ["User Payload"] Obj["Thrown Object<br/>(e.g., 1-byte error enum)"] end end Header --- Payload ```

Estell notes that this header structure is his custom, compact implementation optimized for embedded systems rather than the stock Itanium header. The header stores: - A virtual representation of the CPU state (saved registers), because “exceptions, all they really do is calculate the previous state of what the CPU would have been if you had taken a different pathway.” - A pointer to the `std::type_info` descriptor of the thrown object to match against `catch` clauses. - A pointer to the object's destructor (or `nullptr` if trivially destructible) to clean up the exception object once handled. - Cache fields to accelerate subsequent frame lookups during propagation.

Estell explained why allocating the object and header together through `__cxa_allocate_exception` is preferable to a standard `malloc`: - **ABI stability and agility**: If the unwinder library changes the header size across releases (e.g., stripping unneeded registers or adding cache entries), code calling `__cxa_allocate_exception` remains binary-compatible. Hardcoding a raw `malloc` size directly into compiled application call sites would cause header offsets to break if the runtime changed. - **Allocation efficiency**: Combining both the internal runtime metadata and the exception object into a single allocation avoids heap fragmentation and eliminates the need for a secondary runtime allocation inside `throw`.

[35:01](https://www.youtube.com/watch?v=bY2FlayomlE&t=2101s) Entering `__cxa_throw` and Initiating the Unwind

Once the user object is constructed inside the allocated memory block, the compiler emits a call to `__cxa_throw`, passing three parameters: a pointer to the thrown payload, the `type_info` pointer, and the payload's destructor pointer.

Within `__cxa_throw`: - The active exception pointer is stored in thread-local storage (TLS) so that `std::current_exception` can reference it. Estell quipped about feedback from committee colleagues regarding TLS: “I know Ben Craig told me I need to get rid of TLS. I am working on it, don't worry.” - The runtime computes the location of the exception header by taking the address of the thrown type pointer and subtracting the size of the header: `header_ptr = object_ptr - sizeof(exception_header)`. - The `type_info` and destructor pointers are written into the header fields. - An immediate unwinding step is performed on `__cxa_throw` itself, capturing the CPU state of the throwing function (`baz`). The Link Register is moved into the Program Counter state so unwinding can evaluate the call site. - The runtime initiates unwinding (`_Unwind_RaiseException`). The unwinder enters an evaluation loop, searching the binary's exception tables for the exception entry corresponding to the current Program Counter to determine whether the enclosing function contains cleanup destructors or matching `catch` clauses.

[36:42](https://www.youtube.com/watch?v=bY2FlayomlE&t=2202s) The Unwind Decision Tree and Transitioning Paths

Khalil Estell steps through the logical flow executed during stack unwinding once an exception is active. The unwinder looks up the entry corresponding to the current execution state and begins evaluating questions: first, should unwinding be refused? For instance, if a function is marked `noexcept`, encountering an exception inside it requires an immediate transition to `std::terminate`. If not refused, the unwinder evaluates whether the function is reentrant. A transparent function with no cleanup needs can simply have its frame unwound immediately. If the function is reentrant—meaning it contains cleanup actions like destructor calls or local `catch` blocks—the unwinder must inspect it closely.

```mermaid flowchart TD accTitle: Exception Handling Evaluation Flow accDescr: Flowchart detailing how the exception unwinder decides between termination, entering the sad path, and resuming happy path execution.

subgraph Evaluation ["Frame Inspection"] A["Evaluate Frame Entry"] --> B{"Refuse Unwind?\n(e.g., noexcept)"} B -- "Yes" --> C["Call std::terminate"] B -- "No" --> D{"Reentrant?\n(Destructors / Catch)"} D -- "No" --> E["Unwind Frame"] end

subgraph SadPath ["The Sad Path"] D -- "Yes" --> F["Enter Sad Path"] F --> G{"Target Block"} G -- "Destructor cleanup" --> H["__cxa_cleanup_end"] G -- "Matching catch" --> I["__cxa_end_catch"] end

subgraph Resolution ["Return Flow"] H -->|"Resume unwinding"| A I -->|"Resume normal execution"| J["Happy Path"] end ```

When a matching `catch` or a pending destructor scope is found, execution branches into what Estell calls "the sad path." Leaving the sad path occurs via two standard runtime calls: - `__cxa_cleanup_end`: invoked after running local destructors so control can re-enter the unwinding mechanism to continue searching up the call stack. - `__cxa_end_catch`: invoked when a `catch` block finishes handling the exception, finally returning control to normal application code on the "happy path."

---

[37:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=2248s) Structure of the Arm Exception Index Table

Estell moves to the concrete details of locating exception metadata, examining the exception index section bounded by the linker symbols `__exidx_start` and `__exidx_end`. Under the Arm Exception Handling ABI, every entry in this index table occupies exactly eight bytes. The first 4-byte word provides the function's start address—represented as a relative offset for relocatability. The second 4-byte word points to the exception data, or contains inline instructions directly.

Crucially, entries in the exception index are laid out in strictly ascending order corresponding to the physical memory layout of functions in the text segment. If function `foo` is located at address 0, followed immediately by `bar` and then `baz`, the index mimics that precise ordering:

| Offset Field | Size | Purpose | |:--- |:--- |:--- | | First 32-bit word | 4 bytes | Relative offset pointing to function start address | | Second 32-bit word | 4 bytes | Tagged union: offset to exception data table OR inline unwinding instructions |

Because functions are guaranteed to be sorted monotonically in the index, the runtime does not need linear scans or hash lookups. Given any arbitrary program counter (PC) value sampled during a throw, the runtime performs a fast binary search over the table. If the PC falls strictly between the start address of one entry and the start address of the subsequent entry, the runtime knows with certainty that execution was inside the function to the left.

The second 32-bit field is a tagged union distinguished by its most significant bit (bit 31): - Bit 31 = `0`: The field is a signed relative offset pointing into the secondary exception data table (`.ARM.extab`). - Bit 31 = `1`: The field holds compact, inline unwind personality data directly inside the index table itself.

---

[39:02](https://www.youtube.com/watch?v=bY2FlayomlE&t=2342s) Inline Personality Data and Unwind Bytecode

To conserve ROM on memory-constrained systems, the Arm specification permits inlining instructions directly into the index whenever a function's requirements are minimal. If a transparent function (one lacking destructors or catch blocks) is marked `noexcept`, the offset field is replaced with the sentinel value `0x1` (cantunwind). Encountering `0x1` directs the unwinder to halt immediately and call `std::terminate`.

For transparent functions that do need unwinding, the runtime bytecode can be embedded directly into the lower bits of the entry if the function requires three or fewer instruction bytes. Estell points out that Arm machine code uses either 32-bit instructions (standard Arm or Arm64) or mixed 16/32-bit instructions (Thumb-2), but exception unwind instructions are far more compact: > “Unwind instructions don't have to do much. They have to subtract the stack, pop some registers, and that's about it. So they only need like a byte of information in order to represent them, so they're a compressed version of a simulated version of returning from a function.”

The Arm exception ABI establishes that the vast majority of compiled functions require three or fewer bytes of unwind instructions, and the absolute maximum required by any conceivable function generated by an Arm compiler is seven bytes. These single-byte operations encode actions like adjusting the stack pointer, popping saved registers (`R4`–`R11`, `LR`), completing unwinding (`FINISH`), or refusing to unwind.

The bit layout of inline personality data in the 32-bit word is structured as: - **Bit 31**: Set to `1` (identifies the entry as inline personality data). - **Bits 30–28**: Reserved. - **Bits 27–24**: Personality index. Index `0` defines a short layout where the remaining three bytes of the word contain up to three unwind instructions (instruction 1, instruction 2, instruction 3). - Indexes `1` and `2` denote long personality formats where an embedded length byte specifies the number of additional 32-bit words that follow.

Estell notes with visible frustration that because the ABI capped the maximum number of instructions at seven, the explicit length byte is redundant: > “This can actually only be one to two, because they found out that you only needed seven instructions maximum, which is actually kind of sad because I look at this and I think to myself: we don't need to have the length there. If we had removed the length, then it would have moved up one, and we wouldn't need the padding we need of the finished bytes here that could have saved us four additional bytes, but you know, we can't have nice things all the time.”

- Demonstrating with the function `baz`: The binary search finds `baz`'s index entry, inspects bit 31 (`1`), sees personality format `0`, and reads the instruction to pop registers `R4` through `LR`. Executing this moves `bar`'s return address into the link register, restores `bar`'s preserved `R4` value, and increments the stack pointer. - Moving up the call chain to `bar`: The unwinder discovers that `bar` contains a local object requiring destruction, making it reentrant; inline metadata is insufficient, and execution must consult the detailed exception table.

---

[42:47](https://www.youtube.com/watch?v=bY2FlayomlE&t=2567s) Language Specific Data Area (LSDA) and LEB128 Encoding

When inline entries are insufficient, the index entry points via a relative offset (such as `-2304`) into the exception table (`.ARM.extab`). This table contains long personality data and language-specific data areas (LSDA). Estell explains that the LSDA concept was originally introduced by the Itanium C++ ABI to facilitate language interoperability: > “Language Specific Data Area is something that the Itanium ABI added in so that you could support, or we could support, throwing exceptions through Java and C and C++ and have them all cooperate with each other. Yeah, we have support for that—it's one of the reasons why the normal GCC exceptions is kind of slow. They have to handle Java and other languages, even though that might not be anything you might not be using Java in your code.”

A C++ program can call across a foreign function interface into Java; if Java throws an exception, the unwind runtime propagates the unwinding state through C++, into Java runtimes, and back to outer C++ frames seamlessly. GCC retained this flexible LSDA architecture to construct a unified exception model across architectures.

The LSDA structure contains several critical sub-sections: 1. **Personality Function Pointer**: A reference to the routine that knows how to interpret the tables below it (Estell notes he overrides this routine in his custom lightweight embedded runtime). 2. **Personality Data**: The unwinding bytecode stream. 3. **LSDA Header**: Specifies encoding formats and size offsets for the subsequent tables. 4. **Call Site Table**: Records program scopes relevant to exceptions, demarcating active `try` blocks and cleanup regions needing destructors. 5. **Action Table**: Chains and orders candidate `catch` handlers, preserving evaluation order. 6. **Type Table**: Contains unique references to types handled by catch blocks (for example, if `std::exception` is caught at five distinct locations across a function, only one entry exists in this table).

Estell highlights the **LEB128** (Little-Endian Base 128) variable-length encoding format used throughout the LSDA headers and tables. In LEB128, each byte dedicates its most significant bit as a continuation flag: a `1` indicates that further bytes follow, while a `0` indicates the terminal byte. Stripping these flags and concatenating the remaining 7-bit payloads yields the full integer. This guarantees small offsets consume only a single byte of flash memory, keeping metadata compact.

In embedded environments, the first byte of the LSDA header is almost universally `0xFF`, which signals that Dwarf debugging and landing-pad metadata are omitted, as these sections are stripped from production embedded binaries. The subsequent byte defines the type table encoding—typically program-counter-relative offsets rather than absolute 32-bit pointers—followed by an offset specifying the exact distance from the header to the start of the type table located at the end of the LSDA.

[46:23](https://www.youtube.com/watch?v=bY2FlayomlE&t=2783s) Decoding the Call Site Table and Triggering Destructors

Decoding the call site table in the Language Specific Data Area (LSDA) begins right after the header. Depending on the encoding, call site offsets and lengths can be encoded as ULEB128 or fixed types such as `uint8_t` or `int16_t`. Following the header offset comes the total byte length of the call site table. Each call site record provides four essential fields: `start`, `length`, `landing_pad`, and `action_record`.

```mermaid flowchart TD accTitle: Call Site Record Evaluation accDescr: Flowchart illustrating how a call site record is parsed to trigger destructors or proceed with unwinding. subgraph RangeCheck["PC Range Check"] A["Read Call Site Record<br>(start, length, landing pad, action)"] --> B{"Is PC in<br>[start, start + length)?"} B -- "No" --> C["Check Next Record"] B -- "Yes" --> D{"Landing Pad == 0?"} end subgraph Dispatch["Action Execution"] D -- "Yes" --> E["No cleanup here:<br>Unwind frame"] D -- "No" --> F{"Action == 0?"} F -- "Yes" --> G["Unconditional cleanup:<br>Call Destructors"] F -- "No" --> H["Inspect Action Table<br>for Catch Handler"] end ```

The `start` field indicates an offset from the beginning of the function's assembly code, and `length` specifies the valid range from that offset. If the current program counter (PC) falls within `[start, start + length)`, the CPU was executing within that protected scope when the exception occurred, mapping execution directly to that record's landing pad and action record.

The landing pad defines the code address to resume execution within the function, while the action record determines what must occur: - An action value of `0` denotes unconditional unwinding (calling destructors for objects going out of scope), or indicates that no type table exists. - A non-zero action value provides an offset into the action table to match catch handlers. - A landing pad of `0` indicates that the current scope contains no cleanup code, meaning the runtime should bypass landing pads and unwind through the function frame immediately.

In the example walk-through, the runtime evaluates `bar`, where the active PC sits at `bar + 14`. With a record showing `start = 0`, `length = 26`, and `landing_pad = 30`, the PC falls comfortably within the 0 to 26 byte range. To execute the destructor cleanup, the unwinder calculates the resume target as the function's base address plus the landing pad offset of 30 bytes, setting the least significant bit (LSB) to 1 to satisfy the ARM Thumb execution mode requirements:

```cpp // Pseudocode for landing pad jump setup auto destination = (function_start + landing_pad) | 1; cpu_context.pc = destination; restore_cpu_state(cpu_context); ```

As Khalil Estell explains regarding register setup and cleanup: > “We're just setting up the location of that object from the stack in the first parameter so we can call this destructor.”

The generated cleanup landing pad loads the object address into `r0` (via `add r0, sp, #4`), invokes the object's destructor, and subsequently calls `__cxa_end_cleanup` to retrieve the active exception object, restore the frame state, and continue propagating the in-flight exception.

- Destructors are allowed to utilize stack memory, which means any stack frame contents below the executing destructor's frame are considered undefined and subject to being overwritten. - Compilers can emit multiple landing pads within a single function depending on execution progress; an exception thrown early might only clean up a single constructed object, whereas an exception thrown later will unwind multiple objects in reverse order.

---

[50:23](https://www.youtube.com/watch?v=bY2FlayomlE&t=3023s) Restoring the Caller Frame and Matching Catch Blocks

Once the destructors for the inner function (`bar`) complete, unwinding transitions to the caller (`foo`). The runtime reads the unwind bytecodes within the LSDA to adjust the stack frame. It encounters Personality Index 0, adds 12 bytes back to the stack pointer to deallocate the space previously occupied by local objects, and pops saved registers `r4`, `r5`, and the link register (`lr`) off the stack. With the stack adjusted and the saved program counter pointing into `foo`, the runtime consults the LSDA's type table and action table to locate an appropriate catch handler.

The type table consists of a unique list of RTTI pointers for catchable types, indexed in reverse: - A `nullptr` entry in the type table represents a catch-all block (`catch (...)`). - Types are matched using pointer equality between the thrown exception's `type_info` and the type table entry. - Complex type inheritance checks are only invoked when polymorphic class hierarchies are caught across types.

Khalil Estell dispels a widespread myth regarding RTTI matching overhead: > “I think people think that you have to do like string comparisons to do like catch comparisons no you just got to check the pointers.”

The action table encodes the sequence of catch handlers associated with each try block via a linked list format: 1. **Filter Number**: A positive integer that serves as a negative offset when indexing backward from the end of the type table (e.g., filter `1` corresponds to `type_table[-1]`). A filter of `0` denotes a catch-all. 2. **Next Record Offset**: A signed offset pointing to the next chained action record. A value of `0` signals the end of the action chain for that try block.

```mermaid flowchart LR accTitle: Action Table Filter and Type Resolution accDescr: Diagram showing how an action record references a filter number to look up a type info pointer from the reverse type table. subgraph ActionRecord["Action Table Entry"] F["Filter Number = 1"] N["Next Record Offset = 0"] end subgraph TypeTable["Type Table (Reverse Indexed)"] direction TB T0["End of Type Table Base"] T1["type_table[-1]: target type_info*"] T2["type_table[-2]: alternate type_info*"] T0 --- T1 T1 --- T2 end F -->|"indexes backward"| T1 ```

If the action chain is exhausted without a type match, the runtime unwinds past `foo` to the next caller. When a match is found: - The active exception pointer is placed into `r0`. - The matching filter number is loaded into `r1`. - The PC is updated to the function start plus the landing pad address (with LSB set to 1). - `restore_cpu_state` transfers control to the catch landing pad.

---

[53:53](https://www.youtube.com/watch?v=bY2FlayomlE&t=3233s) Catch Blocks as Switch Statements and Handler Teardown

Under the Itanium C++ ABI and its ARM counterpart, catch blocks are emitted internally as switch-case constructs: > “I don't know if anyone knows this but catch blocks are switch case statements at least in Itanium they're just switch case statements so the filter number is your switch value to which case that you are.”

Upon entering the landing pad, the code evaluates `r1` against the expected filter number: - If `r1 == 1`, execution branches into the body of the catch block. - It invokes `__cxa_begin_catch`, passing the exception object in `r0`. This helper marks the exception as handled and returns a pointer to the thrown payload. - Once the user's catch handler logic finishes, execution enters `__cxa_end_catch`, which runs the thrown object's destructor and frees its allocated storage. - If the filter number does not match (an unexpected fallthrough state), the code branches to an unwind continuation label to resume rethrowing up the call tree.

---

[55:00](https://www.youtube.com/watch?v=bY2FlayomlE&t=3300s) Modeling Real-World Space Costs of Exception Handling

With the complete unwind mechanism mapped out, Estell transitions to analyzing the exact space overhead that exception handling tables impose on firmware binaries. To produce a realistic baseline comparison, the analysis models its assumptions around real-world code compiled strictly for size (`-Os` / `-Oz`):

1. **Size-Optimized Compilation**: Unwind benchmarks only reflect real-world firmware if all components are compiled with size optimization flags enabled. 2. **Upper Bound on Function Size**: It is assumed that practical functions do not exceed 16 KB of machine instructions. To substantiate this claim, Estell analyzed a full debug build of the Firefox codebase: out of hundreds of thousands of functions, only 253 functions exceeded 16 KB, while over 325,000 fell below that threshold. 3. **Upper Bound on Scopes**: The number of catch blocks and automatic objects requiring destruction within any single function is capped at 63. 4. **ULEB128 Encoding**: All variable-length quantities are assumed to use standard ULEB128 compression, minimizing metadata byte overhead across LSDA sections.

[56:03](https://www.youtube.com/watch?v=bY2FlayomlE&t=3363s) Calculating the Concrete Byte Costs of Exception Tables

To demonstrate precisely how much metadata exceptions demand, Khalil Estell breaks down the exact byte footprint mandated by the ARM Exception Handling ABI (EHABI) and generated assembly:

* **Exception Index Table (`.ARM.exidx`):** Exactly 8 bytes per entry as specified by the ABI. * **Personality Data:** Exactly 4 bytes per entry according to the ABI. * **Inline Unwind Data:** A maximum of 12 bytes. * **Catch Blocks:** Because catch blocks function as a `switch`/`case` construct, each catch site requires a comparison instruction (2 bytes), a conditional branch-if-not-equal instruction (2 bytes), a branch-and-link call to `__cxa_begin_catch` (4 bytes), and another 4-byte call to `__cxa_end_catch`. This sums to approximately 12 bytes of assembly per catch block. * **Cleanup Regions (Destructors):** Calculated as $6D + 4$ bytes (where $D$ represents the number of objects needing destruction). Each destructor requires 2 bytes to set up stack or input parameters plus a 4-byte branch-and-link call to the destructor itself, along with an additional 4 bytes for the terminal call to `__cxa_end_cleanup`. * **Action Table:** Sized at $2 \times A$ bytes, where $A$ is the number of catch blocks in the function. Because a signed LEB128 (SLEB128) value spanning $-64$ to $63$ fits within a single byte, under Estell's working constraints the filter number never exceeds this range. Consequently, each filter number takes only 1 byte, and the subsequent record offset never needs to exceed $-3$ (consistently represented as `-3, -3, -3`). * **Type Table:** Consumes 4 bytes per pointer multiplied by $T$, the number of unique exception types referenced in the function. * **Call Site Table:** Ranges between $4 \times C$ and $7 \times C$, where $C$ is the number of call sites. Under the applied architectural constraint that no function exceeds 16 KB ($2^{14}$ bytes, which can be encoded in two unsigned LEB128 bytes), the call site start offset, length, and landing pad start can never exceed 16 KB. The action record index inside this table is constrained to a single unsigned LEB128 byte because the action table tops out at 128 bytes: “Got lucky there; could have been off by one.”

Estell purposefully bypasses an in-depth derivation of the exception table header math on stage: “I'm going to skip this section because I hate every single time I repractice this one; I hate explaining it because it has a lot of math included in it.” He notes that he is preparing a comprehensive formal paper detailing how the header is mathematically proven to be bounded between 5 and 7 bytes under these embedded constraints.

```mermaid flowchart TD accTitle: Exception Handling Metadata Byte Breakdown accDescr: Diagram illustrating the byte cost of ARM exception tables and catch block code.

subgraph StaticTables["Fixed Metadata Tables"] Idx["Exception Index (.ARM.exidx)<br>8 Bytes"] Pers["Personality Routine Pointer<br>4 Bytes"] Hdr["LSDA Header<br>5 to 7 Bytes"] TypeTbl["Type Table<br>4 Bytes × Unique Types (T)"] end

subgraph DynamicTables["Variable Metadata Tables"] CallSite["Call Site Table<br>4 to 7 Bytes × Call Sites (C)"] ActionTbl["Action Table<br>2 Bytes × Catch Blocks (A)"] end

subgraph CodeCosts["Assembly Generation Costs"] CatchCode["Catch Block Routine<br>12 Bytes each (cmp + bne + begin + end)"] CleanCode["Cleanup Destructors<br>(6 Bytes × D) + 4 Bytes (__cxa_end_cleanup)"] end ```

Synthesizing these mathematical models produces realistic real-world footprints: - **Trivially Unwindable Function:** A function with no destructors, no `try`/`catch` blocks, and no extra register handling adds an overhead of only 8 bytes in the index table over its native function code. - **Function with a Single Destructor Scope:** Requires approximately 34 bytes of overhead: 8 bytes for the `.ARM.exidx` entry, 5 bytes for the header, 4 bytes for the personality routine pointer, 4 bytes for the call table, up to 3 padding bytes for word alignment, and 10 bytes for the landing pad code. - **Function with a `try`/`catch` Block:** Reaches a minimum overhead of 47 bytes once the landing pad, action records, type pointers, and runtime calls are tallied. “So yeah, try/catch is a little, little expensive.”

---

[01:00:22](https://www.youtube.com/watch?v=bY2FlayomlE&t=3622s) The Hidden Code Bloat of Return-Type Error Handling

Estell contrasts the fixed metadata overhead of zero-cost exceptions with the code-bloat characteristics of return-type error handling (such as `std::expected`, error codes, and boolean flags). Starting with an ideal baseline function (`none`) that invokes three fallible operations without checking errors (relying purely on standard exception unwinding), the generated machine code requires only 8 instructions.

When the same function is rewritten to use `std::expected<T, E>`, the disassembly swells substantially: - Even when annotating the error paths with `[[unlikely]]`, the generated machine code does not shrink: “I even put unlikely here hoping that it would do something; I removed it, didn't change a darn thing.” - For every checked call site, the assembly must invoke the function (`bl`), load the returned status word from the stack pointer (`ldr`), and execute a compare-and-branch (`cbz` or `cmp`/`bne`) to jump to a local error return path. - This creates an inescapable tax of at least 6 bytes per call site: 4 bytes for the load instruction, and 2 bytes for the branch instruction.

Replacing `std::expected` with a plain `bool` success flag slightly reduces the burden to roughly 4 bytes per call site. While certain 2-byte branch instructions appear in the disassembly, the compiler cannot use them universally across larger scopes due to branch offset range limits.

Examining Rust's canonical `Result<T, E>` types reveals an identical phenomenon. Despite Rust's reputation for highly optimized pattern matching, its generated assembly is essentially equivalent in byte cost to C++'s `std::expected`. Interestingly, altering the Rust payload type from `u32` to `u64` triggers an optimization that collapses the check down to 4 bytes per site (identical to returning a raw boolean). Regardless of language syntax, returning errors as values incurs a distributed penalty of 4 to 6 bytes for every single fallible function invocation.

---

[01:02:39](https://www.youtube.com/watch?v=bY2FlayomlE&t=3759s) Centralized vs. Distributed Error Handling

Estell categorizes error architectures into two fundamental models:

* **Distributed Error Handling:** Error management is scattered across the entire codebase via `if`/`else` control flow, return codes, `std::expected`, out-parameters, or manual propagation. Every intermediate function that invokes a fallible callee must actively spend instructions to inspect the return state and either handle the failure or manually forward it upward. * **Centralized Error Handling:** Propagation is delegated to an external, out-of-band engine (such as the C++ exception unwinder). Intermediate code on the call stack remains completely oblivious to error-routing mechanics and focuses entirely on the "happy path." Estell analogizes this to a municipal infrastructure: “I like to think about this kind of in the same vein as the USPS or like having postal systems: rather than having everyone else manage delivering their own stuff, we have a system for that.”

Estell puts forward his core thesis: “Centralized error handling schemes have an advantage in terms of code size compared to distributed error handling.”

This structural advantage stems from simple arithmetic: - **Branches are not free:** Every distributed check consumes 4 to 6 bytes of flash memory every single time an operation is invoked. If a function calls 5 fallible functions sequentially, manual propagation consumes 20 to 30 bytes of instructions. Under an exception model, that same trivially unwindable function incurs only 8 bytes of table overhead in total. - **The Catch Break-Even Tradeoff:** While a `try`/`catch` block introduces a heavier baseline tax (at least 47 bytes, roughly equivalent to checking 11 to 12 fallible return types), catching is intended to be rare. - **Embedded code is overwhelmingly trivially unwindable:** “Most functions are trivially unwindable. Think about this for a sec: at least for the code that I've written and the code I've seen around, I have found that there's far more code that actually propagates errors versus handling them.” In typical firmware environments, most application logic merely passes errors up the stack, and very few stack-allocated objects require non-trivial destructors since dynamic heap allocations are routinely avoided.

[01:05:12](https://www.youtube.com/watch?v=bY2FlayomlE&t=3912s) Audacity Case Study and Modeling Firmware Binary Costs

Khalil Estell discusses how resource cleanup, like mutex locks, tends to be used sparingly in firmware code bases. Seeking real-world data to evaluate how frequently exceptions and cleanups appear in practice, he recalls reaching out to Jason Turner a couple of months prior for realistic open-source benchmarks. Turner suggested analyzing Audacity. Audacity contains 1,554 C++ files, yet only 39 of those files contain `catch` blocks, totaling just 60 to 69 `catch` blocks across the entire code base. Furthermore, out of approximately 17,000 functions, only 50 contain an explicit `try`/`catch` block. The total memory overhead required for exception handling across the entire Audacity application amounts to roughly 15 kilobytes.

To ensure his analysis does not rely on overly optimistic numbers, Estell constructs an intentionally extreme theoretical firmware scenario. Rather than assuming the 0.28% catch-block frequency seen in real code, he models a worst-case scenario where 5% of functions contain `try`/`catch` blocks, 25% allocate objects requiring cleanup, 55% are trivial unwindable functions, and 10% are leaf functions. Leaf functions require zero bytes of metadata because they never participate in stack unwinding. Assigning generous average metadata sizes—64 bytes for `try`/`catch` blocks, 46 bytes for cleanup functions, 8 bytes for trivial functions, and an 8,000-byte runtime overhead for `libunwind`—the total exception-handling footprint reaches 27,100 bytes across 900 functions.

```mermaid flowchart TD accTitle: Exception Handling Overhead Breakdown accDescr: Visual breakdown of memory cost in bytes for an extreme 900-function model. subgraph FunctionTypes["Function Distribution (900 Total Functions)"] TC["5% Try/Catch (64 B avg)"] CL["25% Cleanup (46 B avg)"] TR["55% Trivial Unwind (8 B avg)"] LF["10% Leaf (0 B)"] end subgraph AggregatedCost["Total Space Cost"] UW["libunwind Runtime: 8,000 B"] Total["Total Footprint: 27,100 Bytes"] end FunctionTypes --> AggregatedCost ```

- Audacity metrics: 1,554 C++ files, 39 files with `catch` blocks, ~60–69 catch blocks total, 50 `try`/`catch` instances across ~17,000 functions, ~15 KB total exception metadata cost. - Extreme model parameters: 5% `try`/`catch` (64 bytes average), 25% cleanup (46 bytes average), 55% trivial functions (8 bytes average), 10% leaf functions (0 bytes). - Base runtime overhead: 8,000 bytes allocated for `libunwind`. - Total modeled exception memory consumption: 27,100 bytes.

[01:07:19](https://www.youtube.com/watch?v=bY2FlayomlE&t=4039s) Comparing Metadata Size to Error-Code Check Overheads

Estell evaluates what happens when replacing exceptions with return-type error handling, such as `std::expected` or result types. If checking a return code consumes a minimum of 4 bytes per call site, a budget of 27,100 bytes permits 6,775 total checks. When distributed across 900 functions, that equates to roughly 7.5 fallible calls per function before manual error checking consumes more binary flash than the entire exception infrastructure. Crucially, that 4-byte estimate is an absolute best-case lower bound that excludes the function epilogue and cleanup return paths.

As the error-checking mechanism becomes more realistic, the breakeven threshold drops sharply. Switching to `std::expected` raises the cost to approximately 6 bytes per check, reducing the budget to five checks per function. Factoring in an extra 2 bytes for the function epilogue needed to propagate an error brings the threshold below four checks per function call. Estell emphasizes that having to worry about calling more than three or four functions without blowing a size budget places unreasonable constraints on everyday software development.

- Best case (4 bytes per check): 6,775 total checks allowed across the binary, or ~7.5 fallible calls per function. - Mid case (`std::expected` at 6 bytes per check): ~5 checks per function. - Realistic case (6 bytes check + 2 bytes epilogue = 8 bytes): under 4 checks per function call before exceptions become smaller.

[01:08:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=4108s) Exceptions as Code Compression

Synthesizing these findings, Estell details the exact architectural reasons why C++ exceptions yield smaller firmware binaries. First, exceptions drastically reduce codegen at fallible call sites because no branch instructions or local validation logic are inserted into the hot path. Second, unwind metadata scales strictly with the total number of function definitions, whereas error-return checks scale with the total number of function *calls*, which is substantially higher in any real program. Third, exceptions eliminate repetitive function epilogues and manual error-return paths. Furthermore, decades of work in both the Itanium ABI and the Arm Exception Handling ABI have tuned the instruction encoding to be as dense as possible. Finally, the runtime unwinder cost is paid only once—8 KB in stock implementations, or around 4.5 KB to 4.7 KB with Estell's custom unwinder.

Estell argues that existing ABIs have not even reached their most compact potential. Current formats still emit unneeded DWARF sections, call-site encodings, and type table encodings that could be eliminated by defining custom personality routines. By replacing verbose runtime routines like `__cxa_end_catch` (invoked via a 4-byte `BL` call) with a simple 2-byte unconditional `B` branch instruction, two bytes could be reclaimed from every catch block. Reflecting on this disassembly analysis, he delivers his central takeaway: “C++ exceptions are code compression.” Rather than being an architectural liability, the exception table and its metadata serve as an effective binary compression scheme that transforms bloat into minimal code and a handful of metadata bytes.

> “After so much time looking at the disassembly, looking at the metadata, and working with these data structures, I came to one conclusion, and that is C++ exceptions are code compression.” — Khalil Estell

[01:11:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=4265s) Unwinder Optimization and Generational Stagnation

Anticipating objections from developers who prioritize execution speed over code footprint, Estell previews his subsequent research on optimizing stack unwinding speed. He notes that while his earlier ACCU presentation demonstrated a 76% reduction in exception handling time, ongoing development pushed that reduction to 88%, with targets of 92% to 95%. Investigating GCC's `libunwind` source code reveals that its Arm implementation has remained virtually untouched for decades: approximately eight people in total have contributed to it, the most recent minor updates date back roughly 15 years, and the core implementation was authored 25 years ago.

Because the standard implementation has languished without modern optimization, stock GCC exhibits severe execution overhead compared to manual unwinding with `std::expected`. Across a call depth of 6 frames, GCC stack unwinding is roughly 27 times slower than returning an expected type, whereas Estell's optimized unwinder reduces that penalty to 4.77 times. Over an extreme unwinding depth of 96 frames, stock GCC runs 21 times slower than explicit return codes, while his optimized implementation is only 2.56 times slower than standard function returns.

| Unwind Depth | GCC `libunwind` vs `std::expected` | Estell Custom Unwinder vs `std::expected` | |:--- |:--- |:--- | | **6 Frames** | ~27× slower | 4.77× slower | | **96 Frames** | ~21× slower | 2.56× slower |

[01:12:25](https://www.youtube.com/watch?v=bY2FlayomlE&t=4345s) The Fundamental Flaw of Exceptions and Binary Inspection

Estell pivots to address the primary downside that drives embedded engineers away from exceptions: invisibility. He bluntly concedes, “exceptions effing suck,” demonstrating that looking at a function signature gives a developer no indication whatsoever of what types it might throw. Developers are forced to rely on out-of-date documentation or spend days emailing third-party vendors for clarification on vendor binary blobs. Even if a vendor confirms that a function throws no unexpected exceptions in version 5.1.0, upgrading to version 5.1.1 or 5.2.0 leaves the developer completely blind as to whether that contract still holds.

This lack of visibility and predictable behavior is the root cause of why developers distrust exceptions and gravitate toward local error handling like result types. However, Estell notes that this problem can be resolved through static inspection tools. While inspecting a binary to identify all `bool` flags or `std::expected` error returns in disassembly is extraordinarily difficult, tracking exceptions is straightforward: throwing an exception requires an unmistakable, explicit call to `__cxa_throw`. This realization serves as the foundation for his static analysis tooling, the "Exceptions Insights Tool."

> “Because of this, it's what get makes people concerned about their code and how how it will behave is because of this that people are trying to push away from exception so much that's why we want to handle errors locally.” — Khalil Estell

[01:14:20](https://www.youtube.com/watch?v=bY2FlayomlE&t=4460s) How the Exception Insights Tool Works

Khalil details the architecture and operational flow of his planned static analysis tool, dubbed the Exception Insights tool. He emphasizes that the tool operates on the final compiled binary rather than source code, bluntly explaining the reasoning: “your source code doesn't know jack—give me your binary thing you're going to actually execute.” By parsing the `.text` section of the binary, the tool locates every single branch and link instruction to `__cxa_throw` and `__cxa_allocate_exception`. Because calls to `__cxa_allocate_exception` reveal allocation arguments, the tool can deduce the exact size and type of every thrown object throughout the application, alongside their precise throw sites.

```mermaid flowchart TD accTitle: Binary Exception Analysis Flow accDescr: Pipeline showing how the tool analyzes an ELF binary to trace thrown exceptions and verify try-catch handlers.

subgraph Parsing["Binary Parsing"] A["ELF Binary Code (.text)"] --> B["Identify branch-and-link to __cxa_allocate & __cxa_throw"] B --> C["Extract throw object types, sizes, and locations"] end

subgraph Mapping["Index & Handler Mapping"] D["Exception Index (.ARM.exidx /.ARM.extab)"] --> E["Locate all exception-participating functions"] E --> F["Map try/catch blocks and catch handlers"] end

subgraph Simulation["Call Graph Simulation"] C --> G["Simulate Exception Propagation"] F --> G G --> H{"Outcome"} H --> I["Caught by Handler"] H --> J["Hits noexcept Function"] H --> K["Escapes to main (Uncaught)"] end ```

The tool then scans the exception index tables (`.ARM.exidx` / `.ARM.extab`) to discover which functions participate in exception handling and where every `try`/`catch` block resides. With this data coupled to a call graph—either supplied by the user or generated automatically—the tool simulates the raising and unwinding of every exception through every execution path. This reveals which exceptions are caught, which ones hit `noexcept` functions, and which ones escape uncaught all the way to `main`. Khalil stresses that this is not an impossible engineering feat and announces that he has dedicated time to building it, with students present at the conference assisting on the project.

[01:15:26](https://www.youtube.com/watch?v=bY2FlayomlE&t=4526s) Acknowledgments and Project Call for Support

Before opening the floor to questions, Khalil pauses to deliver heartfelt personal and professional acknowledgments. He thanks his mother, who traveled from California to see him speak, drawing applause from the room, as well as his fiancée, Malia Laor, a fellow C++ developer who contributed to the slides and is attending the conference. He highlights Herb Sutter's work on error handling and alternative exception designs as the primary catalyst that ignited his interest in the subject. Furthermore, he points to Bjarne Stroustrup's paper *C++ Exceptions and Alternatives*, quoting Bjarne's observation that “there's been little serious research on the topics of performance of exceptions and reliability of the resulting code in C++,” noting that this challenge prompted him to pursue that research himself.

Khalil shares his contact information and project links, clarifying that he is currently independent: “I am not employed; I am working on this on my own accord—this is my own research.” He invites sponsorships via GitHub and direct funding outreach from grant-awarding organizations like the National Science Foundation (NSF) or the Department of Energy at `estell.exceptions@gmail.com`. Additionally, he invites companies using or struggling with exceptions to supply real-world telemetry, requirements, and use cases to help shape community tools. Emphasizing his open-source commitment, he declares: “I believe that the Exception Insight tool and the other work that I'm doing will help promote C++ and keep it alive going into the future.”

[01:17:44](https://www.youtube.com/watch?v=bY2FlayomlE&t=4664s) Q&A: Buffer Upper Bounds, Safety Certifications, and Monadic `std::expected`

The question-and-answer period opens with an audience member addressing the static emergency allocation buffer for `__cxa_allocate_exception` described earlier in the talk, inquiring whether buffer exhaustion could terminate embedded systems and whether alternatives exist. Khalil responds that the Exception Insights tool combined with deterministic allocation structures can calculate an exact upper bound: by knowing the allocation blocks required per thread and every throw site across the system, developers can allocate just enough static storage to ensure out-of-memory crashes never occur during exception handling.

Another attendee working in aerospace and robotics raises strict regulatory requirements: aerospace standards demand full on-target branch coverage tracing back to formal requirements, prompting concern over how hidden compiler-generated exception paths can be audited. Khalil clarifies that the Exception Insights tool aims to solve that exact issue: - **Binary-level inspection**: Analyzes compiled ELF images and closed-source vendor binary blobs directly, eliminating blind spots when source code is unavailable. - **Uncaught exception detection**: Emits stack traces for any unhandled exception path in CLI output. - **Intentional termination tracking**: Provides explicit configuration to allow safety-critical termination while catching and handling recoverable operational errors. - **Symbol mapping**: Maps offending throw sites back to debug symbols when available, identifying the precise originating functions.

A subsequent question asks about usability and code-size comparisons when using monadic operations on `std::expected` (such as `.and_then()` and `.or_else()`), and whether students found them helpful. Khalil remarks that his students “found it too confusing” and preferred traditional `if` statements, noting that heavy lambda usage does not provide code-size advantages over normal branches. When asked whether he plans to upstream his runtime improvements into LLVM Clang or GCC, he indicates a desire to target Clang first, followed by GCC. He adds that his custom exception runtime is packaged as a standard drop-in library (via Conan) that overrides GCC's default exception-handling symbols at link time without requiring code modifications or newer compiler toolchains. Finally, in response to a question regarding shared libraries, Khalil confirms that analyzing dynamic `.so` boundaries requires running the tool on the target machine with runtime ground truth, a feature that remains on their roadmap.

[01:22:57](https://www.youtube.com/watch?v=bY2FlayomlE&t=4977s) Audience Collaboration and Alternative System Call Layers

An audience member shares their own engineering effort to eliminate C runtime dependencies in favor of a pure C++ abstraction layer for system calls. The attendee notes that their momentum had stalled precisely because of the exhaustive unwinding and runtime research required, remarking, “The place where I'm kind of stopped right now is all the research that would be needed to do what you just did so thanks.” Khalil Estell welcomes the appreciation and immediately follows up by asking whether the attendee would find the open-source lightweight unwinding library he has been developing genuinely useful once completed. The attendee affirms that it would be potentially very helpful, prompting Estell to suggest that they connect after the talk to discuss the details and explore collaboration.

- The attendee's project aimed to bypass standard C runtime libraries and replace them with a pure C++ layer targeting system call interfaces. - Progress had been blocked by the deep technical overhead of reverse-engineering and implementing exception runtime unwinding tables. - Estell expressed interest in seeing whether his experimental unwinding implementation could serve as a direct drop-in solution for such embedded frameworks.

[01:23:25](https://www.youtube.com/watch?v=bY2FlayomlE&t=5005s) Architecture Support: x86, RISC-V, and Windows

The Q&A shifts to platform support when another attendee asks whether Estell has investigated porting the implementation to x86, x86-64, or Windows environments. Estell outlines his current priorities, confirming that his roadmap is laser-focused on embedded architectures—specifically ARM and RISC-V. He explains that expanding to x86 is not entirely out of the question, but it would require external backing: “For x86 that will require some support from the community for us to have the resources to work on that so just making that clear.”

When addressing Windows, Estell draws a clear distinction between the MSVC ABI and alternative toolchains. For MSVC, which uses its own distinct Structured Exception Handling (SEH) mechanism and runtime tables, Estell jokes that Microsoft would have to reach out directly to coordinate an effort: “As for Windows Microsoft's going to have to contact me and talk to me about that because if it's for MSVC that's their own thing.” He concedes that targeting Windows through Clang or MinGW using standard DWARF/Itanium unwinder models might be technically feasible, but he has not yet verified how it would behave in practice. With no further questions from the room, Estell wraps up the session to enthusiastic applause from the audience.

```mermaid flowchart TD accTitle: Architecture Support Roadmap accDescr: Diagram illustrating current architecture targets versus future conditional targets for the custom exception library.

subgraph PrimaryTargets["Primary Focus (Embedded)"] ARM["ARM (Cortex-M)"] RISCV["RISC-V"] end

subgraph CommunityTargets["Conditional Support"] X86["x86 / x86-64"] WIN_CLANG["Windows (Clang / MinGW)"] WIN_MSVC["Windows (MSVC)"] end

PrimaryTargets -->|"Requires Community Funding/Resources"| X86 PrimaryTargets -->|"Needs Further Investigation"| WIN_CLANG PrimaryTargets -->|"Requires Microsoft Direct Collaboration"| WIN_MSVC ```

- **ARM and RISC-V**: Main active targets designed for constrained bare-metal microcontrollers and embedded firmware. - **x86 / x86-64**: Dependent on community interest, pull requests, and additional resource allocation. - **Windows / MSVC**: Highly divergent runtime model; supporting MSVC would necessitate proprietary ABI adaptations, though Clang-based Windows builds might have a simpler path forward.

## Notable quotes > [01:08:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=4108s) After so much time looking at the disassembly, looking at the metadata, and working with these data structures, I came to one conclusion, and that is C++ exceptions are code compression. — Khalil Estell

> [01:02:39](https://www.youtube.com/watch?v=bY2FlayomlE&t=3759s) Centralized error handling schemes have an advantage in terms of code size compared to distributed error handling. — Khalil Estell

> [27:22](https://www.youtube.com/watch?v=bY2FlayomlE&t=1642s) From my experience, just to be real, I see more error propagation code than I see error handling code. — Khalil Estell

## Mentioned ARM, ARM Exception Handling ABI (EHABI), Audacity, Ben Craig, Bjarne Stroustrup, Clang, Conan, CppCon, Department of Energy, DigiKey, Embedded Template Library (ETL), Exception Insights tool, Firefox, GCC, Google, Google Pixel Buds, Google Pixel Watch, Herb Sutter, ISO C++ Committee, Itanium C++ ABI, Jason Turner, Java, Khalil Estell, LLVM Clang, Malia Laor, MinGW, MSVC, National Science Foundation (NSF), Patrice Roy, RISC-V, Rust, SG14, San Jose State University, libunwind

## Video description https://cppcon.org CppCon 2024 Early Access: https://cppcon.org/early-access Access All 2024 Session Videos Ahead of Their Official Release To YouTube. At least 30 days exclusive access through the Early Access system. Videos will be released to the CppCon channel on a schedule of one video per business day, with initial releases starting in November. ---

C++ Exceptions for Smaller Firmware - Khalil Estell - CppCon 2024 ---

For years, developers have overlooked a powerful tool for reducing binary size: C++ exceptions. Join me on a deep dive into the world of exceptions and discover how they can be harnessed to create more space efficient firmware. We’ll explore the requirements and best practices of embedded development, and show what is required to use exceptions in that environment. By the end of this talk, you’ll have a thorough understanding of how exceptions are handled, what their space costs are, and how exceptions compare to functional errors as values. ---

Slides: https://github.com/CppCon/CppCon2024/blob/main/Presentations/Cpp_Exceptions_For_Smaller_Firmware.pdf

Work at Hudson River Trading (HRT): https://tinyurl.com/safxfctf ---

Khalil Estell

Khalil is a ISO C++ Committee Member and has extensive experience writing production firmware. ---

CppCon is the annual, week-long, face-to-face gathering for the entire C++ community. The conference is organized by the C++ community for the community. You will enjoy inspirational talks and a friendly atmosphere designed to help attendees learn from each other, meet interesting people, and generally have a stimulating experience. Taking place this year in Aurora, Colorado, near the Denver airport, and including multiple diverse tracks, the conference will appeal to anyone from C++ novices to experts. Annual CppCon Conference - https://www.cppcon.org https://www.linkedin.com/company/cppcon https://x.com/cppcon https://www.facebook.com/CppConference https://www.reddit.com/r/cppcon/ https://mastodon.social/@CppCon ---

Videos Filmed & Edited by Bash Films: http://www.BashFilms.com YouTube Channel Managed by Digital Medium Ltd: https://events.digital-medium.co.uk ---

#exceptionhandling #firmware #firmwareupdate #cpp #cplusplus #cppcon #cppprogramming #cplusplusprogramming #softwaredevelopment #softwareengineering #coding #code #technology #programming

[Source](https://www.youtube.com/watch?v=bY2FlayomlE)

中文简报

C++异常优于错误返回码:Estell实现ARM固件体积缩减 基于表的C++异常在微控制器上起到了二进制代码压缩的作用,因为一旦固件项目超过550个可能出错的操作,剥离分散的错误检查分支所节省的空间就会超过展开元数据的开销。 Khalil Estell挑战了长期以来嵌入式系统中使用`-fno-exceptions`进行编译的教条,证明了C++异常可以作为一种有效的代码压缩形式。尽管微型程序会出现初始运行时开销,但包含大量错误检查样板代码(如`std::expected`或返回码)的现实固件扩展性很差,因为分散的检查分支会在每个调用点增加代码。通过深入剖析ARM异常处理ABI、`.ARM.exidx`、`.ARM.extab`,并通过自定义弱符号覆盖(如`__cxa_allocate_exception`)消除冗余体积,Estell揭示了在更大的代码库中异常的代码密度优于返回码。他还推出了一款ELF检查静态分析工具Exception Insights,以解决裸机环境中传统上与异常相关的安全性和可见性顾虑。 嵌入式C++教条通常因假定的二进制体积膨胀而禁用异常,但默认的GCC运行时膨胀(约150 KB)主要由冗长的终止处理程序引起,通过覆盖弱符号可将其缩减至约13 KB。 (17:22) 代码体积建模表明,基于表的异常具有代码压缩作用,在每个函数大约4到7.5个易出错调用时,即可与错误返回检查(如std::expected)达到持平点。 (67:19) 通过使用静态存储覆盖__cxa_allocate_exception并使用-fno-rtti进行编译,可以完全避免动态内存分配和RTTI膨胀,同时仅保留显式抛出类型的类型信息。 (19:05) 定制的ARM展开器将GCC libunwind运行时体积缩减至约4.5 KB,同时在6帧深度下将展开延迟从相对于std::expected的27倍开销降低到4.77倍。 (71:05)

Reading summary

Embedded systems researcher Khalil Estell argues that table-based C++ exceptions can produce smaller firmware binaries than return-type error handling such as std::expected, while achieving competitive execution speeds through lightweight runtime overrides. Debunking Embedded Dogma and Runtime Bloat (17:42) Firmware developers traditionally disable C++ exceptions via -fno-exceptions due to fears of dynamic allocation, RTTI overhead, and binary size inflation. When Estell first enabled exceptions on bare-metal ARM, the binary surged from 2.6 KB to roughly 150 KB. However, investigating the output revealed that this bloat stemmed largely from standard library diagnostics, specifically __gnu_cxx::__verbose_terminate_handler(), which pulled in string formatting and I/O utilities. By overriding this handler with an empty loop using weak symbols, binary size fell to approximately 13 KB. Estell addressed remaining runtime dependencies by replacing the default dynamic memory allocator. Because __cxa_allocate_exception is declared as a weak symbol in GCC, developers can substitute a fixed, bounded static buffer, eliminating heap requirements and malloc stubs. Furthermore, compiling with -fno-rtti does not break exception handling; compilers selectively retain type descriptors solely for thrown or caught types, which require only 2 to 3 pointers per descriptor rather than bloating the entire system. Centralized Exceptions Versus Distributed Return Checks (62:39) Estell classifies error handling into distributed and centralized architectures. Distributed systems rely on explicit branches, error codes, and types like std::expected. Each check inserts at least 4 to 6 bytes of machine instructions per call site—such as load and compare-and-branch instructions—plus an additional 2 bytes for error-return paths. In contrast, zero-cost exceptions centralize unwinding logic into separate out-of-band metadata tables, keeping happy-path machine code compact. Because return-type overhead scales with the total number of checked calls (Cost = A * N) while exception tables scale with the number of non-leaf function definitions plus a single libunwind runtime cost (Cost = B * M + E), exceptions achieve smaller sizes once a program exceeds a certain complexity. In an automated test isolating error propagation, exceptions produced smaller binaries beyond approximately 550 checks. Real-world application in an educational 64 KB MP3 player project shrank the firmware image from 56 KB down to the 40 KB range. ARM Unwinding Mechanics and Latency Optimization (37:28) Under the ARM Exception Handling ABI, stack unwinding relies on an index table (.ARM.exidx) and a language-specific data area (.ARM.extab). Because function entries are strictly ordered by address, the runtime locates frames via fast binary search rather than linear scans. For trivial functions, unwind bytecodes (such as stack adjustments and register pops) are compressed into single-byte instructions embedded directly into the 8-byte index entry, requiring as few as 3 instruction bytes. While stock GCC libunwind has remained largely untouched for 25 years, Estell developed an optimized unwinder that reduces runtime overhead. Across a call depth of 6 frames, stock GCC unwinding ran approximately 27 times slower than returning std::expected, whereas Estell's unwinder reduced this penalty to 4.77 times slower. At 96 frames, the penalty dropped to 2.56 times slower, achieving an overall execution time reduction of up to 88%. Resolving Visibility and Safety Concerns (72:25) Estell acknowledges that the primary drawback of exceptions in firmware is invisibility: function signatures do not reveal what types might be thrown, creating friction when integrating third-party libraries or meeting strict safety certifications. To address this, he introduced the Exception Insights tool, a static analysis pipeline operating directly on compiled ELF binaries. By scanning the .text segment for explicit calls to __cxa_throw and matching them against the .ARM.exidx and .ARM.extab tables, the tool reconstructs call trees and simulates unwinding paths. This allows developers to trace uncaught exceptions, calculate deterministic upper bounds for the static exception allocation buffer, and satisfy aerospace or robotics verification requirements without relying on source code annotations. Estell's research targets ARM and RISC-V architectures, with broader platform support such as x86 and MSVC Windows contingent on community backing. The model requires Size-Optimized Compilation (-Os or -Oz) to realize footprint gains, leaving open questions on how non-standard ABIs and mixed RTOS environments will adopt custom unwinders at scale.

精炼阅读摘要

Khalil Estell在CppCon 2024演讲中提出,传统嵌入式领域因担心体积膨胀而全面禁用C++异常的教条存在误区;只要通过弱符号覆盖消除冗余运行时依赖,基于表的集中式异常处理在规模化代码库中生成的固件二进制体积反而会显著小于分散检查的std::expected或返回值模式。 嵌入式教条与默认异常体积膨胀的根源 (17:42) 在缺乏操作系统的裸机微控制器环境中,闪存和RAM极其受限。行业普遍采用-fno-exceptions,主要担忧包括堆动态内存分配、二进制膨胀、RTTI元数据开销、异常展开表复杂度以及非确定性延迟。当Estell在GNU ARM工具链上直接开启异常时,二进制体积确实从2.6 KB暴增至约150 KB。然而,深入排查发现该膨胀并非异常机制本身造成,而是GCC默认将未捕获异常导向了__gnu_cxx::__verbose_terminate_handler(),该例程引入了string适配器、sprintf及字符串处理等重型I/O依赖。 针对这些问题,Estell利用GCC和Clang的弱符号(weak symbol)机制重写了默认实现:将verbose_terminate_handler替换为极简死循环后,二进制体积立降91%至约13 KB。针对动态内存分配的顾虑,他通过重写弱符号__cxa_allocate_exception,以固定大小的预分配静态缓冲区替代sbrk和malloc,彻底消除了通用堆依赖。此外,在-fno-rtti编译模式下,编译器仅保留参与throw和catch类型的极小类型描述符(无继承对象仅需2个指针),打破了RTTI必然引发元数据暴涨的传言。 代码压缩模型:集中式与分布式错误处理的算术交叉 (27:22) Estell将错误处理划分为两种架构:一种是基于返回值、错误码或std::expected的“分布式错误处理”;另一种是借助展开运行时的“集中式错误处理”。在分布式方案中,每次调用潜在出错函数都必须包含加载、条件比较与分支跳转指令,即使添加[[unlikely]]修饰,每个调用点也会产生至少4至6字节的代码开销,且随着深层调用链中传播错误的函数返回尾声进一步增加。相比之下,基于表的零成本异常在正常执行路径上不插入任何检查与跳转分支,其展开元数据严格随着“函数定义数量”增长,而非随着“函数调用次数”累加。 由此形成的数学成本模型显示:返回值处理的体积开销等于单次检查字节数乘以调用点总数,而异常开销等于展开引擎的固定体积加上每个非叶函数的平均元数据。在实际代码生成测试中,错误传播检查达到约550次即出现体积交叉点。在一项针对64 KB闪存微控制器的学生MP3播放器项目中,从基于宏和返回值的实现迁移至C++异常后,固件体积由56 KB骤降至40多KB。在极端的理论固件场景评估中,若分布式检查单次产生8字节开销,每个函数只需超过3.5次可失败调用,其代码膨胀就会彻底超过包括8 KB运行时在内的整套异常系统。 ARM展开机制解密与微型化元数据 (37:28) 为消除对异常底层机制的疑虑,Estell深入解析了ARM异常处理ABI(EHABI)的技术细节。在ARM架构下,__exidx展开索引表严格按照代码段内存地址升序排列,每个条目仅占用8字节。遇到异常时,运行时仅需对PC值执行快速二分查找,完全避免了线性遍历或哈希表。对于无析构且不捕获异常的简单函数,展开字节码可直接内嵌在索引条目的低位(通常仅需1至3字节来模拟栈指针恢复和寄存器出栈),甚至标记为0x1(cantunwind)以指示立即终止。 对于需要执行资源清理或catch处理的函数,索引条目指向语言特定数据区(LSDA)。调用点表记录PC范围与对应的着陆垫(landing pad),catch块在底层被编译为紧凑的switch-case结构,类型匹配主要依赖类型信息指针的直接等值比较,而非耗时的字符串比对。结合ULEB128压缩编码,ARM ABI下每个函数的元数据和着陆垫指令被压缩在数十字节以内。Estell指出,异常元数据本质上是对调用点清理逻辑的高度压缩表达,当前ABI甚至仍有进一步精简冗余DWARF段与指令的潜力。 展开延迟优化与二进制静态分析工具 (71:05) 针对执行速度与确定性的质疑,Estell指出GCC现存的ARM libunwind代码已停滞维护十余年。在深度为6层的调用栈展开中,原版GCC展开速度比std::expected慢约27倍,而Estell通过重写定制化嵌入式展开库,将其运行开销压缩到仅慢4.77倍;在96层展开时更将差距缩小至2.56倍,且展开引擎体积缩减至4.5 KB至4.7 KB。 面对异常缺乏静态可见性、函数签名无法直观反映抛出类型的行业痛点,Estell开发了名为“Exception Insights Tool”的静态分析工具。该工具直接解析最终编译生成的ELF二进制文件而非源码,通过扫描.text段对__cxa_throw和__cxa_allocate_exception的调用,精确定位所有抛出点、对象类型与分配大小,并结合.ARM.exidx映射整个调用图。该工具能够自动验证捕获处理程序覆盖率、追踪未捕获异常的堆栈路径,并在编译期计算出确定性的异常内存上限,为高可靠及航空级系统的安全覆盖审计提供支撑。 Estell论证了经过裁剪的C++表驱动异常在代码规模达到一定阈值时具备比Result类型更优的体积压缩特性,但该方案依赖于对工具链底层运行时的深度重写与静态分析验证。该研究目前聚焦于ARM与RISC-V架构,对于x86环境及Windows MSVC专有的SEH机制尚未进行适配与验证。

Detailed notes — zh

## Background 本视频由 CppCon 于 2024 年 12 月 6 日在 YouTube 平台发布,主讲人为 ISO C++ 委员会成员、拥有丰富量产固件开发经验的 Khalil Estell。视频简介指出,长期以来开发者忽视了 C++ 异常在缩减二进制体积上的潜力,本场演讲深入探讨异常在嵌入式环境中的实际运作原理、空间开销,并将其与基于值的函数式错误处理(如 std::expected/Result)进行硬核对比。 ## TL;DR Khalil Estell 通过亲身教学与固件工程实践,挑战了嵌入式领域“必须禁用 C++ 异常”的传统教条。他揭示了初次开启异常时二进制暴增至 150 KB 的根源在于默认的 verbose terminate handler,并通过弱符号重载将其骤降至 13 KB,同时证明了禁用 RTTI 并使用静态内存即可安全驾驭异常。通过详尽拆解 ARM/Itanium ABI 的展开表、LSDA 与机器码开销,他证明当工程中的错误检查超过约 550 次时,集中式的异常表在体积上远胜于处处埋藏分支判断与返回代码的 Result 模式。最后,他提出了将异常视为“代码压缩技术”的核心论断,并展示了用于静态分析全固件抛出路径与内存上界的 Exception Insight 工具。 ## Atmosphere & style 整场演讲充满极客式的探究热情与幽默自嘲,主讲人以其在大学教学中被学生“逼宫”反思的真实窘境开场,叙事生动而引人入胜。台风自信、语速紧凑且充满互动,在直面“异常就是一团糟”与 Rust 社区舆论压力等尖锐话题时坦率风趣,引发现场阵阵心领神会的笑声。随着幻灯片逐行剖析 ARM 汇编、ELF 符号和字节编码,讲座由浅入深演变为一场硬核的技术解剖,为听众带来层层颠覆技术刻板印象的震撼与快感。 ## Bulletin - [17:42](https://www.youtube.com/watch?v=bY2FlayomlE&t=1062s) 在裸机 ARM 固件中启用 C++ 异常初次会导致约 150 KB 的体积膨胀,但罪魁祸首是 GNU 默认的 verbose terminate handler 隐式拉取的字符串格式化工具链;通过弱符号重载一个 5 行的自定义终止函数,即可将固件底噪骤降 91% 至 13 KB。 - [27:22](https://www.youtube.com/watch?v=bY2FlayomlE&t=1642s) 基于受控实验与汇编测算,基于值的错误处理(如 std::expected 或 Result)随调用点线性增长,每处检查平均产生 4 至 16.5 字节的机器码,而异常机制元数据随函数定义数线性增长;当代码库错误检查超过约 550 次时,C++ 异常的二进制体积反而显著小于错误码方案。 - [19:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=1145s) 禁用 RTTI(-fno-rtti)并不会引发未定义行为或阻止异常使用,编译器仅会为被 throw 的类型保留极其紧凑的 RTTI 元数据(无继承仅需 2 个指针大小),且通过弱符号覆盖 __cxa_allocate_exception 可完全摆脱 malloc 堆分配。 - [01:11:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=4265s) 讲师研发了仅占约 4.5 KB 的定制版异常展开器,相比 GCC 25 年未实质重构的旧版 libunwind,将 6 层栈展开的耗时缩减了 88%,并配套构建了基于二进制分析的 Exception Insight 静态诊断工具。 ## Key takeaways - [01:25](https://www.youtube.com/watch?v=bY2FlayomlE&t=85s) 微控制器资源极度受限(绝大多数 Flash < 1 MB、RAM < 100 KB),业界传统上因堆分配依赖、体积膨胀、RTTI 成本及非确定性延迟而全面禁用 C++ 异常。 - [10:53](https://www.youtube.com/watch?v=bY2FlayomlE&t=653s) 将底层所有易出错硬件接口(如 I2C 总线)迁移到 std::expected 会引发“病毒式扩散”,迫使大量业务代码充斥防御性宏或 if 检查,导致可读性崩塌且学生倾向于强制忽略错误。 - [17:42](https://www.youtube.com/watch?v=bY2FlayomlE&t=1062s) 默认异常机制导致二进制膨胀到 150 KB 的主因是 __gnu_cxx::__verbose_terminate_handler 拉取了 sprintf 和 strlen 等流格式化库,利用弱符号覆盖为空死循环后固件立降至 13 KB。 - [19:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=1145s) 通过弱符号覆写 __cxa_allocate_exception,可以用嵌入式静态内存缓冲区彻底替换底层 malloc 堆分配,满足裸机系统零动态内存的要求。 - [19:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=1145s) 在开启 -fno-rtti 时使用异常不仅合法且高度轻量:编译器只为实际抛出的类型生成极小元数据,无继承类型仅占 2 个指针大小,单继承仅占 3 个指针大小。 - [23:04](https://www.youtube.com/watch?v=bY2FlayomlE&t=1384s) 在中大型实际工程(如 64 KB Flash 限制的 MP3 播放器)中,切换为 C++ 异常后固件总大小从 56 KB 缩减至 40 多 KB,打破了异常必然使代码膨胀的刻板印象。 - [24:49](https://www.youtube.com/watch?v=bY2FlayomlE&t=1489s) 代码体积的数学博弈在于:返回值错误处理的成本为 A × N(N 为调用点总数),而异常成本为 B × M + E(M 为非叶子函数数);真实工程中 N 远大于 M,使得异常在规模化后更具优势。 - [27:22](https://www.youtube.com/watch?v=bY2FlayomlE&t=1642s) 同构代码基准测试显示,在约 550 次错误检查处,Result 类型的累积体积开销便与精简异常运行时出现交叉并反超异常方案。 - [37:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=2248s) ARM 异常索引表(.ARM.exidx)每项固定 8 字节并按函数地址严格排序,运行时利用 PC 进行快速二分查找,且绝大多数函数的展开指令在 3 字节内即可直接内联在表项中。 - [50:23](https://www.youtube.com/watch?v=bY2FlayomlE&t=3023s) 基于 Itanium ABI 的 C++ 异常底层将 catch 编译为以 Filter Number 为条件的 switch-case 分支,并通过负向索引动作表与类型表实现极速类型匹配。 - [01:00:20](https://www.youtube.com/watch?v=bY2FlayomlE&t=3620s) 在汇编层面,std::expected 配合 [[unlikely]] 的每次调用仍需 6 字节(bl + ldr + cbz),即使原始 bool 也需至少 4 字节,而平凡可展开函数的异常元数据仅需 8 字节。 - [01:08:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=4108s) 集中式错误处理(异常)将 Happy Path 和 Sad Path 在机器码与内存布局上彻底分离,其实质是一种通过外部只读元数据表消除主干指令冗余的高效“代码压缩机制”。 - [01:11:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=4265s) 现存 GCC ARM libunwind 核心代码长达 25 年未进行实质性优化,讲师构建的定制嵌入式运行时(4.5 KB)将 6 层展开耗时从原本比 expected 慢 27 倍降低至仅慢 4.77 倍。 - [01:14:20](https://www.youtube.com/watch?v=bY2FlayomlE&t=4460s) 开发 Exception Insight 静态分析工具直接扫描二进制 ELF 文件的 __cxa_throw 调用与异常索引表,用以推演异常传播路径并精确定位未捕获异常与内存上限。 - [01:20:51](https://www.youtube.com/watch?v=bY2FlayomlE&t=4851s) 轻量级异常运行时作为独立替换库开发,通过弱符号覆盖可在无需重新编译编译器的情况下直接兼容现有工具链,首要重心支持 ARM 与 RISC-V 架构。 ## Detailed walkthrough ### [00:08](https://www.youtube.com/watch?v=bY2FlayomlE&t=8s) 讲师背景与嵌入式教学的初衷 Khalil Estell(网络常用昵称 Cy)在开场向大家介绍了自己:他于 2017 年毕业于圣何塞州立大学(San Jose State University),曾任职于 Google。在 Google 期间,他是初代 Google Pixel Watch 的“直接责任工程师(Directly Responsible Engineer)”,负责触觉反馈(haptics)、引导加载程序(bootloader)、扬声器和麦克风等模块;之后他转岗至 Google Pixel Buds 团队负责代码大小优化,以便在极度受限的存储空间中塞入更多功能与缺陷修复。此外,他是圣何塞州立大学机器人团队的志愿导师,并自去年起成为 ISO C++ 标准委员会成员。 这一课题的缘起要追溯到大约六年前。当时他在圣何塞州立大学教授一门名为“计算机工程 146(Computer Engineering 146: 嵌入式系统导论)”的课程。为了给学生提供良好的工程实践,他严格遵循 C++ Core Guidelines,为课程设计了一套 C++ 嵌入式固件框架,甚至为学生亲手绘制并制作了一块专属的开发板。 ### [01:25](https://www.youtube.com/watch?v=bY2FlayomlE&t=85s) 裸机开发的残酷现实:微控制器的极限资源 在带学生入门时,Khalil 强调的第一点就是裸机(bare-metal)开发的本质限制:系统完全没有操作系统支持。这意味着开发者无法调用 `fopen`,没有文件系统,没有网络栈,没有可以直接打日志的终端输出窗口,甚至没有内置时钟和线程机制。如果需要这些功能,必须自己从头实现,或者引入第三方库。 然而,第三方库不能随意塞进芯片。通过电子元器件分销网站 DigiKey 的微控制器数据对比,可以清晰看到裸机环境对 Flash 和 RAM 的严苛限制: | 资源类别 | 规格阈值 | DigiKey 在售芯片数量 | 规模对比 | |:--- |:--- |:--- |:--- | | **Flash 闪存** | 小于 1 MB | 约 33,000 款 | 占绝大多数 | | **Flash 闪存** | 大于 1 MB | 仅约 5,000 款 | 极少数大容量芯片 | | **RAM 内存** | 小于 100 KB | 约 31,000 款 | 主流嵌入式芯片标准 | | **RAM 内存** | 大于 100 KB | 仅约 7,000 款 | 较为稀少 | 为了避免内存耗尽(OOM)和严重的内存碎片化,嵌入式领域的主流策略是尽量避免、甚至全面禁用动态内存分配(如 `malloc`、`free`、`new`、`delete`),依赖堆分配的标准容器也因此遭到弃用。虽然部分工程师辩解称“我们只在系统初始化时使用动态内存”,但这必须严格保证后续运行绝不发生再次分配或释放。如果需要使用管理自身内存、不向系统堆申请空间的容器,社区通常会采用 Embedded Template Library(ETL);而在近期的圣路易斯会议上,C++26 正式投票通过了 `inplace_vector`,提供自带存储空间且支持 `try_push_back` 的标准容器方案。 ### [03:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=208s) 嵌入式领域拒绝 C++ 异常的典型理由 在微控制器资源极其宝贵的大背景下,固件开发者长期将 C++ 异常视作禁忌。Khalil 梳理了业内常见的反对理由: - **堆内存依赖与不可控消耗**:传统实现往往依赖 `malloc` 来分配异常对象空间,造成不可预估的内存消耗风险。 - **二进制体积膨胀**:不少开发者甚至误以为只要开启异常,就会将整套 STL(如 `std::string`、`std::vector`)全部拖入二进制中。 - **运行时类型信息(RTTI)的开销**:异常匹配通常依赖 RTTI,而 RTTI 会霸占宝贵的 Flash 存储空间。 - **复杂的辅助结构与代码段**:异常展开表(exception tables)和异常处理运行时本身的实现极其复杂,会占用显著的程序空间。 - **非确定性与延迟问题**:类型匹配机制(类似于 `dynamic_cast` 的开销)、异常表的二分查找、复杂的栈帧展开(frame unwinding)与栈帧评估,导致执行时间难以确定且较为缓慢。 ### [04:45](https://www.youtube.com/watch?v=bY2FlayomlE&t=285s) 接口抽象的冲突:输出引脚与 I2C 总线 Khalil 回顾了当年在教学框架中遇到的一个具体架构设计难题。在最基础的硬件外设控制中,最简单的接口是输出引脚(Output Pin):微控制器芯片上的物理管脚通过输出高电平或低电平,在引脚和地线之间建立电势差,从而点亮或熄灭发光二极管(LED)。最初他设计此接口时写得非常直观: ```cpp void level(bool value); ``` 当时他反复思考是否应该返回 `void`,还是需要返回某种错误状态。在单片机内置引脚的场景下,电平翻转通常是“绝对不会失败(infallible)”的操作。但当引入外部扩展硬件时,这种假设被彻底打破了。在嵌入式开发中,I2C 总线是极其通用的双线式外设通信协议,其典型交互架构包含一个控制器(Controller)与多个挂载设备(Device): ```mermaid flowchart TD accTitle: I2C通信时序与错误节点 accDescr: 描述I2C总线上控制器发起通信、寻址及数据传输过程中可能发生的错误阶段 subgraph Phase1["1. 寻址阶段"] A["控制器发出起始信号 (START)"] --> B["广播设备地址与读写位"] B --> C{"目标设备是否应答 (ACK)?"} end subgraph Phase2["2. 传输与故障"] C -- "未应答 (NACK)" --> E["产生地址未应答错误 (Address NACK)"] C -- "应答 (ACK)" --> D["逐字节读写数据并逐字节ACK确认"] D --> F["电磁干扰/器件干扰引发 IO Error"] end ``` 在 I2C 交互中,如果从设备未能在总线上给予应答,就会触发“地址未应答(Address NACK)”错误;同时电磁干扰或总线冲突还会导致“I/O 错误(IO Error)”。如果可插拔外设暂时缺席,尚能接受;但如果是一架无人机赖以维持姿态平衡的传感器在飞控通信中失联,整架无人机会瞬间坠落。 ### [08:08](https://www.youtube.com/watch?v=bY2FlayomlE&t=488s) 接口兼容性断层:IO 扩展芯片带来的抽象危机 当系统管脚耗尽时,嵌入式工程师通常会外挂一片“I/O 扩展芯片(IO Expander)”,即通过 I2C 总线向扩展芯片发送指令,来驱动其引脚输出高低电平。此时,软件架构遇到了本质冲突: > “这里出现了一个硬伤:I2C 侧是一个会产生失败的接口(fallible interface),而输出引脚接口则是一个绝对不会失败的接口(infallible interface)——至少按照我之前写出的规范它是这样的。”——Khalil Estell 如果编写一个底层包装函数 `io_expander_level` 去驱动扩展芯片上的管脚,底层依赖的 I2C 传输可能随时因物理故障抛出错误,然而上层通用的硬件输出引脚接口却规定其返回 `void` 且不会报错。两种设计理念在系统底层相撞,迫使开发者必须面对如何在极度受限的嵌入式环境下优雅、紧凑地传递并处理错误的难题。 ### [08:50](https://www.youtube.com/watch?v=bY2FlayomlE&t=530s) 错误处理的困境与引入 `std::expected` 在不能使用异常的嵌入式系统里,处理底层硬件失败一直是个棘手难题。Khalil Estell 以一个普通的 GPIO 引脚输出电平为例:如果操作失败,代码能做什么?在禁用异常的前提下,开发者要么设置全局变量(如类似 `errno` 的机制),要么在对象内部维护某种状态并通过独立的 API 去轮询状态。Estell 指出这两种做法都非常糟糕:“设置类似 `errno` 的机制并不是个好选择,大家对此基本都有共识;而在内部维护变量、要求调用方另行调用离线函数去查询状态,也丝毫不比 `errno` 强到哪去。” Estell 进而探讨了硬件失败在实际场景中的严重性。如果学生仅仅是用 GPIO 控制一颗普通的 LED,灯不亮或许无伤大雅,最多是让用户略微不满;但如果那颗 LED 恰恰是汽车仪表盘上的“发动机故障指示灯”(check engine light),问题就完全不同了。更严重的是,若引脚控制的是化学反应装置中的加热元件,或者关键的安全紧急切断继电器(emergency cutoff relay),一旦失败却未能及时报告,就可能导致极其严重的事故。系统在执行这些动作受挫时,至少必须能明确告知上层发生错误。 基于这种需求,Estell 决定将自己的 API 全面迁移到基于值的错误处理机制——`std::expected`。他定义了一个通用的状态类型别名,内部以 `void` 表示成功返回、以自定义的枚举表示具体错误,返回类似 `std::expected<void, error_enum>` 的结构。如果设备未就绪(例如前置条件被破坏),API 就会使用 `std::unexpected` 返回错误码;如果成功,则返回正常的预期结果。 ```mermaid flowchart TD accTitle: 从传统错误处理向 std::expected 的演进 accDescr: 展示从使用全局状态到采用 std::expected 强类型返回错误的处理逻辑转变 subgraph Legacy["传统方式(受限且脆弱)"] L1["硬件操作失败"] --> L2["设置全局 errno / 对象内部状态"] L2 --> L3["调用方容易遗漏检查 / 代码膨胀且脆弱"] end subgraph Expected["值语义模型(std::expected)"] E1["调用硬件 API"] --> E2{"操作是否成功?"} E2 -->|"成功"| E3["返回正常值 / void"] E2 -->|"失败"| E4["返回 std::unexpected&lt;error_enum&gt;"] end ``` --- ### [10:53](https://www.youtube.com/watch?v=bY2FlayomlE&t=653s) 错误向上传播的代价与痛苦的宏语法 当 Estell 开始站在调用者与学生的使用角度审视 API 时,问题暴露无遗。如果使用 C++ 异常,像“闪烁 LED”这样简单的业务逻辑会非常直观自然:将引脚拉高、等待、拉低、等待即可。但一旦改用 `std::expected`,每个函数调用后都必须立即插入一段样本代码:检查返回状态是否成功,若失败则手动将其包装为 `std::unexpected` 并返回,层层向上显式传播。 Estell 曾关注过 C++ 委员会提案 P2641R1 中提出的错误传播操作符(使用 `??` 双问号),该语法类似于 Rust 中的 `?` 操作符,可以在语言层面上将错误直接向上抛出栈外。然而该提案并未落地,且当时与相关作者联络也未获回应。无奈之下,在现有标准 C++ 语法内想要消除这层冗余,唯一的出路就是编写宏。Estell 设计了一个名为 `SJ_CHECK` 的宏,用以封装检查逻辑并自动提早退出。 然而在嵌入式领域,几乎所有底层外设都可以通过类似 I²C 的总线挂载——电机控制器可能走 I²C,微型显示屏也可能走 I²C。由于底层的总线随时可能掉线,这导致整个系统中的每一个 API 都必须被假定为可能失败(fallible)。随着整个库的全面改造,近乎所有接口都染上了 `std::expected` 的烙印。 - **C++ 提案与 Rust 借鉴**:P2641R1 尝试在 C++ 中引入类似 Rust `?` 的 `??` 错误传播操作符,但目前并不存在于标准中。 - **自定义宏替代**:为了不手动手写海量 `if (!res) return unexpected(...)`,只能依赖宏(如 `SJ_CHECK`)在语法层面减少行数。 - **接口污染**:几乎所有外设驱动都会级联调用总线,导致原本简单的接口全面充斥着错误包装层。 --- ### [12:55](https://www.youtube.com/watch?v=bY2FlayomlE&t=775s) 学生的反抗、传言的动摇与直面异常 经过长达两年半对学生的推行,这种机制迎来了使用者的强烈反弹。Estell 观察到三大核心问题: 1. **满屏的 `if` 条件与粗暴的强制转换**:学生极度厌烦在各处手工编写错误分支,甚至为了省事,开始直接用 `(void)` 把函数返回值强制抹去,对错误视而不见。有位学生因此排查了一个星期的 Bug,最终才发现只是总线地址配错了,但因为没有向上正确传播错误,调用处根本看不到提示。 2. **极其严重的侵入性与病毒式扩散(Virality)**:只要底层有一处可能出错,整条调用链路上的所有函数类型签名都必须被污染成返回状态或预期结果,牵一发而动全身。 3. **宏代码的视觉噪音**:代码充斥着宏调用,阅读时甚至需要刻意在脑海中将其遮盖过滤,才能看清正常的执行流(happy path)究竟在干什么。 每逢学期,学生都会不解地质问:“为什么不能直接用 C++ 异常?异常本就是为了错误处理而设计的内置功能,用它不就解决所有问题了吗?”过去,Estell 总会照搬行业共识向他们灌输:搬出各种文章和图表,甚至现场在编译参数中把 `-fno-exceptions` 改成 `-fexceptions`,展示代码体积如何当场失控,以此劝阻他们。大部分学生接受了,但总有人追问到底:为什么异常是非确定性的?为什么它必须那么慢?为什么它一定会导致代码体积膨胀,甚至需要动态堆内存? 面对这些追问,Estell 渐渐无法说服自己:“我开始不喜欢自己在向学生灌输一些我自己都不清楚其运行底层逻辑的东西。对我来说它完全是个黑盒,我只是把道听途说的言论转述给他们而已。”由于对自己没完全搞懂就直接叫停的做法感到愧疚,他决定彻底打破成见,给 C++ 异常一次完全公开透明的实验。 > “我开始对自己劝告学生不要使用它感到愧疚,因为我压根不知道到底为什么不能用它;所以我告诉自己:‘你知道吗,我要给异常一次完全公正的检验,我们要一条道走到黑。’” —— Khalil Estell --- ### [15:24](https://www.youtube.com/watch?v=bY2FlayomlE&t=924s) 在 ARM 架构上启用异常与 150KB 的膨胀迷局 在 ARM 平台上启用异常是一项复杂而布满暗礁的工作。Estell 写了一个极其简单的测试程序:`main` 函数调用一个被显式禁止内联(`noinline`,防止编译器将代码优化掉)的 `start` 函数,并在其中抛出异常,外层配合对应的 `catch` 块去捕获。 当开发者从 GNU 工具链着手运行时,碰到的第一道壁垒就是:异常在默认状态下是被强行禁用的。一旦触发 `throw`,程序甚至不论语法是否合法都会立即调用 `std::terminate` 崩溃退出。更荒谬的是,此时编译系统却依然默默链接了支持异常所需的绝大部分底层运行库,不仅白白侵占了宝贵的固件体积,而且根本无法工作。为了真正点亮功能,必须从工具链源码中将带有 `-fno-exceptions` 的配置重新编译为 `-fexceptions`,并且必须在底层实现一个类似 POSIX `sbrk` 的系统调用,为 `malloc` 堆分配器提供底层内存支撑。 完成这些配置后,异常确实成功捕获并运行了,但紧接着呈现出的数字却令人窒息: | 状态 | 编译出的固件体积 | 占微控制器 Flash(512 KB)比例 | 说明 | |:--- |:--- |:--- |:--- | | **未启用异常(原始基准)** | 约 **2.6 KB** | < 1% | 纯净极简程序 | | **打通异常链路后** | 约 **150,000 字节 (~150 KB)** | **~30%** | 仅仅是一个 throw/catch 动作 | 对于一台只有 512 KB Flash 的嵌入式 MCU 来说,一个最基础的 throw-catch 就直接吞噬了整整 30% 的空间,这样的开销在工业固件中显然不可接受。但这并未让 Estell 放弃,反而激起了他的好奇心:代码极其简单,那些庞大的字节究竟来自哪里?他祭出了嵌入式底层逆向与排查的终极手段——反汇编。Estell 在反汇编生成的巨型代码段中逐行排查并非自己编写的符号,最终顺藤摸瓜,发现了一个名为 `Global String Callback` 的可疑 API 调用。 ### [17:42](https://www.youtube.com/watch?v=bY2FlayomlE&t=1062s) 破除庞大体积:重载默认的 Terminate 处理器 ```mermaid flowchart TD accTitle: 默认异常终止处理器的代码膨胀链条 accDescr: 展示未捕获异常如何通过默认 verbose terminate handler 引入大量字符串与格式化函数,以及通过弱符号覆盖解决此问题的过程。 subgraph Bloat["默认终止流程(引入约 137 KB 膨胀)"] A["未捕获异常抛出至 main"] --> B["__gnu_cxx::__verbose_terminate_handler()"] B --> C["格式化输出: 'terminate called after throwing...'"] C --> D["引入 snprintf / sprintf"] C --> E["引入 strlen / 字符串处理"] C --> F["引入 libsupc++ 字符流适配代码"] end subgraph Fix["弱符号重载优化(缩减 91%)"] G["自定义空实现函数"] -->|"覆盖弱符号"| B G --> H["实现极简死循环: while(true) {}"] H --> I["固件体积骤降至 13 KB"] end ``` 在检查生成的二进制文件内部符号时,Khalil Estell 发现了大量诡异的符号:`append_num`、`sprintf`、`strlen` 以及各种流适配器。这让他极为困惑,因为他自己的裸机代码里根本没有写过任何字符串操作。顺藤摸瓜排查后,他找到了罪魁祸首——GNU C++ 库中的 `__gnu_cxx::__verbose_terminate_handler`。当抛出的异常一路穿透调用栈、最终在 `main` 之外未被捕获时,标准库会打印一行友好的错误提示:“terminate called after throwing an instance of...”,而仅仅为了在控制台上渲染这行字符串,编译器和链接器就不得不把一整套庞大的字符串格式化工具链(包括各种打印函数、数值转换函数等)硬生生拖进二进制文件里。 面对这种严重的膨胀,解决手段出人意料地简短。在 GCC 和 Clang 等编译器中广泛存在“弱符号”(Weak Symbols)机制——平时开发者编写的每个变量或函数通常都是强符号(Strong Symbol),但如果标准库中的某个符号被标记为 `__attribute__((weak))`,开发者就能在自己的代码中定义同名符号将其强行覆盖。Khalil Estell 仅仅用了五行代码,写了一个只有 `while(true) {}` 死循环的自定义终止处理器覆盖了默认实现。这一项改动直接消减了 91% 的二进制体积,原本高达 150 KB 的庞大固件瞬间骤降到仅剩 13 KB。对于嵌入式固件而言,150 KB 几乎无法接受,但 13 KB 已经让异常机制进入了完全可用的实际范畴。 - 默认 verbose terminate handler 为了打印类型名和报错信息,会隐式引入 `sprintf`、`strlen`、`append_num` 等大量标准库辅助代码。 - 弱符号(Weak Symbols)允许在用户层无缝覆写底层库实现,无需魔改标准库源码。 - 重载后的死循环版本消除了所有格式化字符串依赖,使得固件开销从 150 KB 暴跌至 13 KB。 --- ### [19:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=1145s) 解决动态内存与 RTTI 的心理阻碍 虽然体积砍了下来,但另外两大技术屏障仍然横亘在眼前:动态内存分配与运行时类型信息(RTTI)。首先是动态内存分配问题,异常抛出默认会调用 `malloc`,导致底层依然不得不依赖 `_sbrk` 桩函数。Khalil Estell 坦言,一边在课堂上告诫学生嵌入式系统绝对不能使用动态内存,一边自己提供给学生的 SDK 却在异常机制里偷偷调用堆分配,这在心理上很难自洽。他查阅了 `throw` 对应的汇编指令调用链,发现底层分配异常对象的函数是 `__cxa_allocate_exception`。幸运的是,这个函数同样是一个弱符号。通过重载该函数,开发者可以直接提供一块预先分配好的静态内存缓冲区来充当异常分配器,从而彻底摆脱对通用堆(`malloc`)的依赖,拿回对内存使用的完全掌控权。 随后他迎战了嵌入式开发者心目中最大的恐惧——RTTI(运行时类型信息)。随着项目规模扩大、抽象和继承层次增多,RTTI 数据通常被认为会无限膨胀。C++ 异常匹配 `catch` 块时需要依赖 RTTI,那能否在编译时直接关闭它?Khalil Estell 索性直接把编译选项从 `-frtti` 改成了 `-fno-rtti`,并在现场向观众抛出了一个互动竞猜: 1. 编译器直接报错,拒绝在禁用 RTTI 的情况下使用异常; 2. 编译器剥离所有与异常处理无关的 RTTI,仅保留被 `throw` 抛出类型的元数据; 3. 编译器完全沉默,删掉所有元数据,一旦运行时执行 `throw` 就触发未定义行为(UB)。 现场绝大多数人因为对 C++ 的刻板印象,认定结果是第 3 项未定义行为,Khalil 调侃道:“你们全都是 Rustaceans 吗?你们全都潜伏进 CppCon 了吗?为什么你们都默认 C++ 一定会产生未定义行为?”事实上,正确答案是第 2 项。链接器非常聪明,只要加上 `-fno-rtti`,所有未被异常系统引用的类型元数据全被剔除,只有明确被 `throw` 的类型才会生成 RTTI。而且 RTTI 的内存开销极为轻量: - 无父类的独立对象类型仅占用 2 个指针大小; - 单继承对象占用 3 个指针大小; - 多继承(继承 $n$ 个父类)的对象,其占用量也仅为 $3 + n$ 个指针。 对于仅抛出少量错误类型的嵌入式系统而言,这种程度的内存占用微乎其微。再加上现代 ARM 编译器对默认终止处理的精简(如今默认开销已从过去的 150 KB 降到了 84~87 KB 左右),传统上拒绝异常的三大借口——`malloc` 依赖、标准库全量拉取、RTTI 膨胀——在技术上全部被击碎。 --- ### [23:04](https://www.youtube.com/watch?v=bY2FlayomlE&t=1384s) 惊人逆转:真实项目中的固件体积不增反降 ```mermaid flowchart LR accTitle: 演示 Demo 与大型项目在引入异常后的体积走向 accDescr: 比较小型测试用例与较复杂工程在切换至异常处理机制时固件体积的变化趋势。 subgraph Demos["小型 Demo(LED 闪烁 / Hello World)"] D1["切换至异常"] --> D2["代码体积微幅上升"] D2 --> D3["原因: libunwind 基础框架固定开销占主导"] end subgraph RealProjects["实际复杂项目(如学生 MP3 播放器)"] R1["切换至异常"] --> R2["代码体积显著缩减(56KB 降至 40KB+)"] R2 --> R3["原因: 剥离了大量分支返回值检查与错误冒泡传递指令"] end ``` 在扫清底层阻碍后,Khalil Estell 决定重构其开源嵌入式库 `sjsu-dev2`。他彻底删除了项目里的所有错误宏定义,全量拥抱异常处理机制;与此同时,为了做严格的对照实验,他并行创建了另一个拉取请求(PR),将代码库全部重构为现代函数式风格的 `result` 返回值类型。他在当时的提交记录中写道:“经决定,代码库全面转向异常处理而非错误返回类型;对于足够大的项目,异常在代码体积上更小,且在未抛出异常时的执行速度更快。” 在随后的验证过程中,他观察到了一个颠覆认知的现象: 当他在非常简单的 Demo(如 LED 闪烁程序或 Hello World)上测试异常机制时,固件体积确实如预期一样有所上升,毕竟引入了异常展开库(`libunwind`)的基础骨架。然而,当他把测试对象换成稍微复杂一点的真实工程时——比如让学生作为期末大作业设计的一款 MP3 播放器固件——体积不仅没有增加,反而大幅缩水!该工程面向的微控制器 Flash 总容量只有 64 KB,原先使用错误码方案时的二进制大小约为 56 KB;但在重构成 C++ 异常版本后,固件体积直接暴跌到了 40 多 KB。这一发现让他大为震撼。 不过在此后的数年间,Khalil 并没有公开分享这一结论。一方面他不确定嵌入式社区是否真的在乎这一技术细节;另一方面,他幽默地承认自己当时十分忌惮激进的 Rust 社区:“我当时也害怕 Rust 社区找上门来,因为只要我展现出任何有利于 C++ 的言论,我肯定会被他们针对,然后在社交媒体上被彻底喷烂。”因此,这个秘密被他深藏了数年。 --- ### [24:49](https://www.youtube.com/watch?v=bY2FlayomlE&t=1489s) 理论假说:错误返回值检查与异常展开的体积博弈 这一局面的破冰发生在 2023 年的 CppCon。在 ISO C++ 委员会的会议期间,Patrice Roy 正在讨论嵌入式系统中引入多态内存资源向量(`std::pmr::vector`)的可行性。Khalil 与他探讨了 `vector::push_back` 的痛点:在裸机开发中,一旦内存分配失败,`push_back` 无法通过返回值告知上层错误,只能直接调用 `std::terminate` 或触发 Panic,而大多数嵌入式工程师根本无法容忍底层容器随意崩溃。谈话间,Khalil 随口说道:“嵌入式开发者不用 C++ 异常真的很可惜,因为使用异常反而能让他们的二进制固件变得更小。” 这句话当场让 Patrice 震惊不已,Patrice 连连摆手直呼:“等一下,等一下,这完全违背了所有人的常识,你到底是什么意思?”Khalil 随后把当年 MP3 播放器体积大幅缩减的来龙去脉复述了一遍。Patrice 听后立刻要求他拿出数据证明,并在周三的 ISO 委员会分组会议(SG14)上向所有人做正式展示。为了在会议上给出严谨的科学解释,Khalil Estell 构建了一套分析固件代码体积的数学假说模型: | 机制类型 | 体积成本计算公式 | 变量说明 | 核心特征 | |:--- |:--- |:--- |:--- | | **返回值/Result 类型** | $\text{Cost} = A \times N$ | • $A$:单次函数调用后进行错误状态检查的平均机器指令字节数<br>• $N$:整个代码库中**受检查的函数调用点总数** | 随调用点(Call Sites)数量线性递增;调用点越多,分支比较与错误向上传递代码膨胀越严重。 | | **C++ 异常机制** | $\text{Cost} = B \times M + E$ | • $E$:异常传播运行库(`libunwind`)的固有固定开销<br>• $B$:单份异常处理元数据(展开表)的平均字节数<br>• $M$:代码库中**非叶子函数(Non-leaf Functions)的总数** | 只要函数不抛出且不跨越栈帧,几乎无调用开销;元数据按函数本身数量计算,与函数被调用的频次无关。 | 在任何真实的、剔除了无用死代码(Dead Code Stripping)的中大型程序中,受检查的调用点数量 $N$ 必然远远大于非叶子函数的定义数量 $M$(即 $N \gg M$)。错误码模式下,每一次函数调用后都必须紧跟测试寄存器、条件跳转、打包错误并向上一层返回的汇编指令,这些重复的防御性指令会像滚雪球一样在调用树中成倍膨胀;而异常机制通过将错误路径隔离在外部元数据表中,彻底消除了正常路径上的这部分调用点指令开销。这正是 MP3 播放器等中大型工程在开启异常后,二进制体积不增反减的数学根源。 > “在足够大的项目中,异常在代码体积上更小,且在未抛出异常时的执行速度更快。” —— Khalil Estell quoting his past commit message ### [27:22](https://www.youtube.com/watch?v=bY2FlayomlE&t=1642s) 代码尺寸权衡实验:异常 vs `Result` 类型 针对固件开发中长久以来的争论,Khalil Estell 设计了一个受控实验,以检验在不同的错误检查数量下,异常机制与基于值的错误返回机制(如 `Result` 或 `std::expected`)在二进制体积上的实际表现。他编写脚本随机生成了大量同构(isomorphic)的 C++ 代码文件:一组采用异常处理(`accept.cpp`),另一组采用类似 `Result` 的错误码返回(`result.cpp`)。代码中随机定义了包含或不包含平凡析构函数(trivial destructors)的各类结构体,并构造了相互调用的函数链路。为了防止编译器因内联或全局优化将代码整个消除,代码中特意加入了 `volatile` 变量产生副作用。 ```mermaid flowchart LR accTitle: Exceptions vs Result Binary Size Tradeoff accDescr: A curve showing binary size vs number of checks with a crossover point around 550 checks. subgraph CostComponents["空间开销构成"] EX_BASE["异常基础库体积<br/>(libunwind ~867 B)"] RES_PER["每处检查分支代码<br/>(~16.5 B / check)"] end subgraph Crossover["交叉平衡点"] CROSS["约 550 次错误检查"] end EX_BASE --> CROSS RES_PER --> CROSS ``` 实验数据绘制成图表后展现出明显的规律:红线代表 `Result` 类型,蓝线代表异常处理,黄线代表两者的体积差值(Delta)。虽然异常处理有一笔大约 867 字节的固定运行时开销(接近精简版 `libunwind` 的体积),但 `Result` 类型每次通过 `if` 分支进行错误检查和逐层向上传递都会带来大约 16.5 字节的代码膨胀。因此,两条曲线在大约 550 次检查处相交。一旦工程中的错误检查总数超过约 550 次,使用异常的二进制体积反而比返回 `Result` 更小。对于规模稍大的固件而言,这并不是一个遥不可及的数字。 - 实验代码聚焦于错误向上传播(error propagation)而非业务处理,因为在生产环境中传递错误的胶水代码远多于最终处理错误的逻辑。 - 编译器在单翻译单元内极度激进,即使标有 `noinline` 也可能消除无副作用代码,因此必须依赖 `volatile` 保证函数与调用的真实生成。 - 单个复杂的业务类中往往就能包含多达 50 次错误检查,整个工程累积达到 550 次检查门槛十分普遍。 - Khalil Estell 强调这些宏观图表不足以让人信服底层机理:“None of those graphs were meant to convince you at all... If I want to convince you, I need to show you how exceptions work from throw to catch.” --- ### [30:03](https://www.youtube.com/watch?v=bY2FlayomlE&t=1803s) 异常处理中的函数分类与 ARM 调用约定 为了彻底讲透基于表的异常机制(table-based exceptions),Khalil Estell 排除了嵌套异常和非基于表的方案,并给出了一个具体的执行模型:函数 `foo` 内部包含 `try-catch` 块并调用 `bar`;`bar` 构造了一个具有析构函数的局部对象并调用 `baz`;`baz` 内部直接抛出异常。从异常展开(unwinding)的角度,系统中的函数可以严格分为三类: 1. **叶子函数(Leaf Functions)**:不调用任何其他函数,不包含析构函数,也没有 `try-catch` 块。它们根本不参与异常展开。 2. **透明/平凡函数(Transparent / Trivial Functions)**:会调用其他函数,但内部没有析构函数也没有 `try-catch` 块(在示例中为 `baz`)。 3. **可重入函数(Reentrant Functions)**:会调用其他函数,且包含析构函数或 `try-catch` 块(在示例中为 `foo` 和 `bar`)。在展开过程中,这类函数必须被“重新进入”以调用对象的析构函数清理资源,或者跳入捕获处理分支。 随后,他讲解了理解底层栈回溯所必需的 ARM 架构调用约定(Calling Convention)。ARM 架构拥有 16 个核心通用寄存器:`r0` 到 `r3` 用作临时暂存寄存器(scratch registers)、函数入参传递以及返回值返回;`r4` 到 `r11` 为被调用者保存寄存器(preserved registers),被调函数必须保证在返回时这些寄存器的值与调用前完全一致。此外,还包括栈指针 `SP`、存放函数返回地址的链接寄存器 `LR`(Link Register)以及指示当前执行位置的程序计数器 `PC`。 ```mermaid flowchart TD accTitle: ARM Prologue and Epilogue Stack Management accDescr: Sequence diagram of pushing and popping registers in ARM calling convention. subgraph FunctionPrologue["函数入口序言 (Prologue)"] PUSH["push {r4, lr}<br/>保存调用者上下文与返回地址"] end subgraph FunctionBody["函数主体执行"] EXEC["执行计算,使用 r4 暂存数据"] end subgraph FunctionEpilogue["函数退出结语 (Epilogue)"] POP["pop {r4, pc}<br/>恢复 r4,并将 LR 直接弹入 PC 实现跳转返回"] end PUSH --> EXEC --> POP ``` - 函数序言执行 `push {r4, lr}` 将旧上下文推入栈中,随后在退出时通过 `pop {r4, pc}` 恢复上下文,同时将保存的返回地址直接载入程序计数器实现返回。 - 异常机制的核心本质,正是在发生异常时通过读取预先生成的只读数据表,逆向模拟上述反向弹出寄存器的过程,从而精准还原每一层调用栈的 CPU 寄存器状态。 --- ### [32:50](https://www.youtube.com/watch?v=bY2FlayomlE&t=1970s) 内存布局与抛出流程:`__cxa_allocate_exception` 与 `__cxa_throw` 在 Itanium C++ ABI 标准规范下,执行 `throw` 表达式在底层会被编译器转换为两个核心调用:首先调用 `__cxa_allocate_exception` 分配内存,随后调用 `__cxa_throw` 发起真正的展开流程。`__cxa_allocate_exception` 传入抛出对象的大小,但它实际分配的内存空间比该对象大得多:它在对象前面预留了专门的“异常头”(Exception Header)。 ```mermaid flowchart LR accTitle: Exception Memory Allocation Layout accDescr: Memory block showing exception header prepended to the thrown exception object. subgraph AllocatedBlock["连续内存块"] direction LR HDR["异常头 (Exception Header)<br/>- CPU 虚拟寄存器状态镜像<br/>- type_info 指针<br/>- 析构函数指针<br/>- 运行时缓存数据"] OBJ["抛出的异常对象<br/>(Thrown Object Payload)"] end ALLOC["__cxa_allocate_exception(size)"] --> AllocatedBlock ``` 异常头充当展开机制记录系统状态与展开进度的黑板。它包含一个虚拟的 CPU 寄存器状态结构体,用于在每一阶段模拟计算上一层调用者的寄存器值;还包含指向该类型 `type_info` 的指针(用于匹配 `catch` 块)、异常对象的析构函数指针(用于最终销毁异常),以及加速匹配的缓存。Khalil Estell 说明这一具体内存布局是他针对嵌入式环境设计的紧凑实现,与标准 Itanium ABI 存在差异。不直接使用普通 `malloc` 分离分配对象和头部的原因在于保持 ABI 灵活性并消除二次分配开销: > “See, if I resize the exception header in any way in the future... if we use malloc and we bake that straight into your binary and we update it later on, the size of the header will be completely off, the unwind mechanism will be messed up, you'll have problems.” 在 `__cxa_throw` 内部,流程按以下步骤开展: 1. **记录活动异常**:将当前异常存入当前线程上下文(通过 TLS 变量,作者提到 Ben Craig 曾建议移除 TLS,目前正在改进)。 2. **定位异常头**:通过辅助函数 `extract_exception_header`,用传入的对象指针减去异常头的大小,逆向计算出头部的首地址。 3. **初始化元数据**:在头部填入对应的 `type_info` 指针和自定义析构函数指针(平凡析构对象则为 `nullptr`)。 4. **即时栈展开(Immediate Unwind)**:对 `__cxa_throw` 自身执行一次立即展开,将 `LR` 中的地址赋值给 `PC`,令虚拟 CPU 镜像直接跳转回调用 `throw` 的函数(如 `baz`)的上下文状态,随后正式启动全局异常处理循环,去查找对应的异常表条目(Exception Entry)。 ### [36:42](https://www.youtube.com/watch?v=bY2FlayomlE&t=2202s) 异常展开的判定逻辑与退出路径 在定位到异常索引表项之后,展开机制会开始对该项信息进行求值与决策。展开器会首先询问当前函数是否拒绝展开——例如该函数是否被标记为 `noexcept`。如果是 `noexcept` 且发生了异常,系统将直接终止程序(`std::terminate`)。如果允许展开,接下来会检查该函数是否是“可重入的”(reentrant),即当前函数体内是否存在需要执行的逻辑。如果不是可重入的(即透明函数,既没有析构函数也没有 `catch` 块),直接模拟返回、恢复调用者的寄存器状态即可;如果是可重入的,则必须进入该函数的上下文,检查当前程序计数器是否匹配了某个 `catch` 处理块,或者是否存在局部对象的析构函数需要执行。 ```mermaid flowchart TD accTitle: 异常展开逻辑与路径流转 accDescr: 展示从异常表项求值到进入悲伤路径,再返回正常流程或级联展开的判断流程。 subgraph Evaluation ["表项求值阶段"] A["读取异常索引表项"] --> B{"是否拒绝展开 (noexcept)?"} B -->|"是"| C["调用 std::terminate"] B -->|"否"| D{"函数是否为可重入?"} D -->|"否 (透明函数)"| E["直接展开并恢复寄存器"] D -->|"是 (存在析构或catch)"| F["进入悲伤路径 (Sad Path)"] end subgraph SadPath ["悲伤路径处理"] F --> G{"命中 catch 还是析构函数?"} G -->|"命中析构函数"| H["调用 __cxa_cleanup_end 继续展开"] G -->|"命中 catch 块"| I["调用 __cxa_end_catch 回到快乐路径"] end ``` 一旦确认进入“悲伤路径”(Sad Path),函数的退出分支主要有两条:一条是调用 `__cxa_cleanup_end`,当展开器调用完当前作用域内的局部对象析构函数后,必须调用该函数以重新回到异常展开流程,继续向外层调用栈回溯;另一条则是命中匹配的异常捕获块,在执行完相应的处理逻辑后调用 `__cxa_end_catch`,从而彻底结束异常处理,让程序控制流重返正常的“快乐路径”(Happy Path)。 ### [37:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=2248s) ARM 异常索引表结构与内联展开指令 ARM 架构的异常索引表(Exception Index)界定在符号 `__exidx_start` 和 `__exidx_end` 之间。ABI 规定该索引表中的每一个表项严格占用 8 个字节: - **前 4 字节**:函数地址的相对偏移量(通过相对偏移设计可以使代码具备位置无关性)。 - **后 4 字节**:指向异常数据(Exception Data)的偏移量,或者直接嵌入的内联展开数据。 索引表中的条目顺序与代码中函数的物理排布顺序完全一致。如果代码从地址 0 开始依次排列着 `foo`、`bar`、`baz`,那么异常索引表里的条目顺序也严格是 `foo`、`bar`、`baz`。这种严格单调递增的布局使得展开器在运行时只需利用当前的程序计数器(PC)进行一次高效的二分查找(Binary Search):只需比对相邻两个表项的函数起始地址,只要 PC 落在二者之间,就能立刻确定当前执行流归属于左侧的函数。 后 4 个字节本质上是一个带标签的联合体(Tagged Union),最高有效位(MSB,第 31 位)充当判别标志:若最高位为 0,代表这是一个指向外部异常数据表的相对偏移量;若最高位为 1,则说明该 32 位字内部直接内联了异常元数据或展开指令。 如果一个透明函数(无析构函数、无 `try-catch`)被显式声明为 `noexcept`,编译器会直接将这个 4 字节的值替换为常量数字 `1`。数字 `1` 在展开协议中是一个特殊信号:“一旦需要展开此函数,立刻调用 `std::terminate`”。此外,如果一个函数比较简单,其展开清理工作只需要 3 条或更少的展开指令,编译器就会直接把这几条指令塞进这 4 个字节的表项中,完全无需在外部另辟表格,从而大幅削减固件体积。 ### [39:43](https://www.youtube.com/watch?v=bY2FlayomlE&t=2383s) ARM 展开指令集编码与个性化数据格式 ARM 体系下的机器指令长度通常为 32 位(ARM32/A64)或 16/32 位(Thumb-2),但用于栈展开的虚拟指令却极其紧凑,通常以 8 位(1 字节)为单位编码。展开指令不需要做复杂的通用计算,其核心职责无非就是调整栈指针(加减栈偏移)、将保存的寄存器从栈上弹出恢复到 CPU。正如讲师所指出的:“展开指令不需要做太多事情,它们只需要减栈、弹出一组寄存器,仅此而已;因此只需要大约 1 个字节的信息就能表示它们,相当于模拟函数返回过程的高压缩版本。” ARM 异常处理 ABI 文档明确指出,绝大多数函数的栈清理逻辑在 3 字节的展开指令内即可完整表达;而在整个 ARM 体系下,无论编写出多么极端的函数,其展开指令的物理极限也绝不会超过 7 个字节。常用的展开指令类型包括: - 为栈指针累加偏移或减去偏移 - 批量弹出寄存器(如 R4-R11 及 LR) - 将某个寄存器的值直接赋给栈指针 - 展开完成指令(`finish`) - 拒绝展开指令(`refuse to unwind`) 内联数据被称为个性化数据(Personality Data),其内部位域布局定义如下: - **Bit 31**:值为 `1`,表明当前为内联格式。 - **Bits 28–30**:保留位。 - **Bits 24–27**:个性化索引(Personality Index),决定后续字节的解码格式。如果索引为 0,表示后续紧跟的 3 个字节直接就是 Instruction 1、Instruction 2 和 Instruction 3。如果索引为 1 或 2,则采用长个性化数据格式(Long Personality Data),其中包含一个表示后续附加展开字数量的长度字段。 讲师在此处吐槽了 ABI 的设计冗余:既然 ARM 官方早已确认任何函数最多只需 7 条指令,额外数据最多也就 1 到 2 个字,这里专门留出一个长度字段其实完全是浪费。如果去掉这个长度字段,指令就能整体前移,进而省去用于对齐的 `finish` 填充字节,每个长表项本可以多节省 4 个字节。讲师无奈打趣道:“但是没办法,我们不可能在所有时候都拥有完美的东西。” 随后讲师以具体函数 `baz` 为例演示了这一解析流程:展开器二分查找到 `baz` 的条目,读取到最高位为 1,个性化索引为 0。该表项中唯一的有效指令是弹出 `R4-RN` 寄存器范围和链接寄存器(LR)。虚拟机执行这条 1 字节指令后,调用者 `bar` 的返回地址立即被还原至 LR,`bar` 的局部寄存器也被还原进 R4,栈指针也恢复到了外层调用帧。紧接着展开器准备以同样的方式处理外层函数 `bar`,但检测到 `bar` 具有需要调用的析构函数(可重入),内联指令已不足以描述,必须进一步查阅外部异常表。 ### [42:47](https://www.youtube.com/watch?v=bY2FlayomlE&t=2567s) 异常表与 GCC 语言特定数据区 (LSDA) 当内联位为 0 时,第二个 32 位字就是一个负偏移量(例如 `-2340`),展开器顺着该偏移寻址到位于只读数据区的异常表(Exception Table)。异常表包含长格式个性化数据以及 GCC 的语言特定数据区(Language Specific Data Area,简称 LSDA)。 LSDA 的架构源自 Itanium C++ ABI,最初设计的目的是为了实现跨语言异常捕获与回溯,让 Java、C 和 C++ 能够在同一套异常框架下互相协作。通过跨语言调用接口,C++ 函数抛出的异常可以穿透 Java 栈帧,在 Java 中执行清理后再穿透回 C++。讲师指出,标准 GCC 异常处理机制之所以速度偏慢,正是因为它背负了兼容 Java 等多语言的沉重包袱,即便嵌入式固件中根本没有一行 Java 代码,这套通用机制也依然存在。不过 GCC 也顺理成章地将这套成熟的 LSDA 结构复用到了纯 C++ 中,形成了一套与 CPU 架构无关的通用异常描述模型。 LSDA 包含以下核心区段: - **个性化函数(Personality Function)**:一段专门的代码指针,负责理解并解码其后附带的特定语言数据。讲师提到在自己的微型嵌入式运行时中,他直接重写并替换掉了这个函数。 - **个性化数据(Personality Data)**:附加的长展开指令序列。 - **头部信息(Header)**:指明后续各个表格的编码与寻址方式。 - **调用点表(Call-Site Table)**:记录函数内所有关键的代码区间范围(Scopes),包括哪些指令范围内有对象需要析构,以及各个 `try` 块覆盖的指令范围。 - **动作表(Action Table)**:维护 `catch` 处理分支的优先级与级联关系,确保异常按代码中编写的先后顺序进行类型匹配。 - **类型表(Type Table)**:存放该函数所关注的所有异常类型的 `std::type_info` 指针集合。为压缩体积,类型表具有去重特性,即使同一个函数内写了 5 次 `catch (const std::exception&)`,类型表中也只保留一份该类型的条目。 ```mermaid flowchart TD accTitle: GCC 语言特定数据区 (LSDA) 结构划分 accDescr: 描述 LSDA 内部从个性化函数到类型表的数据组成结构。 subgraph LSDA ["语言特定数据区 (LSDA)"] direction TB PF["个性化函数指针 (Personality Function)"] PD["展开指令数据 (Personality Data)"] HD["头部配置 (Header / 编码方式指示)"] CS["调用点表 (Call-Site Table / 析构与try作用域)"] AT["动作表 (Action Table / catch 链表与顺序)"] TT["类型表 (Type Table / 去重后的异常类型集合)"] PF --> PD --> HD --> CS --> AT --> TT end ``` 在解析头部之前,讲师着重介绍了底层的 LEB128(Little Endian Base 128)可变长度编码格式。LEB128 依据每个字节的最高有效位进行流式解析:若 MSB 为 1,表示后续字节仍属于当前数值;若 MSB 为 0,则表示该数值已达末尾,将截取的 7 位有效数据拼接即得到最终数值。这种编码能让小整数仅占用 1 个字节,极大节约了紧缩空间的固件体积。 LSDA 头部的第一个字节在嵌入式系统上几乎恒定为 `0xFF`,其含义是省略 DWARF 调试格式信息(在固件中通常被完全剥离)。随后的字节指示了类型表中条目的编码策略——究竟是位置无关的相对寻址还是绝对地址寻址,紧接着则是利用 LEB128 编码的偏移量,直接指向整个 LSDA 末尾的类型表基准位置。 - ARM 异常索引条目固定为 8 字节:4 字节函数相对偏移 + 4 字节数据/内联联合体。 - 若内联字段的值直接为整数 `1`,专指属于 `noexcept` 的函数,展开时直接触发 `terminate`。 - 绝大多数 ARM 函数的展开指令可在 3 字节内表达,理论上限为 7 字节。 - LEB128 编码机制使用每个字节的第 7 位作为延续位(Continuation bit),为嵌入式元数据提供了极高的数据压缩比。 - LSDA 头部以 `0xFF` 开头以屏蔽冗余的 DWARF 信息,类型表则放置在整段区域的最末端并通过偏移逆向索引。 ### [46:23](https://www.youtube.com/watch?v=bY2FlayomlE&t=2783s) 解析 Call Site 表与执行析构函数(Landing Pad) LSDA(Language Specific Data Area)头部之后紧接着是 Call Site 表。Call Site 的编码方式可以是 ULEB128、uint8_t、uint16_t 等格式,头部指示了该表的总字节长度。在 Call Site 表中,每一条记录都包含四个核心字段:起始位置(Start)、范围长度(Length)、落地区(Landing Pad)以及动作记录(Action Record)。Start 表示函数汇编代码中的起始相对偏移,Length 表示该有效范围的长度。如果 CPU 的程序计数器(PC)当前落在这个范围之内,就说明异常发生在该作用域内,当前记录的 Landing Pad 和 Action Record 就对该帧有效。 ```mermaid flowchart TD accTitle: Call Site Record Lookup Flow accDescr: 流程展示从读取 Call Site 表、匹配程序计数器范围,到决定是直接跳过展开还是跳转到 Landing Pad 执行析构。 A["读取 Call Site 记录<br/>(Start, Length, Landing Pad, Action)"] --> B{"PC 是否在<br/>[Start, Start + Length) 区间?"} B -- "否" --> C["读取下一条记录"] B -- "是" --> D{"Landing Pad == 0?"} D -- "是" --> E["提前返回:该区域无清理工作<br/>继续直接展开栈帧"] D -- "否" --> F["计算跳转目标:<br/>函数首地址 + Landing Pad | 1"] F --> G["调用 restore_cpu_state<br/>恢复虚拟寄存器并跳入 Landing Pad"] ``` 在伪代码实现中,函数 `read_encoded_data` 不断从指针处解码出这四个字段。一旦发现当前程序计数器落在 `[start, start + length)` 范围内,循环即可中断。若匹配到的 Landing Pad 为 0,表明此作用域内没有任何析构或 catch 工作,运行时可以直接跳出当前函数并继续展开栈帧。若 Action Record 为 0(且有时类型表不存在),通常代表这是一次无条件的析构调用(Unconditional Unwinding),只需调用析构函数即可。 为了真正跳转进执行代码,运行时将函数起始地址与 Landing Pad 偏移相加,并将最低有效位(LSB)与 1 进行按位或运算(OR 1)。“这因为是 ARM 架构规范的安全与 Thumb 模式要求,我认为是出于安全性。”随后将目标地址写入程序计数器,并调用 `restore_cpu_state`,将先前保存在虚拟寄存器中的状态写回 CPU 物理寄存器,从而控制流正式跳入 Landing Pad。 以示例中的实际二进制编码为例,每一行为 4 字节字长,包含偏移、DWARF 省略标记、位置无关编码等。解码后可得:Start 为 0,Length 为 26(或十六进制 0x1A 左右),Landing Pad 为 30。由于当前 `bar` 函数内的执行位置在 `bar + 14`,恰好处于 0 到 26 的区间内,因此将 Landing Pad 设置为 30 并直接跳入对应的汇编清理分支。 跳转到 Landing Pad 后的代码首先通过指令 `add sp, #4, r0` 将栈上对象的地址存入寄存器 `r0`。在 ARM 调用约定中,`r0` 到 `r3` 用于传递函数入参,因此这一步是在为调用对象的析构函数准备 `this` 指针。调用完析构函数后,控制流会调用 `__cxa_cleanup` 恢复异常对象、重置帧状态并继续异常抛出流程。Khalil 强调,一旦执行了析构函数,该函数栈帧之下的栈内存就进入未定义状态,因为析构函数自身也会压栈、修改栈内存,任何试图保留在栈底层的数据都可能被破坏。同时,一个函数内可以根据抛出异常时执行到了哪一步,存在多个不同的 Landing Pad,从而决定是仅析构单个对象,还是按逆序依次调用多个对象的析构函数。 ### [50:23](https://www.youtube.com/watch?v=bY2FlayomlE&t=3023s) 展开栈帧与匹配 Catch 块:类型表与动作表 析构完成后进入 `bar` 函数的实际栈展开(Unwind)。运行时利用 LSDA 偏移跳过首个指针,直达 personality 0 的展开指令。指令指示将栈指针增加 12 字节(回收局部对象占用的栈空间),随后从栈上弹出 `r4`、`r5` 以及链接寄存器(LR),彻底清除 `bar` 的栈帧。此时,程序计数器成功回退并指向了上层调用者 `foo`。 在 `foo` 中,展开系统需要解析类型表(Type Table)与动作表(Action Table): - **类型表(Type Table)**:包含所有被抛出类型的唯一指针集合。若类型表中存在空指针(`nullptr`),则代表捕获所有异常的 `catch (...)`。 - **动作表(Action Table)**:对每一个 `try` 块所对应的连续 `catch` 分支链进行编码。表中的核心标识是“Filter Number”(过滤号),它本质上是一个访问类型表时的**负向索引**。运行时定位到类型表的末尾,以负偏移反向索引读取类型指针。 - **动作链遍历(Next Record)**:动作表中除了当前记录的过滤号外,还包含指向下一条记录的相对偏移(Next Record)。运行时依此向前回退指针持续查找,直到 Next Record 为 0(链表终点)。若遍历到底仍无匹配,说明该函数无需捕获,直接继续向上展开。 很多人误以为在进行 catch 匹配时需要像 `strcmp` 那样做耗时的字符串比对,但 Khalil 澄清道:“在没有复杂继承层级的情况下,只需要直接比对两个 `std::type_info` 的指针地址是否相等即可;若是 catch-all,其编码值对应为 0,规则同样十分简单。” ```mermaid flowchart LR accTitle: Type Table and Action Table Matching accDescr: 动作表通过负向索引查询类型表,比对抛出的异常指针并引导控制流走向。 subgraph Action_Table["动作表 (Action Table)"] AR["Action Record"] --> FN["Filter Number (例如: 1)"] AR --> NR["Next Record 偏移"] end subgraph Type_Table["类型表 (Type Table, 负向索引)"] TT_End["类型表末尾 (Offset 13)"] TT_End -->|"-1 * sizeof(void*)"| T1["TypeInfo* 指针 (FooType)"] end FN -->|"负向索引"| T1 T1 --> MATCH{"与抛出异常的<br/>TypeInfo 指针相同?"} MATCH -- "匹配成功" --> JUMP["设置 r0 = 异常对象<br/>设置 r1 = Filter Number<br/>PC = Foo + Landing Pad | 1"] MATCH -- "不匹配" --> NR ``` 当找到匹配的 catch 块后,运行时将 `r0` 赋值为异常对象地址,将 `r1` 赋值为匹配到的 Filter Number,将程序计数器(PC)设置为函数首地址加上 Landing Pad 偏移(同样最低位 OR 1),恢复寄存器并跳入汇编代码。 在 Itanium ABI 体系下,`catch` 语句块在编译底层本质上就是 `switch-case` 语句,而 Filter Number 充当的就是 switch 的条件判断值。汇编中通过比对 `r1` 与立即数 1(`cmp r1, #1`)决定跳转: 1. 若匹配,代码顺理成章进入分支,调用 `__cxa_begin_catch`。该函数将 `r0` 中的异常对象标记为已处理,并返回异常对象的内容指针;在用户代码块执行完毕后,调用 `__cxa_end_catch` 执行异常对象的析构与释放。 2. 若 Filter Number 不匹配(例如异常类型属于后续的其它 catch 分支),则通过分支指令(`bne`)直接跳向下一轮展开入口。 至此,GCC/ARM 平台下基于 Itanium ABI 的异常处理全流程闭环。 - 类型表中的 `nullptr` 或 Filter Number 为 0 均被用作 `catch (...)` 的通配符匹配。 - 动作表中的 Action Record 值为 0 专用于通知运行时仅调用析构函数(Unconditional Cleanup),非零值通过偏移映射到实际类型检查链。 - Landing Pad 内部的流程为:通过 `r0`~`r3` 准备 `this` 调用析构 -> 调用 `__cxa_cleanup` 恢复上下文 -> 继续调用 unwind 展开,或调用 `__cxa_begin_catch` 进入业务错误处理代码。 ### [55:00](https://www.youtube.com/watch?v=bY2FlayomlE&t=3300s) 异常的空间开销评估模型与假设基准 彻底弄清异常机制的底层实现后,便能够精确量化异常处理所需的 Flash/ROM 空间开销。Khalil 建立了一个严格的理论与实证模型,并明确了三项基准假设: 第一,所有被比对的代码必须开启尺寸优化(例如编译选项 `-Os` 或 `-Oz`),“否则如果不是针对代码尺寸进行编译,这种基准测试就毫无意义。” 第二,绝大多数工程函数编译后的二进制机器码长度极少超过 16 KB。为了支撑该假设,Khalil 分享了他过去对大型开源项目 Firefox 进行实际数据挖掘的结果:在分析 Firefox 完整的 Debug 版本符号表与所有函数体积后,在统计出的数十万个函数中,体积超过 16 KB 的仅有 253 个,而低于 16 KB 的函数高达 325,000 个。这证明在现实开发中,函数机器码尺寸超过 16 KB 是极其罕见的极端情况。 第三,在每个函数内部,需要析构的对象数量与 catch 块数量之和上限设定为 63。同时,所有字段尽可能采用 LEB128(ULEB128)编码,这是 DWARF 与异常表中最紧凑、最具空间效率的变长整数存储机制。通过这套约束基准,即可推导出异常数据表与展开代码所带来的精确常数开销。 ### [56:03](https://www.youtube.com/watch?v=bY2FlayomlE&t=3363s) 汇编层面的异常元数据与展开开销测算 Khalil Estell 深入剖析了汇编指令与底层 ABI 结构,逐步拆解异常处理中各项元数据和指令的具体字节大小。根据 ABI 规范,每个函数的异常索引(Exception Index)固定占用 8 字节,Personality 数据的索引占 4 字节,较长的数据段则受限于特定格式,最多占用 12 字节。 对于 `catch` 块的实现,由于其本质是一个 `switch-case` 结构,编译器需要生成多条指令: - `cmp`(比较指令):占用 2 字节。 - `bne`(不相等跳转指令):占用 2 字节。 - 调用 `__cxa_begin_catch` 的 `bl`(带链接跳转指令):占用 4 字节。 - 调用 `__cxa_end_catch` 的 `bl` 指令:占用 4 字节。 这意味着,代码中编写的每一个 `catch` 块大约都会产生 12 字节的代码体积开销。 对于包含对象析构的清理区域(Cleanup Regions),体积计算公式为 $6D + 4$(其中 $D$ 代表需要析构的对象数量)。每个析构调用需要 2 字节来设置栈或入参寄存器,加上 4 字节的 `bl` 析构跳转指令,每个对象共计 6 字节;析构完成后还需要一条 4 字节的跳转指令去调用最终的清理函数(如 `__cxa_end_cleanup`)。 对于动作表(Action Table),其开销为 $2 \times A$($A$ 为 `catch` 块的数量)。由于有符号 LEB128(Signed LEB128)在单字节范围内的取值仅在 -64 到 63 之间,在既定的架构约束下,过滤器编号(Filter Number)不会超出这个范围,因此只需 1 字节即可表示该编号;同时,下一条记录的偏移量永远不会超过 -3(恒为 -3),保证其始终占据 1 字节。类型表(Type Table)的开销则是 $4 \times T$ 字节,其中 4 字节对应指针大小,$T$ 为函数内捕获的唯一类型数量。 调用点表(Call Site Table)的大小在 $4 \times C$ 到 $7 \times C$ 字节之间($C$ 为调用点数量)。这是由于 16 KB 的函数体积约束:两个无符号 LEB128(ULEB128)组合可覆盖 $2^{14} = 16\text{ KB}$ 的寻址范围。只要单个函数的代码体积不超过 16 KB,调用点的起始地址、长度以及 Landing Pad 的起始位置都不会超过该界限,因此调用点与动作记录的编码长度得到了严格收敛。 ```mermaid flowchart TD accTitle: C++ 异常元数据与代码尺寸开销构成 accDescr: 展示异常索引、清理区域、Action Table 及 Catch 代码块的具体字节开销分布。 subgraph MetaData["元数据结构 (Metadata Table)"] Idx["异常索引 (Exception Index)<br/>8 字节"] Pers["Personality 指针<br/>4 字节"] Act["动作表 (Action Table)<br/>2 × A 字节"] Typ["类型表 (Type Table)<br/>4 × T 字节"] end subgraph Assembly["汇编代码开销 (Assembly Instructions)"] Catch["单个 Catch 块<br/>~12 字节 (cmp + bne + begin + end)"] Clean["清理区域 (Cleanup Region)<br/>(6 × D + 4) 字节"] end ``` - 包含 0 个析构函数、0 个 `try-catch` 块的平凡可展开函数(Trivially Unwind Functions),元数据开销仅需 8 字节。 - 具有单析构作用域的函数,总开销约为 34 字节(包含 8 字节入口、5 字节头部、4 字节 Personality、4 字节调用表、3 字节字对齐填充以及 10 字节 Landing Pad)。 - 若函数中包含 `try-catch` 块,综合各项公式测算,其最小开销将达到 47 字节。 --- ### [01:00:20](https://www.youtube.com/watch?v=bY2FlayomlE&t=3620s) 返回值错误处理的体积膨胀:`std::expected`、`bool` 与 Rust 在对比了异常的开销后,Khalil Estell 将视角转向基于返回值的错误处理方式。他给出一个基准示例:一个名为 `none` 的函数顺序调用了三个可能失败的函数,内部没有任何显式的错误检查逻辑,整个函数在汇编下仅占大约 8 条指令。 随后,他展示了改用现代 C++ 推荐的 `std::expected` 进行错误处理的代码,甚至加上了 `[[unlikely]]` 属性以期优化分支布局。然而反汇编结果显示代码体积显著膨胀:对于每一个错误检查点,编译器首先执行一条 `bl` 指令调用目标函数,接着从栈指针处执行 4 字节的 `ldr`(装载字指令)读取返回的状态对象,最后紧随一条 2 字节的 `cbz`(为零则跳转)指令将执行流分流至错误返回路径。也就是说,每一个 `std::expected` 错误检查都会产生至少 6 字节的机器码代价。 若将返回值简化为最原始的 `bool` 类型,开销略微下降,但每次检查仍需至少 4 字节。Khalil 解释说,尽管某些条件分支指令可能只占 2 字节,但由于它们能够跳转的偏移范围非常窄,一旦代码稍长,编译器就无法重复使用短跳转,因而必须退回到完整的 4 字节分支序列。 即使转向以类型系统和无额外运行时开销著称的 Rust,其 `Result` 类型的汇编表现也未能幸免。Rust 代码在调用多个易出错函数时生成的机器码与 C++ 的 `std::expected` 几乎如出一辙,同样在每次调用后都背负着显式的比对与跳转指令开销。虽然当 Rust 的 `Result` 载荷变为 `u64` 相比于 `u32` 时编译器触发了某种特殊的寄存器优化,体积压缩到了接近 `bool` 的水平,但“每个调用点必须付出至少 4 字节检查开销”的底层现实没有改变。 --- ### [01:02:39](https://www.youtube.com/watch?v=bY2FlayomlE&t=3759s) 分布式与集中式错误处理的体积权衡 基于前面的量化数据,Khalil Estell 提出了“分布式错误处理”(Distributed Error Handling)与“集中式错误处理”(Centralized Error Handling)的概念划分。 分布式错误处理将错误处理逻辑散落在整个代码库的各个角落,包括充斥在业务代码中的 `if-else` 分支与输出参数。在此模式下,任何调用可失败函数的上游函数都面临两难选择:要么编写样板代码向上传播错误,要么就地处理它。 集中式错误处理则依赖全局机制来统一处理和传播错误。业务代码无需侵入任何特殊逻辑即可自动参与错误流转,由外部系统负责调度展开。Khalil 将其类比为现代邮政系统:“就像美国邮政局(USPS)或通用邮政系统一样,我们不需要每个人自己去跑腿送快递,而是由一个统一的系统来分发。”异常正是集中式错误处理的典型代表,它将“正常流程(Happy Path)”与“异常流程(Sad Path)”在代码生成和内存布局层面彻底分离。 Khalil Estell 由此抛出核心论点:“在代码体积方面,集中式错误处理方案相较于分布式错误处理具备明显优势。”他指出分支指令绝不是免费的,每一次调用都要付出 4 到 6 字节的代价。如果一个函数连续调用 5 个可能失败的函数,使用返回值检查就要消耗 20 到 30 字节的代码空间;而使用异常时,该函数若为平凡展开,仅仅需要一次性支付 8 字节的元数据开销。 尽管 `try-catch` 结构本身的成本较为昂贵,引入一个 `try-catch` 大致相当于 11 到 12 次带检查的函数调用开销,但软件工程的调用拓扑决定了绝大多数函数根本不需要捕获异常。Khalil 强调,在绝大多数实际工程中,传播错误的函数数量远多于实际捕获和处理错误的函数数量。尤其在嵌入式开发领域,绝大部分对象都是平凡结构,不需要非平凡析构函数,也不涉及堆内存清理。绝大多数函数因而属于可以被 8 字节元数据免费穿透的“平凡展开函数”。“分支不是免费的;每次使用你都必须为它们买单”,而异常恰恰利用集中式的元数据剥离了主干路径上的这一层累赘。 ### [01:05:12](https://www.youtube.com/watch?v=bY2FlayomlE&t=3912s) 现实案例与代码基底的异常开销估算 在嵌入式系统中,类似互斥锁(mutex lock)这种带有特殊析构行为的 RAII 资源通常使用得非常克制,只在真正需要的地方偶尔出现。为了弄清楚现实工程中异常处理的真实开销,Khalil Estell 提到几个月前他曾向 Jason Turner 询问是否有公开的大型开源项目可以作为参考,Jason 建议去看看知名音频编辑软件 Audacity。深入审查 Audacity 的源码后,数据令人大吃一惊:整个项目拥有 1,504 个 C++ 源文件,但其中只有 39 个文件包含 `catch` 块;全工程 177.7 万个函数中,仅有 50 个函数包含 `try/catch` 结构,整个 Audacity 处理异常元数据的内存占用加起来大约只有 15 KB。 ```mermaid flowchart TD accTitle: 典型代码基底函数分类与异常元数据分布 accDescr: 展示假设代码库中各类函数的比例划分及其对应的展开元数据开销 subgraph Functions["工程函数分布估算 (共计 900 个函数)"] F1["5% 包含 try/catch 块<br/>(平均 64 字节)"] F2["25% 构造非平凡对象/需清理<br/>(平均 46 字节)"] F3["55% 简单函数/平凡展开<br/>(平均 8 字节)"] F4["10% 叶子函数 Leaf<br/>(0 字节 / 不参与展开)"] end subgraph TotalCost["展开信息总开销"] F1 --> TC["计算元数据总和"] F2 --> TC F3 --> TC F4 --> TC UW["libunwind 基础开销: ~8,000 字节"] --> TC TC ==> SUM["总开销: 27,100 字节"] end ``` 为了让对比更具说服力,Khalil 采取了一种“极端偏向悲观”的假设模型:即便真实大型项目中包含 `try/catch` 的函数比例低至 0.28%,他直接在模型中将其放大到 5%;需要析构清理(allocate/cleanup)对象的函数设为 25%;无需清理的简单可展开函数占 55%;完全不调用其他函数的叶子函数(leaf functions)设为 10%。在尺寸分配上,他同样往宽裕处预估: - 包含 `try/catch` 的函数元数据理论最小值为 47 字节,按平均 64 字节计算; - 清理函数(cleanup)理论最小值为 34 字节,按平均 46 字节计算; - 简单展开函数通常只有内联展开指令,按 8 字节计算; - 叶子函数完全不参与异常处理,产生 0 字节元数据。 加上 GCC 默认 `libunwind` 运行时库约 8,000 字节的开销,这套包含了 900 个函数的代码基底总开销仅为 27,100 字节。 --- ### [01:07:19](https://www.youtube.com/watch?v=bY2FlayomlE&t=4039s) 错误码检查与异常的体积平衡点 当把这 27,100 字节的固定异常开销拿去和“以返回值形式传递错误”(Result/Expected 机制)进行等价换算时,两者的体积优劣立刻显现出来。如果假设每次错误检查(比如 `if (!res)` 并触发分支)在汇编层面上只消耗 4 个字节,那么 27,100 字节相当于允许你在全工程中做 6,775 次错误检查。在 900 个函数的规模下,平均每个函数内部调用其他易出错函数(fallible functions)的上限是 7.5 次——一旦调用的次数超过这个门槛,手写错误码检查的总二进制体积就会彻底超过启用异常机制的代价。 不仅如此,4 字节的检查成本纯粹是理论上最乐观的极限情况,而且它还没算上函数结尾处因提前返回而产生的函数尾声(epilogue)开销。如果改用标准库的 `std::expected`,每次检查往往需要 6 个字节,此时每个函数内的调用配额立刻缩减至 5 次;若再把提早返回带来的尾声代码折算 2 字节(共 8 字节),每个函数只需要进行不到 4 次函数调用,错误码机制的代码膨胀就会反超异常。Khalil 强调,开发中经常需要在一个函数里调用好几个底层接口,这种由于手工检查错误码而处处受限的感觉极其痛苦:“And this feels really restrictive because I've written a lot of code that... I like to call functions, I don't feel like being restricted from calling functions should be a thing when I'm trying to develop.” --- ### [01:08:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=4108s) 异常的核心本质:代码压缩机制 Khalil 进而总结了 C++ 异常为何能够实现更小固件体积的底层逻辑。首先,它极大地精简了调用可能失败的函数时的代码生成(codegen);其次,展开信息(unwind info)的体积是随着“函数定义数量”线性增长的,而不是随着“函数调用发生的次数”增长,在任何实际项目中,函数调用的总次数都远远大于函数本身的数量;第三,它消除了频繁的提前退出路径与尾声代码;第四,Itanium ABI 与 ARM 异常处理 ABI 历经多年优化,早已将展开表字节码高度压缩;最后,类似 `libunwind` 这样的运行时开销是一次性付清的固定成本(采用 Khalil 自行编写的展开器甚至只有 4.5 KB 到 4.7 KB 左右)。 | 对比维度 | 错误码 / `std::expected` (基于值的错误) | C++ 异常机制 (Zero-Cost Exceptions) | |:--- |:--- |:--- | | **代码体积扩展方式** | 随着**函数调用点**(Call Sites)线性增加 | 随着**函数定义数量**(Function Definitions)增长 | | **调用处代码生成** | 包含分支判断、状态提取、额外的局部变量 | 无额外检查逻辑,仅保持标准函数调用指令序列 | | **控制流开销** | 大量提前返回路径导致频繁生成函数尾声代码 | 正常路径直线执行,错误路径完全交由外部表驱动 | | **运行库固定体积** | 几乎为 0 | 需链接展开器(GCC 约 8 KB,定制版约 4.5 KB) | 更关键的是,目前的展开机制还远非其终极形态,仍有巨大的瘦身空间。例如去掉多余的 DWARF 编码、类型表(type table)以及调用点编码(call site table),甚至可以通过编写定制的运行时人格函数(personality function)进一步压缩。针对每个 catch 块都会调用 `__cxa_begin_catch` 的现状,如果将其换成无条件跳转指令,单条指令就能从 4 字节减到 2 字节。长期沉浸在底层汇编和元数据结构中的 Khalil 提出了他的核心论断:“After so much time looking at the disassembly, looking at the metadata, and working with these data structures, I came to one conclusion, and that is: C++ exceptions are code compression.” 对他而言,异常表并非占用内存的累赘,反而是一种高明的压缩方案。 --- ### [01:11:05](https://www.youtube.com/watch?v=bY2FlayomlE&t=4265s) 解密展开性能与异常体系的致命硬伤 面对那些“不在乎固件体积、只追求极致性能”的质疑,Khalil 剧透了他的下一场演讲内容:将 C++ 异常处理的耗时降低 88%。他指出,GCC 现存针对 ARM 架构的 `libunwind` 实现极为陈旧,全球大概只有 8 个人曾经维护过它,核心主干代码甚至停留在 25 年前无人问津。根据他的实测基准数据,在需要展开 6 层调用栈帧时,GCC 的展开机制比 `std::expected` 慢了整整 27 倍,而他优化后的定制版本仅慢 4.77 倍;当栈帧深度达到极端的 96 层时,GCC 慢 21 倍,定制版本则仅比普通函数逐级返回慢 2.56 倍。 ```mermaid flowchart LR accTitle: 供应商更新与无法推断的抛出异常困境 accDescr: 说明调用黑盒二进制函数时文档落后与无法静态获知抛出类型的问题 subgraph Problem["传统异常的黑盒困境"] API["调用外部接口<br/>firmware_api()"] -->|"不知道会 throw 什么"| Docs["翻阅文档<br/>(一年前更新)"] Docs -->|"不确定是否准确"| Vendor["邮件沟通供应商<br/>(耗时一周确认)"] Vendor -->|"升级版本: v5.10"| Void["未知行为/破坏契约<br/>再次陷入黑盒"] end ``` 然而话锋一转,Khalil 直面了异常机制长期为人诟病的深层原因,毫不掩饰地直言:“Exceptions effing suck.” 面对一个第三方黑盒函数,程序员根本无法从代码签名看出来它究竟会抛出什么类型的异常;哪怕翻阅文档,文档也可能是一年前写好的陈旧信息;即使给供应商发邮件来回沟通了一周终于确认了错误类型,一旦固件升级到 `v5.10` 补丁版本,一切又重回混沌。正是这种不可预测的黑盒行为,让广大嵌入式开发者对异常敬而远之,迫使大家重新退回到局部显式处理错误的阵营。针对这一点,Khalil 在三部曲演讲的最后一讲中介绍了一款名为“Exceptions Insights Tool”的工具:在二进制汇编中,函数返回 `bool` 或 `expected` 往往极难追踪,但抛出异常一定会调用 `__cxa_throw`,这为静态分析工具捕获底层抛出的错误类型提供了突破口。 - Audacity 的 1504 个源文件中仅 39 个含有 `catch` 块,总计 69 个 `catch` 块,全工程展开元数据仅占用约 15 KB。 - 理论上 4 字节的错误码检查在 900 个函数中需要容纳 6,775 次校验,平均单函数支持 7.5 次调用即达开销平衡点。 - 将检查与返回开销增至 8 字节后,每个函数仅需调用不到 4 次易出错接口,异常方案即在体积上胜出。 - Khalil 开发的嵌入式定制展开器体积仅为 4.5 KB 至 4.7 KB,相比 GCC 原版展开时间缩短了 88%。 - GCC 的 ARM `libunwind` 核心实现已有 25 年未进行实质性现代化重构。 ### [01:14:20](https://www.youtube.com/watch?v=bY2FlayomlE&t=4460s) Exception Insight 工具的设计理念与实现路径 Khalil Estell 详细拆解了他正在构建的分析工具 Exception Insight 的底层工作原理。他直言不讳地指出,源码在分析全貌时往往是盲目的:“你的源代码什么都不懂,把你的二进制文件给我,给我你真正要去执行的东西。”该工具不依赖高级源码,而是直接解析 ELF 等二进制文件的 `.text`(代码)段。通过在机器指令层扫描每一次指向 `__cxa_throw` 和 `__cxa_allocate_exception` 的分支与跳转(branch and link),工具能够精确提取出程序中每一个被抛出对象的类型、分配体积以及触发位置。 ```mermaid flowchart TD accTitle: Exception Insight 工作流程 accDescr: 展示从二进制机器码到最终未捕获异常分析的整体流转步骤 subgraph BinaryAnalysis["二进制层解析"] ELF["可执行二进制文件 (ELF)"] --> TextSec["解析.text 代码段"] ELF --> ExIdx["解析异常索引表 (.ARM.exidx /.eh_frame)"] TextSec --> Calls["定位 __cxa_throw 与 __cxa_allocate"] end subgraph SimulationEngine["全局推演引擎"] Calls --> ThrowMeta["提取异常体积、类型与抛出点"] ExIdx --> CatchBlocks["绘制 Try-Catch 覆盖域与调用图"] ThrowMeta --> Sim["抛出模拟与路径推演"] CatchBlocks --> Sim end subgraph OutputReport["分析诊断报告"] Sim --> Path1["命中 main() / 未捕获栈轨迹"] Sim --> Path2["撞击 noexcept 导致 terminate()"] Sim --> Path3["被 catch 块正常处理"] end ``` 接下来,工具会深入读取异常索引区(如 ARM 的 `.ARM.exidx` 或 x86 的 `.eh_frame`),评估其中注册的每一个函数及其对异常展开的参与程度,进而准确定位整个固件中所有的 `try` / `catch` 块范围。利用用户提供的调用图(Call Graph)或工具未来自动生成的调用图,Exception Insight 将在二进制控制流中“模拟异常的抛出与冒泡过程”。通过这场静态推演,开发者能清清楚楚地看到:哪些异常会一直穿透到 `main()` 导致崩溃,哪些会意外撞上 `noexcept` 函数并触发终止,又有哪一些能被特定 catch 正确截获。Khalil 坚信这样的工具完全可行,并且现场已有学生专程赶来协助他推进这一开源项目。 ### [01:15:26](https://www.youtube.com/watch?v=bY2FlayomlE&t=4526s) 致谢导师、灵感来源与独立开源研究 在正式演讲即将结束之际,Khalil Estell 向给予他支持的关键人物表达了谢意。首先是他远道从加利福尼亚赶来现场支持的母亲,以及同为 C++ 开发者、协助他校对幻灯片的未婚妻 Malia LaGore。在技术思想层面,Khalil 重点致谢了 Herb Sutter。正是 Herb Sutter 当年关于静态异常与错误处理的经典演讲,激发了他对 C++ 异常底层机制的浓厚兴趣。 与此同时,C++ 之父 Bjarne Stroustrup 撰写的那篇名为《C++ 中的异常及替代方案》(C++ Exceptions and Alternatives)的论文也深深震撼了他。这篇论文旨在对 Herb Sutter 的轻量异常提议提出反思与制衡。其中 Bjarne 写下了一段直击痛点的话:“在 C++ 中,对于异常的性能以及最终代码的可靠性这两个课题,一直缺乏严肃深入的研究。”Khalil 表示,多年前正是读到这句话,才让他下定决心:“也许我可以成为那个去深入研究它的人。” Khalil 坦陈自己目前处于无全职雇佣状态,所有的代码量测、底层库优化以及 Exception Insight 分析器均由他独立开展。他公开了 GitHub 赞助渠道,并呼吁美国国家科学基金会(NSF)、美国能源部(DOE)等资助机构,或饱受异常体积与行为难以预测困扰的嵌入式企业提供赞助与场景反馈。他郑重承诺所有研发产物将彻底开源,因为他坚信这些工具和底层运行时优化能够“帮助推广 C++,让它在未来的嵌入式世界中保持旺盛的生命力”。 ### [01:17:44](https://www.youtube.com/watch?v=bY2FlayomlE&t=4664s) 现场问答:静态内存上限与航空级代码分支覆盖 在随后的现场 Q&A 环节中,首位提问者对异常替代传统错误码的思路大为赞赏,但对讲座开头提及的使用静态内存缓冲区实现 `__cxa_allocate_exception` 提出顾虑:如果系统在多线程或深层调用中耗尽了这个固定大小的静态内存池,会不会引发程序直接崩溃(terminate)?对此,Khalil 回应称在后续深入讲座中会有详尽讲解。他透露核心机制正是借助 Exception Insight:“有一种方法可以为你的代码可能抛出的每一种异常分配恰到好处的内存”,通过精确分析每个线程所需的异常块上限,工具能够直接算出一个确定的内存占用上界。提问者总结道:“这么说,Exception Insight 工具能帮我们计算出内存占用的理论上界”,Khalil 给予了肯定。 第二位提问者来自航空航天与机器人交叉领域。他指出,航空标准要求必须严格证明针对系统需求的代码分支覆盖率(Branch Coverage)。由于异常会在后台生成复杂的隐式控制流与展开表,他在工程中极其担忧无法追踪到底生成了哪些隐藏机器指令,以及如何将这些路径回溯映射到规范需求中。 Khalil 解释称,这正是 Exception Insight 采取二进制层分析而不是源码层分析的原因。商业工程往往包含闭源的二进制固件库(Binary Blobs),没有源码就无法得知闭源库内部是否会抛出意外异常。该工具的目标是生成一张全局的异常抛出与拦截全景图。在 CLI 模式下,只要传入最终的 ELF 文件,它就能直接生成所有“未被捕获(uncaught)异常”的调用栈回溯路径。系统工程师可以白名单配置哪些致命错误允许直接触发系统挂起/重启,而对于必须拦截的业务故障,工具会明确指出漏掉捕获的具体汇编位置;若编译时附带了调试符号,工具还能直接反解出具体的函数名与行号。 - 静态内存池大小不需要盲目猜测,静态分析能推导出所有异常对象最大尺寸及线程并发深度的上限。 - 航空航天高可靠性场景关注“意外终止”,工具通过分析二进制符号与调用图,专治闭源固件库(Binary Blobs)中未捕获的隐藏异常。 ### [01:20:51](https://www.youtube.com/watch?v=bY2FlayomlE&t=4851s) 单子式 std::expected 的现实局限与运行时库无缝替换 第三位观众提出了两个工程实践问题:其一,在基于值返回的方案中,团队是否尝试过 `std::expected` 的单子式(Monadic)操作(如 `.and_then()`、`.transform()` 等),学生开发者对此体验如何?其二,演讲中提到的轻量高效异常运行时是否计划合入上游编译器? 针对单子操作,Khalil 苦笑着透露,教学实践中学生们根本不愿意写那些单子式接口:“我的学生不会去用它,他们觉得那太让人困惑了,宁可直接写传统的 if 语句。”一旦在业务逻辑中充斥着大量的 Lambda 表达式配合 `and_then` 或 `or_else`,生成的底层机器码体积不仅没有优势,甚至还会略微增大,可读性也断崖式下跌。谈及编译器合入路线,Khalil 规划先向 Clang 提交实现,未来再推向 GCC。 随后,Khalil 补充了一个关键架构优势:他所开发的改进版异常运行时(Enhanced Exception Runtime)本质上是一个独立的替换库(例如通过 Conan 等包管理器接入)。它不需要等待编译器升级,依靠强行覆盖替换 GCC/libstdc++ 默认的弱符号实现,就能无缝接管异常处理。它完全保持 ABI 向后兼容,原有业务代码零修改即可获得固件体积骤降的红利。 在问答尾声,第四位观众询问该分析器是否支持跨动态共享库(Shared Libraries / `.so`)追踪。Khalil 表示团队已有理论设计方案,但尚未完全实操验证。由于共享库版本动态链接的不确定性,要得到绝对准确的“真实基准(Ground Truth)”,分析工具必须在目标机或具备完全一致系统镜像的环境中运行,以锁定加载的具体动态库版本。接着,另一位与会者谈及正在进行的 POSIX 抽象个人项目,现场继续探讨剥离庞大 C 运行时(libC)的可能性。 ### [01:22:57](https://www.youtube.com/watch?v=bY2FlayomlE&t=4977s) 纯 C++ 系统调用接口与轻量级异常库的合作探讨 在问答环节中,一位听众分享了自己的项目进展并向 Khalil Estell 表达了感谢。该听众提到,他目前正在尝试剥离底层的 C 库,并为其系统调用接口(system call interface)替换上一层纯 C++ 的抽象实现。然而,他坦言自己目前卡住了,因为要想走通这条路,需要做大量的前期调研与深入探索,而 Khalil 在这场演讲中展示的解剖与实现成果恰好为他扫清了许多迷雾。听到对方的反馈,Khalil 热情地道谢,并顺势询问对方是否会在自己完成这一套轻量级异常处理库后考虑使用它。听众回应说“很有可能”,并提议双方可以在演讲结束后线下对接进一步交流。 - 听众正致力于移除传统 C 依赖,构建直接面向纯 C++ 的系统调用层。 - Khalil 正在开发的轻量级嵌入式异常运行时库引起了同类底层开发者的浓厚兴趣。 ### [01:23:25](https://www.youtube.com/watch?v=bY2FlayomlE&t=5005s) 架构支持路线:从 ARM / RISC-V 到 x86 与 Windows 的考量 随后,另一位与会者提出了一个平台移植方面的核心问题:这套方案是否已经针对 x86、x64 以及 Windows 系统展开过研究。面对这个问题,Khalil 明确指出了开发团队的优先级规划,强调当前的首要重心是嵌入式世界最主流的 ARM 和 RISC-V 架构。关于桌面和服务器端常用的 x86 系列,Khalil 直截了当地指出:“对于 x86,那需要来自社区的支持,我们才能拥有资源去展开相关工作(for x86 that will require some support from the community for us to have the resources to work on that so just making that clear)。” 谈及 Windows 平台时,Khalil 幽默地表示:“至于 Windows,微软得主动联系我并跟我谈谈这事(As for Windows Microsoft's going to have to contact me and talk to me about that)。”他进一步解释道,如果涉及 MSVC 编译器,那是微软自成一套的专有生态与异常处理机制;但如果是面向 Clang 这类工具链的 Windows 环境,理论上存在可行的空间,不过目前尚未深入研究,无法给出确定结论。随着该问题的回答完毕,现场提问时间正式结束,Khalil 向全场听众致谢,全场在掌声中圆满落幕。 - **核心支持平台**:现阶段绝对优先支持 ARM 和 RISC-V 架构。 - **x86 / x64 支持条件**:需要依赖社区提供资源与赞助,才会投入人力进行适配。 - **Windows 兼容性**:MSVC 具有高度专有的异常实现模型;基于 Clang 的 Windows 环境可能存在适配潜力,但仍需进一步调研。 ## Notable quotes > [01:08:28](https://www.youtube.com/watch?v=bY2FlayomlE&t=4108s) After so much time looking at the disassembly, looking at the metadata, and working with these data structures, I came to one conclusion, and that is: C++ exceptions are code compression. — Khalil Estell > [01:02:39](https://www.youtube.com/watch?v=bY2FlayomlE&t=3759s) 在代码体积方面,集中式错误处理方案相较于分布式错误处理具备明显优势。 — Khalil Estell > [23:04](https://www.youtube.com/watch?v=bY2FlayomlE&t=1384s) 对于足够大的项目,异常在代码体积上更小,且在未抛出异常时的执行速度更快。 — Khalil Estell > [08:50](https://www.youtube.com/watch?v=bY2FlayomlE&t=530s) 设置类似 errno 的机制并不是个好选择,大家对此基本都有共识;而在内部维护变量、要求调用方另行调用离线函数去查询状态,也丝毫不比 errno 强到哪去。 — Khalil Estell > [39:43](https://www.youtube.com/watch?v=bY2FlayomlE&t=2383s) 展开指令不需要做太多事情,它们只需要减栈、弹出一组寄存器,仅此而已;因此只需要大约 1 个字节的信息就能表示它们,相当于模拟函数返回过程的高压缩版本。 — Khalil Estell ## Mentioned Khalil Estell, Herb Sutter, Bjarne Stroustrup, Jason Turner, Patrice Roy, Ben Craig, Malia LaGore, Google, Google Pixel Watch, Google Pixel Buds, San Jose State University, ISO C++ Committee, DigiKey, Embedded Template Library (ETL), Audacity, Firefox, USPS, Microsoft, National Science Foundation (NSF), Department of Energy (DOE), ARM, RISC-V, GCC, Clang, MSVC, Rust, Exception Insight, sjsu-dev2 ## Video description https://cppcon.org CppCon 2024 Early Access: https://cppcon.org/early-access Access All 2024 Session Videos Ahead of Their Official Release To YouTube. At least 30 days exclusive access through the Early Access system. Videos will be released to the CppCon channel on a schedule of one video per business day, with initial releases starting in November. --- C++ Exceptions for Smaller Firmware - Khalil Estell - CppCon 2024 --- For years, developers have overlooked a powerful tool for reducing binary size: C++ exceptions. Join me on a deep dive into the world of exceptions and discover how they can be harnessed to create more space efficient firmware. We’ll explore the requirements and best practices of embedded development, and show what is required to use exceptions in that environment. By the end of this talk, you’ll have a thorough understanding of how exceptions are handled, what their space costs are, and how exceptions compare to functional errors as values. --- Slides: https://github.com/CppCon/CppCon2024/blob/main/Presentations/Cpp_Exceptions_For_Smaller_Firmware.pdf Work at Hudson River Trading (HRT): https://tinyurl.com/safxfctf --- Khalil Estell Khalil is a ISO C++ Committee Member and has extensive experience writing production firmware. --- CppCon is the annual, week-long, face-to-face gathering for the entire C++ community. The conference is organized by the C++ community for the community. You will enjoy inspirational talks and a friendly atmosphere designed to help attendees learn from each other, meet interesting people, and generally have a stimulating experience. Taking place this year in Aurora, Colorado, near the Denver airport, and including multiple diverse tracks, the conference will appeal to anyone from C++ novices to experts. Annual CppCon Conference - https://www.cppcon.org https://www.linkedin.com/company/cppcon https://x.com/cppcon https://www.facebook.com/CppConference https://www.reddit.com/r/cppcon/ https://mastodon.social/@CppCon --- Videos Filmed & Edited by Bash Films: http://www.BashFilms.com YouTube Channel Managed by Digital Medium Ltd: https://events.digital-medium.co.uk --- #exceptionhandling #firmware #firmwareupdate #cpp #cplusplus #cppcon #cppprogramming #cplusplusprogramming #softwaredevelopment #softwareengineering #coding #code #technology #programming [Source](https://www.youtube.com/watch?v=bY2FlayomlE)

Source quotes

[68:28] Khalil Estell: After so much time looking at the disassembly, looking at the metadata, and working with these data structures, I came to one conclusion, and that is: C++ exceptions are code compression. [62:39] Khalil Estell: 在代码体积方面,集中式错误处理方案相较于分布式错误处理具备明显优势。 [23:04] Khalil Estell: 对于足够大的项目,异常在代码体积上更小,且在未抛出异常时的执行速度更快。 [8:50] Khalil Estell: 设置类似 errno 的机制并不是个好选择,大家对此基本都有共识;而在内部维护变量、要求调用方另行调用离线函数去查询状态,也丝毫不比 errno 强到哪去。 [39:43] Khalil Estell: 展开指令不需要做太多事情,它们只需要减栈、弹出一组寄存器,仅此而已;因此只需要大约 1 个字节的信息就能表示它们,相当于模拟函数返回过程的高压缩版本。

Source quotes

[68:28] Khalil Estell: After so much time looking at the disassembly, looking at the metadata, and working with these data structures, I came to one conclusion, and that is C++ exceptions are code compression. [62:39] Khalil Estell: Centralized error handling schemes have an advantage in terms of code size compared to distributed error handling. [27:22] Khalil Estell: From my experience, just to be real, I see more error propagation code than I see error handling code.

Original transcript

[0:08] so everyone I want to teach you guys [0:09] about how you can make your binary [0:11] smaller by using C++ [0:13] exceptions so a little about me if [0:15] you've seen the uh the name Cy anywhere [0:18] around that's probably me I use that [0:20] name that username for almost everything [0:22] so a little B about myself graduated [0:24] from San Jose State in 2017 I'm an ex [0:27] googler I worked on a couple of products [0:29] that you might you have heard of uh the [0:30] Google pixel watch the original I was [0:32] the directly I was the directly [0:34] responsible engineer for the haptics and [0:36] the bootloader and the speaker and the [0:38] microphone and eventually when I got [0:40] tired of that I moved over to the Google [0:42] pixel Buds and worked on code [0:43] optimization so we could fit more [0:45] features and um bug fixes into the [0:48] device I'm also a volunteer at San Jose [0:50] State University and I primarily help [0:51] out with the robotics team at San Jose [0:53] State and I'm also an isoc Plus+ member [0:55] since like last [0:57] year before we go any further I want to [0:59] go over the motivation for why this talk [1:02] even exists and how I got to this [1:04] point so about six years ago I was [1:07] teaching a class called Computer [1:08] Engineering 146 it's intro to embedded [1:11] systems in this class I developed a [1:13] formare framework for my students It Was [1:14] Written in C++ uh we followed the C++ [1:17] core guidelines as close as we could and [1:19] uh just a little shout out this is a [1:21] board that I designed here for my [1:23] students to use for the [1:25] development the first thing that I teach [1:27] a lot of my students is that when you're [1:28] developing for microcontrollers and [1:29] doing metal development you don't have [1:31] an operating system that means you don't [1:33] have access to F open you can't you [1:34] don't have a file system you don't have [1:35] networking you don't have a terminal to [1:37] print to you don't even have a clock or [1:38] even threads if you want any of those [1:40] things need to implement them yourself [1:42] or bring in a library that can do it for [1:44] you but there's another little caveat to [1:46] this you can bring in all those [1:48] libraries but there's only so much [1:49] memory available here is a Google search [1:52] or a bit of a search on a website called [1:54] Digi key if you know embeda systems you [1:56] know this website you can buy a variety [1:58] of different electronic components [1:59] including microcontrollers on these if [2:01] you look at the devices that are less [2:02] than a megabyte in size get about 33,000 [2:05] of them look at the devices that are [2:07] above megabyte you get about 5,000 so [2:08] there's not that many devices with more [2:10] than a megabyte worth of flash [2:12] memory the same goes with ram when it [2:15] comes to your RAM it's only about 100 if [2:17] you look for things that are below 100 [2:19] kilobytes it's about 31,000 of them [2:21] above that though 7,000 so again not [2:23] that many devices that support really [2:25] large amounts of memory and [2:28] RAM so the so little memory how do we [2:31] prevent running out of memory or memory [2:33] fragmentation in general we will try to [2:35] avoid or if not completely ban dynamic [2:38] memory [2:39] allocation uh so free new calic delete [2:41] all those things we tend to try to not [2:43] use that also means containers that use [2:46] those mechanisms are also kind of off [2:48] limits although I've been told many [2:49] times about many other embedded embeded [2:51] developers well hold on hold on we do [2:52] use dynamic memory allocation just at [2:54] initialization and that is true you can [2:56] init all your stuff at first so long as [2:58] you're not deleting or reallocating [2:59] later [3:01] on if you need containers in the [3:03] embedded space that either own their own [3:05] memory and don't keep allocate you can [3:07] use the embedded template Library it's [3:08] called [3:10] ETL but we have a standard in place [3:13] Vector which was voted into C++ 26 at St [3:15] Lewis about a couple months ago and this [3:17] would be your example of something in [3:18] the standard library that you could use [3:19] in embedded systems that owns its own [3:21] memory and is a try push back so you can [3:24] actually test to see if you've exceeded [3:26] your allocated [3:28] memory so given those constraints let's [3:30] talk about the reasons why firmware [3:32] developers don't use C++ [3:34] exceptions number one requires malic got [3:37] to dynamically allocate your exceptions [3:39] you know that's kind of a bummer you [3:40] know that sucks so you know that's one [3:41] of the issues I've also heard from other [3:43] people that you know it's unbounded [3:45] memory usage if you start using [3:46] exceptions you can't tell how much [3:47] memory is going to [3:49] allocate there's also the issue of [3:51] increased binary sizes uh I had one [3:53] individual tell me that if you use [3:55] exceptions it brings in the entire STL [3:57] all string Vector everything that get [3:59] brought into your binary can't handle [4:01] that it's too much memory being used up [4:04] it also everyone a lot of people know [4:05] about this it brings in runtime type [4:07] information and that could use up a lot [4:09] of valuable space that we don't really [4:10] have on these embedded [4:12] systems also there's these things called [4:13] the exception tables and even the [4:15] exception code itself is quite [4:16] complicated so that ends up using up a [4:18] lot of memory and on the time side we [4:21] have the non-determinism issue mostly [4:22] from like Dynamic cast maybe some other [4:24] areas I keep this one down here secret [4:27] just because if I tell you it might give [4:30] too much off for my next following talk [4:32] so I'm not going to tell you too much [4:33] about that and finally it could be slow [4:36] uh there's binary searches that are [4:37] required for this there's the frame [4:39] unwinding which can be complicated and [4:40] there's a frame evaluation which can [4:42] also be complex which could be costly in [4:44] terms of [4:45] time but let's get back to class for a [4:47] sec so at in my class I built a whole [4:49] bunch of different libraries for my [4:50] students and also built these interfaces [4:52] that they could use to guide them [4:53] through developing drivers we're going [4:55] to go over one in particular a very [4:57] basic one called the output pin so if [4:59] you've never seen a microcontroller [5:00] before in your life here we have this [5:02] little black box with a little bit of [5:03] lettering on it and these little metal [5:05] extrusions that come out we call those [5:06] microcontroller [5:08] pins um some of which might be output [5:11] pins with an output pin you can set its [5:13] state to either a low voltage or to a [5:15] high voltage in this case the voltage is [5:17] low so there's no potential difference [5:18] between the ground position and this pin [5:21] therefore no current flows set it to a [5:23] high voltage now there's a potential [5:25] difference current can flow LED turns on [5:27] great we can now turn it off on and off [5:29] an LED [5:30] you can use output pins for a variety of [5:31] things but the basic thing that people [5:33] usually show is an [5:34] LED so to me I'm like all right well if [5:37] I'm building an interface for this I [5:39] mean if I'm just going to be able to [5:40] turn something on and off I just need a [5:41] simple function level give it a true [5:43] false value a bu if it's true turn it [5:45] high it's false turn it low simple [5:49] enough but when I made this I wasn't [5:51] sure if returning void was okay I was [5:54] thinking to myself well what would I [5:55] need to return well is there no [5:56] information I'm going to get out of this [5:57] maybe an error state of some sort but [6:00] what type of Errors could come from this [6:01] I built so many different libraries for [6:03] microcontrollers that most of the time [6:05] this particular function is infallible [6:06] there's no way it's really going to [6:09] fail but to kind of show how it could [6:11] fail let's introduce another concept [6:13] embedded systems i s c if you know [6:16] embedded systems you've interacted with [6:17] this countless times if you don't I'm [6:19] going to go over a quick primer iqu C is [6:22] a bus protocol it uses two pins in order [6:25] to transmit data between multiple [6:26] devices in ice C there's the concept of [6:29] a controller and a [6:31] device where the controll is able to [6:33] initiate conversations on the bus and [6:35] the devices can only speak when spoken [6:37] to those conversations start off like [6:40] this the controller will create a start [6:42] signal this will create an intention [6:44] signal to everyone else saying Hey [6:45] listen up I'm going to speak out look [6:47] for your address then we present an [6:50] address along with a read and write [6:51] signal the device that corresponds to [6:54] that address must acknowledge that [6:56] request and allow the transfer to [6:57] continue if the device is not on the or [6:59] can't handle that type of request then [7:01] it will not [7:02] acknowledge now the way the transfer [7:04] works is that after you've acknowledged [7:06] if the controller is writing to the [7:07] device we the controller will write a [7:09] bite the other device will acknowledge [7:11] writes another bite acknowledges so on [7:13] and so forth on the flip side if you're [7:15] reading from the device the controller [7:18] the device will write the bite onto the [7:19] bus and the controller will acknowledge [7:22] so on so forth until it's complete now [7:24] why I bring this up well there's a [7:25] couple errors that could come with iqu C [7:28] specifically address nonic knowledge and [7:30] an IO eror now for addess knic knowledge [7:34] in some cases not a big deal for example [7:36] I've had I've seen swappable systems [7:38] where you can plug in ITC devices and if [7:40] they're not there at the moment that's [7:41] fine if they are there later on that's a [7:43] that's that's okay but if you're writing [7:45] software for a drum and the sensory you [7:47] use to keep your orientation keep you [7:48] afloat just stops acknowledging you [7:51] you're going to Plum it to the ground [7:52] unless you have like a parachute or [7:53] something like that and then IO erors [7:55] can come from electromagnetic [7:56] interference they can also come from [7:58] other devices on the bus that are [8:00] erroneously controlling the lines in [8:01] some weird [8:02] way now how do we fuse these two [8:05] together W that's a good couch [8:08] um let's consider a thing called an IO [8:11] expander this is a device that you would [8:13] use for your microcontroller if you run [8:15] out of pins I want more IO I want more [8:17] output control pins but I do not have [8:20] the necessary pins on my device so I can [8:23] communicate over iqu C to this device [8:26] here and be able to tell it hey I want [8:27] you to set your PIN zero to a high or [8:30] low voltage But Here Comes the rub the [8:34] i2c side is a fallible interface and the [8:37] output pin is an infallible interface at [8:40] least the way that I wrote it [8:42] prior so kind of illustrate the problem [8:44] let's say we have a really simple [8:45] function called IO expander level it [8:48] returns back a bull it's true if it's [8:50] successful and false if it failed you [8:52] give it the pain you give it what [8:53] voltage level you want and you run from [8:56] there I have some implementation that's [8:57] going to do it but what do we we do when [8:59] we're not [9:01] successful well because we can't use [9:03] exceptions remember uh really isn't much [9:05] I can do I can set a global variable or [9:08] I can set some state within the object [9:10] and then have another function call that [9:11] object's function or call a status [9:13] function to see hey was this successful [9:15] the thing I don't like about that though [9:16] well setting up aror no is not a good [9:19] option we all kind of figured that part [9:20] out and I don't think that using a [9:23] internal variable and having to call an [9:25] out ofline function is as any better [9:27] than [9:28] Aro so I was like okay but is this is [9:30] this a real big problem so I considered [9:33] the use cases that my students might use [9:34] these output pins in let's say they're [9:36] going to control an LED well I think in [9:38] most cases controlling LED is not that [9:40] big a deal the led doesn't turn on I [9:42] mean maybe the customer's a little upset [9:44] but it might be a problem if that led is [9:46] like for your check engine light okay [9:50] how about for a heating element well if [9:52] it's used to control a chemical reaction [9:54] of some sort that could be a problem if [9:55] you can't get that to work properly and [9:58] then I thought well what about an [10:00] emergency cut off relay at the very [10:01] least you might not be able to do [10:02] anything about this but at the very [10:04] least we can tell you that something [10:05] went wrong when you tried to attempt [10:06] this [10:08] action so when I realized that this was [10:11] an issue I decided to switch all my apis [10:13] to use stood expected um I made my own [10:16] little type here where we have a status [10:18] which is the expected the expected type [10:20] is void and the air typees my little [10:23] enumeration and then here we return back [10:25] bu for those who are not familiar with [10:28] the expected it is a type that can be [10:30] either or a actual result or some error [10:33] type in the example here let's say we [10:36] are trying to get some version from some [10:37] particular device device is not [10:39] connected we can return back the [10:41] precondition has been U violated and use [10:44] standard unexpected to create the error [10:46] state of that object or you can just [10:48] return back the number obviously this is [10:50] like slideware it's very simple but [10:51] that's kind of the the [10:53] idea so then I thought to myself okay I [10:57] don't just write apis and just forget [10:59] about I want to think about how do my [11:00] students or how people who are my users [11:02] going to use my apis so I was like all [11:05] right well if I used exceptions this is [11:06] how my code look if I wanted to toggle [11:08] an LED pass it a pin pass it the amount [11:10] of time set it high wait a certain time [11:13] set it low wait a certain time we're [11:15] good if you said expected before you'd [11:17] expect that the code would look like [11:19] this next where for every single one of [11:21] these function calls I need to check to [11:23] see whether or not the status of that [11:25] return was successful otherwise return [11:28] back an unexpected value and propagate [11:30] up the error that you had that you [11:32] acquired here's another option it [11:34] doesn't exist in C++ currently but I was [11:37] really excited when I saw this it is the [11:40] eror propagator from p261 R1 it is it [11:44] uses a double question mark it's similar [11:47] to rust's question mark operator which [11:49] automatically propagates errors up your [11:51] stack I remember emailing the individual [11:54] who uh who was working on this and never [11:55] got anything back and maybe that was a [11:57] good thing because it would it would [11:59] have caused me to not go and do the [12:01] research that I did currently so I [12:02] didn't have this so what can I use a [12:05] macra there's no other real way in C++ [12:07] to do this and if you have a way please [12:09] tell me I'd like to know but I just came [12:11] up with a quick little macro SJ check [12:13] does all the checks for you looks almost [12:15] identical to the question mark question [12:18] mark option a little [12:20] bit so then I had to ask myself which [12:22] apis need to have return need to return [12:25] errors and the answer to that is almost [12:28] all of them because I have made [12:30] interfaces for Motors and look at this [12:31] here's a motor controller that talks [12:33] over I squ C all right and then what [12:35] about this there's a display that talks [12:36] over I squ C almost everything can be [12:38] represented or communicated over through [12:40] i squ c so I have this explosion of how [12:42] anything can be implemented with i squ c [12:44] therefore anything can have an air prop [12:45] gate from underneath it I could have [12:47] separated out the types to have one for [12:48] infallible and non-fall but I didn't [12:50] really like that [12:51] Paradigm so I moved almost everything [12:53] over to S [12:55] expected and here's what two and a half [12:57] years of doing this to my students [12:58] caused [12:59] here's what they thought number one they [13:02] hated all the if statements that they [13:03] had to write everywhere to the point [13:05] where my students got clever and started [13:07] void casting away all the [13:10] errors so much so that it was actually [13:13] kind of sad one of my students like was [13:14] spending spent like a week on this [13:16] particular problem and I looked at their [13:17] code and I was like hey do you're not [13:20] propagating the errors maybe this one [13:21] little area this one little area here [13:23] could be fixed up so we refactored our [13:24] code took a while but we factored all [13:26] the code toate all the errors and we [13:28] found oh yeah you you're using the wrong [13:29] address and that's the reason why you're [13:31] getting an error here but reason why [13:32] your code isn't working so this took [13:34] time out of out of her day the other [13:37] thing they were annoyed about was the [13:38] virality every one of my students was [13:40] like man I got to make every single [13:41] thing return back status or results [13:43] every single time if I want to call [13:45] anything that could potentially be [13:46] fallible I got to put it everywhere it's [13:48] everywhere and it touches [13:50] everything and I found that the macros [13:52] at least were quite noisy because what [13:54] you actually have to do when you look at [13:56] this code is do this try to blank out [13:58] the that have the macro so you can just [14:00] see into what's really happening in your [14:02] code you don't care about the sad path [14:03] you want to know at least what the happy [14:04] path does and then you can go over and [14:06] look at what the sad path does later on [14:08] so they had to do a lot of [14:11] this every single semester my students [14:14] would always ask me why not just use C++ [14:17] exceptions they're built into the [14:18] language they're meant for error [14:19] handling wouldn't that get rid of a lot [14:20] of the problems that we're dealing with [14:22] and I would verbally tell them this [14:24] diagram in its totality I bring up [14:26] articles I bring up things that I found [14:27] online I would even change the uh build [14:31] script to go from F no exceptions to F [14:33] exceptions and show it blow up and say [14:35] here there you go this is awful why [14:37] would you use [14:39] this most asked most most accepted this [14:43] but some continue to ask why like why is [14:46] it non deterministic why is it so slow [14:48] why does it have to be slow why does it [14:50] have to blow my Cod and why does it need [14:52] Heap and again I would figure out ways [14:54] to answer these questions and they'd [14:56] accept them but I started to not accept [14:58] them over time I did not like the idea [15:03] that there was that I was telling my [15:05] students this information about why you [15:06] shouldn't use something that I don't [15:07] know how it works it's a black box to me [15:09] I just know what I've been told from [15:10] other people and I started feeling [15:13] guilty for telling my students not to [15:14] use this thing because I didn't know [15:15] exactly why I couldn't use this thing so [15:17] I told myself you know what I'm going to [15:19] give exceptions a full Shake we're going [15:21] to go all the way with [15:22] exceptions so now we're going to make [15:24] exceptions work on arm I'm going to go [15:26] over the steps of what you have to do to [15:27] make it work on arm because it's not [15:28] trivial [15:30] here's what I want to do I have a [15:32] function called main it just calls a [15:34] function called start it throws [15:35] something I don't inline it because [15:37] otherwise the compiler will get smart [15:38] and get rid of all my code and I just [15:40] want to end up in this catch block here [15:41] I want to end up in this little return [15:44] area first barrier barrier you will [15:47] encounter is that exceptions are default [15:49] disabled when you run your program if [15:51] you download the G uh arm tool chain [15:53] it'll be disabled anytime you try to [15:56] throw you immediately terminate anytime [15:59] doesn't matter how valid your code is it [16:01] will always terminate the sad P sad part [16:04] about this is that arm will still bring [16:06] in all of the libraries and everything [16:07] else that it needs in order to run [16:08] exceptions except it just sets it up so [16:09] that it always terminates so it bloats [16:11] your code and does nothing as far as I'm [16:14] concerned yeah so here's how we so the [16:17] first thing I needed to do is break down [16:19] this barrier how do I enable exceptions [16:20] or figure out if it's even [16:22] possible they changed their instructions [16:24] a while back I used to list them off but [16:26] it's so different now that I can't [16:27] really explain it to you because I don't [16:28] know how to do it um but basically you [16:30] download the tool chain you find [16:32] whichever file has F no exceptions in it [16:34] and then you change it to F exceptions [16:35] you recompile it it should almost work [16:38] you need to do one last thing which is [16:39] to set set break it's a low-level cap [16:42] API which provides memory to [16:44] Malik uh once you provide that [16:47] exceptions [16:48] work but there's one other issue your [16:51] binary is now massive check this out [16:56] 150,000 bytes [16:59] I went from 2.6k and now my binary is [17:01] blown up this is I only have a half a [17:03] megabyte of of memory to do my [17:05] application and this is going to eat up [17:07] 30% so I can throw and catch nah I'm [17:08] good if that's how life's going to be [17:10] oops then I don't want to use this so [17:13] that was my issue but I was like but why [17:15] is it so [17:16] big so next barrier to break eliminating [17:19] the bloat see if there's any way we can [17:20] get rid of that bloat [17:22] somehow so I do the only thing that [17:24] makes sense to me and all the work I've [17:26] done in the past which is you [17:27] disassemble the code and you see you [17:28] look for anything that you did not write [17:30] that would probably come from um [17:33] especially since the code was so simple [17:34] it would come from the compiler at this [17:35] point and when you look in it might be a [17:37] little hard to see but there's this I [17:38] saw this API called Global string [17:40] callback [17:42] adapter I don't have any strings in my [17:44] code what is this doing here you look [17:47] around a little more and you see a pen [17:48] num I see an S print f i see a string [17:50] length what are these doing in my code I [17:52] didn't do any of this stuff in here and [17:54] then I find G new cxx veros terminate [17:58] Handler and I was like ah makes sense [18:01] that's what's happening does everyone [18:02] else have an idea of what's gonna happen [18:04] What happens if you throw an exception [18:05] and it hits [18:06] M you get this nice message terminate [18:09] cold after throwing an instance of my [18:12] air the code blow seems to be coming [18:14] from rendering this [18:16] string so check this out what can we do [18:18] about [18:19] this in five simple words probably [18:23] developers hate me in five simple five [18:24] simple lines you can eliminate all of [18:27] this so there's this thing in GCC and in [18:30] Clan called weak symbols there are [18:32] variables that you can create that can [18:33] be overridden by other code every [18:36] variable you've probably ever made has [18:37] always been a strong symbol you have to [18:39] mark them as weak in order to weaken [18:40] them but if you create the same variable [18:43] name the same symbol it'll override the [18:45] original in this case I overwrote the [18:48] the original terminate Handler with my [18:49] own which just does a little while loop [18:52] here that saves me 91% of my binary as [18:54] I'm down to 13 uh k at this point and [18:57] now I'm pretty okay with using exception [19:00] 150k nah it's not okay but 13 kiloby I [19:02] can do a lot with [19:05] that but we got another barrier still [19:07] uses dynamic memory allocation I still [19:09] had to set up SRE and I know Mal's [19:10] getting [19:11] cold so I felt bad I thought well I [19:15] could just use exceptions normally right [19:17] well it feels weird to teach my students [19:19] not to use except sorry dynamic memory [19:20] allocation and then my mechanism that is [19:23] in their St software development kit [19:25] uses dynamic memory location I want to [19:26] figure out if there's a way to at least [19:27] escape this now now they have to solve [19:29] it just at least Escape [19:31] it so I looked at what does the assembly [19:33] for a throw even look like where's the [19:35] allocation happening TR that there's [19:36] this function called cxa allocate [19:38] exception cool can I overwrite [19:42] that yeah you can same way is before [19:45] it's a weak symbol so you just overwrite [19:46] it with your own thing I'll go over why [19:48] there's like an offset here and like [19:49] that later on as part of the talk but [19:51] yeah I was able to at least give my own [19:53] memory for that allocator and now at [19:55] least I feel like I have control over [19:56] the memory and how much I can [19:57] pre-allocate for my exception handl [20:01] and finally the final barrier the part [20:03] that would make me feel okay with using [20:04] exceptions how can I handle the Spectre [20:07] of [20:08] rtti because the thing is my program is [20:10] quite small right now it's not doing [20:12] much of anything there's not that many [20:15] types but as the program as your [20:17] application starts to grow you'll have [20:18] more and more types more and more [20:20] abstractions this will start to grow and [20:22] grow so this is going to be a [20:23] problem so you have to figure out can we [20:26] disable rtti or can we do something [20:28] about it [20:29] here's what I did I just went well shoot [20:32] let's just change ftti to no rtti and [20:35] let's see what [20:36] happens and we're going to go over a [20:38] couple of these items and we're going to [20:39] have everyone vote on what they think is [20:40] the right answer so and just a little [20:43] note here real quick C++ exceptions [20:46] require rtti to work okay so number one [20:50] the compiler tells you that you cannot [20:53] use C++ exceptions with with rtti [20:55] disables get a little compiler error [20:57] saying nope can't do it [20:59] number two compiler strips all rtti [21:02] information out of your binary sorry [21:04] actually sorry sorry strips all RTI [21:06] information not involved with exception [21:07] handling in your binary and then number [21:09] three the compiler is completely silent [21:12] removes all the stuff and if you call [21:14] throw it's [21:15] U all right guys think you guys got it [21:20] who thinks is number one raise your [21:23] hands okay I got one hand interesting [21:26] okay I got two all right well think this [21:28] number [21:29] two interesting okay good number of [21:32] people who thinks is number [21:38] three do all are you guys all rations [21:40] are you guys all infiltrated cpon [21:42] because here's the thing why do you guys [21:43] all assume that C++ defaults to UB no [21:46] the answer is number [21:49] two yeah it was actually really F funny [21:52] coincidence it made it such that the [21:54] only types that actually need to be in [21:56] your binary are the ones that you throw [21:57] and I don't throw many types so it's not [21:59] that much memory and I think I have a [22:01] little extra time so I'll tell you guys [22:02] I don't have this in a slide I'll tell [22:03] you guys what the memory layout of rtti [22:06] is if you have a if you have an object [22:08] that inherits no types it takes up two [22:10] pointers if you have an object that [22:12] inherits one object it takes three [22:14] pointers if you have an object that [22:16] inherits n number of uh of of parents it [22:19] uses up three pointers plus n number [22:22] pointers for the number of uh classes [22:24] you inherit that's it it's not that much [22:26] to be honest in my opinion [22:30] so at this point I think eh Mal's not [22:32] really a big issue when it comes to this [22:33] I can override cxt allocate the whole [22:36] brings in the whole standard library [22:38] that comes from terminate and before it [22:40] used to bring in iio streams uh locals [22:43] uh basic strings so it's gotten a lot [22:44] better arm's done a good job uh I one [22:48] last thing that 150 kilobytes was back [22:50] in the day now it's like about 84 or [22:52] 87 and for [22:55] rtti I mean it's still there but it's [22:58] quite minimal so I was like that's not [23:00] that big of a reason not to use [23:01] exceptions in my opinion we still have [23:03] more items here to look [23:04] at but at this point I had seen enough I [23:06] was ready like this is great I can move [23:08] on with this I changed all of the sjsu [23:11] dev 2 code over to exception [23:13] handling moved all the macros got got [23:15] over all got rid of all that stuff but [23:18] in parallel I was also developing [23:20] another PR to completely remove [23:22] everything over to result types I was [23:24] going to compare and contrast in [23:26] whichever one I thought was going to [23:27] work out better in terms of code size go [23:29] with uh or just at least in usability [23:32] and there's a little message here you [23:34] probably can't see it actually might be [23:35] able to see that I have a little message [23:36] here saying it was decided to transition [23:38] to exception handling rather than error [23:40] return types for large enough projects [23:42] exceptions are small in code size and [23:44] faster when exceptions are not being [23:45] thrown here's what I [23:48] observed uh when I was building up put [23:50] this back in when I was building all my [23:53] demos to make sure that my code was [23:55] working appropriately as I tested all my [23:57] small demos like BL linkers and hello [23:59] worlds and that kind of stuff they went [24:00] up in size which I expected it's [24:02] exceptions should be bloaty right but [24:04] then I tried out like a couple other [24:05] projects like a MP3 player that I had my [24:08] students build as their like kot Oro as [24:11] their final project for the class and [24:13] those ones went down in size which blew [24:15] my mind it was the code was meant for a [24:17] micro that was 64 kilobytes I think the [24:19] binary size around 56 I lost the code so [24:21] I can't demo it [24:23] unfortunately but when I saw the code [24:25] get down to 40 something K my mind was [24:27] blown [24:29] and I was super excited but I also kind [24:31] of kept it to myself for years mostly [24:32] because one I didn't know if anyone [24:34] really care about this discovery or this [24:35] little bit information I was also afraid [24:37] of the rust Community coming after me [24:39] because if I showed anything positive [24:40] for C++ I was certain they'd come after [24:42] me and I'd get just completely washed on [24:44] any social media so I refrain from [24:47] bringing it up [24:49] until CBB con last year is when I [24:52] attended the iso C+ plus meeting in [24:55] 2023 uh yesterday around the same time [24:58] Patrice was having his talk about things [25:01] that are happening in [25:03] sg14 some water real [25:06] quick and he discussed the potential for [25:11] the polymorphic memory resource Vector [25:14] so standard PMR Vector if it could be [25:17] used for embedded systems or yeah [25:20] basically for embedded [25:22] systems and we had this discussion back [25:24] and forth about this and I told them [25:25] well there's some problems with Vector [25:27] in the fact that push back [25:28] can't tell you can't signal to you if [25:30] anything went wrong you could just [25:32] terminate at that point but a lot of I [25:34] don't think a lot of developers would be [25:35] happy with a panic if their Vector just [25:38] didn't couldn't [25:40] allocate um and then I tell them this [25:43] you know it's a shame that a beta devs [25:44] don't use C++ exceptions because it [25:46] would make their binary smaller and he's [25:48] like hold on hold on that goes against [25:50] all of common knowledge what do you mean [25:52] and I explained him the whole story that [25:53] I just explained to you guys just now [25:54] and he's like do you have any data for [25:56] this I'm like well I lost the old files [25:57] but I can synthesize it he like come to [25:59] Wednesday's ISO meeting and show off [26:02] what you have I like cool we'll do so I [26:04] did spend like a good portion of the [26:06] cpon time working on that and here was [26:10] my [26:11] hypothesis my idea is if I were to [26:13] observe this what could this mean well [26:15] let's think of the cost of result types [26:17] we can think about it as some a * n [26:20] where a is the average cost of the uh [26:23] checks per function call how many bites [26:25] it takes to do a a check for a function [26:27] call and n is the number of function [26:29] calls that you check the number of [26:32] checked function calls throughout your [26:33] entire code [26:34] base for exception handling it is BM [26:37] plus e where e is the cost of lib unwind [26:40] the Machinery you need to use to do the [26:41] propagation B is the average cost of [26:43] exception metad data and M is the number [26:46] of non- Le functions within your [26:49] code my assertion was that a of n was [26:52] going to be greater than B of M well [26:55] there's one you can't say that for sure [26:57] but you can at least say and I think [26:59] everyone can agree with me that you [27:01] probably assuming your code gets [27:02] stripped for all the functions you do [27:04] not use you probably call more functions [27:07] than the number of functions in your [27:09] code right so I thought all right well [27:11] at least those numbers are greater uh [27:14] have this uh uh uh equality or [27:17] inequality and I expected that I was [27:19] going to get something like this where [27:21] we have this little number of checks on [27:22] the x- axis and we have the binary size [27:25] increase on the y-axis there'd be some [27:27] Crossing Point where using exceptions [27:29] becomes more optimal in terms of code [27:31] size than using [27:32] results so here's the here's the [27:34] experiment I generated a whole bunch of [27:36] C++ code uh it would Define a whole [27:38] randomly Define a whole bunch of [27:39] different classes with and without [27:41] trivial destructors it would create a [27:42] sequence of functions that call each [27:44] other these functions would also be used [27:46] would also create objects and call apis [27:48] on those objects I had one file called [27:51] accept that would use acception handling [27:52] and another one called result. CVP these [27:54] two function these two [27:56] files will be isomorphic to each other [27:58] they do the same exact thing except for [28:00] the error propagation and I only made I [28:02] made sure the code was just dedicated to [28:03] just handling and focusing on error [28:05] propagation because from my experience [28:07] just to be just to be uh real I see more [28:10] error propagation code than I see error [28:13] handling code I think you guys will [28:14] agree with me as we go a little further [28:16] along here's an example of what the code [28:18] looks like um here we have uh some [28:21] function group it's randomly generated [28:23] it creates a volatile variable because I [28:25] need side effects otherwise a lot of the [28:26] stuff will get inlined even if you put [28:28] like no in line it gets real clever GC [28:30] is really clever just eliminating your [28:32] entire program if it's in a single [28:33] translation unit creating an object [28:36] calling some apis creting another object [28:38] calling some API so on so forth calling [28:39] another function same thing with [28:42] expected uh just with if statements to [28:45] check here's what I got when I created a [28:48] whole mess of these different files [28:49] compiled them down and looked at their [28:50] binary size red line is the result types [28:53] the blue line is the exceptions and the [28:55] yellow line is the Delta of size as you [28:58] go with more checks you probably can't [29:00] see the number at the top here this one [29:02] right over here I really love this [29:04] number because it's 867 which is really [29:06] close to the size of lib unwind if you [29:09] collect all that together and then here [29:11] we have -6.5 * X that would relate to [29:15] the cost per check for every check you [29:18] have inside your code not saying that it [29:19] really is 16.5 but this is how uh Google [29:22] Sheets calculated out to be looking a [29:25] little closer you can see here at this [29:28] point there that around 550 is checks [29:31] are enough to get you across that point [29:34] which isn't a lot in your code I've [29:36] definitely made Single classes with up [29:38] to 50 checks on its [29:41] own but I just want you guys to [29:43] understand none of those graphs Were [29:44] Meant to convince you at all I don't [29:45] think they're very convincing I think [29:46] they're there to show you [29:48] something if I want to convince you oh I [29:51] need to show you how exceptions work [29:53] from throw to catch so you guys can see [29:57] how all all of the different data [29:59] structures interplay with each other and [30:01] see what makes up the entire process so [30:03] let's get started things I'm not going [30:05] to cover here no nested exceptions and [30:08] uh anything other than table based [30:10] exceptions because to be honest I think [30:11] that table based exceptions are the [30:14] best all right consider the following we [30:17] have some fun we have some error we have [30:19] a function called fu fu calls bar and a [30:21] try catch bar creates an object that's [30:23] destructible we call baz baz throws an [30:26] error [30:29] there are three types of functions in [30:30] exception handling Leaf functions which [30:33] do not call any other functions contain [30:35] no destructors contain no contain no Tri [30:37] catch [30:38] blocks we have trivial or transparent [30:41] functions they call other functions they [30:44] do not contain any destructors and they [30:45] do not have any Tri catch blocks inside [30:47] of them and then finally we have [30:49] reentrant functions these are functions [30:51] that call other functions contain [30:52] destructors and have Tri catch blocks [30:54] they can be re-entered in order to [30:55] destroy objects or to handle errors [30:59] if we were to classify all of the [31:01] functions we have here Fu and bar would [31:03] be reentrant and baz would be [31:05] transparent There Are No Leaf functions [31:09] in here because it would be boring they [31:10] don't participate in exception handling [31:11] or air handling in general [31:14] so in order to understand exception [31:17] handling you need understand a little [31:18] bit about how the callon convention and [31:20] arm specifically works so don't want to [31:23] bore you too much but there are 16 [31:24] registers r0 to R4 or sorry r0 to R3 [31:28] three are used either as scratch [31:30] registers or as input parameters to a [31:32] function or return values from a [31:34] function whereas R4 to R11 are preserved [31:38] registers these ones are registers that [31:40] the function depends on to be the same [31:42] state when you when you call a function [31:44] and when you come back then we have a [31:46] stack pointer basically points out where [31:48] you are in the stack a link register [31:49] which contains the return address for [31:51] your function and your program counter [31:53] which tells you where you currently are [31:54] in your [31:55] application here's a little disassembly [31:58] don't get too you know but at the start [32:00] of Fu we're going to push R4 and LR what [32:03] this means is we're going to use R4 in [32:05] our function we don't want to destroy [32:08] what R4 was before from whatever called [32:09] Fu so we're going to save it onto the [32:11] stack we're also going to save the [32:12] return address on the stack so we can [32:13] pop it back later we can do some stuff [32:16] here we can go over and set let's say R4 [32:18] contain the answer to our our return [32:21] type for this or return value for this [32:22] function then finally we pop our four [32:25] back into its place and we pop the link [32:28] register back into program counter to [32:30] jump you back and that's how the calling [32:32] convention for arm basically works not [32:33] really much else beyond [32:35] that so now that we have this idea of [32:38] the registers in arm as well as like [32:40] basic Ram we can kind of come up with [32:42] what the state of the system would look [32:44] like and we have this little example [32:46] here we are at this location let's take [32:49] a look at [32:50] it in the itanium C++ AI when you call [32:54] throw it calls cxa allocate with the [32:57] size of the object you want to allocate [32:59] in which case this is a [33:00] bite uh all these all the stuff here is [33:03] for constructing that bite as well as [33:05] setting up the call to cxa [33:07] throw and the call to cxa throw takes [33:09] the pointer to the throne exception [33:12] takes a pointer to the type info for [33:14] that exception so you can actually [33:15] compare it against your catch blocks and [33:17] then also contains a pointer to the [33:19] destructor of that object it is null [33:21] pointer if it doesn't exist like if you [33:23] have something that's TR destructible [33:30] now cxa allocate works like this when [33:34] you use it it doesn't just allocate your [33:36] throne object it allocates enough space [33:38] for your throne object plus this thing [33:39] called the exception header the [33:41] exception header is used by the unwi [33:43] mechanism in order to remember the state [33:46] of the system and remember its progress [33:48] as it unwinds the parts within the [33:50] exception header are this a virtual [33:52] version of your CPU exceptions all they [33:54] really do is calculate the previous [33:57] state of what the CPU would have been if [33:58] you had taken a different pathway so we [34:01] need to keep and record all the states [34:03] of the CPU so we can reum jam it back [34:05] into the CPU later [34:06] on we also keep a pointer to the type [34:08] info because we need it for later on we [34:10] keep a pointer to the structure so we [34:11] can clean up afterwards and we have some [34:13] cache which can help you to speed up [34:14] things I want you guys to understand [34:16] that this is not the itanium ABI [34:17] exception header they have their own [34:18] definition for it this is my own for my [34:20] own [34:22] implementation you might be wondering [34:24] well why not just call them malic well [34:26] the reason why is for ABI compatibility [34:27] and flexibility see if I resize the [34:32] exception header in any way in the [34:33] future let's say we realize we want to C [34:34] more information to improve speed or [34:36] realize oh we can remove some registers [34:37] and we don't need to use those for uh [34:40] for the exception in Winding if we use [34:42] Malik and we bake that straight into [34:44] your binary and we update it later on [34:46] the size the header will be completely [34:48] off the on wi mechanism will be messed [34:49] up you'll have problems the other [34:52] benefit here is that you can do [34:53] everything in a single [34:55] allocation I having to allocate the [34:57] object then in throw allocate again for [35:00] the exception [35:01] header let kind of go into my version of [35:04] how cxa throw Works number one active [35:07] exception we need to set that so that [35:09] current exception can work it's a thread [35:11] local stored variable I know Ben um [35:13] Craig told me I need to get rid of TLS I [35:15] am working on it don't worry um next [35:18] thing we got to do is take the throne [35:19] type because we know that the exception [35:21] headers behind it all this function does [35:23] here extract exception header just [35:25] subtracts the throne types pointer [35:26] address by the size of the exception [35:28] header and that's what you have from [35:30] there you can set the type info you can [35:31] set the destructor and then finally we [35:34] can do a quick immediate unwind of cxa [35:36] throw to get oursel into the state of [35:39] whatever the CPU was for uh the collar [35:42] of this [35:43] function so after that we get something [35:45] like this where our program counter is [35:48] pointing to cxa throw that's where we [35:50] are currently our caller is bass that's [35:52] what called us our stat pointers [35:53] initialized and you see this little dark [35:55] yes that's visible cool see that little [35:57] dark orange area that is an indicator of [35:59] where the stack is currently and these [36:01] other registers R6 to R12 I don't care [36:03] about we're not going to use them in [36:05] this um demonstration R4 and R5 are [36:07] somewhat unknown and we have a couple [36:09] other items here who come from the input [36:11] parameters [36:13] here one of the things that it has to do [36:14] to unwind is to take the link register [36:16] and put it back in the program counter [36:17] so we can do our evaluation from that [36:18] point and then finally we raise the [36:23] exception from this we have a couple of [36:26] steps so number one we start off the [36:28] circle we go and we locate the exception [36:31] entry the exception entry is information [36:34] about a function about how you unwind it [36:36] about if there's any destructors that [36:37] need to be called as well as if there's [36:39] any Tri catches that need to be handled [36:41] or [36:42] evaluated from that bit of information [36:44] from that entry we can ask it hey should [36:46] you should we refuse to unwind such as [36:48] are you know accept if true terminate if [36:52] not check are you reentrant if false [36:55] unwind the function but if it was true [36:58] evaluate the function check to see if [37:00] you should enter it hey did I match up [37:02] with a catch maybe we should enter it or [37:04] hey is there destructure a call maybe I [37:05] should enter it so from here we enter [37:08] The sad path from the sad path we have [37:11] two ways of going we can either hit a [37:13] function called called cxa cleanup end [37:16] this is what's called in order to get [37:17] you back into the exception flow if [37:18] you're calling destructors or you can [37:21] hit a function call called cxa and catch [37:23] which brings you back into the happy [37:25] path [37:28] so now that we've gone over how this [37:30] whole little mechanism works we can go [37:32] to the first part here which is locating [37:33] the exception entry so here's what the [37:35] exception index looks like with a couple [37:36] of symbols called Ed uh index start and [37:39] index end each entry in the index is [37:41] eight bytes in length as defined by the [37:44] ABI the first four bytes is the function [37:46] address technically it's an offset to [37:48] where your function is there's reasons [37:50] for that good reasons but it's the [37:52] function address the next part another [37:55] four bytes would be the offset to the [37:57] exception [38:00] data now before we go any further we [38:02] need to talk about what the ordering of [38:03] the index is it is ordered relative to [38:07] the presence of functions in your code [38:09] The Ordering of functions in your code [38:10] so if you have if your code starts at [38:12] address zero and the first function [38:14] address zero is Fu and the next one is [38:16] bar and the next one is baz in the order [38:19] of the exception index will also mimic [38:21] Fu then bar then [38:24] baz this way if my program counter is [38:26] somewhere in the middle of one of them [38:28] I can do a binary search to find where [38:30] that entry is and all I have to do is [38:32] check one entry will have a function [38:34] start other entry have another function [38:35] start if my program counters between [38:37] these two function starts it tells me [38:39] that I'm in the function to the [38:43] left the way that the structure works [38:46] you have a u 32 uh it's a it's an [38:48] address and the other 32-bit value is a [38:53] tagged Union between some metadata or [38:56] inline data the most significant bit is [38:58] what tells you whether or not it is one [39:00] or the [39:02] other so we're going to go over the [39:04] inline data first so let's imagine you [39:06] take a transparent function doesn't have [39:07] destructors or any Tri catches in it and [39:09] you label as no accept what that does is [39:12] it replaces the offset value that would [39:14] normally be there and replaces it with [39:15] the number one the number one signals to [39:17] the unwinding mechanism hey if you see [39:19] this and you need to unwind it simply [39:20] call it [39:22] terminate you can also embed the unwind [39:25] instructions for the function directly [39:27] into the index if there are only a few [39:29] that are required so if you need three [39:31] or less unwinded instructions for your [39:34] function and it's transparent you can [39:36] inline it directly into the exception [39:37] index and this is done in order to save [39:41] memory and now we're talking about [39:43] instructions we might as well go into it [39:44] so we're not going to go into details of [39:46] how all these things work but for the [39:47] most part the instructions are 8 bit [39:50] segments one thing to also know about [39:52] arm is that all of their instructions if [39:54] it's arm 32 and 64 it's 32 bits and if [39:57] it's uh thumb two it's 16 bits and some [40:00] instructions are four sorry 16 bits or [40:02] 32 bits so two bytes or four bytes on [40:05] wine instructions don't have to do much [40:07] they have to subtract the stack pop some [40:09] registers and that's about it so they [40:11] only need like a bite of information [40:12] order to represent them so they're a [40:13] compressed version of a simulated [40:15] version of returning from a [40:17] function the arm exception ABI document [40:21] states that most functions fit within [40:23] three bytes of unwind information and [40:25] the longest you can ever have for any [40:26] function you could possibly make on arm [40:28] is seven bytes of unwind [40:30] instruction so we have instructions for [40:33] adding to the stack subtracting from the [40:34] stack popping [40:36] registers um setting the stack to [40:38] another register finishing saying hey [40:40] we're done with the unwinding and then [40:42] finally refuse to unwind which will be [40:43] cold if you did clean up and then had to [40:45] then discover you need to to uh [40:47] terminate your application which can't [40:51] happen so now we can go into the details [40:53] of what these things are called they're [40:55] called personality data the most bit of [40:57] that inline personality data is this bit [40:59] here which is one if this is set to a [41:01] zero it means that this is an offset [41:02] it's set to a one it means that this is [41:04] inline data bits 28 to 30 are reserved [41:08] bits 24 to 27 are the personality index [41:11] these personality indexes tell you about [41:13] the format of the rest of the bytes to [41:14] come if it's zero then the next set of [41:17] bytes in this 32-bit word will be [41:20] instruction one instruction two and [41:22] instruction [41:24] three if it's uh if the index is one or [41:27] two you'll use a long personality data [41:30] which gives you a length for the number [41:31] of additional words for unwinding this [41:33] particular function now not this can [41:35] actually only be one to two because they [41:36] found out that you only needed seven [41:38] instructions maximum uh which is [41:39] actually kind of sad because I look at [41:41] this and I think to myself we don't need [41:43] to have the length there if we had [41:45] removed the length then it would have [41:46] moved up one and we wouldn't need the [41:47] padding we need of the finished bites [41:48] here that could have saved us four [41:50] additional bites but you know we can't [41:51] have nice things all the [41:54] time now we have everything we need to [41:56] know in order to unwind baz so we do a [41:59] binary search through the index we will [42:01] find its entry and then we will evaluate [42:03] the inline data so we check here we look [42:05] at the most significant bite it's one [42:07] it's inlined personality is [42:09] zero the only instruction in here that [42:11] means anything other than finish is this [42:14] one here pop registers R4 to RN some [42:19] range and the link register so after we [42:21] execute this we will see that bar 14 is [42:25] put into the link register and bars the [42:28] function bars R4 value has been moved [42:31] into [42:32] R4 and the stack pointer has been moved [42:34] up [42:36] appropriately now we're going to do the [42:38] same thing for bar but then we realize [42:39] it's reentrant because a structure to [42:41] Coal so we can't do anything with this [42:43] we need to go a little [42:44] further so we are [42:47] here now we can look at the exception [42:50] table the exception table you look at [42:52] the index you'll get an offset let's say [42:54] it's 2,000 negative 2000 uh 3,00 [42:58] 2,34 we use that offset from the [43:01] location of this data member this object [43:03] here to locate one of these items inside [43:06] the exception [43:07] table think of the uh blue items as [43:09] being like the long personality data and [43:11] think of the red values or red boxes [43:13] being GCC language specific data area [43:19] now what does that mean uh language [43:20] specific data area is something that the [43:22] itanium ABI added in so that you could [43:25] support or we could support throwing [43:27] exceptions through Java and c and C++ [43:30] and have them all cooperate with each [43:31] other yeah we have support for that it's [43:33] one of the reasons why the normal GCC [43:34] exceptions is kind of slow they have to [43:35] handle Java in other other languages [43:37] even though that might not be anything [43:38] you might not be using Java in your [43:40] code so you can technically take a C++ [43:44] API function call pass it over some [43:47] foreign um function interface to Java it [43:50] can do some stuff call it API if it [43:52] throws it'll propagate through C++ [43:54] through to Java back over to C++ [43:59] what's cool is that GCC could just [44:01] leverage this same thing for C++ itself [44:03] so created its own sorry it created its [44:06] own language specific data area in order [44:09] to make a generic uh format for how to [44:11] handle exceptions that work across all [44:15] processors so here are the sections of [44:17] this data the first part is the fun [44:19] personality function this is the [44:20] function that knows how to handle the [44:21] data underneath it the next part is the [44:24] personality data it's the unwind [44:25] information the next part is the header [44:28] which tells you how to encode or decode [44:30] these sections here and here we have the [44:33] callsite table which contains all the [44:35] Scopes that matter in your code such as [44:36] the Scopes you need to know in order to [44:38] destruct stuff as well as where the [44:39] triat Scopes are or where the tri Scopes [44:42] are the action table provides order to [44:45] your Tri to your catches because the [44:47] order of your catch matters and then [44:49] finally we have the type table which [44:50] holds a unique set of the types that are [44:53] thrown in your function so if you like [44:55] throw stood expected like five or sorry [44:58] catch stood expected or sorry stood [45:00] exception five times in your function [45:02] you will only have one entry inside this [45:05] table so here's this will be an offset [45:07] to the function we don't care about this [45:08] one I override this one anyway in my [45:11] runtime uh for this one it's just the [45:13] personality data uh just the same thing [45:14] we went over [45:15] before and before we get into the header [45:18] we have to talk about this encoding [45:19] called Leb [45:20] 128 Leb 28 128 is a variable length [45:24] encoding what it allows you to do is [45:27] let's say we look at these two bytes in [45:28] sequence we can take a look at the most [45:31] significant bit of the bite and say hey [45:33] if it's a one that means there's more [45:34] data to continue if it's a zero it means [45:37] you have finished all your you finished [45:39] hitting the end of your number [45:40] concatenate everything together now you [45:42] have your number this allows you to [45:44] represent small numbers with small [45:46] amount of data and big numbers with not [45:48] amount of amount more [45:50] data so the header works like this the [45:53] first bite in the header is almost [45:54] always on any embedded system going to [45:55] be FF just because it means to Omit [45:58] dwarf information that stuff gets [46:00] stripped out anyway so it's always going [46:01] to be FF The Next Bite will tell you how [46:03] the type table is encoded such as being [46:06] position relative which is normally the [46:07] case or these will be absolute [46:11] addresses the um offset here represents [46:15] the location from this location here to [46:18] the end of the entire language specific [46:20] data area which is where the typ table [46:22] um [46:23] exists then you have an encoding for the [46:25] co site which could either beb 128 or it [46:27] could be a un8 or S6 or whatever and [46:31] then finally this tells you the length [46:33] of the call site from the end of the [46:37] header from the call site it gives you [46:40] four Fields start length landing pad and [46:43] action record start is the location from [46:46] within the Assembly of that function [46:48] down the length is the range from that [46:50] location if your program counter is in [46:52] between that that means you're within [46:54] this scope that means that that landing [46:56] pad and that act record correspond to [46:59] you The Landing Pad tells you where to [47:01] end up in the function when you're going [47:03] to reenter it and the action record can [47:05] either indicate uh uh conditional jump [47:08] uh can indicate unconditional unwinding [47:12] or sorry want Destructor calls or um it [47:15] can indicate a location within the [47:17] action table and you'll have n number of [47:20] these some code to kind of show what it [47:22] looks like uh you have a function called [47:24] read encoded data you have a pointer [47:26] that you can update [47:27] we have the call site format that we use [47:28] to grab the information out get the [47:31] start the length landing pad and the [47:32] action and if our program counter is in [47:34] between that range and Landing it was [47:36] between that range we break out of this [47:38] W Loop if its Landing Pad is zero we [47:40] return early and just say unwind that [47:41] function no more to do [47:45] here from here we can actually execute [47:47] the structures we know enough now at [47:48] this point so let's say we get the [47:49] action number zero and in some cases if [47:51] your type table doesn't exist it'll give [47:53] you this value zero so this is another [47:55] thing you can check for uh in order to [47:57] jump into the code we take the start of [47:59] the function plus The Landing Pad we or [48:03] the least significant bit with one [48:04] because it's a rabi um security thing I [48:07] think it's security we assign the [48:09] destination into the program counter [48:11] then we have a special function called [48:12] restore CPU state which reads our [48:14] virtual registers and populates them [48:16] into the [48:17] CPU this will jump us into this location [48:21] I'm sorry I'll get there in a second so [48:23] um here we can kind of go through the [48:25] encoding here every row in this diagram [48:27] is a four word is a four byte word we [48:31] have the offset we have the unwind [48:32] instructions we see dwarf omit we see [48:35] this thing's position relative the [48:36] offset's zero we can start decoding this [48:38] about four size it's four bytes in size [48:41] start is at zero the length is 28 and [48:45] our Landing Pad is 30 notice that our [48:47] bar our location is bar plus 14 so we're [48:49] within the range of 0 to 28 I'm sorry 26 [48:54] therefore now all we have to do is set [48:56] our landing pad to [48:59] 30 and jump into the [49:01] code so here's what it would look like [49:04] I'm going to zoom in a little bit and [49:06] you'll see here that we have the first [49:08] instruction here add stack pointer plus [49:11] 4 put into r0 remember r0 to R3 are for [49:15] the input parameters of a function we're [49:16] just setting up the location of that [49:19] object from the stack in the first [49:21] parameter so we can call this D structor [49:23] then finally we call cxa cleanup uh and [49:25] cleanup to recover the exception object [49:27] to set the state frame unwind and to [49:29] continue raising the [49:31] exception so it kind of just goes like [49:33] this go from here all the way through [49:35] make your way to unwind and continue the [49:37] propagation [49:38] oops go change that real quick I lost my [49:41] cursor there we go [49:44] cool so after this happens just know [49:47] that the data underneath the stack of [49:50] that object or underneath that function [49:52] is now undefined because destructors can [49:54] use stack memory and they can change [49:56] whatever's on the stack prior so if [49:58] you're going to hold something [49:58] underneath in the stack that could be [50:02] destroyed so after executing the [50:04] destruct oh this is actually one other [50:06] thing about executing destructors you [50:07] can have a you can have multiple landing [50:09] pads depending on what area you are on [50:11] your code such as if you know that you [50:12] only need to destroy this one object you [50:14] can land over here but if you're later [50:16] on in the code you can call it you can [50:17] land here call the first instructor call [50:19] the next instructor and clean [50:23] up now we have the unwind bar pretty [50:26] simple we do the same thing we did [50:27] before we use the offset value to go [50:29] back to the lsda data we can just skip [50:32] the first pointer and go straight to the [50:34] unwind information here we have [50:36] personality zero again the first [50:37] instruction tells us to sub add to the [50:39] stack 12 bytes so that would be move [50:42] from this location where our object used [50:43] to live move this guy up and then [50:46] finally we're going to pop R4 R5 in the [50:48] link register off the stack that'll look [50:51] like this for the first part of the code [50:54] and like this after we're done executing [50:56] the next unwind [50:57] instructions now we're ready to jump [50:59] into food because f is all set up we are [51:01] now pointing at [51:02] food so now we're [51:05] here and now we need to look at the type [51:07] table and the action table this will [51:08] finish up the en the entire thing so [51:11] from here the type table has a unique [51:13] set of all Throne types and if there's a [51:15] null pointer in the type table it means [51:17] catch all so catch dot dot [51:20] dot the ACT table is a little more [51:22] complicated it basically encodes the [51:24] sequence of try of catch blocks for [51:26] every single [51:28] try um and each one of them has a number [51:30] called a filter number the filter number [51:33] is the negative index that you use with [51:35] the type table in order to access its [51:37] elements I should have showed that [51:38] before actually uh the way that this [51:40] works is that you get to the end of the [51:41] type table and then you go backwards to [51:43] find all of your [51:45] pointers and if any one of the type info [51:48] values the pointer of it matches the [51:51] pointer of the throne type those types [51:53] are equal to each other I think people [51:54] think that you have to do like string [51:55] comparisons to do like catch uh [51:57] comparisons no you just got to check the [51:58] pointers if there's multiple types and [52:01] you have some hierarchy you have to do [52:02] something more complicated won't go into [52:03] that now but it's simple as this you [52:05] just have to check the types check the [52:07] pointers um if you get a zero it means [52:09] catch all as well they have catch all in [52:11] two encoded in two ways and if you see [52:13] Zero for the end of oh I'm so sorry I [52:15] skipped over a section after the filter [52:17] number there's this thing called the [52:18] next record this is how you know this is [52:20] what guides you through until eventually [52:22] you hit the end the next record tells [52:24] you from this location here subtract [52:26] your Pointer's position up that amount [52:29] and then read again and do it again and [52:31] keep doing that until you hit [52:33] zero and yeah end of If you hit end of [52:36] record equal to zero that simply means [52:39] uh noral Records at this point and if [52:41] you hit zero end of records and there is [52:44] nothing that matched that means you [52:46] should simply unwind the function [52:47] there's nothing there for [52:49] you if you do find a uh catch that that [52:54] matches what you'll do is you'll set RS [52:56] Z to the exception object address you'll [52:58] set R1 to the filter number and you'll [53:01] set the program counter to the function [53:02] address plus the landing pad or one and [53:05] then you set the state so to kind of [53:07] show off what this would look like we [53:09] have here um the end of the type table [53:12] which is 13 so from this point here it [53:13] actually be 13 bytes to get to the end [53:15] here we have the colite which is four [53:17] bytes we know that Foo is at 10 we check [53:20] the call we check we check the start and [53:22] length within the boundary land pad is [53:25] 16 our action record is one the action [53:28] record is zero means un uh unconditional [53:30] um sorry it means call destructors so we [53:34] have to offset it by one in order to get [53:35] the true offset into the exception into [53:37] the action table we look at filter one [53:40] we do a negative uh access of the type [53:44] table we find this type We compare the [53:46] throne and the C type they're equal to [53:47] each other and now we're ready to jump [53:50] in we now know it's time to jump into [53:52] the catch [53:53] block so from here it'll look like this [53:56] and also a quick little thing uh I don't [53:58] know if anyone knows this but cat blocks [53:59] are switch case statements uh at least [54:01] in itanium they're just switch case [54:02] statements so the filter number is your [54:04] switch value to which case that you are [54:07] so here we compare R1 with the number [54:09] one and if it is one then we simply just [54:12] pass through this little line here it's [54:13] Branch if not equal we enter a function [54:16] called cxa begin catch this will look at [54:19] R zero which has our thrown object it [54:21] will set the exception to handled and [54:24] then it will uh return turn back the [54:27] actual Throne object from there we'll do [54:29] some stuff with it we'll call cxa and [54:32] catch which will do the work of [54:33] deallocating our object and we'll also [54:35] do the work of U sorry calling [54:37] destructors and the allocating our [54:39] object if it's not equal to one let's [54:41] say we got in there somehow with some [54:43] weird filter number we will then jump to [54:44] this location here and begin unwinding [54:48] further and that's pretty much it that [54:51] C++ uh that's exceptions in R GCC pretty [54:54] much complete there's a lot more details [54:56] to it I'm obviously not going to go over [54:57] all that that bore you guys to sleep but [54:58] that's pretty much [55:00] it so now that we have all that [55:01] information we can go over what the [55:03] space costs are for exception [55:05] handling here's a table I'm going to [55:07] fill it out so number one a couple of [55:10] assumptions number one we're going to [55:12] assume that all the codes being compiled [55:14] for code size because otherwise that [55:15] wouldn't really make a good Benchmark [55:18] number two I'm going to assume that most [55:19] functions that almost anyone here has [55:21] written does not exceed 16 kilobytes in [55:24] size and that's like the instructions [55:25] for it [55:27] uh to give some basis for that I looked [55:30] at I took Firefox a while back ago took [55:33] his debug build dumped all of its [55:35] functions and looked at all their sizes [55:37] there are 253 of them that are above 16 [55:40] kilobytes and 325,000 that are below so [55:43] I think it's quite rare to find um [55:46] functions that are greater than 16 [55:48] kilobytes I'm also going to assume that [55:50] between the number of catch blocks and [55:52] object objects that need destruction [55:54] we're going to limit and cap it at 63 [55:56] there the reason why I chose that [55:58] number oh and everything's used as lab [56:01] 128 because it's the most compact way to [56:03] show to hold all [56:05] information so first off exception index [56:08] as defined by the ABI is eight byes in [56:10] length that's it personality data four [56:13] bytes as defined by the [56:15] AI uh long data maximum of 12 bytes [56:18] because of the unfortunate length one or [56:21] two catch blocks got to get a little [56:23] deeper into the assembly there are [56:25] couple things that need to happen for [56:26] catch block remember it's a switch casee [56:27] so we have the compare the branch [56:30] compare Branch the begin catch which if [56:33] you look here might be a little hard to [56:35] see compare is only two bytes of [56:37] information the branch of not equals is [56:40] only two bytes of information the branch [56:41] and Link is four bytes and the other uh [56:45] sorry Branch for begin is four bytes and [56:46] then the uh end catch is also four bytes [56:48] so it cost about 12 byes in total for [56:50] every single catch you [56:52] have for the cleanup regions they are 6D [56:56] + 4 you need two bytes to set up the [56:59] stack or set up the uh input parameter [57:03] for the destructor that branch and Link [57:05] is four bytes so you need to have four [57:07] bytes for that you need that many for [57:09] every single thing you're going to [57:10] destroy and we have an additional four [57:12] bytes for the final call to cxa clean [57:15] up so that's we get 6D plus [57:18] 4 for the action table um the action [57:22] table is 2 * a where a is the number of [57:25] catch blocks you have in your code [57:28] um the range of a Leb 128 number can [57:31] only be signed can only be between -64 [57:34] to 63 so because of that I mean kind of [57:37] having that constraint that I gave us we [57:38] cannot have a filter number that exceeds [57:40] that therefore we only need one bite of [57:43] a filter number in order to represent [57:45] that filter number and because we're [57:46] only use a single we're only using a [57:48] single bite of information the next [57:50] record should never exceed neg3 ever [57:52] should always just be neg3 neg3 neg3 [57:56] for the type table the size for that is [57:58] four bytes for each pointer times T [58:01] which is the number of unique types in [58:02] your uh in your [58:04] function and then for the call site it's [58:06] between four four times the number of [58:09] call sites you have or seven times the [58:11] number of call sites and this comes down [58:13] to the other thing other constraint that [58:14] I gave 16 kilobytes two ulb 128 values [58:18] put together is 2 to the 14 two to the [58:21] 14 is uh 16 kilobytes so I can ensure as [58:25] long as my function never exed see that [58:26] that all my call sites the start length [58:28] and landing pad start if my function [58:31] cannot exceed 16 kiloby my start can [58:32] never be Beyond 16 Kobes my length can [58:35] never exceed that either my Landing Pad [58:37] can never exceed that either so we are [58:38] constrained by the 16 kilobyte length [58:41] the action record because of the other [58:42] constraints we had before is limited to [58:44] I think the maximum size of the action [58:45] table can be is yeah so here 128 bytes [58:48] so that'll always be a single unsigned [58:51] Leb 128 value got lucky there could have [58:53] been off by [58:54] one um [58:56] I'm going to skip this section because I [58:58] hate every single time I re practice [59:00] this one I hate explaining it because it [59:01] has a lot of math included in it [59:03] basically I have to deduce with all the [59:05] other constraints why this particular [59:06] table would be confined to only five to [59:08] seven bytes but I'm going to do a paper [59:10] on all this stuff later on so if you [59:12] guys really want to look into the [59:13] details of this you guys can look at [59:14] that paper in the future but suffice to [59:16] say that the landing the header can only [59:18] be five bytes or up to [59:22] seven and uh here are all the things [59:24] listed out inside of our table [59:28] a couple of [59:29] examples uh for triy Unwind functions if [59:32] they don't have any destructors don't [59:33] have any Tri Cates and they're not like [59:34] doing anything too extra eight bytes of [59:36] information is required to handle this [59:38] on top of whatever is needed for the [59:40] function [59:41] itself for a function without a with a [59:43] single Destructor scope this will be [59:45] about 34 bytes you have the entry which [59:47] is eight bytes then you have the header [59:49] which is five bytes probably not going [59:50] to be seven personality is four bytes [59:52] call table could be four bytes maybe we [59:54] need extra padding to make sure we're [59:55] word the line so three extra bytes then [59:57] we have 10 bytes for The Landing Pad 34 [59:59] bytes just add it on [60:02] up for time we're not going to go into [60:04] the details of this but basically if you [60:06] add up everything we kind of talked [60:08] about before and add it into the [60:09] equation for everything that all the [60:11] different models that I had you get to a [60:13] minimum of 47 bytes if you have a tri [60:15] catch in your function so yeah Tri catch [60:17] is a little little [60:20] expensive now let's take a look at the [60:22] space costs of return type error [60:25] handling [60:28] here's a quick little example we have [60:30] some function called none means doesn't [60:32] do any air handling or nothing there at [60:34] least it has three different functions [60:36] that [60:37] calls uh each one of these isable each [60:39] one of these can throw an [60:40] exception and the number of instructions [60:43] required to do this is just about eight [60:44] of [60:46] them what happens if we used to expect [60:48] it or code will turn into this I even [60:49] put unlikely here hoping that it would [60:51] do something I removed it didn't change [60:52] it Dar and thing um but here we're [60:54] calling the functions taking the value [60:56] do what we need to do from there [60:58] disassembly blows up to this whereas for [61:00] every single check I want you to look a [61:01] little closely I don't know if you can [61:02] see this but Function One branch link [61:04] the function one afterwards we have a [61:06] load word from the stack pointer from [61:09] the object that we got back from this [61:10] value or set to this to the function and [61:14] then we have a compare Branch if zero [61:17] which would jump us to this little area [61:18] here which is the error return path so [61:21] for every single check you have in your [61:22] code it cost you at least six bytes to [61:25] uh in this case four bytes for the load [61:28] of word and two additional bites for [61:30] every single one of the branches that [61:31] you need to [61:33] make but you think to yourself okay [61:35] still expected too heavy weight we're [61:37] not going to use that we're just going [61:38] to return back a bull Isn't that okay [61:40] yeah kind of it gets you down to four [61:42] bytes uh I will say that this [61:43] instruction here is may make you think [61:46] oh well it's only two bytes there well [61:49] that instruction is very limited in its [61:50] scope the reason why the compiler didn't [61:52] reuse it over and over again is because [61:53] this game can only Offset you so far are [61:57] so in general returning back bulls and [61:59] checking them still cost you at least [62:00] four bites per time you do [62:02] it let's consider rust here's some code [62:05] rust is really good at result types so [62:07] they should be like perfect at this n [62:09] it's the same as did expecting pretty [62:11] much there's not that much different the [62:12] the coen's a little smaller though which [62:14] is great but uh it's almost identical [62:16] actually it's pretty much identical to [62:17] stood [62:19] expected but funny enough if you make [62:21] the result type A u63 versus a u32 it [62:24] gets as small as bull [62:26] that's cool um we still have that cost [62:28] there but for some reason there's a [62:30] weird optimization that pops up when [62:31] your object it's too big but in general [62:34] it's four bytes per function [62:36] call and that leads me to my [62:39] conclusion I'm going to discuss [62:41] distributed and centralized error [62:44] handling what is distributed error [62:46] handling this is where you distribute [62:48] your error handling throughout your [62:49] entire code base if else branches all [62:52] that other stuff like that out [62:53] parameters all of that whenever a [62:56] function calls a fallible function it [62:58] has two one of two jobs to do either a [63:01] propagate the fun propagate the error [63:05] or um handle it [63:08] itself whereas on the flip side we have [63:10] centralized err handling this is a form [63:13] of error handling where we have a [63:14] central mechanism for handling [63:16] propagating the errors all the code that [63:18] is inside your code base does not need [63:20] to do anything special in order to [63:22] participate in air handling they simply [63:24] just do what they normally do and the [63:25] exception mechanism will handle it for [63:27] you or the centralized air handling [63:28] system will handle it for you I like to [63:29] think about this kind of in the same [63:30] vein as uh the USPS or like having [63:33] postal systems rather than having [63:34] everyone else manage delivering their [63:36] own stuff we have a system for that [63:39] exceptions are an example of this oh and [63:42] lastly uh normal code and air handling [63:43] code are separated in different domains [63:45] so they don't really class with each [63:46] other so you can kind of view your code [63:48] as the happy path and the other part as [63:49] the sad [63:52] path here is my claim [63:55] centralized air handling schemes have an [63:57] advantage in terms of code size compared [64:00] to distributed air handling why do I say [64:02] that first thing branches are not free [64:05] you have to pay for them every single [64:06] time you use them let's say we wanted to [64:08] add the set of five function calls let's [64:10] say they're all fallible I'd have to pay [64:12] four to six bytes each time I call these [64:14] functions to do a check whereas if I [64:17] just use exceptions I'd only pay eight [64:19] bytes total for this particular type of [64:22] function here's another little example [64:24] of this got I think of like this this is [64:25] the memory usage and having the function [64:27] and the Su personality data and then the [64:30] cost of each comparing Branch remember [64:32] only four bytes in this case for each [64:33] one of [64:35] these but I will be a little I'll be [64:38] I'll be a little generous if you do use [64:40] a tried catch that's the cost about 12 [64:42] to 11 checked function calls so a little [64:45] expensive you know here's my next [64:48] Point most functions are trivially [64:52] unwind think about this for a sec at [64:54] least for the code that I've WR and the [64:56] code I've seen [64:57] around I have found that there's far [64:59] more code that actually propagates [65:02] errors versus handling [65:04] them and for a lot of embedded people I [65:07] bet you're writing a lot of objects that [65:08] don't really need non-al destructors [65:10] don't need a kill memory we don't use [65:12] heat um and maybe you're going to have a [65:15] mutex lock that's a little special but [65:17] in general we're probably going to use [65:18] those very sparingly in specific places [65:19] when we really need [65:21] them I remember talking to Jason Turner [65:23] like a couple months ago and I asked him [65:25] hey I need to wa to figure out uh like [65:28] like any other examples online that I [65:29] can look at and he said we'll take a [65:31] look at [65:32] audacity audacity has [65:34] 1,54 C plus files only 34 39 of them [65:39] have catch blocks in them a total of 60 [65:42] catch blocks and 69 catch blocks were [65:44] found in the source code out of [65:48] 1,777 th000 functions 50 of them [65:51] actually had a TR catch and the total [65:53] cost of for audacity for the amount of [65:57] uh memory used up for exception handling [65:59] is about 15 kilobytes for all of [66:02] audacity well let's be more extreme yeah [66:05] the previous example said that what was [66:07] it like 0 point uh [66:09] 0.28% let's make it 5% let's make the [66:13] number of alloc objects functions that [66:15] allocate objects 25% of our code base [66:17] let's set the percentage for trivial [66:19] unable functions to 55% and 10% to Le [66:22] functions I feel like this is decently [66:24] generous maybe you have different idea [66:26] about this but I think that this is [66:29] reasonable so let's kind of come up with [66:31] an average size for each one of these [66:33] for the TR catches I'm just going to [66:34] give it an average of 64 bytes its [66:36] minimum is 47 bump up a little bit say [66:38] 64 bytes for my cleanup functions at [66:41] default the minimum is 34 bytes let's [66:43] move up to [66:44] 46 trival functions I I said this is [66:48] eight purely because of the fact that [66:49] Lear long unwind information is kind of [66:51] rare so uh just eight byes for this one [66:54] it's going to be in mind and then lead [66:56] functions do not participate in [66:57] exception handling so their metadata [66:59] does not exist there there are zero byes [67:01] for each one you [67:03] create let's add it all up this 8,000 [67:06] here is meant to represent lib unwind [67:09] and the cost of using it in your code we [67:12] add up all these numbers and we get 75 [67:13] 70 27,100 bytes cool it's a bit much now [67:19] let's compare it to the number of checks [67:21] in our code let's say that we're going [67:22] to exchange this for using result types [67:25] let's say that for every check we have [67:26] it cost us four bytes how many checks do [67:28] we get [67:30] 6,775 that's a lot of [67:33] checks uh but across 900 functions it's [67:36] kind of small I don't know 7.5 functions [67:38] per function call before you end up in a [67:40] region where you might as well just use [67:42] exceptions um initially C is a bit [67:45] restrictive also keep in mind that this [67:47] number here is excluding epilogues all [67:49] the return paths in your code this is [67:50] just for the B for the code for checking [67:52] the functions [67:56] and this is the best case scenario with [67:57] four bytes it can't get any better than [67:59] this if you can figure out a way to make [68:01] it better than this please tell me about [68:02] it that' be really [68:03] cool let's choose six bytes for using [68:05] stood expected now we're at five checks [68:08] per function call shoot let's add in two [68:10] additional btes to represent what the [68:12] epilog cost would be and now we're a [68:14] little bit less than four checks per [68:16] function call and that feels really [68:17] restrictive because I've written a lot [68:18] of code that LE I like to put call [68:19] functions I don't feel like being [68:21] restricted from calling [68:23] functions uh should be a thing when I'm [68:24] trying to develop [68:28] so here's my main [68:29] point how does C++ exceptions enable [68:33] smaller binary sizes one a couple of [68:35] ways number one it reduces the codeen [68:39] when calling fallible [68:41] functions number two the unwind [68:44] information scales with the number of [68:47] functions you have and not the number of [68:48] functions you call and remember the [68:50] number of functions you call is going to [68:52] be larger than the number of functions [68:53] you [68:54] have number three it eliminates the [68:56] additional return paths you don't need [68:57] as many because you're not trying to [68:58] return every time you to call a [69:00] function also keep in mind that there's [69:02] been a lot of effort in the past with [69:04] the itanium ABI and the arm exception um [69:07] uh ABI to compress all the instructions [69:10] so that there is small and as uh as [69:12] small as possible and then finally you [69:14] only pay for the mechanism once you pay [69:16] that 8K or at least with my unwind my [69:18] unwinder which is like 4.7 [69:20] 4.5k you only pay for that once and then [69:23] you're done [69:25] and the sad part about this is not even [69:27] exceptions final form exceptions can be [69:29] smaller look at this why do we need the [69:31] dwarf information omitted or why do we [69:33] need a type table en coding why do we [69:34] need a call set en coding why don't we [69:36] make another personality function and [69:38] just strip those out think about like [69:41] this you really shouldn't do this but [69:42] you can technically pack the courts in [69:44] the United States Supreme Court you can [69:45] keep adding more people you can keep [69:47] adding more TCC personalities we can [69:49] make personality be one and that new one [69:51] can have a smaller amount of overhead [69:55] not only that but like literally I [69:57] thought about this as I was making my [69:58] way here we could write a tool to con to [70:00] convert one to the other and keep the [70:02] same [70:03] information and also why do we call cxa [70:07] and catch for every single catch that we [70:09] have change it with a branch branch and [70:11] Link is four bytes Branch unconditional [70:14] is two bytes save two bytes per cash [70:16] block you have and there like so many [70:18] other ideas that I have will into my [70:19] head that we could do to optimize [70:21] exception handling n++ in general but [70:24] that's just it for now we have other [70:25] talks that will go into that so to me I [70:29] have to asked the question of what are [70:31] C++ exceptions [70:33] really after so much time looking at the [70:36] disassembly looking at the metadata and [70:38] working with these data structures I [70:40] came to one conclusion and that is C++ [70:43] exceptions are code [70:46] compression they are a way to turn all [70:48] of this into a little bit of that plus [70:51] eight byes of [70:54] metadata so to me when I look back at [70:56] this diagram of why you shouldn't use [70:58] exceptions the exception table and [71:00] exception code is the opposite of a [71:02] problem it is a solution it's actually [71:04] beneficial to use [71:05] it and I bet everyone's here's like I [71:07] don't care about code size I want it to [71:09] be performant I want my stuff to be fast [71:11] you have anything about that well lucky [71:13] for you my next talk will be on cutting [71:15] C plus exception Time by 88% it was 76 [71:18] before I Accu but then I worked on it a [71:20] little bit more now we're at 88% I'm [71:22] certain I can get it to 95 92% Maybe 5 [71:26] don't quote me on that you know I'm [71:27] trying my best here but yeah know [71:29] there's a lot of things we can do to [71:30] improve the performance oh and uh before [71:33] I move any further since I have a little [71:34] extra [71:34] time does anyone know when the last time [71:38] lib unwind for GCC has been updated any [71:41] of the stuff that's in [71:43] there I think if I looked at it for arm [71:45] there were eight individuals on the [71:47] planet that worked on it total and that [71:49] was around about 15 years ago and then [71:52] the main real chunk of it was 25 years [71:54] ago no one's touched this code for [71:57] decades so comparing my GC my [72:00] implementation of GCC whereas using [72:02] stood expecting your unwinding six [72:04] frames uh TCC is about 27 times slower [72:07] than that mine is still slower but like [72:09] 4.77 I think that's I think is good for [72:12] 96 frames which is kind of extreme uh [72:15] GCC is 21 times slower it gets better as [72:18] you keep going further and then mine is [72:20] at 2.56 slower than just returning from [72:22] your functions [72:25] but I think I spent this entire time [72:27] this whole like 80 minutes so or 70 [72:29] minutes talking about exceptions and how [72:30] they're make they're smaller reduce Cen [72:33] but to be honest I need to be honest [72:35] with myself maybe some people who are [72:36] really big Advocates of exceptions have [72:37] to be honest with themselves as well [72:40] because here's the [72:41] thing exceptions effing suck here's the [72:45] thing what what does this throw be [72:48] honest does anyone have an idea what [72:50] this function throws absolutely not what [72:53] would you do you go check the docs go [72:56] check the docs says it throws some air [72:58] that's cool but does it actually do that [72:59] docs were updated about a year ago we [73:01] keep getting new updates from our vendor [73:02] with new binary blobs I don't know if [73:03] they change [73:04] that we go and email them and we say Hey [73:08] you know do you throw anything else or [73:09] you return a different error code that [73:10] we need to expect for back and forth [73:12] back and forth a week goes by finally [73:13] get a thumbs up yes nothing else gets [73:16] thrown you're good to go on version [73:21] 510 now I've upgraded and now I have no [73:24] idea what's going to happen yeah you [73:26] could you could say oh that should have [73:27] been like a minor patch number or [73:28] something like that if they changed that [73:29] value maybe it should have been a major [73:30] change but the thing is it doesn't [73:32] matter however they go about changing [73:34] their versions I don't know any [73:36] longer no one here knows and to me this [73:40] is the ultimate problem with exception [73:41] handling in [73:43] general because of this it's what get [73:45] makes people concerned about their code [73:47] and how how it will behave is because of [73:49] this that people are trying to push away [73:51] from exception so much that's why we [73:52] want to handle errors locally [73:55] the cool thing is that this is not like [73:56] an impossible problem to solve so I have [73:58] another talk that'll happen after this [73:59] this is the third talk in the trilogy [74:01] called the exceptions insights tool I [74:03] spent so much time looking at the [74:04] disassembly that I noticed something you [74:06] know what's neat when you return like a [74:09] bull or you return said expected it's [74:11] not really easy to look at the [74:12] disassembly and see what's [74:14] happening but whenever you throw there's [74:17] a call to cxa throw in your code so [74:20] here's what my tool will do you pass me [74:22] the binary not your source code your [74:24] source code Doesn't Know Jack give me [74:25] your binary thing you're going to [74:26] actually execute I will par through your [74:29] text section your what's called your [74:30] code your text section I will find every [74:33] single branch in link to cxa throne and [74:35] cxa allocate because I can figure out [74:36] how much you're allocating for every [74:37] single one of your throne objects so I [74:38] know the size of all your throne objects [74:40] know the type for them and I know where [74:41] you're throwing them now I can look [74:43] through your exception index and from [74:45] there I can evaluate where all the [74:47] functions are and how they participate [74:48] in exception handling find everything [74:49] that participates in exception handling [74:51] from the index I can now figure out [74:53] where all your try catch block s and [74:56] from there what I can do is simply [74:58] stimulate throwing an exception raising [75:01] it through all points in your code you [75:02] have to provide me a call graph or we [75:04] will generate it for you we're still [75:05] figuring out if we can do that we can [75:07] simulate calling and throwing all your [75:09] exceptions and figure out which one's [75:10] hit main which ones hit no except [75:12] functions and which ones are caught and [75:13] where they're caught because I don't [75:15] think that this is an impossible tool to [75:16] make and I'm going to dedicate time to [75:17] imple implementing this actually have [75:19] some students here who came here will be [75:20] helping me with doing that so before we [75:23] finish up I have a couple two more [75:24] slides [75:26] special thanks to my mother who came out [75:27] here from California to come see my [75:29] [Music] [75:37] talk my beautiful fiance Malia laor [75:39] who's also part of the conference as [75:40] well she's also C++ programmer she can [75:42] help me out my [75:45] [Music] [75:48] slides and really importantly herb [75:51] Sutter for a variety of reasons but the [75:53] main one is this talk here this is the [75:55] one that inspired me to do my talk this [75:57] one inspired me to get interested in [75:59] this subject uh at all and on top of [76:02] this too I have to really really thank [76:05] BNA I'm not going to put the full quote [76:07] because I think it might be you know [76:08] yeah but he wrote a paper called c f [76:10] exceptions and Alternatives where he [76:13] basically was a counter to herbs talk [76:15] about a new form of exception handling [76:17] he said there's been little serious [76:19] research on the topics of performance on [76:22] of exceptions and reliability of the [76:24] resulting code in C++ so when I read [76:27] that document many years ago I thought [76:29] to myself maybe I can be the person to [76:31] go research [76:32] this and before we finish up uh I have a [76:36] GitHub I am not employed uh I am working [76:39] on this on my own accord this is my own [76:41] research if you would like to support [76:43] the kind of work that I'm doing in [76:44] exception handling and on the exceptions [76:45] Insight tool and trying to minimize the [76:47] code size for exception handling um you [76:50] can consider sponsoring if you are a [76:52] grant giving organization you can [76:55] consider sending an email over to est. [76:56] exceptions gmail.com NSF Department of [76:59] energy whichever one don't really care [77:00] too much and if you're a company or [77:02] you're part of a company that cares [77:04] about exceptions and anything that I've [77:06] spoken about is something that's useful [77:07] to you please tell me what issues you [77:09] have with exceptions tell me how you're [77:10] using them I would love to have as much [77:12] data as possible so we can build the [77:13] right tools for the C++ Community [77:16] understand that everything that I'm [77:16] building is going to be open source [77:18] because I do not [77:19] believe I believe that this tool is [77:22] useful at least for me I think that the [77:24] exception insight tool and the other [77:25] work that I'm doing will help promote [77:26] C++ and keep it alive going into the [77:29] future so that's pretty much it [77:32] [Applause] [77:44] questions [77:46] yes uh hi uh that was a great talk that [77:49] was really interesting it's uh and [77:51] fascinating because it certainly kind of [77:53] flies in the face of the conventional [77:55] wisdom in terms of exception usage so [77:57] that's very cool to see um one thing [77:59] that I am wondering about this just kind [78:01] of stuck in my head from the beginning [78:02] so we were talking about um like copying [78:04] the exception object originally like cxa [78:07] allocate um back at the beginning of the [78:09] talk we were using a static buffer for [78:11] that um in like an embedded environment [78:14] um are there any alternatives to that I [78:16] suppose like what happens if you fill [78:18] the static buffer is there any fallback [78:20] case um I guess that's that's like the [78:22] one kind of unh handleable thing here is [78:25] that as much as I prefer using [78:26] exceptions myself um like it won't [78:28] terminate your program if you run out of [78:30] yeah so yeah yeah so there's two things [78:32] number one is the exception insights [78:33] tool along with u building up some data [78:36] structures the next talks we'll talk [78:37] about that and we'll cover those items [78:39] but um um I can tell you this there is a [78:42] way to allocate just enough information [78:44] just enough memory for every possible [78:46] exception that your code can throw it's [78:48] a certain amount of blocks you need for [78:49] every single thread you have and you can [78:50] go from there but again I have the next [78:52] talk we'll talk about all that stuff [78:54] okay so if could restate um you're [78:55] saying the exception insights tool [78:57] allows you to calculate an upper bound [78:59] correct that's cool thank [79:03] you hi uh first of all respect seriously [79:06] awesome talk thank you um I work in the [79:09] the Confluence of Aerospace and Robotics [79:12] and on the Aerospace side one of the [79:13] things I'm I'm required to do by [79:16] regulation is demonstrate that I've [79:18] covered all of the branches of my code [79:20] from On Target runs to requirements um [79:24] one of the things that that concerns me [79:25] about exceptions is how do I know what [79:29] code has been generated and how do I [79:31] trace that back to the requirements oh [79:34] yeah no okay so kind of going back a [79:37] little bit so I didn't explain the [79:38] exception Insight tool too much it'll be [79:40] in the next talk uh the goal is to give [79:42] you as far as as much as we possibly can [79:45] the a full Global um uh kind of say like [79:48] a either a diagram or full Global [79:50] insight into everywhere where exceptions [79:53] are thrown including in binary BLS that [79:55] you've been given that's why we look at [79:56] the binary level we don't look at source [79:57] code because you might not always have [79:57] the source code available to you and we [80:00] should be able to tell you which ones [80:01] you have that are uncut the idea of the [80:04] uh CLI tool would be exception insights [80:06] you gave your L file and it gives you a [80:07] stack trace of every single exception [80:09] that you missed so the idea is to [80:11] specifically solve this particular [80:13] problem of whenever I do robotics work I [80:15] want to make sure that I caught all the [80:16] errors and the errors that I do not want [80:18] to catch specifically ones that I want [80:19] to terminate me I allow and we can have [80:22] we're still coming up with how we're [80:23] going to uh write the tool to allow you [80:25] to easily specify which things you allow [80:26] to DET terminate you and what things are [80:28] allowed to um um just you know should [80:31] actually give you an error as for where [80:33] it's coming from we'll at least be able [80:34] to give you the locations where that [80:36] data comes from and if you have the [80:37] debug symbols for those functions we can [80:40] potentially tell you what those debug [80:41] symbols are or those functions are but [80:43] if it's completely stripped that might [80:45] be a little [80:46] harder okay thanks [80:51] yeah hi great talk I have two questions [80:53] one uh regarding in um returning um [80:57] expected if you try the montic functions [81:01] like [81:03] reability girl and like what your [81:05] students thought about it oh yeah oh my [81:08] students wouldn't use it they actually [81:08] found it too confusing um so yeah they [81:10] they'd rather just use f statements I [81:12] tried showing them that stuff before and [81:15] uh once you start getting into like a [81:16] lot of use of lambas for like the um the [81:19] or then and that kind of stuff yeah in [81:21] general the cen's about the same if not [81:23] slight the should be around the same and [81:25] my students did not find it uh more [81:27] desirable to work with right and the [81:30] second question do you plan to merge [81:31] this stuff to GCC or what's the CL first [81:34] then GCC [81:36] later very cool thank [81:38] you oh uh one last thing to say is uh uh [81:42] the uh improved exception runtime is a [81:45] library what you do is you bring it into [81:47] your codebase I use C in for this you [81:48] bring it into your codebase you depend [81:50] it as you have it as a dependency and it [81:52] will override all of the GCC mation and [81:55] replace it with yours so you wouldn't [81:56] even need to go to the next compiler [81:58] version you could just bring it into any [81:59] applications currently and it's [82:00] backwards compatible so like you don't [82:02] need to changing your code to use it you [82:03] just kind of get the Improvement um but [82:05] maybe some aspects of how it's designed [82:07] might not fit your your use cases [82:09] question hello uh thanks for the talk [82:11] it's really interesting um does your [82:13] exception analyzer tool like work across [82:15] Dynamic shared Library function called [82:18] yeah so we so we have ideas of how we [82:20] can approach that we have not gotten [82:22] there yet um we are thinking about it [82:24] and we know for a fact that if we are [82:25] going to use if you're going to use that [82:27] tool for any your applications you would [82:28] need to run the analyzer on the machine [82:30] it's going to execute on because you [82:31] need to make sure that we have ground [82:32] truth of what shared libraries you are [82:34] using but um we're figuring out if [82:37] there's any other things are going to be [82:38] barriers to having it work better so it [82:40] is planned [82:44] though I have a personal project that's [82:47] been kind of a back burner uh that I [82:50] call posx PP which is an attempt to uh [82:53] largely [82:54] take remove lib C out from underneath [82:57] C++ and replace it with a pure C++ layer [83:00] for the system call interface uh the [83:03] place where I'm kind of stopped right [83:05] now is uh all the research that would be [83:08] needed to do what you just did so thanks [83:11] you're welcome um would you actually [83:15] find uh the library that I'm working on [83:17] useful once I get it done potentially uh [83:19] let's let's connect up and we can talk [83:20] about it any other questions oh yes uh [83:25] yeah just uh I guess one of the obvious [83:27] questions uh have you looked into x86 [83:30] x64 and uh Windows oo okay all right a [83:35] couple things all right so uh we are [83:37] planning to focus on arm and risk five [83:40] first [83:42] uh for [83:44] x86 that will require some support from [83:47] the community for us to have the [83:48] resources to work on that so just making [83:50] that clear uh as for Windows Microsoft's [83:53] going to have to contact me and talk to [83:55] me about that because if it's for msdc [83:56] that's their own thing if it's for like [83:58] clang and that kind of thing it [83:59] potentially could work I have look into [84:01] that more but I'm not [84:03] certain and I think we are out of [84:06] questions thank you every thank you [84:08] [Applause] [84:23] everyone for