1. 程式人生 > >c++符號表解析

c++符號表解析

約在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)

[email protected]@[email protected]

float func(float)

[email protected]@[email protected]

int C::func(int)

[email protected]@@[email protected]

int C::C2::func(int)

[email protected]@[email protected]@[email protected]

int N::func(int)

[email protected]@@[email protected]

int N::C::func(int)

[email protected]@[email protected]@[email protected]

我們以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;
    }

由於不同的編譯器採用不同的名字修飾方法,必然會導致由不同編譯器編譯產生的目標檔案無法正常相互連結,這是導致不同編譯器之間不能互操作的主要原因之一。

其他內容整理如下(原作者未知):

nm用於列出目標檔案的符號清單,如果沒有指定目標檔案,則預設為“a.out”。nm的格式如下:

nm [‘-a’|‘–debug-syms’] [‘-g’|‘–extern-only’]
[‘-B’] [‘-C’|‘–demangle’[=style]] [‘-D’|‘–dynamic’]
[‘-S’|‘–print-size’] [‘-s’|‘–print-armap’]
[‘-A’|‘-o’|‘–print-file-name’][‘–special-syms’]
[‘-n’|‘-v’|‘–numeric-sort’] [‘-p’|‘–no-sort’]
[‘-r’|‘–reverse-sort’] [‘–size-sort’] [‘-u’|‘–undefined-only’]
[‘-t’ radix|‘–radix=’radix] [‘-P’|‘–portability’]
[‘–target=’bfdname] [‘-f’format|‘–format=’format]
[‘–defined-only’] [‘-l’|‘–line-numbers’] [‘–no-demangle’]
[‘-V’|‘–version’] [‘-X 32_64’] [‘–help’] [objfile…]

對於每一個符號,nm列出其值(the symbol value),型別(the symbol type)和其名字(the symbol name)。
如下例:

 00000024 T cleanup_before_linux
 00000018 T cpu_init
 00000060 T dcache_disable
 00000054 T dcache_enable
 0000006c T dcache_status
 00000000 T do_reset
 0000003c T icache_disable
 00000030 T icache_enable
 00000048 T icache_status

上面的顯示是使用nm cpu.o的輸出,對於cleanup_before_linux這個符號來說,00000024是以16進位制顯示的其值,T為其型別,而cleanup_before_linux是其名字。可以看出,上面顯示的cleanup_before_linux這個symbol的值實際上是該函式在text section中的偏移。但是,每個符號的值的具體含義依其型別而異。當然,對於每個符號的值,其型別、其值以及它們所屬的section是密切相關的。
下面說明符號型別:對於每一個符號來說,其型別如果是小寫的,則表明該符號是local的;大寫則表明該符號是global(external)的。

符號
型別 說明
A 該符號的值是絕對的,在以後的連結過程中,不允許進行改變。這樣的符號值,常常出現在中斷向量表中,例如用符號來表示各個中斷向量函式在中斷向量表中的位置。
B 該符號的值出現在非初始化資料段(bss)中。例如,在一個檔案中定義全域性static int test。則該符號test的型別為b,位於bss section中。其值表示該符號在bss段中的偏移。一般而言,bss段分配於RAM中
C 該符號為common。common symbol是未初始話資料段。該符號沒有包含於一個普通section中。只有在連結過程中才進行分配。符號的值表示該符號需要的位元組數。例如在一個c檔案中,定義int test,並且該符號在別的地方會被引用,則該符號型別即為C。否則其型別為B。
D 該符號位於初始話資料段中。一般來說,分配到data section中。例如定義全域性int baud_table[5] = {9600, 19200, 38400, 57600, 115200},則會分配於初始化資料段中。
G 該符號也位於初始化資料段中。主要用於small object提高訪問small data object的一種方式。
I 該符號是對另一個符號的間接引用。
N 該符號是一個debugging符號。
R 該符號位於只讀資料區。例如定義全域性const int test[] = {123, 123};則test就是一個只讀資料區的符號。注意在cygwin下如果使用gcc直接編譯成MZ格式時,原始檔中的test對應_test,並且其符號型別為D,即初始化資料段中。但是如果使用m6812-elf-gcc這樣的交叉編譯工具,原始檔中的test對應目標檔案的test,即沒有新增下劃線,並且其符號型別為R。一般而言,位於rodata section。值得注意的是,如果在一個函式中定義const char *test = “abc”, const char test_int = 3。使用nm都不會得到符號資訊,但是字串“abc”分配於只讀儲存器中,test在rodata section中,大小為4。
S 符號位於非初始化資料區,用於small object。
T 該符號位於程式碼區text section。
U 該符號在當前檔案中是未定義的,即該符號的定義在別的檔案中。例如,當前檔案呼叫另一個檔案中定義的函式,在這個被呼叫的函式在當前就是未定義的;但是在定義它的檔案中型別是T。但是對於全域性變數來說,在定義它的檔案中,其符號型別為C,在使用它的檔案中,其型別為U。
V 該符號是一個weak object。
W The symbol is a weak symbol that has not been specifically tagged as a weak object symbol.

  • 該符號是a.out格式檔案中的stabs symbol。
    ? 該符號型別沒有定義