linux编译链接

GCC简介

GCC是Linux下的编译工具集,是GNU Compiler Collection的缩写,包括gcc、g++等编译器和其它工具集。
GCC的C编译器为gcc,命令格式为:

1
gcc [options] files ...

文件扩展名含义

文件扩展名 含义
.c C语言的源文件
.h C/C++语言的头文件
.hpp C++语言的头文件
.i 预处理后的C文件
.C .cc .cxx C++语言的源文件
.s 汇编语言的源文件
.o 会变后的目标文件
.a 静态库
.so 动态库
GCC编译命令 含义
cpp 预处理器编译器
cc C语言编译器
gcc C语言编译器
cc1 C语言编译器
cc1plus C++语言编译器
g++ C++语言编译器
as 汇编器
ld 链接器

默认搜索路径

  1. 头文件 /usr/local/include /usr/lib/gcc/ /usr/include
  2. 库文件 /usr/lib/gcc /lib/

编译程序基础

GCC编译器对程序的编译分为4个阶段:预处理、编译和优化、汇编、链接

1
2
          预处理(cpp)        编译和优化(gcc/cc)            汇编(as)              链接(ld)
*.c *.h ------------> *.i -------------------------> *.s -------------> *.o -----------------> 可执行文件

gcc选项

选项 说明
-o 指定生成文件名
-c 生成目标文件
-E 预编译操作
-S 生成汇编代码
-g 加上调试信息

预编译

主要处理源代码文件中以”#”开始的预编译指令。生成.i文件。编译后的.i文件不包含任何宏定义。

1
2
3
4
5
[CaseZheng@VM_187_252_centos Tmp]$ ls
main.c
[CaseZheng@VM_187_252_centos Tmp]$ gcc -E -o main.i main.c
[CaseZheng@VM_187_252_centos Tmp]$ ls
main.c main.i

编译和优化

词法分析、语法分析、语义分析、优化、生成汇编代码文件。

1
2
3
4
5
[CaseZheng@VM_187_252_centos Tmp]$ ls
main.c
[CaseZheng@VM_187_252_centos Tmp]$ gcc -S main.c
[CaseZheng@VM_187_252_centos Tmp]$ ls
main.c main.s

词法分析

词法分析使用类似于有限状态机的算法,将源代码的字符序列分割成一系列的记号。
词法分析产生的记号一般可以分为:关键词、标识符、字面量、特殊符号等。
程序lex可以实现词法扫描,按用户定义的词法规则将字符串分割为一个个记号。

语法分析

语法分析将词法分析产生的记号进行语法分析,产生语法树。
程序yacc可以完成语法分析。

语义分析

语义分析是对表达式的含义进行分析,编译器所能分析的时静态语义(编译期可以确定的语义),动态语义是只在运行期才能确定的语义。
静态语义通常包括声明和类型的匹配,类型的转换。

源码级优化及中间代码生成

源代码级优化器会在源代码级别进行优化。优化器将整个语法树转换成中间代码(三地址码、P-代码)

生成汇编代码及优化

源代码级优化器产生中间代码后的过程属于编译器后端。编译器后端主要包括代码生成器和目标代码优化器。
代码生成器将中间代码转换成目标机器代码(汇编代码)。目标代码优化器对汇编代码进行优化。

预编译和编译可以合并成一个步骤,对C语言预编译和编译的程序是cc1,对C++是cc1plus。

1
2
3
[CaseZheng@VM_187_252_centos /]$ locate cc1
/usr/libexec/gcc/x86_64-redhat-linux/4.8.2/cc1
/usr/libexec/gcc/x86_64-redhat-linux/4.8.2/cc1plus

汇编

将汇编代码转变成机器可执行的指令,每一个汇编语句几乎对应一条机器指令。

1
2
3
4
5
[CaseZheng@VM_187_252_centos Tmp]$ ls
main.c
[CaseZheng@VM_187_252_centos Tmp]$ gcc -c main.c
[CaseZheng@VM_187_252_centos Tmp]$ ls
main.c main.o

目标文件

编译器编译源代码后的生成的文件叫做目标文件。目标文件从结构上讲,是已经编译后的可执行文件格式,只是还没经过链接的过程。

目标文件格式

PC平台流行的可执行文件格式主要是Windows下的PE和Linux下的ELF,都是COFF格式的变种。
目标文件、可执行文件、动态链接库、静态链接库都是按照可执行文件格式存储。

1
2
3
4
5
6
7
8
9
10
11
[CaseZheng@VM_187_252_centos Tmp]$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

[CaseZheng@VM_187_252_centos Tmp]$ gcc -o run main.c hello.c
[CaseZheng@VM_187_252_centos Tmp]$ ls
hello.c hello.o main.c run
[CaseZheng@VM_187_252_centos Tmp]$ file run
run: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=92d675d4aed8cea8d1aeee31f3d4e901f8c57022, not stripped

[CaseZheng@VM_187_252_centos Tmp]$ file /lib/libc-2.17.so
/lib/libc-2.17.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked (uses shared libs), BuildID[sha1]=efcb4322c3353d41309c104563110b0c238bbc51, for GNU/Linux 2.6.32, not stripped

目标文件内容

目标文件将指令代码、数据、链接所需信息以”节”(“段”)的形式存储。
ELF文件开头是文件头,描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接等信息,文件头还包括一个段表,段表时一个描述文件中各个段的数据。段表描述了文件中的偏移位置及段的属性等,从段表中可以得到段的所有信息。文件头后面就是各个段的内容。
程序源代码编译后的机器指令经常放在代码段(.code/.text)
已初始化的全局变量和局部静态变量放在.data段
未初始化的全局变量和局部静态变量放在.bss段。因为未初始化的全局变量和局部静态变量默认值都是0,在.data段分配空间存储0是没有必要的。.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,并没有内容,在文件中不占据空间。
.rodata段存放只读数据,一般是程序里面的只读变量和字符串常量。
.comment段存放编译器版本信息
.debug段调试信息
.hash段符号哈希表
.line段调试时的行号表,即源代码行号与编译后指令的对应表
.strtab段字符串表,存储ELF文件用到的各种字符串
.symtab段符号表
.shstrtab段段名表
.plt .got动态链接的跳转表和全局入口表
.init .fini程序初始化与终结代码段
总体来说,程序源代码被编译后主要分两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据
程序指令和程序数据分开存储的好处:

  1. 安全,程序加载后程序指令只读、程序数据读写
  2. 利于缓存,提高CPU的缓存命中率
  3. 共享指令,节省内存

目标文件结构

  1. 文件头定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。
  2. 段表是保存各个段的基本属性的结构,描述各个段的信息。ELF文件的段结构由段表决定、编译器、连接器和装载器都是依靠段表来定位和访问各个段的属性的。
  3. 重定位表(.rel.text/.rel.data)链接器在处理目标文件时需要对某些部位进行重定位,这些重定位信息都记录在ELF文件的重定位表中。
  4. 字符串表.strtab保存普通字符串。
  5. 段名字符串表.shstrtab保存短表中用到的字符串。
  6. 符号表.symtab记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值。对变量和函数,符号值就是它们的地址。
  7. 特殊符号
    • __executable_start 程序起始地址,不是入口地址,是程序最开始的地址
    • __extext或_extext或extext代码段结束地址,即代码段最末尾的地址
    • _edata或edata数据段结束地址,即数据段最末尾的地址
    • _end或end程序结束地址

符号修饰与函数签名

函数签名包含了一个函数的信息,包括函数名、参数类型、所在类、所在名字空间等信息
编译器及链接器处理符号时,使用某种那个名称修饰方法,使每个函数签名对应一个修饰后的名称
C++编译器会将在extern “C”的大括号内部的代码当作C语言处理。

静态链接

链接的主要内容是将各个目标代码之间相互引用的部分都处理好,使各个模块之间能够正确的衔接。
链接过程包括地址和空间分配、符号决议、重定位等步骤。
运行时库是支持程序运行的基本函数的集合。库其实是一组目标文件的包。
各个目标文件独立编译,变量及函数地址不能独立确定,需要在链接过程中修正,使其指向正确的地址,这个过程叫重定位,每个要修正的地方叫重定位入口。

1
2
3
4
5
[CaseZheng@VM_187_252_centos Tmp]$ ls
main.c
[CaseZheng@VM_187_252_centos Tmp]$ gcc -o run main.c
[CaseZheng@VM_187_252_centos Tmp]$ ls
main.c run

相似段合并

链接器为目标文件分配地址和空间,其中地址和空间有两个含义:1.输出的可执行文件中的空间;2.装载的虚拟地址中的虚拟地址空间。
虚拟地址空间分配关系到链接器关于地址的计算,而可执行文件本身的空间分配与链接过程关系并不大。

  1. 空间与地址分配 扫描所有输入目标文件,获取各个段长度、属性、位置,将目标文件符号表中符号定义和符号引用收集起来,统一放在全局符号表。链接器根据收集的信息合并各个段,计算文件文件中各个段合并后的长度与位置,并建立映射关系。
  2. 符号解析与重定位 使用空间与地址分配收集的信息,读取文件中段的数据、重定位信息,并进行符号解析和重定位、调整代码中的地址。

动态链接

静态链接缺点

  1. 浪费内存和磁盘空间
  2. 模块更新困难

动态链接:不对组成程序的目标文件进行链接,等程序运行时才进行链接,即将链接过程推迟到运行时再进行
Linux系统中,ELF动态连接文件被称为动态共享对象(共享对象),一般都以”.so”为扩展名。Windos中称为动态链接库,一般以”.dll”为扩展名。
共享对象在编译时不能假设自己在进程虚拟地址控件中的位置

装载时重定位

Linux和GCC支持装载时重定位的方法,在产生共享对象是不使用’-fPIC’,只使用’-shared’参数,输出的共享对象就是使用装载时重定位的方法

地址无关代码

装载时重定位是解决动态模块中的办法之一,但其指令部分不能在多个进程间共享,失去了动态连接节省内存的一大优势。

  1. 模块内部调用或跳转 模块内相对位置固定,通过相对地址调用
  2. 模块内部数据访问 相对寻址
  3. 模块间数据访问 ELF在数据段建立一个指向其它模块变量的指针数组(全局偏移表GOT),当代码需要引用该全局变量时,通过GOT中的相对应的项间接引用。
  4. 模块间调用、跳转 建立全局偏移表GOT,保存目标函数的地址。
指令跳转、调用 数据访问
模块内部 1.相对跳转和调用 2.相对地址访问
模块外部 3.间接跳转和调用 4.间接访问

‘-fpic’和’-fPIC’功能相同都是生成地址无关代码,但’-fPIC’生成的代码要大一点,’-fpic’生成代码小,不过’-fpic’在某些平台上会有一些限制,而’-fPIC’则不存在。

延迟绑定

延迟绑定:函数第一次被使用时才进行绑定。

“.interp”段

动态链接器的位置不是由系统配置指定,也不是由环境参数决定,而是由ELF可执行文件决定。“.interp”段保存的一个字符串就是可执行文件所需要的动态链接器的路径

“.dynamic”段

“.dynamic”段保存了动态链接器所需要的基本信息(依赖的共享库、动态链接符号表位置、动态连接重定向表的位置、共享对象初始化代码的地址等)。
ldd命令可以查看一个程序或共享库依赖哪些共享库。

动态符号表

“.dynsym”

动态链接重定向表

静态链接中”.rel.text”代码段重定位表 “.rel.data”数据段重定位表
动态链接中”.rel.dyn”对数据引用的修正 “.rel.plt”对函数引用的修正,它所修正的位置位于”.got.plt”

静态链接库

静态链接库通常以”.a”为后缀,由程序ar生成。
静态链接库优点

  1. 不需要重新编译程序库代码,就可进行程序的重新链接,节省编译时间。
  2. 提供库文件给使用人员,不需要开放源代码。
  3. 静态库执行速度比共享库和动态库要快。

静态链接库生成

  1. 生成目标文件
  2. 用ar工具对目标文件归档。

ar的-r选项,可以创建库,并把目标文件插入指定库中。

1
2
3
4
5
6
7
8
[CaseZheng@VM_187_252_centos Tmp]$ ls
hello.c
[CaseZheng@VM_187_252_centos Tmp]$ gcc -c hello.c
[CaseZheng@VM_187_252_centos Tmp]$ ls
hello.c hello.o
[CaseZheng@VM_187_252_centos Tmp]$ ar -rcs libhello.a hello.o
[CaseZheng@VM_187_252_centos Tmp]$ ls
hello.c hello.o libhello.a

使用静态链接库

  1. 和使用目标文件一致
  2. 使用”-l库名”使用,库名是不包含函数库和扩展名的字符串。

使用”-L”显式指定搜索库的路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[CaseZheng@VM_187_252_centos Tmp]$ rm run 
[CaseZheng@VM_187_252_centos Tmp]$ ls
hello.c hello.o libhello.a main.c
[CaseZheng@VM_187_252_centos Tmp]$ gcc -o run main.c libhello.a
[CaseZheng@VM_187_252_centos Tmp]$ ls
hello.c hello.o libhello.a main.c run
[CaseZheng@VM_187_252_centos Tmp]$ ./run
hello

[CaseZheng@VM_187_252_centos Tmp]$ rm run
[CaseZheng@VM_187_252_centos Tmp]$ ls
hello.c hello.o libhello.a main.c
[CaseZheng@VM_187_252_centos Tmp]$ gcc -o run main.c -L./ -lhello
[CaseZheng@VM_187_252_centos Tmp]$ ls
hello.c hello.o libhello.a main.c run
[CaseZheng@VM_187_252_centos Tmp]$ ./run
hello

检查包的正确性

如果碰到error adding symbols: Archive has no index; run ranlib to add one可以使用ar -t检查打包的正确性

1
2
[CaseZheng@VM_187_252_centos Tmp]$ ar -t libhello.a
hello.o

确认无误的话在使用file查看被打包文件类型

1
2
[CaseZheng@VM_187_252_centos Tmp]$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

文件类型没问题再使用nm导出符号检查

1
2
3
[CaseZheng@VM_187_252_centos Tmp]$ nm hello.o
U puts
0000000000000000 T _Z11print_hellov

动态链接库

生成动态链接库

使用-fPIC或-fpic选项使gcc生成动态链接库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
[CaseZheng@VM_187_252_centos Tmp]$ cat hello.h
#ifndef _HELLO_H_
#define _HELLO_H_

void print_hello();

#endif
[CaseZheng@VM_187_252_centos Tmp]$ cat hello.cpp
#include <stdio.h>
#include "hello.h"

void print_hello()
{
printf("hello\n");
}
[CaseZheng@VM_187_252_centos Tmp]$ g++ -fPIC -shared -o libhello.so.1.0.0 hello.cpp hello.h
[CaseZheng@VM_187_252_centos Tmp]$ ln -s libhello.so.1.0.0 libhello.so
[CaseZheng@VM_187_252_centos Tmp]$ ls
hello.cpp hello.h libhello.so libhello.so.1.0.0 main.cpp
[CaseZheng@VM_187_252_centos Tmp]$ g++ -o run main.cpp hello.h -L./ -lhello
[CaseZheng@VM_187_252_centos Tmp]$ ./run
./run: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory
[CaseZheng@VM_187_252_centos Tmp]$ ldd ./run
linux-vdso.so.1 => (0x00007ffc6fb99000)
libhello.so => not found
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f35af31c000)
libm.so.6 => /lib64/libm.so.6 (0x00007f35af01a000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f35aee04000)
libc.so.6 => /lib64/libc.so.6 (0x00007f35aea37000)
/lib64/ld-linux-x86-64.so.2 (0x00007f35af623000)
[CaseZheng@VM_187_252_centos Tmp]$ sudo vim /etc/ld.so.conf
[CaseZheng@VM_187_252_centos Tmp]$ cat /etc/ld.so.conf
include ld.so.conf.d/*.conf
/usr/local/lib
.
[CaseZheng@VM_187_252_centos Tmp]$ sudo ldconfig
[CaseZheng@VM_187_252_centos Tmp]$ ldd ./run
linux-vdso.so.1 => (0x00007ffc251b6000)
libhello.so => ./libhello.so (0x00007fcc32399000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fcc32092000)
libm.so.6 => /lib64/libm.so.6 (0x00007fcc31d90000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fcc31b7a000)
libc.so.6 => /lib64/libc.so.6 (0x00007fcc317ad000)
/lib64/ld-linux-x86-64.so.2 (0x00007fcc3259b000)
[CaseZheng@VM_187_252_centos Tmp]$ ./run
hello

-Wl,-soname编译选项

-Wl选项告知编译器将后面的参数传递给链接器.
-soname指定动态库的soname(简单共享名,Short for shared object name)

利用soname可以提供动态库的兼容性,当升级一个库时新库和旧库兼容,则可以直接使用相同的soname,如果不兼容换个新的soname则不会影响旧库链接生成的程序.使得升级动态库变的容易.

可以通过readelf -d查看动态库的soname

1
2
3
4
[CaseZheng@VM_187_252_centos ~]$ readelf -d /usr/lib/libz.so.1.2.7 
......
0x0000000e (SONAME) Library soname: [libz.so.1]
......

可以看到前面的动态库链接后libhello.so链接到libhello.so,当新的动态库不兼容旧的动态库时就会影响到旧库链接生成的程序,而指定soname后链接到libhello.so.1.0.0,动态库升级后不会影响到旧库链接生成的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[CaseZheng@VM_187_252_centos Tmp]$ ldd ./run 
linux-vdso.so.1 => (0x00007ffee638a000)
libhello.so => ./libhello.so (0x00007fa8c214f000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fa8c1e48000)
libm.so.6 => /lib64/libm.so.6 (0x00007fa8c1b46000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fa8c1930000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa8c1563000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa8c2351000)
[CaseZheng@VM_187_252_centos Tmp]$ g++ -fPIC -shared -Wl,-soname,libhello.so.1.0.0 -o libhello.so.1.0.0 hello.cpp hello.h
[CaseZheng@VM_187_252_centos Tmp]$ g++ -o run main.cpp hello.h -L./ -lhello
[CaseZheng@VM_187_252_centos Tmp]$ ./run
hello
[CaseZheng@VM_187_252_centos Tmp]$ ldd ./run
linux-vdso.so.1 => (0x00007ffc921b7000)
libhello.so.1.0.0 => ./libhello.so.1.0.0 (0x00007fc55f726000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fc55f41f000)
libm.so.6 => /lib64/libm.so.6 (0x00007fc55f11d000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fc55ef07000)
libc.so.6 => /lib64/libc.so.6 (0x00007fc55eb3a000)
/lib64/ld-linux-x86-64.so.2 (0x00007fc55f928000)
[CaseZheng@VM_187_252_centos Tmp]$ readelf -d libhello.so.1.0.0
......
0x000000000000000e (SONAME) Library soname: [libhello.so.1.0.0]
......
[CaseZheng@VM_187_252_centos Tmp]$ readelf -d libhello.so
......
0x000000000000000e (SONAME) Library soname: [libhello.so.1.0.0]
......

还可以兼容版本使用同一个soname,当有新的不兼容版本时再更换soname

优先使用静态链接的方法

使用动态库的不便之处在于服务部署环境必须安装了该动态库,且版本必须一致或兼容,否则程序无法运行,而静态库则不存在这个问题,当既有动态库,又有静态库时程序默认优先使用动态库,使用-Wl,-Bstatic可以指定优先使用静态库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[CaseZheng@VM_187_252_centos Tmp]$ g++ -o run main.cpp hello.h -L./ -lhello
[CaseZheng@VM_187_252_centos Tmp]$ ./run
hello
[CaseZheng@VM_187_252_centos Tmp]$ ls
hello.cpp hello.h libhello.so libhello.so.1.0.0 main.cpp run
[CaseZheng@VM_187_252_centos Tmp]$ ldd ./run
linux-vdso.so.1 => (0x00007ffc20f4f000)
libhello.so.1.0.0 => ./libhello.so.1.0.0 (0x00007f9025501000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007f90251fa000)
libm.so.6 => /lib64/libm.so.6 (0x00007f9024ef8000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f9024ce2000)
libc.so.6 => /lib64/libc.so.6 (0x00007f9024915000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9025703000)
[CaseZheng@VM_187_252_centos Tmp]$ g++ -c hello.cpp -o hello.o
[CaseZheng@VM_187_252_centos Tmp]$ ar -rcs libhello.a hello.o
[CaseZheng@VM_187_252_centos Tmp]$ g++ -o run main.cpp hello.h -Wl,-Bstatic -L./ -lhello
/usr/bin/ld: cannot find -lstdc++
/usr/bin/ld: cannot find -lgcc_s
/usr/bin/ld: cannot find -lgcc_s
collect2: error: ld returned 1 exit status
[CaseZheng@VM_187_252_centos Tmp]$ g++ -o run main.cpp hello.h -L./ -Wl,-Bstatic -lhello
/usr/bin/ld: cannot find -lstdc++
/usr/bin/ld: cannot find -lgcc_s
/usr/bin/ld: cannot find -lgcc_s
collect2: error: ld returned 1 exit status

找不到库是因为指定-Wl,-Bstatic后所有库都强制使用了静态库导致的,添加-Wl,-Bdynamic让别的库可以使用动态库

1
2
3
4
5
6
7
8
9
10
[CaseZheng@VM_187_252_centos Tmp]$ g++ -o run main.cpp hello.h -L./ -Wl,-Bstatic -lhello -Wl,-Bdynamic
[CaseZheng@VM_187_252_centos Tmp]$ ./run
hello
[CaseZheng@VM_187_252_centos Tmp]$ ldd ./run
linux-vdso.so.1 => (0x00007ffcf61cf000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fa076f64000)
libm.so.6 => /lib64/libm.so.6 (0x00007fa076c62000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fa076a4c000)
libc.so.6 => /lib64/libc.so.6 (0x00007fa07667f000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa07726b000)