编译器:将程序语言的代码转化为其他形式的软件。这个转换过程叫做编译。Eg:c->(gcc)
Linux可执行的文件通常指符合(ELF)Executable and Linking Format)这种特定形式的文件。ls 、cp 这些命令对应的实体文件都是可执行文件,使用file命令能查看文件是否符合ELF形式。ELF文件包含了程序以及如何运行程序的相关信息(元数据),机器语言是唯一一种cpu能够直接执行的语言,这里的程序或代码就是指机器语言的列表。
gcc 是将C语言的程序转化为机器语言描述的程序,将机器语言的程序按照ELF这种特定文件格式注入 文件,得到的就是可执行文件。由hello.c这样单个文件来生成可执行文件的过程如下:预处理(.i)侠义的编译(.s)—>汇编(.o)->链接(.exe)。这个过程统称为build.
预处理:由编译器对#include和#define进行处理。具体 来说,读入头文件,将所有的宏展开。预处理的内容近似于sed命令和awk命令这样纯文本的操作。
狭义的编译:编译器对预处理器的输出进行编译,生成汇编语言(.s)。
汇编:汇编器将汇编语言的代码转换为机器语言。汇编器的输出文件为目标文件(.o)。linux下目标文件也是ELF文件;目标文件和可执行文件是通过ELF文件中用于提取文件种类的标志。File 命令来查看。
链接:目标文件本身还不能直接使用,无论是直接运行还是作为程序库文件使用都不可以。将目标文件转换为最终可以使用的形式的处理称为链接。使用程序库的情况下,在这个阶段处理程序的加载。通过链接生成的并不一定是可执行文件,也可以是程序库文件。
程序运行的全过程:代码的build->ELF->运行加载链接->进程—>结束
运行环境:链接的话题并非仅出现在build过程。如果使用了共享库,那么在开始运行程序时链接才会发生。动态加载就是一种将所有链接处理放到程序于运行时进行的。
编程语言的运行方式:编译器会对程序进行编译,将其转换为可执行的文件(c/c++);解释器不将程序转换为别的语言,而直接运行(python);运行程序的方式不止一种,C语言也可以用解释器来运行,编程语言可以和运行方式自由搭配;编译器、解释器都统称为编程语言的处理器。 根据语言的特点:有静态类型检查,要求较高可靠性的情况下用编译方式;没有静态类型检查,对灵活性要求高于严密性的情况下,则使用解释方式。 静态类检查是指在程序开始运行之前,对函数的返回值以及参数进行检查的功能;在程序运行过程中随时进行类型检查的为动态类型检查。 静态指不运行程序而进行的处理;动态指一边运行程序一边进行某些处理。
狭义的编译过程:语法分析》》语义分析》》生成中间代码》》代码生成。
语法分析 :首先对代码进行解析,将其转化为计算机易于理解的形式,也就是语法树的形式。解析代码的程序模块称为解析器或者语法分析器
语义分析:通过解析语法获得语法树后,接着就要解析语法树,除去多余的内容、添加必要的信息;生成抽象的语法树。
语义分析包括:
1. 区分变量为局部变量还是全局变量
2. 解析变量的声明和引用。
3. 变量和表达式的类型检查
4. 检查引用变量之前是否进行了初始化。
5. 检查函数是否按照定义返回了结果
语法分析只是对代码的表象进行分析;语义分析是对表象之外的部分级进行分析。语法分析生成的语法树只是将代码的构造照搬过来。而语义分析生成的抽象语法树还包含了语义信息。比如,在变量的引用和定义之间添加链接;适当的增加类型转换命令,是表达式的类型一致;另外语法树中的表达式外侧的括号行末的分号在抽象的语法树都将被省略。
生成中间代码:将抽象语法树转化为只在编译器内部使用的中间代码;之所以特地的转化为中间代码,主要是为了支持多种编程语言或机器语言;gcc 使用一种名为RTL(Register Transfer Language)的中间代码。解析代码转化为中间代码为止称为编译器的前端。
代码生成:吧中间代码转换为汇编语言,这个阶段为代码生成;负责代码生成的程序模块为代码生成器。
优化:各个环节都可执行优化
- 语法分析:
1> 词法分析
1.1.1 词法分析就是将代码分割为一个个的单词,也可以称为扫描。
1.1.2 在该过程中,会将空白字符和注释这种对程序没有实际意义的部分剔除。
1.1.3 正是因为预先有了词法分析,语法分析才可以只处理有意义的单词,进而实现简化处理。
1.1.4 负责词法分析的模块称为词法分析器,又称为扫描器。
1.1.5 Token:在编程语言系统中,将一个单词的字面和他的种类以及语义值统称为token。词法解析器的作用就是解析代码并生成token序列。
1.1.1 编程语言的编译器中的解析器的主要作用是解析有扫描器生成的token序列,并生成代码所对应的树形结构,即语法树。
1.1.2 语法树和语法是完全对应的,所以c语言的分号以及表达式的括号等都包含在真实的语法树中,但是,没有意义,因此,实际上大部分情况下会生成一开始就省略分号和括号的抽象语法树。也就是说解析器会跳过语法树,直接生成抽象语法树。
理想情况是将词法分析、语法分析、语义分析这三个阶段做成3个独立的模块,这样的代码是最优美的。但实际上,这三个阶段并不能明确的分割开来。
语法分析的两层含义:一、语法分析中词法分析以外的部分才称为语法分析。二、词法分析和语法分析合起来称为语法分析。
2> 语法分析
定义的分析
语句的分析
表达式的分析
项的分析
3> 语义分析
变量引用的消解
类型名称的消解
类型定义的检查
表达式有效性的检查
静态类型的检查
计算机的中心是总线(bus)。总线是传送数据的通信干线,它连接了计算机中的各个设备,使之通信,就像人类的血管或者神经系统。
1. CPU是负责运算的设备。
1) CPU内部有寄存器,寄存器大小有32位或64位,在cpu计算时,寄存器被用于临时存放数据。通常cpu先将数据从存储器读入寄存器,然后以寄存器为对象进行计算,再将结果写回存储器。
将数据从存储器读入寄存器的操作称为加载
将数据从寄存器写回存储器的操作称为写回
- 存储器是存储二进制数据的设备。
- 进程所使用的地址称为虚拟地址。
λ 物理存储器的实际地址称为物理地址。
λ 虚拟地址的整体范围称为程序的地址空间。
λ 进程使用虚拟地址访问存储器,cpu内部称为MMU的设备会访问地址转换表进行地址转换。 - CPU
λ 386是x86系列的第一款32位cpu.
λ Pentinum 4是intel的x86系列第一款64位cpu。
λ 满足1.具备n位宽的通用寄存器 2.具备n位以上的地址空间。 才真正被称为n位cpu。
λ 32位的cpu的通用寄存器的大小为32位,和指针大小相同,地址空间为无符号的32位整数可以指向的范围。64位一样。
λ X86系列的CPU只要使用PAE(physical address extension)这样的机制,32位的CPU也可以操作36位范围的地址空间 指令集
不同的CPU都能够解释的机器语言体系称为指令集架构(ISA, instruction set architecture),也可以简称指令集。
Intel 将x86系列CPU之中的32位CPU的指令集架构称为IA—32.IA(iIntel Architecture).
ELF文件的结构
Linux使用ELF作为目标文件的格式。ELF格式被用于描述目标文件、可执行文件以及共享库的所有信息。
无论什么场合,使用ELF格式的目的只有一个,那就是把机器代码以及其对应的元数据以方便的链接器和加载器处理的形式保存起来。
代码的元数据包含如下的信息:- 代码文件的大小以及转换前的源代码文件名。
- 符号
符号指的是变量或者函数的名称。简单的情况下直接使用原编程语言中的函数名或者变量名即可。有时候也会根据不同的编程语言进行特定的变换后得到的符号名称。这种变换称为名称重整。比如c++里的重载。 - 重定位信息
重定位信息用于表示在链接完成前无法确定内存地址的代码位置信息。比如,在共享库内的函数,那么在最终链接完成后才能确定的其内存地址。在这种情况下,目标文件中就会留有“代码中这个位置的内存引用尚未确定”这样的信息。
这样的信息就是重定向信息。 - 调试信息
ELF的节和段
ELF文件结构的二元结构。目的:为了兼顾链接器、汇编器等编译工具以及程序加载到内存中的加载器两者的易用性的需求。
二元结构:如果以程序头信息来处理,则ELF文件可以解释为段集合。如果以节头信息来处理,则可以解释成节集合。
ELF头
程序头(描述段)
.text节
.rodata节
.data节
.got节
.symtab节
.strtab节
节头(描述节)
节(section):是汇编器、链接器等处理ELF文件内容的单位。ELF文件把不同目的的代码、数据等分割成节保存。比如,机器码统一保存到.text节中。全局变量的初始化数据则保存在.data节中。
段(segment):则是把程序加载到内存的加载器处理ELF文件时的单位。段由1个以上的字节构成。内存上不同范围有着“只读”、“可写”、“可执行”等不同的属性。因而需要根据属性进行分段。比如机器码如果不可执行就毫无意义,因此要统一到具有可执行属性段中。
目标文件的主要节
节名
内容
.text节
机器码。配置机器码的节,虽然叫text,但和文本文件没有关系。
.rodata节
读专用的.data。配置的字符串字面量等不能更新的数据
.data节
全局变量等。在文件中无大小信息。配置的是拥有初始值的全局变量等,这个节的数据在加载后有可能发生变更。
.bss
通用符号等。在文件中无大小信息。配置的是没有初始值的全局变量,并且加载到内存后,会被分配所有字节都初始化为0的内存空间。BSS是(Block Started by Symbol)。
.rel.text节
.text段中的符号的重定位信息
.symtab节
文件中包含的符号表。实际的字面量在.strtab节中保存
.strtab节
符号等字符串列表
.shstrtab
节名字符串列表
.line
代码和原始代码行号对照
.debug
调试用的符号信息
.fini
进程结束前执行的代码
.fini_array
进程结束前执行的函数的指针数组
.init
目标文件加载时执行的代码
.init_array
目标文件加载时执行的函数的指针数组
.note
用于保障兼容性等
Linux下的 binutils包中包含readelf命令可以输出elf文件的结构。
1. readelf –S hello #输出hello的节头信息。
2. readelf –l hello #查看hello的程序头。
3. readelf –s hello #输出符号表。
gcc
gcc – c main.c //在编译后中断build.
-o 指定输出文件名。
-v 详细输出其内部处理过程
Linux下负责链接的程序是/usr/bin/ld,这个程序称为GNU ld,一般称为链接器。
链接器可处理的文件:
文件类型
格式
后缀名
生成器
可重定位文件
ELF
.o
汇编器
可执行文件
ELF
无
链接器
共享库
ELF
.so
链接器
静态库
UNIX ar
.a
ar命令
可重定位文件指汇编器生成的目标文件(.o)。GNU as 生成的可重定位文件没有程序头,因此不能直接运行,只有配合链接器与其他可重定位文件、库产生连接后才可执行。
可执行文件指的是链接生成的用户可直接运行的目标文件。Linux下可执行文件没有后缀名
共享库是链接生成的另一种形式的目标文件,其中集合了各个函数、变量等供用户调用,因此需要能够再次和其他目标文件链接使用。共享库不会直接运行。共享库也叫动态链接库。Linux下的共享库文件名一般以lib开头,以.so作为后缀,并加上版本号。
静态库文件可以作为链接器的输入。和共享库文件一样,静态库文件也集合了各种函数、变量供其它用户使用。一般以lib开头,以.a作为后缀。静态库文件利用ar命令把多个可重定位文件打包成一个,因此链接静态库文件就相当于链接其中打包的所有可重定位文件。
什么事链接
链接指的是把多个目标文件关联为一个整体。而通过关联多个目标文件,就可生成同时使用多个目标文件定义的变量、函数的程序。
具体步骤:1、合并节。 2、重定位。 3、符号相消。此外,链接时还必须进行很多其他的处理。比如,在生成ELF文件时,需要为程序生成合适的程序头信息。不过归根到底,链接的主旨是关联目标文件,因此主要处理也就是上述三点。
合并节:在链接多个目标文件时,需要从各个目标文件中抽取节,把相同种类的节合并到一起。
重定位:指根据程序实际加载到内存时的地址,对目标文件中的代码和数据进行调整。
在链接文件时,根据整体情况决定“真实的”内存地址,把所有用虚拟内存地址的地方替换成真实的内存地址。这个处理就是重定位。
符号相消:指为了可以使用其他目标文件和库文件中提供的变量和函数,把尚未和实体链接的符号与具体的变量和函数等实体链接起来的操作。例如:mian.c中有printf函数,汇编器会把“这个目标文件中使用的printf函数的函数体在其他文件中”这个信息保留下来。这个信息就是未定义的符号。接下来,再进行链接操作的时候,再检索未定义的符号,把相关的变量或者函数的内存地址链接进来。这个处理就是符号消解。
符号相消和重定位联系紧密,比如上面的printf函数,编译mian.c时printf函数的地址是未知的,这时编译器为printf函数分配虚拟地址,并生成类似call printf的汇编指令,然后在链接时再把函数的内存地址修正为正确的地址。而这个“先设置虚拟地址,在链接时修正为正确的地址”的处理正是重定位操作,因此符号消解本身可以通过重定位来实现。
总体来说,像上面这样解释目标文件代码的含义,把目标文件从物理上、逻辑上连接起来,从而生成可执行文件的处理就是“链接”。
动态链接和静态链接
静态库在build,也就是执行ld命令的时候就会进行目标文件的链接,
而共享库在build的时候不会进行目标文件的链接,而只是检查共享库和符号是否存在,在程序运行时才在内存上实际链接目标文件。
其中,在build时链接目标文件的的链接操作称为静态链接。
而在程序执行时链接目标文件的链接操作则称为动态链接。
给链接器输入多个重定位文件时,这些文件被执行静态链接。
动态链接有容易更新、节省磁盘空间、节省内存的优点。Linux下也主要使用共享库和动态链接。gcc也是如此,不加任何选项的话执行的动态链接,而静态库的静态链接只在个别情况下使用。缺点:性能稍差、链接具有不确定性。
Eg:
动态链接:
gcc –c main.c
gcc –c f.c
gcc main.o f.o –lc –o prog
-l选项可以为链接指定库
Ldd prog //查看是否被动态链接。
静态链接:
gcc –static main.o f.o –lc –o prog
file prog
生成库
生成静态库
用ar生成静态库,和tar命令差不多
eg:$ ar crs libmy.a f.o g.o h.o
选项
含义
c
如果存档不存在,则创建
r
向存档添加文件
S
生成加速链接的索引
Linux下优化执行时共享库的检索速度,加载器会对共享库的信息建立缓存文件。这个缓存文件就是 /etc/ld.so.cache。安装新版本的共享库时,一定要更新这个缓存文件,更新缓存文件的需要以管理员的权限运行ldconfig命令。
gcc –c –fPIC f.c
gcc –c –fPIC g.c
gcc –share –WL, -soname, libfg.so.1 f.o g.o –o libfg.so.1
file libfg.so.1
加载程序
利用mmap系统调用进行文件映射,把程序加载到内存中。所谓的映射,意思是可以通过读取内存直接获得文件的内容,也可以通过写内存对文件的内容进行修改
在linux下,通过使用Proc文件系统,就可以表示进程利用mmap系统调用把文件映射到内存的范围信息。例如,利用cat /proc/44337/maps就可以表示44437进程中文件映射的信息。通过readelf –l /tmp/showmap 可以输出程序头。里面有elf段和内存空间的对应关系。
ELF文件中拥有实体的段都是通过mmap系统调用来加载的。不过进程的内存空间中也存在不和ELF文件对应的部分,比如,和.bss等节对应的空间、机器栈、堆。
动态链接的过程
目标文件的种类不同,加载ELF文件的主体也不同。程序由系统内核加载,共享库由动态链接加载器加载。
动态链接加载器是指加载并链接动态链接的程序本身及其链接的共享库,设置程序运行状态的程序。Linux下常用的动态链接加载器是/lib/ld-linux.so.2。动态链接加载器的统称为ld.so。使用ELF文件的系统中,程序ELF文件的INTERP段需要指定动态链接加载器的路径。系统内核在启动程序时读入此段的内容,从而加载,启动程序。换句话说,动态链接器和动态链接加载器的运作过程并无二致。
从ld.so链接程序到程序的执行完毕过程。
1. 加载程序
2. 启动ld.so
3. 读入共享库
4. 符号相消和重定位
5. 初始化
6. 跳转到程序入口
7. 程序终止处理
首先系统内核加载程序和ld.so,准备好运行环境后交由ld.so处理。完成启动的ld.so根据系统内核传递的参数进行初始化。接着读取程序的DYNAMIC段,加载所有可执行文件链接的共享库。对已经加载的共享库也执行同样的处理,递归加载所有的共享库。一旦加载完所需要的库,马上消解所有程序和代码库中的符号,并重定位代码。这样就完成了启动程序的准备工作。在执行了各个文件的初始化代码后,跳转到程序的入口,这样就启动了程序。在C语言程序中,也就是执行了main函数的意思。程序执行完毕后,最后会对每个文件执行终止处理,这样整个执行过程最终完成。
反汇编指的是从机器码恢复到汇编代码的过程。Linux上使用binutils包的objdump命令就可以反汇编一个程序,eg: objdump –d hello
C语言中设定程序是从main函数开始执行,但实际上程序最初是从_start函数开始执行的。_start函数由lib提供的/usr/lib/crtl.o文件定义,ctrl.o在这个文件在编译时是默认链接的。_start函数会初始化libc,之后调用mian函数。
执行终止处理,接下来从main函数返回,接着ld.so会执行终止处理代码。用于初始化的有.init节和.init_array节,相应的,终止处理有.fini节和.fini_array节。.fini节保存进程终止时的代码,而.fini_array则保存进程终止时执行的函数指针列表。程序执行完后,ld.so会调用exit系统调用终止进程。Exit系统调用和平时使用的exit函数不同。C语言调用exit系统调用时,调用的是_exit函数。_exit函数执行libc的终止处理代码(.fini节和.fini_array节)后,执行exit系统调用结束进程。而exit系统调用会跳过终止处理,立即结束进程。这就是ld.so所有处理过程。
动态加载指的是在程序运行时指定共享库名称进行加载的方法。动态加载经常被用于实现所谓的插件。Linux中使用dlopen()函数进行动态加载。动态链接的程序最初一定已经加载了ld.so。而程序启动后它依然保存在内存上。因此只需要调用内存中的ld.so的代码,就可以在程序开始执行之后也能进行动态链接处理。
地址无关代码指的是无论加载到那个地址,都不需要重定位也能运行的代码。共享库的代码一定要是地址无关的代码,这一点很重要共享库一定要设置为地址无关代码,是为了实现库共享。要实现地址无关的代码,必须改变两点:一是全局变量的访问,二是 外部函数的调用。
访问全局变量的代码一定要把绝对地址改为相对地址。可以使用全局偏移表(GOT)的结构。GOT是指向全局变量的指针数组,链接器为其申请内存空间,动态链接加载器初始化其内容。地址无关代码就是通过 从这个GOT中读取地址而做到地址无关的。
外部函数如何调用地址无关的代码。Linux下为了使函数调用地址独立,使用了一种可以称之为GOT的函数版的方法—过程链接表(PLT)。不过PLT一般比GOT的入口数多,因此会采取延迟初始化。也就是说,外部函数第一次调用该函数时,该函数才会被链接。
地址无关的可执行文件(PIE)。指的是使用地址无关代码的可执行文件。因为地址无关,所以可以被加载到任意地址。