Const Interface Bug引起的内存泄漏(重发)

博客主要探讨Delphi中接口机制引发的内存泄漏问题。介绍了因Const Interface Bug导致的隐蔽内存泄漏,通过四个例子对比,指出问题关键在于Const。还给出一个与该Bug无关的极端例子,说明编译器优化选项打开时也可能造成内存泄漏。

不好意思,重发一遍,原来的那篇好像不太对劲,不能修改。

明天就是感恩节,今天上班发现大家都想打发时间混日子,不好好干活,终于同事Alex的一封email让大家理直气壮的不务正业起来,^_^

 

他的email提出了这么两个函数,简便起见,我把他们称为HostClientClient是一个在Host中被调用的函数,另外,里面还有一个InterfacedObject类型的变量TManager, TManager=class(InterfacedObject, IManager).

 

Procedure Host;
Begin
       Client(TManager.Create);
End;

Procedure Client(const aMgr : IManager);
Begin
       If Assigned(aMgr) then beep;
End;

非常简单的两个方法,但是,里面有一个Bug!! 上述代码中有内存泄漏,如果看到这里哪位大侠已经知道是什么泄露了,那么请允许我景仰一下。另外大家如果不相信,请自行测试,特别可以将Host运行个一百万次,看看程序的内存使用。

 

这里的内存泄漏是因为TManager.Create所生成的对象一直到Host执行完毕都没有被释放。我做了一下实验以及Google,发现了问题所在。Alex提出的问题源于一个Delphi中接口机制的”Bug”,称为Const Interface Bug,这个Bug所说的是,如果一个接口被作为常量传递到Client方法,那么在Client方法里是不会自动生成对这个常量的_AddRef以及_Release代码的(关于Delphi中接口的引用计数机制我就不在这里罗嗦了,网上大把资料可以参考),而我读到了一篇关于这个Bug帖子,大家可以参考一下,不过,我在Bug上加引号的原因是,典型的Const Interface Bug直接后果是AVAccess Violation),这种结果很容易被发现,所以引发的后果并不严重。而我在这里想说的是,Const Interface Bug可以引起很隐蔽的内存泄漏问题。

 

让我们言规正传(请大家在继续之前再重新看一边上面对Const Interface Bug的描述,呵呵), 让我给出内存泄漏的原因:

 

Procedure Host;
Begin
       Client(TManager.Create);
{
这里,毫无疑问我们为一个新对象分配了内存,但是注意,因为这里没有将这个接口赋值给任何local变量,因此Host方法不会对这个对象的计数进行增减,这个工作应该交给Client来完成。}
End; //
也就是说,Host在结束的时候,不会free这个对象。

Procedure Client(const aMgr : IManager);
Begin
       If Assigned(aMgr) then beep;
{
而在Client方法中,由于aMgr是作为常量被传递,所以Delphi不会自动生成_AddRef以及_Release}
End; //
Client结束的时候,也不会free这个aMgr

我给出另外三种情况,加上上面这一种,一共四种,大家可以比较一下,上面的case称为1

 

2.我推荐的标准写法

Procedure Host;
Var
       aManager : IManager;
Begin
       aManager := TManager.Create;
       Client(aManager);
{
这里,由于local变量aManagerDelphi将会在Host里生成_AddRef以及_Release,这就能保证在Client执行完毕返回以后,aManager的引用数为0,进而aManager所引用的接口被释放}
End;
Procedure Client(const aMgr : IManager);
Begin
       If Assigned(aMgr) then beep;
End;

3.我们可以看看如果没有const关键字会怎么样。

Procedure Host;
Begin
    Client(TManager.Create);
{
这里和1一样,Host不会对新生成的对象进行任何管理,其计数的引用由Client负责}

End;

Procedure Client(aMgr : IManager);
Begin
       If Assigned(aMgr) then beep;
{
这里的区别在于,aMgr作为一个一般参数,其实相当于一个local变量,因此Client会自动添加_AddRef以及_Release,因此,在Client结束的时候,aMgr计数为0,对象被自动释放。}
End;

4.和2类似的,我们可以把local引用加在Client里,像这样:

Procedure Host;
Begin
       Client(TManager.Create);
End;

Procedure Client(const aMgr : IManager);
Var
       aManager : IManager;
Begin
       aManager := aMgr;
       If Assigned(aManager) then beep;
End;

类似的,由于aManager的出现,使得aMgr被释放,但是这种Client方法的写法非常不符合常规,所以出现的可能性不大。

 

通过比较上面四个例子,大家可以发现问题的关键在于Const,在于Const Interface Bug

 

下面我给出一个更怪异的例子,这个例子也会引起内存泄漏但是和Const Interface Bug无关,99%大家根本不会遇到这钟情况:

5.极端例子:

Procedure Host;
Begin
       Client(TManager.Create);
End;

Procedure Client(aMgr : IManager);
Begin
End;

注意,这里的Client是一个空函数,而且没有Const关键字(当然,如果有Const的话,参照第1个例子,自然会发生内存泄漏),这时,几乎所有的人都会发现有内存泄漏,你一定会问,为什么这里的aMgr就不是local变量,这里的aMgr就不会被Client管理计数而自动释放呢?答案是,这里的aMgrlocal变量,Delphi也为aMgr生成了_AddRef_Release,问题并不在aMgr。注意到我前面说的“几乎所有人”了吗?这是因为Delphi中,编译器的优化选项是默认打开的,在优化状态下,这个Client的函数体将会被删除,因此里面也不会生成什么_AddRef或者_Release,而我们的aMgr则再次成了孤魂野鬼。不信?关掉你的优化选项,重新编译所有单元再看看结果。

<think> 我们正在处理一个复杂的嵌入式软件开发任务,涉及NVR(网络视频录像机)软件平台。用户要求分为几个主要部分: 1. 用gdb搭建NVR软件编译环境,熟悉dsd接口及ubus实现方法 2. 在NVR软件平台添加自动启动独立进程,实现ubus方法触发内存泄漏和空指针问题 3. 在dsd添加接口供postman通过TDCP调用ubus方法 4. 用gdb、objdump、trace alloc分析内存泄漏和空指针问题 5. 整理详细文档 注意:用户提供的引用[1]和[2]提到了多线程环境下使用ubus导致死锁的问题,这表明在多线程编程中需要特别注意ubus调用的安全性。 我们将分步骤进行: 步骤1: 搭建NVR软件编译环境并使用GDB调试 - 获取NVR软件源代码,搭建交叉编译环境(假设目标平台是ARM架构)。 - 在编译时加入调试信息(gcc的-g选项),以便使用GDB调试。 - 使用GDB进行调试的基本方法: 编译程序时加入-g选项,例如:`arm-linux-gnueabihf-gcc -g -o nvr_main nvr_main.c` 在目标板上运行gdbserver:`gdbserver :1234 ./nvr_main` 在主机上使用交叉编译工具链中的gdb连接:`arm-linux-gnueabihf-gdb`,然后在gdb中运行`target remote <目标板IP>:1234` 步骤2: 熟悉dsd接口及ubus实现方法 - dsd可能是设备服务层(Device Service Layer)的接口,需要查阅相关文档和代码。 - ubus是OpenWrt/LEDE项目中的进程间通信(IPC)机制,类似于DBus但更轻量。在代码中,ubus提供了以下功能: * 注册对象和方法 * 调用方法 * 订阅事件 - 学习现有代码中如何使用ubus: * 如何初始化ubus上下文(ubus_connect) * 如何注册对象和方法(ubus_add_object) * 如何实现一个ubus方法(处理函数) 步骤3: 添加独立自动启动进程 - 在NVR软件平台中添加一个新的进程,该进程在系统启动时自动运行。可以通过在init脚本(如/etc/init.d)中添加启动项实现。 - 在该进程中实现ubus方法,这些方法故意触发内存泄漏和空指针问题: * 内存泄漏:在ubus方法中分配内存而不释放(例如,使用malloc分配内存,但不在函数中释放) * 空指针访问:在ubus方法中故意解引用空指针(例如,`int *p = NULL; *p = 1;`) 步骤4: 在dsd中添加接口供postman通过TDCP调用ubus方法 - dsd层可能是提供设备服务接口的中间层。需要在该层添加一个新的接口,该接口内部调用ubus方法。 - TDCP(可能是自定义的通信协议)调用dsd接口,然后dsd接口通过ubus调用我们添加的ubus方法。 - 具体步骤: * 在dsd层代码中新增一个函数,例如`dsd_trigger_bug`。 * 在该函数中使用libubus的API调用我们之前创建的ubus方法(使用`ubus_invoke`)。 * 暴露dsd接口给上层(可能是HTTP接口或RPC接口),以便postman可以通过TDCP调用。 * 配置TDCP服务,将某个URL映射到我们新增的dsd接口。 步骤5: 使用工具分析问题 - 使用GDB调试: * 运行程序,通过postman触发问题。 * 当程序崩溃(空指针)时,使用GDB获取回溯信息(bt)。 * 对于内存泄漏,使用GDB可能不够直接,需要结合其他工具。 - 使用objdump:可以通过反汇编查看代码的汇编指令,帮助定位问题。 `arm-linux-gnueabihf-objdump -S -d nvr_main > nvr_main.asm` - trace alloc:可能是跟踪内存分配的工具。在Linux中,我们可以使用mtrace(如果支持)或者valgrind(但嵌入式平台可能受限)。另一种方法是使用内核的kmemleak(如果是内核模块)或者用户空间的工具如dmalloc。但更实际的做法可能是利用ubus方法中的日志记录分配和释放的情况,或者使用钩子函数记录内存操作。 步骤6: 整理文档 - 记录每一步的操作,包括代码修改、配置更改、测试步骤、问题重现方法、分析过程和解决方案。 注意事项: - 在步骤3中故意制造bug时,务必确保这些bug只在特定触发条件下发生,避免影响系统其他功能。最好在测试环境中进行。 - 多线程环境下的ubus调用需要小心死锁问题(如引用所述),因此我们在实现ubus方法时,应避免在锁的使用上造成死锁。 现在,我们根据以上步骤给出详细指导。 由于这是一个具体的开发任务,我们无法提供实际的代码(因为不知道NVR软件的具体实现),但可以给出示例代码和步骤。 详细步骤: 1. 搭建编译环境和使用GDB - 假设你已经有了交叉编译工具链,编译选项中加入`-g`。 - 部署到目标板,运行gdbserver,然后在主机上使用交叉编译工具链中的gdb连接。 2. 熟悉dsd和ubus - 阅读现有代码中关于dsd接口的定义和实现。 - 学习ubus的基本使用: 初始化: ```c struct ubus_context *ctx; ctx = ubus_connect(NULL); // 连接到默认的ubus路径 ``` 注册对象和方法: ```c static struct ubus_method test_methods[] = { UBUS_METHOD("leak_memory", leak_memory, leak_policy), UBUS_METHOD("null_pointer", null_pointer, null_policy), }; static struct ubus_object_type test_object_type = UBUS_OBJECT_TYPE("test", test_methods); static struct ubus_object test_object = { .name = "test", .type = &test_object_type, .methods = test_methods, .n_methods = ARRAY_SIZE(test_methods), }; ubus_add_object(ctx, &test_object); ``` 实现方法(示例内存泄漏): ```c static int leak_memory(struct ubus_context *ctx, struct ubus_object *obj, struct ubus_request_data *req, const char *method, struct blob_attr *msg) { // 分配100字节,不释放 void *ptr = malloc(100); // 可以在这里使用ptr,但故意不释放 return 0; } ``` 空指针访问: ```c static int null_pointer(struct ubus_context *ctx, struct ubus_object *obj, struct ubus_request_data *req, const char *method, struct blob_attr *msg) { int *p = NULL; *p = 1234; // 这里会触发空指针访问 return 0; } ``` 3. 添加自动启动独立进程 - 编写一个独立的守护进程(例如叫做`bug_tigger`),在main函数中初始化ubus并注册上述对象和方法。 - 在init启动脚本(如`/etc/init.d/bug_tigger`)中添加启动项,确保开机启动。 - 注意:这个进程应该以守护进程方式运行。 4. 在dsd中添加接口 - 假设dsd层有一个模块叫做`dsd_bug.c`,我们在其中添加一个函数: ```c int dsd_trigger_bug(int bug_type) { // 使用ubus调用我们注册的方法 // 首先建立ubus连接 struct ubus_context *ctx = ubus_connect(NULL); // 根据bug_type调用不同的方法 if (bug_type == 1) { // 调用leak_memory ubus_invoke(ctx, "test", "leak_memory", ...); } else if (bug_type == 2) { // 调用null_pointer ubus_invoke(ctx, "null_pointer", ...); } ubus_free(ctx); return 0; } ``` - 然后,在dsd的对外接口(例如RPC接口)中,添加一个接口映射到这个函数。 - 配置TDCP服务(具体配置根据TDCP的文档),将某个请求(例如`/api/trigger_bug`)映射到`dsd_trigger_bug`函数。 5. 分析问题 - 内存泄漏分析: * 使用`trace alloc`:我们可以通过在一个日志文件中记录所有的malloc和free操作来追踪内存泄漏。可以使用宏定义来覆盖malloc和free,然后记录到日志中。例如: ```c #define malloc(size) trace_malloc(size, __FILE__, __LINE__) #define free(ptr) trace_free(ptr, __FILE__, __LINE__) void *trace_malloc(size_t size, const char *file, int line) { void *ptr = _malloc(size); log("malloc: %p %s:%d", ptr, file, line); return ptr; } void trace_free(void *ptr, const char *file, int line) { _free(ptr); log("free: %p %s:%d", ptr, file, line); } ``` 然后运行程序,调用leak_memory方法,检查日志中该分配是否有对应的释放。 * 如果没有释放,则确认泄漏。 - 空指针分析: * 当程序崩溃时,使用GDB获取崩溃时的堆栈信息。 * 或者使用core dump文件分析。 6. 整理文档 - 将上述所有步骤、代码片段、测试结果和分析过程整理成文档。 相关问题: 1. 在嵌入式环境中使用ubus多线程调用时如何避免死锁? 2. 除了gdb和objdump,还有哪些工具可以用来分析嵌入式平台的内存问题? 3. 如何在交叉编译环境下调试程序? </think> 根据你的需求,我将分步骤说明NVR软件开发的完整流程,重点解决ubus相关问题和调试技术。 ### 一、搭建NVR编译环境及熟悉核心组件 1. **交叉编译环境搭建** - 安装ARM交叉编译工具链(如`gcc-arm-linux-gnueabihf`) - 配置环境变量: ```bash export PATH=$PATH:/path/to/toolchain/bin export CROSS_COMPILE=arm-linux-gnueabihf- ``` - 编译时添加调试信息: ```makefile CFLAGS += -g -O0 -Wall -Wextra ``` 2. **GDB远程调试配置** - 目标板启动gdbserver: ```bash gdbserver :1234 /path/to/nvr_main ``` - 主机端连接调试: ```bash arm-linux-gnueabihf-gdb nvr_main (gdb) target remote 192.168.1.100:1234 ``` 3. **dsd接口与ubus实现要点** - **dsd层架构**: ```c // 典型调用流程 dsd_request() -> libubus -> ubusd守护进程 ``` - **ubus核心操作**: ```c // 注册对象 struct ubus_method test_methods[] = { UBUS_METHOD("leak_test", leak_test_handler, leak_policy), UBUS_METHOD("null_test", null_test_handler, null_policy) }; ubus_add_object(ctx, &test_object); // 方法实现示例(内存泄漏) static int leak_test_handler(...) { char *buf = malloc(1024); // 故意不释放 return UBUS_STATUS_OK; } ``` ### 二、添加自动启动进程触发问题 1. **创建独立进程** - 编写守护进程(如`bug_trigger.c`): ```c #include <libubox/uloop.h> int main() { uloop_init(); ubus_add_objects(); // 注册ubus方法 uloop_run(); return 0; } ``` - 编译后放入`/usr/sbin/bug_trigger` 2. **配置开机自启** - 创建init脚本`/etc/init.d/bug_trigger`: ```bash START=99 start() { /usr/sbin/bug_trigger & } ``` - 启用服务: ```bash ln -s /etc/init.d/bug_trigger /etc/rc.d/S99bug_trigger ``` ### 三、在dsd层添加TDCP接口 1. **dsd接口扩展** ```c // dsd_tdcp.c int dsd_trigger_bug(struct tdcp_request *req) { char *method = req->param("method"); ubus_invoke(ctx, "bug_trigger", method, ...); // 转发到ubus } ``` 2. **Postman测试流程** ```http POST /tdcp/interface Content-Type: application/json { "method": "leak_test", "params": {"size": 2048} } ``` ### 四、问题分析与调试技巧 1. **内存泄漏追踪** - 使用`mtrace`分析: ```bash # 目标板执行 export MALLOC_TRACE=/tmp/mtrace.log ./bug_trigger # 主机分析 arm-linux-gnueabihf-mtrace bug_trigger mtrace.log ``` - **trace alloc实现**: ```c void *__wrap_malloc(size_t size) { void *p = __real_malloc(size); log("ALLOC[%p] %s:%d", p, __FILE__, __LINE__); return p; } ``` 2. **空指针崩溃分析** - GDB核心转储分析: ```bash (gdb) core-file core.dump (gdb) backtrace (gdb) disassemble /m $pc ``` - Objdump反汇编定位: ```bash arm-linux-gnueabihf-objdump -S bug_trigger > disasm.txt ``` ### 五、死锁预防与多线程安全 1. **ubus线程安全实践** - 使用递归锁保护共享资源[^2]: ```c pthread_mutexattr_t attr; pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); pthread_mutex_init(&ubus_mutex, &attr); ``` - 避免跨线程直接调用ubus,改用消息队列 2. **死锁检测技巧** - GDB线程状态检查: ```bash (gdb) thread apply all bt ``` - 添加跟踪点: ```c fprintf(stderr, "[THREAD %lu] LOCK %p\n", pthread_self(), &lock); ``` ### 六、文档整理要点 1. **问题分析报告结构**: ``` 1. 现象描述 2. 复现步骤(Postman请求样本) 3. 调试日志(mtrace/gdb输出) 4. 代码定位(文件名+行号) 5. 修复方案(补丁diff) ``` 2. **核心风险控制**: - 测试环境隔离:通过`ubus ACL`限制危险方法访问 - 自动化测试:使用`libubus`编写测试脚本 - 内存防护:开启`-fsanitize=address`编译选项 [^1]: 复位流程中线程调用顺序需验证,避免跨线程资源竞争 [^2]: socket通信与poll机制在多线程场景需要同步保护
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值