鏈接OpenSSL:libeay64/ssleay64庫編譯與集成)
簡介OpenSSL 1.0 的 64 位靜態(tài)庫難找libeay64.lib 與 ssleay64.lib 一包搞定。資源面向在 Windows 下使用 Visual Studio 開發(fā)、需要直接鏈接 OpenSSL 的 C/C 程序員省去編譯源碼、排查工具鏈的繁瑣過程。壓縮包共 168 個文件以 138 個頭文件為主覆蓋 SSL、EVP、X509、EC 等常用模塊的 API 聲明與常量定義另含 25 個 .in 配置模板、4 個 .lib 庫文件及 1 個 applink.c整體僅 5.14MB方便快速集成到 x64 工程。作者附帶了條件編譯示例可按 _M_X64 自動選擇 64 位或 32 位庫免去手動調(diào)整項目配置的麻煩。已有 313 人學習下載如果你正需要 OpenSSL 1.0 的加密與 SSL/TLS 能力這份編譯好的資源可明顯縮短開發(fā)準備周期。 做Windows平臺C/C開發(fā)的大概率都跟OpenSSL打過交道尤其是老項目。最近因為接手一個歷史遺留的64位服務(wù)端程序需要在Visual Studio環(huán)境里靜態(tài)鏈接OpenSSL目標就是標題里這個經(jīng)典的組合libeay64.lib和ssleay64.lib。可能有人覺得奇怪OpenSSL 1.0都EOL多少年了怎么還有人往回找現(xiàn)實就是存量系統(tǒng)、商業(yè)組件綁定、老設(shè)備協(xié)議棧這些不是說換就能換的。與其天天被兼容性問題折磨不如把這一套老庫的獲取、編譯、集成、排障路徑徹底捋清楚。這篇東西適合誰三類人一是正在維護Windows老項目的工程師二是需要在不升級系統(tǒng)的情況下給老服務(wù)補安全補丁的朋友三是單純想搞明白“為什么我編出來的OpenSSL庫文件名和教程不一致”的新手。我盡量按實操路線講從原理到命令再到VS里的配置全走一遍。1. 項目背景與需求拆解1.1 這倆文件到底是什么來頭libeay64.lib和ssleay64.lib叫法上是OpenSSL在Windows平臺上的64位靜態(tài)庫。更準確地說libeay對應(yīng)的是OpenSSL的底層加解密庫cryptossleay對應(yīng)的是SSL/TLS協(xié)議層ssl。這兩個名字是老傳統(tǒng)的延續(xù)早期OpenSSL在Windows上分成libeay32和ssleay32后來為了區(qū)分32位和64位很多團隊在編譯或分發(fā)時會把64位版本改名為libeay64.lib、ssleay64.lib。這里有個特別容易踩的坑OpenSSL官方1.0.x版本的Makefile即使是64位編譯默認生成的靜態(tài)庫名依然叫l(wèi)ibeay32.lib和ssleay32.lib只有個別第三方發(fā)布包或自己改過的構(gòu)建腳本才會用libeay64.lib這個名字。所以你如果下載到名為libeay64.lib的庫不要懷疑它就是OpenSSL 1.0.x的64位靜態(tài)版只是編譯者改了輸出名。1.2 為什么還在用OpenSSL 1.0聊這個得講點實在的。OpenSSL 1.0.2u是1.0系列的最后一個版本官方早已停止維護但是它在行業(yè)里的存量極多。常見原因包括老產(chǎn)品代碼基于1.0.x的API寫的升級到1.1.x甚至3.x接口變更太猛維護成本高。某些行業(yè)軟件、硬件設(shè)備、加密機只認1.0的協(xié)議棧行為。項目里其他第三方庫和OpenSSL耦合太深牽一發(fā)動全身。系統(tǒng)是老版本Linux或Windows編譯新版本依賴太高跑不動。所以在這個背景下我強烈建議如果你能用OpenSSL 1.1.1或3.x優(yōu)先用新的如果實在離不開1.0至少把補丁級別打到1.0.2u并且把降級、遷移的成本提前評估好。下面所有內(nèi)容都是基于1.0.2u這個版本做的。1.3 靜態(tài)庫與動態(tài)庫的選擇邏輯標題里點明要的是靜態(tài)庫這背后的部署需求很典型。靜態(tài)庫的優(yōu)點是鏈接進exe/dll后不依賴外部OpenSSL動態(tài)庫部署機器上不用特意裝VC運行庫之外的DLL程序拷過去就能跑。缺點是多個模塊鏈接同一份靜態(tài)庫時內(nèi)存里會有多份代碼副本而且一旦OpenSSL有安全更新你得重新編譯整個程序。動態(tài)庫的優(yōu)點是方便升級DLL但Windows上“DLL地獄”大家都懂特別是OpenSSL這種依賴一堆系統(tǒng)庫的組件DLL版本不一致會直接導(dǎo)致加載失敗或運行時崩潰。很多企業(yè)級軟件堅持用靜態(tài)庫就是為了規(guī)避這種問題。我在做這塊的時候也延續(xù)了這個思路最終交付的目錄里絕對不允許出現(xiàn)libeay32.dll和ssleay32.dll這種運行期依賴。2. 工具鏈準備與編譯原理2.1 編譯OpenSSL 1.0.x需要哪些武器Windows上編OpenSSL靜態(tài)庫核心工具四件套工具作用推薦選擇PerlOpenSSL的Configure腳本依賴Perl沒有它寸步難行Strawberry Perl 或 ActivePerlNASM匯編優(yōu)化編譯x64匯編代碼提速關(guān)鍵算法NASM 2.14及以上Visual StudioC編譯器、頭文件、nmake構(gòu)建工具VS2015/2017/2019均可命令行環(huán)境必須用VS提供的x64環(huán)境普通cmd沒有nmake所需變量“x64 Native Tools Command Prompt”這里說明一下為什么裝NASM。OpenSSL里AES、SHA等核心實現(xiàn)有匯編版本性能比純C快很多64位編譯時默認會嘗試匯編優(yōu)化。如果你機器上沒裝NASMConfigure階段可以用no-asm參數(shù)跳過但編出來的庫性能會差一些。我們平時自己用、給內(nèi)部項目用性能不是瓶頸所以no-asm也不丟人但如果你做的是網(wǎng)關(guān)、負載均衡這類高吞吐組件NASM必須裝而且版本不能太老。2.2 為什么必須用VS的x64命令行很多朋友在普通cmd里敲nmake結(jié)果提示“未找到命令”是因為nmake不是系統(tǒng)命令它由VS安裝目錄提供需要vcvars64.bat之類的環(huán)境初始化。最穩(wěn)妥的方式是直接打開“開始菜單 - Visual Studio 2019 - x64 Native Tools Command Prompt for VS 2019”這樣環(huán)境變量、PATH、INCLUDE、LIB都已就緒后面編譯OpenSSL時Perl和nmake能正確找到編譯器。2.3 編譯目標的配置邏輯OpenSSL 1.0.2的Configure參數(shù)里有一個關(guān)鍵選項VC-WIN64A。這個字符串代表“Visual C Windows x64平臺”。很多人會寫VC-WIN32那是32位的別搞混。我們需要靜態(tài)庫所以目標makefile選nt.mak而不是ntdll.mak。nt.mak生成靜態(tài)庫ntdll.mak生成動態(tài)庫這個區(qū)別非常關(guān)鍵。我在本地實操時用的配置過程大致如下perl Configure VC-WIN64A no-asm --prefixC:\build\openssl-1.0.2u-x64 ms\do_win64a.bat nmake -f ms\nt.mak這里刻意用了no-asm來減小環(huán)境依賴如果你裝了NASM且希望啟用匯編優(yōu)化去掉no-asm即可。另外--prefix參數(shù)是指定后續(xù)nmake install的安裝目錄如果你只需要編譯產(chǎn)物不需要全局安裝這個參數(shù)可以留著反正不影響靜態(tài)庫生成本身。整個過程中如果報錯先檢查Perl版本和VS環(huán)境OpenSSL 1.0.2對Perl 5.30以上的兼容性偶爾會有小毛病但Strawberry Perl 5.32實測能編過。3. 編譯流程與集成實操3.1 從源碼到libeay64.lib的完整步驟為了保證大家復(fù)現(xiàn)時不打轉(zhuǎn)我按自己實際跑通的順序完整列一遍。假設(shè)你把OpenSSL 1.0.2u源碼解壓到了C:\openssl-1.0.2uVS打開的是x64 Native Tools命令行。cd C:\openssl-1.0.2u perl Configure VC-WIN64A no-asm --prefixC:\build\openssl-1.0.2u-x64 ms\do_win64a.bat nmake -f ms\nt.mak執(zhí)行完三步后文件會生成在源碼目錄下的out64文件夾里典型產(chǎn)物包括libeay32.lib、ssleay32.lib、libeay32.dllnt.mak理論上不生成dll但有時候我之前手里的版本會順帶輸出dll見鬼以及一堆頭文件。接下來如果你想要標題里說的libeay64.lib和ssleay64.lib只要手動把out64里的libeay32.lib重命名成libeay64.libssleay32.lib重命名成ssleay64.lib即可。當然你也可以在Makefile里改輸出名但改動過多容易引入問題手動重命名最省心。我建議把重命名后的庫連同頭文件一起放到一個獨立的目錄比如C:\ThirdParty\OpenSSL-1.0.2u\x64\lib和C:\ThirdParty\OpenSSL-1.0.2u\x64\include方便后續(xù)各個項目統(tǒng)一引用。3.2 Visual Studio項目的鏈接配置庫編出來了頭文件拷好了接下來是VS工程里怎么正確引用。這部分我踩過不少坑重點是以下幾個方面。第一C/C - 常規(guī) - 附加包含目錄添加crypto頭文件和ssl頭文件的所在目錄。OpenSSL 1.0的頭文件結(jié)構(gòu)比較樸素include目錄下直接就是openssl文件夾你不需要額外加openssl子目錄作為包含目錄編譯器會在代碼里寫#include openssl/ssl.h所以附加目錄指到include上級即可。第二鏈接器 - 常規(guī) - 附加庫目錄指向libeay64.lib、ssleay64.lib所在目錄。第三鏈接器 - 輸入 - 附加依賴項至少要寫libeay64.lib ssleay64.lib ws2_32.lib crypt32.lib user32.lib advapi32.lib gdi32.lib為什么還需要后面這幾個系統(tǒng)庫因為OpenSSL在Windows上要調(diào)用socket相關(guān)APIws2_32、證書庫crypt32和底層系統(tǒng)服務(wù)advapi32。不把這些補全鏈接階段會報一堆LNK2001無法解析的外部符號。靜態(tài)庫的特性就是你的exe必須把所有依賴收口不能指望DLL幫你兜底。第四預(yù)處理定義里不要漏掉OPENSSL_USE_APPLINK這個宏在一些場景下和Windows的applink.c機制有關(guān)如果你鏈接時出現(xiàn)與文件訪問、動態(tài)加載相關(guān)的詭異問題大概率是這個宏沒定義。完整的做法是編譯OpenSSL時在工程里加入applink.c文件但多數(shù)場景加上這個宏就夠了。3.3 運行庫類型要跟庫的編譯選項保持一致這個坑幾乎人人都會踩。OpenSSL 1.0.x的官方構(gòu)建腳本編譯時默認使用動態(tài)運行庫/MD。如果你的VS項目設(shè)置成了多線程調(diào)試/MTd或者多線程/MT鏈接時就會報類似“LNK2038檢測到RuntimeLibrary的不匹配”的錯。解決方案有兩個方向方向一把項目運行庫改成“多線程DLL/MD”或“多線程調(diào)試DLL/MDd”跟OpenSSL保持一致。方向二在Configure階段給OpenSSL加上額外參數(shù)讓它編譯時也用靜態(tài)運行庫這需要改Configure腳本或環(huán)境變量復(fù)雜且容易出錯。我的建議是你項目里統(tǒng)一定義成/MD因為Windows上大多數(shù)第三方庫都是這個默認值改OpenSSL反而孤僻。另外注意如果你的exe最終部署到?jīng)]有安裝VC運行庫的機器上那你需要在發(fā)布包里帶上對應(yīng)版本的VC Runtime或者在項目里用靜態(tài)運行庫/MT并重新編譯整個依賴鏈。這屬于另一個層面的部署選擇改之前先想清楚。3.4 驗證庫是否鏈接成功鏈接成功不代表萬事大吉還是得寫一段最簡單的代碼驗證一下比如打印版本號、做一個TLS握手示例。一個小例子#include stdio.h #include openssl/ssl.h int main() { SSL_library_init(); SSL_CTX* ctx SSL_CTX_new(TLS_client_method()); if (ctx) { printf(OpenSSL version: %s\n, OpenSSL_version(OPENSSL_VERSION)); SSL_CTX_free(ctx); } return 0; }這段代碼在1.0.2里編譯會有個小問題OpenSSL_version這個函數(shù)是1.1.0才加的1.0.2里對應(yīng)的應(yīng)該是SSLeay_version(SSLEAY_VERSION)。所以如果你拿到的是1.0.x庫用下面這版#include stdio.h #include openssl/ssl.h int main() { SSL_library_init(); SSL_CTX* ctx SSL_CTX_new(SSLv23_client_method()); if (ctx) { printf(OpenSSL version: %s\n, SSLeay_version(SSLEAY_VERSION)); SSL_CTX_free(ctx); } return 0; }能正常編譯并打印出OpenSSL 1.0.2u字樣說明鏈接鏈路沒問題。再用Dependency Walker或系統(tǒng)自帶dumpbin檢查exe的導(dǎo)入表如果里面有l(wèi)ibeay64.lib和ssleay64.lib里導(dǎo)出的符號且沒有依賴libeay32.dll那就是標準的靜態(tài)鏈接成功。4. 常見問題與排查技巧實錄4.1 鏈接期符號解析失敗的排查套路使用靜態(tài)庫時LNK2001和LNK2019是最常見的鏈接錯誤。除了上一節(jié)提到的系統(tǒng)庫依賴還容易漏掉OpenSSL內(nèi)部依賴。比如你在代碼里用了EVP相關(guān)的函數(shù)但鏈接器提示無法解析EVP_aes_256_cbc這時候首先要檢查libeay64.lib是否真的進入了附加依賴項因為EVP系列函數(shù)屬于crypto庫也就是libeay64.lib。如果確認庫都在但符號還是找不到可以用dumpbin工具看導(dǎo)出的符號名是否和你調(diào)用的API一致比如dumpbin /LINKERMEMBER libeay64.lib | findstr EVP_aes_256_cbc如果導(dǎo)出了但名稱后面帶一個數(shù)字那說明你鏈接的庫是32位的thiscall約定或編譯器調(diào)用約定不匹配。但OpenSSL是C接口正常情況不會這樣出現(xiàn)這種詭異現(xiàn)象十有八九是庫混用了32位的庫、64位的項目或者反過來。記住libeay64.lib是64位庫VS項目平臺必須是x64不是x86。4.2 /MT還是/MD導(dǎo)致的LNK2038問題LNK2038字面意思是運行時庫不匹配。這個問題我在幫同事排查時見過太多次。OpenSSL 1.0.2的官方構(gòu)建默認是/MD所以項目里最好也設(shè)置成“多線程DLL/MD”。如果你必須用/MT需要手工修改OpenSSL的編譯參數(shù)讓Perl配置時傳入--with-ssl-dir是沒用的必須在makefile里調(diào)整CFLAGS的/MD為/MT然后重新編譯整個OpenSSL。這個路徑比較復(fù)雜如果你不是對OpenSSL構(gòu)建機制很熟建議還是改自己項目的運行庫設(shè)置。另外還要注意/MD和/MDd是兩碼事。鏈接release版的libeay64.lib時項目配置項里運行庫選/MDd會導(dǎo)致調(diào)試和發(fā)布符號不一致可能也能編譯過去但運行時不安全。最好嚴格對齊release項目用/MDdebug項目可以考慮單獨編一份debug版靜態(tài)庫編譯參數(shù)加/MTd或/MDd。4.3 運行時崩潰和初始化問題鏈接能過但運行崩潰先看有沒有錯誤碼。常見的是0x000126這不是OpenSSL專門的錯誤碼而是系統(tǒng)找不到所需的DLL或入口點。雖然我們用了靜態(tài)庫但如果代碼里同時加載了舊版本的libeay32.dll系統(tǒng)服務(wù)所依賴的DLL還是可能觸發(fā)這個問題。排查方式是用Process Monitor或dumpbin看exe依賴確保加載路徑下沒有殘留的libeay32.dll或ssleay32.dll。很多時候程序目錄里放著以前用過的DLL靜態(tài)鏈接版程序運行時還是會優(yōu)先加載同目錄DLL導(dǎo)致執(zhí)行了舊代碼行為錯亂。另一個運行時坑是SSL_library_init()忘記調(diào)用。1.0.x時代有些api可以自動初始化但SSL_CTX_new之前手動調(diào)SSL_library_init還是穩(wěn)妥的。舊版本教程里經(jīng)常不寫這行結(jié)果一運行就崩還找不到原因。4.4 服務(wù)端返回unexpected eof while reading怎么辦這個詞最近在熱搜里出現(xiàn)率很高具體報錯是error:0A000126:SSL routines::unexpected eof while reading雖然這個錯誤碼格式偏向OpenSSL 3.x但底層的“EOF while reading”問題在1.0里也有。通常原因有三個服務(wù)端關(guān)閉了連接但沒有發(fā)送close_notify協(xié)商的TLS版本不受支持或者中間設(shè)備中斷連接。如果你用1.0靜態(tài)庫做客戶端去連老設(shè)備大概率是服務(wù)端只支持SSLv3或TLS1.0而1.0.2默認已經(jīng)不開啟這些舊協(xié)議。處理辦法是在SSL_CTX上設(shè)置SSL_CTX_set_options(ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);或者反過來如果你的場景需要兼容極老設(shè)備可以調(diào)整允許的協(xié)議范圍??傊@個錯誤不一定是靜態(tài)庫本身的問題先抓包確認對端TLS行為再決定配置方向。4.5 常見問題速查表現(xiàn)象可能原因解決辦法LNK2001無法解析的外部符號缺少系統(tǒng)依賴庫在附加依賴項加入ws2_32.lib、crypt32.lib、user32.lib、advapi32.lib、gdi32.libLNK2038檢測到RuntimeLibrary不匹配OpenSSL是/MD項目是/MT把項目的運行庫改成“多線程DLL /MD”LNK2019符號找不到32/64位庫混用確認項目平臺是x64使用libeay64.lib運行時0x000126錯誤程序目錄存在舊版OpenSSL DLL清理exe同目錄下的libeay32.dll、ssleay32.dll調(diào)用SSL函數(shù)崩潰未初始化SSL庫在使用SSL_CTX_new前調(diào)用SSL_library_init()對端握手后報eof錯誤協(xié)議版本或?qū)Χ岁P(guān)閉異常正確設(shè)置SSL_OP_NO_SSLv2/SSLv3必要時抓包確認5. 給老項目的一點遷移建議5.1 從1.0.x到1.1.x/3.x的差異注意點如果未來條件允許還是建議往新版本遷移畢竟1.0.2已經(jīng)停止維護。遷移時最大的感受是API變化主要集中在這幾個地方很多函數(shù)加了前綴比如HMAC()變成了HMAC()依然存在但初始化上下文的方式變了。SSL_CTX_new(TLS_client_method())統(tǒng)一替代了老的SSLv23_client_method()。OpenSSL版本宏從SSLeay_version換成了OpenSSL_version。編譯產(chǎn)物命名也變了在1.1.0之后Windows上crypto庫變成了libcrypto.lib、ssl庫變成了libssl.lib不再有l(wèi)ibeay和ssleay的叫法。如果你的代碼里到處是RSA_、EVP_、SSL_CTX_*這類老接口把庫換成新版本后編譯會先爆炸一輪。建議遷移前先把接口層封裝好不要讓業(yè)務(wù)代碼直接和OpenSSL API糾纏。5.2 二進制兼容性底線如果你暫時無法遷移至少要保證三點一是使用最新補丁版本1.0.2u二是編譯時啟用FIPS相關(guān)的選項要慎重因為FIPS模塊的認證和庫構(gòu)建方式都會影響最終集成三是定期用第三方掃描工具檢查老庫的已知漏洞清單把風險記錄在案。另外靜態(tài)庫方式雖然部署方便但每次上游修復(fù)安全漏洞后你都必須重新編譯所有依賴于它的模塊。這實際上是把維護成本從“換DLL”變成了“重編譯回歸測試”做決策前要有預(yù)期。我個人的經(jīng)驗是老庫不是不能用但使用方必須清楚它背后維護的責任邊界。把編譯腳本、版本信息、依賴清單全部固化到文檔里等哪天真要升級這些東西能幫你省一半時間。畢竟OpenSSL的坑誰踩誰知道老版本的坑更是踩一個準一個。本文還有配套的精品資源點擊獲取