石峰 范曉琴
摘要:在使用C++語言開發(fā)實時軟件過程中,一般會使用靜態(tài)數(shù)組在多個線程中共享數(shù)據(jù)。ELF文件是目前流行的Linux/Unix系統(tǒng)可執(zhí)行文件的存儲格式。使用C++語言開發(fā)的軟件經(jīng)過編譯、鏈接以后會形成符合ELF文件格式的可執(zhí)行文件存儲在系統(tǒng)中。因此,研究ELF文件的結(jié)構(gòu)可以清晰地呈現(xiàn)靜態(tài)數(shù)組的構(gòu)建原理。該文利用反匯編等手段分析幾段C++代碼,從編譯和鏈接的角度研究了靜態(tài)數(shù)組定義及運用過程,得到了關(guān)于靜態(tài)數(shù)組構(gòu)建的三條結(jié)論,并指出了在使用靜態(tài)數(shù)組開發(fā)軟件時需要注意的問題。
關(guān)鍵詞:ELF文件;靜態(tài)數(shù)組
中圖分類號:TP312 文獻標識碼:A 文章編號:1009-3044(2015)15-0198-04
在使用C++語言開發(fā)實時軟件過程中,經(jīng)常需要在多個線程中共享數(shù)據(jù)。我們一般會定義一個靜態(tài)類并將需要共享的數(shù)據(jù)定義為類的靜態(tài)成員。由于靜態(tài)類在程序運行時只有一個副本,非常類似C語言的全局變量,但與全局變量比較起來,使用類的靜態(tài)成員具有信息隱蔽、避免命名沖突兩個優(yōu)勢,非常適合多人協(xié)作式的開發(fā)模式。數(shù)組是一個單一類型對象的集合。使用靜態(tài)數(shù)組成員可以將一組性質(zhì)相同的數(shù)據(jù)傳遞到程序中。ELF文件是目前流行的Linux、Unix系統(tǒng)可執(zhí)行文件的存儲格式。它有四種類型:可重定位文件、可執(zhí)行文件、共享目標文件、核心轉(zhuǎn)儲文件。使用C++語言開發(fā)的軟件經(jīng)過編譯、鏈接以后會形成符合ELF文件格式的可執(zhí)行文件存儲在系統(tǒng)中。因此,研究ELF文件的結(jié)構(gòu)可以清晰地呈現(xiàn)靜態(tài)數(shù)組的構(gòu)建原理。
本文利用反匯編等手段分析幾段C++代碼,從編譯和鏈接的角度研究了靜態(tài)數(shù)組定義及運用過程,得到了關(guān)于靜態(tài)數(shù)組構(gòu)建的三條結(jié)論,并指出了在使用靜態(tài)數(shù)組開發(fā)軟件時需要注意的問題。
1 ELF文件的總體結(jié)構(gòu)
表1描述的是ELF目標文件的總體結(jié)構(gòu),其中省去了一些繁瑣的結(jié)構(gòu),把重要的結(jié)構(gòu)提取出來,形成了ELF文件的基本結(jié)構(gòu)圖。從表中可以看到,ELF文件的開頭是個“頭文件”,它描述了整個文件的屬性,包括文件是否可執(zhí)行、是靜態(tài)鏈接還是動態(tài)鏈接及入口地址(如果是可執(zhí)行文件)、目標硬件、目標操作系統(tǒng)等信息,文件頭還包括一個段表(Section Table),段表其實是一個描述文件中各段的數(shù)組。段表描述了文件中各段在文件中的偏移位置及段的屬性等,從段表里可以得到每個段的所有信息。文件頭后面是各個段的內(nèi)容,比如代碼段(.text)保存的就是程序的指令,數(shù)據(jù)段(.data)保存的就是程序的靜態(tài)變量。
2 分析ELF文件的常用工具
本文所分析的ELF文件都是基于64位Intel x86平臺下Linux系統(tǒng)的ELF文件。因此會用到Linux系統(tǒng)中一些常用的命令及工具。第一個是gcc。Linux系統(tǒng)下功能強大、性能優(yōu)越的編譯器。gcc編譯器能將C\C++語言源程序經(jīng)過預處理、編譯、匯編、鏈接四步,最終形成可執(zhí)行文件。也可根據(jù)所選參數(shù)的不同,完成其中一個或幾個階段的工作。如有一個名為hello.cpp的c++源程序。運行$gcc hello.cpp –o hello 就可以生成名為hello的可執(zhí)行文件。運行$gcc –c hello.cpp –o hello.o 就可以生成hello.o的目標文件。第二個是readelf。readelf命令可以用來顯示ELF文件的信息。該命令有許多選項,可以顯示ELF文件不同類型的信息。比如-a 選項可以顯示ELF文件的全部信息。-S 可以顯示ELF文件的段表;-s選項可以顯示ELF文件的符號表;-r選項可以顯示ELF文件的重定位表。第三個是objdump。是用來查看目標文件或可執(zhí)行文件的文件構(gòu)成的GCC工具。本文主要使用它來反匯編目標文件。第四個是gdb。GNU開源組織發(fā)布的一個強大的UNIX/Linux下的程序調(diào)試工具。
3 靜態(tài)數(shù)組的定義與聲明
在類cMyGlobal中定義一個整型靜態(tài)數(shù)組global_array[5][20] 。如圖1所示,頭文件newglobal.h的第5行給出了數(shù)組的聲明。靜態(tài)變量和全局變量一樣,在程序中只能有一次定義,這就意味著靜態(tài)數(shù)據(jù)成員的定義不能放在頭文件里。而應該放在含有類的非inline 函數(shù)定義的文件中。所以,在源文件newglobal.cpp的第2行至第6行給出了數(shù)組的定義部分,并對數(shù)組元素進行了初始化。接下來,運行g(shù)cc命令,生成目標文件newglobal.o。
ELF文件中的符號表往往是文件中的一個段,段名一般叫做“.symtab”。這個表里面記錄了目標文件中所用到的所有符號。表中Num表示符號表數(shù)組的下標,從0開始,共8個符號。value是符號值即符號對應的虛擬地址。size是符號的大小即分配的存儲空間。Type是符號的類型。Bind是符號的綁定情況,表示符號的局部或全局屬性。Vis在目前的C++語言中沒有使用。Ndx表示符號所在的段。Name就是符號的名字。靜態(tài)數(shù)組global_array的綁定屬性是GLOBAL,說明是全局可見的;它的Ndx屬性為2,說明被定義在數(shù)據(jù)段;它的Size屬性值為400,說明被分配了400字節(jié)的存儲空間。由于c++編譯器在將源文件編譯成目標文件時,會將函數(shù)和變量的名字進行修飾,形成符號名。因此,數(shù)組global_array在目標文件中的符號名為_ZN9cMyGlobal12global_arr。值得注意的是類名cMyGlobal并沒有作為一個單獨的符號名出現(xiàn),而是出現(xiàn)在數(shù)組global_array的符號名中,用來表示數(shù)組global_array是類cMyGlobal的數(shù)據(jù)成員。
4 靜態(tài)數(shù)組的使用
對比newglobal.o(圖2)與mainpro.o(圖4)的符號表,可以看出,mainpro.o的符號表中_ZN9cMyGlobal12global_arr的Size屬性為0,Type屬性為NOTYPE,Ndx屬性為UND,說明在mainpro.o中,符號_ZN9cMyGlobal12global_arr是一個外部定義的符號,也就是說關(guān)于它的定義在別的目標文件中(newglobal.o)。
mov %eax -0x4(%rbp) 這5段代碼對應著源文件中靜態(tài)數(shù)組cMyGlobal::global_array對變量test_var的五次賦值。寄存器%rip為指令計數(shù)器,代表下一條要執(zhí)行的指令的地址。0(%rip)表示的地址就是數(shù)組元素所在的地址。C++代碼在目標文件階段各符號的地址是不確定的,經(jīng)過鏈接之后才會被賦予確定的地址,因此用0表示,并不代表不同的數(shù)組元素擁有相同的地址。運行命令readelf可已看到目標文件的重定位表(圖6)。
最后一列是符號名加偏移量。靜態(tài)數(shù)組global_array是一個整型數(shù)組,每個元素占4個字節(jié)。以元素cMyGlobal::global_array[1][0]為例,該元素為整型數(shù)組的第20個元素。所以元素cMyGlobal::global_array[1][0]的首地址相對于數(shù)組首地址的偏移量為0x50。該元素位于代碼段首地址偏移0x1f的位置,正好是第三個0(%rip)在代碼段的位置。綜合以上分析,靜態(tài)數(shù)組cMyGlobal::global_array的五個元素在目標文件的代碼段映射為五個待重定位的地址。下一步,運行命令g++ 生成最終的可執(zhí)行文件。
5 一種編碼錯誤的分析
為了說明本節(jié)內(nèi)容,對newglobal.h的代碼進行修改。將圖1中newglobal.h的第1行#define NUM 20改為#define NUM 10并存儲為文件oldglobal.h。這么做的目的是將靜態(tài)數(shù)組global_array聲明為5*10的二維數(shù)組。將圖3中mainpro.cpp的第2行#include "newglobal.h"改為#include "oldglobal.h"。運行命令 g++生成可執(zhí)行文件oldpro。
可以看到變量test_var分別被賦值為1、2、80、81、82。結(jié)合對靜態(tài)數(shù)組global_array定義部分的分析,第二次輸出了數(shù)組[0][1]、[0][2]、[0][10]、[0][11]、[0][12]的內(nèi)容,并不是期望的[0][1]、[0][2]、[1][0]、[1][1]、[1][2]。出現(xiàn)這樣的問題很明顯是因為引用了不同的頭文件。再看圖4中mainpro.o的符號表。程序在編譯階段,編譯系統(tǒng)把符號_ZN9cMyGlobal12global_arr理解為外部符號,既_ZN9cMyGlobal12global_arr的定義在別的目標文件里,在鏈接階段才會對該符號進行決議。而_ZN9cMyGlobal12global_arr真正定義在newglobal.o中。編譯程序在編譯源程序mainpro.cpp時,只是知道_ZN9cMyGlobal12global_arr是個外部變量,并不會對符號的類型進行檢查。在編譯代碼的過程中,數(shù)組的元素被直接根據(jù)聲明的維數(shù)轉(zhuǎn)化為待重定位的地址。在鏈接階段地址是符號_ZN9cMyGlobal12global_arr確定的,和聲明時數(shù)組的大小維數(shù)沒有任何關(guān)系,甚至和數(shù)據(jù)類型也沒有任何關(guān)系。于是就出現(xiàn)了雖然數(shù)組的聲明改變了,但由于數(shù)組的定義保持不變,程序輸出了錯誤數(shù)據(jù)的情況。
6 結(jié)論
1)編譯程序?qū)υ次募幾g后形成目標文件。在目標文件的代碼段中,靜態(tài)數(shù)組的元素根據(jù)聲明的維數(shù)轉(zhuǎn)化為待重定位的虛擬地址,沒有下標的概念。
2)編譯程序在編譯源文件時,對源文件引用的外部變量不會進行類型檢查。
3)在程序的鏈接階段,編譯程序根據(jù)目標文件符號表中的符號來確定代碼段靜態(tài)數(shù)組需要重定位的首地址。與聲明的數(shù)組大小沒有關(guān)系。
通過本文分析,得到了靜態(tài)數(shù)組的三條結(jié)論。在軟件開發(fā)過程中,引用靜態(tài)數(shù)組一定要確定引用了正確的頭文件。另一方面,更改了靜態(tài)數(shù)組的定義,一定要對使用它的程序重新編譯,以免因為目標文件版本不一致導致程序執(zhí)行出錯。
參考文獻:
[1] Lippman S B.C++ Primer[M]. 潘愛民,張麗, 譯. 3rd ed. 北京: 中國電力出版社, 1998.
[2] 俞甲子, 石凡, 潘愛民. 程序員的自我修養(yǎng)[M]. 北京: 電子工業(yè)出版社, 2012.