5.5.19 · D4Embedded Systems & Real-Time Software

Exercises — MISRA C — rules for safety-critical C code

3,294 words15 min readBack to topic

This page is a self-test ladder for the MISRA C parent note. Work each problem before opening its solution. Levels climb from "spot the rule" to "design a compliant module." Everything follows MISRA C:2012 unless a problem says otherwise.

Prerequisites worth a glance: Memory Safety, Embedded C Best Practices, Static Analysis Tools.


Level 1 — Recognition

"Can I name the rule and see the violation?"

Exercise 1.1 — Spot the banned call

State whether this line is MISRA-compliant. Name the rule.

int *buf = malloc(64 * sizeof(int));
Recall Solution

Not compliant. This violates Rule 21.3 (Required) — the memory allocation/deallocation functions of <stdlib.h> (malloc, calloc, realloc, free) shall not be used. Why banned: dynamic allocation can fail at runtime, fragment the heap unpredictably, and inject timing non-determinism — all fatal in a hard real-time system (see Real-Time Operating Systems (RTOS)). Fix: static allocation with a compile-time bound.

#define BUF_LEN 64
int buf[BUF_LEN];

Exercise 1.2 — Signed bit-twiddling

Which rule does this violate?

int flags = 0xF0;
int high  = flags >> 4;
Recall Solution

Violates Rule 10.1 (Required) — bitwise/shift operators shall not be applied to essentially signed operands. Why: right-shifting a signed value is implementation-defined (the compiler may fill with the sign bit or with zeros). The bit pattern of a negative int depends on two's-complement details you should never lean on. Fix: make the operands unsigned.

unsigned int flags = 0xF0U;
unsigned int high  = flags >> 4;   /* logical shift, defined */

Exercise 1.3 — The missing braces

Is this compliant, and what is the trap?

if (ready)
    arm();
    fire();
Recall Solution

Not compliant. Violates Rule 15.6 (Required) — the body of a selection/iteration statement shall be a compound statement ({ }). The trap: the indentation lies. Only arm() is guarded by the if; fire() runs unconditionally. This is the classic dangling-body bug. Fix:

if (ready) {
    arm();
}
fire();   /* now visibly unconditional */

Level 2 — Application

"Can I rewrite code to be compliant?"

Exercise 2.1 — Kill the implicit conversion

Rewrite so the intent is explicit and MISRA-compliant. What value ends up in u?

unsigned int u = -1;
Recall Solution

The write itself relies on an implicit signed→unsigned conversion that silently wraps. On a 32-bit unsigned int, becomes . This offends the essential type model (Rule 10.3, Required) because you assign a signed constant to an unsigned object. Fix — write the intended pattern directly:

unsigned int u = 0xFFFFFFFFU;   /* value visible, no hidden wrap */

Now on purpose, and a reviewer sees the intent.

Exercise 2.2 — Return the dangling pointer

This function compiles but is a landmine. Name the rule and fix it.

int *get_reading(void) {
    int reading = 42;
    return &reading;
}
Recall Solution

Violates Rule 18.7 (Required) — the address of an object with automatic storage shall not be copied to something that outlives it. (Historically MISRA C:2004 Rule 17.6.) Why fatal: reading lives on the stack. The instant get_reading returns, that stack slot is free to be reused; the returned pointer dangles. See Memory Safety. Fix — return by value:

int get_reading(void) {
    int reading = 42;
    return reading;   /* a copy travels out, no address escapes */
}

Exercise 2.3 — Pointer runs off the array

Explain the violation and produce a compliant loop that sums data[0..n-1].

int  data[4] = {1, 2, 3, 4};
int *p = &data[0];
int  s = 0;
for (int i = 0; i <= 4; i++) {   /* note the <= */
    s += *p;
    p++;
}
Recall Solution

Two problems, both from Rule 18.1 (Required) — a pointer produced by arithmetic must still address an element of the same array (one-past-the-end is allowed to hold but not to dereference). On the final iteration (i == 4) the loop dereferences *p when p points one past the end — undefined behaviour and a read of garbage. Fix: use < n, and if you keep the loop simple you don't even need a raw pointer.

int data[4] = {1, 2, 3, 4};
int s = 0;
for (int i = 0; i < 4; i++) {   /* strictly less-than */
    s += data[i];
}
/* s == 10 */

Correct sum: .


Level 3 — Analysis

"Can I reason about correctness, overflow, and edge cases?"

Exercise 3.1 — Overflow in the intermediate

An 8-bit ADC returns adc in [0, 255]. We want celsius = (adc * 130) / 255 - 40 using integer types. A junior writes (assume #include <stdint.h> at the top of the file, giving us uint8_t/int8_t):

#include <stdint.h>
 
uint8_t adc;              /* 0..255 */
int8_t  celsius;
celsius = (adc * 130) / 255 - 40;

Trace the arithmetic for adc = 255 and decide whether the intermediate adc * 130 is safe. What compliant types fix it, and what is celsius at adc = 255 and adc = 0?

Recall Solution

Promotion analysis. In adc * 130, adc (a uint8_t) is promoted to int and 130 is int, so the product is computed in int. For adc = 255: , which fits in a 16-bit int's guaranteed range? No — the C standard only guarantees int holds , and . On a 16-bit target this overflows signed int → undefined behaviour. This is the essential-type danger MISRA warns about: relying on the width of int. Fix: force a wide unsigned intermediate so the math is defined and portable. The uint32_t/int16_t types below also come from <stdint.h>.

#include <stdint.h>
 
uint8_t  adc;
int16_t  celsius;
uint32_t scaled = ((uint32_t)adc * 130U);   /* defined, wide */
celsius = (int16_t)(scaled / 255U) - 40;

Values:

  • adc = 255: .
  • adc = 0: . Both land inside [-40, 125], so int16_t holds them safely.

Exercise 3.2 — How many cleanup paths?

A function acquires a mutex, then has these exits (MISRA C:2012 permits multiple returns, Rule 15.5 is Advisory):

int handle(int x) {
    lock(&m);
    if (x < 0)  return -1;      /* exit A */
    if (x == 0) return 0;       /* exit B */
    if (x > 100) return -2;     /* exit C */
    unlock(&m);
    return x;                   /* exit D */
}

Using the parent note's counting argument, how many exit paths are there, on how many is the mutex correctly released, and how many are bugs?

Recall Solution

There are exit paths (A, B, C, D). The unlock sits only before exit D.

  • Exits A, B, C return while still holding the lock → 3 buggy paths (deadlock on next entry).
  • Exit D → 1 correct path. This is exactly why Rule 15.5 (Advisory) recommends a single exit: with returns you get up to places cleanup can be forgotten. Collapsing to one exit makes the unlock happen in one place. Single-exit fix:
int handle(int x) {
    int result;
    lock(&m);
    if (x < 0)        { result = -1; }
    else if (x == 0)  { result =  0; }
    else if (x > 100) { result = -2; }
    else              { result =  x; }
    unlock(&m);        /* one guaranteed release */
    return result;
}

Buggy paths in the original: 3.

The figure below traces those four exit paths side by side. Follow each red box up from a decision node: three of them (A, B, C) leave the function while the mutex is still locked — that is the deadlock waiting to happen. Only the green path (D) reaches the single unlock. It is the visual argument for why Rule 15.5 prefers one exit: one release, one place to verify.

Figure — MISRA C — rules for safety-critical C code

Exercise 3.3 — Signed shift outcome

A compiler documents arithmetic right shift for signed types. What are the two possible documented results of (-1) >> 1 across compilers, and which MISRA rule makes you never depend on either?

Recall Solution

-1 in two's complement is all-ones (0xFFFF...).

  • Arithmetic shift (sign-preserving): fills the top bit with the sign → stays all-ones → result -1.
  • Logical shift (zero-fill): top bit becomes 0 → 0x7FFF... → result on 32-bit. Because the choice is implementation-defined, Rule 10.1 forbids shifting essentially-signed operands at all. Portable code shifts only unsigned.

Level 4 — Synthesis

"Can I combine several rules into one correct module?"

Exercise 4.1 — Compliant bounded lookup

Design a function lookup(idx) that returns table[idx] from a fixed table of 8 uint16_t values, returning a sentinel 0xFFFFU for any out-of-range index. Honor: no dynamic memory (21.3), Boolean controlling expression (14.4), braces everywhere (15.6), single exit (15.5, advisory), and no signed bit tricks. Write it and justify each choice.

Recall Solution
#include <stdint.h>
#define TABLE_LEN (8U)
#define SENTINEL  (0xFFFFU)
 
static uint16_t lookup(uint16_t idx);   /* Rule 8.2 prototype form */
 
static uint16_t lookup(uint16_t idx) {
    static const uint16_t table[TABLE_LEN] =
        { 10U, 20U, 30U, 40U, 50U, 60U, 70U, 80U };
    uint16_t result;
    if (idx < TABLE_LEN) {          /* 14.4: genuine Boolean compare */
        result = table[idx];        /* 18.1: idx proven in-range */
    } else {                        /* 15.6: braces on both arms */
        result = SENTINEL;
    }
    return result;                  /* 15.5: single exit */
}

Justifications:

  • static const table → no malloc (21.3), lives in flash, compile-time bounded.
  • idx < TABLE_LEN is a comparison → essentially Boolean, satisfying 14.4; it also proves the subsequent index is valid for 18.1.
  • Every branch braced (15.6); one return at the end (15.5).
  • All arithmetic in unsigned — no signed shift/mask worries (10.1).
  • uint16_t comes from <stdint.h> (included at the top). Test values: lookup(0) == 10, lookup(7) == 80, lookup(8) == 0xFFFF (=65535), lookup(100) == 65535.

Exercise 4.2 — Deviation or redesign?

Your legacy driver must call a vendor hal_alloc() that internally uses malloc. Your target certifies to ISO 26262 ASIL-D. Under the MISRA classification system, what are your two legitimate options and which category is 21.3?

Recall Solution

Rule 21.3 is Required (not Mandatory), so deviation is possible but not free. Option 1 — Formal documented deviation. Because Required rules permit deviation only with a formal, recorded deviation, you must: document the rationale, the safety analysis showing the residual risk is controlled (e.g. all hal_alloc calls occur once at init, never in the hard-real-time loop), get it reviewed/approved, and log it. This is the ISO 26262 / DO-178C-style paper trail. Option 2 — Redesign. Replace hal_alloc with a static pool allocated at startup, eliminating the violation entirely. Preferred for ASIL-D because it removes the hazard rather than arguing it away. Note: had 21.3 been Mandatory (a category new to C:2012), no deviation would be permitted and only Option 2 would remain.


Level 5 — Mastery

"Can I audit unfamiliar code and defend every judgement?"

Exercise 5.1 — Full audit

List every MISRA C:2012 violation and fix this function. State the rule number for each. (Assume the surrounding file already has #include <stdint.h>, which supplies uint8_t/int8_t.)

#include <stdint.h>
 
int8_t *scale(uint8_t *src, int n) {
    int8_t out[8];
    for (int i = 0; i <= n; i++) {
        int v = src[i] << 1;
        if (v > 127)
            out[i] = 127;
        else
            out[i] = v;
    }
    return out;
}
Recall Solution

Violations found (with rule numbers):

  1. Returning out → Rule 18.7 (Required). out has automatic storage (a stack array); the returned pointer dangles the instant scale returns.
  2. i <= n off-by-one → Rule 18.1 (Required) + Rule 14.2 loop discipline. On the last iteration it reads src[n] and writes out[n], both one element past the intended range; worse, out has only 8 slots, so any n >= 8 overruns the stack buffer entirely (see Memory Safety).
  3. src[i] << 1 in signed context → Rule 10.1 (Required). src[i] is promoted to int (signed) before the shift, so a shift is applied to an essentially-signed operand — implementation-defined and banned.
  4. Missing braces on the if and else bodies → Rule 15.6 (Required).
  5. Implicit narrowing out[i] = v (intint8_t) → Rule 10.3 (Required), essential-type model. The assignment silently narrows a wider signed type into int8_t with no explicit cast.
  6. Multiple exit / structure concerns → Rule 15.5 (Advisory). The original returns a pointer at the end; the redesign below returns void and writes through a caller-owned buffer, sidestepping the dangling-pointer class entirely.

Compliant rewrite — caller owns the output buffer (no dangling pointer, so 18.7 is satisfied); bounded loop clamped to the buffer (18.1); unsigned math (10.1); braces on every body (15.6); explicit narrowing cast (10.3); single exit (15.5):

#include <stdint.h>
#define OUT_LEN (8U)
 
static void scale(const uint8_t *src, uint16_t n, int8_t *out);
 
static void scale(const uint8_t *src, uint16_t n, int8_t *out) {
    uint16_t i;
    uint16_t count = (n < OUT_LEN) ? n : OUT_LEN;  /* clamp to buffer */
    for (i = 0U; i < count; i++) {                 /* strict <, bounded */
        uint16_t v = ((uint16_t)src[i]) << 1U;     /* unsigned shift */
        if (v > 127U) {
            out[i] = 127;
        } else {
            out[i] = (int8_t)v;                    /* explicit narrow */
        }
    }
}

Worked value check: src[i] = 100v = 200, clamped to 127. src[i] = 40v = 80, stays 80. src[i] = 00.

Exercise 5.2 — Saturation table

Using the compliant scale above, src = {10, 60, 100, 128}, n = 4. Give the output array.

Recall Solution

Double each, then saturate at 127:

  • 127 (clamped)
  • 127 (clamped)

Output = {20, 120, 127, 127}.

The saturation curve below makes the clamp visible: the violet line is . It climbs at slope 2 until the input hits the knee at , then flattens against the orange ceiling. The magenta dots are our four sample inputs — the last two sit on the ceiling because their doubled values ( and ) both exceed .

Figure — MISRA C — rules for safety-critical C code

Recall Self-test summary

Rule 21.3 bans ::: dynamic memory (malloc/free from <stdlib.h>) Rule 18.1 requires ::: pointer arithmetic stays within the same array Rule 18.7 prevents ::: returning/storing the address of automatic (stack) storage Rule 10.1 forbids ::: bitwise/shift operators on essentially signed operands Rule 10.3 forbids ::: assigning across incompatible essential types without an explicit cast Rule 15.6 requires ::: braces around every if/else/loop body Rule 15.5 (Advisory) recommends ::: a single point of exit per function The exact-width types uint8_t/int16_t/etc. come from ::: the standard header <stdint.h> The three C:2012 categories are ::: Mandatory, Required, Advisory