编译过程<2>(目标文件)
前言
从源码到可执行文件,中间的过程中会生成 .o 文件,也就是目标文件,目标文件放的是什么呢?前面经常用objdump 反汇编过,似乎是以莫种格式存放的,什么格式呢? 可执行文件又是什么格式呢?
本文只讨论Linux下的目标文件
目标文件的格式
我们先用 file 命令查看
hrp@ubuntu:~$ file main
main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0
hrp@ubuntu:~$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
hrp@ubuntu:~$
- 可执行文件 main 是 ELF 64-bit LSB shared object ,表示它已经经过链接,可以直接在系统上执行
- 目标文件 hello.o 是 ELF 64-bit LSB relocatable,可重定位的目标文件,包含了编译后的程序的机器码和未解析的重定位信息
两者都有 ELF ,那ELF是什么?
ELF(Executable and Linkable Format)是一种常见的可执行文件和目标文件格式,用于在类Unix操作系统上存储程序的二进制代码以及与之相关的元数据。对于windows ,称之为 PE(Portable Executable)
从file 命令看目标文件和可执行文件 都是elf 格式,所以再Linux 下,统称为 ELF文件; 想动态库,静态库也是 elf 格式的文件
目标文件是怎样的?
源码编译成目标文件后,可以分为两个部分:
- 代码段(.text): 存放汇编指令
- 数据段(.bss / .data ):数据又分为两部分
- 初始化的全局变量和静态变量存放再 data
- 未初始化的存放再 .bss(Block Started by Symbol)
用size 命令可观测到
hrp@ubuntu:~$ cat hello.c
#include <stdio.h>
int a = 10;
static int b ;
void hello() {
printf("Hello, world!\n");
}
# data 区4个字节,变量a的; bss 4个字节 ,变量b 未初始化的
hrp@ubuntu:~$ gcc -c hello.c && size hello.o
text data bss dec hex filename
89 4 4 97 61 hello.o
hrp@ubuntu:~$
为什么需要bss 区? \\为什么需要bss 区? \\
本质上都可以存放,但 未初始化的全局变量都为0,如果放date区扩展空间,并存放数据为0,反而会占用空间,为了节省空间,未初始化的全局变量和声明为static的局部变量不占有任何空间,只是保存了在运行时它们要占的空间的大小,所以 bss 也被戏称为“Better Save Space”
为什么要区分代码段和数据段?
- 区分读写权限: 数据是可读写的,而代码段是只读的,防止代码段被恶意修改
- 代码段的复用: 内存中加载的同一程序的多个实例,或者不同程序中引用相同库的情况下,它们可以共享相同的代码段。这种共享可以节省系统内存,并提高程序的执行效率
对于第二点,我们可以举个例子看看, 编译并生成两个相同的可执行文件, 然后分别gdb 进去,看两者内存的映射
hrp@ubuntu:~$ cat test.c
// test.c
#include <stdio.h>
void foo() {
printf("Hello from foo()\n");
}
int main() {
foo();
return 0;
}
hrp@ubuntu:~$ gcc -g test.c -o test2
hrp@ubuntu:~$ gcc -g test.c -o test1
可以发现两个运行者的程序共用一段内存地址
但这理由证据还不够充分,怎么知道 这就是代码段的映射的内存呢?
于是我去 /proc/pid/maps 看这个程序的内存映射; 地址和gdb 看的时一样的, 但多了个信息 r-xp ,其中 x 标志可执行,也就说明这个内存区间是存放代码段,且运行两个程序,都是相同的内存地址,所以是 同一程序的多个实例共享 同一个代码段的!
hrp@ubuntu:~$ cat /proc/3915/maps
555555554000-555555555000 r-xp 00000000 08:01 1102662 /home/hrp/test1
555555754000-555555755000 r--p 00000000 08:01 1102662 /home/hrp/test1
555555755000-555555756000 rw-p 00001000 08:01 1102662 /home/hrp/test1
...
深入 .o 文件组成
都有那些组成
从objdump 反汇编看( -h 选项是 打印出这个目标文件的section 内容)
hrp@ubuntu:~$ objdump -h hello.o
hello.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000013 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 00000054 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 0000000000000000 0000000000000000 00000058 2**2
ALLOC
3 .rodata 0000000e 0000000000000000 0000000000000000 00000058 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 0000002a 0000000000000000 0000000000000000 00000066 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000090 2**0
CONTENTS, READONLY
6 .eh_frame 00000038 0000000000000000 0000000000000000 00000090 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
hrp@ubuntu:~$
除了刚才说的 三个 section,还有多了好几个section 名字
.rodata:包含只读数据的段.comment:包含注释的节.note.GNU-stack:用于控制栈的属性的节- .eh\_frame\`:包含异常处理信息的节
这里偏移量是从 0x40开始的,那0~0x40是什么呢?
对于elf 格式的文件,都有个elf 头,用 描述这个elf 的基本信息,elf 头大小是固定的, 也可以通过 readelf -h 查看(后面再解释elf )
hrp@ubuntu:~$ readelf -h hello.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
...
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
//64字节
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 12
所以现在的elf 结构是
接下来以 “ 看得见” 的方式介绍 各个 段:
.text
//待更新