总结(二)
编译和链接
源程序变成可执行程序的过程
源程序 hello.c
预编译:
- 将所有#define展开
- 处理#if #endif 等宏
- 删除注释
gcc -E hello.c -o hello.i
将源程序编译成hello.i
编译:(编译原理内容)
gcc -S hello.i -o hello.s
经历词法分析、语法分析、语义分析及优化产生汇编代码
汇编:
汇编器将汇编代码转变成机器码
gcc -c hello.s -o hello.o
链接
gcc -static --verbose hello.c
/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/collect2 --build-id --no-add-needed --hash-style=gnu -m elf_x86_64 -static /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbeginT.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../.. /tmp/ccbaCOfD.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crtn.o
删除路径后就是这样
collect2 --build-id --no-add-needed --hash-style=gnu -m elf_x86_64 -static crt1.o crti.o crtbeginT.o ccbaCOfD.o(hello.o) --start-group -lgcc -lgcc_eh -lc --end-group crtend.o crtn.o
连接过程主要包括地址和空间的分配、符号决议和重定位等步骤。
目标文件的格式
可执行文件文件格式:
Windows的PE(Portable Executable)和Linux下的ELF(Executable Linkable Format)
ELF文件
包含可重定位文件,可执行文件,共享目标文件,核心转储文件
Linux可以使用file命令查看文件类型信息
ELF文件的内容
首先,包含编译后的机器指令代码、数据。其次包含链接时需要的一些信息。
目标文件的信息通常以Section来存储,在链接的角度上,Section是将相同的类型的信息聚合在一块儿。比如说代码段(Code Section),数据段(Data Section)
为了减少内存的损耗和浪费目标文件的信息,从装载的角度上来看,目标文件的信息通常使用Segment来及进行储存,Segment是以可执行文件信息的权限进行分配,rwx这样的权限。(后面章节的内容)
目标文件的例子:
/**
* SimpleSection.c
*/
int printf(const char* format, ...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i) {
printf("%d\n", i);
}
int main() {
static int static_var = 1;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
使用编译选项编译成SimpleSection.o gcc -c SimpleSection.c
使用objdump工具-h选项来查看elf文件查看基本信息
其中包含的几个Section有.data数据段 .text代码段 .bss未初始化的全局变量和局部静态变量 .rodata只读数据段 .comment注释段 .note.GUN-stack堆栈提示段
上面的表头的意思分别是,该段所在的段表的下表,该段名称,段大小,虚拟地址(Virtual Memory Address),装载地址(Load Memory Address), 文件偏移大小, 对齐大小。
其中每个段(Section)的第二行对应着每个段(Section)属性。
代码段
其中存放着程序运行的指令信息,可以通过objdump -d 反汇编查看,机器指令信息对应的汇编语言代码
数据段和只读数据段
数据段存放着两个数,看了一下自己的电脑是小端模式,就是低位存放在低地址中,所以两个数应该是 0x00000054 = 84 0x00000001 = 1,正是全局变量和静态变量的值
只读数据段中只放了字符串常量 "%d\n" 对应着 25 64 0a 00
BSS段
存放着未初始化的全局变量和局部静态变量,对于未初始化的全局静态变量有些会放在bss段中,有些则当作符号来处理,只保留一个未定义的全局变量的符号。
Tip: static int x1 = 0; 和 static int x2 = 1;许多编译器将x1当作为未初始化的变量存储的bss中
文件结构
文件头
文件头包含着描述整个文件的基本属性:
ELF版本,目标机器型号,程序入口地址等,还有其中最重要的段表
查看ELF文件头的内容readelf -h SimpleSection.o
其中关于ELF头文件结构的结构体定义在/usr/include/elf.h中
参照课本的解释:
- Magic前16个字节 -- e_ident[EI_NIENT]
前四个字节 0x7f(DEL) 45(E) 4c(L) 46(F) 表示是ELF
第五个字节 00 表示无效文件 | 01 表示32位 | 02表示64位
第六个字节 00 表示无效 | 01 表示小端 | 02 表示大端
第七个字节 表示ELF的主版本号
后面的9个字节是扩展字节,没有定义,填0 - e_type ELF文件类型,此处为可重定位文件(REL) 还有ET_EXEC 和 ET_DYN
- e_machine 机器的CPU平台属性
- e_version ELF版本号, 一般1
- e_entry 程序入口地址
- e_phoff
- e_shoff 段表在文件中的偏移
- e_flags 标志位,用来标志平台相关属性
- e_ehsize 文件头大小
- e_phentsize
- e_phnum
- e_shentsize段表描述符大小,表头大小
- e_shnum段表表头的数量
- e_shstrndx段表的字符串表在段表中的位置下表
段表
查看段表内容:
头文件/usr/include/elf.h中的段表的结构
- sh_name段名
- sh_type段类型
- sh_flags段的标志位
- sh_addr段的虚拟地址
- sh_offset段在文件中的偏移
- sh_size段的长度
- sh_link段的链接信息
- sh_info
- sh_addralign对齐方式
- sh_entsize项的长度,0表示没有固定长度
这里有几个表:
段类型:
段标志:
系统保留段的属性一览:
段的链接信息:(后面章节)
符号--链接的接口
SimpleSection.o的符号
符号表的结构也储存在elf.h的头文件里面
符号绑定信息
- 0 STB_LOCAL 局部符号外部不可见
- 1 STB_GLOBAL 全局符号外部可见
- 2 STB_WEAK 弱符号
符号类型
NOTYPE 未知类型
FILE 文件名
SECTION 段
OBJECT 数据对象
FUNC 函数或可执行代码
符号所在段
ABS 表示绝对值,比如文件名
COM 比如未初始化的全局变量,COMMON块
UNDEF 未定义
特殊符号
在链接过程中产生的符号,比如ld链接器产生的符号end edata等
C与C++的符号修饰和函数签名
C++支持函数重载,其理由是因为在编译过程中处理了符号冲突的发生,其解决办法是对符号进行修饰,按照一定规则对函数名进行改写,包含信息有所述命名空间所属类其参数类型与个数等等
例如在gcc下 int func(int) 改写后的名称为 _Z4funci,各个编译器的修饰方式不同。
使用宏条件判断来处理C与C++的许多兼容性问题。
强符号和弱符号
当多个文件中包含相同名字的全局符号定义时,那么在目标文件链接的时候会出现符号重复定义的错误。
对于C/C++语言来说,初始化的全局变量和函数是强符号,未初始化的全局变量时弱符号。
对于强弱符号,链接器会应用一下原则:
- 不允许强符号多次定义;如果有强符号的多次定义,链接器会报错。
- 如果一个符号在目标文件中的强符号,其他文件中都是弱符号,那么链接器使用强符号定义。
- 如果所有文件中的符号都是弱符号,那么链接器选择使用空间最大的那一个。
Tips: 尽量不要使用不同类型的弱符号定义。
弱符号的例子
#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("multi-thread version!\n");
} else {
printf("single-thread version!\n");
}
return 0;
}
对于声明的pthread_create的弱符号类型, 如果外部的pthread_create强符号被定义(链接到lpthread库),那么就是用外部的定义,否则使用本地定义的弱符号。所以可以通过这种方式来解决这类问题。
弱引用
这个是一个坑,书中的例子并没有跑起来。
通过声明弱引用,可以让没有定义的函数符号在链接时并不会报错,如果外部有定义,那么就会连接到外部定义,功能可以正常使用,如果没有定义,程序也不会出错,实现可以扩展的功能。
书中的一段描述:
这种弱符号和弱引用对于库来说十分有用,库中的弱符号可以被用户的强符号所覆盖,从而使程序可以使用自定义的库函数;或者程序的扩展模块使用弱引用,将扩展模块和程序连接时,模块可以正常使用,如果去掉功能模块,程序也可以正常链接。