程序员的自我修养--链接、装载与库笔记:运行库
1. 入口函數(shù)和程序初始化
程序從main開始嗎?:操作系統(tǒng)裝載程序之后,首先運行的代碼并不是main的第一行,而是某些別的代碼,這些代碼負責準備好main函數(shù)執(zhí)行所需要的環(huán)境,并且負責調(diào)用main函數(shù),這時候你才可以在main函數(shù)里放心大膽地寫各種代碼:申請內(nèi)存、使用系統(tǒng)調(diào)用、觸發(fā)異常、訪問I/O。在main返回之后,它會記錄main函數(shù)的返回值,調(diào)用atexit注冊的函數(shù),然后結(jié)束進程。
運行這些代碼的函數(shù)稱為入口函數(shù)或入口點(Entry Point),視平臺的不同而有不同的名字。程序的入口點實際上是一個程序的初始化和結(jié)束部分,它往往是運行庫的一部分。一個典型的程序運行步驟大致如下:
(1). 操作系統(tǒng)在創(chuàng)建進程后,把控制權(quán)交到了程序的入口,這個入口往往是運行庫中的某個入口函數(shù)。
(2). 入口函數(shù)對運行庫和程序運行環(huán)境進行初始化,包括堆、I/O、線程、全局變量構(gòu)造,等等。
(3). 入口函數(shù)在完成初始化之后,調(diào)用main函數(shù),正式開始執(zhí)行程序主體部分。
(4). main函數(shù)執(zhí)行完畢以后,返回到入口函數(shù),入口函數(shù)進行清理工作,包括全局變量析構(gòu)、堆銷毀、關(guān)閉I/O等,然后進行系統(tǒng)調(diào)用結(jié)束進程。
入口函數(shù)如何實現(xiàn):
GLIBC入口函數(shù):glibc的啟動過程在不同的情況下差別很大,比如靜態(tài)的glibc和動態(tài)的glibc的差別,glibc用于可執(zhí)行文件和用于共享庫的差別,這樣的差別可以組合出4種情況,這里只選取最簡單的靜態(tài)glibc用于可執(zhí)行文件的時候作為例子。
glibc的程序入口為_start(這個入口是由ld鏈接器默認的鏈接腳本所指定的,也可以通過相關(guān)參數(shù)設(shè)定自己的入口)。_start由匯編實現(xiàn),并且和平臺相關(guān)。
可以從https://www.gnu.org/software/libc/sources.html 下載glibc的源碼,最新的發(fā)布版本為2.30。
環(huán)境變量:是存在于系統(tǒng)中的一些公用數(shù)據(jù),任何程序都可以訪問。通常來說,環(huán)境變量存儲的都是一些系統(tǒng)的公共信息,例如系統(tǒng)搜索路徑、當前OS版本等。環(huán)境變量的格式為key=value的字符串,C語言里可以使用getenv這個函數(shù)來獲取環(huán)境變量信息。
MSVC CRT入口函數(shù):MSVC的CRT默認的入口函數(shù)名為mainCRTStartup。
運行庫與I/O:IO(或I/O)的全稱是Input/Output,即輸入和輸出。對于計算機來說,I/O代表了計算機與外界的交互,交互的對象可以是人或其它設(shè)備。而對于程序來說,I/O覆蓋的范圍還要寬廣一些。一個程序的I/O指代了程序與外界的交互,包括文件、管道、網(wǎng)絡(luò)、命令行、信號等。更廣義地講,I/O指代任何操作系統(tǒng)理解為”文件”的事務(wù)。許多操作系統(tǒng),包括Linux和Windows,都將各種具有輸入和輸出概念的實體----包括設(shè)備、磁盤文件、命令行等----統(tǒng)稱為文件,因此這里所說的文件是一個廣義的概念。對于一個任意類型的文件,操作系統(tǒng)會提供一組操作函數(shù),這包括打開文件、讀文件、寫文件、移動文件指針等。C語言文件操作是通過一個FILE結(jié)構(gòu)的指針來進行的。在操作系統(tǒng)層面上,文件操作也有類似于FILE的一個概念,在Linux里,這叫做文件描述符(File Descriptor),而在Windows里,叫做句柄(Handle)(以下在沒有歧義的時候統(tǒng)稱為句柄)。用戶通過某個函數(shù)打開文件以獲得句柄,此后用戶操作文件皆通過該句柄進行。設(shè)計這么一個句柄的原因在于句柄可以防止用戶隨意讀寫操作系統(tǒng)內(nèi)核的文件對象。無論是Linux還是Windows,文件句柄總是和內(nèi)核的文件對象相關(guān)聯(lián)的,但如何關(guān)聯(lián)細節(jié)用戶并不可見。內(nèi)核可以通過句柄來計算出內(nèi)核里文件對象的地址,但此能力并不對用戶開放。I/O初始化的職責:首先I/O初始化函數(shù)需要在用戶空間中建立stdin、stdout、stderr及其對應(yīng)的FILE結(jié)構(gòu),使得程序進入main之后可以直接使用printf、scanf等函數(shù)。
MSVC CRT的入口函數(shù)初始化:MSVC的入口函數(shù)初始化主要包含兩個部分,堆初始化和I/O初始化。
系統(tǒng)堆初始化:MSVC的堆初始化由函數(shù)_heap_init完成,它調(diào)用HeapCreate創(chuàng)建一個系統(tǒng)堆。
MSVC的I/O初始化:主要進行了如下幾個工作:建立打開文件表;如果能夠繼承自父進程,那么從父進程獲取繼承的句柄;初始化標準輸入輸出。
2. C/C++運行庫
C語言運行庫:任何一個C程序,它的背后都有一套龐大的代碼來進行支撐,以使得該程序能夠正常運行。這套代碼至少包括入口函數(shù),及其所依賴的函數(shù)所構(gòu)成的函數(shù)集合。當然,它還理應(yīng)包括各種標準庫函數(shù)的實現(xiàn)。這樣的一個代碼集合稱之為運行時庫(Runtime Library)。而C語言的運行庫,即被稱為C運行庫(CRT)。
一個C語言運行庫大致包含了如下功能:
(1). 啟動與退出:包括入口函數(shù)及入口函數(shù)所依賴的其它函數(shù)等。
(2). 標準函數(shù):由C語言標準規(guī)定的C語言標準庫所擁有的函數(shù)實現(xiàn)。
(3). I/O:I/O功能的封裝和實現(xiàn)。
(4). 堆:堆的封裝和實現(xiàn)。
(5). 語言實現(xiàn):語言中一些特殊功能的實現(xiàn)。
(6). 調(diào)試:實現(xiàn)調(diào)試功能的代碼。
C語言標準庫:美國國家標準協(xié)會(American National Standards Institute, ANSI)在1983年成立了一個委員會,旨在對C語言進行標準化,此委員會所建立的C語言標準被稱為ANSI C。第一個完整的C語言標準建立于1989年,此版本的C語言標準稱為C89。在C89標準中,包含了C語言基礎(chǔ)函數(shù)庫,由C89指定的C語言基礎(chǔ)函數(shù)庫就稱為ANSI C標準運行庫(簡稱標準庫)。其后在1995年C語言標準委員會對C89標準進行了一次修訂,在此次修訂中,ANSI C標準庫得到了第一次擴充,頭文件iso646.h、wchar.h和wctype.h加入了標準庫的大家庭。在1999年,C99標準誕生,C語言標準庫得到了進一步的擴充,頭文件complex.h、fenv.h、inttypes.h、stdbool.h、stdint.h和tgmath.h進入標準庫。C11標準是C語言標準的第三版,前一個標準版本是C99標準。C11標準中又新增了5個頭文件stdalign.h、stdatomic.h、stdnoreturn.h、threads.h、uchar.h。至此,C標準函數(shù)庫共29個頭文件。除了之前的14個頭文件,剩下的15個頭文件(C89標準)為:assert.h、ctype.h、errno.h、float.h、limits.h、locale.h、math.h、setjmp.h、signal.h、stdarg.h、stddef.h、stdio.h、stdlib.h、string.h、time.h。C語言的標準庫非常輕量,它僅僅包含了數(shù)學(xué)函數(shù)、字符/字符串處理、I/O等基本方面。關(guān)于每個頭文件的介紹可以參考:http://www.cplusplus.com/reference/clibrary/
變長參數(shù):是C語言的特殊參數(shù)形式,例如printf函數(shù),其聲明如下:
int printf(const char * format, ...);
如此的聲明表明,printf函數(shù)除了第一個參數(shù)類型為const char*之外,其后可以追加任意數(shù)量、任意類型的參數(shù)。在函數(shù)的實現(xiàn)部分,可以使用stdarg.h里的多個宏來訪問各個額外的參數(shù):假設(shè)lastarg是變長參數(shù)函數(shù)的最后一個具名參數(shù)(例如printf里的format),那么在函數(shù)內(nèi)部定義類型為va_list的變量:va_list ap; 該變量以后將會依次指向各個可變參數(shù)。ap必須用宏va_start初始化一次,其中l(wèi)astarg必須是函數(shù)的最后一個具名的參數(shù)。va_start(ap, lastarg); 此后可以使用va_arg宏來獲得下一個不定參數(shù)(假設(shè)已知其類型為type):type next=va_arg(ap, type); 在函數(shù)結(jié)束前,還必須用宏va_end來清理現(xiàn)場。關(guān)于這幾個宏的用法可以參考:https://blog.csdn.net/fengbingchun/article/details/78483471
變長參數(shù)宏:在很多時候我們希望在定義宏的時候也能夠像printf一樣可以使用變長參數(shù),即宏的參數(shù)可以是任意個,這個功能可以由編譯器的變長參數(shù)宏實現(xiàn)。
// 在GCC編譯器下,變長參數(shù)宏可以使用”##”宏字符串連接操作實現(xiàn),比如:
#define printf(args …) fprintf(stdout, ##args)
// 而在MSVC下,我們可以使用__VA_ARGS__這個編譯器內(nèi)置宏,比如:
#define printf(…) fprintf(stdout, __VA_ARGS__) // 它的效果與前面的GCC下使用##效果一樣
glibc與MSVC CRT:運行庫是平臺相關(guān)的,因為它與操作系統(tǒng)結(jié)合得非常緊密。C語言的運行庫從某種程度上來講是C語言的程序和不同操作系統(tǒng)平臺之間的抽象層,它將不同的操作系統(tǒng)API抽象成相同的庫函數(shù)。比如我們可以在不同的操作系統(tǒng)平臺下使用fread來讀取文件,而事實上fread在不同的操作系統(tǒng)平臺下的實現(xiàn)是不同的,但作為運行庫的使用者我們不需要關(guān)心這一點。Linux和Windows平臺下的兩個主要C語言運行庫分別為glibc(GNU C Library)和MSVCRT(Microsoft Visual C Run-time)。值得注意的是,像線程操作這樣的功能并不是標準的C語言運行庫的一部分,但是glibc和MSVCRT都包含了線程操作的庫函數(shù)。比如glibc有一個可選的pthread庫中的pthread_create()函數(shù)可以用來創(chuàng)建線程;而MSVCRT中可以使用_beginthread()函數(shù)來創(chuàng)建線程。所以glibc和MSVCRT事實上是標準C語言運行庫的超集,它們各自對C標準庫進行了一些擴展。
??glibc:即GNU C Library,是GNU旗下的C標準庫。最初由自由軟件基金會FSF(Free Software Foundation)發(fā)起開發(fā),目的是為GNU操作系統(tǒng)開發(fā)一個C標準庫。glibc的發(fā)布版本主要由兩部分組成,一部分是頭文件,比如stdio.h、stdlib.h等,它們往往位于/usr/include;另外一部分則是庫的二進制文件部分。二進制部分主要的就是C語言標準庫,它有靜態(tài)和動態(tài)兩個版本。動態(tài)的標準庫為/lib/x86_64-linux-gnu/libc.so.6,而靜態(tài)標準庫為/usr/lib/x86_64-linux-gnu/libc.a。事實上glibc除了C標準庫之外,還有幾個輔助程序運行的運行庫,這幾個文件可以稱得上是真正的”運行庫”,它們就是/usr/lib/x86_64-linux-gnu/crti.o、/usr/lib/x86_64-linux-gnu/crt1.o、/usr/lib/x86_64-linux-gnu/crtn.o,雖然它們都很小,但這幾個文件都是程序運行的最關(guān)鍵的文件。
glibc啟動文件:crt1.o里面包含的就是程序的入口函數(shù)_start,由它負責調(diào)用__libc_start_main初始化libc并且調(diào)用main函數(shù)進入真正的程序主體。crti.o和crtn.o兩個目標文件中包含的代碼實際上是_init()函數(shù)和_finit()函數(shù)的開始和結(jié)尾部分,當這兩個文件和其它目標文件按照順序鏈接起來以后,剛好形成兩個完整的函數(shù)_init()和_finit()。可以用objdump查看這兩個文件的反匯編代碼,結(jié)果如下圖所示:于是在最終鏈接完成之后,輸出的目標文件中的”.init”段只包含了一個函數(shù)_init(),這個函數(shù)的開始部分來自于crti.o的”.init”段,結(jié)束部分來自于crtn.o的”.init”段。為了保證最終輸出文件中的”.init”和”.finit”的正確性,我們必須保證在鏈接時,crti.o必須在用戶目標文件和系統(tǒng)庫之前,而crtn.o必須在用戶目標文件和系統(tǒng)庫之后。鏈接器的輸入文件順序一般是:ld crt1.o crti.o [user_objects] [system_libraries] crtn.o,由于crt1.o(crt0.o)不包含”.init”段和”.finit”段,所以不會影響最終生成”.init”和”.finit”段時的順序。
在默認情況下,ld鏈接器會將libc、crt1.o等這些CRT和啟動文件與程序的模塊鏈接起來,但是有些時候,我們可能不需要這些文件,或者希望使用自己的libc和crt1.o等啟動文件,以替代系統(tǒng)默認的文件,這種情況在嵌入式系統(tǒng)或操作系統(tǒng)內(nèi)核編譯的時候很常見。GCC提供了兩個參數(shù)”-nostartfile”和”-nostdlib”,分別用來取消默認的啟動文件和C語言運行庫。
其實C++全局對象的構(gòu)造函數(shù)和析構(gòu)函數(shù)并不是直接放在.init和.finit段里面的,而是把一個執(zhí)行所有構(gòu)造/析構(gòu)的函數(shù)的調(diào)用放在里面,由這個函數(shù)進行真正的構(gòu)造和析構(gòu)。除了全局對象構(gòu)造和析構(gòu)之外,.init和.finit還有其它的作用。由于它們的特殊性(在main之前/之后執(zhí)行),一些用戶監(jiān)控程序性能、調(diào)試等工具經(jīng)常利用它們進行一些初始化和反初始化的工作。當然我們也可以使用”__atrribute__((section(“.init”)))”將函數(shù)放到.init段里面,但是要注意的是普通函數(shù)放在”.init”是會破壞它們的結(jié)構(gòu)的,因為函數(shù)的返回指令使得__init()函數(shù)會提前返回,必須使用匯編指令,不能讓編譯器產(chǎn)生”ret”指令。
GCC平臺相關(guān)目標文件:crtbeginT.o、libgcc.a、libgcc_eh.a、crtend.o這幾個文件實際上不屬于glibc,它們是GCC的一部分,它們都位于GCC的安裝目錄/usr/lib/gcc/x86_64-linux-gnu/4.9/下。crtbeginT.o及crtend.o這兩個文件是真正用于實現(xiàn)C++全局構(gòu)造和析構(gòu)的目標文件。C++這樣的語言的實現(xiàn)是跟編譯器密切相關(guān)的,而glibc只是一個C語言運行庫,它對C++的實現(xiàn)并不了解。而GCC是C++的真正實現(xiàn)者,它對C++的全局構(gòu)造和析構(gòu)了如指掌。于是它提供了兩個目標文件crtbeginT.o和crtend.o來配合glibc實現(xiàn)C++的全局構(gòu)造和析構(gòu)。由于GCC支持諸多平臺,能夠正確處理不同平臺之間的差異性也是GCC的任務(wù)之一。比如有些32位平臺不支持64位的long long類型的運算,編譯器不能夠直接產(chǎn)生相應(yīng)的CPU指令,而是需要一些輔助的例程來幫助實現(xiàn)計算。libgcc.a里面包含的就是這種類似的函數(shù),這些函數(shù)主要包括整數(shù)運算、浮點數(shù)運算(不同的CPU對浮點數(shù)的運算方法很不相同)等,而libgcc_eh.a則包含了支持C++的異常處理(Exception Handing)的平臺相關(guān)函數(shù)。另外GCC的安裝目錄下往往還有一個動態(tài)鏈接版本libgcc_s.so。
MSVC CRT:同一個版本的MSVC CRT根據(jù)不同的屬性提供了多種子版本,以供不同需求的開發(fā)者使用。按照靜態(tài)/動態(tài)鏈接,可以分為靜態(tài)版和動態(tài)版;按照單線程/多線程,可以分為單線程版和多線程版;按照調(diào)試/發(fā)布,可分為調(diào)試版和發(fā)布版;按照是否支持C++分為純C運行庫版和支持C++版;按照是否支持托管代碼分為支持本地代碼/托管代碼和純托管代碼版。這些屬性很多時候是相互正交的,也就是說它們之間可以相互組合。比如可以有靜態(tài)單線程純C純本地代碼調(diào)試版;也可以有動態(tài)的多線程純C純本地代碼發(fā)布版等。但有些組合是沒有的,比如動態(tài)鏈接版本的CRT是沒有單線程的,所有的動態(tài)鏈接CRT都是多線程安全的。這樣的不同組合將會出現(xiàn)非常多的子版本,于是微軟提供了一套運行庫的命名方法。這個命名方法是這樣的,靜態(tài)版和動態(tài)版完全不同。靜態(tài)版的CRT位于MSVC安裝目錄下的C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\lib,它們的命名規(guī)則為:p表示C Plusplus,即C++標準庫;mt表示Multi-Thread,即表示支持多線程;d表示Debug,即表示調(diào)試版本。
libc? [p]? [mt]? [d]? .lib
動態(tài)版的CRT的每個版本一般有兩個相對應(yīng)的文件,一個用于鏈接的.lib文件,一個用于運行時用的.dll動態(tài)鏈接庫。它們的命名方式與靜態(tài)版的CRT非常類似,稍微有所不同的是,CRT的動態(tài)鏈接庫DLL文件名中會包含版本號。比如Visual C++ 2013的多線程、動態(tài)鏈接版的DLL文件名為msvcr120.dll。下表列舉了一些最常見的MSVC CRT版本(以Visual C++ 2013為例):
C++ CRT:MSVC還提供了相應(yīng)的C++標準庫。如果你的程序是使用C++編寫的,那么就需要額外鏈接相應(yīng)的C++標準庫。這里的”額外”的意思是,如下表所列的C++標準庫里面包含的僅僅是C++的內(nèi)容,比如iostream、string、map等,不包含C的標準庫。
3. 運行庫與多線程
CRT的多線程困擾:
線程的訪問權(quán)限:線程的訪問能力非常自由,它可以訪問進程內(nèi)存里的所有數(shù)據(jù),甚至包括其它線程的堆棧(如果它知道其它線程的堆棧地址,然后這是很少見的情況),但實際運用中線程也擁有自己的私有存儲空間,包括:棧(盡管并非完全無法被其它線程訪問,但一般情況下仍然可以認為是私有的數(shù)據(jù));線程局部存儲(Thread Local Storage, TLS),是某些操作系統(tǒng)為線程單獨提供的私有空間,但通常只具有很有限的尺寸;寄存器(包括PC寄存器),是執(zhí)行流的基本數(shù)據(jù),因此為線程私有。從C程序員的角度來看,數(shù)據(jù)在線程之間是否私有如下表所示:
多線程運行庫:對于C/C++標準庫來說,線程相關(guān)的部分是不屬于標準庫的內(nèi)容的,它跟網(wǎng)絡(luò)、圖形圖像等一樣,屬于標準庫之外的系統(tǒng)相關(guān)庫。這里所說的”多線程相關(guān)”主要有兩個方面,一方面是提供那些多線程操作的接口,比如創(chuàng)建線程、退出線程、設(shè)置線程優(yōu)先級等函數(shù)接口;另外一方面是C運行庫本身要能夠在多線程的環(huán)境下正確運行。對于第一方面,主流的CRT都會有相應(yīng)的功能。比如Windows下,MSVC CRT提供了諸如_beginthread()、_endthread()等函數(shù)用于線程的創(chuàng)建和退出;而Linux下,glibc也提供了一個可選的線程庫pthread(POSIX Thread),它提供了諸如pthread_create()、pthread_exit()等函數(shù)用于線程的創(chuàng)建和退出。很明顯,這些函數(shù)都不屬于標準的運行庫,它們都是平臺相關(guān)的。對于第二個方面,C語言運行庫必須支持多線程的環(huán)境,實際上,最初CRT在設(shè)計的時候是沒有考慮多線程環(huán)境的,因為當時根本沒有多線程這樣的概念。
CRT改進:(1). 使用TLS;(2). 加鎖:在多線程版本的運行庫中,線程不安全的函數(shù)內(nèi)部都會自動地進行加鎖;(3). 改進函數(shù)調(diào)用方式:C語言的運行庫為了支持多線程特性,一種改進的辦法就是修改所有的線程不安全的函數(shù)的參數(shù)列表,改成某種線程安全的版本。但是很多時候改變標準庫函數(shù)的做法是不可行的。標準庫之所以稱之為”標準”,就是它具有一定的權(quán)威性和穩(wěn)定性,不能隨意更改。
線程局部存儲實現(xiàn):TLS的用法很簡單,如果要定義一個全局變量為TLS類型的,只需要在它定義前加上相應(yīng)的關(guān)鍵字即可。對于GCC來說,這個關(guān)鍵字就是__thread。對于MSVC來說,相應(yīng)的關(guān)鍵字為__declspec(thread)。一旦一個全局變量被定義成TLS類型的,那么每個線程都會擁有這個變量的一個副本,任何線程對該變量的修改都不會影響其它線程中該變量的副本。
Windows TLS的實現(xiàn):對于Windows系統(tǒng)來說,正常情況下一個全局變量或靜態(tài)變量會被放到”.data”或”.bss”段中,但當我們使用__declspec(thread)定義一個線程私有變量的時候,編譯器會把這些變量放到PE文件的”.tls”段中。當系統(tǒng)啟動一個新的線程時,它會從進程的堆中分配一塊足夠大小的空間,然后把”.tls”段中的內(nèi)容復(fù)制到這塊空間中,于是每個線程都有自己獨立的一個”.tls”副本。所以對于用__declspec(thread)定義的同一個變量,它們在不同線程中的地址都是不一樣的。對于一個TLS變量來說,它有可能是一個C++的全局對象,那么每個線程在啟動時不僅僅是復(fù)制”.tls”的內(nèi)容那么簡單,還需要把這些TLS對象初始化,必須逐個地調(diào)用它們的全局構(gòu)造函數(shù),而且當線程退出時,還要逐個地將它們析構(gòu),正如普通的全局對象在進程啟動和退出時都要構(gòu)造、析構(gòu)一樣。
顯示TLS:使用__thread或__declspec(thread)關(guān)鍵字定義全局變量為TLS變量的方法往往被稱為隱式TLS,即程序員無須關(guān)心TLS變量的申請、分配賦值和釋放,編譯器、運行庫還有操作系統(tǒng)已經(jīng)將這一切悄悄處理妥當了。在程序員看來,TLS全局變量就是線程私有的全局變量。相對于隱式TLS,還有一種叫做顯示TLS的方法,這種方法是程序員需要手工申請TLS變量,并且每次訪問該變量時都要調(diào)用相應(yīng)的函數(shù)得到變量的地址,并且在訪問完成之后需要釋放該變量。在Windows平臺上,系統(tǒng)提供了TlsAlloc()、TlsGetValue()、TlsSetValue()和TlsFree()這4個API函數(shù)用于顯示TLS變量的申請、取值、賦值和釋放。Linux下相對應(yīng)的庫函數(shù)為pthread庫中的pthread_key_create()、pthread_getspecific()、pthread_setspecific()和pthread_key_delete()。相對于隱式的TLS變量,顯式的TLS變量的使用十分麻煩,而且有諸多限制。
在Windows下創(chuàng)建一線程的方法有兩種,一種是調(diào)用Windows API CreateThread()來創(chuàng)建線程;另外一種就是調(diào)用MSVC CRT的函數(shù)_beginthread()或_beginthreadex()來創(chuàng)建線程。在使用靜態(tài)鏈接CRT(/MT, /MTd)時,CreateThread()可能會導(dǎo)致內(nèi)存泄漏。當使用CRT時(基本上所有的程序都使用CRT),盡量使用_beginthread()/_beginthreadex()/_endthread()/_endthreadex()這組函數(shù)來創(chuàng)建線程。
4. C++全局構(gòu)造與析構(gòu)
glibc全局構(gòu)造與析構(gòu): 對于每個編譯單元(.cpp),GCC編譯器會遍歷其中所有的全局對象,生成一個特殊的函數(shù),這個特殊函數(shù)的作用就是對本編譯單元里的所有全局對象進行初始化。GCC在目錄代碼中生成了一個名為_GLOBAL_I_Hw的函數(shù),由這個函數(shù)負責本編譯單元所有的全局/靜態(tài)對象的構(gòu)造和析構(gòu)。由于全局對象的構(gòu)建和析構(gòu)都是由運行庫完成的,于是在程序或共享庫中有全局對象時,記得不能使用”-nonstartfiles”或”-nostdlib”選項,否則,構(gòu)建與析構(gòu)函數(shù)將不能正確執(zhí)行。
MSVC CRT的全局構(gòu)造和析構(gòu):MSVC CRT的全局構(gòu)造實現(xiàn)在機制上與Glibc基本是一樣的,只不過它們的名字略有不同。Glibc下通過__cxa_exit()向exit()函數(shù)注冊全局析構(gòu)函數(shù),MSVC CRT也通過atexit()實現(xiàn)全局析構(gòu),它們除了函數(shù)命名不同之外幾乎沒有區(qū)別。
5. fread實現(xiàn)
緩沖(Buffer):緩沖最為常見于IO系統(tǒng)中,設(shè)想一下,當希望向屏幕輸出數(shù)據(jù)的時候,由于程序邏輯的關(guān)系,可能要多次調(diào)用printf函數(shù),并且每次寫入的數(shù)據(jù)只有幾個字符,如果每次寫數(shù)據(jù)都要進行一次系統(tǒng)調(diào)用,讓內(nèi)核向屏幕寫數(shù)據(jù),就明顯過于低效,因為系統(tǒng)調(diào)用的開銷是很大的,它要進行上下文切換、內(nèi)核參數(shù)檢查、復(fù)制等,如果頻繁進行系統(tǒng)調(diào)用,將會嚴重影響程序和系統(tǒng)的性能。一個顯而易見的可行方案是將對控制臺連續(xù)的多次寫入放在一個數(shù)組里,等到數(shù)組被填滿之后再一次性完成系統(tǒng)調(diào)用寫入,實際上這就是緩沖最基本的想法。當讀文件的時候,緩沖同樣存在。我們可以在CRT中為文件建立一個緩沖,當要讀取數(shù)據(jù)的時候,首先看看這個文件的緩沖里有沒有數(shù)據(jù),如果有數(shù)據(jù)就直接從緩沖中取。如果緩沖是空的,那么CRT就通過操作系統(tǒng)一次性讀取文件一塊較大的內(nèi)容填充緩沖。這樣,如果每次讀取文件都是一些尺寸很小的數(shù)據(jù),那么這些讀取操作大多都直接從緩沖中獲得,可以避免大量的實際文件訪問。除了讀文件有緩沖以外,寫文件也存在著同樣的情況,而且寫文件比讀文件要更加復(fù)雜。因為當我們通過fwrite向文件寫入一段數(shù)據(jù)時,此時這些數(shù)據(jù)不一定被真正地寫入到文件中,而是有可能還存在于文件的寫緩沖里面,那么此時如果系統(tǒng)崩潰或進程意外退出時,有可能導(dǎo)致數(shù)據(jù)丟失,于是CRT還提供了一系列與緩沖相關(guān)的操作用于彌補緩沖所帶來的問題。C語言標準庫提供與緩沖相關(guān)的幾個基本函數(shù),如下表所示:所謂flush一個緩沖,是指對寫緩沖而言,將緩沖內(nèi)的數(shù)據(jù)全部寫入實際的文件,并將緩沖清空,這樣可以保證文件處于最新的狀態(tài)。之所以需要flush,是因為寫緩沖使得文件處于一種不同步的狀態(tài),邏輯上一些數(shù)據(jù)已經(jīng)寫入了文件,但實際上這些數(shù)據(jù)仍然在緩沖中,如果此時程序意外地退出(發(fā)生異常或斷電等),那么緩沖里的數(shù)據(jù)將沒有機會寫入文件,flush可以在一定程度上避免這樣的情況發(fā)生。C語言支持兩種緩沖,即行緩沖(Line Buffer)和全緩沖(Full Buffer)。全緩沖是經(jīng)典的緩沖形式,除了用戶手動調(diào)用fflush外,僅當緩沖滿的時候,緩沖才會被自動flush掉。而行緩沖則比較特殊,這種緩沖僅用于文本文件,在輸入輸出遇到一個換行符時,緩沖就會被自動flush,因此叫行緩沖。
文本換行:在Windows的文本文件中,回車(換行)的存儲方式是0x0D(用CR表示),0x0A(用LF表示)這兩個字節(jié),以C語言字符串表示則是”\r\n”。而在其它的一些操作系統(tǒng)中,回車的表示卻有區(qū)別。例如,Linux/Unix,回車用\n表示;Mac OS,回車用\r表示;Windows,回車用\r\n表示。而在C語言中,回車始終用\n來表示,因此在以文本模式讀取文件的時候,不同的操作需要將各自的回車符表示轉(zhuǎn)換為C語言的形式,也就是,Linux/Unix,不做改變;Mac OS,每遇到\r就將其改為\n;Windows,將\r\n改為\n。
GitHub:https://github.com/fengbingchun/Messy_Test
總結(jié)
以上是生活随笔為你收集整理的程序员的自我修养--链接、装载与库笔记:运行库的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 程序员的自我修养--链接、装载与库笔记:
- 下一篇: 程序员的自我修养--链接、装载与库笔记: