GCC基本操作
前言
该篇文章是主要参照播布客的GCC编译器使用入门培训视频所写。播布客的该视频对如今而言,虽然有些古早,但讲解内容相对细致,并且结合官方文档讲解,仍然是不错的gcc入门视频。
开始
使用gcc编译c程序生成hello可执行文件
gcc -Wall hello.c -o hello
使用路径执行程序
(shell默认以程序名时,调用$PATH
环境变量的路径中的程序,可以使用export PATH=$PATH:<path>
来改变环境变量)
./hello
编译多个c程序
gcc -Wall main.c hello.c -o hello
#gcc file.x [file.x] [...] -o <outfile>
gcc中面对头文件会自动在系统文件目录寻找头文件。所以,一般不用主动添加头文件目录
#include "file"
而""默认从./
路径先寻找,#include <file>
而<>直接在系统文件目录寻找
使用-v可打印编译过程的信息
gcc -v <file>
编译过程
gcc 将文件编译成可执行文件需要4个步骤:过预处理(preprocessing)、编译(compilation)、汇编(assembly)和链接(linking)
上图是编译过程,通过-E
、-S
、-c
可以将它编译到指定阶段,而可以使用对应文件(.x -o file
)编译器便自动执行剩下的过程,生成可执行文件。通常将编译拆分到目标文件即-c
,因为编译过程中的错误在这阶段产生。
还有就是使用-save-temps
可将编译过程的产生的临时文件保存下来
gcc hello.c -save-temps -o hello
#生成 hello.i hello.s hello.o 文件
基本编译
编译目标文件
通过将各个源文件编译成.o目标文件
(将源文件编译成机器码)
gcc -Wall -c hello.c -o hello.o
gcc -Wall -c main.c -o main.o
然后将各个目标文件链接生成可执行文件
(链接文件一般遵循函数调用顺序排列xx1.o xx2.o
,但现代编译器不需要我们考虑这些文件次序)
gcc main.o hello.o -o hello
在大型程序中,包含N多个文件,通过将它们一个个编译在链接,当修改文件时就不需要编译所以文件,只需编译部分文件。
(使用make工具时,通过对比编译的时间戳,当原程序的时间比.o文件新时,重新编译该.o,再链接)
链接lib
lib:库文件,目标文件集合体。
- 静态库会将对应调用的机器码拷贝到可执行文件中。
- 动态库会在程序运行的时候将地址空间映射在其中。
面对多程序调用该动态库的情况,动态库可以多个应用共享其地址空间,可以节省内存空间的占用大小,防止内存浪费。
并且当动态库发生改变时,不用重新编译程序,只需要重新编译动态库。
通过路径链接库文件
gcc -Wall main.c /usr/libm.a -o calc
通过-l<name>
链接库文件
gcc -Wall main.c -lm -o calc
gcc 会在系统的库目录下和当前目录下寻找对应的库文件,并且它的文件命名为 lib<name>.a
(需要注意的是库文件要在源文件的后面)
链接动态库同理,不过动态库运行后要调用系统库路径,所以需要配置环境变量或者放在对应系统文件夹中。
gcc -Wall main.c /usr/libm.so -o calc
gcc -Wall main.c -lm -o calc
当动态库和静态库同名时,会优先调用动态库,编译程序。
所以可以使用-static
来强制调用静态库
搜索路径
gcc会默认在系统路径寻找头文件和库文件它们通常的路径为
#include
/usr/local/include
/usr/include
#编译器下的头文件
#lib
/usr/lib
/lib
/usr/local/lib
#编译器下的库文件
可以通过echo 'main(){}'| gcc -E -v -
查询头文件和库文件路径
通过指定路径添加搜索路径
#头文件路径
gcc -Wall main.c -Iinclude
# -I<include_path>
#库文件路径
gcc -Wall main.c -Llib
# -L<lib_path>
通常来说可以将需要外部的头文件和库文件放在对应的系统目录文件下,方便调用,或者在项目中包含这写文件通过-I和-L来调用,但需要注意的是要使用相对路径,绝对路径会造成移植难。
不过也可以通过添加环境变量的方式将一些目录,添加在搜索中
头文件环境变量
C_INCLUDE_PATH
CPLUS_INCLUDE_PATH
库文件环境变量
LD_LIBRARY_PATH 动态库
LIBRARY_PATH 静态库
添加临时环境变量(环境变量以:
分隔)
export <环境变量>=$<环境变量>:<ptah>
通过该命令写在用户的shell配置或者系统级的配置文件,每次登录或启动新的shell会话时便会自动设置。
将上述命令写在~/.bashrc
文件下,为该用户享有
写在/etc/profile
文件下,所有用户享有
上述三种方法推荐使用-I和-L方便移植不会污染系统目录,当然如果是通用类型的或是开发板上的可以放在系统目录下,而对于动态库可以考虑系统目录和环境变量。
编译库文件
创建静态库
ar cr libNAME.a file.o file2.o ... filen.o
查看静态库中的目标文件
ar t libNAME.a
ar
是一个用于处理静态库和目标文件的工具
c
选项代表创建一个新的库文件,如果库文件不存在,则会创建一个新文件;如果库文件已存在,则会清空库中的旧内容并添加新的模块。r
选项代表将指定的模块添加到库文件中。通常这个选项后面会跟上要添加的目标文件名。
创建动态库
gcc -shared -o libNAME.so file.o ...
使用标准
使用-ansi
,遵循ANSI/ISO标准,以忽略GUN c标准
gcc -ansi main.c -o an
#例如asm为gun c 的关键词,使用该名作为变量将无法通过编译,需要选择ANSI/ISO标准
使用-pedantic
, 将严格遵守语言标准,不符合标准将出现警告
gcc -ansi -pedantic main.c -o an
#编译器尽管使用-ansi,但仍然会忽略一些支持gun c标准的一些特性,可以编译通过,使用该选项便会严格遵守,发出警告
使用-std=c<version>
,指定使用该版本的c标准,可以通过这个查询自己版本下的默认标准GCC Releases - GNU Project,具体可以参考GCC编译器的-std选项_gcc std-CSDN博客
gcc -std=c17 main.c -o hell17
(gcc默认使用的标准是-std=gun<version>
)
警告选项
-Wcomment
:这个选项会在注释中使用/*
开始序列时发出警告,或者在//
注释中使用反斜杠换行符时发出警告。这有助于识别并修正注释中的格式问题。防止注释嵌套。(可能导致意外注释代码)
-Wformat
:会检查printf和scanf这样的格式化函数的参数,确保格式字符串与参数匹配。如果使用不正确,编译器会产生警告。
-Wunused
:会发出未使用变量、函数、参数等的警告。这有助于识别那些可能无意义的代码,或者可能是开发者忘记使用的代码。
-Wimplicit
:用于警告隐式声明的函数,即在没有显式声明函数原型的情况下使用了函数。这通常是一个不良的编程习惯,因为它可能导致不可预见的行为。
-Wreturn-type
:警告函数返回类型与实际返回值不匹配的情况。这有助于避免因类型不匹配而导致的潜在错误。
-Wall
:它会启用大部分警告,帮助开发者发现代码中可能存在的问题。
-Wall
选项基本可以发现常见潜在错误,满足基本的使用需求了,有需要还可以包含一下选项:
-W
:这个选项是启用所有警告的简写形式。它相当于-Wall
、-Wextra
和-Wdeprecated
的组合,用于显示大多数常见的警告。-Wconversion
:这个选项会在可能发生精度损失的类型转换时发出警告。例如,将一个较大的整型转换为较小的整型,或者将浮点数转换为整数,都可能导致精度损失。-Wshadow
:当局部变量隐藏了全局变量或在相同作用域内的其他变量时,这个选项会发出警告。这通常是一个编程错误,因为它可能导致意外的行为。-Wcast-qual
:当对指向类型限定符(如const)的指针进行不合适的转换时,这个选项会发出警告。-Wwrite-strings
:这个选项在写入到字符串字面量时发出警告,因为这是未定义行为。-Wtraditional
:这个选项启用了传统的C语言警告。这包括一些在现代C语言中不推荐使用的特性。
外部宏定义
使用-D<macro_var>[=value]
可以外部将宏传入程序中,可以用于配置程序,开关代码块等选项
#缺省默认值为1,不能覆盖已有宏定义的值
gcc -Wall dtest.c -Dc=120 -o dtest
#因为()对shell是有用的,可添加""或者''可以传入算式,宏函数等
gcc -Wall dtest.c -Dc="(120+160)" -o dtest
gcc -Wall dtest.c -D"c=(120+160)" -o dtest
gcc -Wall dtest.c -o dtest -D'add(a,b)=(a*b)'
#添加\"\"传入字符串
gcc -Wall dtest.c -o dtest -Dok=\"ok\"
Debug
通过使用-g
选项可以生成操作系统原生格式的调试信息(stabs、COFF、XCOFF或DWARF)。GDB可以使用这些调试信息。
gcc null.c -o null -g
gdb null
假设包含调试的可执行文件是包含段错误(Segmentation fault (core dumped)
)等可以令程序崩溃的错误,我们可以让Linux系统在奔溃前生成core文件,以便迅速定位到错误位置。
core文件是Linux系统中用于记录程序崩溃时的内存映像和信息的文件,通常在进程异常终止时由系统自动生成。
#查看可生成core文件大小,为0时不生成
ulimit -c
#设置core文件大小,当最小设置为4之后才会生成core文件
ulimit -c [filesize]
#设置core文件,不限大小
ulimit -c unlimited
#不过这些都是临时的,需要永久生效得同设置环境变量一样
#设置在用户的shell配置或者系统级的配置文件中
不过设置时要指定路径和命名规则,才可以使用,在/proc/sys/kernel/core_pattern中修改,还要注意的是路径位置以免没有权限创建文件,这边就不赘述了
然后执行gdb [exec file] [core file]
查看错误位置
gdb ./null core-null-6847-1710914631
还需要补充的是
-g0
:不生成调试信息,相当于没有使用-g;
-g1
:生成最小的调试信息,足够在不打算调试的程序中进行堆栈查看。最小调试信息包括函数描述,外部变量,行数表,但不包括局部变量信息。
-g2
:默认-g的调试级别;
-g3
:相对-g,生成额外的信息,例如所有的宏定义;
优化
-O0
: 默认的优化选项,减少编译时间和生成完整的调试信息。
-O1
:它执行了一些基本的优化,如删除未使用的代码、简化控制流等。
-O2
:在-O1
的基础上进行了更多的优化,包括对循环的优化、对全局变量的优化等。
-O3
:会进行更多的优化,如函数内联、循环展开等。但是,这也可能导致编译时间增加和生成的代码变大。
-Os
:会优化生成的代码以减小其大小,而不是优化其执行速度。这在嵌入式系统或存储空间受限的环境中非常有用。
-Ofast
:会启用所有不会使代码行为变得不确定的优化,包括那些可能会改变程序行为的优化。
-Og
:会启用一些有助于调试的优化,如保留变量的原始值、不进行函数内联等。
-funroll-loops
:用于控制循环展开的优化,一般来说,如果循环较小且没有依赖关系或副作用,那么开启循环展开可以带来性能上的提升。
在使用这些优化选项时,需要注意以下几点:
- 优化选项会影响编译时间和生成的代码大小。一般来说,优化级别越高,编译时间越长,生成的代码也越大。
- 优化选项可能会改变程序的行为。例如,
-Ofast
选项可能会启用某些改变程序行为的优化。因此,在开发过程中,建议使用-Og
选项以便于调试。 - 在某些情况下,优化可能会导致程序崩溃或者产生不正确的结果。因此,在启用优化选项后,需要对程序进行充分的测试,以确保其正确性。
很多项目的线上版本都是使-O2 -g
的编译选项进行编译,以便发生问题的时候容易定位。但这有一个很大的弊端就是目标文件会比不开启调试信息的情况下大很多,所以一般对外发布的软件都是不含有调试信息的release版本,同时也会发布含有调试信息的debug版本,两者的性能是一样的只是debug多了调试信息而已。
杂项
gcc相关工具/命令
file是一个标准的Unix/Linux命令行工具,它的主要功能是根据文件的内容和结构来判断文件的类型。
通常用于诊断问题,例如确定某个文件是否损坏,或者一个文件是否符合预期的格式。
ldd是Linux下的一个shell脚本,可以查看程序运行的动态库依赖。
nm 是一个标准的Unix/Linux命令行工具,是一个展示二进制文件中符号信息的工具,用于列出对象文件、可执行文件或对象文件库中定义的函数和全局变量等信息。
它可以帮助开发者理解和分析程序的结构,特别是在解决链接错误和进行程序优化时。通过nm命令,开发者可以看到哪些函数和变量被导出,哪些是内部使用的,以及它们在内存中的地址。
测试工具
gcc 使用-pg
会向可执行文件插入一些调试代码,收集程序的性能分析数据。
gcc -Wall -pg collatz.c -o collatz
当我们执行后会产生gmon.out文件,使用gprof工具后,便可以获取对应的函数调用次序等性能分析数据
gprof collatz
通过性能分析,开发者可以定位到程序中效率低下的部分,并对这些部分进行针对性的优化。
可以通过-fprofile-arcs
和 -ftest-coverage
选项配合gcov工具对执行次数等进行分析,测试代码覆盖率
gcc cov.c -fprofile-arcs -ftest-coverage -o cov
会产生对应可执行文件名<ELF>.gcno
,当运行程序后便产生<ELF>.gcda
文件,使用gcov调用源码便会产生分析文件<src_file>.c.gcov
gcov cov.c
附录
常用编译选项
gcc 编译选项
-Wall
(Warning all) 打印所有编译器警告(产生在编译过程)
-o
(out) 输出文件,根据不同阶段可输出不同类型文件
-v
(verbose)打印编译过程的详细信息,不加文件时打印gcc应用信息
-c
(compile)只编译不产生目标文件,默认文件名为源文件名.o
-l<name>
链接系统目录,当前目录,和指定库目录的对应名为lib<name>.a/so
的库文件
-I<include_path>
搜索指定路径下的头文件
-L<lib_path>
搜索指定路径下的库文件
#静态库
ar cr lib<name>.a <file>.o ...
-shared
将多个.o文件合并成动态库文件
-static
强制调用静态库文件(当与动态库文件同名时)
-std=c<version>、-std=c++<version>
指定对应版本的标准,(c++使用g++)
-ansi
遵循ANSI/ISO标准,当gcc不使用gun c的特性时,需要直接声明(为了可移植性需要声明该选项)
-pedantic
严格遵守语言标准,不符合标准的将发出警告,(通常和-ansi连用提高代码的可移植性和健壮性)
-D<macro_var>[=value]
传入一个宏定义以及它的值,缺省默认为1,但不能覆盖已有宏定义的值。因为()
对shell有意义,所以可以使用''
或者""
传入宏函数-D'name(args...)=definition'
通过\"\"
传入字符串
-g
生成操作系统原生格式的调试信息包含到可执行文件中,可以使用gdb进行调试
-O<LEVEL>
gcc提供level 0~3级别的优化,调试时使用-O0
,投入生成时-O2
是常用的优化选项。