如何在单元测试中检测硬实时函数是否调用malloc等特定函数?
可行的解决方案:实时函数内存分配检测
针对你的硬实时模块测试需求,这里有几个实用的方案,既能满足「动态开启/关闭检查」「优雅失败」的要求,也能在Linux下稳定运行,部分方案还能扩展到跨平台:
方案1:LD_PRELOAD 动态拦截(推荐Linux下使用)
你提到不想为所有关注的函数做LD_PRELOAD,但其实可以写一个轻量的拦截库,只在检测阶段触发检查,平时完全透传原函数调用。
实现思路:
- 编写一个共享库,拦截
malloc、free、operator new等函数。 - 在库中维护一个全局的「检测状态标志」(比如
bool g_checking = false),以及一个「是否触发违规调用」的标志(bool g_allocation_called = false)。 - 当处于检测状态时,任何对拦截函数的调用都会将
g_allocation_called设为true,然后可以选择返回一个预分配的内存块(避免程序崩溃)或者直接记录后调用原函数。 - 在你的测试代码中,通过
START_CHECKING_FUNCTION和STOP_CHECKING_FUNCTION宏来修改这个全局状态,并在检查结束后断言g_allocation_called为false。
示例代码片段:
拦截库(intercept_alloc.c):
#include <stdlib.h> #include <stdbool.h> #include <dlfcn.h> bool g_checking = false; bool g_allocation_triggered = false; // 保存原函数指针 static void* (*original_malloc)(size_t) = NULL; static void* (*original_operator_new)(size_t) = NULL; __attribute__((constructor)) void init() { // 获取原函数地址 original_malloc = dlsym(RTLD_NEXT, "malloc"); original_operator_new = dlsym(RTLD_NEXT, "_Znwm"); // operator new(size_t)的 mangled 名 } void* malloc(size_t size) { if (g_checking) { g_allocation_triggered = true; // 返回一个预分配的 dummy 块,避免程序崩溃 static char dummy[1024]; return dummy; } return original_malloc(size); } void* operator new(size_t size) { if (g_checking) { g_allocation_triggered = true; static char dummy[1024]; return dummy; } return original_operator_new(size); } // 暴露给测试代码的接口 void start_checking() { g_checking = true; g_allocation_triggered = false; } void stop_checking() { g_checking = false; } bool did_allocate() { return g_allocation_triggered; }
测试代码中的宏定义:
#include <cassert> // 声明拦截库的接口 extern "C" { void start_checking(); void stop_checking(); bool did_allocate(); } #define START_CHECKING_FUNCTION() start_checking() #define STOP_CHECKING_FUNCTION() stop_checking() void my_unit_test() { my_object obj; obj.perform_initialization(); // 先执行允许分配的初始化操作 START_CHECKING_FUNCTION(); obj.perform_realtime_stuff(); // 实时操作,禁止分配 STOP_CHECKING_FUNCTION(); // 优雅失败:用测试框架的断言(这里用标准assert示例) assert(!did_allocate() && "实时函数中意外调用了内存分配函数!"); }
编译与运行:
# 编译拦截库 gcc -shared -fPIC -ldl intercept_alloc.c -o libintercept_alloc.so # 运行测试时加载库 LD_PRELOAD=./libintercept_alloc.so ./my_test_binary
优点:
- 无需修改业务代码,完全解耦
- 支持拦截所有标准分配函数(C和C++)
- 可以在测试中精准控制检测时机
- 不会导致程序abort,而是通过断言优雅报告错误
方案2:使用GCC的内存分配钩子(轻量但仅限GCC)
GCC提供了__malloc_hook和__free_hook等钩子函数,可以在运行时替换内存分配的逻辑,不需要LD_PRELOAD。不过注意这些钩子已经被标记为deprecated,但在Linux下的GCC版本中仍然可以使用。
实现思路:
- 在测试代码中定义全局的钩子函数,当检测状态开启时,钩子函数会记录分配行为。
- 使用
__malloc_hook = custom_malloc_hook来注册钩子,测试结束后恢复原钩子。
示例代码:
#include <malloc.h> #include <cassert> bool g_checking = false; bool g_allocated = false; // 原malloc钩子 static void* (*original_malloc_hook)(size_t, const void*) = NULL; // 自定义malloc钩子 void* custom_malloc_hook(size_t size, const void* caller) { if (g_checking) { g_allocated = true; } // 调用原钩子 return original_malloc_hook(size, caller); } #define START_CHECKING_FUNCTION() \ do { \ g_checking = true; \ g_allocated = false; \ original_malloc_hook = __malloc_hook; \ __malloc_hook = custom_malloc_hook; \ } while(0) #define STOP_CHECKING_FUNCTION() \ do { \ __malloc_hook = original_malloc_hook; \ g_checking = false; \ } while(0) void my_unit_test() { my_object obj; obj.init(); // 允许分配 START_CHECKING_FUNCTION(); obj.perform_realtime_stuff(); STOP_CHECKING_FUNCTION(); assert(!g_allocated && "实时函数调用了malloc!"); }
优点:
- 不需要额外编译共享库,代码更紧凑
- 同样支持动态开启/关闭检查
缺点:
- 仅支持GCC,兼容性差
- 钩子是全局的,多线程测试可能有线程安全问题
- 无法直接拦截
operator new,需要额外重载全局operator new配合使用
方案3:重载全局new/delete(C++专属)
如果你的代码都是C++,可以重载全局的operator new和operator delete,在其中加入检测逻辑。
实现思路:
- 重载全局
operator new,加入检测状态判断。 - 用全局标志控制检测阶段,记录是否有分配行为。
示例代码:
#include <cstdlib> #include <cassert> bool g_checking_new = false; bool g_new_called = false; // 重载全局operator new void* operator new(size_t size) { if (g_checking_new) { g_new_called = true; // 返回一个预分配块避免崩溃 static char dummy[4096]; return dummy; } return malloc(size); } void operator delete(void* ptr) noexcept { if (!g_checking_new) { free(ptr); } } #define START_CHECKING_FUNCTION() \ do { \ g_checking_new = true; \ g_new_called = false; \ } while(0) #define STOP_CHECKING_FUNCTION() \ do { \ g_checking_new = false; \ } while(0) void my_unit_test() { my_object obj; obj.init(); START_CHECKING_FUNCTION(); obj.perform_realtime_stuff(); STOP_CHECKING_FUNCTION(); assert(!g_new_called && "实时函数调用了operator new!"); }
优点:
- 纯C++实现,不需要依赖系统特性
- 直接拦截C++的内存分配
缺点:
- 无法拦截C风格的
malloc,如果代码中混用C和C++分配,需要结合方案1或2 - 重载全局new可能影响整个程序,需要确保测试环境隔离
总结
如果你的测试环境主要是Linux,**方案1(LD_PRELOAD拦截)**是最灵活可靠的选择,既能覆盖C和C的分配函数,又能完全解耦业务代码,还能在CI流水线中轻松集成。如果是纯C项目,方案3也可以作为轻量替代,但要注意覆盖所有分配路径。
所有方案都能实现「优雅失败」——通过断言而非abort来报告错误,完全符合你的需求。
内容的提问来源于stack exchange,提问作者Jean-Michaël Celerier
相关产品推荐
相关产品推荐

