总结(二)

编译和链接

源程序变成可执行程序的过程

源程序 hello.c
预编译:

  1. 将所有#define展开
  2. 处理#if #endif 等宏
  3. 删除注释

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中

参照课本的解释:

  1. 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
  2. e_type ELF文件类型,此处为可重定位文件(REL) 还有ET_EXEC 和 ET_DYN
  3. e_machine 机器的CPU平台属性
  4. e_version ELF版本号, 一般1
  5. e_entry 程序入口地址
  6. e_phoff
  7. e_shoff 段表在文件中的偏移
  8. e_flags 标志位,用来标志平台相关属性
  9. e_ehsize 文件头大小
  10. e_phentsize
  11. e_phnum
  12. e_shentsize段表描述符大小,表头大小
  13. e_shnum段表表头的数量
  14. e_shstrndx段表的字符串表在段表中的位置下表

段表

查看段表内容:

头文件/usr/include/elf.h中的段表的结构

  1. sh_name段名
  2. sh_type段类型
  3. sh_flags段的标志位
  4. sh_addr段的虚拟地址
  5. sh_offset段在文件中的偏移
  6. sh_size段的长度
  7. sh_link段的链接信息
  8. sh_info
  9. sh_addralign对齐方式
  10. 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库),那么就是用外部的定义,否则使用本地定义的弱符号。所以可以通过这种方式来解决这类问题。
弱引用
这个是一个坑,书中的例子并没有跑起来。
通过声明弱引用,可以让没有定义的函数符号在链接时并不会报错,如果外部有定义,那么就会连接到外部定义,功能可以正常使用,如果没有定义,程序也不会出错,实现可以扩展的功能。

书中的一段描述:

这种弱符号和弱引用对于库来说十分有用,库中的弱符号可以被用户的强符号所覆盖,从而使程序可以使用自定义的库函数;或者程序的扩展模块使用弱引用,将扩展模块和程序连接时,模块可以正常使用,如果去掉功能模块,程序也可以正常链接。

posted @ 2018-06-06 23:48  可达龙  阅读(265)  评论(0编辑  收藏  举报