GCC工具的常用指令
1、安装
sudo apt install gcc
gcc --version
2、编译的四个阶段
C源码编译可以细分为四个阶段,分别为:
-
预处理Preprocession
-
编译Compiling
-
汇编Assembling
-
链接Linking
正常情况下,GCC会直接走完4个阶段,然后输出可执行文件。但是使用不同的指令,我们也可以分阶段编译,并且对每个阶段进行控制。
2.1 预处理
预处理阶段,展开宏定义、插头文件代码。
对于预处理阶段,我们可以使用-E
使预处理结束编译器即停止。
#-o选项,将预处理后文件输出到circle.i文件中;否则,直接输出到标准输出流
gcc -E -o circle.i circle.c
如果嫌弃预处理后文件庞杂难懂,可以使用-C
选项保留源文件和头文件的注释。
gcc -E -C -o circle.i circle.c
注意,预处理后文件默认是xxx.i
形式。
预处理阶段主要做的是头文件和宏展开工作,下面几个指令主要用于头文件寻址和利用宏实现条件编译的指令:
1 条件编译
gcc -E -Dname=banana circle.c
-Dname
配合源码中的宏#ifdef banana
实现条件编译。
2 头文件寻址
# -Idirectory1:directory2:directory3... 可以指定除标准include目录以外的头文件目录
gcc -E -I/usr/local/myinclude:/home/myinclude/ circle.c
# -iquote dir1:dir2:dir3... 可以指定双引号头文件目录
# #include "cirfunction.h"
gcc -iquote /usr/local/header1:/usr/local/header2 circle.c
更多的指令可以查询手册,但是我想以上两个基本可以一招鲜吃遍天了。
3 include头文件搜索顺序
- 1 源文件当前目录内
- 2
-iqueto
选项执行的目录,这个选项只对双引号(""
)头文件有效 - 3
-Idir1:dir2
指定目录 - 4
CPATH
环境变量指定目录 - 5
isystem
选项所指定的目录 - 6
C_INCLUDE_PATH
环境变量指定的目录 - 7 系统默认include目录
以上优先级由上至下依次递减。
2.2 编译
编译阶段将预处理后文件翻译成汇编语言文件。汇编语言几乎等同于机器语言,但是更易于阅读。它避免程序员使用0x2ffeff 0x2238fa
之类的纯二进制指令,而是使用诸如add
、load
等易于识别、记忆的词汇代替。使用汇编器可以翻译汇编语言为机器语言。
常用指令为:
gcc -S -fverbose-asm -o circle.s circle.c
-S
选项强制在编译后停止,输出xxx.s
形式的汇编文件,-fverbose-asm
可以将C源码的变量名作为输出文件的注释。
2.3 汇编
承接上一步,汇编阶段将汇编源文件编译成机器语言文件,即对象文件xxx.o
。该文件既包含源码机器语言,还包含描述链接函数的符号表(symbol table)。符号表是为链接阶段准备的。
常用指令:
# -c选项确保生成circle.o对象文件,且不会进行到链接阶段
#一般掌握这个也就够了
gcc -c circle.c
额外的,如果使用-Wa
选项可以对汇编器(不是gcc,gcc只是在汇编阶段调用了汇编器)传递指令,譬如:
gcc -v -o circle -Wa,-as=circle.sym,-L circle.c
以上指令,紧接在-Wa
后面的选项(无空格,逗号分割)就是专门传给汇编器的指令选项。-as=circle.sym
表示单独保存符号表(symbol table)到circle.sym文件中。-L
表示符号表包含本地符号。
2.4 链接(重要)
链接阶段将对象文件引用的外部对象和一些C标准函数都加入进来,是程序能够最终运行。
前三个阶段一般程序员都不会特别关系,也很少出错,而链接阶段却是必须要了解的。
我们一般引用的C标准函数使用xxx.h
的头文件,这些文件主要是声明函数,做一些宏定义之类的。函数对象本身一般放在libc.a
文件中,如果是动态共享链接则放在libc.so
文件中(so——shared object,有共享对象的意思)。动态链接是最常用的链接方式。
如果只引用标准库,使用gcc -o circle circle.c
即可完成编译,但是使用非标准库则需要注意外部链接寻址的问题:
1 链接库在gcc标准搜索目录中
这种情况直接引用即可,gcc -o circle.o circle.c -lncurses
,该指令使用-l
链接ncurses
库,两者需要写在一起,无空格。
2 链接库在默认库搜索目录以外
gcc默认标准库目录一般为/usr/lib
、/usr/lib64
,若不在此目录,还有3种方案:
# 1 直接引用对象文件地址
# 一般函数对象文件,都是在库名前加一个'lib',静态链接后缀为'.a',动态为'.so'
# 此方式,动、静链接都可,取决于使用什么文件
gcc -o circle circle.c /usr/local/lib/libncurses.a
# 2 使用'-L'指令添加一个搜索目录——/usr/local/lib
# 如果ncurses是动态链接库libncurses.so,还需要使用-Wl,rpath选项告诉程序在执行时去哪链接动态库
# -L是给链接器使用的,主编译;rpath指令是给程序用的,能够实时找到动态链接库位置
gcc -o circle circle.c -L/usr/local/lib -Wl,rpath=/usr/local/lib -lncurses
# 需要注意的是,如果想用-l指令做静态链接,则需要添加-static
gcc -static -o circle circle.c -L/usr/local/lib -lncurses
# 3 将链接库路径添加到环境变量'LIBRARY_PATH'中
export LIBRARY_PATH=/usr/local/lib:$LIBRARY_PATH
2.5 保留中间文件和语法检查指令
1 保留所有中间文件
# 保存所有的中间文件——circle.i circle.s circle.o
gcc -save-temps -o circle circle.c
2 只做语法检查
# 只是检查源文件语法,不做编译
gcc -fsyntax-only circle.c
3、动态共享链接库
动态链接库会在程序实际运行时,才开始调用,相比静态链接库直接将引用函数的指令直接插入可执行文件的做法,有下面几点优势:
- 引用库不用直接插入,可执行文件更小
- 只要接口不变,共享库的实现可以随时更新,静态链接则只能重新编译才能享受更新
- 有效内存的使用效率更高
1 创建共享链接库
我们把circle.c
依赖的circulararea.c
做成共享链接文件
# 先生成对象文件,再生成.so文件
# -fpic,将源码编译成位置独立代码,否则链接库的外部变量就无法引用而报错
gcc -c -fpic circulararea.c
gcc -shared -o libcirculararea.so circulararea.o
2 使用自定义链接库
和其他链接库使用方式完全一样:
gcc -o circle circle.c libcirculararea.so -lncurses
也可以将libcirculararea.so
文件安装在标准目录中,则可以直接使用-lcirculararea
来引用。
4、警告的控制
警告(Warning)不同于错误(error),编译时出现警告并不影响gcc继续工作,但是不建议完全忽视他们。
再编译时,可以使用-W
开头的选项细致的控制警告条目——哪些问题弹出警告,同时忽视哪些问题的警告。
小栗子,
# switch-default表示使用switch表达式时候,忘了些default就发出警告
gcc -Wswitch-default circle.c
使用-Wall
,虽然不会检查所有的(all)警告情况,但是已经可以穷尽大部分警告了:
gcc -Wall circle.c
使用no-xxx
形式的选项,可以取消对应的警告,譬如
# 取消switch-default警告
gcc -Wall -Wno-switch-default circle.c
5、调试选项与剖析器
1 调试选项
-g
选项使生成的对象文件或可执行文件包含符号表和源码行号信息,利用这些结合gdb便于逐步调试程序。
gcc -g -o circle circle.c -lncurses
这样生成文件会非常臃肿,只适用于调试。
2 剖析器
对于GNU剖析器gprof,编译时使用-pg
选项(这里的g代表gnu的意思,和调试用的-g无关),会生成pmon.out
文件。之后使用gprof剖析器分析,可以得到调用图,显示函数之间的调用细节等。
# -pg生成剖析文件,-g提供源码行号信息
gcc -pg -g circle.c -lncurses
6、优化选项
gcc提供优化选项,使用-O
选项选择不同的优化等级,或者使用-f
进行专项的优化。
6.1 优化等级选择
从-O0
、-O1
一直到-O3
选项,优化等级一次递增。
-O0
,不采取优化-O1
,会优化循环、合并一致常量等,使文件更小、执行更快的浅优化,因此编译时间不会过分拖沓-O2
,几乎应用全部支持的优化技术,譬如公共子表达式删除、指令重排、数据流分析,消耗时间相当长的深度优化-O3
,包含-O2
全部优化,且使用inline
函数优化寄存器变量分配问题-Os
,类似于O2等级优化,但是不会为了让生成程序更小,不会应用使用导致代码字节增大的技术
典型指令,
gcc -Wall -O3 -o circle circle.c curculararea.c -lncurses
6.2 -f控制优化细节
-f
即flag。它可以单独使用或者再-O
的基础上执行细节控制——添加或者取消某个特定的优化技术。
典型指令,
# no-inline-functions表示取消inline函数优化,unroll-loops表示重写循环语句,使用迭代代替循环
gcc -Wall -O3 -fno-inline-functions -funroll-loops -o circle circle.c circulararea.c -lncurses
6.3 优化的取舍
优化可以带来很多优势——程序更小、执行更快、内存效率更高。但是优化编译会造成很长编译时间,指令重排、代码合并等会导致之后的代码变得难以理解,有些甚至会改变代码的运行逻辑,造成无法调试。想要获得最优性能,还是针对代码细节进行不同程度的优化尝试,反复试验,确定最佳性能。
7、常见编译相关的环境变量
C_PATH,C_INCLUDE_PATH:逗号分割的目录列表,设置头文件的搜索位置
LIBRARY_PATH:链接库的搜索位置,这是给链接器用的。第三方链接库如果没指定在编译时链接阶段会报错
LD_LIBRARY_PATH:动态链接库的搜索位置,由可执行文件读取该路径,这是给程序本身用的。第三方库不指定,则在运行时报错,找不到库