← Back to list

Function Mocking on Target Using GNU Linker’s Wrap Feature

On-target? “Blasphemy!”, the clean-code simulation TDD purist years-ago me would have yelled.

Usman Mehmood · 2026-03-12 07:21 · 0 claps · 6.4 min read
#embedded-systems #c #linker #unit-testing #tdd
Open on Medium ↗

Function Mocking on Target Using GNU Linker’s Wrap Feature

On-target? “Blasphemy!”, the clean-code simulation TDD purist years-ago me would have yelled.

Pretext

And while it is easy to imagine a scenario where your unit testing is being done off-target in some nifty CI/CD pipeline on the cloud, where the hardware layers are perfectly abstracted away from the application layers. In practice, that’s not how most codebases are. When you are working on a brand-new type of controller with a tight deadline, abstracting away the hardware is not a priority.

Alternatively, you might inherit a codebase that has deeply intertwined hardware and software layers, or you could be developing the hardware layer itself. Sometimes, your embedded compiler might exhibit behaviors SO distinct from standard compilers that off-target code behaves differently compared to on-target execution. In such cases, on-target testing is not just preferable but inevitable.

Or in some extreme cases of safety-critical applications, testing not done on the actual hardware “doesn’t count” that much for the compliance authorities. They rightfully assert that the testing and its “real” coverage shall be measured at the CPU instruction level. And that can only happen when the test has run on-target.

In such tightly coupled systems, modifying or directly accessing hardware-specific functions for testing can be very difficult, and creating “mocks” for such functions becomes necessary. This is where the GNU linker’s wrap feature comes in, a simple tool to create mocks and use them in a very “dynamic” way.

GNU Linker Wrap Feature

This feature of the GNU linker helps create “wrappers” for inaccessible functions or functions that cannot be easily modified. And it can be exploited to make some very elaborate mocks which can be enabled and disabled at runtime. So one test can use the mock of a function and another test can use the real function.

Before we go into the details,

  • Module Under Test: The module which is being tested right now.
  • Dependency Module: The module whose functions are being called by the module under test, and its functions need to be mocked.

Let’s assume that we are implementing some communication protocol called “super_bus” over UART. Maybe something like if we send some amount of data over it:

  • It adds a “message number” before every packet.
  • It adds the CRC of the data to every packet.

Let’s further assume that the register/hardware level code of UART is not accessible to us. Maybe the silicon vendor didn’t give us access to it (looking at you, Espressif). Or maybe we simply don’t want to deal with the innards of a complicated function.

All we know is that we have the read and write functions inside the header file uart.h.

int uart_write(uint8_t *p_data, uint16_t length);
int uart_read(uint8_t *p_data, uint16_t length);

And in uart.c, the uart_write() function could very well be something like this. (Not that it matters to us.)

int uart_write(uint8_t *p_data, uint16_t length)
{
    int err;
    SOME_HARDWARE_PERIPHERAL.REG |= 0xABCD;
    SOME_DMA_REGISTER            = length;
    SOME_OTHER_DMA_REGISTER      = p_data;
    // some more complicated register manipulation and logic
    return err;
}

All we know and care about is that when we send some data to uart_write(), it sends that data over via the UART peripheral.

Module Under Test

In super_bus.h, the function to be tested is declared.

int super_bus_stream(uint8_t *p_data, uint16_t length);

And in super_bus.c, the function to be tested is defined.

int super_bus_stream(uint8_t *p_data, uint16_t length)
{
    if (length + 4 > STACK_SIZE) return ENOMEM;
    uint16_t crc = calculate_crc(p_data, length);
    *(uint16_t*)&super_bus_stack[0] = ++internal_message_counter;
    *(uint16_t*)&super_bus_stack[2] = crc;
    memcpy(&super_bus_stack[4], p_data, length);
    return uart_write(super_bus_stack, length + 4); // <- there it is!
}

And so when testing this function, we send, lets say, 10 bytes of data. Values 1 to 10. We have to see that the outgoing UART data contains:

  • An incrementing internal counter.
  • The CRC of the 10 bytes.
  • The 10 byte data itself.

What we want is to somehow be able to see exactly what data is sent to uart_write() when we call super_bus_stream(). We can then easily check if those above mentioned conditions are met or not.

How The Wrap Feature Works

The wrap feature (--wrap=symbol) rewrites undefined references to symbol so they resolve to __wrap_symbol, and rewrites undefined references to __real_symbol so they resolve back to symbol.

A very simplified way to look at it is, when this GNU linker feature is used for a function, the linker “splits” the original function call into two parts. A “wrap” function call and a “real” function call. All calls to the function itself are re-routed to the “wrap” version, and the “real” version has to be explicitly called.

How I imagine it must work. I don’t really know how the linker works.

How I imagine it must work. I don’t really know how the linker works.

For example, when we use it for uart_write(), the linker would expect us to give these two additional functions.

int __wrap_uart_write(uint8_t *p_data, uint16_t length);
int __real_uart_write(uint8_t *p_data, uint16_t length);

__wrap_uart_write() is the function that every other piece of code will call. And __real_uart_write() is the actual, “real” uart_write().

__real_uart_write() just has to be declared, while __wrap_uart_write() has to be declared and defined by us. Which means we can implement whatever we want in it.

The use-case suggested by GNU explains the usage perfectly. A simple printf log added to the original function.

int __wrap_uart_write(uint8_t *p_data, uint16_t length)
{
    printf("uart_write was called to send %u bytes\n", length);
    return __real_uart_write(p_data, length);
}

Now, if we use a flag and a function to set and clear that flag, we can choose to either call the real function, or we can run some other code in the “wrap” version. Like storing the incoming data in a buffer that we can read from. And we can choose these on the fly.

In another file, mock_uart.c We can have the flag and its setting function like this.

static bool use_mock_uart_write = false;
void set_uart_mock(bool should_mock) { use_mock_uart_write = should_mock; }

Then, we can have a buffer that stores the last message sent to uart_write(). And a function to get that data. This can be extended into a queue as well, recording data from multiple calls.

static uint8_t last_data_mock_uart_write[100] = { 0 };
uint8_t * get_last_data_mock_uart_write(void)
{
    return &last_data_mock_uart_write[0];
}

And then we can implement the mock like this.

int __wrap_uart_write(uint8_t *p_data, uint16_t length)
{
    // If mock is not enabled, use the real function instead of the mock
    if (use_mock_uart_write == false)
        return __real_uart_write(p_data, length);

    // because size of last_data_mock_uart_write is 100
    if (length > sizeof(last_data_mock_uart_write))
        return ENOMEM; // ENOMEM probably mentioned in the API docs

    // Copy the incoming data to the local buffer
    memcpy(&last_data_mock_uart_write[0], p_data, length);
    return 0;
}

So by using the set_uart_mock() function, we can choose to either let the real function execute, or our mock function logic to execute. We can even switch between the real and mock one within the same test!

And by using the get_last_data_mock_uart_write() function, we can extract the data received by the uart_write() function.

Back in our unit test, we can finally use this contraption and get some hard-earned Dopamine.

void test__super_bus_stream__adds_crc_and_counter(void)
{
    // Arrange
    int err;
    const uint8_t  tx_data[]    = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    const uint16_t bus_counter  = 15;     // arbitrary counter value
    const uint16_t expected_crc = 0xCD4B; // I calculated
    super_bus_set_counter(bus_counter);

    // Act
    set_uart_mock(true);
    err = super_bus_stream(tx_data, sizeof(tx_data)); // <- unit under test
    set_uart_mock(false);

    // Assert
    uint8_t  *p_uart_write_data = get_last_data_mock_uart_write();
    uint16_t received_counter   = p_uart_write_data[0..1];
    uint16_t received_crc       = p_uart_write_data[2..3];
    ASSERT_INT(err, 0);
    ASSERT_UINT(received_counter, (bus_counter + 1))   // supposed to increment
    ASSERT_UINT(received_crc, expected_crc)            // CRC should match
    ASSERT_MEMORY(&p_uart_write_data[4], tx_data, 10); // data should match
}

Usage

In your build configuration (e.g., CMake or Make), append this to the linker options -Wl,--wrap=function_name. Like this:

-Wl,--wrap=uart_write

Pitfalls

The first pitfall of this method is that it doesn’t work when the target and mock functions are in the same translation unit. If a function is referenced inside the same source file where it is defined, that internal call is not wrapped, because only undefined references are redirected. GNU ld documents this explicitly.

The second pitfall is excessive boilerplate. I have used this method to create mocks for a dozen or so modules, and I am already tired of writing so much boilerplate code for each function. It becomes very complicated very quickly so one has to use very strict rules while naming and using the functions.

Automating the boilerplate generation is one way to solve this. If I need and use this enough, I might create a tool that writes these mocks for me.

I also think that the memory overhead introduced by all the extra variables created would pose issues for firmware that are already pushing their controller to the limit. It might not be possible to create a mock for every function that is required to have a mock.

This implies that in such a situation, multiple builds might be needed so each build can mock and test one portion of the firmware.

If this technique proves to be useful enough, eventually someone (maybe myself) would end up automating away these issues.

Conclusion

The GNU linker provides this handy feature for embedded software testing, particularly when direct access to hardware functions is restricted or when systems are too complex to be abstracted into layers.

By allowing developers to intercept and “redefine” function calls dynamically, they can add sophisticated function mocks into their test suites. This method can make testing possible for a lot of untested software. And even if a firmware is tested, it can help make the tests more elaborate and allow for a far better coverage.


메타데이터
post_id
bfd73a06e2d9
slug
function-mocking-on-target-using-gnu-linkers-wrap-feature-bfd73a06e2d9
url
https://medium.com/@usmanmehmood55/function-mocking-on-target-using-gnu-linkers-wrap-feature-bfd73a06e2d9
canonical_url
https://medium.com/@usmanmehmood55/function-mocking-on-target-using-gnu-linkers-wrap-feature-bfd73a06e2d9
author_url
https://medium.com/@usmanmehmood55
status
ok
fetched_at
2026-07-12 01:03:28