基于PowerPC Linux的ELF格式分析
第一部分 ELF格式概述
ELF(Executable and Linkable Format)是一種對可執行檔案、目标檔案以及庫檔案使用的檔案格式,它在Linux下成為标準檔案已經有很長的一段時間,代替了早期的a.out格式。ELF格式的一個優點是同一個檔案格式可以用在Linux Kernel支援的所有體系結構之上。這不僅簡化了使用者空間工具程式的建立,也簡化了核心自身的程式設計,比如必須為可執行程式生成裝載例程時。但是檔案格式相同并不意味着不同系統上的程式之間存在二進制相容性,例如FreeBSD和Linux都使用ELF作為二進制格式,但是FreeBSD上的程式不能運作于Linux上,因為兩者在系統調用機制和系統調用語義方面仍然有所不同,反之亦然;如果想讓二者之間的程式能夠運作,必須要有一個中間仿真層。同樣PowerPC平台編譯的ELF程式,也不能在X86平台的機器上運作,反之依然,因為兩者的體系結構是完全不同的。但是由于ELF格式的存在,相同體系結構上的ELF程式本身的相關資訊,以及程式的各個部分在二進制檔案中的編碼方式都是相同的。Linux不僅将ELF用于使用者空間應用程式和庫,還用于工具子產品,另外Linux核心本身也是ELF格式。
備注:ELF檔案是一種開放的格式,其規範可以自由獲得。
ELF檔案有三種類型:
可重定位檔案:也就是通常稱的目标檔案,字尾為.o;
共享檔案:也就是通常稱的庫檔案,字尾為.so;
可執行檔案:本文主要讨論的檔案格式;
總的來說,可執行檔案的格式與上述兩種檔案的格式之間的差別主要在于觀察的角度不同:一種稱為連接配接視圖(Linking View),一種稱為執行視圖(Execution View)。
示意圖如下:
第二部分 布局和結構
ELF Header:除了用于辨別ELF檔案的幾個位元組之外,ELF檔案頭還包含了有關檔案類型和大小的資訊,以及檔案加載後程式執行的入口資訊等等,總之ELF頭部是一個關于本ELF檔案的路線圖(road map),從總體上描述檔案的結構。
程式表頭(Program Head Table):向系統提供了可執行檔案的資料在程序虛拟位址空間中組織方式的相關資訊,它還表示了檔案可能包含的段的數目,段的位置以及用途。
各個段SegmentX(X=1,2,…)和節SectionX(X=1,2,…):儲存了與檔案相關的各種形式的資料,例如符号表、實際的二進制代碼、固定值或者程式使用的數值常數。
節頭表(Section Header Table):包含了與各段相關的附加資訊。
下面我們一個例子,用readelf工具來分析ELF檔案:
例如如下:
//test.c
#include <stdio.h>
#include <stdlib.h>
int add(int a, int b)
{
printf("Numbers are added together\n");
return a + b;
}
int main()
int a, b;
a = 3;
b = 4;
int ret = add(a,b);
printf("Result : %u\n",ret);
exit(0);
我們用file指令來顯示編譯器生成的兩個檔案的資訊:一個是可執行檔案,另一個是可重定位檔案,示意圖如下:
2.1 ELF header檔案結構
我們用readelf工具來分析上例中生成ELF檔案的ELF header,視圖如下:
在test檔案的開始處是四個标志位元組,0x 7f、0x45、0x4c、0x46。其中的0x45、0x4c、0x46分别代碼“E”“L”“F”的ASCII碼,這使得所有處理ELF檔案的工具都可以識别ELF類型的檔案。還有一些與機器體系結構相關的資訊。比如上圖中Machine類型為PowerPC表明該ELF檔案運作于PowerPC平台;ELF32表明這是一個運作在32位平台的機器;檔案類型為EXEC表明這是一個可執行程式;Version用于區分目前ELF檔案的各個修訂版本,目前ELF檔案是基于版本1;另外還包含ELF檔案各個部分的長度和索引的位置,後面會相信讨論。
如果ELF檔案是可重定位檔案,不同的字段如下圖所示:
如圖所示:test.o的檔案類型為REL,即它是一個可重定位的檔案,其代碼可以移動到任意位置,該檔案沒有程式頭Program Headers,對于需要連結的對象而言該表是不需要的,是以其是以長度均為0。
2.2 程式頭表(Program Head Table)分析
下面我們來分析一下ELF可執行檔案test中Program Head Table,示意圖如下:
在Program Headers中列出了8個段,這些段組成了最終在記憶體中執行的程式,并且還提供了各個段在虛拟位址空間和實體位址空間中的位置、大小、通路授權和其它方面的資訊。從上圖中我們可以看出,示例程式中包含的各個段的語義如下:
PHDR:儲存Program Headers Table
INTERP:制定程式已經從可執行檔案映射到記憶體之後,必須調用的解釋器。在這裡,解釋器并不意味着二進制檔案的内容必須由另一個程式解釋(比如Java位元組代碼必須由Java虛拟機來解釋),它指的是這樣的程式:通過連結其它庫來滿足未解決的引用。
LOAD:表示一個需要從二進制檔案映射到虛拟位址空間的段,其中儲存常量資料(如字元串),程式的目标代碼等等。
DYNAMIC:儲存了由動态連結區(即INTERP中指定的解釋器)使用的資訊。
NOTE:儲存了專用資訊,與目前主題無關。
GNU_EH_FRAM和GNU_STACK:用來分析棧幀,與體系結構相關,在PowerPC體系結構中需要分析棧幀實作回溯。
備注:我們需要特别指出段是可以重疊的,比如在上圖中LOAD段從實體位址0x1000 0000到實體位址0x1000 0000+0x007f8的範圍,該範圍包含了PHDR、INTERP、NOTE、GNU_EH_FRAM和GNU_STACK段,在ELF标準中是允許這種行為的。
2.3 節頭标(Section Header Table)分析
在ELF檔案中描述各段的内容時,是指定了哪些節的資料映射到段中。在ELF中是有一張節頭表來管理檔案中的各個節,readelf同樣可以用于顯示可重定位檔案test.o中的各個節,示意圖如下:
這裡指定的偏移量0x168是相對于二進制檔案的。節資訊不需要複制到在虛拟位址空間中做為可執行檔案建立的最終程序映象,盡管如此在ELF二進制檔案中節資訊總是存在的。
每個節都指定了一個類型,定義了節資料的語義。包含PROGBITS、SYSTAB、RELA和STRTAB。
PROGBITS:程式必須解釋的資訊,比如二進制代碼,程式的二進制代碼被稱之為text,指的是用作機器代碼的二進制資訊
SYSTAB:符号表
RELA:重定位資訊
STRTAB:用于存儲與ELF相關的字元串,但與程式沒有直接的關系。
各個節Section都指定了其大小和在二進制檔案内部的偏移量。
位址Addr:指定加載到虛拟位址空間的位置,因為我們的例子中處理的是一個可連結的對象,目标位址是沒有定義的,因而表示為0。
标志Flg:指出各個節Section如何被通路或者處理。我們對标志A比較感興趣,因為它控制着裝載檔案時是否将節的資料複制到虛拟位址空間中。
盡管節的名稱是可以自由選擇的,但是Linux(和其它所有使用ELF的類UNIX系統)都提供了一些标準節,其中一些是強制性的,例如總是有一個名為.text的節來儲存二進制代碼(即與該ELF檔案相關聯的程式資訊),.rel.text儲存了節的重定位資訊。
備注:節名以點開始是由系統自身使用的,如果應用程式要想定義自身的節,就不應該以點開頭,以避免與系統節名相沖突。
可執行程式包含了一些重定位的資訊,示意圖如下:
與可重定位檔案的11個節相比,可執行檔案有36個節,并非所有的節都與我們的讨論有關。上圖中标出的節是有具體意義的:
.interp:儲存了解釋器的檔案名,例如在我們的例子中用的是是ld.so.1;
.data:儲存了初始化的資料,這是普通程式資料的一部分,可以在程式運作時修改;
.rodata:儲存了隻讀資料,可以讀但不可以修改,例如編譯器将所有出現在printf中的靜态字元串封裝到該節;
.init和.fini:儲存了程序初始化和結束時所用的代碼,這兩個節通常是編譯器自動添加的,無需應用程式員關注;
.hash:是一個散清單,允許在不對全表元素進行線性搜尋的情況下,快速通路所有的符号表項。
備注:可執行檔案ELF各個Section中的Addr字段儲存了有效位址的值,因為相應的代碼必須映射到虛拟位址空間中某些已經定義好的位置,在基于PowerPC的Linux中應用程式通常使用0x1000 0000以上的記憶體區間。
2.4 符号表(Symbol Table)(符号表隻有一個)
符号表是每個ELF檔案的一個重要的組成部分,因為它儲存了程式實作或者使用過程中所有的(全局)變量和函數,如果程式使用了一個自身代碼沒有定義的符号,則稱之為未定義符号(例如例子中的printf函數是定義在C标準庫中,自身沒有定義),此類應用必須在靜态連結期間用其它的目标子產品或者庫來解決,或者在加載期間使用lib/ ld.so.1來解決。我們可以使用nm工具生成程式定義或者使用的所有符号清單,如果下圖所示:
左側一列給出了符号的值,即符号定義在目标檔案中的位置,例子中包含了兩個不同的符号類型:如果函數定義在text段,縮寫為T;而未定義的引用用U标明。邏輯上沒有定義的函數沒有符号值。
可在可執行ELF檔案test中,還會有更多的符号。當時大多數都是由編譯器自動生成的,供運作時系統内部使用,以下例子中我們僅标出了同時出現在test.o中的符号:
U exit@@GLIBC_2.0
1000058c T add
100005dc T main
U puts@@GLIBC_2.0
解釋:exit和puts雖然是沒有定義的,但是同時增加了一些版本資訊,标明能夠提供函數的GNU标準庫的最低版本,在我們的例子中要求庫的最低版本不能低于2.0,這意味着該程式無法使用Libc5和Lib4工作,因為Libc4和Libc5是Linux專用的C标準庫,Glibc_2.0是該庫的第一個跨平台版本,它替換了就版本,它替換了舊版本。
由函數本身定義的add和main已經移到虛拟位址空間中的固定位置(在檔案加載時,對應的代碼将會映射到這些位置)
ELF使用以下的三個節(Section);來實作字元串的管理:
.systalb:确定字元串的名稱與其值的索引,但符号的名稱不是以字元串的形式出現的,而是表示為某個字元串數組的索引;
.strtab:儲存了字元串數組;
.hash:儲存了一個hash表用于快速查找符号。
簡而言之,符号名在字元串表(string table )中的位置和符号的值,為了說明字元串表示如何管理ELF中的字元串,我們看下面的例子:(字元串表)
表的第一個位元組是NULL(即ASCII碼值為0),後續的各個字元串通過NULL位元組分割,為了引用字元串必須指定一個位置,即字元串的索引。這将選擇下一個NULL位元組之前的所有字元(如果用NULL位元組的位置作為索引,那麼将會對應空串)。如果允許索引不僅僅選擇字元串的起始位址,也可以選擇字元串中間的任何位置,就能夠支援字串的用法(但是非常受限)
上述的.strtab節并不是預設情況下的唯一的字元串表,.shstrtab用于存放檔案中各個節的文本名稱(比如.text)
第三部分 Linux 核心中的資料結構
核心在兩處使用了ELF檔案格式:ELF用于處理可執行檔案和庫,用于實作子產品。
這些地方使用了不同的代碼來讀取和操作資料,但這兩種情況下都利用了我們本文所介紹的資料結構,其基礎是include/elf.h檔案,它實作了ELF标準,基本沒有做改動!
3.1 資料類型
ELF是一個與CPU和體系結構無關的格式,它不能依賴與特定的字長和位元組序(大端還是小端),至少對檔案中的那些需要在所有系統上讀取和了解的資料元素來說是這樣。出現在.text段中的機器代碼存儲為宿主系統的表示格式,以避免轉換工作。為此Linux核心定義了一些資料類型,在所有體系結構上具有相同的位寬,如下所示:
//32-bit ELF 基本類型
typedef __u32 Elf32_Addr;
typedef __u16 Elf32_Half;
typedef __u32 Elf32_Off;
typedef __s32 Elf32_Sword;
typedef __u32 Elf32_Word;
// 64-bit ELF 基本類型
typedef __u64 Elf64_Addr;
typedef __u16 Elf64_Half;
typedef __s16 Elf64_SHalf;
typedef __u64 Elf64_Off;
typedef __s32 Elf64_Sword;
typedef __u32 Elf64_Word;
typedef __u64 Elf64_Xword;
typedef __s64 Elf64_Sxword;
因為體系結構相關的代碼必須總是明确定義整數類型的符号和位寬,ELF标準的資料類型可以毫不費力的通過typedef實作
3.2 ELF頭部格式實作(Elf32_Ehdr 資料結構隻有一個)
對ELF格式的各種頭部檔案,32位和64位需要分别定義其資料結構:
32位的ELF頭
在include/elf.h定義如下:
#define EI_NIDENT 16
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
解釋:
e_ident:可以容納16個位元組,這些位元組在所有的體系結構上都是由char資料類型表示的,前4個位元組分别為0x7f(即ASCII碼中的DEL)和字母E、L、F,我們在2.1節曾介紹過。其它的位元組位置有其特定的含義:
e_ident[4]:EI_CLASS辨別檔案的類型,将檔案分為32位和64位兩類,即ELFCLASS32和ELFCLASS64
e_ident[5]:EI_DATA指定了格式所使用的位元組序,即ELFDATA2LSB(小端)和ELFDATA2MSB()大端
ei_ident[6]:EI_VERSION指定ELF頭檔案的版本(該版本可能獨立于資料段的版本),目前,值允許使用EV_CURRENT,這是第一個版本
ei_ident[7]: EI_OSABI指定采用的OS的ABI版本,目前是UNIX System V ABI
從ei_ident[8]EI_PAD起的剩餘位元組:用NULL來填充,因為這些位置上ELF不需要。
e_type用去區分各種ELF檔案的類型,如下所示:
e_machine指定了檔案所需的體系結構,下表列出了Linux支援的各種選項:
注意:每種體系結構都需要定義elf_check_arch,并由核心的通用代碼使用,來確定加載的ELF檔案可以在相應的體系結構上運作。
e_version:儲存了版本資訊,用于區分不同的ELF變體,目前該規範僅支援版本1,由EV_CURRENT指出來
e_entry:給出了檔案在虛拟記憶體中的入口點,在程式已經加載并映射到記憶體之後,執行開始的位置
e_phoff:儲存了程式頭表(Program Header Table)在二進制ELF檔案中的偏移
e_shoff:儲存了節表頭(Section Header Table) 在二進制ELF檔案中的偏移
e_flags:儲存了特定于處理器的标志,目前Linux核心不适用該标志
e_ehsize:指定了ELF Header的長度,機關是位元組
e_phentsize:指定了Program Header中一個表項的長度,機關是位元組(所有表項的長度均相同)
e_phnum:指定了Program Header Table中表項的數目
e_shentsize:指定了Section Header Table中一個表項的長度,機關是位元組(所有表項的長度均相同)
e_shnum:指定了節頭表中項的數目
e_shstrndx:儲存了包含各節(Section)名稱的字元串表在Section Header中的索引位置
備注:64位下ELF頭的資料結構可以同樣定義,唯一的差别在于其中使用了對應的64位資料類型,這使得頭檔案稍大。但兩者資料結構的前16位元組都是相同的,兩種體系結構的類型能夠根據這些位元組來識别不同字長機器的ELF檔案
3.3 Program Header實作(Elf32_Phdr資料結構有多個,在ELF Header中儲存)
程式頭表由幾個項實作,其處理方式類似于數組項(項的資料由ELF頭中的e_phnum指定),表項的資料類型定義為獨立的結構,在32的機器上其内容如下:
typedef struct elf32_phdr{
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
p_type:表示目前項描述的段的種類,為其定義了下列常數:
PT_NULL:表示沒有使用的段
PT_LOAD:表示可裝載段,在程式執行前從二進制檔案映射到記憶體
PT_DYNAMIC:表示段包含了用于動态連結器的資訊
PT_INTERP:表示目前段指定了用于動态連結的解釋程式,比如ld.so.1
PT_NOTE:指定一個段,其中可能含有專有的編譯器資訊
還有兩個常數PT_LOPROC和PT_HIGHPROC用于定義與處理器相關的用途,核心并不使用
p_offset:指出所描述段從ELF檔案起始處的偏移量,以位元組為機關
p_vaddr:給出段的資料映射到虛拟位址空間中的位置(對應PT_LOAD類型的段),隻支援實體尋找,不支援虛拟尋址的系統将使用p_paddr儲存的資訊
p_filesz:指定了段在ELF檔案中的長度
p_memsz:指定了段在虛拟位址空間中的長度,與檔案中實體段的內插補點可以通過階段資料或者填充0位元組來填充
p_flags:儲存了标志資訊,定義了段的通路權限:PF_R讀權限,PF_W寫權限,PF_X執行權限
p_align:指定了段在記憶體和二進制檔案中的對齊方式(即p_vaddr和p_offset位址必須是模p_align,即為p_align的倍數)
比如p_align的值為0x1000=4KB,這意味着段必須4KB對齊。
對64為體系結構定義的類似的資料結構,與32位相比,唯一的差别在于所使用的資料結構,各資料項的語義都是相同的。其具體内容如下:
typedef struct elf64_phdr {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset; //Segment file offset
Elf64_Addr p_vaddr; //Segment virtual address
Elf64_Addr p_paddr; //Segment physical address
Elf64_Xword p_filesz; //Segment size in file
Elf64_Xword p_memsz; //Segment size in memory
Elf64_Xword p_align; //Segment alignment, file & memory
} Elf64_Phdr;
3.4 節頭表Section Header Table代碼實作(Elf32_Shdr資料結構與節的個數相同)
節頭表通過數組實作,每個數組項包含一節的資訊,各個節構成了程式頭表Program Header Table定義的各個段的内容。下來資料結構表示一個節:有多少個節section就存在多少個這樣的結構體資料。
typedef struct elf32_shdr {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;-》Section Header
sh_name:指定了節的名稱,其值不是字元串本身,而是字元串表的一個索引。
sh_type:指定了節的類型,有下列的類型可用:
SH_NULL表示該節不使用,其資料将會被忽略
SH_PROGBITS儲存程式的相關資訊,其格式是不定義的,與這裡的讨論無關
SH_SYMTAB儲存一個符合表,其結構我們在2.4節已經讨論,SH_DYNSYM也儲存一個符号表,二者的差别我們在稍後讨論
SH_STRTAB表示一個包含字元串表的節
SH_RELA和SHT_RELA儲存重定位資訊,我們也将在後面讨論
SH_HASH定義了一個節儲存HASH表,用于快速的查找符号表中的項
SH_DYNAMIC儲存了動态連結表的資訊,我們也将在後面讨論
還有類型值SHT_HIPROC、SHT_LOPROC、SHT_HIUSER、SHT_LOUSER,這些專項特定于CPU和應用程式的用途,與這裡讨論的内容無關
sh_flags:表示節是否可重寫(SHF_WRITE),是否将其配置設定虛拟記憶體(SHR_ALLOC),是否包含可執行的機器代碼(SHF_EXECINSTR)
sh_addr:指定節映射到虛拟位址空間中的位置
sh_offset:指定了節在記憶體中的位置
sh_size:指定了節的長度
sh_link:引用另一個節頭表項,可以根據節類型進行不同的解釋,其性能我們在後面單獨讨論
sh_info和sh_link聯用,其确切語義我們也會在下文中再讨論。
sh_addralign:指定了節資料在記憶體中的對齊方式
sh_entsize:指定了節中各個資料項的長度,前提是這些資料項的長度均相同。例如字元串表根據節類型不同,sh_link和sh_info的用法也不盡相同,具體情況如下:
第一:SHT_DYNAMIC類型的節使用sh_link指向節資料指向的字元串表,這種情況下不使用sh_info,sh_info設定為0;
第二:散清單(SHT_HASH類型的節)使用sh_link指向所散列的符合表,sh_info不使用
第三:類型為SHT_RELA和SHT_REL的重定位節,使用sh_link指向相關的符号表,sh_info中儲存的是節頭表中的索引,表示對哪個節進行重定向;
第四:sh_link指定了用作符号表的字元串表(SHT_SYMTAB和SHT_DYNSYM),而sh_info表示符号表中緊随最後一個局部符号之後的索引位置(STB_LOCAL類型)
而64位系統有一個單獨的資料結構,其内容和32系統相同,除了使用64位的資料類型。内容如下:
typedef struct elf64_shdr {
Elf64_Word sh_name; // Section name, index in string tbl
Elf64_Word sh_type; // Type of section
Elf64_Xword sh_flags; // Miscellaneous section attributes
Elf64_Addr sh_addr; // Section virtual addr at execution
Elf64_Off sh_offset; // Section file offset
Elf64_Xword sh_size; // Size of section in bytes
Elf64_Word sh_link; // Index of another section
Elf64_Word sh_info; // Additional section information
Elf64_Xword sh_addralign; // Section alignment
Elf64_Xword sh_entsize; // Entry size if section holds table
} Elf64_Shdr;
ELF标準定義了若幹固定名稱的節,這些節用于執行大多數目标檔案所需要的标準任務,所有名稱都從點開始,以便與使用者定義的節或者非标準的節相區分。最重要的标準節如下所示:
.bss:儲存程式未初始化的資料,在程式開始時填充0位元組
.data:儲存已經初始化的資料,例如在編譯期間用靜态資料初始化的結構,這些資料可以在程式運作期間更改
.rodata:儲存了程式使用的隻讀資料,不能更改
.dynamic和.dynstr:儲存了動态資訊,後面讨論
.interp:儲存了程式解釋器的名稱,形式為字元串
.shstrtab:包含了一個字元串表,定義了節名稱
.strtab:儲存了一個字元串表,主要包含了符合表所需要的各個字元串
.symtab:儲存了二進制檔案的符合表
.init和.fini:儲存了程式初始化和結束時執行的機器指令,這兩個節的内容有編譯器或者輔助工具自動建立,主要是為程式建立一個适當的運作環境
.text:儲存了主要的機器指令
3.5 字元串表String Tables (是一個簡單的一維字元串數組)
字元串表的格式我們在2.4的結尾處讨論過,因為其格式非常動态,核心不同提供一個固定的資料結構,必需手工分析現存資料
3.6 符号表Symbol Tables(Elf32_Sym結構體資料與符号的個數相同)
符号表儲存了查找程式符号、為符号指派、重定位符号所需要的全部資訊,如上所述,有一個專門類型的節來儲存符号表,其格式有下列資料結構定義:每一個函數符号都有一個這樣的結構體資料。有多少個符号就有多少個這樣的結構體資料。
typedef struct elf32_sym{
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
符号表的任務就是将一個字元串和一個值關聯起來,例如printf符号表表示printf函數在虛拟空間中的位址,該函數的機器碼就存在于該位址處。符号也可能有絕對值,有程式解釋,例如資料常數。
一個符号的确切用途有st_info定義,它分為兩個部分(比特位如何劃分與我們的讨論不相關),其中定義了下列資訊:
第一:符号的綁定(binding),這确定了符号的可見性,允許有下列三種不同的設定:
1:局部符号(STB_LOCAL):隻有在目标檔案内部可見,在于程式的其它部分連接配接時是不可見的。如果一個程式的幾個目标檔案都定義同名的此類符号是沒有問題的,這些局部符号間彼此不會幹擾。
2:全局符号(STB_GLOBAL):在定義的目标檔案内部可見,也可以由構成程式的其它目标檔案引用,每個全局符号在一個程式的内部隻引用一次,否則連結器就會報錯。
執行全局符号的位定義引用,将在重定位期間确定相關符号的位置,如果對全局符号的未定義引用無法解決,則拒絕程式執行或者靜态綁定。
3:弱符号(STB_WEAK):在整個程式中可見,當可以由多個定義。如果程式中一個全局符号和一個局部符号名稱相同,則全局符号就優先處理;即使一個弱符号沒有定義,程式也是可以靜态連結或者動态連結,這種情況下若符号的值為0
第二:符号類型也有若幹備選項,隻有以下三個與目标的主題相關(對其它值的描述由ELF标準提供)
STT_OBJCT:符号關聯到一二資料對象,比如變量,數組和指針;
STT_FUNC:符号關聯到一個函數或者過程;
STT_NOTTYPE:符号的類型為制定,用于未定義引用;
Elf32_Sym結構體除了包含st_name,st_value,st_info,還包含以下成員,其語義如下:
st_size:制定對象的長度,例如一個指針的長度或者struct對象中包含的位元組數,如果長度有位置,其值可以設定為0;
标準的ELF标準不支援st_other
st_shndx:儲存一個節(在節頭表)中的索引,符号将綁定到該節,該符号通常定義在此節的代碼中,但下列兩個值具有特殊的語義:
SHN_ABS制定符号的絕對值,不因重定位而改變
SHN_UNDER标準未定義符号,必須通過外部來源(比如連結目标檔案或者庫)來解決。
同樣,符号表也有一個64位的變體,除了使用的資料類型不同,其内容與32位的對應結構是相同的,如下所示;
typedef struct elf64_sym {
Elf64_Word st_name; // Symbol name, index in string tbl
unsigned char st_info; // Type and binding attributes
unsigned char st_other; // No defined meaning, 0
Elf64_Half st_shndx; // Associated section index
Elf64_Addr st_value; // Value of the symbol
Elf64_Xword st_size; // Associated symbol size
} Elf64_Sym;
我們可以使用readelf來查找程式的符号表中所有的符号,圖中标出的六項在test.o目标檔案中特别重要,其它的資料項由編譯器自動生成的,與我們的讨論無關O(∩_∩)O~
分析:源檔案中名稱”test.c”存儲為一個絕對值,它是常數不随着重定位而改變。該局部符号使用SET_FILE類型,将一個目标檔案關聯到對應的源檔案、
檔案中定義了兩個函數main和add,存儲為SET_FUNC類型的全家符号,兩個符号都執行節1,即ELF檔案中的.text節,儲存了兩個函數的機器代碼。
__nldbl_printf、puts、exit符号屬于未定義引用,節索引值為UND,因而在程式連結時它們必須關聯到标準庫中的函數,或者其它庫中以該名稱定義的函數。因為編譯器不指定所涉及的符号的類型,因而這兩個符号的類型都是STT_NOTYPE。
第四部分 重定位項Relocation Entries
重定位是将ELF檔案中為定義符号關聯到有效值的處理過程,在我們的例子test.o中,這意味着對printf、exit、puts的未定義引用必須替換為該程序的虛拟位址空間中相應的機器代碼所在的空間,在目标檔案中用到的符号之處都将被替換。
對使用者空間程式的替換,核心并不會牽涉其中。因為所有的替換操作都是由外面工具完成的。當對于Linux核心子產品來說,情況有所不同,因為核心所接受到得子產品裸資料與存儲在二進制檔案中的資料完全相同,核心負責重定位操作。
在每個目标檔案中都有一個專門的表,包含了重定位表項,辨別了需要進行重定位的地方。每個表項都包含了下列資訊:
第一:一個偏移量,用來指定所要修改的項的位置
第二:對符号的引用(符号表的索引),提供了所要插入的重定位位置的資料
為了說明如何使用重定位資訊,我們來看一下此前test.c測試程式,我們用readelf顯示的所有重定位項如下圖所示:
在程式運作時或者test.o産生可執行檔案時,如果某些機器代碼使用了虛拟位址空間中位置尚不明确的符号或者函數,則會使用offset列的資訊。main函數的彙編語言代碼調用了若幹函數,分别位于偏移量0x 7c,0x98,0xa0如下所示:
00000050 <main>:
50: 94 21 ff e0 stwu r1,-32(r1)
54: 7c 08 02 a6 mflr r0
58: 90 01 00 24 stw r0,36(r1)
5c: 93 e1 00 1c stw r31,28(r1)
60: 7c 3f 0b 78 mr r31,r1
64: 38 00 00 03 li r0,3
68: 90 1f 00 08 stw r0,8(r31)
6c: 38 00 00 04 li r0,4
70: 90 1f 00 0c stw r0,12(r31)
74: 80 7f 00 08 lwz r3,8(r31)
78: 80 9f 00 0c lwz r4,12(r31)
7c: 48 00 00 01 bl 7c <main+0x2c>
80: 90 7f 00 10 stw r3,16(r31)
84: 3c 00 00 00 lis r0,0
88: 30 00 00 1c addic r0,r0,28
8c: 7c 03 03 78 mr r3,r0
90: 80 9f 00 10 lwz r4,16(r31)
94: 4c c6 31 82 crclr 4*cr1+eq
98: 48 00 00 01 bl 98 <main+0x48>
9c: 38 60 00 00 li r3,0
a0: 48 00 00 01 bl a0 <main+0x50>
備注:
我們可以用objdump工具檢視,示意圖如下:
在printf和add函數的位址已經确定後,必須将它們插入指定的偏移量處,以便可以生成正确運作的可執行代碼。
4.1 Linux核心中相關的資料結構
由于技術原因,有兩種類型的重定位資訊,有兩種稍有不同的資料結構表示:
第一種:普通重定位,SHT_REL類型的節中的重定位表項由以下的資料結構表示:
typedef struct elf32_rel {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
r_offset:指定需要重定位的項的位置
r_info:不僅提供了符合表中的一個位置,還包括重定位類型的有關資訊。這是通過把值劃分為兩個部分來達到的。
第二種:需要添加常數的重定位項隻出現在SHT_RELA類型的節中,這類結構由下列資料結構定義:
typedef struct elf32_rela{
Elf32_Sword r_addend;
} Elf32_Rela;
這裡除了有第一種重定位類型提供的r_offset和r_info字段之外,還補充了r_addend字段,其中存放一個稱之為加數(addend)的值。在計算重定位值時,将根據重定位類型,對該值進行不同的處理。
請注意:
在使用elf32_rel時也會出現加數這個值,盡管在資料結構中沒有明确的儲存,但連結器根據該值應該在記憶體中出現的位置,将計算出的重定位長度作為加數填入,該值的用途将在下面的例子中說明。
對兩種重定位類型,都有功能等效的64位資料結構:
typedef struct elf64_rel {
Elf64_Addr r_offset; //Location at which to apply the action
Elf64_Xword r_info; //index and type of relocation
} Elf64_Rel;
typedef struct elf64_rela {
Elf64_Sxword r_addend; //Constant addend used to compute value
} Elf64_Rela;
解釋:這兩個資料結構和32位的對應類型非常相似,這裡就不在讨論
4.2 重定位類型
ELF标準定義了很多重定位類型,對于每種支援的體系結構,都有一個獨立的集合,這些類型大部分用于生成動态庫或者與裝載位置無關的代碼。Linux核心隻對子產品的重定位感興趣,是以使用下面的兩種重定位類型:相對重定位和絕對重定位。
我們通過一個例子介紹一下相對從定位:
1000058c <add>:
1000058c: 94 21 ff e0 stwu r1,-32(r1)
100005d8: 4e 80 00 20 blr
100005dc <main>:
100005dc: 94 21 ff e0 stwu r1,-32(r1)
...........................
10000608: 4b ff ff 85 bl 1000058c <add>
..........................
10000624: 48 01 03 35 bl 10010958 <__nldbl_printf@plt>
10000628: 38 60 00 00 li r3,0
1000062c: 48 01 03 45 bl 10010970 <exit@plt>
Disassembly of section .plt:
10010900 <__register_frame_info@plt-0x48>:
...
10010948 <__register_frame_info@plt>:
10010950 <__deregister_frame_info@plt>:
10010958 <__nldbl_printf@plt>:
10010960 <puts@plt>:
10010968 <__gmon_start__@plt>:
10010970 <exit@plt>:
我們首先介紹一下bl指令的格式:
0-5 6-29 30 31
OPCD LI AA LK
AA=0,表示LI中存放的是相對位址LI*4,基址是目前指令的位址
AA=1,表示LI中存放的是絕對位址LI*4
LK=1,表示轉移到目的位址的同時,将目前指令的下一條指令存入LR寄存器
LK=0,僅僅表示跳轉到目的位址,而不用修改LR寄存器
目前bl指令的機器碼是0x4b ff ff 85
是以4*LI的值為:0b11 1111 1111 1111 1111 1000 0100符号擴充為0xffff ff84對應的真值為:-124(十進制)=-0x7c
因為目前指令的位址為0x1000 0608
是以跳轉的目标位址為0x1000 0608+(-0x7c)= 1000058c,即為add函數的入口位址1000058c
定義指令10000624: 48 01 03 35 bl 10010958 <__nldbl_printf@plt>
同理可以得到4*LI=0x10334
是以跳轉到得目的位址就是0x1000 0624+0x10334=0x1001 0958,即為.plt表中__nldbl_printf的入口位址。
4.3 動态連結
核心對必須與庫動态連結産生的ELF檔案不感興趣,子產品中的所有引用都可以通過重定位解決,而使用者空間程式的動态連結則完全由使用者空間中的ld.so進行。以下兩個節用來儲存動态連結器所需要的資料。
.dynsym儲存了有關符号表,包含了所有需要外部引用解決的符号
.dynamic儲存了一個數組,數組項為Elf32_Dyn類型,這些選項提供了以下幾個段落所描述的資料。
dynamic的内容可以使用readelf查詢,示意圖如下:
輸出的内容不僅包含若幹生成可執行檔案時自動添加的符号,還包括機器代碼使用的__nldbl_printf和exit函數,@GLIBC_2.0指出至少使用GNU标準庫中的2.0版本才能解決這些引用。
dynamic節中數組項的資料類型在核心中定義如下:
typedef struct dynamic{
Elf32_Sword d_tag;
union{
Elf32_Sword d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
d_tag用于區分各種指定資訊類型的标記,該結構中的共用體根據該标志進行解釋:
d_un或者儲存一個虛拟位址,或者儲存一個整數,可以根據特定的标志進行解釋。
最重要的标記如下所示:
DT_NEEDED指定該執行程式所需要的一個動态庫,d_un指向一個字元串表項,給出了庫的名稱,對于test.c測試程式來說隻需要C标準庫,如下圖所示:
DT_STRTAB儲存了字元串表的位置,其中包括了dynamic節所需要的所有的動态庫和符号的名稱。
DT_SYMTAB儲存了符号表的位置,其中包含了dynamic節所需要的所有的資訊
DT_INIT和DT_FINI儲存了用于初始化和結束程式的位址。
參考資料:
Professional Linux Kernel Architecture.Wolfgang Mauerer. Page1273-Page1267
http://www.ibm.com/developerworks/cn/linux/l-excutff/
http://blog.chinaunix.net/space.php?uid=16329656&do=blog&id=2747482