?
openssl是一個很有名的開源軟件,它在解決SSL/TLS通訊上提供了一套行之有效的解決方案,同時在軟件算法領(lǐng)域,它也集成絕大部分常見的算法,真可謂是程序員開發(fā)網(wǎng)絡通訊和信息安全加解密的一個利器。
熟悉github的朋友,一定在github上目睹過openssl的真容【https://github.com/openssl/openssl】,它的官網(wǎng)地址是【/index.html】。就拿github來說,高達8.8K顆星,被fork4千多次,總共有2萬多次的提交記錄,足以可見該開源項目的熱度有多高。
?編輯
然而,就是這樣的一個開源利器,能給我們工作帶來便利的同時,倘若你使用不當,那么給你帶來的不是喜悅,而是煩惱。通過觀察openssl提供的API,你會發(fā)現(xiàn),它的很多API返回的都是指針類型,在應用層調(diào)用時,僅需用一個對應類型的指針去接收返回的指針,即可取得對應的數(shù)據(jù)或操作方法,使用非常靈活。類似這樣的接口有很多,例如:
//新生成一個BIGNUM結(jié)構(gòu)
BIGNUM *BN_new(void);
//將s中的len位的正整數(shù)轉(zhuǎn)化為大數(shù)
BIGNUM *BN_bin2bn(const unsigned char *s, int len, BIGNUM *ret);
//初始化一個RSA結(jié)構(gòu)
RSA * RSA_new(void);
//RSA私鑰產(chǎn)生函數(shù)
//產(chǎn)生一個模為num位的密鑰對,e為公開的加密指數(shù),一般為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結(jié)構(gòu),釋放完后a=NULL;
void BN_free(BIGNUM *a);
//釋放一個RSA結(jié)構(gòu)
void RSA_free(RSA *rsa);
看到這里,也許你就會明白我今天要講的主題了,既然這些“生成”API提供了返回指針類型的功能,那么很明顯指針所指向內(nèi)容的存儲空間,必定是在openssl內(nèi)部通過malloc等動態(tài)內(nèi)存申請的方式獲取的;所以在使用了這段內(nèi)存后,自然而然就是要執(zhí)行內(nèi)存釋放的動作,這與C語言動態(tài)內(nèi)存申請講的“malloc--free”必須配套使用,是如出一轍的;只不過,現(xiàn)在這些openssl的API是把malloc和free的動作封裝在了接口的內(nèi)部,而暴露給調(diào)用者的只有類型XXX_init和XXX_free,或者XXX_new和XXX_delete,諸如此類的接口,僅此而已。
回到openssl的API接口的使用上來,博主有一次在使用openssl的某個接口有些疑惑,想在網(wǎng)上找找調(diào)用的demo時,結(jié)果一搜,一眼就進到 【OpenSSL編程-RSA編程詳解 http://www.qmailer.net/archives/216.html】這篇博文。它確實給初學者提供了幾組常用API的簡單demo,正常情況下這些代碼都是能跑通的,但近來我在日常工作中,有在做一些【內(nèi)存泄露】相關(guān)的【代碼優(yōu)化】工作,所以對這個切入點比較敏感,果不其然,細讀其中的部分示例代碼,就發(fā)現(xiàn)了其中不嚴謹?shù)牡胤?,很有可能就會發(fā)生【內(nèi)存泄露】的風險。如果本身系統(tǒng)的內(nèi)存比較吃緊,比如像在嵌入式系統(tǒng)上運行,這樣的【內(nèi)存泄露】可以說是致命的。
還是拿代碼來說事,以下代碼片段是上文提及的參考博文中截取到的,如下:
1. 數(shù)據(jù)加、密解密示例
#include
#include
#include
#include
#include
#include
#define PRIKEY "prikey.pem"
#define PUBKEY "pubkey.pem"
#define BUFFSIZE 4096
/************************************************************************
* RSA加密解密函數(shù)
*
* 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; //函數(shù)出口1
}
/* 讀取公鑰PEM,PUBKEY格式PEM使用PEM_read_RSA_PUBKEY函數(shù) */
if ((rsa = PEM_read_RSAPublicKey(fp, NULL, NULL, NULL)) == NULL) {
return NULL; //函數(shù)出口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; //函數(shù)出口3
}
RSA_free(rsa);
fclose(fp);
return en; //函數(shù)出口4
}
通過簡單分析,我們可以知道m(xù)y_encrypt這個函數(shù),有一個入口,但是有4個出口(見代碼注釋):
函數(shù)出口1: 很明顯出錯的可能性是,打開pubkey_path文件失敗,這個很好理解,可能是文件不存在,或者路徑文件不正確等等,此處出錯,對外返回NULL,是完全沒有問題的。
函數(shù)出口2: 這里出錯的可能性是fp指向的pubkey_path文件,壓根不是一個pem格式的公鑰文件,自然會出錯;但是此處直接對外返回NULL,而不管fp的死活,這是不可取的!
函數(shù)出口3: 這里出錯的可能是公鑰加密輸入的數(shù)據(jù)長度不對或者數(shù)據(jù)填充不對等等,然而這里也是出錯后,立即對外返回NULL,完全不理fp和rsa,還有en這條友【往往更容易忽略】,這3者的死活,更是不可取的!
函數(shù)出口4: 這個沒的說,正常的函數(shù)出口;大家注意,在這個正常的函數(shù)出口中,它在return前是執(zhí)行了RSA_free(rsa); fclose(fp);
的動作;沒錯,這個就是我們要講的使用完的內(nèi)存要及時釋放,它的使用需要注意的關(guān)鍵點就在這里。那么如上可能出現(xiàn)內(nèi)存泄露的代碼片段應該如何優(yōu)化呢?直接貼上,優(yōu)化后的示例代碼:
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語句,保證函數(shù)單一入口,單一出口
}
/* 讀取公鑰PEM,PUBKEY格式PEM使用PEM_read_RSA_PUBKEY函數(shù) */
if ((rsa = PEM_read_RSAPublicKey(fp, NULL, NULL, NULL)) == NULL) {
return NULL;
goto exit_entry; //使用goto語句,保證函數(shù)單一入口,單一出口
}
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申請不到內(nèi)存的時候,也不能往下執(zhí)行了,需要退出
}
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理論上已經(jīng)不為空了,當在這一步出錯時,對外en的內(nèi)存已經(jīng)變得無意義了,所以必須要釋放掉,同時將en置為NULL,防止外部調(diào)用者邏輯出錯
en = NULL;
}
goto exit_entry;
}
exit_entry: //函數(shù)統(tǒng)一出口,退出前執(zhí)行相應的內(nèi)存釋放動作
//先判斷是否需要執(zhí)行內(nèi)存釋放
if (rsa) {
RSA_free(rsa);
}
//文件打開的fp句柄要及時關(guān)閉
if (fp) {
fclose(fp);
}
return en;
}
通過如上的示例代碼,基本上很好地修復了因異常情況處理不當導致的【內(nèi)存泄露】隱患,同時利用goto語句,使得函數(shù)的結(jié)構(gòu)的緊湊性有所提高,代碼的可讀性也提升了不少。
有的朋友可能會有疑問,“我們在學C語言教程的時候,老師不是常常跟我們說,盡量不要使用goto語句,這樣會帶來代碼災難,為何博主卻推薦使用goto語句來優(yōu)化代碼呢?”
原因很簡單,C語言的goto語句并不會造成代碼災難,而是使用goto語句的程序員造成的災難!怎么說呢,goto語句是有點偏匯編層面的關(guān)鍵字,它有點像匯編指令里面的jump指令,也就是說使用好它,指不定還可以提升代碼的運行效率。但是值得注意的是,goto語句不能濫用,尤其是使用goto語句往前跳轉(zhuǎn),或者使用goto語句執(zhí)行遞歸、循環(huán)等操作時,代碼的邏輯將有可能變得不可控制,或者難以控制,基本上除了寫代碼本身的人能讀懂外【估計過個一兩個月,他自己也讀不懂了】,其他人估計就摸不著頭腦了。但是,如果像用在如上所優(yōu)化的代碼那樣,僅僅在函數(shù)的出口做一個symbol標簽,當函數(shù)中間執(zhí)行異常的時候,立即跳轉(zhuǎn)到定義的出口標簽,同時執(zhí)行一些函數(shù)退出的收尾工作,比如清理內(nèi)存、釋放不再使用的內(nèi)存、接口返回值轉(zhuǎn)換等操作;這樣的代碼,將會大大提升了代碼的可讀性,這也盡可能地將錯誤規(guī)避掉,讓bug無處藏身,代碼更加整潔,反而能編寫出可讀性強的高質(zhì)量代碼。
如上僅是提出了對【內(nèi)存泄露】的小小看法和感悟,借助openssl的demo例子,也僅僅是拋磚引玉,也許讀者有更高的見解。期待有讀者與我一同討論相關(guān)話題。
注:文中引用了【博主:大佟,文章地址:http://www.qmailer.net/archives/216.html】的示例代碼,如有版權(quán)問題,請及時與我聯(lián)系。不勝感激!
?審核編輯:湯梓紅
-
接口
+關(guān)注
關(guān)注
33文章
8598瀏覽量
151153 -
開源
+關(guān)注
關(guān)注
3文章
3349瀏覽量
42499 -
OpenSSL
+關(guān)注
關(guān)注
0文章
21瀏覽量
8691
發(fā)布評論請先 登錄
相關(guān)推薦
評論