約在20世紀70年代以前,編譯器編譯源代碼產生目標文件時,符號名與相應的變量和函數的名字是一樣的。比如一個匯編源代碼里面包含了一個函數foo,那么匯編器將它編譯成目標文件以后,foo在目標文件中的相對應的符號名也是foo。當后來UNIX平台和C語言發明時,已經存在了相當多的使用匯編編寫的庫和目標文件。這樣就產生了一個問題,那就是如果一個C程序要使用這些庫的話,C語言中不可以使用這些庫中定義的函數和變量的名字作為符號名,否則將會跟現有的目標文件沖突。比如有個用匯編編寫的庫中定義了一個函數叫做main,那么我們在C語言里面就不可以再定義一個main函數或變量了。同樣的道理,如果一個C語言的目標文件要用到一個使用Fortran語言編寫的目標文件,我們也必須防止它們的名稱沖突。
為了防止類似的符號名沖突,UNIX下的C語言就規定,C語言源代碼文件中的所有全局的變量和函數經過編譯以后,相對應的符號名前加上下划線"_"。而Fortran語言的源代碼經過編譯以后,所有的符號名前加上"_",后面也加上"_"。比如一個C語言函數"foo",那么它編譯后的符號名就是"_foo";如果是Fortran語言,就是"_foo_"。
這種簡單而原始的方法的確能夠暫時減少多種語言目標文件之間的符號沖突的概率,但還是沒有從根本上解決符號沖突的問題。比如同一種語言編寫的目標文件還有可能會產生符號沖突,當程序很大時,不同的模塊由多個部門(個人)開發,它們之間的命名規范如果不嚴格,則有可能導致沖突。於是像C++這樣的后來設計的語言開始考慮到了這個問題,增加了名稱空間(Namespace)的方法來解決多模塊的符號沖突問題。
但是隨着時間的推移,很多操作系統和編譯器被完全重寫了好幾遍,比如UNIX也分化成了很多種,整個環境發生了很大的變化,上面所提到的跟Fortran和古老的匯編庫的符號沖突問題已經不是那么明顯了。在現在的Linux下的GCC編譯器中,默認情況下已經去掉了在C語言符號前加"_"的這種方式;但是Windows平台下的編譯器還保持的這樣的傳統,比如Visual C++編譯器就會在C語言符號前加"_",GCC在Windows平台下的版本(cygwin、mingw)也會加"_"。GCC編譯器也可以通過參數選項"-fleading-underscore"或"-fno-leading-underscore"來打開和關閉是否在C語言符號前加上下划線。
C++符號修飾
眾所周知,強大而又復雜的C++擁有類、繼承、虛機制、重載、名稱空間等這些特性,它們使得符號管理更為復雜。最簡單的例子,兩個相同名字的函數func(int)和func(double),盡管函數名相同,但是參數列表不同,這是C++里面函數重載的最簡單的一種情況,那么編譯器和鏈接器在鏈接過程中如何區分這兩個函數呢?為了支持C++這些復雜的特性,人們發明了符號修飾(Name Decoration)或符號改編(Name Mangling)的機制,下面我們來看看C++的符號修飾機制。
首先出現的一個問題是C++允許多個不同參數類型的函數擁有一樣的名字,就是所謂的函數重載;另外C++還在語言級別支持名稱空間,即允許在不同的名稱空間有多個同樣名字的符號。比如清單3-4這段代碼:
清單3-4 C++ 函數的名稱修飾
部結構我們在這里先不展開了,在下一章分析靜態鏈接過程的時候,我們還會詳細地分析重定位表的結構。
- int func(int);
- float func(float);
- class C {
- int func(int);
- class C2 {
- int func(int);
- };
- };
- namespace N {
- int func(int);
- class C {
- int func(int);
- };
- }
這段代碼中有6個同名函數叫func,只不過它們的返回類型和參數及所在的名稱空間不同。我們引入一個術語叫做函數簽名(Function Signature),函數簽名包含了一個函數的信息,包括函數名、它的參數類型、它所在的類和名稱空間及其他信息。函數簽名用於識別不同的函數,就像簽名用於識別不同的人一樣,函數的名字只是函數簽名的一部分。由於上面6個同名函數的參數類型及所處的類和名稱空間不同,我們可以認為它們的函數簽名不同。在編譯器及鏈接器處理符號時,它們使用某種名稱修飾的方法,使得每個函數簽名對應一個修飾后名稱(Decorated Name)。編譯器在將C++源代碼編譯成目標文件時,會將函數和變量的名字進行修飾,形成符號名,也就是說,C++的源代碼編譯后的目標文件中所使用的符號名是相應的函數和變量的修飾后名稱。C++編譯器和鏈接器都使用符號來識別和處理函數和變量,所以對於不同函數簽名的函數,即使函數名相同,編譯器和鏈接器都認為它們是不同的函數。上面的6個函數簽名在GCC編譯器下,相對應的修飾后名稱如表3-18所示。
表3-18
函數簽名 |
修飾后名稱(符號名) |
int func(int) |
_Z4funci |
float func(float) |
_Z4funcf |
int C::func(int) |
_ZN1C4funcEi |
int C::C2::func(int) |
_ZN1C2C24funcEi |
int N::func(int) |
_ZN1N4funcEi |
int N::C::func(int) |
_ZN1N1C4funcEi |
GCC的基本C++名稱修飾方法如下:所有的符號都以"_Z"開頭,對於嵌套的名字(在名稱空間或在類里面的),后面緊跟"N",然后是各個名稱空間和類的名字,每個名字前是名字字符串長度,再以"E"結尾。比如N::C::func經過名稱修飾以后就是_ZN1N1C4funcE。對於一個函數來說,它的參數列表緊跟在"E"后面,對於int類型來說,就是字母"i"。所以整個N::C::func(int)函數簽名經過修飾為_ZN1N1C4funcEi。更為具體的修飾方法我們在這里不詳細介紹,有興趣的讀者可以參考GCC的名稱修飾標准。幸好這種名稱修飾方法我們平時程序開發中也很少手工分析名稱修飾問題,所以無須很詳細地了解這個過程。binutils里面提供了一個叫"c++filt"的工具可以用來解析被修飾過的名稱,比如:
- $ c++filt _ZN1N1C4funcEi
- N::C::func(int)
簽名和名稱修飾機制不光被使用到函數上,C++中的全局變量和靜態變量也有同樣的機制。對於全局變量來說,它跟函數一樣都是一個全局可見的名稱,它也遵循上面的名稱修飾機制,比如一個名稱空間foo中的全局變量bar,它修飾后的名字為:_ZN3foo3barE。值得注意的是,變量的類型並沒有被加入到修飾后名稱中,所以不論這個變量是整形還是浮點型甚至是一個全局對象,它的名稱都是一樣的。
名稱修飾機制也被用來防止靜態變量的名字沖突。比如main()函數里面有一個靜態變量叫foo,而func()函數里面也有一個靜態變量叫foo。為了區分這兩個變量,GCC會將它們的符號名分別修飾成兩個不同的名字_ZZ4mainE3foo和_ZZ4funcvE3foo,這樣就區分了這兩個變量。
不同的編譯器廠商的名稱修飾方法可能不同,所以不同的編譯器對於同一個函數簽名可能對應不同的修飾后名稱。比如上面的函數簽名中在Visual C++編譯器下,它們的修飾后名稱如表3-19所示。
表3-19
函數簽名 |
修飾后名稱 |
int func(int) |
?func@@YAHH@Z |
float func(float) |
?func@@YAMM@Z |
int C::func(int) |
?func@C@@AAEHH@Z |
int C::C2::func(int) |
?func@C2@C@@AAEHH@Z |
int N::func(int) |
?func@N@@YAHH@Z |
int N::C::func(int) |
?func@C@N@@AAEHH@Z |
我們以int N::C::func(int)這個函數簽名來猜測Visual C++的名稱修飾規則(當然,你只須大概了解這個修飾規則就可以了)。修飾后名字由"?"開頭,接着是函數名由"@"符號結尾的函數名;后面跟着由"@"結尾的類名"C"和名稱空間"N",再一個"@"表示函數的名稱空間結束;第一個"A"表示函數調用類型為"__cdecl"(函數調用類型我們將在第4章詳細介紹),接着是函數的參數類型及返回值,由"@"結束,最后由"Z"結尾。可以看到函數名、參數的類型和名稱空間都被加入了修飾后名稱,這樣編譯器和鏈接器就可以區別同名但不同參數類型或名字空間的函數,而不會導致link的時候函數多重定義。
Visual C++的名稱修飾規則並沒有對外公開,當然,一般情況下我們也無須了解這套規則,但是有時候可能須要將一個修飾后名字轉換成函數簽名,比如在鏈接、調試程序的時候可能會用到。Microsoft提供了一個UnDecorateSymbolName()的API,可以將修飾后名稱轉換成函數簽名。下面這段代碼使用UnDecorateSymbolName()將修飾后名稱轉換成函數簽名:
- /* 2-4.c
- * Compile: cl 2-4.c /link Dbghelp.lib
- * Usage: 2-4.exe DecroatedName
- */
- #include <Windows.h>
- #include <Dbghelp.h>
- int main( int argc, char* argv[] )
- {
- char buffer[256];
- if(argc == 2)
- {
- UnDecorateSymbolName( argv[1], buffer, 256, 0 );
- printf( buffer );
- }
- else
- {
- printf( "Usage: 2-4.exe DecroatedName\n" );
- }
- return 0;
- }
由於不同的編譯器采用不同的名字修飾方法,必然會導致由不同編譯器編譯產生的目標文件無法正常相互鏈接,這是導致不同編譯器之間不能互操作的主要原因之一。我們后面的關於C++ ABI和COM的這一節將會詳細討論這個問題。