Blog › ICP guides
C developer on retainer: memcpy/memmove overlap, undefined behavior, pointer aliasing, and embedded C on monthly retainer
October 3, 2026 · ~14 min read
A C developer was maintaining embedded firmware for an industrial sensor controller running on an ARM Cortex-M4 microcontroller with newlib-nano as the C runtime. The firmware received sensor measurement packets in a ring buffer; when the ring buffer’s read pointer wrapped around the end of the buffer, the firmware compacted the unread data by copying the tail segment to the beginning using memcpy. The destination was the beginning of the buffer and the source was a later region — but the tail segment was 32 bytes long and the buffer header region the data was being compacted into was only 32 bytes before the source start, meaning the destination and source regions overlapped by exactly the length of the copy.
On x86 development hardware with glibc’s memcpy, which copies in forward order, the overlap happened to produce correct results because the destination was earlier in memory and the copy proceeded before the source bytes were overwritten. On the ARM Cortex-M4 target with newlib-nano, the ring buffer compaction happened to work correctly — but the developer had also used memcpy in a second location: copying a configuration struct into a packet buffer where the packet buffer and configuration struct were laid out at adjacent addresses in a linker script that placed them contiguously, producing a 4-byte overlap. In that second location, newlib-nano’s memcpy produced corrupted data: the 4-byte overlap caused the last 4 bytes of the configuration struct to be read after they had already been overwritten by the first bytes of the destination write.
The C standard (C99 §7.21.2.1, C11 §7.1.4) is unambiguous: if copying takes place between objects that overlap, the behavior of memcpy is undefined. On x86 glibc, memcpy happened to produce correct output for the overlapping configuration copy. On ARM Cortex-M4 newlib-nano, the same code produced corrupted data: 4 wrong sensor readings over 72 hours of operation, dropping to 0 after replacing memcpy with memmove. memmove is defined to handle overlapping buffers correctly: it checks whether destination < source (copy forward) or destination > source (copy backward) to avoid overwriting source bytes before they are read. The fix was one word change. The investigation — identifying that the corruption was software-caused rather than analog signal noise, isolating it to the configuration copy path, cross-referencing the linker script to confirm the 4-byte overlap, and verifying the fix on hardware — was three hours.
The reason this class of bug is systematically invisible is that undefined behavior in C is not a runtime trap. The C standard defines “undefined behavior” as a condition where the implementation is permitted to do anything: crash, produce garbage output, appear to work correctly on one platform and fail on another, or behave differently on the same platform depending on compiler optimization flags. On x86 glibc, memcpy happens to produce correct output for forward-overlapping copies. On ARM newlib-nano, the same code produces corrupted data. The bug is present in both builds — it is undefined behavior in both — but only becomes observable on the ARM target. The compiler produces no diagnostic. The code compiles cleanly. The symptom — 4 wrong readings over 72 hours — looks like a sensor calibration issue or analog signal noise, not a software defect.
C: ANSI C, ISO C, and the undefined behavior model
C standardization follows a progression that spans five decades. K&R C (Kernighan and Ritchie, 1978) established the language’s core; ANSI C / C89 (ANSI X3.159-1989, ratified as ISO C90) produced the first formal specification; C99 (ISO/IEC 9899:1999) added variable-length arrays, restrict pointers, designated initializers, and _Bool; C11 (ISO/IEC 9899:2011) added _Generic, atomics (<stdatomic.h>), and thread support (<threads.h>); C17 (ISO/IEC 9899:2018) made only clarifications and defect corrections; C23 (ISO/IEC 9899:2024) added #embed, typeof, improved type inference with auto, and removed several deprecated features. Embedded toolchains commonly target C99 or C11; MISRA C (Motor Industry Software Reliability Association) defines subsets of C designed for safety-critical embedded systems and restricts or bans many UB-prone constructs.
The C standard defines three categories of behavior. Defined behavior is specified completely: 1 + 1 is always 2. Implementation-defined behavior is left to the implementation but must be documented: the size of int, the representation of negative integers, and the behavior of right-shifting a signed negative value are implementation-defined. Undefined behavior (UB) is behavior for which the standard imposes no requirements: the implementation may do anything. The critical consequence for production code is that optimizing compilers use UB as a license for transformations. GCC and Clang may assume that UB never occurs; if a code path would require UB to reach, the compiler may prove that code path is unreachable and eliminate it. This means UB does not merely produce wrong output at runtime — it can cause the compiler to silently delete safety checks.
The most consequential UB categories in production C code: signed integer overflow (C99 §6.5; signed overflow is UB; the compiler may assume x + 1 > x is always true and eliminate an overflow check as dead code; fix: use unsigned arithmetic, use __builtin_add_overflow, or rewrite to check before the operation with if (x > INT_MAX - 1)). Null pointer dereference (§6.5.3.2; behavior if a null pointer is dereferenced is undefined; the compiler may eliminate null checks that follow a dereference of the same pointer). Out-of-bounds array access (§6.5.6; pointer arithmetic outside array bounds is UB; overflows stack buffers, corrupts adjacent structs in BSS/data segments). Use-after-free (reading or writing memory after it has been passed to free() is UB; the allocator may have reused or poisoned the region). Strict aliasing violation (§6.5 effective type; accessing memory through a pointer whose type is not compatible with the stored object’s effective type is UB; common in embedded code: casting a uint32_t* to a float* to reinterpret ADC bits as IEEE 754; fix: use memcpy for bit-reinterpretation, or a union with C99 aliasing rules). GCC and Clang both provide runtime sanitizers: -fsanitize=undefined instruments the binary to trap UB at runtime during testing; -fsanitize=address detects out-of-bounds accesses, use-after-free, and heap/stack buffer overflows. Running UBSan and ASan on the x86 build is the standard method for surfacing UB that is silent on the embedded target.
Pointer arithmetic, C memory model, and the string/memory library
C’s pointer model is both its most powerful feature and the source of most of its UB. A pointer is a memory address paired with a base type; pointer arithmetic is defined only within the bounds of the array object the pointer was derived from (including one past the end). Comparing pointers derived from different objects with < or > is undefined behavior; only equality comparisons between unrelated pointers are defined. For buffer management in firmware — ring buffer head/tail pointers, DMA buffer descriptors, packet frame pointers — all arithmetic must stay within the declared object bounds or the C abstract machine provides no guarantee about the result.
Stack allocation (automatic variables) gives objects fixed lifetimes tied to their enclosing block. Heap allocation via malloc/calloc/realloc/free gives explicit lifetime control; calloc zero-initializes the allocation (avoiding uninitialized-read UB for structs with padding); realloc may return a different pointer from the one passed in, invalidating all copies of the old pointer. Use-after-free (reading or writing through a pointer after calling free()) and double-free (calling free() twice on the same pointer) are both undefined behavior. On bare-metal embedded targets, dynamic allocation is frequently avoided entirely due to fragmentation risk and determinism requirements; the “heap” is replaced by static allocation pools or stack-allocated frame buffers, which shifts the risk from use-after-free toward stack overflow and adjacent-struct corruption.
Buffer overflow — writing beyond the declared bounds of an array — is UB. On x86, stack buffer overflows can overwrite saved return addresses, enabling control flow hijacking; compilers mitigate this with stack canaries (-fstack-protector). On embedded firmware, stack buffer overflows silently corrupt adjacent structs in the BSS or data segment at predictable addresses determined by the linker script. This is the exact mechanism behind the linker-script-induced memcpy overlap described above: the packet buffer and configuration struct were adjacent in BSS because the linker script placed them in the same section in declaration order, and the developer had not audited the layout.
The C string and memory library contract for overlapping buffers is precisely specified. memcpy (C99 §7.21.2.1): “If copying takes place between objects that overlap, the behavior is undefined.” memmove (§7.21.2.2): “Copying takes place as if the n characters from the object pointed to by s2 are first copied into a temporary array of n characters that does not overlap the objects pointed to by s1 or s2, and then the n characters from the temporary array are copied into the object pointed to by s1.” In practice, memmove implementations check the pointer relationship at runtime and copy forward or backward accordingly; the temporary array is a conceptual model, not an actual allocation. strcpy and strncpy both require null-terminated source strings; strncpy does not guarantee null termination if the source string is longer than n, a persistent source of buffer read overruns that post-C11 code often replaces with strlcpy (BSD) or explicit memcpy plus null termination.
Embedded C introduces platform-specific layers that change the behavior of portable code. newlib-nano (the MCU-optimized subset of newlib) uses size-minimized implementations of memcpy, memset, and memcmp that may use different copy orders or SIMD strategies than glibc. Full newlib adds floating-point printf/scanf support that newlib-nano omits by default. CMSIS (Cortex Microcontroller Software Interface Standard) provides vendor-neutral register definitions and intrinsics for ARM Cortex-M MCUs. A HAL (Hardware Abstraction Layer) wraps peripheral register access in typed functions, reducing the direct pointer casting that creates strict aliasing UB. Linker scripts (.ld files) define the memory map: the placement of .text (code), .rodata (read-only data), .data (initialized globals), .bss (zero-initialized globals), and the stack region. Adjacent placement of structs in .bss is determined by the linker script and the declaration order in translation units — a detail that is invisible from C source code alone.
Typical C retainer work and what it looks like in a work log
The memcpy/memmove UB bug is the most invisible category of C retainer work. The pattern: embedded firmware uses memcpy for in-buffer compaction or struct copying; the source and destination overlap; the overlap is invisible in the C source because it depends on the runtime memory layout determined by the linker script; the bug is absent on the x86 development build because glibc’s memcpy happens to produce correct output for that overlap direction; the bug appears on the ARM Cortex-M4 target as silent data corruption. Work log entry: “compact_ring_buffer: memcpy(buf, buf+32, 32) — 32-byte overlap; ARM Cortex-M4 newlib-nano produced corrupted data; x86 glibc appeared correct; C99 §7.21.2.1: behavior undefined for overlapping; replaced with memmove; corrupt reads: 4 → 0; 3h.” Also: “copy_config_to_packet: packet buffer and config struct adjacent in BSS (linker script, same section, declaration order); 4-byte overlap at struct boundary; memcpy UB; replaced with memmove; audited all other memcpy calls against linker map for adjacency; 0 additional overlaps found; 1.5h.”
Signed integer overflow UB check elimination is the second pattern. A developer writes if (count + MAX_BURST > BUFFER_SIZE) where count is declared as int. With -O2, GCC reasons: if count + MAX_BURST overflowed, that would be undefined behavior, and I may assume UB never occurs; therefore count + MAX_BURST > BUFFER_SIZE is evaluated under the assumption that no overflow occurred; but in addition, the compiler may determine that the check is always false (because for any non-overflowing value of count, the arithmetic is valid) and eliminate it — or, more precisely, GCC’s value range propagation may remove the branch. The observable effect: when count is close to INT_MAX, the addition wraps to a large negative number (on two’s-complement hardware, which all modern targets use), the overflow check is absent, and a subsequent write at buf[count] overflows the buffer. Fix: rewrite as if (count > BUFFER_SIZE - MAX_BURST) — the comparison is performed on values that cannot overflow because BUFFER_SIZE - MAX_BURST is a compile-time constant and count is compared against it without addition. Work log entry: “check_sample_count: if (count + MAX_BURST > BUFFER_SIZE) — count is int; GCC -O2 eliminates overflow check; buffer overflow when count near INT_MAX; rewrite: if (count > BUFFER_SIZE - MAX_BURST); overflows: counted 2 → 0; 2.5h.”
Strict aliasing violation is the third pattern. A developer casts a uint32_t* pointing at a raw ADC register value to a float* to reinterpret the 32-bit bit pattern as an IEEE 754 single-precision float: float val = *(float*)&raw_adc;. GCC with -O2 and strict aliasing enabled (the default) treats a uint32_t* write and a float* read as accessing different objects (the strict aliasing rule: a pointer of type T* may only alias an object whose effective type is compatible with T, and uint32_t and float are not compatible). The compiler may schedule the float* read before the uint32_t* write, producing a stale value. Fix: replace the pointer cast with a memcpy-based reinterpretation: memcpy(&val, &raw_adc, sizeof val). This is well-defined bit-reinterpretation in C99 and later; the compiler recognizes the pattern and generates a register move, not an actual memory copy. Alternatively, a union with both members can perform the reinterpretation: C99 §6.5.2.3 footnote 95 (in C11 the normative text) permits reading a union member that was not the last written when the types share a common initial sequence or are unsigned integer types of the same size. Work log entry: “decode_adc_value: *(float*)&raw_adc type-pun; GCC -O2 strict aliasing reordered read; 3 sensor values read stale data; replaced with memcpy-based reinterpret; stale reads: 3 → 0; 2h.”
Track C developer retainer hours without the status emails
When a 3-hour firmware debug session traces ARM Cortex-M4 sensor reading corruption to a memcpy UB on overlapping buffers — surfaceable via GCC’s UBSan on the x86 build, invisible in production without memmove — the work log must name the buffer offsets, the overlap length, the platform where it appeared, and the reading count before and after. HourTab gives your C retainer client a public dashboard URL they can bookmark: hours used, hours remaining, and a work log naming the UB category and the fix. No client login. No status emails. CSV in, URL out.
How HourTab tracks C developer retainer hours
C retainer work is invisible by the same mechanism that makes undefined behavior dangerous: the C compiler produces no diagnostic for UB. The code compiles without warnings on both targets under -Wall -Wextra. It runs without a runtime trap on x86. It fails silently on ARM Cortex-M4 with a different memcpy implementation. The symptom — 4 wrong sensor readings over 72 hours — looks like a sensor calibration issue or analog signal noise, not a software defect, sending initial investigation in the wrong direction. The first two hours of the engagement were spent ruling out hardware causes before the investigation turned to software.
The work log needs to name the mechanism: which function (memcpy, memmove), which buffer addresses or offset expressions, what the overlap length was, which C standard section makes it UB, and what the reading count change was before and after the fix. A log entry that says “fixed buffer copy bug, 3h” is not auditable. A log entry that names copy_config_to_packet, the 4-byte overlap caused by BSS adjacency in the linker script, C99 §7.21.2.1, the platform where it manifested, and the sensor reading count before and after is auditable and defensible.
HourTab gives C developers a public retainer-hours URL they send to clients — industrial IoT firmware teams, automotive ECU software groups, medical device embedded teams, and systems programming consultancies maintaining safety-critical C codebases. For C retainers, each work log entry should name the mechanism: which UB category, which function and buffer, which platform exposed it, and what inputs demonstrated the incorrect behavior. Comparative context: C retainer work has structural overlap with adjacent systems languages where the UB model and memory safety guarantees differ. Zig retainers cover a language where @memcpy requires non-overlapping source and destination and the constraint is enforced at compile time for computable sizes — the class of bug described here is well-defined by construction. Rust retainers cover a language where ownership prevents use-after-free and the borrow checker makes aliasing explicit at compile time, eliminating the strict aliasing UB category entirely. C++ retainers cover a language where the same C UB rules apply to low-level memory operations — std::copy on overlapping ranges has the same undefined behavior as memcpy for non-trivially-copyable types, and std::memmove provides the same defined-overlap guarantee.
FAQ: C developer retainers
What does a C developer on retainer typically do?
A C developer on monthly retainer covers memcpy/memmove UB diagnosis (identifying where memcpy is called on overlapping source and destination regions, which is undefined by C99 §7.21.2.1; the fix is replacing memcpy with memmove); signed integer overflow analysis (identifying where an overflow check is eliminated by GCC -O2 as dead code; the fix is rewriting the comparison to avoid overflow before the operation); strict aliasing violation identification (identifying where a type-pun cast through an incompatible pointer type causes GCC to reorder reads and writes; the fix is memcpy-based reinterpretation); embedded platform porting (identifying behavior differences between ARM Cortex-M targets with newlib-nano and x86 Linux with glibc); and cross-platform UB auditing (running GCC/Clang UBSan and ASan on the x86 build to surface UB that is silent in production on the MCU target).
What C work is most commonly underlogged?
Undefined behavior bugs that produce correct output on one platform and incorrect output on another are the most systematically underlogged C retainer work. memcpy on overlapping buffers (C99 §7.21.2.1: behavior undefined), signed integer overflow check elimination (GCC -O2 deduces the check is always false and removes it), and strict aliasing type-pun reordering (GCC reorders a uint32_t* write and a float* read because the optimizer assumes they cannot alias) are all invisible to the compiler, invisible to the developer on x86, and silent at runtime until they manifest as data corruption on the embedded target. Two to five hours invisible per occurrence.
What are typical C developer retainer rates?
Entry-level C developers with experience in ANSI C, basic pointer arithmetic, and the C standard library typically bill at $65 to $120 per hour. Mid-level C programmers with experience in the UB model, embedded C, cross-platform debugging, and CMake/makefiles typically bill at $100 to $180 per hour. Senior C developers with deep knowledge of compiler internals, UBSan/ASan, strict aliasing, embedded MCU HAL layers, and linker scripts typically bill at $145 to $265 per hour. Monthly retainer ranges: $1,500 to $2,800 per month for advisory engagements covering UB audits and embedded platform analysis (12 to 20 hours per month); $2,400 to $5,500 per month for active maintenance including firmware bug fixes, cross-platform porting, and UBSan/ASan integration.
What should a C developer retainer agreement include?
A C developer retainer agreement should specify: C standard version (C99, C11, C17, or C23; each revision changes rules around UB, VLAs, atomics, and type-generic math); embedded target spec (MCU family, toolchain version, C runtime: newlib vs newlib-nano vs uclibc vs picolibc; RTOS or bare-metal); UB audit scope (whether the retainer covers a full codebase UBSan/ASan audit pass under GCC and Clang); compiler flag scope (optimization levels, whether -fno-strict-aliasing or -fwrapv are in use, and whether the retainer covers removing those flags and fixing the underlying violations); test coverage scope (cross-platform testing on x86 with UBSan, QEMU emulation of the MCU target, or hardware-in-the-loop testing); and linker script scope (whether the retainer covers reviewing the linker script for unintended section adjacency that creates overlapping-buffer risks in BSS/data segments).
How should C developer retainer hours be logged?
Log each C retainer session with: for memcpy/memmove bugs, the function name, the source and destination address expressions, the overlap length in bytes, the platform where the failure appeared (e.g., ARM Cortex-M4 with newlib-nano), the C standard section (C99 §7.21.2.1), the fix (replaced memcpy with memmove), and the reading count before and after (e.g., corrupt reads: 4 → 0; 3h). For signed integer overflow check elimination: the function name, the overflowing expression, the variable type (int), the compiler flag that triggered elimination (GCC -O2), the rewrite that avoids overflow, and the overflow count before and after. For strict aliasing violations: the function name, the pointer cast, the UB category (strict aliasing, §6.5 effective type), the compiler reordering observed, the fix (memcpy-based reinterpret), and the stale read count before and after.