進程影像(一)——從程式碼到進程
一、源程式、目標程式碼
1、 先使用Dev-C++編寫C程式碼:main.c和test.c
/*main.c*/
int add(int,int);
int mian()
{
return add(20,13);
}
/*test.c*/
int add(int i,iny j)
{
int x=i+j;
return x;
}
2、 將上述倆文件放到虛擬機的共享文件夾下(與虛擬機共享),或者通過郵箱發送在虛擬機中通過郵箱下載到與gcc同目錄下。
3、 在gcc目錄下打開終端,輸入 #gcc –c main.c將產生main.c的目標文件main.o
4、 輸入ls-l查看main.o文件大小
5、 用file命令來識別一個文件的類型
從上面的file命令執行結果看,main.o是一個編譯成x86-64架構的指令、可重定位(relocatable)的目標文件。該文件按照ELF(Executable and linkable Format)的布局來存放在磁碟文件中,而且還保留著符號表。
二、目標程式碼
1、用objdump –d命令完成程式碼段的反彙編
圖2.1
總共只有0x19個位元組(0~0x18)的指令序列,最左邊是位元組偏移量,中間是二進位程式碼,右邊是彙編程式碼。
2、用objdump –hrt main.o來了解更多關於這個目標文件的資訊。
圖2.2
因為使用-h (–headers/–section-headers)參數,因此將列印出該ELF格式的目標文件中各個節(section)的頭部摘要資訊(節是程式碼組織的一個單位,即某段相同屬性的程式碼或數據)。另外,使用了-r (–reloc)參數來顯示重定位入口。第三個參數-t(–syms)顯示符號表項(即SYMBOL TABLE部分)。
上面的結果表明mian.o中包含7個節,各個節的資訊按列劃分為:Idx是編號,Name是節的名字、Size給出節的大小、VMA給出節在虛存空間的起點、LMA (LoadMemory Address)是裝載地址(除基於ROM的系統外,通常與VMA相同)、File off是該節在文件中的偏移量、Algn是地址對齊方式(2的整數次冪)。各個節的第二行有CONTENTS、ALLOC、LOAD、RELOC、READONLY、CODE、DATA等屬性說明。CODE表示本節包含可執行程式碼;DATA表示本節是可寫數據;READONLY表示本節是只讀區域;ALLOC表示本節將要佔用記憶體空間;CONTENTS表示本節確實在文件中佔用了空間。
其中,編號為0的.text節是main.c編譯後的x86-64平台上的可執行二進位程式碼,共有0x19個位元組。經過鏈接後在運行時由程式裝載器把它構造成進程的程式碼段。編號為1的.data是用於保存已初始化的全局變數和局部靜態變數。但這裡的main.c中沒有帶初始值的全局變數也沒有靜態局部變數,所以作為目標程式碼(甚至可執行文件)並不需要佔用空間來保存這些資訊,因此該section長度為0,否則它的長度是所有具有初始值數據所佔空間大小。編號為2的.bss (Block Started by Symbol),保存未初始化的全局變數和局部靜態變數。未初始化的全局變數和局部靜態變數默認值都為0,本來它們也可以放在.data段,但因為它們都是0,所以為它們在.data段分配空間並且存放數據0是沒有必要的。程式運行的時候這些變數的確是要佔記憶體空間,所以目標文件必須記錄所有未初始化的全局變數和局部靜態變數的大小總和,記為.bss段。也就是說,.bss 段只是為未初始化的全局變數和局部靜態變數預留位置而已,它並沒有內容(該節標誌中沒有CONTENTS一項),在磁碟文件中也不佔據空間。編號為3的.comment 包含0x2b個位元組的注釋。編號為4、5的.note是額外的編譯器資訊—例如程式的公司名、發布版本號等。由於是目標文件,所有的節都還未經過鏈接定位,也就是說還未布局,所以各個節的起始虛存地址VMA和物理地址LMA (Load Memory Address)都為無效的0(進行鏈接後將按照可執行文件的布局來確定地址)。
SYMBOL TABLE包含所有的符號資訊。各列分別是節內偏移、標記位、所在的節、對齊方式(或長度)和符號名。標記位共7位,第一位作用域可以是以下取值:局部的『I』、全局的』g’、全局唯一的』u’、不是全局也不是局部』_'(空格)以及既是全局又是局部的”!’。第二位可以為GCC 中的弱符號』w’或強符號』_』。第三位可以是 constructor 符號』C』或普通符號』_’。第四位可以是警告符號』w’或普通符號’_’。第五位可以是間接引用符號』i’、需重定位的函數』T』或普通符號』_’。第六位可以是調試符號』d』、動態符號』D’或普通符號』_’。第七位可以是函數名』F』、文件名’f』、對象’0』或普通符號’_’。第三列給出了符號所在的節,如果是*ABS*表明這個符號是絕對符號(與各節無關),如果是*UND*則這個符號不在本目標文件中定義。本例子中大多數符號為調試符號。對於函數名和對象(變數)的符號,倒數第二列就不是對齊方式而是所佔空間的長度。
本例中目標文件中含有未確定(Unresolved,也有稱為未分析、未決斷)的地址。RELOCATION RECORDS FOR [.text]以及RELOCATION RECORDS FOR [.eh_frame]是text和eh frame兩個節的重定位表。例如,圖2.2中RELOCATION RECORDS FOR [.text]里的兩個地址 0000 0000 0000 0013對應地在圖2.1偏移為0x13的位置都為「00 00 00 00」,本來應該是字元串add函數的地址,它們需要在鏈接後重新定位——所以它們在重定位表中指出(等到生成可執行文件的時候這兩個地址就不為0)。
根據上面的資訊,可以大致繪製出這個目標文件的磁碟布局情況(使用readelf -S 命令可以全面查看節的更多資訊),如圖2.3所示。
圖2.3
3、用objdump –s main.o查看.text程式碼以及.comment等部分的二進位編碼內容
三、可執行文件與進程影像
1、 生成可執行文件
鏈接後的可執行文件main 使用ELF格式。ELF格式的文件可以用於多種用途——可執行文件(Executable File)、可重定位文件(Relocatable File)、共享目標文件(Shared Object File)和核心轉儲文件(Core Dump File)。用file 命令可以證實main確實是ELF格式的可執行文件(Executable)。
2、 用readlelf -S命令查看main的「節」的情況(也可用objdump –h命令)
我們發現這裡的.text節和目標里的.text大小部分發生了改變,.text的大小0x195比目標文件里的0x19位元組大太多了(因加入入口初始化程式碼)。應注意節是編譯器進行編號的,並不是按照各個節在文件或在虛存空間中的位置排序的。
3、 用objdump –t main查看符號表
圖3.1
4、 用readelf –l main 查看ELF可執行文件的程式頭(Program file)里描述的與裝入過程有關的資訊,告知作業系統如何將它們映射到虛擬空間中。
圖3.2
從圖3.1看出main函數地址為0x0000000000001129,與圖3.2的Entry point 0x1040地址不一致,這是因為入口地址指向的是編譯器提供的一段初始化程式碼(叫做_tart函數,符號表給出的地址是0x0000000000001040)。
從圖3.2的輸出可以看出有13個段(Segment),從裝載的角度上看,只需要關心「LOAD」類型的段,其他諸如「NOTE」或「GNU_STACK」等只是在裝載過程中起輔助作用。這兩個需要裝載的段對應編號為02、03、04和05,於是可以在「Section to Segmentmapping」裡面找出這兩個段包含哪些節。因此可以將編號為02的段所對應的節:
裝入到虛存空間的某個區域中,而且此虛存空間區域的屬性需要設置為「R」——表示只讀。具體裝入操作需要將文件中偏移0x0000000000000000 (Offset)開始的0x00000000000005c8 (FileSiz)個位元組裝入到虛存空間0x0000000000000000( VirtAddr)位置處,佔用0x00000000000005c8 (Memsize)的記憶體空間。編號為03、04、05段類似,在這裡就不具體說明了。
5、運行可執行文件
圖5.1
用跟蹤工具strace命令查看啟動過程中執行的系統調用。從圖5.1運行strace ./main命令之後的輸出可以看出,在shell命令行下執行main可執行文件的基本流程。Shell作為父進程調用了fork系統調用將自己複製一份後產生子進程,該子進程再執行execve系統調用,將使用main可執行文件重建進程記憶體的映像。在執行過程中涉及不少文件和記憶體相關的系統調用。最後exit_group(33)(33=20+13)退出程式。