0%

gcc 01

GCC 介绍

GCC = GNU Compiler Collection

Major features of GCC

  • GCC is a portable compiler
  • GCC is not only a native compiler, 能够交叉编译任何程序
  • GCC 有多语言的前端
  • GCC 是模块化的
  • GCC is free

Compiling a C program

Programs can be compiled from a single source file or from multiple source files and may be use system libraries and header files.

Compilation refers to the process of converting a program from the textual source code, in a programming language such as C or C++, into machine code, the sequence of 0 and 1 used to control the central processing unit CPU of the computer. This machine code is then stored in a file known as an executable file, sometimes referred to as a binary file.

编译一个 C 程序

hello.c

1
2
3
4
5
6
7
#include <stdio.h>

int main(void)
{
printf("hello world!\n");
return 0;
}
1
$ gcc -Wall hello.c -o hello 

会把源码文件 hello.c 编译为机器码并存储到一个可执行文件 hello 中。使用 -o 指定输出的包含机器码的可执行文件名。这个选项一般在 gcc 命令的最后。如果不加 -o 选项,默认输出到 a.out 文件。如果当前目录下已经有了同名的文件,那么 gcc 会覆盖这个文件。

选项 -Wall 打开所有最常用的编译器警告,建议始终使用此选项!除非启用了警告,否则 gcc 不会产生任何警告信息。编译器警告一定要打开。

1
2
$ ./hello # This loads the executable file into memory and causes the CPU to begin executing the instructions contained within it.
hello world!

使用编译器警告找到程序中的错误

一个错误的程序 bad.c

1
2
3
4
5
6
7
#include <stdio.h>

int main(void)
{
printf("2 * 2 = %f\n", 4);
return 0;
}
1
2
3
4
5
6
$ gcc -Wall bad.c -o bad
bad.c: In function ‘main’:
bad.c:5:23: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
printf("2 * 2 = %f\n", 4);
~^ ~
%d

编译器产生的信息通常是 file:line-number:message 的格式,编译器能够区分 error message 和 warning message,前者会中断编译,后者会提示可能的问题但是不会终止编译过程。

如果不加 -Wall 选项,程序看上去是 compile cleanly 的,但是运行结果却是错误的。

格式说明符 format specifier 不正确会导致输出损坏 corrupted,因为传递给函数 printf 的是整数而不是浮点数。整数和浮点数在内存中的存储格式不同,通常占用的字节数也不同,从而导致错误的结果。上面显示的实际输出可能会有所不同,具体取决于特定的平台和环境。

一个简单的例子就能让程序产生错误,所以打开编译器警告是非常有必要的。

编译多个源代码文件

一个程序的源代码会包含多个文件,好处就是方便编辑和理解,同样修改某些地方只需要重新编译那个文件就行,而不需要全部重新编译。

hello.c 文件分成三个源文件 main.chello_fn.chello.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// main.c
#include "hello.h"
int main(void)
{
hello("world");
return 0;
}

// hello.h
void hello(const char *name);

//hello_fn.c
#include <stdio.h>
#include "hello.h" // 函数声明

void hello(const char *name)
{
printf("Hello, %s!\n", name);
}

包含头文件 ""<> 的区别是,前者先搜索当前目录然后在搜索系统头文件目录,后者不会搜索当前目录。

1
$ gcc -Wall main.c hello_fn.c -o newhello

使用这个命令编译多个文件,注意头文件并没有出现在命令行中,源代码中的 #include "hello.h" 会指导编译器在合适的时候自动包含这个头文件。这样多个文件就被编译到单个可执行程序中了。

单独的编译文件

如果程序存储在单个文件中,则对单个函数的任何更改都需要重新编译整个程序以生成新的可执行文件。大型源文件的重新编译可能非常耗时。

当程序存储在独立的源文件中时,源代码修改后,只需要重新编译发生变化的文件。在这种方法中,源文件被单独编译,然后链接 linked 在一起,这是一个两阶段的过程 two stage process。在第一阶段,编译一个文件而不创建可执行文件,编译结果称为目标文件 object file,使用 GCC 时 obj 文件扩展名为.o

在第二阶段,目标文件由一个称为链接器的单独程序合并在一起。链接器将所有目标文件组合在一起以创建单个可执行文件。

目标文件包含机器代码,其中对其他文件中函数(或变量)的内存地址的任何引用均未定义。这样,源文件就可以在不直接引用彼此的情况下进行编译。链接器在生成可执行文件时会填充这些缺失的地址。

An object file contains machine code where any references to the mem- ory addresses of functions (or variables) in other files are left undefined. This allows source files to be compiled without direct reference to each other. The linker fills in these missing addresses when it produces the executable.

从源文件创建目标文件

gcc 命令选项 -c 就是用来把单个源文件生成目标文件的,比如 gcc -Wall -c main.c 这个命令使用 main.c 生成了目标文件 main.o,这个目标文件包含了 main 函数的机器码,并且包含一个对外部函数 hello 的引用,但是在这一阶段目标文件中对外部函数 hello 的引用的内存地址还未确定(undefined),之后会被 **linker** 填充。

注意在生成目标文件时没必要使用 -o 指定目标文件的名称,当使用 -c 进行编译时,编译器会自动创建一个与源文件同名的目标文件,但用 .o 代替原始扩展名。

不需要在命令行上放置头文件 hello.h,因为它会被 main.chello_fn.c 中的 #include 预处理指定自动包含。

从目标文件生成可执行文件

创建可执行文件的最后一步是使用 gcc 将目标文件链接在一起并填充缺失的外部函数地址。

要把目标文件链接在一起,使用 gcc main.o hello_fn.o -o hello。这是不需要使用 -Wall 警告选项的少数情况之一,因为各个源文件已经成功编译为目标代码了。一旦源文件被编译,链接就是一个明确的过程,它要么成功,要么失败(只有当存在无法解析的引用时才会失败)。

为了执行链接步骤,gcc 使用链接器 ld,它是一个单独的程序。通过运行链接器,gcc 从目标文件创建一个可执行文件。

在类 Unix 系统上,编译器和链接器的传统行为是在命令行指定的目标文件中从左到右搜索外部函数。这意味着包含函数定义的目标文件应该出现在调用该函数的任何文件之后。

在这种情况下,包含函数 hello 的文件 hello_fn.o 应该在 main.o 本身之后指定,因为 main 调用 hello

1
2
$ gcc main.o hello_fn.o -o hello  # correct
$ cc hello_fn.o main.o -o hello # incorrect

大多数现代的编译器和链接器都会搜索所有目标文件,而不管顺序如何,但由于并非所有编译器都这样做,因此最好遵循从左到右对目标文件进行排序的惯例。

如果遇到未定义引用的意外问题,并且所有必要的目标文件似乎都存在于命令行中,则值得记住这一点。

Recompiling and relinking

如果只修改一个文件,只要重新编译修改的文件就行。一般来说,链接比编译更快——在包含许多源文件的大型项目中,仅重新编译已修改的文件可以节省大量时间。在一个项目中只编译修改过的文件可以自动由 GNU Make 完成。

Linking with external libraries

A library is a collection of precompiled object files which can be linked into programs. 库通常存储在扩展名为 .a 的特殊归档文件中,称为静态库。Libraries are typically stored in special archive files with the extension .a, referred to as static libraries. 它们是使用单独的工具 GNU 归档程序 ar 从目标文件创建的,并由链接器在编译时用于解析对函数的引用。

标准系统库通常位于目录 /usr/lib /lib 中,如果系统同时支持 32 位和 64 位,那么前面的表示 32 位的库文件目录,/usr/lib64/lib64 表示 64 位的库目录。例如,C 数学库通常存储在类 Unix 系统上的文件 /usr/lib/libm.a 中。该库中函数的相应原型声明在头文件 /usr/include/math.h 中给出。C 标准库本身存储在 /usr/lib/libc.a 中,包含 ANSI/ISO C 标准中指定的函数,例如 printf ——每个 C 程序默认链接到此库。

1
2
3
4
5
6
7
8
9
// calc.c
#include <math.h>
#include <stdio.h>
int main(void)
{
double x = sqrt(2.0);
printf("The square root of 2.0 is %f\n", x);
return 0;
}

假设系统路径上没有对应的目录,gcc -Wall calc.c -o calc

1
2
3
/tmp/ccbR6Ojm.o: In function ‘main’:
/tmp/ccbR6Ojm.o(.text+0x19): undefined reference
to ‘sqrt’

问题在于,如果没有外部数学库 libm.a,则无法解析对 sqrt 函数的引用。函数 sqrt 未在程序或默认库 libc.a 中定义,并且除非明确选择,否则编译器不会链接到文件 libm.a。顺便说一句,错误消息中提到的文件 /tmp/ccbR60jm.o 是编译器从 calc.c 创建的临时对象文件,目的是执行链接过程。

为了使编译器能够将 sqrt 函数链接到主程序 calc.c,需要提供库 libm.a。一个明显但繁琐的方法是,在命令行上明确指定它: gcc -Wall calc.c /usr/lib/libm.a -o calc

libm.a 包含所有数学函数的目标文件,例如 sincosexplogsqrt。链接器会搜索这些文件以查找包含 sqrt 函数的目标文件。一旦找到 sqrt 函数的目标文件,就可以链接主程序并生成完整的可执行文件。可执行文件包括主函数的机器代码和 sqrt 函数的机器代码,从库 libm.a 中相应的目标文件复制而来。

为了避免在命令行上指定长路径,编译器提供了一个用于链接库的快捷选项 -l。例如,gcc -Wall calc.c -lm -o calc,和前面的等价。

一般来说,编译器选项 -lNAME 将尝试将目标文件与标准库目录中的库文件 libNAME.a 或者 libNAME.so 链接起来。

命令行上库的顺序与目标文件的顺序相同:从左到右搜索。对于目标文件,大多数现代编译器都会搜索所有库,无论顺序如何。但是,由于并非所有编译器都这样做,因此最好遵循从左到右对库进行排序的惯例。

1
2
$ gcc -Wall calc.c -lm -o calc # correct order
$ cc -Wall -lm calc.c -o calc # incorrect order

Using library header files

使用库时,必须包含适当的头文件,以便用正确的类型声明函数参数和返回值。如果没有声明,函数的参数可能会以错误的类型传递,从而导致结果损坏。

C 源码可以不包含头文件吗,在编译的时候指定对于的目标文件。虽然在某些情况下不包含头文件可以编译通过,但通常不推荐这么做。建议尽可能包含所需的头文件,以确保代码的可移植性和可维护性。

为什么需要 -l-l 一般要和 -L 配合。

gcc 默认只会链接一些基础库,比如 libc(标准 C 库),但对于其他库,比如数学库 libm、线程库 libpthread 等,依然需要显式指定 -l 选项来链接。