和JavaScript这样的解释执行语言不同,编译执行的语言通过编译、链接最终生成可执行文件。
ELF(Executable and Linkable Format),指.o文件和可执行文件,本文分析.o文件格式。
1. 示例程序
demo.s
#understanding .obj sections and .elf segments
.section .data
mydata1:
.ascii "This is my data1 xxx."
mydata2:
.ascii "This is my data2 xxx."
.section .text
.globl _start
_start:
movl $mydata1, %edi #replace "xxx" with "1st"
movb $49, 17(%edi) #"1st"
movb $115, 18(%edi)
movb $116, 19(%edi)
movl $mydata2, %edi #replace "xxx" with "2nd"
movb $50, 17(%edi) #"2nd"
movb $110, 18(%edi)
movb $100, 19(%edi)
movl $1, %eax #syscall exit
movl $0, %ebx #argument 0, ie. exit(0)
int $0x80
上述程序的功能是将两段字符串中"XXX"分别替换为"1st"和"2nd"
编译
as -o demo.o demo.s
得到demo.o文件
链接
ld -o demo demo.o
得到demo可执行文件。
2. 调试
加载到调试器中
gdb demo
设置断点
break *_start
让程序在_start处暂停。
查看当前指令
display/2i $eip
查看eip指针指向的两条指令。显示格式为I (instruction)
运行
r
运行,显示输出:
1: x/2i $eip
=> 0x8048074 <_start>: mov $0x80490a4,%edi 0x8048079 <_start+5>:
movb $0x31,0x11(%edi)
0x80490a4即为mydata1在内存中的起始地址。
查看mydata
display/5s 0x80490a4
显示地址0x80490a4开始的5条字符串。输出格式为s(string)
执行下一条指令
ni
输入ni,执行next instruction。然后一直按回车,重复执行ni。
最终显示输出:
2: x/5s 0x80490a4
0x80490a4 : "This is my data1 1st.This is my data2 2nd."
0x80490cf: ".symtab"
0x80490d7: ".strtab"
0x80490df: ".shstrtab"
0x80490e9: ".text"
"This is…."存放.data节中,".sysmtab",".strtab"等字符串存放在.shstrtab 节中。最终加载到内存中,.shstrtab刚好位于.data节后,所以将.shstrtab中的内容也打印出来了。(结论:.shstrtab也是会被加载到内存中的。)
3. 分析.o文件的结构。
输出elf文件内容
readelf -a demo.o > elf.demo.o
用readelf工具将 demo3.o的内容输出到elf.demo3.o中。
反汇编.text段
objdump -d demo.o > objdump.demo.o
用objdump工具将.text段的代码反汇编,输出到objdump.demo.o中
说明:下面内容中中括[]号内表示文件中的起止位置(包括首尾),单位为byte,计数进制为16进制, 圆括号()表示不包含首尾字节。
(1) ELF Header [0,33](共52 bytes)
文件开始的52byte为ELF Header,这52字节按如下结构进行解释
elf32_hdr结构体
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; /* Entry point */
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;
本实例显示内容为:
ELF Header
(2) .text [34,61](共46 bytes)
.text存放的是汇编代码,共2e(即十进制的46) 字节。
.text
0: bf 00 00 00 00 mov $0x0,%edi
5: c6 47 11 31 movb $0x31,0x11(%edi)
9: c6 47 12 73 movb $0x73,0x12(%edi)
d: c6 47 13 74 movb $0x74,0x13(%edi)
11: bf 15 00 00 00 mov $0x15,%edi
16: c6 47 11 32 movb $0x32,0x11(%edi)
1a: c6 47 12 6e movb $0x6e,0x12(%edi)
1e: c6 47 13 64 movb $0x64,0x13(%edi)
22: b8 01 00 00 00 mov $0x1,%eax
27: bb 00 00 00 00 mov $0x0,%ebx
2c: cd 80 int $0x80
(3) 填充字符 [62,63]
用00 00填充。
(4) .data [64,8D](共42 bytes)
.data段对应汇编语言中的.data段,存放了两个字符串,共2a (即十进制的42)字节。
.text段后有两个空字符 00 00,然后才是.data段。
.data
This is my data1 xxx.This is my data2 xxx.
(5) 填充字符 [62,63]
用 00 00填充。
(6) .bss (90,90)(共0bytes)
.bss段,只有section header,没有对应section。所以它对应的section大小设置为0字节。.bss段不占用.o文件中的空间,当进程被加载到内存中时,才为.bss分配空间,并用0填充该空间。
(7) .shstrtab [90,BF](共48 bytes)
.shstrtab 即 section header string table,存放的是各个section名称字符串。其中第一个字符串为'\0'. 此处为30 (即十进制的48)字节。
.shstrtab
""
".symtab"
".strtab"
".shstrtab"
".rel.text"
".data"
".bss"
上面字符串按照标准C语言格式表示的,即 “.symtab”实际由: '.' 's' 'y' 'm' 't' 'a' 'b' '\0' 字符组成。每个字符串用'\0'结尾,在文件视图中显示为一个点“.”(16进制00)。另外,名称
注意:这些字符串之间并没有用换行分隔,这里只是为了显示清晰,每个字符串单独占一行。
(8) Section Headers [C0,1FF](共320 bytes)
section headers 处存放的是用来描述各个section的section header,每个section header占40 bytes, 这里共有8个section header。
每个占用40个字节的section header根据如下结构体解释:
Elf32_Shdr
typedef struct {
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的有效信息如下:
section headers
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 00002e 00 AX 0 0 4
[ 2] .rel.text REL 00000000 000288 000010 08 6 1 4
[ 3] .data PROGBITS 00000000 000064 00002a 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 000090 000000 00 WA 0 0 4
[ 5] .shstrtab STRTAB 00000000 000090 000030 00 0 0 1
[ 6] .symtab SYMTAB 00000000 000200 000070 10 7 6 4
[ 7] .strtab STRTAB 00000000 000270 000018 00 0 0 1
第0条对应ELF Header.
(9) .symtab [200,26F](共112 bytes)
.symtab 存放的是代码中使用到的符号字符串索引以及它对应的地址、属性(local,global,weak global)等信息。在这里共占用0x70(即十进制的112)字节。
其中每一项占用16个字节,这里共有7项,所以占用112字节。每项都通过如下结构体进行解释
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;
在本示例中,.symtab中7项内容的有效信息如下:
Symbol table '.symtab' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 SECTION LOCAL DEFAULT 1
2: 00000000 0 SECTION LOCAL DEFAULT 3
3: 00000000 0 SECTION LOCAL DEFAULT 4
4: 00000000 0 NOTYPE LOCAL DEFAULT 3 mydata1
5: 00000015 0 NOTYPE LOCAL DEFAULT 3 mydata2
6: 00000000 0 NOTYPE GLOBAL DEFAULT 1 _start
1~3条分别表示索引号为1、3、4的section的名称,即.text,.data和.bss。第0条默认为STN_UNDEF(undefined)类型
其中,后三项对应代码中的三个符号。
(10) .strtab [270,287](共24 bytes)
.strtab即 string table 用来存放在代码中定义的符号的字符串。
和.shstrtab一样,第一个字符串为'\0',每个字符串以'\0'结尾。
.strtab
""�"mydata1"�"mydata2"�"_start"
(11) .rel.text [288,297](共16 bytes)
.rel.text存放的是.text节的重定位信息,。其中每一项占8个字节,这里共两项所以占16字节。
PS: .rel.XXX节为.XXX节的重定位表,如:.data段的重定位表为.rel..data
每一项通过如下结构解释:
Elf32_Rel
typedef struct elf32_rel {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;
本示例中,有效信息如下:
.rel.text
Relocation section '.rel.text' at offset 0x288 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
00000001 00000201 R_386_32 00000000 .data
00000012 00000201 R_386_32 00000000 .data
(12) 重定位
在源码中定义的符号代码一个地址,在.o文件中,该地址指向对应符号所在文件中的相对位置。链接过程中需要将这个相对位置替换为虚拟内存地址,才能使得程序正常运行。
汇编代码
movl $mydata1, %edi
movb $49, 17(%edi)
movb $115, 18(%edi)
movb $116, 19(%edi)
movl $mydata2, %edi
movb $50, 17(%edi)
movb $110, 18(%edi)
movb $100, 19(%edi)
movl $1, %eax #syscall exit
movl $0, %ebx #argument 0, ie. exit(0)
int $0x80
.o文件中的.text段
00000000 <_start>:
0: bf 00 00 00 00 mov $0x0,%edi
5: c6 47 11 31 movb $0x31,0x11(%edi)
9: c6 47 12 73 movb $0x73,0x12(%edi)
d: c6 47 13 74 movb $0x74,0x13(%edi)
11: bf 15 00 00 00 mov $0x15,%edi
16: c6 47 11 32 movb $0x32,0x11(%edi)
1a: c6 47 12 6e movb $0x6e,0x12(%edi)
1e: c6 47 13 64 movb $0x64,0x13(%edi)
22: b8 01 00 00 00 mov $0x1,%eax
27: bb 00 00 00 00 mov $0x0,%ebx
2c: cd 80 int $0x80
可执行文件中的.text段
08048074 <_start>:
8048074: bf a4 90 04 08 mov $0x80490a4,%edi
8048079: c6 47 11 31 movb $0x31,0x11(%edi)
804807d: c6 47 12 73 movb $0x73,0x12(%edi)
8048081: c6 47 13 74 movb $0x74,0x13(%edi)
8048085: bf b9 90 04 08 mov $0x80490b9,%edi
804808a: c6 47 11 32 movb $0x32,0x11(%edi)
804808e: c6 47 12 6e movb $0x6e,0x12(%edi)
8048092: c6 47 13 64 movb $0x64,0x13(%edi)
8048096: b8 01 00 00 00 mov $0x1,%eax
804809b: bb 00 00 00 00 mov $0x0,%ebx
80480a0: cd 80 int $0x80
观察上面代码中绿色阴影标记部分,在.o文件中,使用的是mydata1和mydata2在.data段中的相对位置。在可执行文件中,使用的是mydata1和mydata2的虚拟内存地址。
在.rel.text中可以看到,.o文件中将汇编源码中使用了mydata1和mydata2的地方标记为需要重定位,重定位类型为:R_386_32
。
一个符号就代表一个地址,这两个符号代表的值是它们在.data段中的偏移量,分别为0和15(即十进制 的21)。所以.o中两条mov指令使用的分别是0x1和0x15。
已经初始化的数据存放在.data section中。这些数据实际上引用的是.data的地址。例如,定义了一个变量static int x = 10. 我们知道符号实际上代表一个地址而已,在这里引用x时,实际上是这样的:.symtab中记录了x在.data section中的偏移量(假设是12),其他地方要用到x变量时,实际上是通过 .data地址+偏移量 来进行引用的。
对于上面的 movl $mydata1, %edi
指令也是如此。因此,在重定位表中记录的需要重定位的项不是mydata1和mydata2而是.data。
Offset Info Type Sym.Value Sym. Name
00000001 00000201 R_386_32 00000000 .data
00000012 00000201 R_386_32 00000000 .data
offset 表示要更改的地方在.text section中起始地址。要更改的内容默认为为该处的4个字节;
info
1. 表示更改的方式为:0X01 即:R_386_32
2. 被引用的符号在.symtab中的索引为: 0x000002,即.data
需要重定位的地方如上表所示,offset为需要重定位的字段(四字节)在.text section中的偏移量(.rel.text是.text section的重定位信息表)。被引用的symbol为.data(因为它是已经初始化的数据,因此被引用符号为.data而不是mydata1和mydata2). info字段中,低八位表示重定位类型(上面显示的是十六进制),因此这里是01即R_386_32
. 高24位表示的是被引用字符在.symtab中的索引,这里高24位为2,查找上面的.symtab可以发现它对应第三条(粗体显示):
Symbol table '.symtab' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 SECTION LOCAL DEFAULT 1
2: 00000000 0 SECTION LOCAL DEFAULT 3
3: 00000000 0 SECTION LOCAL DEFAULT 4
4: 00000000 0 NOTYPE LOCAL DEFAULT 3 mydata1
5: 00000015 0 NOTYPE LOCAL DEFAULT 3 mydata2
6: 00000000 0 NOTYPE GLOBAL DEFAULT 1 _start
1~3条分别表示索引号为1、3、4的section的名称,即.text,.data和.bss。第0条默认为STN_UNDEF(undefined)类型 第三条中的Ndx字段为3,表示它是第三节(即.data)中的符号。
R_386_32
重定位方式:
.text section中偏移量为0x1和偏移量为0x12处的四字节需要更改为.data section在内存中的起始地址+它原来的值(分别为0x0和0x15)。(生成的.o文件中。汇编器(as命令)已经将我们汇编代码中引用到的mydata1和mydata2分别替换为了这两个符号在.data中的偏移地址了)
细节情况如下:
R_386_32
:重定位一个使用32位绝对地址的引用。其地址计算方法为.symtab中对应的value值加上原始值。链接过程中先将.symtab中索引为2的项(即.data)的value设置为它的虚拟地址(本示例是:80490a4)。用该虚拟地址加上原值,便得到了新的值,这也就是之前所说的那个过程。
重定位的方式还有很多种,这里只讨论其中之一,也是最常用的一种。注意:当被引用的符号是未初始化的变量时,它和上面的过程一样,整个过程仅仅是把上面涉及的.data符号改为被引用的符号而已。
总结:
编译器将程序的入口地址设置为标号_start的地址。所以汇编程序必须提供_start符号,C程序由glibc自动提供,因此在C程序中不能定义新的_start全局符号。
附录
输出文件
elf.demo.o
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 192 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 8
Section header string table index: 5
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 00002e 00 AX 0 0 4
[ 2] .rel.text REL 00000000 000288 000010 08 6 1 4
[ 3] .data PROGBITS 00000000 000064 00002a 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 000090 000000 00 WA 0 0 4
[ 5] .shstrtab STRTAB 00000000 000090 000030 00 0 0 1
[ 6] .symtab SYMTAB 00000000 000200 000070 10 7 6 4
[ 7] .strtab STRTAB 00000000 000270 000018 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
There are no section groups in this file.
There are no program headers in this file.
Relocation section '.rel.text' at offset 0x288 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
00000001 00000201 R_386_32 00000000 .data
00000012 00000201 R_386_32 00000000 .data
There are no unwind sections in this file.
Symbol table '.symtab' contains 7 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 SECTION LOCAL DEFAULT 1
2: 00000000 0 SECTION LOCAL DEFAULT 3
3: 00000000 0 SECTION LOCAL DEFAULT 4
4: 00000000 0 NOTYPE LOCAL DEFAULT 3 mydata1
5: 00000015 0 NOTYPE LOCAL DEFAULT 3 mydata2
6: 00000000 0 NOTYPE GLOBAL DEFAULT 1 _start
No version information found in this file.
objdump.demo.o
demo.o: file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
0: bf 00 00 00 00 mov $0x0,%edi
5: c6 47 11 31 movb $0x31,0x11(%edi)
9: c6 47 12 73 movb $0x73,0x12(%edi)
d: c6 47 13 74 movb $0x74,0x13(%edi)
11: bf 15 00 00 00 mov $0x15,%edi
16: c6 47 11 32 movb $0x32,0x11(%edi)
1a: c6 47 12 6e movb $0x6e,0x12(%edi)
1e: c6 47 13 64 movb $0x64,0x13(%edi)
22: b8 01 00 00 00 mov $0x1,%eax
27: bb 00 00 00 00 mov $0x0,%ebx
2c: cd 80 int $0x80