日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

静态链接中的那点事儿(2):C++二进制兼容性及跨平台初步

發布時間:2025/3/15 c/c++ 47 豆豆
生活随笔 收集整理的這篇文章主要介紹了 静态链接中的那点事儿(2):C++二进制兼容性及跨平台初步 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

C++的一些語言特性使之必須由編譯器和鏈接器共同支持才能完成工作。最主要的有兩個方面,其一,C++的重復代碼的消除;其二,全局構造與析構。此外,由于C++的各種特性,比如虛函數、函數重載、繼承、異常等,使得C++背后的數據結構異常復雜。而且最為不幸的是,這些數據結構往往在不同的編譯器和鏈接器之間不能相互通用,使得C++程序的二進制兼容性成為一個難題。本篇博客將結合項目經驗初步討論C++程序的二進制兼容性問題。

1.C++中重復代碼是如何消除的?

C++編譯器在很多時候會產生重復的代碼,比如模板(Templates)、外部內聯函數(External Inline Function)和虛函數表(Virtual Function Table)都有可能在不同的編譯單元里生成相同的代碼。最簡單的情況就是拿模板來說事,模板從本質上來講很像宏,當模板在一個編譯單元里被實例化時,他并不知道自己是否在別的編譯單元也被實例化了。所以當一個模板在多個編譯單元同時實例化成相同的類型的時候,必然會生成重復的代碼。當然,最簡單的方式就是不管這些,就將這些重復的代碼都保留下來。但這會存在以下幾個問題:

1.空間浪費。可以想象一個由好幾百個編譯單元的工程同時實例化了許多個模板,最后鏈接的時候必須將這些重復的代碼消除掉,否則最終程序的大小坑定會膨脹的很厲害。

2.地址容易出錯。有可能兩個指向同一個函數的指針會不想等。

3.指令運行效率低。因為現代的CPU都會對指令和數據進行緩存,如果同樣一份指令有多個副本,那么指令Cache的命中率就會降低。

一個比較有效的做法就是將每個模板的示例代碼都單獨地存放在一個段里,每個段只包含一個模板實例。比如有個模板函數是add<T>(),某個編譯單元以int類型【add<int>()】和float類型【add<float>()】實例化了該模板函數,那么該編譯單元的目標文件中就包含了兩個該模板實例的段,為了簡單起見,我們假設這兩個段的名字分別叫.temp.add.<int>和 .temp.add<float>。這樣,當別的編譯單元也以int和float類型實例化該模板函數后,也會生成相同的名字,這樣鏈接器在最終鏈接的時候可以區分這些相同的模板實例段,然后將他們合并入最后的代碼段。

這種重復代碼消除對于模板來說是這樣的,對于外部的內聯函數和虛函數表的做法也類似。

函數級別鏈接

由于現在的程序和庫通常來講都非常龐大,一個目標文件可能包含成百上千個函數或變量。當我們需要用到某個目標文件中的任意一個函數或變量時,就必須把這個文件整個的鏈接起來,也就是說那些沒有用到的函數也被一起鏈接了進來。這樣導致的直接結果就是:鏈接輸出文件的輸出結果會變得相當的龐大,所有用到到沒用到的變量和函數都一起塞到了輸出文件中

無論是VC++編譯器還是GCC編譯器都提供了類似模板代碼冗余消除的方法!具體為:讓所有的函數氮素保存到一個段里面,當連接器需要用到某個函數的時候,就會將他直接合并到輸出文件中,對于那些沒有用到的函數進行舍棄。該方法很大程度上減小了輸出文件的長度,減少了空間浪費。

2.全局的構造函數與析構函數是如何執行的?

我們知道一般一個C++/C程序是從main函數開始執行的,隨著main函數的結束而結束。然而,其實在main函數被調用之前,為了保證程序可以順利地進行,首先要初始化進程執行環境,比如堆分配初始化(malloc/free)、線程子系統等,C++全局對象的構造函數就是在該時期被執行的。【實際上C++的全局對象的構造函數在main之前被執行,C++全局對象的析構函數在main之后被執行】

根據前面的情境,我們能夠發現,對于某些場合,程序的一些特定的操作必須在main函數之前被執行,還有一些操作必須在main之后進行執行。當然啦,我最習慣舉得例子就是,C++中全局對象的構造與析構的過程。為了滿足這一特殊情況,linux系統下的ELF文件結構還定義了兩種特殊的段:

.init: 該段里面保存的是可執行指令,他構成了進程的初始化代碼。在main函數之前被調用。

.fini: 該字段保存著進程終止代碼指令,在main函數執行結束之后被調用。


3.C++的兼容問題及跨平臺編程初步

既然每個編譯器都能將源代碼編譯成目標文件,那么不同編譯器編譯出來的目標文件可以相互鏈接嗎?比如說:MSVC編譯出來的目標文件和GCC編譯出來的目標文件能否鏈接在一起,從而形成可執行文件呢?

對于上面這些問題,首先可以想到的是,如果要將兩個不同編譯器編譯出來的編譯結果鏈接到一起,那么,鏈接器必須同時支持這兩個編譯器產生的目標文件格式。比如說,MSVC編譯器的目標文件是PE/COFF格式,而GCC編譯的結果是ELF格式的。只有連接器同時認識這兩個格式才行,否則肯定沒戲!那么鏈接器是不是只要同時認識目標文件的格式就可以了呢?不見得!!!

事情絕對沒有這么簡單,如果我們要使兩個編譯器編譯出來的目標文件能夠相互鏈接,那這兩個/多個目標文件必須滿足下面的幾個條件:

1.采用相同的目標文件格式;

2.擁有同樣的函數符號修飾標準;

3.變量的內存分布方式相同;

4.函數的調用方式相同。

其中,國際慣例,把符號修飾標準、變量內存分布布局、函數調用方式等這些跟可執行代碼二進制兼容性相關的內容稱為ABI(Application Binary Interface)。

C++一直為人詬病的一大原因就在于他的二進制兼容性不好!或者說,比起C語言來說更不容易。一方面,不同的編譯器編譯出來的二進制代碼之間無法相互兼容,甚至有時候,連同一個編譯器的不同版本之間的兼容性也不好。最普通的例子:我有一個庫A是Company A 用Compiler A編譯的;還有一個庫B是Company A 用Compiler B編譯的,當我寫一個C++程序來同時使用庫A和庫B是,就會相當的棘手!!!我的朋友曾和我說過,只要我可以把一個開源項目的所有源代碼在同一個編譯環境下跑一遍,那不就OK了?!當然,這對于小型項目而言是容易可行的,但是考慮到一些大型程序,逐一進行編譯明顯是不合理的,這從某種意義上來講,就造成軟件開發的效率低下。

此外,很多時候,庫廠商往往不希望用戶看到哭的源代碼,所以一般都會以二進制的方式提供給用戶。這樣就會帶來一個問題,如果庫廠商提供產品的編譯器型號和版本編譯器型號不同時,代碼基本上就被判了死刑。當然,這個問題不僅僅存在用戶和庫廠商之間;當一個程序由多個部門或多個公司聯合開發時,類似的問題更明顯。

所以絕大多數程序員一直期待能有統一的C++二進制兼容標準(C++ ABI),諸多的團體以及社區都在致力于C++ ABI標準的統一,但是前景非常的不樂觀。基本形成了以微軟的Visual C++和GNU的GCC為首的兩大派系。

4.難兄難弟——API與ABI

很多時候,我們都會碰到這兩個詞,他們長得太像了,僅僅一字之差。API = Application Programming Interface; ABI = Application Binary Interface,實際上他們都是應用程序接口。只是他們所描述的接口所在的層面不一樣。API往往是指源代碼的應用程序接口;而ABI是指二進制層面的接口,所以ABI的兼容程度要比API的兼容性嚴格得多。比如:我們可以說C++的對象內存分布(object Memory Layout)是C++ ABI的一部分。相比之下API更關注源代碼層面。 ABI的概念相比較之下歷史更悠久,因為每一個程序員都希望在不經過任何修改的情況下,重新利用辛辛苦苦寫的程序!如果可以,最好的情況就是,二進制的指令和數據還能夠不加修改地得到重用,但是由于MS和GNU惡性競爭的原因,二進制級別的重用任重而道遠。 下面是百度給的一段理解:

正式因為這些挑戰的存在,才有了諸君存在的價值,開源社區算是MS和GNU之外的第三股力量,希望某天開源社區能夠統ABI,這樣廣大程序猴們就能夠去做更有意義的事了......

總結

以上是生活随笔為你收集整理的静态链接中的那点事儿(2):C++二进制兼容性及跨平台初步的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。