`
dr2tr
  • 浏览: 142301 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

GCC - 一切从这里开始

阅读更多
要想读懂本文,你需要对C语言有基本的了解,本文将介绍如何使用gcc编译器。首先,我们介绍如何在命令行方式下使用编译器编译简单的C源代码。然后,我们简要介绍一下编译器究竟作了那些工作,以及如何控制编译过程。我们也简要介绍了调试器的使用方法。

GCC rules

你能想象使用封闭源代码的私有编译器编译自由软件吗?你怎么知道编译器在你的可执行文件中加入了什么?可能会加入各种后门和木马。Ken Thompson是一个著名的黑客,他编写了一个编译器,当编译器编译自己时,就在'login'程序中留下后门和永久的木马。请到 这里 阅读他对这个杰作的描述。幸运的是,我们有了gcc。当你进行 configure; make; make install 时, gcc在幕后做了很多繁重的工作。如何才能让gcc为我们工作呢?我们将开始编写一个纸牌游戏,不过我们只是为了演示编译器的功能,所以尽可能地精简了代码。我们将从头开始一步一步地做,以便理解编译过程,了解为了制作可执行文件需要做些什么,按什么顺序做。我们将看看如何编译C程序,以及如何使用编译选项让gcc按照我们的要求工作。步骤(以及所用工具)如下: 预编译 (gcc -E), 编译 (gcc), 汇编 (as),和 连接 (ld)。

 

开始...

首先,我们应该知道如何调用编译器。实际上,这很简单。我们将从那个著名的第一个C程序开始。(各位老前辈,请原谅我)。

 

#include <stdio.h>

int main()
{ printf("Hello World! "); }

 

把这个文件保存为 game.c。 你可以在命令行下编译它:

gcc game.c
在默认情况下,C编译器将生成一个名为 a.out 的可执行文件。你可以键入如下命令运行它:
a.out
Hello World

每一次编译程序时,新的 a.out 将覆盖原来的程序。你无法知道是哪个程序创建了 a.out。我们可以通过使用 -o 编译选项,告诉 gcc我们想把可执行文件叫什么名字。我们将把这个程序叫做 game,我们可以使用任何名字,因为C没有Java那样的命名限制。
gcc -o game game.c
game
Hello World

 

到现在为止,我们离一个有用的程序还差得很远。如果你觉得沮丧,你可以想一想我们已经编译并运行了一个程序。因为我们将一点一点为这个程序添加功能,所以我们必须保证让它能够运行。似乎每个刚开始学编程的程序员都想一下子编一个1000行的程序,然后一次修改所有的错误。没有人,我是说没有人,能做到这个。你应该先编一个可以运行的小程序,修改它,然后再次让它运行。这可以限制你一次修改的错误数量。另外,你知道刚才做了哪些修改使程序无法运行,因此你知道应该把注意力放在哪里。这可以防止这样的情况出现:你认为你编写的东西应该能够工作,它也能通过编译,但它就是不能运行。请切记,能够通过编译的程序并不意味着它是正确的。

下一步为我们的游戏编写一个头文件。头文件把数据类型和函数声明集中到了一处。这可以保证数据结构定义的一致性,以便程序的每一部分都能以同样的方式看待一切事情。

#ifndef DECK_H
#define DECK_H

#define DECKSIZE 52

typedef struct deck_t
{
  int card[DECKSIZE];
  /* number of cards used */
  int dealt;
}deck_t;

#endif /* DECK_H */

把这个文件保存为 deck.h。只能编译 .c 文件,所以我们必须修改 game.c。在game.c的第2行,写上 #include "deck.h"。在第5行写上 deck_t deck;。为了保证我们没有搞错,把它重新编译一次。

gcc -o game game.c

 

如果没有错误,就没有问题。如果编译不能通过,那么就修改它直到能通过为止。

 

预编译

编译器是怎么知道 deck_t 类型是什么的呢?因为在预编译期间,它实际上把"deck.h"文件复制到了"game.c"文件中。源代码中的预编译指示以"#"为前缀。你可以通过在gcc后加上 -E 选项来调用预编译器。

gcc -E -o game_precompile.txt game.c
wc -l game_precompile.txt
  3199 game_precompile.txt
几乎有3200行的输出!其中大多数来自 stdio.h 包含文件,但是如果你查看这个文件的话,我们的声明也在那里。如果你不用 -o 选项指定输出文件名的话,它就输出到控制台。预编译过程通过完成三个主要任务给了代码很大的灵活性。
  1. 把"include"的文件拷贝到要编译的源文件中。
  2. 用实际值替代"define"的文本。
  3. 在调用宏的地方进行宏替换。
这就使你能够在整个源文件中使用符号常量(即用DECKSIZE表示一付牌中的纸牌数量),而符号常量是在一个地方定义的,如果它的值发生了变化,所有使用符号常量的地方都能自动更新。在实践中,你几乎不需要单独使用 -E 选项,而是让它把输出传送给编译器。

 

 

编译

作为一个中间步骤,gcc把你的代码翻译成汇编语言。它一定要这样做,它必须通过分析你的代码搞清楚你究竟想要做什么。如果你犯了语法错误,它就会告诉你,这样编译就失败了。人们有时会把这一步误解为整个过程。但是,实际上还有许多工作要gcc去做呢。

 

汇编

as 把汇编语言代码转换为目标代码。事实上目标代码并不能在CPU上运行,但它离完成已经很近了。编译器选项 -c 把 .c 文件转换为以 .o 为扩展名的目标文件。 如果我们运行

gcc -c game.c
我们就自动创建了一个名为game.o的文件。这里我们碰到了一个重要的问题。我们可以用任意一个 .c 文件创建一个目标文件。正如我们在下面所看到的,在连接步骤中我们可以把这些目标文件组合成可执行文件。让我们继续介绍我们的例子。因为我们正在编写一个纸牌游戏,我们已经把一付牌定义为 deck_t,我们将编写一个洗牌函数。这个函数接受一个指向deck类型的指针,并把一付随机的牌装入deck类型。它使用'drawn' 数组跟踪记录那些牌已经用过了。这个具有DECKSIZE个元素的数组可以防止我们重复使用一张牌。

 

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include "deck.h"

static time_t seed = 0;

void shuffle(deck_t *pdeck)
{
  /* Keeps track of what numbers have been used */
  int drawn[DECKSIZE] = {0};
  int i;

  /* One time initialization of rand */
  if(0 == seed)
  {
    seed = time(NULL);
    srand(seed);
  }
  for(i = 0; i < DECKSIZE; i++)
  {
    int value = -1;
    do
    {
      value = rand() % DECKSIZE;
    }
    while(drawn[value] != 0);

    /* mark value as used */
    drawn[value] = 1;

    /* debug statement */
    printf("%i
", value);
    pdeck->card[i] = value;
  }
  pdeck->dealt = 0;
  return;
}

把这个文件保存为 shuffle.c。我们在这个代码中加入了一条调试语句,以便运行时,能输出所产生的牌号。这并没有为我们的程序添加功能,但是现在到了关键时刻,我们看看究竟发生了什么。因为我们的游戏还在初级阶段,我们没有别的办法确定我们的函数是否实现了我们要求的功能。使用那条printf语句,我们就能准确地知道现在究竟发生了什么,以便在开始下一阶段之前我们知道牌已经洗好了。在我们对它的工作感到满意之后,我们可以把那一行语句从代码中删掉。这种调试程序的技术看起来很粗糙,但它使用最少的语句完成了调试任务。以后我们再介绍更复杂的调试器。

请注意两个问题。
  1. 我们用传址方式传递参数,你可以从'&'(取地址)操作符看出来。这把变量的机器地址传递给了函数,因此函数自己就能改变变量的值。也可以使用全局变量编写程序,但是应该尽量少使用全局变量。指针是C的一个重要组成部分,你应该充分地理解它。
  2. 我们在一个新的 .c 文件中使用函数调用。操作系统总是寻找名为'main'的函数,并从那里开始执行。 shuffle.c 中没有'main'函数,因此不能编译为独立的可执行文件。我们必须把它与另一个具有'main'函数并调用'shuffle'的程序组合起来。

运行命令

gcc -c shuffle.c
并确定它创建了一个名为 shuffle.o 的新文件。编辑game.c文件,在第7行,在 deck_t类型的变量 deck 声明之后,加上下面这一行:
shuffle(&deck);
现在,如果我们还象以前一样创建可执行文件,我们就会得到一个错误
gcc -o game game.c

/tmp/ccmiHnJX.o: In function `main':
/tmp/ccmiHnJX.o(.text+0xf): undefined reference to `shuffle'
collect2: ld returned 1 exit status
编译成功了,因为我们的语法是正确的。但是连接步骤却失败了,因为我们没有告诉编译器'shuffle'函数在哪里。那么,到底什么是连接?我们怎样告诉编译器到哪里寻找这个函数呢?

 

 

连接

连接器ld,使用下面的命令,接受前面由 as 创建的目标文件并把它转换为可执行文件

gcc -o game game.o shuffle.o
这将把两个目标文件组合起来并创建可执行文件 game

 

连接器从shuffle.o目标文件中找到 shuffle 函数,并把它包括进可执行文件。目标文件的真正好处在于,如果我们想再次使用那个函数,我们所要做的就是包含"deck.h" 文件并把 shuffle.o 目标文件连接到新的可执行文件中。

象这样的代码重用是经常发生的。虽然我们并没有编写前面作为调试语句调用的 printf 函数,连接器却能从我们用 #include <stdlib.h> 语句包含的文件中找到它的声明,并把存储在C库(/lib/libc.so.6)中的目标代码连接进来。这种方式使我们可以使用已能正确工作的其他人的函数,只关心我们所要解决的问题。这就是为什么头文件中一般只含有数据和函数声明,而没有函数体。一般,你可以为连接器创建目标文件或函数库,以便连接进可执行文件。我们的代码可能产生问题,因为在头文件中我们没有放入任何函数声明。为了确保一切顺利,我们还能做什么呢?

 

另外两个重要选项

-Wall 选项可以打开所有类型的语法警告,以便帮助我们确定代码是正确的,并且尽可能实现可移植性。当我们使用这个选项编译我们的代码时,我们将看到下述警告:

game.c:9: warning: implicit declaration of function `shuffle'
这让我们知道还有一些工作要做。我们需要在头文件中加入一行代码,以便告诉编译器有关 shuffle 函数的一切,让它可以做必要的检查。听起来象是一种狡辩,但这样做 可以把函数的定义与实现分离开来,使我们能在任何地方使用我们的函数,只要包含新的头文件 并把它连接到我们的目标文件中就可以了。下面我们就把这一行加入deck.h中。
void shuffle(deck_t *pdeck);
这就可以消除那个警告信息了。

 

另一个常用编译器选项是优化选项 -O# (即 -O2)。 这是告诉编译器你需要什么级别的优化。编译器具有一整套技巧可以使你的代码运行得更快一点。对于象我们这种小程序,你可能注意不到差别,但对于大型程序来说,它可以大幅度提高运行速度。你会经常碰到它,所以你应该知道它的意思。

 

调试

我们都知道,代码通过了编译并不意味着它按我们得要求工作了。你可以使用下面的命令验证是否所有的号码都被使用了

game | sort - n | less
并且检查有没有遗漏。如果有问题我们该怎么办?我们如何才能深入底层查找错误呢?

 

你可以使用调试器检查你的代码。大多数发行版都提供著名的调试器:gdb。如果那些众多的命令行选项让你感到无所适从,那么你可以使用KDE提供的一个很好的前端工具 KDbg。还有一些其它的前端工具,它们都很相似。要开始调试,你可以选择 File->Executable 然后找到你的 game 程序。当你按下F5键或选择 Execution->从菜单运行时,你可以在另一个窗口中看到输出。怎么回事?在那个窗口中我们什么也看不到。不要担心,KDbg没有出问题。问题在于我们在可执行文件中没有加入任何调试信息,所以KDbg不能告诉我们内部发生了什么。编译器选项 -g 可以把必要的调试信息加入目标文件。你必须用这个选项编译目标文件(扩展名为.o),所以命令行成了:
gcc -g -c shuffle.c game.c
gcc -g -o game game.o shuffle.o
这就把钩子放入了可执行文件,使gdb和KDbg能指出运行情况。调试是一种很重要的技术,很值得你花时间学习如何使用。调试器帮助程序员的方法是它能在源代码中设置“断点”。现在你可以用右键单击调用 shuffle 函数的那行代码,试着设置断点。那一行边上会出现一个红色的小圆圈。现在当你按下F5键时,程序就会在那一行停止执行。按F8可以跳入shuffle函数。呵,我们现在可以看到 shuffle.c 中的代码了!我们可以控制程序一步一步地执行,并看到究竟发生了什么事。如果你把光标暂停在局部变量上,你将能看到变量的内容。太好了。这比那条 printf 语句好多了,是不是?

 

 

小结

本文大体介绍了编译和调试C程序的方法。我们讨论了编译器走过的步骤,以及为了让编译器做这些工作应该给gcc传递哪些选项。我们简述了有关连接共享函数库的问题,最后介绍了调试器。真正了解你所从事的工作还需要付出许多努力,但我希望本文能让你正确地起步。你可以在 gccasldmaninfo page中找到更多的信息。

自己编写代码可以让你学到更多的东西。作为练习你可以以本文的纸牌游戏为基础,编写一个21点游戏。那时你可以学学如何使用调试器。使用GUI的KDbg开始可以更容易一些。如果你每次只加入一点点功能,那么很快就能完成。切记,一定要保持程序一直能运行!

要想编写一个完整的游戏,你需要下面这些内容:

  • 一个纸牌玩家的定义(即,你可以把deck_t定义为player_t)。
  • 一个给指定玩家发一定数量牌的函数。记住在纸牌中要增加“已发牌”的数量,以便能知道还有那些牌可发。还要记住玩家手中还有多少牌。
  • 一些与用户的交互,问问玩家是否还要另一张牌。
  • 一个能打印玩家手中的牌的函数。 card 等于value % 13 (得数为0到12),suit 等于 value / 13 (得数为0到3)。
  • 一个能确定玩家手中的value的函数。Ace的value为零并且可以等于1或11。King的value为12并且可以等于10。

 

 

 

站点链接

(www.linuxfocus.org,作者:Lorne Bailey 翻译:Zhang Wei)


分享到:
评论

相关推荐

    【离线安装包】gcc-c++.zip

    在使用这个离线包之前,确保你的系统已经安装了基础的GCC工具和其他必要的依赖。 安装步骤如下: 1. 首先,你需要下载提供的`gcc-c++.zip`文件到你的系统上。 2. 使用`unzip`命令解压缩文件,命令如下: ``` ...

    交叉编译环境的建立arm-linux-gcc-4.3.2的安装

    这条命令将会把`arm-linux-gcc-4.3.2.tgz`解压到`/usr/local/`目录下。需要注意的是,解压的过程可能需要一定的时间。 #### 四、配置环境变量 为了能够顺利地使用arm-linux-gcc-4.3.2进行交叉编译,我们需要将它...

    MinGW-W64 GCC-8.1.0.rar

    安装mingw-w64-install.exe这个执行文件,会帮助用户在本地系统上安装所需的编译工具链,包括g++(C++编译器)和gcc(C编译器)。 在安装过程中,你需要选择合适的架构(x86_64 for 64-bit 或 i686 for 32-bit)...

    winlibs-x86-64-posix-seh-gcc-13.2.0-llvm-16.0.6-mingw-w64msvcrt

    总结来说,这个工具包提供了在Windows 64位环境下开发C和C++程序所需的一切,包括GCC 13.2.0和LLVM 16.0.6编译器,它们支持POSIX标准和SEH异常处理,适用于Nuitka这样的Python编译器。通过MinGW64,开发者能够在...

    gcc-11.2.0.tar.gz

    `--prefix`选项用于指定安装路径,这里我们选择`/usr/local/gcc-11.2.0`,避免与系统默认的GCC冲突。 3. **编译**:接下来,编译源代码: ``` make -j4 ``` `-j4`参数表示使用4个线程并行编译,可以根据你的...

    centos下安装gcc-4.8.5 rpm完整依赖

    本文将详细介绍如何在CentOS环境下离线安装GCC 4.8.5,这是一个重要的编译器,用于构建和编译C、C++和其他语言的程序。 GCC(GNU Compiler Collection)是GNU项目的一部分,它提供了对多种编程语言的支持,包括C、...

    arm-linux-gcc-3_3_2 交叉编译器的建立.

    - 首先,你需要下载`arm-linux-gcc-3.3.2.tar.bz2`这个源码包。可以从官方或可靠的第三方网站获取。例如: ```sh wget http://www.applieddata.net/develx-gcc-3.3.2.tar.bz2 ``` - 假设你已经将文件下载到了`/...

    升级gcc-8.2.1.zip

    如果一切顺利,你应该能看到GCC 8.2.1的版本信息。通过以上步骤,你已经在CentOS 8系统上成功安装了GCC 8.2.1,现在可以愉快地进行软件开发和编译工作了。 请注意,升级GCC可能会影响到依赖于旧版本GCC的已安装软件...

    tdm64-gcc-5.1.0-2.zip

    在这个版本中,5.1.0代表了GCC的版本号,而tdm64表示这是由TDM-GCC项目提供的,专为Windows 64位系统优化的版本。 编译FFMPEG源码通常涉及以下步骤: 1. **获取源码**:首先,你需要从FFMPEG的官方网站或GitHub...

    安装arm-linux-gcc-3.4.1交叉编译环境.pdf

    这个版本的交叉编译器可以从`http://www.handhelds.org/download/projects/toolchain/`或者通过其他链接获取,例如`http://blog.csdn.net/vcvbve/archive/2010/03/11/5369077.aspx`,找到`arm-linux-gcc-3.4.1.tar....

    GCC安装程序及说明.rar

    压缩包中的`gcc9.2.0SN.txt`可能包含了安装步骤的详细说明,建议在安装过程中参照该文件以确保一切按照预期进行。如果遇到任何问题,这个文档应该能提供解决方案或额外的指导。在安装开源软件时,仔细阅读文档是非常...

    arm-linux-gcc的安装方法

    这一步会创建一个名为`arm-gcc-4.4.1-20100409`的目录,其中包含了整个工具链的所有文件。 ##### 4.2 安装路径设置 接下来,需要为这个工具链指定一个安装路径。这里选择安装到`/usr/local/arm/4.4.1`目录下。如果...

    解决MATLAB 2016a无c++编辑器问题tdm64-gcc-5.1.0-2.exe

    1. **下载与安装**:首先,你需要从可靠来源下载tdm64-gcc-5.1.0-2.exe。双击运行安装程序,按照提示完成安装过程。确保安装路径不含中文字符,以防后续配置出现问题。 2. **配置MATLAB**:打开MATLAB,进入“设置...

    gcc-win编译器

    2. **编译源代码**:在命令行中,切换到源代码文件所在的目录,然后输入`gcc hello.c -o hello`(C语言)或`g++ hello.cpp -o hello`(C++),这将生成名为`hello`的可执行文件。 3. **运行程序**:输入`./hello`...

    gcc-centos.zip

    这个压缩包包含了一切在CentOS上构建和安装GCC所需的文件,用户可以下载到本地,然后在目标机器上解压并按照一定的步骤进行安装。 首先,你需要将这个压缩包传输到CentOS系统中。这可以通过USB驱动器、网络共享或者...

    gcc-4.9.2-Ee5500-aeabi.rar

    总的来说,这个“gcc-4.9.2-Ee5500-aeabi.rar”软件包提供了在Linux系统上为Ee5500处理器编译和优化代码所需的一切工具,对于在该平台上的软件开发工作至关重要。通过理解和充分利用这个编译器,开发者可以创建高效...

    Dev-Cpp-5.11-TDM-GCC-4.9.2-Setup.exe

    这个软件包的名称“Dev-Cpp-5.11-TDM-GCC-4.9.2-Setup.exe”揭示了其版本和编译器的信息。其中,"5.11"代表的是Dev-Cpp的具体版本号,"TDM-GCC"是基于MinGW的GCC(GNU Compiler Collection)的特殊构建,专为Windows...

    gcc的rpm包全套

    这个"gcc的rpm包全套"包含了GCC 4.4.7版本的所有相关组件,RPM(Red Hat Package Manager)是Linux发行版如Fedora、CentOS等常用的软件包管理器,它负责软件的安装、升级和卸载。RPM包格式便于管理和维护系统中的...

    centos下gcc5.4.0升级包及自动化升级脚本

    4. **下载GCC 5.4.0**:从官方或者其他可靠源下载`gcc-5.4.0.tar.gz`,这是GCC 5.4.0的源码包。 5. **编译和安装GCC 5.4.0**:解压GCC源码,配置编译选项,然后进行编译和安装。配置可能包括指定依赖库的位置,例如...

    Dev-Cpp 5.11 TDM-GCC 4.9.2 Setup.zip 2024年更新

    在这个特定的版本中,GCC提供了先进的优化选项,以及对C++11标准的良好支持,使得开发者能够利用最新的语言特性,如lambda表达式、右值引用和类型推断。 TDM-GCC是GCC的一个定制版,由TinyDynamo和MinGW项目合作...

Global site tag (gtag.js) - Google Analytics