2.1汇编max.s 先看一个简单的汇编max.s .section .data data_items: .long 3,67,34,222,45,75,54,34,44,33,22,11,66,0 .section .text .globl _start _start: movl $0, %edi # move 0 into the index register movl data_items(,%edi,4), %eax # load the first byte of data movl %eax, %ebx # since this is the first item, %eax is # the biggest start_loop: # start loop cmpl $0, %eax # check to see if we've hit the end je loop_exit incl %edi # load next value movl data_items(,%edi,4), %eax cmpl %ebx, %eax # compare values ebx is the biigest num , jle start_loop # jump to loop beginning if the new one isn't bigger: jle means if eax is less than or equals ebx movl %eax, %ebx # move the value as the largest jmp start_loop # jump to loop beginning loop_exit: # %ebx is the status code for the _exit system call # and it already has the maximum number movl $1, %eax #1 is the _exit() syscall int $0x80
汇编、链接、运行:
$ as max.s -o max.o $ ld max.o -o max $ ./max $ echo $?2.2ELF文件
ELF文件格式是一个开放标准,各种UNIX系统的可执行文件都采用ELF格式,它有三种不同的类型:
- 可重定位的目标文件(Relocatable,或者Object File)
- 可执行文件(Executable)
- 共享库(Shared Object,或者Shared Library)
现在详细解释一下这个程序的汇编、链接、运行过程:
- 写一个汇编程序保存成文本文件
max.s
。- 汇编器读取这个文本文件转换成目标文件
max.o
,目标文件由若干个Section组成,我们在汇编程序中声明的.section
会成为目标文件中的Section,此外汇编器还会自动添加一些Section(比如符号表)。- 然后链接器把目标文件中的Section合并成几个Segment,生成可执行文件
max
。- 最后加载器(Loader)根据可执行文件中的Segment信息加载运行这个程序。
ELF格式提供了两种不同的视角,链接器把ELF文件看成是Section的集合,而加载器把ELF文件看成是Segment的集合。如下图所示。
图 18.1. ELF文件
2.2.1目标文件
下面用
readelf
工具读出目标文件max.o
的ELF Header和Section Header Table,然后我们逐段分析。$ readelf -a max.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: 200 (bytes into file) //从文件地址200(0xc8)开始, // 每个Section Header占40字节,共40*8=320字节,到文件地址0x207结束。 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 2.2.2 section头 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 00002a 00 AX 0 0 4 [ 2] .rel.text REL 00000000 0002b0 000010 08 6 1 4 [ 3] .data PROGBITS 00000000 000060 000038 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 000098 000000 00 WA 0 0 4 [ 5] .shstrtab STRTAB 00000000 000098 000030 00 0 0 1 [ 6] .symtab SYMTAB 00000000 000208 000080 10 7 7 4 [ 7] .strtab STRTAB 00000000 000288 000028 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific) There are no program headers in this file.注:从Section Header中读出各Section的描述信息,
其中
.text
和.data
是我们在汇编程序中声明的Section,而其它Section是汇编器自动添加的。
Addr
是这些段加载到内存中的地址(我们讲过程序中的地址都是虚拟地址),加载地址要在链接时填写,现在空缺,所以是全0。
Off
和Size
列指出了各Section的起始文件地址和长度。比如.data
段从文件地址0x60开始,一共0x38个字节,回去翻一下程序,.data
段定义了14个4字节的整数,一共是56个字节,也就是0x38。根据以上信息可以描绘出整个目标文件的布局。表 18.1. 目标文件的布局
起始文件地址 Section或Header 0 ELF Header 0x34 .text
0x60 .data
0x98 .bss
(此段为空)0x98 .shstrtab
0xc8 Section Header Table 0x208 .symtab
0x288 .strtab
0x2b0 .rel.text
这个文件不大,我们直接用hexdump
工具把目标文件的字节全部打印出来看。 》hexdump -C max.o 00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 01 00 03 00 01 00 00 00 00 00 00 00 00 00 00 00 |................| 00000020 c8 00 00 00 00 00 00 00 34 00 00 00 00 00 28 00 |........4.....(.| 00000030 08 00 05 00 bf 00 00 00 00 8b 04 bd 00 00 00 00 |................| 00000040 89 c3 83 f8 00 74 10 47 8b 04 bd 00 00 00 00 39 |.....t.G.......9| 00000050 d8 7e ef 89 c3 eb eb b8 01 00 00 00 cd 80 00 00 |.~..............| 00000060 03 00 00 00 43 00 00 00 22 00 00 00 de 00 00 00 |....C...".......| 00000070 2d 00 00 00 4b 00 00 00 36 00 00 00 22 00 00 00 |-...K...6..."...| 00000080 2c 00 00 00 21 00 00 00 16 00 00 00 0b 00 00 00 |,...!...........| 00000090 42 00 00 00 00 00 00 00 00 2e 73 79 6d 74 61 62 |B.........symtab| 000000a0 00 2e 73 74 72 74 61 62 00 2e 73 68 73 74 72 74 |..strtab..shstrt| 000000b0 61 62 00 2e 72 65 6c 2e 74 65 78 74 00 2e 64 61 |ab..rel.text..da| 000000c0 74 61 00 2e 62 73 73 00 00 00 00 00 00 00 00 00 |ta..bss.........| 000000d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|我们继续分析
readelf
输出的最后一部分,是从.rel.text
和.symtab
这两个Section中读出的信息。... Relocation section '.rel.text' at offset 0x2b0 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000008 00000201 R_386_32 00000000 .data 00000017 00000201 R_386_32 00000000 .data There are no unwind sections in this file. Symbol table '.symtab' contains 8 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 data_items 5: 0000000e 0 NOTYPE LOCAL DEFAULT 1 start_loop 6: 00000023 0 NOTYPE LOCAL DEFAULT 1 loop_exit 7: 00000000 0 NOTYPE GLOBAL DEFAULT 1 _start No version information found in this file. 注:.rel.text
告诉链接器指令中的哪些地方需要做重定位,在下一小节详细讨论。.symtab
是符号表。Ndx
列是每个符号所在的Section编号, 例如符号data_items
在第3个Section里(也就是.data
段), 各Section的编号见Section Header Table。Value
列是每个符号所代表的地址,在目标文件中,符号地址都是相对于该符号所在Section的相对地址, 比如data_items
位于.data
段的开头,所以地址是0,_start
位于.text
段的开头,所以地址也是0, 但是start_loop
和loop_exit
相对于.text
段的地址就不是0了。 从Bind
这一列可以看出_start
这个符号是GLOBAL
的,而其它符号是LOCAL
的,GLOBAL
符号是在汇编程序中用.globl
指示声明过的符号。 2.2.3现在剩下
.text
段没有分析,objdump
工具可以把程序中的机器指令反汇编(Disassemble),那么反汇编的结果是否跟原来写的汇编代码一模一样呢?我们对比分析一下。$ objdump -d max.o max.o: file format elf32-i386 Disassembly of section .text: 00000000 <_start>: 0: bf 00 00 00 00 mov $0x0,%edi 5: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax c: 89 c3 mov %eax,%ebx 0000000e <start_loop>: e: 83 f8 00 cmp $0x0,%eax 11: 74 10 je 23 <loop_exit> 13: 47 inc %edi 14: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax 1b: 39 d8 cmp %ebx,%eax 1d: 7e ef jle e <start_loop> 1f: 89 c3 mov %eax,%ebx 21: eb eb jmp e <start_loop> 00000023 <loop_exit>: 23: b8 01 00 00 00 mov $0x1,%eax 28: cd 80 int $0x802.3. 可执行文件
现在我们按上一节的步骤分析可执行文件
max
,看看链接器都做了什么改动。$ readelf -a max 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: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x8048074 Start of program headers: 52 (bytes into file) Start of section headers: 256 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 2 Size of section headers: 40 (bytes) Number of section headers: 6 Section header string table index: 3 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 08048074 000074 00002a 00 AX 0 0 4 [ 2] .data PROGBITS 080490a0 0000a0 000038 00 WA 0 0 4 [ 3] .shstrtab STRTAB 00000000 0000d8 000027 00 0 0 1 [ 4] .symtab SYMTAB 00000000 0001f0 0000a0 10 5 6 4 [ 5] .strtab STRTAB 00000000 000290 000040 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific) There are no section groups in this file. Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x0009e 0x0009e R E 0x1000 LOAD 0x0000a0 0x080490a0 0x080490a0 0x00038 0x00038 RW 0x1000 Section to Segment mapping: Segment Sections... 00 .text 01 .data There is no dynamic section in this file. There are no relocations in this file. There are no unwind sections in this file. Symbol table '.symtab' contains 10 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 08048074 0 SECTION LOCAL DEFAULT 1 2: 080490a0 0 SECTION LOCAL DEFAULT 2 3: 080490a0 0 NOTYPE LOCAL DEFAULT 2 data_items 4: 08048082 0 NOTYPE LOCAL DEFAULT 1 start_loop 5: 08048097 0 NOTYPE LOCAL DEFAULT 1 loop_exit 6: 08048074 0 NOTYPE GLOBAL DEFAULT 1 _start 7: 080490d8 0 NOTYPE GLOBAL DEFAULT ABS __bss_start 8: 080490d8 0 NOTYPE GLOBAL DEFAULT ABS _edata 9: 080490d8 0 NOTYPE GLOBAL DEFAULT ABS _end No version information found in this file.在ELF Header中,
Type
改成了EXEC
,由目标文件变成可执行文件了,Entry point address
改成了0x8048074(这是_start
符号的地址),还可以看出,多了两个Program Header,少了两个Section Header。在Section Header Table中,
.text
和.data
段的加载地址分别改成了0x08048074和0x080490a0。.bss
段没有用到,所以被删掉了。.rel.text
段就是用于链接过程的,做完链接就没用了,所以也删掉了。多出来的Program Header Table描述了两个Segment的信息。
.text
段和前面的ELF Header、Program Header Table一起组成一个Segment(FileSiz
指出总长度是0x9e),.data
段组成另一个Segment(总长度是0x38)。VirtAddr
列指出第一个Segment加载到虚拟地址0x08048000(注意在x86平台上后面的PhysAddr
列是没有意义的,并不代表实际的物理地址),第二个Segment加载到地址0x080490a0。Flg
列指出第一个Segment的访问权限是可读可执行,第二个Segment的访问权限是可读可写。最后一列Align
的值0x1000(4K)是x86平台的内存页面大小。在加载时文件也要按内存页面大小分成若干页,文件中的一页对应内存中的一页,对应关系如下图所示。
这个可执行文件很小,总共也不超过一页大小,但是两个Segment必须加载到内存中两个不同的页面,因为MMU的权限保护机制是以页为单位的,一个页面只能设置一种权限。此外还规定每个Segment在文件页面内偏移多少加载到内存页面仍然要偏移多少,比如第二个Segment在文件中的偏移是0xa0,在内存页面0x08049000中的偏移仍然是0xa0,所以从0x080490a0开始,这样规定是为了简化链接器和加载器的实现。从上图也可以看出
.text
段的加载地址应该是0x08048074
,_start
符号位于.text
段的开头,所以_start
符号的地址也是0x08048074,从符号表中可以验证这一点。原来目标文件符号表中的
Value
都是相对地址,现在都改成绝对地址了。此外还多了三个符号__bss_start
、_edata
和_end
,这些符号在链接脚本中定义,被链接器添加到可执行文件中。再看一下反汇编的结果:
$ objdump -d max max: file format elf32-i386 Disassembly of section .text: 08048074 <_start>: 8048074: bf 00 00 00 00 mov $0x0,%edi 8048079: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax 8048080: 89 c3 mov %eax,%ebx 08048082 <start_loop>: 8048082: 83 f8 00 cmp $0x0,%eax 8048085: 74 10 je 8048097 <loop_exit> 8048087: 47 inc %edi 8048088: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax 804808f: 39 d8 cmp %ebx,%eax 8048091: 7e ef jle 8048082 <start_loop> 8048093: 89 c3 mov %eax,%ebx 8048095: eb eb jmp 8048082 <start_loop> 08048097 <loop_exit>: 8048097: b8 01 00 00 00 mov $0x1,%eax 804809c: cd 80 int $0x80指令中的相对地址都改成绝对地址了。我们仔细检查一下改了哪些地方。首先看跳转指令,原来目标文件的指令是这样:
... 11: 74 10 je 23 <loop_exit> ... 1d: 7e ef jle e <start_loop> ... 21: eb eb jmp e <start_loop> ...现在改成了这样:
... 8048085: 74 10 je 8048097 <loop_exit> ... 8048091: 7e ef jle 8048082 <start_loop> ... 8048095: eb eb jmp 8048082 <start_loop> ...改了吗?其实只是反汇编的结果不同了,指令的机器码根本没变。为什么不用改指令就能跳转到新的地址呢?因为跳转指令中指定的是相对于当前指令向前或向后跳多少字节,而不是指定一个完整的内存地址,内存地址有32位,这些跳转指令只有16位,显然也不可能指定一个完整的内存地址,这称为相对跳转。这种相对跳转指令只有16位,只能在当前指令前后的一个小范围内跳转,不可能跳得太远,也有的跳转指令指定一个完整的内存地址,可以跳到任何地方,这称绝对跳转.
再看内存访问指令,原来目标文件的指令是这样:
... 5: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax ... 14: 8b 04 bd 00 00 00 00 mov 0x0(,%edi,4),%eax ...现在改成了这样:
... 8048079: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax ... 8048088: 8b 04 bd a0 90 04 08 mov 0x80490a0(,%edi,4),%eax ...指令中的地址原本是0x00000000,现在改成了0x080490a0(注意是小端字节序)。那么链接器怎么知道要改这两处呢?是根据目标文件中的
.rel.text
段提供的重定位信息来改的:... Relocation section '.rel.text' at offset 0x2b0 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000008 00000201 R_386_32 00000000 .data 00000017 00000201 R_386_32 00000000 .data ...第一列
Offset
的值就是.text
段需要改的地方,在.text
段中的相对地址是8和0x17,正是这两条指令中00 00 00 00的位置。
linux内核之elf格式
最新推荐文章于 2021-10-06 15:24:15 发布