文章目录
- openssl3.2 - crypto-mdebug被弃用后, 内存泄漏检查的替代方法
- 概述
- 笔记
- 查看特性列表
- openssl3.2编译脚本 - 加入enable-crypto-mdebug
- 看看有没有替代内存诊断的方法?
- main.cpp
- my_openSSL_lib.h
- my_openSSL_lib.c
- 备注
- 备注
- 这招不行啊
- 显势调用默认上下文也不行
- 找到一种还可以的解决方法, 现在看来可以准确观测到openssl内存泄漏点
- 内存泄漏观测的程序实现
- main.cpp
- CMemHookRec.h
- CMemHookRec.cpp
- 备注
- 备注
- END
openssl3.2 - crypto-mdebug被弃用后, 内存泄漏检查的替代方法
概述
调用openssl接口后, 如果用到了openssl对象, 需要释放, 否则会发生内存泄漏.
即使不是新手, 也不能保证释放函数都调用了. 想想我们自己写程序, new后, 没有delete的情况就知道, 可以理解.
谁能保证自己手搓的应用实现100%没内存泄漏呢?
看资料时, 发现openssl本身有这个检查库本身发生内存泄漏的特性, 大概就是申请内存时, openssl自己记录了一下, free内存时, 将对应记录删掉.
这样, 在程序退出前, 再调用一下内存分配记录列表接口, 就知道哪里的内存没释放.
那试试, 加入crypto-mdebug特性, 模拟一下内存泄漏(调用openssl_new(), 不调用openssl_free()), 看看啥效果.
笔记
查看特性列表
perl configdata.pm --dump > my_log.txt
查看my_log.txt, 就有openssl 特性列表.
有启用的特性列表, 也有被禁掉的特性列表.
如果要加入特性, 就看禁止列表中的特性.
怎么打开crypto-mdebug特性呢?
看Configure可知, 只要带上参数 enable-crypto-mdebug即可.
结合我最后实验可用的编译脚本, 加入 enable-crypto-mdebug
openssl3.2编译脚本 - 加入enable-crypto-mdebug
解开官方源码包打开vs2019x64本地命令行, 选择管理员身份运行cd /d D:\3rd_prj\crypt\openssl-3.2.0set path=c:\nasm;%path%perl Configure VC-WIN64A --debug enable-crypto-mdebug zlib-dynamic --with-zlib-include=D:\my_dev\lib\zlib_1d3 --with-zlib-lib=.\my_zlib_1d3.dll --prefix=c:\openssl_3d2 --openssldir=c:\openssl_3d2\commonnmake手工拷贝, 将 my_zlib_1d3.dll 拷贝到以下4个目录
.\
.\apps
.\fuzz
.\testnmake testnmake install手工拷贝
D:\my_dev\lib\zlib_1d3\my_zlib_1d3.dll => C:\openssl_3d2\bin\my_zlib_1d3.dll归档
C:\openssl_3d2 剪切到自己的库目录 => D:\my_dev\lib\openssl_3d2
写个测试程序, 调用一下内存泄漏检查的相关接口, 看看能否编译过, 然后试试接口怎么用.
/*!
* \file main.cpp
*/#include "my_openSSL_lib.h"#include <openssl/crypto.h> // for mem leak APIint main(int argc, char** argv)
{CRYPTO_mem_leaks(NULL);return 0;
}/*!
编译错误
已启动重新生成…
1>------ 已启动全部重新生成: 项目: prj_template, 配置: Debug x64 ------
1>main.cpp
1>D:\my_dev\my_local_git_prj\study\openSSL\exp\call_mem_leak_API\main.cpp(12,2): error C4996: 'CRYPTO_mem_leaks': Since OpenSSL 3.0
1>已完成生成项目“prj_template.vcxproj”的操作 - 失败。
========== 全部重新生成: 成功 0 个,失败 1 个,跳过 0 个 ==========
*/
直接编译不过…
看官方说明 file:///D:/3rd_prj/crypt/openssl-3.2.0/doc/html/man3/OPENSSL_malloc.html
The following functions have been deprecated since OpenSSL 3.0, and can be hidden entirely by defining OPENSSL_API_COMPAT with a suitable version value, see openssl_user_macros(7):int CRYPTO_mem_leaks(BIO *b);int CRYPTO_mem_leaks_fp(FILE *fp);int CRYPTO_mem_leaks_cb(int (*cb)(const char *str, size_t len, void *u),void *u);int CRYPTO_set_mem_debug(int onoff);int CRYPTO_mem_ctrl(int mode);int OPENSSL_mem_debug_push(const char *info);int OPENSSL_mem_debug_pop(void);int CRYPTO_mem_debug_push(const char *info, const char *file, int line);int CRYPTO_mem_debug_pop(void);
DESCRIPTION
看到官方说, 这些内存诊断函数已经弃用了, 用clang的检查代替(忘了是哪个文档了, 反正是官方文档中说的).
去看内存诊断函数的实现, 都是空的, 官方确实已经弃用了.
尝试将vs2019的工具链改为clang, 看不到效果, 且不能单步进入库函数内部.
用clang工具链编译, 可以编译过, 也可以运行, 不过无法进入调试版的pdb实现中.
已启动重新生成…
1>------ 已启动全部重新生成: 项目: prj_template, 配置: Debug x64 ------
1>main.cpp(12,2): warning : 'CRYPTO_mem_leaks' is deprecated: Since OpenSSL 3.0 [-Wdeprecated-declarations]
1>D:\my_dev\lib\openssl_3d2\include\openssl/crypto.h(411,1): message : 'CRYPTO_mem_leaks' has been explicitly marked deprecated here
1>D:\my_dev\lib\openssl_3d2\include\openssl/macros.h(194,49): message : expanded from macro 'OSSL_DEPRECATEDIN_3_0'
1>D:\my_dev\lib\openssl_3d2\include\openssl/macros.h(44,22): message : expanded from macro 'OSSL_DEPRECATED'
1>prj_template.vcxproj -> D:\my_dev\my_local_git_prj\study\openSSL\exp\call_mem_leak_API\x64\Debug\prj_template.exe
1>'pwsh.exe' 不是内部或外部命令,也不是可运行的程序
1>或批处理文件。
1>已完成生成项目“prj_template.vcxproj”的操作。
========== 全部重新生成: 成功 1 个,失败 0 个,跳过 0 个 ==========
那就不用clang来编译.
看看有没有替代内存诊断的方法?
找到一个opensslAPI CRYPTO_get_alloc_counts(), 可以取当前内存分配次数.
将这个API封装一下, 卡在openssl应用函数外边, 就可以间接的知道是否有内存泄漏, 如果有opensslAPI调用引起的内存泄漏, 可以迅速的缩小排查范围.
main.cpp
/*!
* \file main.cpp
*/#include "my_openSSL_lib.h"#include <openssl/crypto.h>bool is_OSSL_mem_leak();
void test_mem_leak(bool b_have_mem_leak);int main(int argc, char** argv)
{openssl_mdebug_begin();test_mem_leak(true);openssl_mdebug_end(true, false); // 如果需要断言, 参数2为trueopenssl_mdebug_begin();test_mem_leak(false);openssl_mdebug_end(true, true);/*run resulttest_mem_leak(test have mem leak)>> malloc_count = 0, realloc_count = 0, free_count = 0<< malloc_count = 1, realloc_count = 0, free_count = 0err : !!! mem leak happentest_mem_leak(test no mem leak)*/return 0;
}void test_mem_leak(bool b_have_mem_leak)
{void* pBuf = NULL;printf("test_mem_leak(%s)\n", (b_have_mem_leak ? "test have mem leak" : "test no mem leak"));pBuf = OPENSSL_malloc(2);// do some task ...if (!b_have_mem_leak){OPENSSL_free(pBuf);pBuf = NULL;}
}
my_openSSL_lib.h
/*!
\file my_openSSL_lib.h
*/#ifndef __MY_OPENSSL_LIB_H__
#define __MY_OPENSSL_LIB_H__#ifdef __cplusplus
extern "C" {
#endif#ifdef _WIN32
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib") // for select()#include <windows.h>#include <stdbool.h>#pragma comment(lib, "libcrypto.lib")
#pragma comment(lib, "libssl.lib")#endif /* #ifdef _WIN32 */// --------------------------------------------------------------------------------
// 开关宏 - begin
// --------------------------------------------------------------------------------#define MY_USE_APPLINK// --------------------------------------------------------------------------------
// 开关宏 - END
// --------------------------------------------------------------------------------typedef struct _tag_openssl_mem_counter{int malloc_count_begin;int realloc_count_begin;int free_count_begin;int malloc_count_end;int realloc_count_end;int free_count_end;
} TAG_OPENSSL_MEM_COUNTER;void openssl_mdebug_begin();
bool openssl_mdebug_end(bool show_tip, bool b_assert);#ifdef __cplusplus
}
#endif#endif /* #ifndef __MY_OPENSSL_LIB_H__ */
my_openSSL_lib.c
/*!
* \file D:\my_dev\my_local_git_prj\study\openSSL\nmake_test\test_c\prj_005_afalgtest.c\my_openSSL_lib.c
*/#include "my_openSSL_lib.h"
#include "openssl/crypto.h" // for CRYPTO_get_alloc_counts#include <assert.h>#ifdef MY_USE_APPLINK
#include <openssl/applink.c> /*! for OPENSSL_Uplink(00007FF8B7EF0FE8,08): no OPENSSL_Applink */
#endif // #ifdef MY_USE_APPLINKstatic TAG_OPENSSL_MEM_COUNTER gs_openssl_mdebug;void openssl_mdebug_begin()
{CRYPTO_get_alloc_counts(&gs_openssl_mdebug.malloc_count_begin, &gs_openssl_mdebug.realloc_count_begin, &gs_openssl_mdebug.free_count_begin);
}bool openssl_mdebug_end(bool show_tip, bool b_assert)
{bool b_rc = false;long lCntBegin = 0;long lCntEnd = 0;CRYPTO_get_alloc_counts(&gs_openssl_mdebug.malloc_count_end, &gs_openssl_mdebug.realloc_count_end, &gs_openssl_mdebug.free_count_end);lCntBegin = gs_openssl_mdebug.malloc_count_begin + gs_openssl_mdebug.realloc_count_begin - gs_openssl_mdebug.free_count_begin;lCntEnd = gs_openssl_mdebug.malloc_count_end + gs_openssl_mdebug.realloc_count_end - gs_openssl_mdebug.free_count_end;b_rc = (lCntBegin == lCntEnd);if (!b_rc && show_tip){printf(">> malloc_count = %d, realloc_count = %d, free_count = %d\n", gs_openssl_mdebug.malloc_count_begin, gs_openssl_mdebug.realloc_count_begin, gs_openssl_mdebug.free_count_begin);printf("<< malloc_count = %d, realloc_count = %d, free_count = %d\n", gs_openssl_mdebug.malloc_count_end, gs_openssl_mdebug.realloc_count_end, gs_openssl_mdebug.free_count_end);printf("err : !!! mem leak happen\n");}if (b_assert){assert(true == b_rc);}return b_rc;
}
备注
以后有机缘再查查怎么用clang来查openssl应用是否有内存泄漏.
查资料时, 很多情况下都不是想查就能查到的.
很多时候, 是心里带着问题, 查其他资料时, 突然给了启发, 才将以前的问题搞掉.
这种调用CRYPTO_get_alloc_counts()来间接的排查是否调用opensslAPI时, 是否没有成对的分配,释放内存.
没有那么直接和方便, 不过也算是一种方法了. 有总比没有强.
希望以后能找到其他更好的方法来定位opensslAPI调用时的内存泄漏点.
备注
openssl的编译检查, 测试用例还是很严格的.
对外提供的API, 都有测试程序.
CRYPTO_get_alloc_counts()这个API的调用, 也能找到至少一处.
用SI将openssl源码编译的目录中的能识别的东东都包进来搜索, 必然能看到测试用例或者API调用的痕迹.
如果某个API虽然定义, 但是官方源码编译目录的实现中都没有用到, 那么咱么就不能用这个API.
这招不行啊
今天正好写个测试程序, 用到了这种内存泄漏检测方法. 有断言, 不好使.
int main(int argc, char** argv)
{BIO* bio = NULL;openssl_mdebug_begin();test_bio_mem_leak();// test_bio_new_mem_buf();openssl_mdebug_end(false, true);return 0;
}void test_bio_mem_leak(void)
{BIO* bio = BIO_new_mem_buf("Hello World\n", 12);BIO_free(bio);
}
只能先关注这事, 以后再想办法了.
跟了一下openssl代码, 是产生默认库上下文中有很多openssl_malloc()引起的内存分配.
后续再看看, 自己显势调用产生销毁默认上下文的API, 是否可以继续用这种方法.
显势调用默认上下文也不行
int main(int argc, char** argv)
{OSSL_LIB_CTX* _ossl_lib_ctx = NULL;BIO* bio = NULL;do {openssl_mdebug_begin();_ossl_lib_ctx = OSSL_LIB_CTX_get0_global_default();if (NULL == _ossl_lib_ctx){assert(false);break;}test_bio_mem_leak();// test_bio_new_mem_buf();OSSL_LIB_CTX_free(_ossl_lib_ctx);openssl_mdebug_end(true, false); // 到这里, 还是报错} while (false);return 0;
}void test_bio_mem_leak(void)
{BIO* bio = BIO_new_mem_buf("Hello World\n", 12);BIO_free(bio);
}
报错原因, openssl函数调用中, 会有其他默认的上下文建立会调用内存分配.
具体是啥函数, 要去单步.
这种用内存分配计数的方法, 不能真实的间接观察到openssl内部的内存泄漏, 做了一个没用的实验…
还是要老老实实的看官方例子, 手工调用对应API的释放函数.
找到一种还可以的解决方法, 现在看来可以准确观测到openssl内存泄漏点
openssl设计的可以, 预留了很多有用的API.
翻翻openssl提供的API, 找到了以下有用的API.
CRYPTO_get_mem_functions()
CRYPTO_set_mem_functions()
OPENSSL_init_crypto()
OPENSSL_cleanup()
这些可以用于辅助观测openssl内存泄漏点, 不止是openssl API, openssl内部发生的内存泄漏也能观测到(如果有的话)
内存泄漏观测的程序实现
main.cpp
/*!
* \file main.cpp
*/#include "my_openSSL_lib.h"
#include <openssl/crypto.h>
#include <openssl/bio.h>#include <stdlib.h>
#include <stdio.h>
#include <assert.h>#include "CMemHookRec.h"void test_bio_mem_leak(bool b_have_mem_leak);int main(int argc, char** argv)
{setvbuf(stdout, NULL, _IONBF, 0); // 清掉stdout缓存, 防止调用printf时阻塞// mem IF hook, 必须在执行任何openssl API之前替换mem_hook();// 自己的openssl接口应用test_bio_mem_leak(true);test_bio_mem_leak(false);// mem IF un hook, 必须在openssl结束后, 再恢复原始函数mem_unhook();/* run result, 如果有openssl应用接口没释放引起的内存泄漏, 有断言, stdout有输出一个openssl API引起的内存泄漏可能对应这多条内存分配记录, 不过排查范围缩小多了, 线索也清晰多了 结合对自己的openssl调用代码块的注释排除法, 很容易找到自己哪里没调用正确的openssl释放API.free map, g_mem_hook_map.size() = 42AAA2F36560 128 crypto\bio\bio_lib.c:832AAA2F4EC40 16 crypto\bio\bss_mem.c:1112AAA2FEA470 32 crypto\bio\bss_mem.c:1192AAA2FEAA10 32 crypto\buffer\buffer.c:35error : !!! find openssl call memory leakAssertion failed: false, file D:\my_dev\my_local_git_prj\soft\xx\src\case\exp002_mem_hook\main.cpp, line 57*/return 0;
}void test_bio_mem_leak(bool b_have_mem_leak)
{BIO* bio = BIO_new_mem_buf("Hello World\n", 12);if (false == b_have_mem_leak){BIO_free(bio);}
}
找内存泄漏时很方便, 只要在程序入口处调用mem_hook(), 然后自己正常调用openssl API, 程序结束前调用mem_unhook()
如果有openssl 释放API没调用引起的内存泄漏, 就会有断言.
如果是控制台程序, 就可以直接看到内存泄漏点.
如果不是控制台程序, 将mem_unhook()中的内存泄漏改写到其他输出(e.g. 文件).
这排查起来就容易多了.
CMemHookRec.h
/*!
\file CMemHookRec.h
\note openssl 分配API hook 之后, 记录的内存分配记录
*/#ifndef __CMEMHOOKREC_H__
#define __CMEMHOOKREC_H__#include <cstdint>
#include <stdio.h>
#include <map>
#include <openssl/crypto.h>#define SAFE_DELETE(p) \
do { \if (NULL != (p)) { \delete (p);\} \(p)=NULL; \
} while (0);class CMemHookRec
{
public:void show_info();public:uint64_t u64_addr;size_t num;const char* file;int line;uint64_t rec_sn;
};void mem_hook();
void mem_unhook();void* my_CRYPTO_malloc(size_t num, const char* file, int line);
void* my_CRYPTO_realloc(void* addr, size_t num, const char* file, int line);
void my_CRYPTO_free(void* ptr, const char* file, int line);
bool free_map();#endif // #ifndef __CMEMHOOKREC_H__
CMemHookRec.cpp
/// \file CMemHookRec.cpp#include "CMemHookRec.h"
#include <assert.h>void CMemHookRec::show_info()
{printf("entry sn = [%I64d], %I64X %zu %s:%d\n", this->rec_sn, u64_addr, num, ((NULL != file) ? file : "NULL"), line);
}std::map<uint64_t, CMemHookRec*> g_mem_hook_map;
typedef std::pair<uint64_t, CMemHookRec*> Map_Pair_mem_hook;
typedef std::map<uint64_t, CMemHookRec*>::iterator It_mem_hook;// malloc_fn_org 0x00007ffb0796614f {libcrypto-3-x64.dll!CRYPTO_malloc} void *(*)(unsigned __int64, const char *, int)
CRYPTO_malloc_fn malloc_fn_org = NULL;// realloc_fn_org 0x00007ffb07967a1d {libcrypto-3-x64.dll!CRYPTO_realloc} void *(*)(void *, unsigned __int64, const char *, int)
CRYPTO_realloc_fn realloc_fn_org = NULL;// free_fn_org 0x00007ffb079631b1 {libcrypto-3-x64.dll!CRYPTO_free} void(*)(void *, const char *, int)
CRYPTO_free_fn free_fn_org = NULL;void mem_hook()
{int i_rc = 0;uint64_t u64_init_opt = 0;CRYPTO_get_mem_functions(&malloc_fn_org, &realloc_fn_org, &free_fn_org);CRYPTO_set_mem_functions(my_CRYPTO_malloc, my_CRYPTO_realloc, my_CRYPTO_free);// openssl init allOPENSSL_INIT_SETTINGS* _ossl_init_setting = OPENSSL_INIT_new();u64_init_opt = OPENSSL_INIT_LOAD_CRYPTO_STRINGS |OPENSSL_INIT_ADD_ALL_CIPHERS |OPENSSL_INIT_ADD_ALL_DIGESTS |OPENSSL_INIT_LOAD_CONFIG |OPENSSL_INIT_ASYNC |OPENSSL_INIT_NO_ATEXIT;// 将openssl库中的静态初始化函数显势调用一下, 免得在我们手工调用API时(因为有些openssl对象或变量没初始化)自动调用静态初始化函数引起多余的非我们应用产生的内存分配// 初始化函数在应用中只能调用一次.i_rc = OPENSSL_init_crypto(u64_init_opt, _ossl_init_setting);assert(1 == i_rc);
}void mem_unhook()
{// openssl uninit all// 只能调用一次OPENSSL_cleanup();// 必须在openssl cleanup之后调用map显示, 这样才能过滤掉系统自动分配的内存.// 如果map不为空, 这些记录才是我们自己应用没释放的内存// 在free_map()时, 如果看到多条未被释放的内存, 也一定是那么多个API调用没释放, 因为一条openssl API会对应者多个openssl_mallc(), e.g. BIO_new_mem_buf() 就有4个内存分配动作.if (!free_map()){printf("error : !!! find openssl call memory leak\r\n");assert(false);/* run resultfree map, g_mem_hook_map.size() = 42AAA2F36560 128 crypto\bio\bio_lib.c:832AAA2F4EC40 16 crypto\bio\bss_mem.c:1112AAA2FEA470 32 crypto\bio\bss_mem.c:1192AAA2FEAA10 32 crypto\buffer\buffer.c:35error : !!! find openssl call memory leakAssertion failed: false, file D:\my_dev\my_local_git_prj\soft\krgy_software_protect\src\case\exp002_mem_hook\main.cpp, line 57*/}CRYPTO_set_mem_functions(malloc_fn_org, realloc_fn_org, free_fn_org);
}bool free_map()
{It_mem_hook it = g_mem_hook_map.end();size_t size = g_mem_hook_map.size();printf("free map, g_mem_hook_map.size() = %zd\n", size);for (it = g_mem_hook_map.begin(); it != g_mem_hook_map.end(); it++) {if (NULL != it->second) {it->second->show_info();}SAFE_DELETE(it->second);}g_mem_hook_map.clear();return (0 == size);
}static uint64_t g_u64_malloc_cnt_all = 0;
void* my_CRYPTO_malloc(size_t num, const char* file, int line)
{void* p = NULL;It_mem_hook it;CMemHookRec* rec = NULL;p = malloc(num);if (NULL != p){it = g_mem_hook_map.find((uint64_t)p);if (it != g_mem_hook_map.end()) {// printf("find key\n");assert(false);}else {// printf("not find key\n");rec = new CMemHookRec();if (NULL != rec){rec->rec_sn = ++g_u64_malloc_cnt_all;rec->u64_addr = (uint64_t)p;rec->num = num;rec->file = file;rec->line = line;g_mem_hook_map.insert(Map_Pair_mem_hook(rec->u64_addr, rec));}}}return p;
}void* my_CRYPTO_realloc(void* addr, size_t num, const char* file, int line)
{void* p = NULL;It_mem_hook it;CMemHookRec* rec = NULL;p = realloc(addr, num);if (NULL != p){it = g_mem_hook_map.find((uint64_t)addr);if (it != g_mem_hook_map.end()) {// printf("find key\n");SAFE_DELETE(it->second);g_mem_hook_map.erase(it);}rec = new CMemHookRec();if (NULL != rec){rec->rec_sn = ++g_u64_malloc_cnt_all;rec->u64_addr = (uint64_t)p;rec->num = num;rec->file = file;rec->line = line;g_mem_hook_map.insert(Map_Pair_mem_hook(rec->u64_addr, rec));}}return p;
}void my_CRYPTO_free(void* ptr, const char* file, int line)
{It_mem_hook it;CMemHookRec* rec = NULL;if (NULL != ptr){free(ptr);it = g_mem_hook_map.find((uint64_t)ptr);if (it != g_mem_hook_map.end()) {// printf("find key\n");SAFE_DELETE(it->second);g_mem_hook_map.erase(it);}else {// printf("not find key\n");assert(false);}}
}
备注
此笔记前面实验的CRYPTO_get_alloc_counts()方法, 在mem_hook中用不了了, 因为将内存分配函数换掉了.
不过, 可以在上面这个实验上改改, 不hook内存分配函数, 只用 OPENSSL_init_crypto() + OPENSSL_cleanup() + CRYPTO_get_alloc_counts(), 应该也能看到内存泄漏的计数, 但是看不到哪个文件哪行发生的泄漏.
因为这个想法不好, 我就没去再实验.
毕竟有多个解决方法, 只取最优的那个.
mem_hook()这个方法好, 能看到发生泄漏的具体库文件和行数, 排查起来可操作性强.
备注
将 void CMemHookRec::show_info()的输出改了一下, 上面的实验代码已经更新.
void CMemHookRec::show_info()
{printf("entry sn = [%I64d], %I64X %zu %s:%d\n", this->rec_sn, u64_addr, num, ((NULL != file) ? file : "NULL"), line);
}
这样的话, 等观测到内存泄漏时, 号码连续的就是一个openssl API的调用引起的内存泄漏, 更方便调试排查.