GCC简单使用

GCC特性

首先,GCC是一个可移植的编译器它可以运行在很多平台上,并且可以产生很多不同类型的处理器运行代码。除了支持个人电脑的处理器外还支持微控制器DSPs和64位的CPUs。

GCC支持交叉编译,可以为其他系统生成可执行文件。这样可以为那些不适合运行编译器的嵌入式系统编译程序。GCC使用C语言编写非常重视可移植性并且可以编译自己本身,所以可以很容易移植到新系统上。

GCC有多种语言前端用于解析不同语言所以不同语言的程序可以被编译或者为任何架构交叉编译。例如一个ADA程序可以被编译在一个微控制器上运行或者一个C程序可以被编译在一个超级电脑上运行。

GCC是模块化设计,允许添加模块来支持新语言和架构。添加一个新语言前端可以在任何架构下使用此语言只需要提供必要的运行时环境(例如:库)。相似的添加支持一个新架构则这个架构可以使用所有语言。

最后也是最重要的是GCC是免费软件,在GNU GPL许可证下发布。这意味着你可以自由使用和修改GCC就像所有GNU软件一样。如果你需要支持一个新的CPU,一种新的语言或者一个新的特性你可以自己添加或者雇佣其他人来增强GCC和修复bug如果它对于你的工作非常重要。

你甚至可以自由分享任何你对GCC的强化也可以利用别人对GCC的改进。现今GCC的许多特性都体现了这种自由合作有益于你和任何使用GCC的人。

编译C程序

本章描述怎样使用gcc编译C程序。程序可以使用一个源文件编译或者多个源文件,也可能使>用系统库和头文件。

编译器参照步骤将如C和C++语言的文本源文件转化为机器码,使用'1'和'0'序列来控制电脑>的中央处理单元(CPU)。这些机器码随后被存储为一个可执行文件,有时做为一个二进制文>件被引用。

编译一个简单的C程序

最经典的C语言示例程序是hello world。以下是我们自己的hello world程序源代码:

#include <stdio.h>

int main(void)
{
	printf("Hello World\n");
	
	return 0;
}

假设这个源代码储存在名为'hello.c'的文件中。使用gcc编译'hello.c'文件使用以下命令:

$ gcc -Wall hello.c -o hello

这会将'hello.c'源文件编译成名为'hello'的可执行文件。'-o'选项指定输出文件名,这个选项通常在命令行中做为最后一个参数。如果这个选项没有给出则输出文件名默认为'a.out'

注意如果当前目录下已经存在一个同名的可执行文件则这个文件会被重写

'-Wall'选项打开了编译器所有最通用的警告--建议你始终使用这个选项!'-Wall'选项是最重要的。GCC默认不会产生任何警告除非他们被使能。当编译C和C++程序时编译器警告信息是非常重要的探测错误的方法。

这个例子中,在'-Wall'选项下编译器没有产生任何警告信息直到程序完全编译通过。源代码不产生任何警告信息被称为'compile cleanly'

键入可执行文件的路径名来执行程序,如下:

youngcyan@DESKTOP-341LNNG:~/C_Code> ./hello
Hello World

程序会被加载到内存然后CPU开始执行程序中的指令。'./'代表当前路径所以'./hello'命令会加载然后运行位于当前目录下的文件'hello'。

查找简单程序中的错误

正如上面提到的,当编写C和C++程序时编译器警告是非常有效的检查错误的方法。为了证明这种说法下面的程序有一个微妙的错误:printf函数的使用有错误,为一个整型值指定了一个浮点格式'%f'的格式化字符串:

#include <stdio.h>

int main(void)
{
	printf("%f\n", 4);
	
	return 0;
}

一眼看去这个错误并不明显但是如果使用了'-Wall'警告选项,编译器可以检查出来这个错误。

使用'-Wall'选项编译上面的程序'bad.c'会产生下面的信息:

youngcyan@DESKTOP-341LNNG:~/C_Code> gcc  -Wall bad.c -o bad
bad.c: In function ‘main’:
bad.c:5:11: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
  printf("%f\n", 4);
          ~^
          %d

它指出'bad.c'文件中的第6行的格式化字符串使用不正确。GCC输出的信息总是以这样的格式显示“文件:行号:信息”。编译器会辨别阻止成功编译的错误信息和包含潜在问题的警告信息(但是不会停止编译程序)。

在这个例子中,正确的格式应该是'%d'(printf允许指定的格式可以再任何C语言书中找到,例如《GNU C Library Reference Manual》)。

如果没有'-Wall'选项,程序的编译会没有任何警告信息但是程序的运行会输出错误的结果:

youngcyan@DESKTOP-341LNNG:~/C_Code> gcc  bad.c -o bad
youngcyan@DESKTOP-341LNNG:~/C_Code> ./bad
0.000000

为printf()函数指定错误的格式导致了错误的输出是因为printf函数传递了一个整型数而不是一个浮点数。整型数和浮点数在内存中以不同的格式存放并且一般占用不同的字节数,加载了一个错误的结果。实际输出的结果依赖于不同的平台和环境而不同。

很明显没有检查编译器警告的程序是非常危险的。如果任何一个函数使用错误会导致整个程序崩溃挥着产生错误的结果。打开编译器警告选项'-Wall'会检测到很多C程序中的常见错误。

编译多文件程序

一个程序可以分为几个文件。这样会是程序更容易编辑和理解,特别是当程序很大时--它也可以单独编译程序的不同模块。

在下面的例子中我们将Hello world程序分为三个文件:'main.c','hello_fn.c'和头文件'hello.h'。这是主程序'main.c':

#include <stdio.h>
#include "hello.h"
int main (void)
{
	hello ("world");
	
	return 0;
}

先前'hello.c'中的系统调用printf被替换为一个新的外部函数hello,这个函数在'hello_fn.c'中定义。

主程序也包含头文件'hello.h'这个头文件中包含hello函数的声明。这个声明用于确定调用函数时传递给函数的参数和返回值类型与函数定义时的参数和返回值一致。

我们不再需要包含系统头文件'stdio.h'在'main.c'中来声明函数printf,因为'main.c'不在直接调用printf函数了。

'hello.h'文件中的声明只有一行用于指定hello函数的类型:

void hello (const char * name);

hello函数自身的定义包含在'hello_fn.c'文件中:

include <stdio.h>
include "hello.h"

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

这个函数使用参数name的值来打印"Hello,name!"字符串。

#include "hello.h" 和 #include<stdio.h> 这两种格式的不同是前一种格式首先在当前目录下查找此文件然后在查找系统头文件目录。#include<stdio.h> 这种格式只会查找系统头文件默认不会查找当前目录。
使用gcc编译这些文件使用如下命令:
$ gcc -Wall main.c hello_fn.c -o newhello

在这个例子中我们使用'-o'选项指定一个不同的可执行文件'newhello'。注意头文件'hello.h'没有在命令行中指定。编译器会在适当的时候自动包含'hello.h'文件。

键入程序路径名来执行程序:

youngcyan@DESKTOP-341LNNG:~/C_Code/hello_fn> ./newhello
world

程序的所有部分都会包含在一个可执行文件中,程序执行的结果和之前的单源文件程序的结果一样。

单独编译文件

如果程序存储在一个文件中任何单独函数的改动都会导致整个程序程序编译来产生新的可执行文件。重新编译大文件是非常耗费时间的。

当程序存储在多个文件中时只有那个代码被改变的文件会被重新编译。这种方法分为两个阶段:源文件分开编译然后一起链接。在第一个阶段中文件编译后产生的文件不是可执行文件而是文件后缀名为'.o'(使用GCC编译器时)的文件被称为目标文件。

在第二个阶段中目标文件被连接器合并到一起。连接器将所有目标文件合并创建为一个可执行文件。

一个目标文件中包含机器码,所有其他文件中函数引用的内存地址都被视为未定义。这样允许源文件在编译时不需要直接相互引用。连接器在生成可执行文件时会补充这些丢失的内存地址。

源文件生成目标文件

'-c'命令行选项用于编译源文件为目标文件。例如下面的命令会编译源文件'main.c'为一个目标文件:

$ gcc -Wall -c main.c

生成的目标文件'mian.o'包含mian函数的机器码。也包含外部函数hello的一个引用但是对应的内存地址在这个阶段还没有定义。

相应的命令编译'hello_fn.c'文件中的hello函数:

$ gcc -Wall -c hello_fn.c

生成目标文件'hello_fn.o'。

注意在这种情况下不需要使用'-o'选项指定输出文件名。当使用'-c'选项时编译器自动创建和源文件同名的目标文件然后跟上'.o'后缀。

没有必要将'hello.h'头文件包含在命令行中因为'main.c'和'hello_fn.c'文件中的#include声明会自动包含此头文件。

使用目标文件创建可执行文件

使用gcc生成可执行文件的最后一个步骤是链接所有目标文件,填充外部函数的地址。使用下面命令链接目标文件:

$ gcc main.o hello_fn.o -o hello

这里不需要使用'-Wall'警告选项因为源文件已经成功的被编译为目标文件。一旦源文件编译成功,链接是非常明确的过程要么成功要么失败(当引用不能被解决时会导致失败)。

gcc使用一个单独的程序--连接器ld来完成链接工作。在GNU操作系统中使用GNU链接器和GNU ld,在其他操作系统中可能使用GNU链接器或者使用它们自己的链接器。链接器会在后面讨论(请看第11章[编译器时怎样工作的])链接器运行时gcc使用目标文件创建可执行文件。

可执行文件现在可以运行:

$ ./hello
Hello, world!

它运行的结果和前面章节中使用一个源文件的版本是一样的。

目标文件的连接顺序

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

在这个例子中含有hello函数的文件'hello_fn.o'应该在'main.o'的文件后面指定因为main调用了hello函数:

$ gcc main.o hello_fn.o -o hello (正确的顺序)

有些编译器或者链接器如果指定文件的顺序错误会导致编译失败

$ cc hello_fn.o main.o -o hello (错误的顺序)

main.o: In function ‘main’:

main.o(.text+0xf): undefined reference to ‘hello’

因为'main.o'后面没有目标文件包含hello函数。

大多数现代编译器和链接器会查找所有目标文件不依赖与文件顺序,但是由于不是所有编译器会这样做所以最好的办法是按照规定的顺序从左到右排列目标文件。

这是非常值得注意的当你遇到没有被定义的引用这样的问题并且所有必要的目标文件都已经在命令行中。

posted @ 2022-09-20 22:33  月傍山楼水映月  阅读(192)  评论(0编辑  收藏  举报