绪论
大家好,欢迎来到【程序员的自我修养】专栏。正如其专栏名,本专栏主要分享学习《程序员的自我修养——链接、装载与库》的知识点以及结合自己的工作经验以及思考。编译原理相关知识本身就比较有难度,我会尽自己最大的努力,争取深入浅出。若你希望与一群志同道合的朋友一起学习,也希望加入到我们的学习群中。文末有加入方式。
简介
我们知道linux 可执行文件是ELF格式(Executable Linkable Format)。但实际上,不仅是可执行文件,可重定位文件(.o目标文件和静态库)、共享目标文件(动态库)、核心转储文件(core文件)都是以ELF格式存储的。因此深入了解ELF文件就显示的十分重要。
注:静态库的实质就是多个.o文件的集合。
查看文件类型
我们可以通过file命令查看文件的类型。如下:
//a.c #include<stdio.h> int a() { printf("i'm a\n"); } //main.c extern int a(); int main() { a(); return 0; } |
疑问:为什么可执行程序main显示的是共享目标文件shared object?这说明了什么?
答:那是因为gcc默认开启了--enable-default-pie参数,其目的是让程序能装载在随机的地址,从而减少攻击者借用系统中的可执行代码实施攻击。类似缓冲区溢出之类的攻击将无法实现。可以在链接选项中增加-no-pie禁用该默认选项。
ELF文件应该包含哪些内容
问题:静态全局变量和静态局部变量存储在哪里?全局变量和局部变量存储在哪里?初始化和未初始化的全局变量又分别存储在哪里?
我相信这些应该是大家面试过程中经常遇到的问题。让我们再重温一下。如下:
由上可知,初始化的静态变量和全局变量保存在.data段;未初始化的静态变量和全局变量保存在.bss段;局部变量和代码语句保存在.text段。
总体而言,上面的源代码编译之后会分成两个部分:程序代码和程序数据。其中.data和.bss都属于程序数据。不知大家是否有这样的疑问:我们写代码时,并不会区分代码和数据,都是交叉编写的。为什么ELF文件要将它们区分开来,分段存储呢?
其实分段主要有三个好处,这也值得我们学习。
- 防止程序的指令被有意或无意的改写。因为程序装载之后,我们可以将代码段和数据段映射到不同的虚拟区域A,B。由于数据的是可读可写的,那么我们可以将虚拟区域B设置为可读写权限;而代码段对于进程而言是只读的,所以虚拟区域A设置为只读权限。
- 提高CPU的缓存命中率。现在的CPU一般会有数据缓存和指令缓存,再结合局部性原理。分段就能极大提高缓存命中率。
- 共享内存。这个也是主要的原因,我们知道动态库被多个程序依赖时,内存中只需要有一份代码段的副本即可。若进行分段,这样对于操作系统操作起来就什么简单。若代码段和数据段交叉混合,那么管理起来就非常困难。
难道我们的ELF文件的内容布局正如上图吗?答案肯定不是。这仅仅是其中的一部分。下一节,我们开始深入了解ELF的文件格式。
本章节的示例代码如下:
//example.c int global_init_var = 84; int global_unint_var; extern int printf(char* argv,...); void func1(int i) { printf("%d\n",i); } int main() { static int static_var = 85; static int static_var2;
int a = 1; int b; func1(static_var + static_var2 + a +b); return 0; } |
ELF 文件头信息
我们不妨从”头“开始了解ELF文件信息。我们可以通过readelf -h example.o命令查看文件头,如下:
yihua@ubuntu:~/test/example$ readelf -h example.o ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 1104 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 13 Section header string table index: 12 |
一般情况下,我们比较关注以下信息:
- Magic:其中第五个字节标识文件类型:0x01表示32位,0x02表示64位,因为我的虚拟机自带GCC编译链是64位的,所以是0x02。第六个字节是字节序,表示该ELF文件是大端或小端,0x00无效、0x01小端格式、0x02大端格式。
注:ELF文件的大小端并不是由编译平台决定的,而是由编译和链接决定的。它与目标执行平台大小端无关,它的作用是告诉目标执行平台,本文件的代码段,数据段的存储方式,用于解析。
- Machine: 表示文件的CPU平台属性,由上可知,X86-64。
- Entry point address:入口地址,规定该ELF程序的入口虚拟地址,即:操作系统在加载完该程序后从这个地址开始执行进程的指令。可重定位文件一般没有入口地址,可执行文件有。如下:
由图可知:example可执行文件的入口地址在文件的偏移0x540地址。我们再通过反汇编查看这个地址是。
yihua@ubuntu:~/test/example$ objdump -d example
example: file format elf64-x86-64
Disassembly of section .init:
00000000000004f0 <_init>: 4f0: 48 83 ec 08 sub $0x8,%rsp 4f4: 48 8b 05 ed 0a 20 00 mov 0x200aed(%rip),%rax # 200fe8 <__gmon_start__> 4fb: 48 85 c0 test %rax,%rax 4fe: 74 02 je 502 <_init+0x12> 500: ff d0 callq *%rax 502: 48 83 c4 08 add $0x8,%rsp 506: c3 retq
Disassembly of section .plt:
0000000000000510 <.plt>: 510: ff 35 aa 0a 20 00 pushq 0x200aaa(%rip) # 200fc0 <_GLOBAL_OFFSET_TABLE_+0x8> 516: ff 25 ac 0a 20 00 jmpq *0x200aac(%rip) # 200fc8 <_GLOBAL_OFFSET_TABLE_+0x10> 51c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000000520 <printf@plt>: 520: ff 25 aa 0a 20 00 jmpq *0x200aaa(%rip) # 200fd0 <printf@GLIBC_2.2.5> 526: 68 00 00 00 00 pushq $0x0 52b: e9 e0 ff ff ff jmpq 510 <.plt>
Disassembly of section .plt.got:
0000000000000530 <__cxa_finalize@plt>: 530: ff 25 c2 0a 20 00 jmpq *0x200ac2(%rip) # 200ff8 <__cxa_finalize@GLIBC_2.2.5> 536: 66 90 xchg %ax,%ax
Disassembly of section .text:
0000000000000540 <_start>: 540: 31 ed xor %ebp,%ebp 542: 49 89 d1 mov %rdx,%r9 545: 5e pop %rsi 546: 48 89 e2 mov %rsp,%rdx 549: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp 54d: 50 push %rax 54e: 54 push %rsp 54f: 4c 8d 05 ca 01 00 00 lea 0x1ca(%rip),%r8 # 720 <__libc_csu_fini> 556: 48 8d 0d 53 01 00 00 lea 0x153(%rip),%rcx # 6b0 <__libc_csu_init> 55d: 48 8d 3d 0a 01 00 00 lea 0x10a(%rip),%rdi # 66e <main> 564: ff 15 76 0a 20 00 callq *0x200a76(%rip) # 200fe0 <__libc_start_main@GLIBC_2.2.5> 56a: f4 hlt 56b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
000000000000064a <func1>: 64a: 55 push %rbp 64b: 48 89 e5 mov %rsp,%rbp 64e: 48 83 ec 10 sub $0x10,%rsp 652: 89 7d fc mov %edi,-0x4(%rbp) 655: 8b 45 fc mov -0x4(%rbp),%eax 658: 89 c6 mov %eax,%esi 65a: 48 8d 3d d3 00 00 00 lea 0xd3(%rip),%rdi # 734 <_IO_stdin_used+0x4> 661: b8 00 00 00 00 mov $0x0,%eax 666: e8 b5 fe ff ff callq 520 <printf@plt> 66b: 90 nop 66c: c9 leaveq 66d: c3 retq
000000000000066e <main>: 66e: 55 push %rbp 66f: 48 89 e5 mov %rsp,%rbp 672: 48 83 ec 10 sub $0x10,%rsp 676: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp) 67d: 8b 15 91 09 20 00 mov 0x200991(%rip),%edx # 201014 <static_var.1801> 683: 8b 05 93 09 20 00 mov 0x200993(%rip),%eax # 20101c <static_var2.1802> 689: 01 c2 add %eax,%edx 68b: 8b 45 f8 mov -0x8(%rbp),%eax 68e: 01 c2 add %eax,%edx 690: 8b 45 fc mov -0x4(%rbp),%eax 693: 01 d0 add %edx,%eax 695: 89 c7 mov %eax,%edi 697: e8 ae ff ff ff callq 64a <func1> 69c: b8 00 00 00 00 mov $0x0,%eax 6a1: c9 leaveq 6a2: c3 retq 6a3: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 6aa: 00 00 00 6ad: 0f 1f 00 nopl (%rax) |
可知:程序的入口是__start函数,在正式进入我们main函数前,其实已经执行了多个函数处理。
- Start of section headers 、Size of section headers、Number of section headers:这三个参数,表示段表的内容。Start of section headers表示段表,在本文件中的偏移地址。Size of section headers表示每个段的大小。Number of section headers表示段的数量。
到此,我们已经进一步了解ELF文件的格式了,大致如下:
总结
本章介绍了linux平台下可重定位文件,动态库文件,可执行文件的存储格式都是ELF类型。之后我们尝试探究ELF文件中应该有哪些内容,通过自己的猜测应该具备.text、.data、.bss段。
再通过readelf -h命令,又了解到ELF文件中还有file Header 和 section table。下一章,我们再通过了解段表,更进一步了解ELF文件的内容布局。还请关注不迷路哦~~~
有任何相关问题欢迎留言讨论,我会尽快回复。
若您正遇到相关问题,苦于没有一群志同道合的朋友交流,探讨。也欢迎加入我们的讨论组群。可通过私聊,我拉您入群。