5.5.19 · D3Embedded Systems & Real-Time Software

Worked examples — MISRA C — rules for safety-critical C code

4,462 words20 min readBack to topic

This page takes the rules from the parent note and drills them through every kind of situation a real code review can throw at you. Before we start, one promise: nothing here assumes you already "get" C's traps. Each example rebuilds the trap from scratch, shows you the landmine, then defuses it the MISRA way.


The scenario matrix

Every trap in this topic falls into one of these case classes. The examples that follow are labelled with the cell they cover, so together they fill the whole grid.

# Case class What makes it tricky Covered by
A Signed → unsigned conversion negative value silently wraps to a huge positive Ex 1
B Narrowing / truncation a wide value squeezed into a narrow type loses top bits Ex 2
C Signed shift (sign-bit case) right-shifting a negative number is implementation-defined Ex 3
D Pointer arithmetic across arrays pointer walks off the array it belongs to Ex 4
E Dangling pointer (degenerate: object already dead) address of a local returned after it vanishes Ex 5
F Dynamic memory (banned resource) malloc may fail / fragment / be non-deterministic Ex 6
G Control-flow ambiguity (dangling else / missing braces) code looks nested but isn't Ex 7
H Real-world word problem (sensor scaling with overflow) intermediate arithmetic overflows before you notice Ex 8
I Exam-style twist (spot-the-category) classify a rule + choose Mandatory / Required / Advisory Ex 9
Z Zero / limiting / boundary inputs value at 0, at type max, "down through zero", "one past the end" Ex 1 (Step 5), Ex 4, Ex 8

Prerequisite ideas from the vault we lean on: Memory Safety, Embedded C Best Practices, Static Analysis Tools.


Example 1 — Cell A & Z: signed → unsigned (with the zero/limit boundary)

Step 1 — Draw what "unsigned" means. Look at figure s01 below — "Unsigned int is a RING": it draws a circle of values from to with no negative side, with an amber arrow showing where lands. An unsigned int wraps around like a clock: there is no room below zero.

Figure — MISRA C — rules for safety-critical C code
Figure s01 — the unsigned "clock". Cyan ring = all legal unsigned int values ; the amber dot at the top-left is where wraps to, namely . There is deliberately no bottom half: negatives have nowhere to go.

Why this step? You cannot reason about the conversion until you see there is nowhere for to go on the unsigned ring — so C sends it "one step below zero", which wraps to the top.

Step 2 — Define , then apply C's conversion rule. Let be the width of the target type in bits — for a 32-bit unsigned int, , so the ring has slots. C converts a signed value into the unsigned type by taking (the "wrap onto the ring" operation). Here: Why this step? We name explicitly so the formula generalises to any unsigned width (an 8-bit type would use , an unsigned long maybe ). This is the exact rule the compiler silently follows — no warning, no error — and the "error value" the engineer imagined is actually the largest possible number.

Step 3 — Name the MISRA rule. This mixing of an essentially signed constant (-1) with an essentially unsigned target violates Rule 10.3 (Required) of the essential type model.

Why this step? Naming the rule turns "this looks wrong" into a citable review finding that a static analyser will flag automatically.

Step 4 — The compliant fix. Make the value visible and intentional:

unsigned int u = 0xFFFFFFFFU;  /* value stated explicitly, reviewer sees intent */

Why this step? Writing the intended bit pattern directly removes the hidden conversion — the reviewer now sees exactly the number that ends up stored, so nothing is left to inference.

Step 5 — The zero/limit boundary (Cell Z, promised in the matrix). The dangerous edge is where the ring crosses zero going down. Because there is no negative side, 0U - 1U is not -1 — it is 0xFFFFFFFF (wrap back to the top of the ring). The classic consequence: the loop for (unsigned i = n; i >= 0; i--) never terminates, because after i == 0U the decrement wraps to and the condition i >= 0 is always true for an unsigned value. Why this step? The scenario matrix promised a boundary case for Cell Z here; this step is that case, tied directly to the same ring picture — the infinite loop is just the ring being traversed the wrong way past zero.

Recall Reveal — Example 1 answer

u holds ::: 0xFFFFFFFF = 4294967295 (not -1, not an error); rule violated is 10.3 (Required) 0U - 1U equals ::: 0xFFFFFFFF, which is why unsigned down-counting loops never stop

Verify: , and . ✓ (checked in VERIFY).


Example 2 — Cell B: narrowing / truncation

Step 1 — Count the bits. uint8_t has 8 bits, so it holds distinct values: to . 300 needs 9 bits.

Why this step? We must know the capacity of the destination before we can see what overflows it; truncation keeps only the low 8 bits and throws away everything above bit 7 silently.

Step 2 — Do the modulo. Keeping the low 8 bits is exactly : So small == 44, not the "clamped" 255 a beginner expects.

Why this step? Discarding the high bits is mathematically identical to ; computing it shows the surprising real value and disproves the intuitive "it just caps at 255" guess.

Step 3 — Rule. Narrowing a wider value into a narrower essential type is a Rule 10.3 (Required) violation (an assignment that changes the essential type category and may lose value).

Why this step? Same rule family as Example 1, but the narrowing face of it — recognising both faces means you catch either at review time.

Step 4 — Compliant fix: clamp explicitly so the intent is in the code, not in luck:

uint16_t v = 300U;
uint8_t small = (v > 255U) ? 255U : (uint8_t)v;  /* explicit clamp */

Why this step? An explicit clamp makes the out-of-range decision visible and deliberate (here it really does cap at 255) instead of relying on silent truncation to 44.

Recall Reveal — Example 2 answer

(uint8_t)300 gives ::: 44 (low 8 bits: ), NOT 255 and NOT an error Rule violated ::: 10.3 (Required), narrowing across essential types

Verify: . ✓


Example 3 — Cell C: signed right-shift (the sign-bit case)

Step 1 — Why bitwise ops care about signedness. Shift treats the number as a bit pattern. For a negative number the top bit (the sign bit) is 1, and the standard does not fix what fills the vacated top bits on a right shift.

Why this step? The whole trap lives in that one unfixed detail — until you see that "what fills the top" is not standardised for signed values, the danger is invisible.

Step 2 — The two legal behaviours.

  • Arithmetic shift (copies the sign bit): result stays negative, .
  • Logical shift (fills with 0): result becomes a large positive number.

Why this step? Listing both permitted outcomes is the point: because the C standard allows either, the value of (-8) >> 1 is implementation-defined — your test on compiler X may pass while the same code silently changes on compiler Y. That non-portability is exactly what MISRA bans.

Step 3 — Rule. Rule 10.1 (Required): shift/bitwise operators shall not be applied to essentially signed operands. (In MISRA C:2004 this was Rule 12.7.)

Why this step? Citing the rule converts the portability worry into a concrete, tool-checkable finding.

Step 4 — Compliant fix, and why it is compliant. Shift only unsigned bit patterns:

uint32_t v = 0xFFFFFFF8U;  /* the bits of -8, now an unsigned value */
uint32_t r = v >> 1;       /* well-defined: 0x7FFFFFFC */

Why this step? This fix is MISRA compliant precisely because both operands are now essentially unsigned, so Rule 10.1's prohibition no longer applies — and for unsigned right-shift the C standard guarantees zero-fill (logical shift), so the vacated top bit is filled with 0 on every compiler. There is no sign bit to copy, so the implementation-defined choice from Step 2 disappears and the result 0x7FFFFFFC is portable.

Recall Reveal — Example 3 answer

(-8) >> 1 result ::: implementation-defined (arithmetic gives , logical gives a large positive) — that is why it is banned Unsigned 0xFFFFFFF8U >> 1 ::: 0x7FFFFFFC = 2147483644, guaranteed zero-fill, MISRA compliant

Verify: the arithmetic-shift result is ; the unsigned `0xFFFFFFF8U >> 1 = 0x7FFFFFFC = 2147483644$. Both checked. ✓


Example 4 — Cell D & Z: pointer arithmetic and "one past the end"

Step 1 — Picture the array as fenced land. See figure s02"legal pointer targets for int a[4]": four cyan boxes for the real elements plus a dashed amber "gate" box just past the last one, with red ✗ marks on the illegal targets.

Figure — MISRA C — rules for safety-critical C code
Figure s02 — int a[4] drawn as four solid cyan cells a[0]a[3], each labelled with its offset a+0a+3. The dashed amber cell to the right is a+4, the "one-past-the-end" gate (may be formed and compared, never dereferenced). The red ✗ at a-1 and a+5 mark offsets that Rule 18.1 forbids.

Why this step? Bounds are abstract until you see the fence; the picture makes the special "one gate cell past the end" exception concrete rather than something to memorise.

Step 2 — Classify each pointer.

Expression Points to Legal to form?
a + 0 element 0
a + 3 element 3 (last real)
a + 4 one-past-the-end (the gate) ✓ (form/compare only)
a + 5 beyond the gate Rule 18.1 violation
a - 1 before the array Rule 18.1 violation

Why this step? Enumerating every offset — including the two boundary offsets a+4 and a-1 (Cell Z) — is what guarantees no reader hits an unshown case.

Step 3 — Rule. Rule 18.1 (Required): a pointer produced by arithmetic must still address an element of the same array (or one past its end).

Why this step? The rule names the invariant the table is testing, so you can apply it to arrays of any size, not just this one.

Step 4 — Why the compiler can't just check it. The pointer might arrive as a function parameter with no size info attached; bounds are unknowable at compile time. MISRA therefore restricts the construction, not the check. See Memory Safety.

Why this step? Understanding why the restriction is on forming the pointer (not on some later runtime check) explains why MISRA rules look conservative — the tool literally cannot know the bound.

Recall Reveal — Example 4 answer

Legal to form ::: a+0, a+3, a+4 (one-past-end, form/compare only) Illegal (Rule 18.1) ::: a+5 and a-1

Verify: with int a[4], legal offsets are (count ); illegal small examples are and . ✓


Example 5 — Cell E: dangling pointer (degenerate — object already dead)

Step 1 — Trace the lifetime. x has automatic storage — it lives on the stack frame of f and is destroyed the instant f returns. The address &x now points at freed stack space.

Why this step? The pointer isn't wrong while inside f; it becomes a dangling pointer exactly at the return. Tracing lifetime pinpoints the moment the object dies — this is the degenerate case where the object is dead before the pointer is used.

Step 2 — What the caller reads. Undefined behaviour: it may read 10, garbage, or crash — often it "works" in debug builds and fails after optimisation. Never rely on it.

Why this step? Showing all three possible outcomes stops the reader from concluding "it printed 10 on my machine, so it's fine".

Step 3 — Rule. Rule 18.7 (Required) — the address of an automatic object shall not outlive the object. (In MISRA C:2004 the analogous restriction was Rule 17.6.)

Why this step? Naming the rule (and its 2004 predecessor) lets you find it in either version of the standard.

Step 4 — Compliant fix: let the caller own the storage:

void f(int *out) { *out = 10; }   /* caller supplies live memory */

Why this step? Moving ownership to the caller guarantees the storage outlives the write, which is what makes this version compliant.

Recall Reveal — Example 5 answer

Value read by caller ::: undefined behaviour (maybe 10, maybe garbage, maybe crash) — never rely on it Rule violated ::: 18.7 (Required); fix = caller passes in the storage

Verify: conceptual (no numeric answer) — the checkable fact is the object count/lifetime logic, verified structurally in VERIFY as True.


Example 6 — Cell F: dynamic memory (banned resource)

Step 1 — Why the heap is banned. malloc can (1) fail at runtime, (2) fragment memory so a later allocation fails even with free space, and (3) take non-deterministic time — fatal in a hard RTOS deadline.

Why this step? You cannot justify a different design without first seeing exactly which three failure modes the heap introduces.

Step 2 — Rule. Rule 21.3 (Required): the allocation/deallocation functions of <stdlib.h> shall not be used.

Why this step? The rule is the citation your safety case needs; it names the exact functions (malloc, calloc, realloc, free) that are off-limits.

Step 3 — Fixed-bound static allocation. Because n ≤ 256 is known, reserve the worst case up front:

#define MAX_N 256U
static int buf[MAX_N];          /* sized for the worst case, allocated once */
/* use buf[0 .. n-1], with n <= MAX_N enforced by a check */

Why this step? Reserving the worst case at compile time removes all three heap failure modes: the memory exists for the whole program lifetime, cannot fragment, and its size is provable statically.

Step 4 — Boundary check (Cell Z again): reject n > MAX_N before indexing.

Why this step? Static storage only protects you if n never exceeds the reserved bound; the guard is what keeps the guarantee true at runtime.

Recall Reveal — Example 6 answer

Replace malloc with ::: a fixed-size static array sized for the worst case (static int buf[256]), plus a runtime check n <= MAX_N Rule ::: 21.3 (Required) Worst-case size ::: depends on sizeof(int); on a 4-byte-int platform, bytes

Verify: worst-case footprint ; on a 4-byte-int platform that is bytes; on a 2-byte-int platform it is bytes. Allocated exactly once. ✓


Example 7 — Cell G: control-flow ambiguity (missing braces)

Step 1 — Read it the way the compiler does. Indentation is a lie to the compiler. Only the first statement after if is conditional:

if (x > 0) { y = 1; }
z = 2;                 /* ALWAYS runs */

So for x = 0: y is untouched, and z = 2 runs regardless.

Why this step? The bug is entirely a mismatch between how a human reads indentation and how the compiler reads statements; rewriting it the compiler's way makes the mismatch visible.

Step 2 — Rule. Rule 15.6 (Required): the body of an if/loop shall be a compound statement (braced). Braces make "what is inside the if" unambiguous and kill the dangling-else class of bugs.

Why this step? Naming the rule shows this is not a style preference but a required, tool-enforced guard against exactly this ambiguity.

Step 3 — Compliant version:

if (x > 0) {
    y = 1;
}
z = 2;   /* clearly unconditional */

Why this step? With braces, the code the human sees and the code the compiler runs are identical — which is the whole point of Rule 15.6.

Recall Reveal — Example 7 answer

For x = 0 ::: y is unchanged (its prior value), z == 2 runs regardless — z = 2 was never inside the if Rule ::: 15.6 (Required), mandatory braces

Verify: for x = 0, z == 2 and y unchanged (its prior value). Checked numerically in VERIFY. ✓


Example 8 — Cell H (word problem) & Z: sensor scaling that overflows

Step 1 — Check the intermediate against the type limit. uint16_t maxes at . But which is far larger — the multiply overflows (wraps mod ) long before the divide. Wrong answer, silently.

Why this step? The bug hides in the intermediate value, not the final one; comparing that intermediate against the type's max is the only way to catch it.

Step 2 — Rule + fix. Do the arithmetic in a wider unsigned intermediate so no wrap can occur — this is exactly the uint32_t scaled; pattern the parent note's temperature function uses.

uint32_t scaled = (uint32_t)adc * 1650U;   /* up to 6 756 750, fits in 32 bits */
int16_t t = (int16_t)((int32_t)(scaled / 4095U) - 400);

Why this step? Promoting to uint32_t before the multiply gives the intermediate room to hold , so the result is correct; keeping it unsigned also keeps the multiply MISRA-clean.

Step 3 — Evaluate the two boundary inputs (Cell Z).

  • adc = 0 (min): . ✓ matches TEMP_MIN.
  • adc = 4095 (max): , then . ✓ matches TEMP_MAX.

Why this step? Testing both ends confirms the formula produces exactly the documented range and nothing spills outside it.

Step 4 — Verify the result type fits. and both fit an int16_t ().

Why this step? The intermediate being safe is not enough — the final storage type must also hold the extreme outputs, or you have just moved the overflow.

Recall Reveal — Example 8 answer

4095 * 1650 ::: 6 756 750 — overflows a uint16_t (max 65535); use a uint32_t intermediate adc = 4095 gives ::: 1250 tenths = +125.0 °C; adc = 0 gives -400 tenths = -40.0 °C

Verify: ; endpoints give and ; both fit int16_t. All checked.


Example 9 — Cell I: exam-style "spot the category"

Step 1 — Recall the three tiers (the Mandatory tier was introduced with MISRA C:2012 Amendment 1; C:2004 had only two tiers).

Tier Deviation allowed?
Mandatory Never
Required Only with a formal, documented deviation
Advisory Recommended; deviations still ought to be recorded

Why this step? You cannot classify any single rule until you have the full ladder of tiers in front of you; the "deviation allowed?" column is what each answer below turns on.

Step 2 — Classify (a) Rule 21.3. It is Required. Reason: the heap is dangerous but not always impossible to justify, so a formal, documented deviation may be granted (e.g. a certified fixed-block pool allocator), though in practice it rarely is.

Why this step? "Required" is the tier that most rules sit in; recognising that a documented deviation is possible here is exactly what separates it from Mandatory.

Step 3 — Classify (b) Rule 15.5. It is Advisory in MISRA C:2012. Reason: single-point-of-exit was relaxed from C:2004's Required Rule 14.7 down to Advisory — multiple return statements are now permitted, single exit is merely recommended because it centralises cleanup.

Why this step? This is the classic exam trap: candidates who learned C:2004 answer "Required" and lose the mark. Showing the version change fixes the misconception.

Step 4 — Answer (c) the Mandatory question. No — a Mandatory rule permits no deviation, ever. There is no documented-deviation escape hatch for the Mandatory tier; that is the entire distinction between Mandatory and Required.

Why this step? The question hinges on the single word "ever"; stating the flat "no" and why (no escape hatch exists) is the complete, exam-ready answer.

Step 5 — Why the tiers matter in practice. A static analyser reports violations by tier; your safety case (e.g. ISO 26262, DO-178C, or IEC 62304) must formally justify every Required deviation and can never carry a Mandatory one. See Formal Verification for how these justifications are discharged.

Why this step? Tying the classification back to real certification consequences shows the tiers are not trivia — they drive what you are legally allowed to ship.

Recall Reveal — Example 9 answers

(a) Rule 21.3 ::: Required — documented deviation possible but rare (b) Rule 15.5 (single exit) ::: Advisory in C:2012 (was Required Rule 14.7 in C:2004) (c) Mandatory rule deviation ::: never allowed — no escape hatch exists

Verify: classification encoded as a truth check in VERIFY.


Recall Master self-test (reveal after answering all)

unsigned int u = -1; value on 32-bit ::: 0xFFFFFFFF = 4294967295 (Rule 10.3) (uint8_t)300 ::: 44 (low 8 bits; Rule 10.3) Why is (-8) >> 1 banned ::: right-shift of a signed/negative value is implementation-defined (Rule 10.1) Legal offsets for int a[4] ::: a+0a+4 (one-past-end); a+5 and a-1 violate Rule 18.1 Rule banning malloc ::: Rule 21.3 (Required) adc=4095 temperature in tenths ::: 1250 → +125.0 °C Single-exit rule status in C:2012 ::: Advisory (Rule 15.5), relaxed from Required in C:2004