LV02-03-GCC-编译工具

本文主要是GCC编译器基础知识的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---
点击查看测试文件详情
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include "func1.h"
#include "func2.h"

int main(int argc, const char *argv[])
{
printf("This is main file!\n");
func1();
func2();
return 0;
}
1
2
3
4
5
6
#include <stdio.h>

void func1(void)
{
printf("This is func1.c file\n");
}
1
2
3
4
5
6
#include <stdio.h>

void func2(void)
{
printf("This is func2.c file\n");
}
1
2
3
4
5
6
#ifndef __FUNC1_H__
#define __FUNC1_H__

void func1(void);

#endif
1
2
3
4
5
6
#ifndef __FUNC2_H__
#define __FUNC2_H__

void func2(void);

#endif

一、 GCC 简介

GCC 全称为 GNU CC , GNU 项目中符合 ANSI C 标准的编译系统 ,可以编译如 C 、 C++ 、 Object C 、 Java 、 Fortran 、 Pascal 、 Modula-3 和 Ada 等多种语言。

它是可以在多种硬体平台上编译出可执行程序的超级编译器,其执行效率与一般的编译器相比平均效率要高 20%~30% 。它是一个交叉平台编译器 ,适合在嵌入式领域进行开发编译。GCC 还能运行在不同的操作系统上,如 Linux 、 Solaris 、 Windows 等。

点击查看 GCC 支持的文件后缀名解释
.cC原始程序
.C/.cc/.cxx C++原始程序
.h预处理文件(头文件)
.i已经过预处理的C原始程序
.ii已经过预处理的C++原始程序
.s/.S汇编语言原始程序
.o目标文件
.a/.so编译后的库文件
.mObjective-C源代码文件
GCC官网 http://gcc.gnu.org/

二、编译器主要组成

一个编译器主要的组成部分如下所示:

分析器 分析器将源语言程序代码转换为汇编语言。因为要从一种格式转换为另一种格式(C到汇编),所以分析器需要知道目标机器的汇编语言。
汇编器 汇编器将汇编语言代码转换为CPU可以执行字节码。
链接器 链接器将汇编器生成的单独的目标文件组合成可执行的应用程序。链接器需要知道这种目标格式以便工作。
标准C库核心的C函数都有一个主要的C库来提供。如果在应用程序中用到了C库中的函数,这个库就会通过链接器和源代码连接来生成最终的可执行程序。

三、GCC 编译过程

GCC 编译的流程有四个步骤,以 test.c 文件为例,来看一看最终的可执行程序 a.out 是怎么来的吧。

image-20220919223139549

【编译命令】

1
2
3
4
gcc -E test.c -o test.i # 预处理,得到预处理文件 *.i
gcc -S test.i -o test.s # 编译, 得到汇编文件 *.s
gcc -c test.s -o test.o # 汇编, 得到二进制文件 *.o
gcc test.o -o test # 链接, 得到可执行文件 *.out

【GCC编译步骤】

  • (1) 预处理( Pre-Processing ) :预处理主要是进行宏的替换,例如对 #include 和 #define 等进行处理,需要包含的进行包含,需要展开的宏进行展开等,所以在后续进行编译的时候是没有 include 和 define 的,所以 .i 文件里都是一些库函数的声明。

  • (2) 编译( Compiling ):这个阶段编译器主要做词法分析、语法分析、语义分析等,在检查无错误后后,把代码翻译成汇编语言。

  • (3) 汇编( Assembling ):汇编器 as 将 汇编语言文件翻译成机器语言。

  • (4) 链接( Linking ):链接各个库,得到最终的可执行文件。

四、GCC选项

1.基本选项

在 GCC 编译的四个步骤中,我们单独进行每一步的时候都需要使用相应的参数选项:

选项 说明
-E 只激活预处理,不生成文件,需要把它重定向到一个输出文件里面。
例如,gcc -E hello.c > hello.txt
-S 只激活预处理和编译,就是指把文件编译成为汇编代码。
例如,gcc -S hello.c # 将生成.s的汇编代码
-c 只激活预处理,编译,和汇编,也就是他只把程序做成obj文件。
例如,gcc -c hello.c # 将生成.o的obj文件
-o 指定目标名称,缺省的时候,gcc 编译出来的文件默认名称是a.out。
例如,gcc test.c -o test

【说明】

使用 GCC 编译器编译 C 或者 C++ 程序,必须要经历这 4 个过程,就是预处理,编译,汇编和链接。但考虑在实际使用中,用户可能并不关心程序的执行结果,只想快速得到最终的可执行程序,因此 gcc 和 g++ 都对此需求做了支持。所以我们可以通过 gcc 命令一步直接得到可执行文件:

1
2
3
4
5
6
gcc -c test.c         # 将会直接生成 a.out 可执行文件
gcc -c test.c -o test # 直接生成可执行文件,并且指定生成文件名为 test

# 或者直接省略 -c
gcc test.c # 将会直接生成 a.out 可执行文件
gcc test.c -o test # 直接生成可执行文件,并且指定生成文件名为 test

2.多文件编译

2.1使用格式

有的时候我们在一个 C 文件中调用了另一个 C文件中的函数,但是这些文件必须只有一个 main 函数,他们编译的时候要一起编译。一般格式如下:

1
2
# 一般我们会在main函数所在的源文件目录下编译
gcc <file1_dir>/file1.c <file2_dir>/file2.c ... main.c -o file_name

【参数说明】

  • file_dir :为相应源文件的路径。
  • file_name :表示最终生成的可执行文件的名称。

2.2使用实例

如果说源文件不在同一个目录下的话,我们编译需要加上 c 文件的路径,否则 GCC 编译的时候是找不到源文件的,例如如下文件目录结构(此处仅为展示目录结构,用的并不是笔记开头的文件):

1
2
3
4
5
6
.
├── test1
│ ├── test1.c
├── test2
│ ├── test2.c
├── main.c

我们在 main.c 所在目录进行编译,编译的时候需要指明 test1.c 和 test2.c 的位置:

1
gcc ./test1/test1.c ./test2/test2.c main.c -o main

3. -I (大写的 i )选项

3.1使用格式

 -I (大写的 i )选项用于将指定头文件路径添加到 GCC 的头文件搜索路径中,一般使用格式如下:

1
2
-Idir                # 单个路径
-Idir1 -Idir2 ... # 多个路径,每个路径都要有 -I ,不同的 -I 之间用空格分隔开

【注意】

(1)-I 和 dir 直接可以有空格,也可以没有空格.

(2)这个参数是大写的 i ,有些字体下,看着比较容易与另一个字母混淆。

3.2使用实例

  • 测试文件及目录结构(笔记开头有文件详情)
1
2
3
4
5
6
7
8
.
├── dir1
│ └── func1.h
├── dir2
│ └── func2.h
├── func1.c
├── func2.c
├── main.c

我们直接使用如下命令编译:

1
gcc func1.c func2.c main.c -o main

这样会直接报错:

1
2
main.c:2:19: fatal error: func1.h: 没有那个文件或目录
compilation terminated.

我们加上 -I 选项:

1
gcc func1.c func2.c main.c -o main -I dir1 -I dir2

这个时候就可以正常编译啦,编译完成后会生成 main 可执行文件,我们直接在终端执行 ./main,则会看到如下输出:

1
2
3
This is main file!
This is func1.c file
This is func2.c file

4. -D 选项

有时候我们想在 c 源文件中使用 Makefile 中定义的某些变量,根据变量的取值做出不同的处理,比如 debug 开关、版本信息等,这时候我们可以通过 gcc 的 -D 选项来满足这一需求,它等同于在 C 文件中通过 #define 语句定义一个宏。

常用的场景就是用 -DDEBUG 定义DEBUG宏,然后文件中有 DEBUG 宏部分的相关信息,用个 -DDEBUG 在编译的时候来选择开启或关闭 DEBUG 。

4.1使用格式

GCC的 -D 选项就相当于是在 C 语言中定义了一个 #define 宏,一般格式如下:

1
gcc -D key[=value]
  • key :就是表示定义key这个宏
  • [=value] :表示定义的宏的值,这个是可选的,如果不写的话,宏的值默认是1

【注意】要是需要给宏一个值的话, 等号两端不能有空格。

4.2使用实例

4.2.1 GCC实例

  • 测试文件内容
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(int argc, const char *argv[])
{
printf("This is main file!\n");
#ifdef TEST
printf("TEST=%d\n", TEST);
#else
printf("TEST is not defined!\n");
#endif
return 0;
}

我们在终端执行以下命令编译程序,然后执行,会看到如下信息:

1
2
3
4
5
hk@vm:~/1sharedfiles/test$ gcc main.c 
hk@vm:~/1sharedfiles/test$ ./a.out
# 以下为执行可执行程序的输出信息
This is main file!
TEST is not defined!

然后我们加上 -D 选项,定义一个宏:

1
2
3
4
5
hk@vm:~/1sharedfiles/test$ gcc main.c -D TEST
hk@vm:~/1sharedfiles/test$ ./a.out
# 以下为执行可执行程序的输出信息
This is main file!
TEST=1

然后我们加上 -D 选项,定义一个带有值的宏:

1
2
3
4
5
hk@vm:~/1sharedfiles/test$ gcc main.c -D TEST=5
hk@vm:~/1sharedfiles/test$ ./a.out
# 以下为执行可执行程序的输出信息
This is main file!
TEST=5

4.2.2 Makefile实例

这部分是后来新加的,make相关知识可以看后边的笔记。

  • 测试文件内容
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(int argc, const char *argv[])
{
printf("This is main file!\n");
#ifdef TEST
printf("TEST=%d\n", TEST);
#else
printf("TEST is not defined!\n");
#endif
return 0;
}

我们编写一个 Makefile 文件,内容如下:

1
2
3
TEST = 8
all:
gcc main.c -Wall -o main -D TEST=$(TEST)

我们编译,并执行可执行文件,结果如下:

1
2
3
4
5
6
7
8
hk@vm:~/1sharedfiles/test$ make
# 以下为执行可执行程序的输出信息
gcc main.c -Wall -o main -D TEST=8
#-------------------------------------------
hk@vm:~/1sharedfiles/test$ ./main
# 以下为执行可执行程序的输出信息
This is main file!
TEST=8

5. -l (小写的 L)选项

5.1链接库

链接器把多个二进制的目标文件(object file)链接成一个单独的可执行文件。在链接过程中,它必须把符号(变量名、函数名等一些列标识符)用对应的数据的内存地址(变量地址、函数地址等)替代,以完成程序中多个模块的外部引用。

标准库的大部分函数通常放在静态链接库 libc.a 中(文件名后缀 .a 代表“achieve”,译为“获取”),或者放在用于共享的动态链接库 libc.so 中(文件名后缀 .so 代表“share object”,译为“共享对象”)。这些链接库一般位于 /lib/ 或 /usr/lib/,或者位于 GCC默认搜索的其他目录。

当使用 GCC 编译和链接程序时,GCC 默认会链接 libc.a 或者 libc.so,但是对于其他的库(例如非标准库、第三方库等),就需要手动添加。

5.2使用格式

-l (小写的 L)参数,用来指定程序要链接的库(库文件在/lib、/usr/lib和/usr/local/lib下),-l 参数紧接着就是库名,一般使用格式如下:

1
gcc -l<lib_name>
  • lib_name :表示要链接的库名称(不包括库的前缀和后缀)。

【注意】这个是小写的 L

5.3使用实例

我们在使用 math.h 中的数学函数的时候,会产生函数未定义错误,标准头文件 <math.h> 对应的数学库默认不会被链接,如果没有手动将它添加进来,就会发生函数未定义错误。

数学库的文件名是 libm.a。前缀 lib 和后缀 .a 是标准的,m 是库名称,GCC 会在 -l 选项后紧跟着的基本名称的基础上自动添加这些前缀、后缀

如下程序,调用了 math.h 中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <math.h> /* cos */

#define PI 3.14159265

int main(int argc, const char *argv[])
{
double param;
double result;
param = 60.0;
result = cos ( param * PI / 180.0 );
printf ("The cosine of %f degrees is %f.\n", param, result );
return 0;
}

然后我们在终端执行以下命令,我们先不加 -l 选项,编译程序:

1
2
3
4
hk@vm:~/1sharedfiles/test$ gcc main.c
/tmp/ccgF1ptl.o:在函数‘main’中:
main.c:(.text+0x3a):对‘cos’未定义的引用
collect2: error: ld returned 1 exit status

会发现有上边的报错信息出现,当我们加上 -l 选项的时候,再编译:

1
hk@vm:~/1sharedfiles/test$ gcc main.c -lm

这个时候就不会有信息输出,因为我们的程序编译没有任何问题了。

6. -L 选项

放在 /lib 和 /usr/lib 和 /usr/local/lib 里的库直接用 -l 参数就能链接了,但如果库文件没放在这三个目录里,而是放在其他目录里,这时我们只用-l参数的话,链接还是会出错,GCC在链接的时候并不知道除了这些地方的库外要链接的库在哪里。

6.1使用格式

-L 选项可以为GCC增加一个搜索链接库的目录,当我们生成了自己的链接库的时候,我们可以用这个参数将其添加到GCC的搜索路径下:

1
2
3
4
5
# 单个路径
gcc -L<dir>
# 多个路径
gcc -L<dir1> -L<dir2> ...
gcc -L<dir1>:<dir2>: ...
  • dir :这就是我们要添加的链接库的目录。

【注意】要是有多个链接库的目录要添加的话,可以使用多个 -L 选项,或者就是在一个-L选项中用冒号分隔不同的路径。

6.2使用实例

6.2.1文件准备

我们使用笔记开头的几个文件作为测试文件,我们先进行一个文件的准备:

1
2
3
mkdir include lib source
mv func1.c func2.c source
mv func1.h func2.h lib

准备完成后,所有文件目录结构如下:

1
2
3
4
5
6
7
8
9
.
├── include
│ ├── func1.h
│ └── func2.h
├── lib
├── main.c
└── source
├── func1.c
└── func2.c

6.2.2制作静态库

由于要链接库,所以要先只做静态库,后边有笔记专门记录静态库和动态库的制作,这里就是记录一个过程。

1
2
3
4
5
6
7
cd source/                  # 进入func1.c 和 func2.c 源文件所在目录

gcc -c func1.c -o func1.o # 编译func1.c,生成 func1.o
gcc -c func2.c -o func2.o # 编译func2.c,生成 func2.o

ar -rsv libfunc.a func1.o func2.o # 打包成静态库
mv libfunc.a ../lib/ # 拷贝到库文件目录下

6.2.3编译主函数源文件

此时我们开始编译主函数,需要注意的是,我们即便做了静态链接库,但是还是需要指定头文件的位置:

1
2
3
4
5
6
hk@vm:~/1sharedfiles/test$ gcc main.c -o main -I include/
# 下边是编译过程提示信息
/tmp/ccZGXsbr.o:在函数‘main’中:
main.c:(.text+0x1a):对‘func1’未定义的引用
main.c:(.text+0x1f):对‘func2’未定义的引用
collect2: error: ld returned 1 exit status

我们发现是在 ld 的时候出现问题,这说明我们没有链接相应的库,我们加上 -l 选项:

1
2
3
hk@vm:~/1sharedfiles/test$ gcc main.c -o main -I include/ -lfunc
/usr/bin/ld: 找不到 -lfunc
collect2: error: ld returned 1 exit status

此时提示,找不到链接库,这时候我们为 GCC 指条明路:

1
hk@vm:~/1sharedfiles/test$ gcc main.c -o main -I include/ -lfunc -L lib/

这个时候,就会发现,没有报错,正常生成了可执行文件。