一、编译与链接
- 编译与链接的过程可以分解为4个步骤:预处理(Preprocess)、编译(Compilation)、汇编(Assembly)和链接(Linking)
- P117 见详细的过程
1.源代码.cpp和头文件.h通过预处理得到
.i文件
2..i文件使用g++编译,得到
.s文件
3..s文件通过as汇编,得到
.o文件
3..o文件通过静态库进行链接,得到
目标文件.out
1.预处理
- 预处理命令:
g++
-E
helloworld.cpp -o helloworld.i //-E的编译选项,表示只执行到预编译,直接输出预编译的结果即.i文件
- 预处理做的主要处理:
1.将所有#define删除,并且展开所有的宏定义
2.处理所有条件预编译指令,如#ifdef,#else,#elif
3.处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
预编译程序把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理
4.过滤所有注释内容
5.添加行号和文件名标识
6.保留所有的#pragma编译器指令
- 预处理之后得到的.i文件不包含任何宏定义
2.编译
- 编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析以及优化后产生相应的汇编代码文件
- 编译命令:
g++
-S helloworld.i -o helloworld.s
- 编译过程一般分六步:
1.(扫描)词法分析:
2.语法分析:
3.语义分析:编译器能够分析的只有静态语义,即在编译期间就可以确定的语义。
4.中间语言的生成:即优化工作。以中间代码为界可以把编译器分成前端和后端
5.目标代码的生成:编译器后端
6.目标代码的优化:
编译器后端
- 如果变量定义在当前文件中,那么在编译阶段就可以为变量和指针分配空间。如果是在其他文件中的全局变量或者函数,在最终运行时的绝对地址都要在最终链接的时候确定
3.链接
- 把每个源代码模块独立的汇编得到.o文件,然后按照要求把他们组装起来,生成目标文件.out,这个组装模块的过程就是链接。
- 链接的主要作用是把各个模块之间相互引用的部分都处理好,使各个模块之间能够正确的衔接
- 链接的过程P123
- 每个模块的源文件通过编译器编译成目标文件.o文件,目标文件和库一起链接形成最终的可执行文件
- 最常见的库是运行时库。库其实就是一组目标文件的包,就是一些最常用的代码编译成目标文件之后打包存放
- 重定位:某个变量在当前文件中使用了但是没定义,那么编译的时候其地址为空,在链接中来确定其地址,是一个地址修正的过程。每个要被修正的地方叫一个重定位入口
- 每个目标文件提供三个表
1.未解决符号表:提供了所有在该单元里引用但是定义并不在本编译单元的符号以及其出现的地址
2.导出符号表:提供了本编译单元具有定义,并且愿意提供给其他单元使用的符号及其地址
3.地址重定向表:提供了本编译单元中所有对自身地址的引用的记录
extern声明的变量置入未解决符号表。属于外部链接
static声明的全局变量不置入未解决符号表,也不置入导出符号表。属于内部链接
普通变量和函数被置入导出符号表
- 链接分为静态链接和动态链接
1)静态链接
- 函数库在编译时期完成的链接是静态链接,程序在运行时和函数库再无瓜葛。这样的函数库被称为静态函数库,通常形如"libxxx.a"
1.链接是把编译完成(
此处指完成了汇编)的.o文件链接成目标的.out文件,
把.cpp和.h文件编译成.o文件的命令行为
g++ -c add.cpp //在当前的文件夹中生成了add.o文件
2.由.o文件创建静态库.a文件
ar cr libmymath.a sub.o add.o //libmymath.a为输出的文件名,sub.o和add.o为要添加进静态库的文件名
//ar命令的r选项,在库中插入模块,如果模块已经存在,则替换模块
//ar命令的c选项,创建一个库,不管是否已经存在
3.最后进行链接生成可执行文件
g++ -o main main.cpp -L. -lmymath
- g++会在静态库名称前加上前缀lib,然后追加扩展名.a得到静态库文件名来查找静态库文件
2)动态链接
- 在程序运行时期对库函数进行的链接称为动态链接库技术
- g++会在动态库名称前加上前缀lib,然后追加扩展名.so得到静态库文件名来查找动态库文件
3)静态链接库、动态链接库各自的特点
- 静态链接库在编译的时候将库函数装载到程序中去,动态库在运行时才被装在,因此用静态库会快一些
- 动态链接库有利于进程间的资源共享
- 将一些程序升级变的简单
- 链接载入由程序员在程序代码中控制
4.g++与gcc的区别
1)
1.后缀为.c的,gcc把它当作C程序,而g++当作C++程序。后缀为.cpp的,两者都认为是C++程序
2.编译阶段,g++会调用gcc,对于.cpp两者是等价的。但是
gcc命令不能自动和C++程序使用的库链接
,所以一般使用g++来完成链接。所以.cpp一般使用g++,.c一般使用gcc
2)
误区:gcc不会定义__cplusplus宏
这个宏标志着编译器将会把代码按c还是c++语法来解释
如果后缀为.c,并且采用gcc编译器,则该宏就是未定义的(
唯一一种该宏未定义的情况)
3)
误区:编译只能使用gcc,链接只能使用g++
通常使用g++来完成链接
编译中g++自动调用gcc,所以两者相同
4)
误区:"extern C"与gcc/g++有关系
没有关系 P130
如果有
"extern C",那么gcc/g++编译得到的函数命名都是以C的方式命名
二、makefile的撰写
- makefile带来的好处是“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译
- make命令是一个命令工具,是一个解释makefile中指令的命令工具
- makefile和shell脚本????
#this is a simple makefile test //注释
main:main.o add.o sub.o //main这个目标文件依赖main.o add.o sub.o 这三个文件
g++ main.o add.o sub.o -o main //命令行,g++编译使用main.o add.o sub.o 生成main文件
add.o:add.cpp
g++ -c add.cpp -o add.o //-c表示只完成到汇编步骤,生成.o文件
sub.o:sub.cpp
g++ -c sub.cpp -o sub.o
main.o:main.cpp
g++ -c main.cpp -o main.o
clean: //在命令行输入make clean命令是,会删除所有.o文件和main文件
rm -f *.o main
//一个简单的makefile测试程序,上面的文件就是编译一个main测试文件
//格式一般为
A:B
(TAB)command
//其中A为目标文件列表,B为依赖文件列表
//每个命令行前面都需要加tab符号
//在命令行输入make即可以执行makefile
- 在makefile使用变量:
OBJS = main.o add.o sub.o //设定变量,把main.o add.o sub.o设定成了一个变量名OBJS
xx = g++
CFLAGS = -Wall -O -g //-Wall:输出所有警告信息 -O:编译时进行优化 -g:编译debug版本
main:$(OBJS)
$(xx) $(OBJS) -o main
add.o:add.cpp add.h
$(xx) $(CFLAGS) -c add.cpp -o add.o
sub.o:sub.cpp sub.h
$(xx) $(CFLAGS) -c sub.cpp -o sub.o
main.o:main.cpp add.h sub.h
$(xx) $(CFLAGS) -c main.cpp -o main.o
clean:
rm -f *.o main
//要使用一个变量,在一行的前端写下变量名,后面跟一个等号"=",后面跟要设定的这个变量值即可。如前三行代码所示
- 在makefile中使用函数:
CC = gcc //设定变量名
XX = g++
CFLAGS = -Wall -O -g
TARGET = main
%.o:%.c //把.c文件都编译成对应的.o文件
$(CC) $(CFLAGE) -c $< -o $@ //特殊符号
%.o:%.cpp
$(XX) $(CFLAGS) -c $< -o $@
SOURCES = $(wildcard *.c *.cpp) //设定变量名,使用了函数
OBJS = $(patsubst %.c,%.o,$(patsubst %.cpp,%.o,$(SOURCES)))
$(TARGET):$(OBJS)
$(XX) $(OBJS) -o $(TARGET)
clean:
rm -f *.o $(TARGET)
//
- makefile中通配符
%:
%为Makefile规则通配符,一般用于规则描述,如%.o:%.c表示所有.o文件为目标文件,所有.c文件为依赖文件
*:不具备上述功能,尤其是在makefile,当变量定义或者函数调用时,该通配符的展开功能就失效了,此时需要借助函数wildcard。所见例子中只在clean中单独使用了
- 几个特殊符号
$@:展开为目标文件的名字
$<:依赖文件列表中的第一个文件
$^:整个依赖列表
- 函数
函数wildcard:用于展开*
$(wildcard PATTERN...):展开成为已经存在的、使用空格分开的、匹配此模式的所有文件列表
例子SOURCES = $(wildcard *.c *.cpp) ,SOURCE为所有的.c和.cpp文件
函数patsubst:用于匹配替换
例子$(patsubst %.cpp,%.o,$(SOURCES)),第一个参数是需要匹配的样式,第二个是用什么文件来替换,第三个是要被替换的文件列表
三、目标文件
- 目标文件即ELF文件,ELF是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储的标准文件格式
- UNIX最早的可执行文件格式为a.out格式,后来为了兼容共享库又设计了COFF格式,而ELF正是从COFF格式继承而来的,具有更好的扩展性和灵活性,用于取代COFF
- COFF的主要贡献实在目标文件里面引入了“段”的机制,不同的目标文件可以拥有不同数量以及不同类型的“段”
1.ELF文件类型
1)可重定位的目标文件,即.o文件
- 由编译器汇编生成.o文件
- 链接器使用可重定位的目标文件作为输入,经过链接处理后,生成一个可执行的目标文件或者一个可被共享的对象文件(.so文件)。也可以使用ar工具将众多的.o文件归档(archive)为一个.a的静态库文件
2)可执行的目标文件
- 在Linux系统里面,存在两种可执行文件,除了可执行的目标文件,另外一种就是可以执行的脚本文件(如shell脚本),注意这些脚本不是可执行的目标文件,他们只是文本文件,执行这些脚本的解释器才是可执行的,比如bash shell程序
3)可被共享的目标文件,即动态库文件,也即.so文件
- 动态库文件的使用步骤
1.链接器拿它和其他的可重定位的文件(.o文件)以及其他.so文件作为输入,经链接处理后,生成另外的可共享的目标文件.so或者可执行的目标文件
2.在运行时,动态链接器拿它和一个可执行的目标文件以及另外一些可共享的目标文件(.so)来一起处理,在Linux系统里面创建一个进程映像
2.链接视图下的ELF内容
- ELF格式需要使用在两种场合,所以有两种不同的视图:链接视图和执行视图
1.组成不同的可重定位文件,以参与可执行文件或者可被共享的对象文件(.so)的链接创建
2.组成可执行文件或者可被共享的对象文件,以在运行时内存中进程映像的构建
1)ELF头部
- 使用readelf -h filename来阅读ELF文件的头部,注意readelf不支持显示archive文档(如.a文档)
//readelf -h add.o 读取ELF文件add.o的头部得到的数据
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0 //程序进入点,即程序真正执行起来的时候其第一条要运行的指令的位置。这里为0X0表示没有程序进入点。即.o文件只用于链接。
Start of program headers: 0 (bytes into file)
Start of section headers: 1416 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0 //program header为0,在可执行文件中和.so文件中该项一般不是0
Size of section headers: 64 (bytes)
Number of section headers: 19 //包含有19个节区
Section header string table index: 16
//section是在ELF文件里,用以装载内容数据的最小容器。在ELF文件里面,每一个section内都装载了性质属性都一样的内容,比如说:
1.text section:装载了可执行代码
2.data section:装载了被初始化的数据
3.bss section:装载了未被初始化的数据
4. .rec section:装载了重定位条目
5.symtab section:状态了符号信息
6.strtab section:装载了字符串信息
7.其他为满足不同的目的所设置的section,比如满足调试的目的、满足动态链接与加载的目的等。
//.shstrtab section:字符串表,储存是section的名字
2)ELF section表的总体预览
//readelf -S add.o
There are 19 section headers, starting at
offset 0x588:
Section Headers:
[Nr] Name Type
Address
Offset //size为section的字节大小,EntSize只对某些形式的section有意义P138,Align是地址对齐要求,Link和Info中记录的是section head table中的条目索引
Size EntSize Flags Link Info Align //Address的值为0,因为是链接的重定位文件,不参与进程映像的重建。如果是可执行文件那么这个值一般有特殊取值。 offset 该section离头文件的距离
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000004 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 00000044
0000000000000000 0000000000000000 WA 0 0 1
[ 3] .bss NOBITS 0000000000000000 00000044
0000000000000000 0000000000000000 WA 0 0 1
[ 4] .debug_info PROGBITS 0000000000000000 00000044
0000000000000071 0000000000000000 0 0 1
[ 5] .rela.debug_info RELA 0000000000000000 000003c0
00000000000000c0 0000000000000018 I 17 4 8
[ 6] .debug_abbrev PROGBITS 0000000000000000 000000b5
000000000000004a 0000000000000000 0 0 1
[ 7] .debug_aranges PROGBITS 0000000000000000 000000ff
0000000000000030 0000000000000000 0 0 1
[ 8] .rela.debug_arang RELA 0000000000000000 00000480
0000000000000030 0000000000000018 I 17 7 8
[ 9] .debug_line PROGBITS 0000000000000000 0000012f
000000000000003b 0000000000000000 0 0 1
[10] .rela.debug_line RELA 0000000000000000 000004b0
0000000000000018 0000000000000018 I 17 9 8
[11] .debug_str PROGBITS 0000000000000000 0000016a
0000000000000087 0000000000000001 MS 0 0 1
[12] .comment PROGBITS 0000000000000000 000001f1
0000000000000035 0000000000000001 MS 0 0 1
[13] .note.GNU-stack PROGBITS 0000000000000000 00000226
0000000000000000 0000000000000000 0 0 1
[14] .eh_frame PROGBITS 0000000000000000 00000228
0000000000000030 0000000000000000 A 0 0 8
[15] .rela.eh_frame RELA 0000000000000000 000004c8
0000000000000018 0000000000000018 I 17 14 8
[16] .shstrtab STRTAB 0000000000000000 000004e0
00000000000000a3 0000000000000000 0 0 1
[17] .symtab SYMTAB 0000000000000000 00000258
0000000000000150 0000000000000018 18 13 8
[18] .strtab STRTAB 0000000000000000 000003a8
0000000000000012 0000000000000000 0 0 1
Key to Flags: //Flags的是对应section的相关标志。
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
//特别注意标志A(Allocable),说明对应的section是可分配的。所谓的可分配是指在运行,进程(process)需要使用它们,所以它们被加载器加载到内存中去。
//与此相反,存在一些non-Allocable的section,它们只是被链接器、调试器或者其他类似工具所使用,而并非参与进程的运行中去。如.strtab 和.symtab等
//当运行最后的可执行程序是,加载器会加载哪些哪些Allocable的部分,而non-Allocable的部分则会被继续留在可执行文件中
non-Allocable的section都可以被stip工具从最后的可执行文件中删除掉,删除掉这些section的可执行文件仍然能够运行,只不过没办法进行调试等操作
3)ELF的.text section(装载了可执行代码)
- 使用readelf -x SecNum来打印出不同的section中的内容
//objdump -d -j .text add.o:-d表示对-j指定的文件add.o进行反汇编
//add.o的汇编指令
add.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z3addii>:
0: 8d 04 37 lea (%rdi,%rsi,1),%eax
3: c3 retq
4)ELF的.data section(装载了被初始化的数据)
//objdump -d -j .data add.o:在add.cpp中定义了一个全局变量g_Value并赋初值12
add.o: file format elf64-x86-64
Disassembly of section .data:
0000000000000000 <
g_Value
>:
0:
0c
00 00 00
5)ELF的.strtab section(装载了字符串信息)
//readelf -x num file //以16进制显示指定文件file的指定段num的内容
//readelf -x 18 add.o //以16进制显示add.o文件的第10段的内容
Hex dump of section '
.strtab
':
0x00000000 00616464 2e637070 005f5a33 61646469 .add.cpp._Z3addi
0x00000010 6900675f 56616c75 6500 i.g_Value.
//
.strtab section储存的是以字符为分隔符的字符串,这些字符串所表示的内容,通常是程序中定义的函数名称、所定义过的变量名称等。
//当对象文件中的其他地方需要和一个这样的字符串相关联的时候,往往会在需要的地方先储存.strtab section中的索引值
6)ELF的.symtab section(装载了符号信息)
//字符串表在真正的链接和生成进程映像过程中是不需要使用
//readelf -s add.o:显示文件的符号表段中的项
Symbol table '.symtab' contains 15 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS add.cpp
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 4
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 7
8: 0000000000000000 0 SECTION LOCAL DEFAULT 9
9: 0000000000000000 0 SECTION LOCAL DEFAULT 11
10: 0000000000000000 0 SECTION LOCAL DEFAULT 13
11: 0000000000000000 0 SECTION LOCAL DEFAULT 14
12: 0000000000000000 0 SECTION LOCAL DEFAULT 12
13: 0000000000000000 4 FUNC GLOBAL DEFAULT 1 _Z3addii //add的类型是FUNC
14: 0000000000000000 4 OBJECT GLOBAL DEFAULT 2 g_Value //g_Value的类型是OBJECT
//在符号表中针对每一个符号,都会相应的设置一个条目。
3.执行视图下的ELF内容
//readelf -l main:显示可执行文件main的内容
Elf file type is EXEC (Executable file)
Entry point 0x400770
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr //这里的segment和下面是一一对应的
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040 //R可读,W可写,E可执行
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP
0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000b2c 0x0000000000000b2c R E 200000
LOAD 0x0000000000000df8 0x0000000000600df8 0x0000000000600df8
0x000000000000026c 0x00000000000003a0 RW 200000
DYNAMIC 0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
0x00000000000001e0 0x00000000000001e0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x00000000000009a4 0x00000000004009a4 0x00000000004009a4
0x000000000000004c 0x000000000000004c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000df8 0x0000000000600df8 0x0000000000600df8
0x0000000000000208 0x0000000000000208 R 1
Section to Segment mapping:
Segment Sections... //这里的segment和上面是一一对应的
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got
.text
.fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
//在可执行文件main中,总共有9个
segment。该结果显示了哪些section映射到哪个segment中去了。比如.text就映射到了编号02的
segment中了
//INTERP的segment中只有一个.interp section。在这个section中包含了动态链接过程中需要使用的解释器路径和名称
//程序在Linux里面运行时,当在shell中键入一个命令要执行时,内核会创建一个新的进程
4.阅读ELF文件的工具——readelf
P144
5.获得二进制文件里符号的工具——nm
//nm add.o:获取add.o文件中使用的全部符号
0000000000000000 D g_Value //第一列是当前符号的地址,第二列是当前符号的类型,第三列是当前符号的名称
0000000000000000 T _Z3addii
//nm -C add.o:转换一下形式
0000000000000000 D g_Value
0000000000000000 T add(int, int)
6.减少目标文件大小的工具——strip
- UNIX下文件压缩命令有compress和tar,但是compress压缩之后的文件要用uncompress来解压之后才能正常运行
- strip能够清楚文件中不必要的标识符及调试信息,可以减少文件大小而不影响正常使用。但文件一旦strip之后便不能复原,所以其实是一个减肥操作
- strip能够有选择的除去行号信息、重定位信息、调试段、typchk段、注释段、头文件以及所有或部分符号表
- 通常只在已经调试和测试过的生成模块上使用strip命令