c 混合使用不同標準編譯潛在的問題

2022-07-31 19:27:12 字數 3938 閱讀 4061

最近專案使用的c++的版本到c++11了,但是由於有些靜態庫(.a)沒有原始碼,因此鏈結時還在使用非c++11版本的庫檔案。目前跑了幾天,似乎是沒出什麼問題,但是我還是想說一下這樣做有哪些潛在的風險。

首先需要說明的是,公升級到c++11之後,部分std的資料結構的記憶體布局有可能發生改變(待考究)。最開始,我認為只要靜態庫暴露出來的介面沒有使用這些不相容的資料結構即可。也就是說,如果靜態庫暴露的所有介面都是純c風格的,沒有使用任何c++ std的資料結構,則鏈結這種靜態庫應該是安全的。但是後來發現似乎並不是這樣...

來看看c++消除重複**的機制

假設有乙個模板類myclass定義在標頭檔案my_template.h中。原始檔x.cpp和y.cpp都包含了這個標頭檔案,並且用型別int例項化了這個模板類。由於在編譯時這兩個原始檔是完全獨立的,因此兩個原始檔生成的object file(x.o和y.o)中都會包含了myclass的**。但是實際上對應乙個可執行的程式來說,myclass的**存在乙份即可。因此在鏈結階段會有乙個重複**消除的步驟,回到上述例子就是x.o和y.o裡的myclass的**會被合併,最後在可執行程式裡僅存在乙份。

linux gcc通過elf的comdat section來實現消除重複的模板**。comdat是一種特殊的section(elf和coff都有comdat section的概念),通常它會關聯乙個字串(也可能就是section的名字)。鏈結器在處理object file時,對遇到的同名的section會執行去重操作,保證在輸出的output file中僅存在乙份例項。

看看下面的**

//

my_template.h

template

class

myclass

public

:#ifdef test

inti1;

inti2;

#else

inti2;

inti1;

#endif

};

上述**定義了模板類myclass,並且其記憶體布局依賴於乙個test巨集。然後我們這樣去使用它:

//

x.cpp

#include

#include

"my_template.h

"void

func_in_x()

#include

#define test#include

"my_template.h

"void

func_in_y()

可以看到,在x.cpp中沒有定義巨集test,而在y.cpp中定義了巨集test。因此這兩個編譯單元看到的myclass的記憶體布局應該是不一樣的。

objdump -s x.o 看到成員函式func1的**是這樣的:

0000000000000000

<_zn7myclassiie5func1ev>:

template class myclass

1d:90nop

1e: 5d pop

%rbp

1f: c3 retq

objdump -s y.o 看到成員函式func1的**是這樣的:

0000000000000000

<_zn7myclassiie5func1ev>:

template class myclass

1d:90nop

1e: 5d pop

%rbp

1f: c3 retq

看上述生成的彙編**可知,myclass的記憶體布局確實不一樣。在x.o中成員i1的起始位址在物件記憶體的第4個位元組處(偏移是0x4),而在y.o中成員i1的起始位址就是物件的位址(偏移是0x0)。

主函式**如下:

void

func_in_x();

void

func_in_y();

intmain()

執行程式,發現結果如下:

$ ./a.out

i1=1, i2=2

i1=2, i2=1

雖然我們在x.cpp和y.cpp都是對i1賦值為1,對i2賦值為2。但是卻出現了乙個i1值為2,i2值為1的結果。

objdump -s a.out 發現func1的**如下:

0000000000000712

<_zn7myclassiie5func1ev>:

template class myclass

72f:

90nop

730: 5d pop

%rbp

731: c3 retq

可以發現,到了可執行程式中確實僅有乙份myclass的**,並且是用了x.cpp的那乙份。因此在x.cpp中輸出的結果是對的,在y.cpp中輸出的結果確是相反的。

$ g++ y.o x.o main.o

$ ./a.

outi1=

2, i2=1

i1=1, i2=2

這次變成x.cpp中輸出是錯的,y.cpp中輸出是對的了。

從上述例子可知:鏈結器在對comdat section去重時,並沒有辨別section的內容是否一致。因此在不同的object file中記憶體布局不一致的c++模板被鏈結到一起是有潛在的風險的。靜態庫(.a)檔案實際上是單純的object file集合再加上乙個符號表,因此鏈結靜態庫(.a)檔案情況和鏈結object file是一樣的。而動態庫(.so)的情況可能稍有不同。

因此剩下的問題就是,使用不同c++標準去編譯std的資料結構會生成同名comdat section嗎?

以vector::push_back函式為例:

$ g++ -std=c++98 x.cpp -c

$ readelf -g x.o | grep

push_back

comdat group section [

4] `.group'

[_znst6vectoriisaiiee9push_backerki] contains 2 sections:

[ 64

] .text._znst6vectoriisaiiee9push_backerki

[

65] .rela.text._znst6vectoriisaiiee9push_backerki

$ g++ -std=c++11 x.cpp -c

$ readelf -g x.o | grep

push_back

comdat group section [

5] `.group'

[_znst6vectoriisaiiee9push_backeoi] contains 2 sections:

[ 72

] .text._znst6vectoriisaiiee9push_backeoi

[

73] .rela.text._znst6vectoriisaiiee9push_backeoi

發現為vector::push_back生成的comdat section確實是不同名字的。但是在沒有徹底弄清楚這個命名規則(mangling)之前,也僅能說明的是vector::push_back這個函式是沒有問題,不代表其它的情況。

還有一些問題尚未解決,這裡記錄一下:

(1)不同c++版本編譯對mangling的影響?對生成comdat section的影響?

(2)動態庫(.so)是否也存在這種問題?對動態庫不同的使用方式會有影響?比如程序執行時鏈結和通過dlopen方式是否會不同?

ubuntu伺服器下不同使用者使用不同的cuda版本

安裝cuda 安裝過程中在建立軟鏈結時需要注意一下,如果你是第一次安裝cuda,那麼毫無疑問輸入y yes 但是如果你是安裝額外版本的cuda,是否選擇y yes 就要看你的具體需求而定,簡言之,就是如果你希望啟用當前安裝的cuda版本,就選y,如果你只是想安裝這個版本,而暫時還不想啟用該版本時,就...

C和C 的混合編譯 extern 「C」的使用

在實際程式設計中,有時會需要用到c c 的混合編譯,通過extern c 即可實現混合編譯,以下是個人參照的資料以及自己的實際操作的收穫。一 c 中呼叫c程式 中print函式通過extern c 來指明按照c的編譯方式來編譯,編譯完成執行結果如下 二 在c中呼叫c 程式,分三個檔案進行 1 cpp...

混合使用標準庫類 string 和 C 風格字串

許多 c 程式在有標準類之前就已經存在了,因此既沒有使用標準庫型別 string 也沒有使用 vector。而且,許多 c 程式為了相容現存的 c 程式,也不能使用 c 標準庫。因此,現代的 c 程式經常必須相容使用陣列和 或 c 風格字串的 標準庫提供了相容處理。毫無疑問,當然可以用字串字面值來初...