第3章 目标文件里有啥
目标文件格式
ELF文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件(Relocatable File) | 待链接的目标文件,包括静态链接库 | .o, .obj |
可执行文件 | 可以直接执行的程序 | /bin/bash文件,.exe |
共享目标文件 | 共享库 | .so, DLL |
核心转储文件 | 当进程意外终止时,系统可以把该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件 | core dump |
目标文件内部是怎样的
代码编译后生成的机器指令通常被放在代码段
全局变量和局部静态变量等常量数据放在数据段
ELF文件头: 描述了文件属性,是否可执行,静态还是动态,入口地址,段表(描述各个段的数组,记录段的属性)
bss段: 某些未初始化的数据放在这个段里,而不是放在data段,因为没初始化默认是0,还要给他们分配内存没必要,只需要标记一下,为他们预留位置
总体来说,程序源代码被编译以后主要分为两种段:指令和数据。代码段属于指令,数据段和.bss术语数据。
分段的好处
代码是只读,数据可读可写,分区映射到不同的内存区,避免相互篡改
CPU缓存命中率提高
存在多个程序副本时,大家可以共用一块指令区,只保证数据区不一样,节省内存
对于代码文件SimpleSection.c
/*
SimpleSection.c
Linux:
gcc -c SimpleSection.c
*/
int printf(const char *fmt, ...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i)
{
printf("%d\n", i);
}
int main(void)
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
我们使用指令
$ gcc -c SimpleSection.c
-c 表示只编译不链接
得到一个.o文件
继续
$ objdump -h SimpleSection.o
-h 查看段的基本信息
SimpleSection.o: file format mach-o arm64
Sections:
Idx Name Size VMA Type
0 __text 00000088 0000000000000000 TEXT
1 __data 00000008 0000000000000088 DATA
2 __cstring 00000004 0000000000000090 DATA
3 __bss 00000004 00000000000000d8 BSS
4 __compact_unwind 00000040 0000000000000098 DATA
图文无关,仅供参考,如果对应上面运行在macOS上的代码,请使用MachOView查看,点击左上角RVA选项,查看地址
代码段
objdump -s -d SimpleSection.o
-s 把段内容以16进制打印
-d 将包含指令的段反汇编
SimpleSection.o: file format mach-o arm64
Contents of section __text:
0000 ff8300d1 fd7b01a9 fd430091 a0c31fb8 .....{...C......
0010 a8c35fb8 e10308aa 00000090 00000091 .._.............
0020 e9030091 210100f9 00000094 fd7b41a9 ....!........{A.
0030 ff830091 c0035fd6 ff8300d1 fd7b01a9 ......_......{..
0040 fd430091 bfc31fb8 28008052 e80b00b9 .C......(..R....
0050 09000090 280140b9 09000090 2a0140b9 ....(.@.....*.@.
0060 08010a0b ea0b40b9 08010a0b ea0740b9 ......@.......@.
0070 00010a0b 00000094 e00b40b9 fd7b41a9 ..........@..{A.
0080 ff830091 c0035fd6 ......_.
Contents of section __data:
0088 54000000 55000000 T...U...
Contents of section __cstring:
0090 25640a00 %d..
Contents of section __bss:
<skipping contents of bss section at [00d8, 00dc)>
Contents of section __compact_unwind:
0098 00000000 00000000 38000000 00000004 ........8.......
00a8 00000000 00000000 00000000 00000000 ................
00b8 38000000 00000000 50000000 00000004 8.......P.......
00c8 00000000 00000000 00000000 00000000 ................
Disassembly of section __TEXT,__text:
0000000000000000 <ltmp0>:
0: ff 83 00 d1 sub sp, sp, #32
4: fd 7b 01 a9 stp x29, x30, [sp, #16]
8: fd 43 00 91 add x29, sp, #16
c: a0 c3 1f b8 stur w0, [x29, #-4]
10: a8 c3 5f b8 ldur w8, [x29, #-4]
14: e1 03 08 aa mov x1, x8
18: 00 00 00 90 adrp x0, #0
1c: 00 00 00 91 add x0, x0, #0
20: e9 03 00 91 mov x9, sp
24: 21 01 00 f9 str x1, [x9]
28: 00 00 00 94 bl 0x28 <ltmp0+0x28>
2c: fd 7b 41 a9 ldp x29, x30, [sp, #16]
30: ff 83 00 91 add sp, sp, #32
34: c0 03 5f d6 ret
0000000000000038 <_main>:
38: ff 83 00 d1 sub sp, sp, #32
3c: fd 7b 01 a9 stp x29, x30, [sp, #16]
40: fd 43 00 91 add x29, sp, #16
44: bf c3 1f b8 stur wzr, [x29, #-4]
48: 28 00 80 52 mov w8, #1
4c: e8 0b 00 b9 str w8, [sp, #8]
50: 09 00 00 90 adrp x9, #0
54: 28 01 40 b9 ldr w8, [x9]
58: 09 00 00 90 adrp x9, #0
5c: 2a 01 40 b9 ldr w10, [x9]
60: 08 01 0a 0b add w8, w8, w10
64: ea 0b 40 b9 ldr w10, [sp, #8]
68: 08 01 0a 0b add w8, w8, w10
6c: ea 07 40 b9 ldr w10, [sp, #4]
70: 00 01 0a 0b add w0, w8, w10
74: 00 00 00 94 bl 0x74 <_main+0x3c>
78: e0 0b 40 b9 ldr w0, [sp, #8]
7c: fd 7b 41 a9 ldp x29, x30, [sp, #16]
80: ff 83 00 91 add sp, sp, #32
84: c0 03 5f d6 ret
可以看到ltmp0的第一个ff 83 00对应代码段开头是一样的,结尾的ret对应的5f d6也可以对应到上面代码段的结尾。
数据段和只读数据段
Contents of section __data:
0088 54000000 55000000 T...U...
这里可以看到data段就是8个字节,一个global_init_var和一个static_var各占4个字节,分别对应的值0x54和0x55换成10进制就是84和85,采用小端存储
BSS段
存放未初始化的全局变量和局部变量,有些变量并没有放在.bss段。编译器只预留了一个未定义的全局符号,等到链接时再在.bss段分配空间。
其他段
ELF文件结构描述
ELF文件头中定义了ELF魔数、文件机器字节长度、数据存储方式等等信息
ELF魔数 包含ELF字长(32/64为)、字节序(大小端)、ELF文件版本(一般是1)
文件类型 .o文件,可执行文件,共享文件比如.so
机器类型 运行在什么平台下,比如EM_386表示运行Intel x86
段表 (Section Header Table)
描述了name,type,flags,addr,offset等段的相关信息
段的类型 表示是程序段(代码段、数据段)还是符号表等等
段的标志位 表示该断在进程虚拟地址空间的属性,比如是否可写,可执行。
段的链接信息 如果是段的类型跟链接相关,比如重定位表、符号表
重定位表
链接器在处理目标文件时,需要对目标文件进行重定位,即代码段和数据段中那些对绝对地址的引用,这些都记录在重定位表里面.
字符串表
通过字符串表和偏移来查找对应的字符串
链接的接口-符号
在链接中,我们把函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)
每一个目标文件都会有一个相应的符号表(Symbol Table)
每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对应变量和函数来说,符号值就是它们的地址。
符号表类型
- 定义在本目标文件的全局符号
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,一般叫做外部符号(External Symbol)
- 段名称,如.text,.data等
- 局部符号
- 行号信息
符号表结构,通常是一个结构体数组,每个结构体存储一个符号,里面包含名称,对应的值,类型(数据,函数,段,文件名),所属的段,符号绑定信息(局部,全局,弱引用)
特殊符号:链接器脚本中定义的一些符号,可以再代码中声明并且使用
函数签名:包含了一个函数的信息,包括名称,参数类型,所在类和名称空间等。用于识别不同的函数。
名称修饰:编译器在将源码编译成目标文件时,会将函数和变量的名字进行修饰,形成符号名。
extern "C"
C++为了与C兼容,在符号的管理上,添加了一个声明或定义一个C的符号的关键字用法。C++编译器会将在extern "C"
的大括号内部的代码当做C语言代码处理,因为C语言的符号修饰规则和C++不一样,所以最终生成的符号是C的风格而非C++风格。
弱符号和强符号
对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。
- 不允许强符号被多次定义
- 一个符号在某个目标文件中是强符号,在其他文件都是弱符号,那么选择强符号
- 如果一个符号在所有目标文件中都是弱符号,那么选择占用空间最大的一个
强引用: 如果符号引用在目标文件被最终链接时,没有找到该符号的定义,没有被正确决议,链接器就会报符号未定义错误,这种被称为强引用(Strong Reference)
弱引用(Weak Reference) 如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。一般对于未定义的弱引用,链接器默认其为0
弱符号和弱引用对于库来说十分有用,可以用来裁剪和组合某些功能,比如如下代码,用来决定是否有多线程,在链接时是否连接Glib库(是否在编译时有-lpthread选项)
#include <stdio.h>
#include <pthread.h>
int pthread_create(pthread_t *, const pthread_attr_t *, void* (*)(void*), void*) __attribute__((weak));
int main()
{
if (pthread_create) {
printf("This is multi-thread version!\n");
} else {
printf("This is single-thread version!\n");
}
}
编译运行
$ gcc pthread.c -o pt
$ ./pt
This is single-thread version!
$ gcc pthread.c -lpthread -o pt
$ ./pt
This is multi-thread version!
调试信息
现在编译器几乎都支持源代码级别的调试,这会在目标文件中添加很多debug相关的段,保存了目标代码地址和源代码中的哪一行、函数和变量的类型、结构体定义等相关的信息。
现在的ELF文件采用一个叫DWARF(Debug With Arbitrary Record Format)的标准的调试信息格式,一般发布的时候需要裁剪掉它