充分理解 C/C++ 重要概念:運(yùn)行時庫
作者 | robot
在C/C++開發(fā)領(lǐng)域,運(yùn)行時庫(Run Time Library)是一個非常重要且基礎(chǔ)的概念,但是相關(guān)的介紹文章卻很少,以至于對很多開發(fā)同學(xué)來說,這是一個偏神秘的存在,本文作者查閱了大量資料,并結(jié)合自己的理解,希望能夠通俗易懂的科普和揭秘一下這一領(lǐng)域,內(nèi)容包括什么是C/C++運(yùn)行時庫,它的主要功能,各平臺的存在形式,以及開發(fā)中要注意的問題。
一、認(rèn)識C/C++運(yùn)行時庫
1. 初識概念
你是否知道,我們開發(fā)的C/C++程序,在運(yùn)行過程中,背后有一個基礎(chǔ)模塊,在默默提供著支持?
這背后的基礎(chǔ)模塊,也叫基礎(chǔ)庫,或運(yùn)行時庫。
見下圖,支撐C/C++程序運(yùn)行的,除了CPU/內(nèi)存等硬件和操作系統(tǒng)之外,還有C/C++運(yùn)行時庫:
那么,它具體在干什么呢?要理解這一概念,需要先從C/C++語言說起。
2. 編程語言
基本每種編程語言,包括C/C++語言,都包括兩個部分:
- 語法部分:比如判斷、循環(huán)、定義變量、函數(shù)、類、注釋等,及一些內(nèi)置類型。
- 標(biāo)準(zhǔn)庫部分:該語言最常用的函數(shù)或類庫,比如輸入輸出、內(nèi)存管理、字符串操作、數(shù)學(xué)函數(shù)、線程相關(guān)等。因為太基礎(chǔ)常用,所以被納入語言標(biāo)準(zhǔn)的一部分,稱為標(biāo)準(zhǔn)庫。
狹義的語言,僅僅是指語法部分,可稱之為語言本身;廣義的語言,是指語法部分+標(biāo)準(zhǔn)庫部分。
語言的標(biāo)準(zhǔn)庫+各種第三方庫,組成了我們程序中常見的各種庫。
C/C++運(yùn)行時庫,就是在運(yùn)行時,為語言的這兩部分提供基礎(chǔ)支持的庫?;蛘吆喲灾?,C/C++運(yùn)行時庫,就是C/C++程序在運(yùn)行時依賴的基礎(chǔ)庫。
3. 其它語言的運(yùn)行時庫
除了C/C++語言之外,其它語言其實也有自己的運(yùn)行時庫。
比如Java,其運(yùn)行時庫就是Java運(yùn)行時環(huán)境(JRE),它包括Java虛擬機(jī)(JVM)和Java標(biāo)準(zhǔn)庫。
- 比如Python,其運(yùn)行時庫是指Python的解釋器 + Python標(biāo)準(zhǔn)庫。
- 比如JavaScript,其運(yùn)行時庫是指瀏覽器 + JavaScript解釋器。
總的來說,語言的運(yùn)行時庫,就是為該語言編寫的程序,在運(yùn)行時提供基礎(chǔ)支持的庫或環(huán)境。
二、C/C++運(yùn)行時庫的功能
這一部分,我們來看看C/C++運(yùn)行時庫的具體功能。
對于 C 和 C++,其實是兩種不同的語言,后者是在前者的基礎(chǔ)上擴(kuò)展而來,它們的運(yùn)行時庫,也是有兩個,我們分開來看看。
1. C運(yùn)行時庫
C語言包括如下常用函數(shù):
- 輸入輸出函數(shù),比如 printf、scanf、puts
- 內(nèi)存管理函數(shù),比如 malloc、free、realloc
- 文件系列函數(shù),比如 fopen、fread、fwrite
- 字符串相關(guān)函數(shù),比如 strcpy、strcmp、strlen
- 數(shù)學(xué)函數(shù),比如 sin、cos、sqrt
這些函數(shù)是怎么實現(xiàn)的?是由誰提供的?
這些函數(shù)并不是由操作系統(tǒng)提供的,而是由C語言提供的,準(zhǔn)確說是由C運(yùn)行時庫提供的。
比如內(nèi)存相關(guān):
Linux系統(tǒng)原本提供的內(nèi)存分配函數(shù)(也叫API),是brk、mmap等,而Windows系統(tǒng)提供的API是HeapAlloc、HeapFree、VirtualAlloc等,C語言(準(zhǔn)確說是C運(yùn)行時庫)把各操作系統(tǒng)提供的API封裝成統(tǒng)一的malloc、free。
如下圖,Linux下malloc調(diào)用系統(tǒng)API的堆棧:
Windows下malloc調(diào)用系統(tǒng)API的堆棧:
(事實上,malloc內(nèi)部并不是簡單調(diào)用各系統(tǒng)API,而是做了一個內(nèi)存池,小內(nèi)存從池中分配,大內(nèi)存調(diào)系統(tǒng)API)
對于文件:
- C語言提供的fopen、fread、fwrite等,即FILE系列的緩沖文件,同樣是調(diào)用各平臺的系統(tǒng)API實現(xiàn)的。Linux下是調(diào)用open、write等非緩沖文件接口,Windows下是調(diào)用CreateFile、WriteFile等。
- 除了上面說的平臺相關(guān)函數(shù)外,C運(yùn)行時庫里,還有一些是平臺無關(guān)或關(guān)系不大的函數(shù),比如字符串、數(shù)學(xué)相關(guān)函數(shù)等。
- 另外C運(yùn)行時庫為了支持程序的運(yùn)行,還在統(tǒng)一的main函數(shù)前后做了一些邏輯,比如在main之前初始化一些全局變量、環(huán)境變量、命令行參數(shù)等,在main之后做一些資源的清理等。
總結(jié)一下,C運(yùn)行時庫里提供了一系列常用的庫函數(shù),包括把平臺相關(guān)函數(shù)封裝成統(tǒng)一的接口、平臺無關(guān)的,以及給我們提供了一個統(tǒng)一的main入口。
這些庫函數(shù)是事先編譯好的,通常隨編譯器一起發(fā)布,編譯器在編譯我們的程序時,自動幫我們做了鏈接,讓我們無感。
這也是為什么教科書說C語言是可移植語言的原因之一,因為這些跨平臺實現(xiàn)幫我們屏蔽了操作系統(tǒng)的差異,否則就需要調(diào)各操作系統(tǒng)的API,為不同平臺編寫不同的代碼了。
2. C++運(yùn)行時庫
C++語言,是在C語言的基礎(chǔ)上提供了更多的功能特性,包括:
- 語言本身,提供了類、多態(tài)、new/delete、異常、RTTI等。
- 標(biāo)準(zhǔn)庫,提供了string、vector、list、map等各種容器和算法,以及輸入輸出流、智能指針、日期時間、線程相關(guān)等。
C++運(yùn)行時庫,也是在C運(yùn)行時庫的基礎(chǔ)上,為C++語言的這兩部分特性提供支持。
這些庫,語言提供者們幫我們實現(xiàn)好,放在一個動態(tài)庫或靜態(tài)庫中,編譯器最終做鏈接即可。
3. 總結(jié)一下
C/C++運(yùn)行時庫的具體功能,綜合來看,有幾方面:
- 支持程序的啟動和退出。包括main之前的全局變量、環(huán)境變量、命令行參數(shù)的初始化,和main之后的資源清理等。
- 把一些平臺相關(guān)API封裝成統(tǒng)一的庫函數(shù)或類,便于我們跨平臺開發(fā)。
- 其它常用功能的實現(xiàn),封裝成函數(shù)或類。
- 少量語言特性的支持,比如異常處理、RTTI等。
三、各平臺的C/C++運(yùn)行時庫
前面介紹了C/C++運(yùn)行時庫的通用概念和主要功能,這部分介紹一下在各平臺的具體存在形式。
為了更好的理解,在之前先介紹一下語言的標(biāo)準(zhǔn)和實現(xiàn)、動態(tài)庫與靜態(tài)庫,以及各平臺的庫文件格式。
1. 標(biāo)準(zhǔn)與實現(xiàn)
C和C++語言,都是只有一種標(biāo)準(zhǔn),但有多種實現(xiàn)。
(1) 一種標(biāo)準(zhǔn)
即 ISO 的C/C++標(biāo)準(zhǔn)委員會制定的,但這個標(biāo)準(zhǔn)按時間順序,有多個版本,后一個版本在前一個版本基礎(chǔ)上改進(jìn),增加新的特性,同時也可能廢棄一些特性。
對于C語言,有C89/C90、C99、C11等;C++ 有 C++98、C++11、C++17、C++20 等。
C標(biāo)準(zhǔn)各版本簡介:
- C89:ANSI C,第一個版本,是美國標(biāo)準(zhǔn),有32個關(guān)鍵字,1989年
- C90:和C89差不多,被國際ISO標(biāo)準(zhǔn)采納,1990年
- C99:1999年發(fā)布,增加了多個特性,包括變長數(shù)組、inline關(guān)鍵字、//注釋、宏可變參數(shù)
- C11:2011年發(fā)布,增加了多線程、內(nèi)存對齊、Unicode等支持
C++標(biāo)準(zhǔn)各版本簡介:
- C++98:C++的第一個標(biāo)準(zhǔn)版本,1998年由國際標(biāo)準(zhǔn)化組織(ISO)發(fā)布。
- C++03:對C++98的一個小修訂,2003年發(fā)布。
- C++11:2011年發(fā)布,重大更新,引入了很多新特性,如auto類型、范圍for循環(huán)、列表初始化、lambda表達(dá)式等。
- C++14:2014年發(fā)布,對C++11的一個小修訂。
- C++17:2017年發(fā)布,重大更新,引入了很多新特性,如結(jié)構(gòu)化綁定、并行算法、模板參數(shù)自動推導(dǎo)等。
- C++20:2020年發(fā)布。引入了很多新特性,如概念、協(xié)程、模塊等。
語言的標(biāo)準(zhǔn),只是定義了語言的語法語義,以及有哪些頭文件、庫函數(shù)聲明等,但并不負(fù)責(zé)語言及這些庫函數(shù)的實現(xiàn)。
(2) 多種實現(xiàn)
語言的具體實現(xiàn),是由各編譯器廠商完成的,包括語法語義的實現(xiàn),和標(biāo)準(zhǔn)庫(運(yùn)行時庫)的實現(xiàn)。
主要的有Linux下GNU的實現(xiàn),Windows下的MSVC實現(xiàn)、以及LLVM的實現(xiàn)等,后文會具體介紹。
標(biāo)準(zhǔn)和實現(xiàn)的關(guān)系:
標(biāo)準(zhǔn)和實現(xiàn)不一定是完全一致的。比如某編譯器版本,可能對標(biāo)準(zhǔn)某特性的遵循不夠完善,也有可能某個編譯器先實現(xiàn)了某語言特性,然后才被納入標(biāo)準(zhǔn)。
2. 靜態(tài)庫與動態(tài)庫
與我們自己開發(fā)的庫是分為靜態(tài)庫與動態(tài)庫(也叫共享庫)一樣,C/C++運(yùn)行時庫也是分為靜態(tài)庫與動態(tài)庫。比如在Linux平臺,靜態(tài)庫的后綴是.a,動態(tài)庫的后綴是.so。
- 動態(tài)庫(共享庫)會被多個程序共享,好處是程序體積小,但缺點是運(yùn)行時會多一些依賴。
- 靜態(tài)庫正好相反,優(yōu)點是運(yùn)行時少依賴,但缺點是體積大,因為它被鏈接進(jìn)可執(zhí)行文件內(nèi)部。
這些靜態(tài)庫或動態(tài)庫,是由廠商開發(fā),提前編譯好,然后在編譯器編譯鏈接我們的程序時,自動鏈接進(jìn)去。
3. 庫的文件格式
不同平臺的庫文件的二進(jìn)制格式是不一樣的,文件名后綴也不一樣,為了避免在后文中部分同學(xué)疑惑,這里簡單介紹一下各平臺的庫文件格式。
可執(zhí)行/庫文件格式,常見的就是三種,我列成一個表格:
注:
- ELF:Executable and Linkable Format
- PE:Portable Executable
- Mach-O:Mach Object file format
4. 各平臺的具體實現(xiàn)
現(xiàn)在我們開始看看各平臺的具體實現(xiàn),包括Linux平臺的GNU實現(xiàn)、Windows的實現(xiàn)、LLVM的實現(xiàn)、移動端的實現(xiàn),以及其它嵌入式平臺的實現(xiàn)。
(1) GNU的實現(xiàn)
這是Linux后臺開發(fā)最常見的實現(xiàn),其C運(yùn)行時庫是GNU C Library,簡稱glibc。
- GNU簡介:GNU是一個開源項目,全稱GNU's Not Unix,目標(biāo)是開發(fā)一個操作系統(tǒng),但由于其內(nèi)核開發(fā)緩慢,我們實際上用到的是GNU的各種上層工具+Linux內(nèi)核的結(jié)合,即GNU/Linux。
- GNU項目具體包括:GCC(多種編譯器集合)、C庫(即glibc)、bash及各種命令行工具、編輯器等。我們?nèi)粘=佑|到的Linux,其實準(zhǔn)確叫GNU/Linux,它是由GNU的各種上層工具+Linux內(nèi)核組成。常見的各種Linux發(fā)行版(Ubuntu、Debian、CentOS等),都屬于GNU/Linux。
在Linux平臺,一個最簡單的C語言程序,在運(yùn)行時會依賴哪些庫?
如下Hello World代碼:
// hello.c
#include <stdio.h>
int main() {
printf("Hello World!\n");
return 0;
}
使用 gcc 編譯,默認(rèn)動態(tài)鏈接C運(yùn)行時庫:
gcc hello.c -o hello
使用 ldd 命令查看依賴:
圖中的libc.so,即C的運(yùn)行時庫。
其實嚴(yán)格來說,圖中的這三個庫,都是C程序的運(yùn)行時庫,因為都是為程序的運(yùn)行提供支持的。
順便看看文件大?。?/p>
Linux平臺,一個最簡單的C++程序,運(yùn)行時會依賴哪些庫?
如下Hello World代碼:
// hello.cpp
#include <iostream>
int main() {
std::cout << "Hello World!" << std::endl;
return 0;
}
使用g++編譯,默認(rèn)動態(tài)鏈接C/C++運(yùn)行時庫:
g++ hello.cpp -o hello
用 ldd 命令查看依賴:
圖中的libstdc++.so,即C++的運(yùn)行時庫。
附其它幾個庫簡介:
- linux-vdso.so:虛擬庫,用于程序更高效的調(diào)用部分內(nèi)核接口
- libm.so:數(shù)學(xué)庫
- libgcc_s.so:gcc的支持庫,用異常處理、RTTI等
- ld-linux-x86-64.so:動態(tài)庫的加載器
庫的靜態(tài)與動態(tài):
gcc、g++對于C/C++運(yùn)行時庫,默認(rèn)都是鏈接到其動態(tài)庫版,但也可以鏈接到其靜態(tài)庫版。
方法是:
- C庫:gcc指定 -static 參數(shù),這樣就可以鏈接到靜態(tài)庫 libc.a(事實上-static會把所有庫都靜態(tài)鏈接)
- C++庫:g++ 指定 -static-libstdc++ 參數(shù),這樣就可以鏈接到靜態(tài)庫版libstdc++.a
一個C程序使用 -static 靜態(tài)鏈接后,再看看其依賴的庫:
看看文件大小:
圖中看出,二進(jìn)制不再依賴 libc.so 等庫了,這些庫被靜態(tài)鏈接到了可執(zhí)行文件內(nèi)部,體積也比之前大了。
(2) Windows的實現(xiàn)
在Windows平臺,微軟也實現(xiàn)了自己的C/C++運(yùn)行時庫,一般隨Visual Studio發(fā)行。
一個最簡單的C/C++程序,使用vs2022編譯,默認(rèn)指定/MD選項
用 Depends 工具查看其依賴的動態(tài)庫:
圖中的紅框部分,就是C/C++運(yùn)行時庫。
在 Visual Studio 里,也可以設(shè)置動態(tài)或靜態(tài)鏈接運(yùn)行時庫,方法是:
工程設(shè)置里(C/C++ -> 代碼生成 -> 運(yùn)行庫)
- /MT 多線程,即鏈接到靜態(tài)庫版
- /MD 多線程DLL,即鏈接到動態(tài)庫版
另外還有個 /MTd 、/MDd 是用于調(diào)試版本。
當(dāng)設(shè)置 /MT 即靜態(tài)鏈接后,就不再依賴這些庫了,如下圖:
hello.exe 文件體積從 13K 增加到 200K。
Windows平臺C/C++運(yùn)行時庫動態(tài)版的文件名,不同的版本不一樣。
- 早期的Vc6,是 msvcrt.dll 和 msvcp6.dll
- 后來的版本,是 msvcrXX.dll 和 msvcpXX.dll(XX為版本號)
- 從vs2015開始,微軟對其進(jìn)行了重構(gòu),拆分成了ucrtbase.dll、msvcp140.dll、vcruntime140.dll等
(3) LLVM的實現(xiàn)
LLVM是一個較新的開源項目,可以說是專門為做編譯器相關(guān)而生,是一個更優(yōu)的編譯器,各種配套的工具也比較完善,近年來越來越多的項目開始使用LLVM。
LLVM里用于C系語言(C、C++ 和 Objective-C)的編譯器前端,叫Clang。
除了做編譯器外,LLVM也開發(fā)了自己的C++庫,名字叫 libc++ (不同于GNU的 libstdc++)。LLVM也有自己的C庫,不過目前還不夠完善。
LLVM是一個跨平臺項目,支持多種平臺,包括Linux、Windows、macOS、iOS、Android等。
比如在Linux平臺,其C++運(yùn)行時庫的文件名,動態(tài)庫版叫l(wèi)ibc++.so,靜態(tài)庫版叫l(wèi)ibc++.a。
(4) 移動端的實現(xiàn)
① iOS:
在iOS及macOS平臺,蘋果使用了LLVM的Clang作為Xcode內(nèi)置的C/C++編譯器。
- 其C運(yùn)行時庫文件名是 libSystem.dylib,這個庫也包含了其它系統(tǒng)庫的功能。
- 其C++運(yùn)行時庫是 libc++.dylib 或 libc++.a。
② Android:
Android平臺一般使用Java或Kotlin開發(fā),但在某些性能要求高的場合,也會使用C/C++開發(fā),即NDK開發(fā)。
- Android平臺的C運(yùn)行時庫,叫Bionic libc,這是Google為Android專門開發(fā)的,比glibc更輕量,更適合移動設(shè)備。它同時提供了動態(tài)庫版本(libc.so)和靜態(tài)庫版本(libc.a)。
- Android NDK的C++運(yùn)行時庫,以前支持三種:libc++(即LLVM的)、libstdc++(即GNU的)和STLport,后來從NDK r18開始,只支持libc++了。
Android平臺里的 libc++ 庫的名字不太一樣,其動態(tài)庫版叫 libc++_shared.so,靜態(tài)庫版本叫 libc++_static.a。
是鏈接到靜態(tài)庫還是動態(tài)庫,可以在工程里指定,比如:
APP_STL := c++_shared
(5) 其它實現(xiàn)
除了上面常見的實現(xiàn)之外,還有一些早期的C/C++編譯器,也都帶有自己的運(yùn)行時庫,比如Turbo C(很多人在學(xué)校里用的)、Borland C++(有人用過嗎)、C++Builder 等。
在一些嵌入式平臺,會使用一些更輕量的C運(yùn)行時庫,主要有開源的 Newlib、uClibc、musl 等。
5. 總結(jié)一下
總結(jié)一個表格:
四、C/C++運(yùn)行時庫相關(guān)問題
在開發(fā)中,我們常碰到的C/C++運(yùn)行時庫相關(guān)問題,主要有多實例問題和多版本問題,下面分別介紹一下。
1. 運(yùn)行時庫的多實例問題
為簡單起見,這里先只考慮單一開發(fā)環(huán)境下(即只有一個編譯器和運(yùn)行時庫版本),進(jìn)程內(nèi)有多個運(yùn)行時庫實例的問題。
先看一段代碼:
- 動態(tài)庫導(dǎo)出一個接口函數(shù):
char *GetData() {
char *data = malloc(100);
strcpy(data, "Hello World!"); // 僅演示,工作中不要用strcpy
return data;
}
- 主程序調(diào)用動態(tài)庫的接口函數(shù):
int main() {
char *data = GetData();
free(data);
return 0;
}
這段代碼,在有些平臺下運(yùn)行(主要是Windows平臺),可能會crash。
為什么會crash,根本原因是和運(yùn)行時庫的內(nèi)存堆有關(guān),下面具體講講。
(1) 單內(nèi)存堆
在C運(yùn)行時庫內(nèi)部,有一個內(nèi)存堆,也就是一個內(nèi)存池,如下圖:
圖片
大多數(shù)情況下,進(jìn)程內(nèi)只有一個C運(yùn)行時庫實例,也就是只有一個內(nèi)存堆。
上述代碼的依賴關(guān)系:
App.exe
A.dll
crt.dll
注:用縮進(jìn)表示依賴關(guān)系,crt.dll 表示C運(yùn)行時庫,相當(dāng)于linux的 libc.so
A.dll 向 crt.dll 申請內(nèi)存,用完后被 App.exe 再歸還給 crt.dll,這樣沒問題。
但是當(dāng)進(jìn)程內(nèi)里有多個內(nèi)存堆時,情況就不一樣了。
(2) 多內(nèi)存堆
當(dāng)進(jìn)程內(nèi)有多個C運(yùn)行時庫實例時,就會有多個內(nèi)存堆實例。
比如在Windows平臺,A.dll 設(shè)置 /MT 選項,即靜態(tài)鏈接C/C++運(yùn)行時庫,主程序默認(rèn)/MD選項,即動態(tài)鏈接C/C++運(yùn)行時庫。
這時的依賴關(guān)系:
App.exe
A.dll (靜態(tài)鏈接crt)
crt.dll
這種場景下,A.dll 會從自己的內(nèi)存堆中分配內(nèi)存,用完后被 App.exe 歸還給了 crt.dll 的內(nèi)存堆,這樣就引起了內(nèi)存堆的結(jié)構(gòu)異常,出現(xiàn)crash。
(3) 隱藏更深的情況
下面再看一種隱藏更深的情況,工作中更為常見。
動態(tài)庫 A 導(dǎo)出一個接口:
void GetData(std::string &data) {
data = "Hello World!"; // 這句賦值內(nèi)部,string會分配內(nèi)存
}
在主程序或另一個動態(tài)庫中,調(diào)用動態(tài)庫 A 的接口:
void Test() {
std::string data;
GetData(data);
// data析構(gòu),釋放string內(nèi)部之前分配的內(nèi)存,導(dǎo)致crash
}
上述代碼在靜態(tài)鏈接C/C++運(yùn)行時庫時,會出現(xiàn)crash。
這里crash的本質(zhì),其實和前面一樣的,即一個模塊的分配的內(nèi)存,交給了另一個模塊釋放。
除此之外,這個代碼,還有另一個風(fēng)險,即如果兩個模塊使用了不同的編譯器或C++庫(比如一個使用GCC編譯,一個使用LLVM編譯),就可能會出現(xiàn)string的內(nèi)存結(jié)構(gòu)不一致而異常。
(4) 其它情況
在進(jìn)程內(nèi)有多C/C++運(yùn)行時庫實例時,還有一些其它有問題的情況,比如跨模塊傳遞文件指針、跨模塊傳遞環(huán)境變量等。
比如:
// 動態(tài)庫導(dǎo)出該接口
void WriteData(FILE *file) {
fwrite(...);
fclose(file);
}
// 主程序里
int main() {
FILE *file = fopen(...);
WriteData(file);
return 0;
}
(5) 總結(jié)一下
運(yùn)行時庫的多實例,是由靜態(tài)鏈接C/C++運(yùn)行時庫引起。在這種多實例場景下,一些不太好的代碼寫法,就會表現(xiàn)出問題。
如何避免這些問題,建議:
① 作為動態(tài)庫的設(shè)計者:
- 盡量做到內(nèi)存「誰分配誰釋放」的原則
- 盡量避免庫間接口傳遞C/C++對象
這樣將會有更好的兼容性,即使在進(jìn)程內(nèi)有多個C/C++運(yùn)行時庫時,也不會有問題。
② 作為App的總體設(shè)計者:
盡量保證進(jìn)程內(nèi)只有一份C/C++運(yùn)行時庫實例
這樣也是會有更好的兼容性,能避免很多潛在問題。
如果用生活中的例子來類比,多運(yùn)行時庫,就相當(dāng)于一個公司對接了多個銀行,某個部門從 A 銀行借來的錢,被另一個部門還給了 B 銀行,這樣就引起了問題。
解決方法就是誰借的錢,誰來還,另外就是一個公司盡量只對接一個銀行。
關(guān)于運(yùn)行時庫的多實例,幸運(yùn)的是,大部分平臺的編譯器,已經(jīng)幫我們規(guī)避了可能會導(dǎo)致運(yùn)行時庫多實例的問題。
我簡單測了一下:
- Linux平臺:對于主程序和動態(tài)庫,都默認(rèn)動態(tài)鏈接C/C++運(yùn)行時庫,主程序允許靜態(tài)鏈接C/C++運(yùn)行時庫(方法是 -static 選項),動態(tài)庫不支持靜態(tài)鏈接C/C++運(yùn)行時庫。
- Windows平臺:對于主程序和動態(tài)庫,都支持動態(tài)或靜態(tài)鏈接C/C++運(yùn)行時庫。
- iOS和macOS平臺:不支持靜態(tài)鏈接C/C++運(yùn)行時庫。
- Android平臺:動態(tài)庫支持動態(tài)或靜態(tài)鏈接C++運(yùn)行時庫,默認(rèn)靜態(tài)鏈接,這可能導(dǎo)致風(fēng)險,官方文檔提供了說明。
所以總結(jié)來看,這個問題主要是在Windows和Android平臺可能會出現(xiàn),其它平臺編譯器不提供這種設(shè)置(特殊方法除外,比如自己開發(fā)一個運(yùn)行時庫并靜態(tài)鏈接)。
如果你是做Windows和Android平臺開發(fā),或者做跨平臺開發(fā),涉及到這兩個平臺,就必須考慮運(yùn)行時庫多實例問題,否則就可以不用太操心這個問題,編譯器已經(jīng)防止了你犯錯。
2. 運(yùn)行時庫的多版本問題
最后這一部分,簡單說說運(yùn)行時庫的多版本問題。
和我們自己開發(fā)的軟件或庫,會不斷升級,會有多個版本一樣,C/C++運(yùn)行時庫,也是在不斷升級,有很多個版本。
運(yùn)行時庫的多版本,會引起兩個問題:
- 編譯時用的庫,和運(yùn)行時用的庫,版本不一致
- 編譯同一個App的多個部分,用的庫版本不一致
后者在開發(fā)一些大型項目,不同模塊分屬不同團(tuán)隊開發(fā)時,更容易出現(xiàn)。
這種不一致,可能會引起不匹配,從而產(chǎn)生各種問題。
(1) 問題現(xiàn)象
有些是在鏈接時不通過,提示符號沖突或找不到。比如:
undefined reference to `std::string::operator=(std::string const&)'
undefined reference to `std::ostream::operator<<(std::ostream& (*)(std::ostream&))'
(也有可能是設(shè)置不統(tǒng)一引起)
有些是在啟動時,動態(tài)庫加載器檢測出異常,提示缺少符號,比如:
./test: /lib64/libstdc++.so.6: version `CXXABI_1.3.9' not found (required by ./test)
./test: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by ./test)
./test: /lib64/libstdc++.so.6: version `GLIBCXX_3.4.22' not found (required by ./test)
有些是啟動時沒檢測出異常,而在運(yùn)行時會有莫名其妙的問題。
(2) 解決方法
解決的方法,無它,只能是盡量保證一致,包括:
- 編譯各模塊時,用的開發(fā)環(huán)境(包括編譯器版本+運(yùn)行時庫版本+參數(shù)設(shè)置)一致。
- 運(yùn)行時和編譯時的庫環(huán)境一致。
具體怎么保持一致,有如下一些方法:
① 靜態(tài)鏈接
即不再依賴環(huán)境中的C/C++運(yùn)行時庫,用體積換取少依賴。這個在Windows平臺較常用,Android平臺在只有一個動態(tài)庫時,官方也推薦靜態(tài)鏈接。
② 開發(fā)環(huán)境多版本共存時
選擇其中一個編譯器和C/C++運(yùn)行時庫版本。比如在Windows平臺選擇工具集(v142/v143等),Linux平臺通過環(huán)境變量選擇GCC版本、Android平臺環(huán)境變量選擇NDK版本(r23/r25等)。
③ 運(yùn)行環(huán)境多版本共存時
通過指定搜索路徑選擇其中一個版本。比如Linux平臺在二進(jìn)制中指定(rpath)、環(huán)境變量指定(LD_LIBRARY_PATH)。
④ Docker
Linux平臺常用,即運(yùn)行時提供一個隔離的干凈的環(huán)境。
五、總結(jié)
本文介紹了什么是C/C++運(yùn)行時庫,運(yùn)行時庫的主要的功能,各平臺的存在形式,以及開發(fā)要注意的問題,包括多實例問題和多版本問題等。遵守一些開發(fā)原則,以及保證運(yùn)行時庫單實例、開發(fā)和運(yùn)行環(huán)境的一致,可以避免很多潛在問題。