openssl是一个很有名的开源软件,它在解决SSL/TLS通讯上提供了一套行之有效的解决方案,同时在软件算法领域,它也集成绝大部分常见的算法,真可谓是程序员开发网络通讯和信息安全加解密的一个利器。
熟悉github的朋友,一定在github上目睹过openssl的真容【https://github.com/openssl/openssl】,它的官网地址是【/index.html】。就拿github来说,高达8.8K颗星,被fork4千多次,总共有2万多次的提交记录,足以可见该开源项目的热度有多高。
编辑
然而,就是这样的一个开源利器,能给我们工作带来便利的同时,倘若你使用不当,那么给你带来的不是喜悦,而是烦恼。通过观察openssl提供的API,你会发现,它的很多API返回的都是指针类型,在应用层调用时,仅需用一个对应类型的指针去接收返回的指针,即可取得对应的数据或操作方法,使用非常灵活。类似这样的接口有很多,例如:
//新生成一个BIGNUM结构
BIGNUM *BN_new(void);
//将s中的len位的正整数转化为大数
BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret);
//初始化一个RSA结构
RSA * RSA_new(void);
//RSA私钥产生函数
//产生一个模为num位的密钥对,e为公开的加密指数,一般为65537(0x10001)
RSA *RSA_generate_key(int num, unsigned long e,void (*callback)(int,int,void *), void *cb_arg);
//从文件中加载RSAPublicKey格式公钥证书
RSA *PEM_read_RSAPublicKey(FILE *fp, RSA **x, pem_password_cb *cb, void *u);
//从BIO重加载RSAPublicKey格式公钥证书
RSA *PEM_read_bio_RSAPublicKey(BIO *bp, RSA **x, pem_password_cb *cb, void *u);
聪明的你,留意到这些“生成”功能的API接口的同时,一定也留意到它们都有对应的“销毁”API接口。上面列表一一对应的是:
//释放一个BIGNUM结构,释放完后a=NULL;
void BN_free(BIGNUM *a);
//释放一个RSA结构
void RSA_free(RSA *rsa);
看到这里,也许你就会明白我今天要讲的主题了,既然这些“生成”API提供了返回指针类型的功能,那么很明显指针所指向内容的存储空间,必定是在openssl内部通过malloc等动态内存申请的方式获取的;所以在使用了这段内存后,自然而然就是要执行内存释放的动作,这与C语言动态内存申请讲的“malloc--free”必须配套使用,是如出一辙的;只不过,现在这些openssl的API是把malloc和free的动作封装在了接口的内部,而暴露给调用者的只有类型XXX_init和XXX_free,或者XXX_new和XXX_delete,诸如此类的接口,仅此而已。
回到openssl的API接口的使用上来,博主有一次在使用openssl的某个接口有些疑惑,想在网上找找调用的demo时,结果一搜,一眼就进到 【OpenSSL编程-RSA编程详解 http://www.qmailer.net/archives/216.html】这篇博文。它确实给初学者提供了几组常用API的简单demo,正常情况下这些代码都是能跑通的,但近来我在日常工作中,有在做一些【内存泄露】相关的【代码优化】工作,所以对这个切入点比较敏感,果不其然,细读其中的部分示例代码,就发现了其中不严谨的地方,很有可能就会发生【内存泄露】的风险。如果本身系统的内存比较吃紧,比如像在嵌入式系统上运行,这样的【内存泄露】可以说是致命的。
还是拿代码来说事,以下代码片段是上文提及的参考博文中截取到的,如下:
1. 数据加、密解密示例
#include
#include
#include
#include
#include
#include
#define PRIKEY "prikey.pem"
#define PUBKEY "pubkey.pem"
#define BUFFSIZE 4096
/************************************************************************
* RSA加密解密函数
*
* file: test_rsa_encdec.c
* gcc -Wall -O2 -o test_rsa_encdec test_rsa_encdec.c -lcrypto -lssl
*
* author: tonglulin@gmail.com by www.qmailer.net
************************************************************************/
char *my_encrypt(char *str, char *pubkey_path)
{
RSA *rsa = NULL;
FILE *fp = NULL;
char *en = NULL;
int len = 0;
int rsa_len = 0;
if ((fp = fopen(pubkey_path, "r")) == NULL) {
return NULL; //函数出口1
}
/* 读取公钥PEM,PUBKEY格式PEM使用PEM_read_RSA_PUBKEY函数 */
if ((rsa = PEM_read_RSAPublicKey(fp, NULL, NULL, NULL)) == NULL) {
return NULL; //函数出口2
}
RSA_print_fp(stdout, rsa, 0);
len = strlen(str);
rsa_len = RSA_size(rsa);
en = (char *)malloc(rsa_len + 1);
memset(en, 0, rsa_len + 1);
if (RSA_public_encrypt(rsa_len, (unsigned char *)str, (unsigned char*)en, rsa, RSA_NO_PADDING) < 0) {
return NULL; //函数出口3
}
RSA_free(rsa);
fclose(fp);
return en; //函数出口4
}
通过简单分析,我们可以知道my_encrypt这个函数,有一个入口,但是有4个出口(见代码注释):
函数出口1: 很明显出错的可能性是,打开pubkey_path文件失败,这个很好理解,可能是文件不存在,或者路径文件不正确等等,此处出错,对外返回NULL,是完全没有问题的。
函数出口2: 这里出错的可能性是fp指向的pubkey_path文件,压根不是一个pem格式的公钥文件,自然会出错;但是此处直接对外返回NULL,而不管fp的死活,这是不可取的!
函数出口3: 这里出错的可能是公钥加密输入的数据长度不对或者数据填充不对等等,然而这里也是出错后,立即对外返回NULL,完全不理fp和rsa,还有en这条友【往往更容易忽略】,这3者的死活,更是不可取的!
函数出口4: 这个没的说,正常的函数出口;大家注意,在这个正常的函数出口中,它在return前是执行了 RSA_free(rsa); fclose(fp);
的动作;没错,这个就是我们要讲的使用完的内存要及时释放,它的使用需要注意的关键点就在这里。那么如上可能出现内存泄露的代码片段应该如何优化呢?直接贴上,优化后的示例代码:
char *my_encrypt(char *str, char *pubkey_path)
{
RSA *rsa = NULL;
FILE *fp = NULL;
char *en = NULL;
int len = 0;
int rsa_len = 0;
if ((fp = fopen(pubkey_path, "r")) == NULL) {
en = NULL;
goto exit_entry; //使用goto语句,保证函数单一入口,单一出口
}
/* 读取公钥PEM,PUBKEY格式PEM使用PEM_read_RSA_PUBKEY函数 */
if ((rsa = PEM_read_RSAPublicKey(fp, NULL, NULL, NULL)) == NULL) {
return NULL;
goto exit_entry; //使用goto语句,保证函数单一入口,单一出口
}
RSA_print_fp(stdout, rsa, 0);
len = strlen(str);
rsa_len = RSA_size(rsa);
en = (char *)malloc(rsa_len + 1);
if (!en) {
goto exit_entry; //当en申请不到内存的时候,也不能往下执行了,需要退出
}
memset(en, 0, rsa_len + 1);
if (RSA_public_encrypt(rsa_len, (unsigned char *)str, (unsigned char*)en, rsa, RSA_NO_PADDING) < 0) {
if (en) {
free(en); //走到这里的时候en理论上已经不为空了,当在这一步出错时,对外en的内存已经变得无意义了,所以必须要释放掉,同时将en置为NULL,防止外部调用者逻辑出错
en = NULL;
}
goto exit_entry;
}
exit_entry: //函数统一出口,退出前执行相应的内存释放动作
//先判断是否需要执行内存释放
if (rsa) {
RSA_free(rsa);
}
//文件打开的fp句柄要及时关闭
if (fp) {
fclose(fp);
}
return en;
}
通过如上的示例代码,基本上很好地修复了因异常情况处理不当导致的【内存泄露】隐患,同时利用goto语句,使得函数的结构的紧凑性有所提高,代码的可读性也提升了不少。
有的朋友可能会有疑问,“我们在学C语言教程的时候,老师不是常常跟我们说,尽量不要使用goto语句,这样会带来代码灾难,为何博主却推荐使用goto语句来优化代码呢?”
原因很简单,C语言的goto语句并不会造成代码灾难,而是使用goto语句的程序员造成的灾难!怎么说呢,goto语句是有点偏汇编层面的关键字,它有点像汇编指令里面的jump指令,也就是说使用好它,指不定还可以提升代码的运行效率。但是值得注意的是,goto语句不能滥用,尤其是使用goto语句往前跳转,或者使用goto语句执行递归、循环等操作时,代码的逻辑将有可能变得不可控制,或者难以控制,基本上除了写代码本身的人能读懂外【估计过个一两个月,他自己也读不懂了】,其他人估计就摸不着头脑了。但是,如果像用在如上所优化的代码那样,仅仅在函数的出口做一个symbol标签,当函数中间执行异常的时候,立即跳转到定义的出口标签,同时执行一些函数退出的收尾工作,比如清理内存、释放不再使用的内存、接口返回值转换等操作;这样的代码,将会大大提升了代码的可读性,这也尽可能地将错误规避掉,让bug无处藏身,代码更加整洁,反而能编写出可读性强的高质量代码。
如上仅是提出了对【内存泄露】的小小看法和感悟,借助openssl的demo例子,也仅仅是抛砖引玉,也许读者有更高的见解。期待有读者与我一同讨论相关话题。
注:文中引用了【博主:大佟,文章地址:http://www.qmailer.net/archives/216.html】的示例代码,如有版权问题,请及时与我联系。不胜感激!
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !