Thingino Moves to GCC 16

Most embedded projects pick a toolchain version and freeze it there for years. The logic is sound: “the chip hasn’t changed, the code hasn’t changed, why invite problems?” A compiler upgrade on a shipping firmware is not free — someone has to test it, fix the new warnings, and verify the binaries still fit in flash.

But staying put also means leaving performance, hardening, and developer productivity on the table. Thingino previously defaulted to GCC 15, with GCC 13 and 14 available as alternatives. As of this month the default is GCC 16.1.0, and GCC 13 support has been removed.

This article is not a changelog. It is a tour of what the cumulative improvements from GCC 14 through 16 mean for a resource-constrained IP camera — the kind of device where every kilobyte and every millisecond counts.

What changed

The core.fragment that every Thingino camera defconfig inherits now selects GCC 16:

BR2_THINGINO_TOOLCHAIN_GCC_16=y

You get it as a prebuilt external toolchain — Buildroot downloads a tarball from the Thingino releases page and runs with it. If you prefer to build the toolchain yourself, a Buildroot-internal GCC 16 fragment exists too. The external path is the default because it saves 30–60 minutes of compile time on a fresh build.

The toolchain targets mipsel (little-endian MIPS) with uClibc, the same target tuple Thingino has always used. No flag changes, no ABI breakage. Your existing code compiles. The difference is in what the compiler does with it.

Better code generation

The vectorizer received a rolling overhaul across GCC 14, 15, and 16.

GCC 14 learned to vectorize loops with any number of early breaks — not only the single-exit pattern that older versions handled. GCC 15 extended this to loops where the number of iterations is unknown at compile time, peeling for alignment on fixed-length vectors. GCC 16 generalized the reduction analysis to find parallelism in more complex patterns, added support for uncounted loops (loops where it cannot determine the trip count at all), and introduced mutual peeling for alignment — two adjacent loops can share the same alignment analysis.

For an IP camera, this matters in the video pipeline. Color conversion, scaling, motion detection — these are tight loops over pixel buffers. A vectorizer that can handle more loop shapes means fewer hand-written assembly paths to maintain. The compiler catches up to what the hardware can do.

A full LTO pass can add minutes to a rebuild. Incremental LTO (-flto-incremental=), introduced in GCC 15, cuts that to seconds when only one function changed. For Thingino developers iterating on streamer or ISP code, this turns LTO from a CI-only feature into something you use during development.

Hardening without the RAM tax

IP cameras sit on a network. They face the internet directly or live one NAT hop away. The attack surface is not theoretical.

GCC 14 introduced -fhardened, an umbrella flag that enables a curated set of hardening options: stack protector, fortify sources, PIE, read-only relocations, immediate binding. No research required — one flag and your binary is meaningfully harder to exploit.

GCC 14 also brought -fharden-control-flow-redundancy, which verifies at function exit that the basic blocks visited form a legitimate execution path. It catches control-flow hijacking that jumps into the middle of a function — a class of attack that stack canaries do not protect against.

GCC 16 added counted_by support for pointer members. If you have a struct with a flexible array member and a separate length field, the compiler can now propagate that relationship through pointer access and catch out-of-bounds writes:

struct buf {
    int count;
    int items[] __attribute__((counted_by(count)));
};

Before GCC 16, counted_by only worked on the immediately enclosing struct. Now it survives indirection — pass a struct buf * to another function, and the analyzer still knows the array bounds. In a codebase that handles network packets, RTP frames, and ONVIF XML, this catches real bugs.

Nested functions without trampolines

GCC 16 guarantees that a nested function that does not access its enclosing scope will not generate a trampoline — a small stub of executable code written to the stack at runtime. Trampolines require an executable stack. uClibc on MIPS has never been enthusiastic about executable stacks, and on some hardware configurations they are outright forbidden.

With GCC 16, nested functions that are self-contained compile to plain function calls. No stack execution, no kernel configuration to tweak, no mysterious SIGILL on a new camera model.

Diagnostics that catch what testing misses

The static analyzer (-fanalyzer) got a substantial rework in GCC 16. The memory model was rewritten — faster, easier for GCC developers to extend, and more precise about what it tracks. It now uses GCC’s value-range analysis to eliminate false positives: if the analyzer knows a variable is between 0 and 15, it stops worrying about the default branch of a 16-entry switch.

GCC 15 made C23 the default language version. That means bool, nullptr, constexpr, #embed, and the improved type-checking rules are available without -std=c23. C23’s stricter rules catch implicit conversions and type mismatches that slipped through under C17. Every warning that fires at build time is a bug that does not ship to a camera in someone’s living room.

The diagnostic output itself got better. SARIF output now respects the build directory, HTML output is available with -fdiagnostics-add-output=experimental-html, and the new pub/sub framework lets plugins hook into compiler events. These are developer productivity features, not runtime improvements — but they shrink the gap between “something is wrong” and “I know what to fix.”

What this means for your build

If you build Thingino from source with the standard flow, the switch is transparent. The prebuilt toolchain downloads automatically on the first build. If you maintain a custom defconfig, verify that you are not pinning an older GCC fragment — the core fragment sets GCC 16, but a local override in your defconfig could pull it back.

The toolchain binaries are available for x86_64 hosts (the GitHub Actions runner architecture and every developer machine that matters). If you build on aarch64, the Buildroot-internal GCC 16 fragment is your path.

Binary size is neutral to slightly improved. The new vectorizer transformations can increase code size on individual functions, but the improved inlining heuristics and LTO analysis tend to offset it. As always with Thingino, the final image is a SquashFS with XZ extreme compression — flash space is a first-class concern and nothing in this upgrade makes it worse.

What you learned

  • GCC 16 is the new default for Thingino, delivered as a prebuilt external toolchain to keep build times short.
  • Vectorizer improvements across GCC 14–16 handle more loop shapes (early breaks, uncounted loops, mutual peeling) — the kind of code that dominates video processing.
  • -fhardened gives you a curated set of exploit mitigations with one flag — stack protector, RELRO, PIE, and more — with no runtime RAM cost per flag.
  • Trampoline-free nested functions remove the need for an executable stack on MIPS/uClibc, eliminating a class of hard-to-debug runtime failures on new hardware.
  • The static analyzer and C23 diagnostics catch bugs at compile time that testing on real hardware might not hit for weeks.