高效编程技巧:编译器的核心原理与应用

更新:11-17 神话故事 我要投稿 纠错 投诉

大家好,今天给各位分享高效编程技巧:编译器的核心原理与应用的一些知识,其中也会对进行解释,文章篇幅可能偏长,如果能碰巧解决你现在面临的问题,别忘了关注本站,现在就马上开始吧!

内核版本:Linux版本4.18.0-25-generic

GNU GCC版本:gcc版本7.4.0(Ubuntu 7.4.0-1ubuntu1~18.04.1)

C 标准修订版:C11GNU Compiled BYGMP 版本:6.1.2MPFR 版本:4.0.1MPC 版本:1.1.0isl 版本:isl-0.19-GMPGNU 汇编程序版本:2.30 (x86_64-linux-gnu) 使用BFD 版本(GNU Binutils for Ubuntu) )2.30

链接器版本:

collect2版本:7.4.0gcc一般是collect2,而不是ld。 collect2 是ld 链接器的一个包。最后调用ld来完成链接工作。 collect2通过第一次链接程序检查链接器输出文件,以查找具有特定名称的构造函数。程序collect2 的工作原理是链接该程序一次并在链接器输出文件中查找符号。具有表明它们是构造函数的特定名称。如果找到任何文件,它会创建一个新的临时“.c”文件,其中包含它们的表,对其进行编译,并再次链接该程序,包括该文件。) GNU ld(GNU Binutils for Ubuntu):2.30

二、GCC编译过程

:010 -1010 编译过程-simple.png预处理

删除所有#define,展开所有宏定义,处理所有条件预编译指令#if、#endif、#ifdef、#ifndef、#elif、#else,处理#include预编译指令,并将包含文件插入到include位置(递归地)删除所有注释添加行号和文件名标识符(调试时使用) 保留所有#pragma 编译器指令(编译器需要使用这些指令) # 单独生成预处理文件(本模块假设hello.c 为源代码程序,hello.i是hello.c的预处理文件,hello.s是hello.c的编译文件,hello.o是hello.c的编译文件,hello是hello.c可执行程序的最终文件)

# 使用gcc命令生成预处理文件

$ gcc -E 你好.c -o 你好.i

# 使用cpp命令生成预处理文件

$ cpp hello.c hello.i编译:预处理后的文件经过一系列词法分析、语法分析、语义分析、中间代码生成、目标代码生成和优化,生成相应的汇编代码文件。

词法分析:扫描器运行类似于有限状态机的算法,将代码的字符序列拆分为一系列标记。语法分析:语法分析器对扫描器生成的标记进行语法分析,生成语法树(以表达式为节点的树)。 Tree)语义分析:语义分析器决定语句的含义(例如两个指针相乘是没有意义的),而编译器只能分析静态语义(编译时就可以确定的语义,通常包括声明和类型匹配,类型转换;相反,动态语义是可以在运行时确定的语义(例如,使用0作为除数是运行时语义错误) # 编译预处理文件,生成汇编代码文件。

$ gcc -S 你好.i -o 你好.s

#编译源文件生成汇编代码文件

$ gcc -S 你好.c -o 你好.s

# 目前的gcc编译器将预处理和编译两个步骤合并为一步,使用一个名为cc1的程序来完成这一过程

$ /usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o hello.s 汇编:将汇编代码转换成机器可以执行的指令(根据汇编指令对照表一一翻译和机器指令)

# 使用as处理汇编文件生成目标文件

$ 作为hello.s -o hello.o

# 使用gcc处理汇编文件生成目标文件

$ gcc -c 你好.s -o 你好.o

# 使用gcc处理源文件生成目标文件

$ gcc -c hello.c -o hello.o link:将目标文件链接在一起形成可执行文件,主要包括地址和空间分配、符号解析和重定位步骤。

符号解析:也称为符号绑定、名称绑定、名称解析等。从细节上看,解析更倾向于静态链接,绑定更倾向于动态链接。

重定位:编译文件时,如果不知道要调用的函数或需要操作的变量的地址,则调用该函数或操作该变量的指令的目标地址将被搁置,而链接器会等到最后的链接才传输这些指令。目标地址修正,这个地址修正过程也称为第二次链接,每个需要修正的地方都称为重定位

2.1 GCC编译过程

使用下面的例子,包含两个源文件hello.c和func.c(后面分析会用到这两个文件)

/* hello.c:主测试程序,包括全局静态变量、局部静态变量、全局变量、局部变量、基本函数调用*/

//导出变量

外部intexport_func_var;

//全局变量

int global_uninit_var;

int global_init_var_0=0;

int global_init_var_1=1;

//常量变量

const char *const_string_var="const 字符串";

//静态全局变量

静态int static_global_uninit_var;

静态int static_global_init_var_0=0;

静态int static_global_init_var_1=1;

//函数头

无效func_call_test(int num);

int 主函数(无效){

//本地变量

int local_uninit_var;

int local_init_var_0=0;

int local_init_var_1=1;

//静态局部变量

静态int static_local_uninit_var;

静态int static_local_init_var_0=0;

静态int static_local_init_var_1=1;

//调用函数

func_call_test(8);

//导出变量操作

导出函数变量=导出函数变量* 2;

返回0;

}/* func.c: 包含一个简单的被调用函数和一个全局变量*/

int 导出函数变量=666;

无效func_call_test(int num){

int double_num=num * 2;

}使用gcc -v hello.c func.c 编译并生成可执行文件a.out,产生以下输出(简化版)

[delta@delta: 代码]$ gcc -v func.c hello.c

# func.c的预处理和编译过程

/usr/lib/gcc/x86_64-linux-gnu/7/cc1 func.c -o /tmp/ccfC6J5E.s

# 将func.c生成的.s文件进行汇编,生成二进制文件

作为-v --64 -o /tmp/ccF4Bar0.o /tmp/ccfC6J5E.s

# hello.c的预处理和编译过程

/usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o /tmp/ccfC6J5E.s

# 将hello.c生成的.s文件进行组装,生成二进制文件

作为-v --64 -o /tmp/cc7UmhQl.o /tmp/ccfC6J5E.s

# 链接过程

/usr/lib/gcc/x86_64-linux-gnu/7/collect2 -动态链接器ld-linux-x86-64.so.2 Scrt1.o crti.o crtbeginS.o /tmp/ccF4Bar0.o /tmp/cc7UmhQl .o crtendS.o crtn.o

2.2 实际编译过程

重定位入口目标文件的格式是什么?

多个目标如何联系在一起?

三、链接过程解析

3.1 目标文件

Windows 下的PE(可移植可执行文件) Linux 下的ELF(可执行可链接格式) 注意:

PE 和ELF 格式都是COFF(通用文件格式)格式的变体。目标文件的内容和结构与可执行文件类似,因此一般以相同的格式存储。从广义上讲,目标文件和可执行文件可以视为同一类型的文件。它们在Windows下统称为PE-COFF文件格式,在Linux下统称为ELF文件。可执行文件格式存储的不仅是可执行文件,还有动态链接库(DLL,Dynamic Linking Library)(Windows的.dll和Linux的.so)和静态链接库(Static Linking Library)(Windows的.lib和Linux的.so) .lib .a) 文件以可执行文件格式存储。 (静态链接库稍有不同,它把很多目标文件捆绑在一起形成一个文件,再加上一些索引,可以理解为包含很多目标文件的文件包)

3.1.1目标文件类型

ELF 文件类型描述示例可重复Relocatable File包含代码和数据,可用于链接到可执行文件或共享对象文件。静态链接库可以归为这一类。 Linux的.o、Window的.obj可执行文件(Executable File)包含可以直接执行的程序。一般来说,Linux的/bin/bash文件没有扩展名。 Windows的.exe共享对象文件(Shared Object File)包含代码和数据。链接器可以释放该文件和其他可重定位文件。与共享目标文件链接以生成新的目标文件;动态链接器可以将多个共享对象文件与可执行文件组合起来作为进程映像的一部分来运行Linux的.so、Windows的.dll核心转储文件(Core Dump File)进程意外终止,系统转储进程地址空间的内容并终止时的其他信息保存到核心转储文件中。 Linux下的核心转储

3.1.2 ELF文件类型

包含编译后的指令代码和数据。它还包括链接所需的一些信息(符号表、调试信息和字符串等)。一般目标文件根据不同的属性以Q:的形式存储这些信息(有时也称为节(Section))。如下图

ELF结构.png

3.1.3目标文件结构

段名描述.text/.code代码段、编译后的机器指令.data数据段、全局变量和局部静态变量.bss未初始化的全局变量和局部静态变量(.bss段只是预留空间未初始化的全局变量和局部静态变量)。rodata只读信息段。rodata1存储只读数据、字符串常量和全局const变量。同.rodata.comment编译器版本信息.debug调试信息.dynamic动态链接信息.hash符号哈希表.调试时的line行号表,即源代码行号与编译指令的对应表.note额外的编译器信息。程序的公司名称,发布版本号。strtabString表,字符串表,用于存放ELF文件中使用的各种字符串。symtab符号表,符号表。shstrtab节字符串表,节名表.plt/.got动态链接跳转表和全局入口表.init/.fini程序初始化和终止代码段

3.1.3.1常见的段

ELF文件头:

使用gcc -c hello.c -o hello.o 生成目标文件hello.o,使用readelf -h hello.o 读取目标文件的ELF 文件头。可以看到ELF文件头定义了段(Segment)等,如下图

ELF 头:

魔术: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00

:级ELF64

Data: 2 的补码,小端

版本: 1(当前)

OS/ABI: UNIX - 系统V

ABI 版本: 0

类型: REL(可重定位文件)

Machine: Advanced Micro Devices X86-64

版本:0x1

入口点地址:0x0

程序头开始: 0(字节到文件中)

节头开始: 1328(文件中的字节)

标志:0x0

该标头的大小: 64(字节)

程序头大小: 0(字节)

程序头数量: 0

节头大小: 64(字节)

节标题数量: 15

节头字符串表index: 14 ELF 文件头结构在/usr/include/elf.h 中定义。目标文件hello.o的文件头中机器字节长度为ELF64。找到64位版本文件头结构Elf64_Ehdr定义,如下所示

类型定义结构

{

无符号字符e_ident[EI_NIDENT]; /* 幻数和其他信息*/

Elf64_Half e_type; /* 目标文件类型*/

Elf64_半电子_机器; /* 建筑学*/

Elf64_Word e_版本; /* 目标文件版本*/

Elf64_Addr e_entry; /* 入口点虚拟地址*/

Elf64_Off e_phoff; /* 程序头表文件偏移*/

Elf64_Off e_shoff; /* 节头表文件偏移量*/

Elf64_Word e_flags; /* 处理器特定的标志*/

Elf64_Half e_ehsize; /* ELF 标头大小(以字节为单位)*/

Elf64_Half e_phentsize; /* 程序头表项大小*/

Elf64_Half e_phnum; /* 程序头表项计数*/

Elf64_Half e_sentsize; /* 节头表项大小*/

Elf64_Half e_shnum; /* 节头表条目计数*/

Elf64_Half e_shstrndx; /* 节头字符串表索引*/

Elf64_Ehdr;结构体中除了e_ident对应readelf输出的Magic to ABI Version部分外,其他都是一一对应的。

e_shstrndx变量代表.shstrtab在段表中的下标

段表

使用gcc -c hello.c -o hello.o 生成目标文件hello.o,使用readelf -S hello.o 读取目标文件的段表部分

节标题:

[编号] 名称类型地址偏移量

尺寸EntSize 标志链接信息对齐

[0] 空0000000000000000 00000000

0000000000000000 0000000000000000 0 0 0

[1] .text PROGBITS 0000000000000000 00000040

0000000000000035 0000000000000000 AX 0 0 1

[2] .rela.text RELA 0000000000000000 00000440

0000000000000048 0000000000000018 我12 1 8

[3] .data PROGBITS 0000000000000000 00000078

000000000000000c 0000000000000000 WA 0 0 4

[4] .bss 诺比特0000000000000000 00000084

0000000000000014 0000000000000000 西澳0 0 4

[5] .rodata PROGBITS 0000000000000000 00000084

000000000000000d 0000000000000000A 0 0 1

[6] .data.rel.local PROGBITS 0000000000000000 00000098

0000000000000008 0000000000000000 西澳0 0 8

[7] .rela.data.rel.lo RELA 0000000000000000 00000488

0000000000000018 0000000000000018 我12 6 8

[8] .comment PROGBITS 0000000000000000 000000a0

000000000000002c 0000000000000001 毫秒0 0 1

[9] .note.GNU-stack PROGBITS 0000000000000000 000000cc

0000000000000000 0000000000000000 0 0 1

[10] .eh_frame PROGBITS 0000000000000000 000000d0

0000000000000038 0000000000000000 A 0 0 8

[11] .rela.eh_frame RELA 0000000000000000 000004a0

0000000000000018 0000000000000018 我12 10 8

[12].symtab SYMTAB 0000000000000000 00000108

0000000000000240 0000000000000018 13 16 8

[13].strtab STRTAB 0000000000000000 00000348

00000000000000f6 0000000000000000 0 0 1

[14] .shstrtab STRTAB 0000000000000000 000004b8

0000000000000076 0000000000000000 0 0 1

标记的关键

s: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific)段表结构体定义在/usr/include/elf.h中,目标文件hello.o的文件头中机器字节长度为ELF64,找到64位版本段表结构体定义Elf64_Shdr(每个Elf64_Shdr对应一个段,Elf64_Shdr又称为段描述符),如下所示 typedef struct { Elf64_Word sh_name; /* Section name (string tbl index) */ Elf64_Word sh_type; /* Section type */ Elf64_Xword sh_flags; /* Section flags */ Elf64_Addr sh_addr; /* Section virtual addr at execution */ Elf64_Off sh_offset; /* Section file offset */ Elf64_Xword sh_size; /* Section size in bytes */ Elf64_Word sh_link; /* Link to another section */ Elf64_Word sh_info; /* Additional section information */ Elf64_Xword sh_addralign; /* Section alignment */ Elf64_Xword sh_entsize; /* Entry size if section holds table */ } Elf64_Shdr;Elf64_Shdr部分成员解释 变量名说明sh_name段名是一个字符串,位于一个叫.shstrtab的字符串表中,sh_name是段名字符串在.shstrtab中的偏移sh_addr段虚拟地址,如果该段可以加载,sh_addr为该段被加载后在进程地址空间的虚拟地址,否则为0sh_offset段偏移,如果该段存在于文件中则表示该段在文件中的偏移,否则无意义sh_link、sh_info段链接信息,如果该段的类型是与链接相关的,则该字段有意义sh_addralign段地址对齐,sh_addralign表示是地址对齐数量的指数,如果sh_addralign为0或者1则该段没有字节对齐要求sh_entsize对于一些段包含了一些固定大小的项,比如符号表,则sh_entsize表示每个项的大小重定位表:hello.o中包含一个.rela.text的段,类型为RELA,它是一个重定位表。链接器在处理目标文件时必须对文件中的某些部位进行重定位,这些重定位信息都记录在重定位表中。对于每个需要重定位的代码段或者数据段,都会有一个相应的重定位表。字符串表 .strtab:字符串表,保存普通的字符串,比如符号的名字 .shstrtab:段表字符串表,保存段表中用到的字符串,比如段名结论:ELF文件头中的e_shstrndx变量表示.shstrtab在段表中的下标,e_shoff表示段表在文件中的偏移,只有解析ELF文件头,就可以得到段表和段表字符串表的位置,从而解析整个ELF文件

3.1.4 链接的接口——符号

3.1.4.1 符号定义
定义:在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量地址的引用。在链接中,将函数和变量统称为符号(Symbol),函数名或变量名称为符号名(Symbol Name)。 每个目标文件都有一个符号表记录了目标文件中用到的所有符号(每个定义的符号都有一个符号值,对于函数和变量来说,符号值就是它们的地址),常见分类如下 符号类型说明定义在本目标文件中的全局符号可以被其它目标文件引用的符号在本目标文件中引用的符号,却没有定义在本目标文件中外部符号(External Symbol)段名,由编译器产生它的值就是该段的起始地址局部符号只在编译单元内部可见,链接器往往忽略它们行号信息目标文件指令与代码行的对应关系,可选
3.1.4.2 符号结构分析
符号表结构:符号表结构体定义在/usr/include/elf.h中,如下所示 typedef struct { Elf64_Word st_name; /* Symbol name (string tbl index) */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf64_Section st_shndx; /* Section index */ Elf64_Addr st_value; /* Symbol value */ Elf64_Xword st_size; /* Symbol size */ } Elf64_Sym;Elf64_Sym成员解释 变量名说明st_name符号名在字符串表中的下标st_info符号类型和绑定信息st_other符号可见性st_shndx符号所在的段st_value符号对应的值st_size符号大小使用gcc -c hello.c -o hello.o生成目标文件hello.o,并使用readelf -s hello.o读取目标文件的符号表部分 Symbol table ".symtab" contains 24 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 0 SECTION LOCAL DEFAULT 6 7: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 static_global_uninit_var 8: 0000000000000008 4 OBJECT LOCAL DEFAULT 4 static_global_init_var_0 9: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_global_init_var_1 10: 0000000000000008 4 OBJECT LOCAL DEFAULT 3 static_local_init_var_1.1 11: 000000000000000c 4 OBJECT LOCAL DEFAULT 4 static_local_init_var_0.1 12: 0000000000000010 4 OBJECT LOCAL DEFAULT 4 static_local_uninit_var.1 13: 0000000000000000 0 SECTION LOCAL DEFAULT 9 14: 0000000000000000 0 SECTION LOCAL DEFAULT 10 15: 0000000000000000 0 SECTION LOCAL DEFAULT 8 16: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var 17: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 global_init_var_0 18: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var_1 19: 0000000000000000 8 OBJECT GLOBAL DEFAULT 6 const_string_var 20: 0000000000000000 53 FUNC GLOBAL DEFAULT 1 main 21: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_ 22: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND func_call_test 23: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND export_func_var注: 1. static_global_uninit_var、static_local_init_var_0和static_local_uninit_var、static_global_init_var_0和global_init_var_0在bss段(因为初始化为0和不初始化是一样的) 2. static_global_init_var_1、static_local_init_var_1和global_init_var_1在data段(初始化的全局变量) 3. static变量的类型均为LOCAL,表明该符号只为该目标文件内部可见;非Static全局变量的类型为GLOBAL,表明该符号外部可见 4. 在hello.c中引用了func_call_test和export_func_var符号,但是没有定义,所以它的Ndx是UND(注:export一个变量但是并未使用则符号表中不会出现这个边浪符号信息;export一个不存在的变量但是并未使用编译不会报错;export一个不存在的变量并使用会报错<**注意系统环境**>) 5. 未初始化的全局非静态变量global_uninit_var在COM块中 6. const_string_var在.data.rel.local段中特殊符号:当使用链接器生成可执行文件时,会定义很多特殊的符号,这些符号并未在程序中定义,但是可以直接声明并引用它们
3.1.4.3 符号修饰与函数签名
​ 符号修饰与函数签名:在符号名前或者后面加上修饰符号,防止与库文件和其它目标文件冲突。现在的linux下的GCC编译器中,默认情况下去掉了加上这种方式,可以通过参数选项打开 C++符号修饰:C++拥有类,继承,重载和命名空间等这些特性,导致符号管理更为复杂。例如重载的情况:函数名相同但是参数不一样。然后就有了符号修饰和符号改编的机制,使用函数签名(包括函数名,参数类型,所在的类和命名空间等信息)来识别不同的函数 C++符号修饰栗子 class C { public: int func(int); class C2 { public: int func(int); }; }; namespace N { int func(int); class C { public: int func(int); }; } int func(int num){ return num; } float func(float num){ return num; } int C::func(int num){ return num; } int C::C2::func(int num){ return num; } int N::func(int num){ return num; } int N::C::func(int num){ return num; } int main(){ int int_res = func(1); float float_var = 1.1; float float_res = func(float_var); C class_C; int_res = class_C.func(1); return 0; }使用g++ -c hello.cpp -o hello_cpp.o编译产生目标文件hello_cpp.o,使用readelf -a hello_cpp.o查看目标文件中的符号表,如下 Symbol table ".symtab" contains 18 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hello.cpp 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 5: 0000000000000000 0 SECTION LOCAL DEFAULT 5 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 7: 0000000000000000 0 SECTION LOCAL DEFAULT 8 8: 0000000000000000 0 SECTION LOCAL DEFAULT 6 9: 0000000000000000 12 FUNC GLOBAL DEFAULT 1 _Z4funci 10: 000000000000000c 16 FUNC GLOBAL DEFAULT 1 _Z4funcf 11: 000000000000001c 16 FUNC GLOBAL DEFAULT 1 _ZN1C4funcEi 12: 000000000000002c 16 FUNC GLOBAL DEFAULT 1 _ZN1C2C24funcEi 13: 000000000000003c 12 FUNC GLOBAL DEFAULT 1 _ZN1N4funcEi 14: 0000000000000048 16 FUNC GLOBAL DEFAULT 1 _ZN1N1C4funcEi 15: 0000000000000058 119 FUNC GLOBAL DEFAULT 1 main

16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_ 17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __stack_chk_fail可以看出函数签名与修饰后的名称的对应关系 函数签名修饰后名称(符号名)int func(int)_Z4funcifloat func(float)_Z4funcfint C::func(int)_ZN1C4funcEiint C::C2::func(int)_ZN1C2C24funcEiint N::func(int)_ZN1N4funcEiint N::C::func(int)_ZN1N1C4funcEiextern “C”:C++编译器会将在extern C大括号内的内部代码当做C语言代码处理,也就是名称修饰机制将不会起作用。当需要兼容C和C++,例如在C++代码中调用C中的memset函数,可以使用C++的宏__cplusplus,C++在编译程序时会默认定义这个宏 #ifdef __cplusplus extern “C” { #endif void *memset(void *, int, size_t); #ifdef __cplusplus } #endif由于不同的编译器采用不同的名字修饰方法,必然会导致不同编译器产生的目标文件无法正常互相链接,这是导致不同编译器之间不能互操作的原因
3.1.4.4 弱符号与强符号
​ 在编程中经常遇到符号重定义的问题,例如hello.c和func.c都定义了一个_global并将它们都初始化,在编译时就会报错。对于C/C++来说,编译器默认函数和初始化的全局变量为强符号,未初始化的全局变量为弱符号。编译器处理符号规则不允许强符号被多次定义如果一个符号在一个文件中是强符号,在其它文件中是弱符号,则选择强符号如果一个符号在所有的文件中都是弱符号,则选择其中占用空间最大的一个(int型和double型会选择double型)弱引用与强引用:对外部目标文件中的符号引用在目标文件最终被链接成可执行文件时都哟啊被正确决议,如果没有找到该符号的定义,则会报未定义错误,这种被称为强引用;与之对应的弱引用,在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器也不会报错。弱符号与弱引用的作用(对库来说很有用) 库中定义的弱符号可以被用户定义的强符号所覆盖,从而使程序可以使用自定义版本的函数程序可以对某些扩展功能模块的引用定义为弱引用,当扩展模块与程序链接到一起时,功能模块可以正常使用;如果去掉了某些功能模块,则程序也可以正常链接,只是缺少了相应的功能,这使得程序的功能更容易裁剪和组合

3.2 静态链接

3.2.1 空间和地址分配

链接器在合并多个目标文件的段时,采用相似段合并的方式,并分配地址和空间(虚拟地址空间的分配)两步链接法:空间和地址分配:扫描所有的目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表,这一步中,链接器将能够获得所有输入目标文件的段长度,并将它们合并,计算输出文件中各个合并之后的段的长度,建立映射关系。符号解析与重定位:使用空间和地址分配中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。当进行了空间和地址分配之后,各个段的虚拟地址也就确定了,由于各个符号在段内的位置是相对的,所以各个符号的地址也就确定了。

3.2.2 符号解析与重定位

使用gcc -c hello.c -o hello.o生成目标文件hello.o,并使用objdump -d hello.o读取目标文件的.text的反汇编结果,如下所示(简略部分内容);同理使用gcc -c func.c -o func.o生成目标文件func.o。 [delta@rabbit: c_code ]$ objdump -d hello.o Disassembly of section .text: 0000000000000000: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 10 sub $0x10,%rsp 8: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp) f: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp) 16: bf 08 00 00 00 mov $0x8,%edi 1b: e8 00 00 00 00 callq 2020: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 2626: 01 c0 add %eax,%eax 28: 89 05 00 00 00 00 mov %eax,0x0(%rip) # 2e2e: b8 00 00 00 00 mov $0x0,%eax 33: c9 leaveq 34: c3 retq分析:由以上结果可以看出,在链接之前,main函数在调用func_call_test函数时,使用的地址是0x00000000,根据反汇编结果就是下一条指令(e8 00 00 00 00之中e8是callq的指令码,00 00 00 00是目的地址相对于下一条指令的偏移量);在使用export_func_var变量时,编译器就将0x0看做是export_func_var的地址 使用ld hello.o func.o -e main链接两个目标文件,生成可执行文件a.out(并不能执行,因为缺少部分目标文件,但是符号已经被重新定位;-e main表示将main函数作为程序入口),使用objdump -d a.out查看a.out的.text段反汇编结果,如下图所示(简略部分内容) [delta@rabbit: c_code ]$ objdump -d a.out Disassembly of section .text: 00000000004000e8: 4000e8: 55 push %rbp 4000e9: 48 89 e5 mov %rsp,%rbp 4000ec: 48 83 ec 10 sub $0x10,%rsp 4000f0: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp) 4000f7: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp) 4000fe: bf 08 00 00 00 mov $0x8,%edi 400103: e8 15 00 00 00 callq 40011d400108: 8b 05 0a 0f 20 00 mov 0x200f0a(%rip),%eax # 60101840010e: 01 c0 add %eax,%eax 400110: 89 05 02 0f 20 00 mov %eax,0x200f02(%rip) # 601018400116: b8 00 00 00 00 mov $0x0,%eax 40011b: c9 leaveq 40011c: c3 retq 000000000040011d: 40011d: 55 push %rbp 40011e: 48 89 e5 mov %rsp,%rbp 400121: 89 7d ec mov %edi,-0x14(%rbp) 400124: 8b 45 ec mov -0x14(%rbp),%eax 400127: 01 c0 add %eax,%eax 400129: 89 45 fc mov %eax,-0x4(%rbp) 40012c: 90 nop 40012d: 5d pop %rbp 40012e: c3 retq使用nm a.out查看a.out中的符号信息(简略),可以看到export_func_var的地址为0000000000601018 [delta@rabbit: c_code ]$ nm a.out 0000000000601018 D export_func_var分析:在链接之后,可以从反汇编中看出main函数的调用func_call_test函数的地方地址已经被修正为func_call_test真正的地址000000000040011d,使用export_func_var变量的地方的地址也修正为export_func_var真正的地址0000000000601018(在nm a.out输出的符号表中)。所以链接器在完成地址空间分配之后就可以确定所有符号的虚拟地址了,链接器就可以根据符号的地址对每个需要重定位的地方进行地址修正。链接器如何知道哪些地址需要修正呢?有一个重定位表的结构专门保存与重定位相关的信息(比如.text如果有需要重定位的地方,那么就会有一个叫.rela.text的段保存了代码段的重定位信息),使用objdump -r hello.o查看重定位信息如下(简略),可以看到所有需要重定位的地方 [delta@rabbit: c_code ]$ objdump -r hello.o RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 000000000000001c R_X86_64_PLT32 func_call_test-0x0000000000000004 0000000000000022 R_X86_64_PC32 export_func_var-0x0000000000000004 000000000000002a R_X86_64_PC32 export_func_var-0x0000000000000004符号解析:使用nm hello.o可以查看hello.o 中所有的符号信息,如下所示,可以看到export_func_var和func_call_test符号都是未定义状态(U)。所以档链接器扫描完所有的输入目标文件之后,所有的这些未定义的符号都能够在全局符号表中找到,否则就会报符号未定义(undefined reference to)错误。 # 输出hello.o 中所有的符号信息 [delta@rabbit: c_code ]$ nm hello.o 0000000000000000 D const_string_var U export_func_var U func_call_test 0000000000000000 B global_init_var_0 0000000000000000 D global_init_var_1 U _GLOBAL_OFFSET_TABLE_ 0000000000000004 C global_uninit_var 0000000000000000 T main 0000000000000008 b static_global_init_var_0 0000000000000004 d static_global_init_var_1 0000000000000004 b static_global_uninit_var 000000000000000c b static_local_init_var_0.1809 0000000000000008 d static_local_init_var_1.1810 0000000000000010 b static_local_uninit_var.1808# 符号未定义错误 [delta@rabbit: c_code ]$ ld hello.o ld: warning: cannot find entry symbol _start; defaulting to 00000000004000e8 hello.o: In function `main": hello.c:(.text+0x1c): undefined reference to `func_call_test" hello.c:(.text+0x22): undefined reference to `export_func_var" hello.c:(.text+0x2a): undefined reference to `export_func_var"指令修正方式:A:保存正在修正位置的值;P:被修正的位置<相对于段开始的偏移量或者虚拟地址>;S:符号的实际地址;L:表示其索引位于重定位条目中的符号的值)以下计算参考 # hello.o中的重定位信息(简略) [delta@rabbit: c_code ]$ objdump -r hello.o RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 000000000000001c R_X86_64_PLT32 func_call_test-0x0000000000000004 0000000000000022 R_X86_64_PC32 export_func_var-0x0000000000000004 000000000000002a R_X86_64_PC32 export_func_var-0x0000000000000004 # 解析: # 根据输出符号的重定位类型有R_X86_64_PLT32和R_X86_64_PC32 # R_X86_64_PLT32 : L + A - P(绝对地址修正) # R_X86_64_PC32 : S + A - P(相对寻址修正) # 其它方式参考:http://www.ucw.cz/~hubicka/papers/abi/node19.html绝对地址修正:绝对地址修正后的地址为该符号的实际地址,例如调用func_call_test符号的地址被修正成为了绝对地址40011d相对地址修正:相对地址修正后的地址为符号距离被修正位置的地址差,例如使用export_func_var符号的地址被修正成为了相对地址0x200f0a,mov指令(第一个mov指令)的下一条地址40010e加上这个偏移量0x200f0a就是export_func_var的绝对地址0x601018COMMON块:根据nm hello.o的输出,如下所示(简略),可以看到global_uninit_var符号的类型为COMMON类型,编译器将未初始化的全局变量作为弱符号处理[delta@rabbit: c_code ]$ nm hello.o 0000000000000004 C global_uninit_var多个符号定义类型情况分析两个或以上强符号类型不一致:报重定义错误有一个强符号和多个弱符号:取强符号,若是有弱符号比强符号空间大的情况则编译时会出现warning两个或者以上弱符号类型不一致:取占用空间最大的弱符号:当编译器将一个编译单元编译成目标文件时,如果该编译单元包含弱符号(未初始化或者初始化为0的全局变量是典型),那么该符号所占用的最终空间就是不确定的,所以编译器无法在该阶段为该符号在BSS段分配空间。但是经过链接之后,任何一个符号的大小都确定了,所以它可以在最终输出文件的BSS段为其分配空间。总体来看,未初始化的全局变量是放在BSS段的

3.2.3 静态库链接

定义:静态库可以简单地看做是一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件(Linux上常用的C语言静态库libc位于/usr/lib/x86_64-linux-gnu/libc.a)静态链接过程:在链接过程中ld链接器会自动寻找所有需要的符号以及它们所在的目标文件,将这些目标文件从libc.a中“解压”出来,最终将它们链接到一起形成一个可执行文件。使用gcc -v hello.c func.c编译生成可执行文件a.out,可以看到详细的链接过程,产生如下输出(简化版本) [delta@delta: code ]$ gcc -v func.c hello.c # 对func.c的预处理和编译过程 /usr/lib/gcc/x86_64-linux-gnu/7/cc1 func.c -o /tmp/ccfC6J5E.s # 对func.c产生的.s文件汇编产生二进制文件 as -v --64 -o /tmp/ccF4Bar0.o /tmp/ccfC6J5E.s # 对hello.c的预处理和编译过程 /usr/lib/gcc/x86_64-linux-gnu/7/cc1 hello.c -o /tmp/ccfC6J5E.s # 对hello.c产生的.s文件汇编产生二进制文件 as -v --64 -o /tmp/cc7UmhQl.o /tmp/ccfC6J5E.s # 链接过程 /usr/lib/gcc/x86_64-linux-gnu/7/collect2 -dynamic-linker ld-linux-x86-64.so.2 Scrt1.o crti.o crtbeginS.o /tmp/ccF4Bar0.o /tmp/cc7UmhQl.o crtendS.o crtn.o ############################################ # 实际各个目标文件的位置 /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/crtbeginS.o /tmp/ccF4Bar0.o /tmp/cc7UmhQl.o /usr/lib/gcc/x86_64-linux-gnu/7/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/crtn.o可以看到Scrt1.o crti.o crtbeginS.o /tmp/ccF4Bar0.o /tmp/cc7UmhQl.o crtendS.o crtn.o被链接入了最终可执行文件各个文件的解释(来源) 目标文件说明crt0.oOlder style of the initial runtime code ? Usually not generated anymore with Linux toolchains, but often found in bare metal toolchains. Serves same purpose as crt1.o (see below).crt1.oNewer style of the initial runtime code. Contains the _start symbol which sets up the env with argc/argv/libc _init/libc _fini before jumping to the libc main. glibc calls this file "start.S".crti.oDefines the function prolog; _init in the .init section and _fini in the .fini section. glibc calls this "initfini.c".crtn.oDefines the function epilog. glibc calls this "initfini.c".scrt1.oUsed in place of crt1.o when generating PIEs.gcrt1.oUsed in place of crt1.o when generating code with profiling information. Compile with -pg. Produces output suitable for the gprof util.Mcrt1.oLike gcrt1.o, but is used with the prof utility. glibc installs this as a dummy file as it"s useless on linux systems.crtbegin.oGCC uses this to find the start of the constructors.crtbeginS.oUsed in place of crtbegin.o when generating shared objects/PIEs.crtbeginT.oUsed in place of crtbegin.o when generating static executables.crtend.oGCC uses this to find the start of the destructors.crtendS.oUsed in place of crtend.o when generating shared objects/PIEs.通常链接顺序: crt1.o crti.o crtbegin.o [-L paths] [user objects] [gcc libs] [C libs] [gcc libs] crtend.o crtn.o链接过程控制:链接过程需要考虑很多内容:使用哪些目标文件?使用哪些库文件?是否保留调试信息、输出文件格式等等。链接器控制链接过程方法: 使用命令行来给链接器指定参数将链接器指令存放在目标文件里面,编译器通常会使用这种方式向链接器传递指令。使用链接控制脚本

3.2.4 BFD库简介

定义:由于现代的硬件和软件平台种类繁多,每个平台都有不同的目标文件格式,导致编译器和链接器很难处理不同平台的目标文件。BFD库(Binary File Descriptor library)希望通过统一的接口来处理不同的目标文件格式。现代GCC(具体来讲是GNU 汇编器GAS)、链接器、调试器和GDB及binutils的其他工具都是通过BFD库来处理目标文件,而不是直接操作目标文件。

3.3 装载与动态链接

3.3.1可执行文件的装载

进程的虚拟地址空间:每个程序运行起来之后,它将拥有自己独立的虚拟地址空间,这个虚拟地址空间的大小由计算机的硬件平台决定,具体来说是CPU的位数决定(32位平台下的虚拟空间为4G<2^32>,通过cat /proc/cpuinfo可以看到虚拟地址的位数,如本机为address sizes : 39 bits physical, 48 bits virtual,虚拟地址位数为48位,则虚拟空间为2^48)。 进程只能使用操作系统分配给进程的地址,否则系统会捕获到这些访问并将其关闭(Window:进程因非法操作需要关闭;Linux:Segment Fault段错误)装载的方式:程序运行时是有局部性原理的,所以可以将程序最常用的部分驻留在内存中,将不常用的数据存放在磁盘里(动态装入的基本原理) 覆盖装入(几乎被淘汰):覆盖装入的方法吧挖掘内存潜力的任务交给了程序员,程序员在编写程序时将程序分为若干块,然后编写一个辅助代码来管理这些这些模块何时应该驻留内存,何时应该被替换掉(在多个模块的情况下,程序员需要手工将它们之间的依赖关系组织成树状结构)页映射:页映射不是一下子将指令和数据一下子装入内存,而是将内存和磁盘中的所有数据和指令按照页(Page)为单位划分,之后所有的装载和操作的单位就是页。操作系统角度来看可执行文件的加载: 创建一个独立的虚拟地址空间:创建映射函数所需要的对应的数据结构 读取可执行文件头,建立虚拟空间和可执行文件的映射关系:程序在发生页错误时,操作系统从物理空间分配出来一个物理页,然后将“缺页”从磁盘读取到内存中,并设置缺页的虚拟页与物理页的映射关系,很明显,操作系统捕获到缺页错误时,它应该知道当前所需要的页在可执行文件的哪一个位置。这就是虚拟空间与可执行文件之间的映射关系(这种映射关系只是保存在操作系统内部的一个数据结构,Linux中将进程虚拟空间中的一个段叫做虚拟内存区域(VMA))。 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行 注:页错误处理:

OK,本文到此结束,希望对大家有所帮助。

用户评论

独角戏°

这东西我大学学过一点,感觉很有意思!

    有16位网友表示赞同!

冷眼旁观i

每次看代码的时候就想起编译器的概念,真复杂啊!

    有5位网友表示赞同!

面瘫脸

学习编译原理可以让我更好地理解软件的运行机制吧?

    有16位网友表示赞同!

花花世界总是那么虚伪﹌

现在用的电脑都太智能了,根本不用管这些东西,是不是?

    有17位网友表示赞同!

雨后彩虹

听起来专业 banget sih, 我想看一看能做什么。

    有18位网友表示赞同!

熏染

编译原理这门课好像很难啊,需要数学和逻辑思维都很强吧。

    有6位网友表示赞同!

Edinburgh°南空

小时候以为程序员只做代码,没想到还有这么深层的知识。

    有16位网友表示赞同!

空谷幽兰

学习编译器应该很有成就感,把自己的一套想法变成一个实实在在的工具吧!

    有16位网友表示赞同!

一笑傾城゛

我想知道编译过程具体是怎么一步步完成的?太好奇了!

    有11位网友表示赞同!

追忆思域。

这东西和人工智能有关吗?

    有5位网友表示赞同!

情字何解ヘ

感觉编译器就像机器翻译,把一种语言翻译成另一种人类能理解的语言吧。

    有20位网友表示赞同!

该用户已上天

我现在用Python写代码,有没有什么好用的编译器推荐?

    有11位网友表示赞同!

清原

希望有一天我能自己写一个编译器!

    有10位网友表示赞同!

晨与橙与城

学习编译原理一定很锻炼人的思考能力。

    有13位网友表示赞同!

命硬

编译器的设计应该需要考虑很多因素吧,比如性能、安全性这些。

    有18位网友表示赞同!

心安i

感觉这个领域的研究应该很有意思,可以探索不同的编程语言和架构。

    有5位网友表示赞同!

古巷青灯

编译器这块技术会不会在未来几年有更大的发展?

    有8位网友表示赞同!

败类

听说有些开源的编译器可以修改吗?我想要试试看!

    有13位网友表示赞同!

泡泡龙

我想查阅一下相关资料,了解这方面的知识点。

    有13位网友表示赞同!

【高效编程技巧:编译器的核心原理与应用】相关文章:

1.动物故事精选:寓教于乐的儿童故事宝库

2.《寓教于乐:精选动物故事助力儿童成长》

3.探索动物旅行的奇幻冒险:专为儿童打造的童话故事

4.《趣味动物刷牙小故事》

5.探索坚韧之旅:小蜗牛的勇敢冒险

6.传统风味烤小猪,美食探索之旅

7.探索奇幻故事:大熊的精彩篇章

8.狮子与猫咪的奇妙邂逅:一场跨界的友谊故事

9.揭秘情感的力量:如何影响我们的生活与决策

10.跨越两岸:探索彼此的独特世界

上一篇:2024年度最受欢迎手游平台网站排行:日活跃量最高TOP5推荐 下一篇:深入解析C语言中的fflush()函数:高效文件缓冲区清空技巧