导语

GCCGNU Compiler Collection,GNU 编译器套件) 是由 GNU 开发的编程语言编译器,支持C、C++、Objective-C、Fortran、Java、Ada和Go语言等多种预言的前端,以及这些语言的库(如libstdc++、libgcj等等),它是以 GLP 许可证所发行的自由软件,也是 GNU 计划的关键部分。GCC 原本作为GNU操作系统的官方编译器,现已被大多数类 Unix 操作系统(如Linux、BSD、Mac OS X 等)采纳为标准的编译器,GCC同样适用于微软的Windows 。

本文主要记录 GCC 学习使用过程中的一些操作,便于后期使用时查阅,前期入门阶段主要参考 An Introduction to GCC

编译C程序

单个源文件

#include <stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}

编译源代码:

$ gcc helloworld.c

未指定编译输出文件名则默认输出 a.out

$ gcc -Wall helloworld.c -o helloworld

-o: output 该选项用于指定存储机器码的输出文件;

-Wall: Warning all 该选项用于打开所有最常用的编译警告;

$ ./helloworld
Hello, world!

多个源文件

如果将源码分为多个源文件,我们也可以使用以下命令进行编译:

$ gcc -Wall hello.c world.c hello_world.c -o helloworld

对于包含多个源文件的工程,我们可以将编译过程分为两个阶段:

第一阶段:所有 .c .h 源文件经过编译,分别生成对应的 .o 对象文件;

第二阶段:所有 .o 对象文件经过链接生成最终的可执行文件。

.o 对象文件包含机器码,任何对其他文件中的函数或变量的内存地址的引用都留着没有被解析,这些都留给链接器 GNU ld 生成可执行文件时再处理。

  • 源文件生成对象文件

    $ gcc -Wall -c hello.c world.c hello_world.c

    注意:这里不需要使用 -o 来指定输出文件名, -c 选项会自动生成与源文件同名的对象文件

  • 对象文件生成可执行文件

    $ gcc hello.o world.o hello_world.o -o hello_world

    注意:这里不需要使用者-Wall ,因为源文件已经成功编译成对象文件了,链接是一个要么成功要么失败的过程。

通常,链接快于编译,因此,对于大型项目,将不同功能在不同的源文件中进行实现,在修改功能时,可以只编译被修改的文件,显著节省时间。

静态库文件

编译生成的 .o 对象文件可以通过归档器 GNU ar 打包成 .a 静态库文件,将某些功能提供给外部使用。在上述多个源文件的例子当中,我们可以将 hello.oworld.o 打包成静态库文件:

$ ar cr libhello.a hello.o world.o

这样便生成了 .a 库文件。在使用的时候,编译器需要通过路径找到对应的库文件,而标准的系统库,通常能在 /usr/lib/lib 目录下找到,自己工程中生成的库文件的位置需要通过编译选项告知给编译器。

一种直接的方式是在编译命令中通过绝对路径或者相对路径指定静态库的位置,例如:

$ gcc -Wall hello_world.c ./libhello.a -o helloworld

实际上,为了避免使用长路径名,我们可以使用 -l 来链接库文件,例如:

$ gcc -Wall -L./ hello_world.c -lhello -o helloworld

两种方式的效果应当是一致的,这里 -L 选项指示库文件的位置位于 ./ ,而 -lhello 选项会指示编译器试图链接 ./ 目录下的文件名为 libhello.a 的静态库文件。

编译选项

库文件

外部库文件包含2种类型:静态库和共享库。

静态库文件格式为 .a ,文件包含对象文件的机器码,链接静态库文件时,链接器将程序用到的外部函数的机器码从静态库文件中提取出来,并复制到最终的可执行文件当中。

共享库文件格式为 .so ,表示 shared object ,使用共享库的可执行文件仅仅包它含用到的函数的表格,而不是外部函数所在对象文件的整个机器码。共享库在使用时需要先于可执行文件加载到内存,并支持同时被多个程序共享,这个过程称为动态链接( dynamic linking )。

相比于静态库,共享库具备如下优点:

  • 减少可执行程序文件大小;
  • 多个程序共用;
  • 库文件升级无需重新编译可执行程序

搜索路径

在编译过程中,如果找不到头文件会报错,默认情况下,GCC会在下面的目录中搜索头文件,这些路径称为 include路径

/usr/local/include/
/usr/include/

同理,在链接过程中,如果找不到库文件也会报错,默认情况下,GCC在下面的目录中搜索库文件,这些路径称为 链接路径

/usr/local/lib/
/usr/lib/

如果需要检索其他路径下的头文件或者库文件,可以通过 -I-L 的方式来分别扩展头文件和库文件的搜索路径,这里介绍2种搜索路径的设置方法:

-命令行选项

$ gcc -Wall [-static] -I<INC_PATH> -L<LIB_PATH> <INPUT_FILES> -l<INPUT_LIBS> -O <OUTPUT_FILES>

在前面生成的静态库文件的基础上,我们可以进一步生成最终的可执行文件:

$ gcc -Wall [-static] -I. -L. hello_world.c -lhello -o helloworld

上述命令,-I 将指定头文件位于当前路径 .-L 将指定库文件位于当前路径 .-lhello 指定参与编译的自定义的库文件。

需要注意的是,gcc 编译器在使用 -l 选项时会默认优先链接到共享库文件,如果确认使用静态库,则可以使用 -static 选项进行限制。

-环境变量

我们还可以在 shell 登录文件(例如 .bashrc)中,预先扩展可能用到的头文件目录和库文件目录,这样,每次登录shell时,将会自动设置他们。

对于C头文件路径,我们有环境变量 C_INCLUDE_PATH ,对于C++头文件路径,我们有环境变量 CPP_INCLUDE_PATH 。 例如:

$ C_INCLUDE_PATH=./
$ export C_INCLUDE_PATH

对于静态库文件,我们有环境变量 LIBRARY_PATH

$ LIBRARY_PATH=./
$ export LIBRARY_PATH

对于共享库文件,我们有环境变量 LD_LIBRARY_PATH

$ LD_LIBRARY_PATH=./
$ export LD_LIBRARY_PATH

上述目录经过环境变量指定后,将在标准默认目录之前搜索,后续编译过程也无需在编译命令中指定了。上面的编译指令也可以进一步简化:

$ gcc -Wall hello_world.c -lhello -o helloworld

对于多个搜索目录,我们可以遵循标准Unix搜索路径的规范,在环境变量中用冒号分割的列表进行表示:

DIR1:DIR2:DIR3: ...

DIR 可以用单个点 . 指示当前目录。举个例子:

$ C_INCLUDE_PATH=.:/opt/gdbm-1.8.3/include:/net/include
$ LIBRARY_PATH=.:/opt/gdbm-1.8.3/lib:/net/lib

如果环境变量中已经包含路径信息,则可以用以下语法进行扩展:

$ C_INCLUDE_PATH= NEWPATH:$C_INCLUDE_PATH
$ LIBRARY_PATH= NEWPATH:$LIBRARY_PATH
$ LD_LIBRARY_PATH= NEWPATH:$LD_LIBRARY_PATH

-搜索顺序

方式2和方式3本质上是同一种方法的不同表现方式。当环境变量和命令行选项被同时使用时,编译器将按照下面的顺序搜索目录:

  1. 从左到右搜索由命令行 -I-L 指定的目录;
  2. 由环境变量 C_INCLUDE_PATH LIBRARY_PATH 指定的目录;
  3. 默认的系统目录。

C语言标准

默认情况下, gcc 编译程序时使用的是GNU C 语法规则,而非 ANSI/ISO C 标准语法规则,GNU CANSI/ISO C 标准语法基础上增加了一些对C语言的扩展功能,因此标准 C 源码在 GCC 下一般来说是无需修改即可编译的。

同时,GCC也提供了对 C 语言标准的控制选项,用以解决不同语法规则之间的冲突问题,最常用的是 -ansi-pedantic-std

-ansi:禁止与ANSI/ISO标准冲突的GNU扩展特性,包括对GNU C标准库 glibc 的支持;

-pedantic:禁止与ANSI/ISO标准不符的GNU扩展特性,更加严格。

-std:

  • -ansi:兼容 ANSI/ISO 标准

    一个合法的 ANSI/ISO C 程序,可能无法兼容 GNU C 的扩展特性,可以通过 -ansi 选项禁用那些与 ANSI/ISO C 标准冲突的 GNU 扩展,即令 GCC 编译器以兼容 ANSI/ISO C 标准的方式编译程序。例如:

    #include <stdio.h>
    int main(void)
    {
    const char asm[] = "6502";
    printf("the staring asm is '%s'\n", asm);
    return 0;
    }

    这里,变量名 asmANSI/ISO 标准中是合法的,但 asmGNU 扩展中是关键词,用于指示 C 函数中混编的汇编指令,直接编译会出现错误:

    $ gcc -Wall ansi.c -o ansi

    使用 -ansi 选项后,即以 ANSI/ISO C 标准编译,可成功编译。

    $ gcc -Wall -ansi ansi.c -o ansi

    asm 类似的关键词包括:inlinetypeofunixvax 等等,更多细节参考GCC参考手册 “Using GCC”。

    -ansi 选项还会同时关闭 GNU C库,对于 GNU C 库中特有的变量、宏定义、函数接口的调用都会出现未定义错误, GNU C 库对外提供了一些功能特性的宏开关,可以打开部分特性,例如,POSIX扩展(_POSIX_C_SOURCE),BSD扩展(_BSD_SOURCE),SVID扩展(_SVID_SOURCE),XOPEN扩展(_XOPEN_SOURCE)和GNU扩展(_GNU_SOURCE)。

    举个例子,下面的预定义M_PI 是 GNU C库 math.h 的一部分,不在 ANSI/ISO C 标准库中。

    #include <math.h>
    #include <stdio.h>
    int main(void)
    {
    printf("the value of pi is %f\n",M_PI);
    return 0;
    }

    如果强制使用 -ansi 编译会出现未定义错误。

    $ gcc -Wall -ansi pi.c -o pi

    如果一定需要使用GNU C库宏定义,可以单独打开对GNU C库的扩展。

    $ gcc -Wall -ansi -D_GNU_SOURCE pi.c

    这里_GNU_SOURCE 宏打开所有的扩展,而 POSIX 扩展在这里如果与其他扩展有冲突,则优先于其他扩展。有关特征测试宏进一步信息可以参见 GNU C 库参考手册。

  • -pedantic:严格的 ANSI/ISO 标准

    同时使用 -ansi -pedantic 选项,编译器将会以更加严格的标准检查语法规则是否符合 ANSI/ISO C 标准,同时拒绝所有 GNU C 扩展语法规则。

    下面是一个用到变长数组的程序,变长数组是 GNU C 扩展语法,但也不会妨碍合法的 ANSI/ISO 程序的编译。

    int main(int argc, char *argv[])
    {
    int i, n = argc;
    double x[n];
    for (i = 0; i < n; i++)
    x[i] = i;
    return 0;
    }

    因此,使用 -ansi 不会出现相关编译错误:

    $ gcc -Wall -ansi gnuarray.c -o gnuarray

    但是,使用 -ansi -pedantic 编译,会出现违反 ANSI/ISO 标准的警告。

    $ gcc -Wall -ansi -pedantic gnuarray.c -o gnuarray
  • -std:指定标准

    可以通过 -std 选项来控制 GCC 编译时采用的C语言标准。支持的可选项包括:

    • -std=c89 -std=iso9899:1990
    • -std=iso9899:199409
    • -std=c99 -std=iso9899:1999
    • -std=gnu89
    • -std=gnu99

编译警告

  • -Wall

    -Wall 警告选项本身会打开很多常见错误的警告,这些错误通常总是有问题的代码构造,或是很容易用明白无误的方法改写的错,因此可以看作潜在严重问题的指示。这些错误主要包括:

    • -Wcomment:对嵌套注释进行警告;

      /* commented out
      double x = 1.23; /* x-position*/
      */
    • -Wformat:对格式化字符串与对应函数参数的类型一致性进行警告;

    • -Wunused:对声明但未使用的变量进行警告;

    • -Wimplicit:对未声明就被使用的变量进行警告;

    • -Wreturn-type:对函数声明返回类型与实际返回类型的一致性进行警告;

      int main(void)
      {
      printf("hello, world!\n");
      return;
      }

      -Wall 包含的警告选项都可以在GCC参考手册 Using GCC 中找到。

  • 其他警告

    GCC提供了很多可选的警告选项,它们没有包含在 -Wall 中,但仍然很有参考价值。这些警告选项可能会对合法代码也报警,所以编译时通常不需要长期开启,建议周期性的使用,检查输出结果,或在某些程序和文件中打开,更加合适。

    • -W:对常见的编程错误进行报警,类似 -Wall ,也常和 -Wall 一起用。
    • -Wconversion:对可能引起意外结果的隐式类型转换进行报警。
    • -Wshadow:对重复定义同名变量进行报警。
    • -Wcast-qual:对可能引起移除修饰符特性的操作进行报警。
    • -Wwrite-strings:该选项隐含的使得所有字符串常量带有 const 修饰符。
    • -Wtraditional:对那些在 ANSI/ISO 编译器下和在 ANSI 之前的“传统”译器下编译方式不同的代码进行警告。
    • -Werror:将警告转换为错误,一旦警告出现即停止编译。

警告选项会产生诊断性的信息,但不会终止编译过程,如果需要出现警告后停止编译过程可以使用 -Werror

预处理选项

宏定义

这里主要介绍GNU C预处理器中宏定义的常见用法。首先,看一个宏定义的例子:

#include <stdio.h>
int main(void)
{
#ifdef TESTNUM
printf("TestMum is %d\n",TESTNUM);
#endif
#ifdef TESTMSG
printf("TestMsg:%s\n",TESTMSG);
#endif
printf("Runing...\n");
return 0;
}

如果在编译命令中不加任何宏定义选项,则编译器会在预处理阶段忽略 TESTNUM 宏定义包裹的代码:

$ gcc -Wall dtest.c -o dtest
$ ./dtest
Runing...

如果在编译中增加 -D 选项,则编译器会在预处理阶段将 TESTNUM 宏定义包裹的代码进行编译:

$ gcc -Wall -DTESTNUM dtest.c -o dtest
$ ./dtest
TestNum is 1
Runing...

如果对宏定义进行宏赋值,则编译器会在预处理阶段将赋值内容替换到 TESTNUM 宏定义位置:

$ gcc -Wall -DTESTNUM=20 dtest.c -o dtest
$ ./dtest
TestNum is 20
Runing...

利用命令行上的双引号,宏可以被定义成字符串,字符串可以包含引号,需要用 \ 进行转义:

$ gcc -Wall -DTESTMSG="\"Hello,World!\"" dtest.c -o dtest
$ ./dtest
Hello,World!
Runing...

上述字符串也可以定义成空值,例如:-DTESTMSG="" ,这样的宏还是会被 #ifdef 看作已定义,但该宏会被展开为空。

预处理输出

使用 -E 选项,GCC 可以只允许运行预处理器,并直接显示预处理器对源代码的处理结果,并且不会进行后续的编译处理流程:

$ gcc -DTESTMSG="\"Hello,World!\"" -E dtest.c

预处理器会对宏文件进行直接替换,并对头文件进行展开,预处理器还会增加一些以 #line-number "source-file" 形式记录源文件和行数,便于调试和编译器输出诊断信息。

被预处理的系统头文件通常产生许多输出,它们可以被重定向到文件中,或者使用 -save-temps 选项进行保存:

$ gcc -c -save-temps dtest.c

运行该命令之后,预处理过的输出文件将被存储到 .i 文件中,同时还会保存 .s 汇编文件和 .o 对象文件。

调试信息

通常,编译器输出的可执行文件只是一份作为机器码的指令序列,而不包含源程序中的任何引用信息,例如变量名或者行号等,因此如果程序出现问题,我们将无法确定问题在哪里。

  • 添加调试信息

    GCC 提供 -g 选项,可以在编译生成可执行文件时添加另外的调试信息,这些信息可以在追踪错误时从特定的机器码指令对应到源代码文件中的行,调试器可以在程序运行时检查变量的值。

    $ gcc -Wall -g helloworld.c -o helloworld
    
    
  • 检查core文件

    程序异常退出时,操作系统将程序崩溃瞬间的内存状态写入到 core 文件,结合 -g 选项生成的符号表中的信息,可以进一步确定程序崩溃时运行到的位置和此刻变量的值。

    但是,通常情况下操作系统配置在默认情况是下不写 core文件 的,在开始之前,我们可以先查询 core文件 的最大限定值:

    $ ulimit -c

    如果结果为0,则不会生成 core文件 ,我们可以扩大 core文件 上限,以便允许任何大小的 core 文件

    $ ulimit -c unlimited

    这里,再准备一个包含非法内存错误的简单程序,我们用它来生成 core 文件:

    int a(int *p)
    {
    int y = *p;
    return y;
    }
    int main(void)
    {
    int *p = 0;
    return a(p);
    }

    编译生成带调试信息的可执行文件:

    $ gcc -g null.c
    $ ./a.out
    Segmentation fault (core dumped)

    根据可执行文件和 core文件 即可利用 gdb 进行调试,定位错误位置:

    $ gdb a.out core
  • 回溯堆栈

    利用 gdbbacktrace 命令可以方便的显示当前执行点的函数调用及其参数,并且利用 up down 命令在堆栈的不同层级之间移动,检查变量变化。

    gdb 相关操作可以参考 “Debugging with GDB: The GNU Source-Level Debugger”

优化选项

编译器的优化目标通常是 提高代码的执行速度 或者 减少代码体积

源码级优化

  • 公共子表达式消除

    在优化功能打开之后,编译器会自动对源代码进行分析,使用临时变量对多次重用的计算结果进行替代,减少重复计算。例如:

    x = cos(v)*(l+sin(u/2)) + sin(w)*(l-sin(u/2))

    可以用临时变量 t 替换 sin(u/2)

    t=sin(u/2)
    x = cos(v)*(l+t) + sin(w)*(l-t)
  • 函数内嵌

    函数调用过程中,需要花费一定的额外时间来实施调用过程(压栈、跳转、返回执行点等),而函数内嵌优化会将计算过程简单但是调用频繁的函数调用直接用函数体进行替换,提升那些被频繁调用函数的执行效率。例如:

    double sq(double x)
    {
    return x * x;
    }
    int main(void)
    {
    double sum;
    for (int i = 0; i < 1000000; i++)
    {
    sum += sq(i + 5);
    }
    }

    经过嵌入优化后,大致会得到:

    int main(void)
    {
    double sum;
    for (int i = 0; i < 1000000; i++)
    {
    double t = (i + 5);
    sum += t * t;
    }
    }

    GCC 会使用一些启发式的方法选择哪些函数要内嵌,比如函数要适当小。另外,嵌入优化方式只在单个对象文件基础上实施。关键字 inline 可以显示要求某个指定函数在用到的地方尽量内嵌。

速度-空间优化

编译器会根据指定的优化条件,对可执行文件的执行速度和空间进行折中优化,使得最终结果可能会牺牲一些执行速度来节省文件大小,也可能会牺牲文件的空间占用来提升运行速度,或是在两者之间取得一定平衡。

循环展开 即是一种以常见的空间换时间的优化方式,例如:

for(i = 0; i < 4; i++)
{
y[i] = i;
}

直接将该循环展开后进行直接赋值,可以有效减少循环条件的判断,减少运行时间:

y[0] = 0;
y[1] = 1;
y[2] = 2;
y[3] = 3;

对于支持并行处理的处理器,经过优化后的代码可以使用并行运行,提高速度。对于未知边界的循环,例如:

for(i = 0; i < n; i++)
{
y[i] = i;
}

可能会被编译器优化成这样:

for(i = 0; i < (n % 2); i++)
{
y[i] = i;
}
for(; i + 1 < n; i += 2)
{
y[i] = i;
y[i+1] = i+1;
}

上面第二个循环中的操作即可进行并行化处理。

指令调度优化

指令化调度是最底层的优化手段,由编译器根据处理器特性决定各指令的最佳执行次序,以获取最大的并行执行,指令调度没有增加可执行文件大小,但改善了运行速度,对应的代价主要体现在编译过程所需处理时间,以及编译器占用的内存空间。

优化级别选项

GCC 编译器为了生成满足速度和空间要求的可执行文件,对优化级别使用 -O 选项进行定义。

  • -O0 或不指定 -O 选项(默认)

    不实施任何优化,源码被尽量直接转换到对应指令,编译时间最少,适合调试使用。

  • -O1-O

    打开那些不需要任何速度-空间折衷的最常见形式的优化,对代码大小和执行时间进行优化,生成的可执行文件更小、更快,编译时间较少。

  • -O2

    在上一级优化的基础上,增加指令调度优化,文件大小不会增加,编译时间有所增加,它是各种 GNU 软件发行包的默认优化级别。

  • -O3

    在上一级优化的基础上,增加函数内嵌等深度优化,提升可执行文件的速度,但会增加它的大小,这一等级的优化可能会产生不利结果。

  • -Os

    该选项主要针对内存和磁盘空间受限的系统生成尽可能小的可执行文件。

  • -funroll-loops

    该选项独立于上述优化选项,可以打开循环展开,增加可执行文件大小。

通常,开发调试过程可以使用 -O0 ,开发部署时可以用 -O2 ,优化等级也不是越多越好、越高越好,需要尽量根据程序差异和使用平台的差异经过测试数据确定。

优化和编译警告

开启优化后,作为优化过程的一部分,编译器检查所有变量的使用和他们的初始值,称为 数据流分析 。数据流分析的一个作用是检查是否使用了未初始化的变量,在开启优化之后,-Wall 中的 -Wuninitialized 选项会对未初始化变量的读操作产生警告。因此,开启优化后,GCC 会输出一些额外的警告信息,而这些信息在不开启优化时是不会产生的。

编译过程

这一部分主要介绍 GCC 怎么把源文件转变成可执行文件。编译过程是一个多阶段的过程,涉及到多个工具,包括 GNU 编译器(gcc 或 g++ 前端),GNU汇编器 as ,GNU 链接器 ld ,编译过程中用到的整套工具被称为工具链。

预处理过程

预处理过程是利用预处理器 cpp 来扩展宏定义和头文件,GCC 执行下面的命令来实施这个步骤:

$ cpp hello.c > hello.i

该命令可以输出经过预处理器处理输出的源文件 hello.i

编译过程

编译过程是编译器把预处理的源代码经过翻译处理成特定处理器的汇编语言,命令行 -S 选项可以将预处理过的 .i 源文件转变成 .s 汇编文件。

$ gcc -Wall -S hello.i

该命令可以输出经过编译器处理输出的汇编文件 hello.s

汇编过程

汇编过程是汇编器 as 把编译处理的汇编文件转变成机器码,并生成对象文件,如果汇编文件中包含外部函数的调用,汇编器会保留外部函数的地址处于未定义状态,留给后面的链接器填写。

$ as hello.s -o hello.o

这里, -o 选项用来指定输出 .o 文件。

链接过程

链接过程是链接器 ld 将各对象文件链接到一起,生成可执行文件。在链接过程中,链接器会将汇编输出的 .o 文件和系统中的 C 运行库中必要的外部函数链接到一起。

$ gcc hello.o

链接器主要调用 ld 命令,也可以直接把对象文件与C标准库链接,生成可执行文件。

编译工具

归档工具 ar

GNU 归档工具 ar 用于把多个对象文件组合成归档文件,也被称为库,归档文件是多个对象文件打包在一起发行的简便方法。

在上面的多个源文件例子中,假设有 hello.c world.c hello_world.c 三个程序, 我们可以现将三者编译成对象文件:

$ gcc -Wall -c hello.c
$ gcc -Wall -c world.c
$ gcc -Wall -c hello_world.c

生成 hello.o world.o hello_world.o ,我们将两个子函数打包成静态文件库:

$ ar cr libhello.a hello.o world.o

选项 cr 不需要 - ,代表 creat and replacelibhello.a 为目标文件,hello.o world.o 表示输入文件。

也可以通过 t 选项,查看库文件中包含的文件:

$ ar t libhello.a
hello.o
world.o

再利用 libhello.ahello_world.o 来链接生成可执行文件:

$ gcc -Wall hello_world.o libhello.a -o hello
$ ./hello
Hello, world

或者使用 -l 选项:

$ gcc -Wall -L. hello_world.o -lhello -o hello
$ ./hello
Hello, world

性能剖析器 gprof

GNU 性能剖析器 gprof 是衡量程序性能的有用工具,它可以记录每个函数调用的次数和每个函数每次调用所花的时间。

这里准备了一个数学上的 Collatz 猜想程序,我们用 gprof

来对其进行分析:

#include <stdio.h>
unsigned int step(unsigned int x)
{
if(x % 2 == 0)
{
return (x / 2);
}
else
{
return (3 * x + 1);
}
}
unsigned int nseq(unsigned int x0)
{
unsigned int i = 1, x;
if(x0 == 1 || x0 == 0)
return i;
x = step(x0);
while(x != 1 && x != 0)
{
x = step(x);
i++;
}
return i;
}
int main(void)
{
unsigned int i, m = 0, im = 0;
for(i = 1; i < 500000; i++)
{
unsigned int k = nseq(i);
if(k > m)
{
m = k;
im = i;
printf("sequence length = %u for %u\n", m, im);
}
}
return 0;
}

为了剖析性能,程序在编译时需要用到 -pg 选项参与编译链接:

$ gcc -Wall -c -pg collatz.c
$ gcc -Wall -pg collatz.o

这样即可生成可分析的可执行文件,其包含有记录每个函数所花时间的额外指令。

为了进行分析,需要先正常运行一次可执行文件:

$ ./a.out

运行结束后,会在本目录下生成一个 gmon.out 文件。再以可执行文件名作为参数运行 gprof 就可以分析这些数据:

% cumulative self self total
time seconds seconds calls ns/call ns/call name
50.00 0.13 0.13 499999 260.00 500.00 nseq
46.15 0.25 0.12 62135400 1.93 1.93 step
3.85 0.26 0.01 frame_dummy

剖析数据的第一列显示的是该程序的所有子函数的运行时间。

代码覆盖测试工具 gcov

GNU 代码覆盖测试工具 gcov 可以用于分析程序运行期间每一行代码执行的次数,因此可以用于查找没有用到的代码区域。

我们准备下面这个小程序来展示 gcov 的功能。

#include <stdio.h>
int main(void)
{
int i;
for(i = 1; i < 10; i++)
{
if(i % 3 == 0)
printf("%d is divisible by 3\n",i);
if(i % 11 == 0)
printf("%d is divisible by 11\n",i);
}
return 0;
}

为了对该程序进行代码覆盖测试,编译时必须携带 –fprofile-arcs–ftest-coverage 选项:

$ gcc -Wall -fprofile-arcs -ftest-coverage cov.c

其中,–fprofile-arcs 用于添加计数被执行到的行的次数,而 –ftest-coverage 被用与合并程序中每条分支中的用于计数的代码。可执行程序只有在运行后才能生成代码覆盖测试数据:

$ ./a.out

.c 源文件为参数调用 gov 命令,命令会生成一个原始源码文件的带注释信息的版本,其后缀名为 gcov,包含执行到的每一行代码的运行次数,没有执行到的行数被用 ###### 标记上,根据注释信息就可以看到该源文件的覆盖情况。

文件信息

辨识文件

对于一个可执行命令执行 file 命令可以查看该文件的编译环境信息。

$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=121d574fcb968c6a83624f4d982eb74495951841, not stripped

下面是输出信息的解释:

  • ELF :可执行文件的内部格式,ELF表示 Executable and Linking Format ,另外的格式还有 COFF(Common object File Format)
  • 32-bit :表示字的位宽,另外的位宽还有64-bit
  • LSB :表示该文件的大小端方式。
  • Intel 80386 :表示该文件适用的处理器。
  • version 1 (SYSV) :表示文件内部格式的版本
  • dynamically linked :表示文件会用到的共享库,另外的还有 statically linked 表示程序是静态链接的,比如用到 -static 选项 。
  • not stripped :表示可执行文件包含符号表。

符号映射表

符号映射表存储了函数和命令变量的位置,用 nm 命令可以看到内容:

$ nm a.out
0804a01c B __bss_start
0804a01c b completed.7200
0804a014 D __data_start
0804a014 W data_start
……
0804840b T main
U puts@@GLIBC_2.0
08048380 t register_tm_clones
08048310 T _start
0804a01c D __TMC_END__
08048340 T __x86.get_pc_thunk.bx

其中,T 表示这是定义在对象文件中的函数,U 表示这是本对象文件中没有定义的函数(在其他对象文件中找到了)。

nm 命令最常用的用法是通过查找 T 项对应的函数名,检查某个库是否包含特定函数的定义。

动态链接库

当程序用到 .so 动态链接库时,需要在运行期间动态载入这些库。 ldd 命令可以列出所有可执行文件依赖的共享库文件。

$ ldd a.out
linux-gate.so.1 => (0xb7749000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7578000)
/lib/ld-linux.so.2 (0x80017000)

ldd 命令也能够用于检查共享库本身,可以跟踪共享库依赖链。

参考资料

GCC编译器基础入门的更多相关文章

  1. C语言的本质(33)——GCC编译器入门

    GCC(GNU CompilerCollection,GNU编译器套装),是由 GNU 开发的编程语言编译器.它是以GPL许可证所发行的自由软件,也是 GNU计划的关键部分.GCC原本作为GNU操作系 ...

  2. GCC编译器入门[转载]

    GCC编译器(GNU C Compiler)是GNU组织的一款开源 编译器,它是Linux环境下的默认C语言编译器.它处理能够高效的编译C语言以外,还可以编译其他语言.并且,现在的GCC已经不光包括编 ...

  3. Linux及Arm-Linux程序开发笔记(零基础入门篇)

    Linux及Arm-Linux程序开发笔记(零基础入门篇)  作者:一点一滴的Beer http://beer.cnblogs.com/ 本文地址:http://www.cnblogs.com/bee ...

  4. 【Linux开发】Linux及Arm-Linux程序开发笔记(零基础入门篇)

    Linux及Arm-Linux程序开发笔记(零基础入门篇) 作者:一点一滴的Beer http://beer.cnblogs.com/ 本文地址:http://www.cnblogs.com/beer ...

  5. [Spring框架]Spring AOP基础入门总结二:Spring基于AspectJ的AOP的开发.

    前言: 在上一篇中: [Spring框架]Spring AOP基础入门总结一. 中 我们已经知道了一个Spring AOP程序是如何开发的, 在这里呢我们将基于AspectJ来进行AOP 的总结和学习 ...

  6. Linux 基础入门(新版)”实验报告一~十二

    实验报告 日期: 2015年9月15日 一.实验的目的与要求 熟练地使用 Linux,本实验介绍 Linux 基本操作,shell 环境下的常用命令. 二.主要内容 1.Linux 基础入门& ...

  7. Linux基础入门学习笔记20135227黄晓妍

    学习计时:共24小时 读书:1小时 代码:8小时 作业:3小时 博客:12小时 一.学习目标 1. 能够独立安装Linux操作系统   2. 能够熟练使用Linux系统的基本命令   3. 熟练使用L ...

  8. Linux 基础入门 第二周9.21~9.27

    一.学习内容 本周主要学习内容主要贴合: 在进行<深入理解计算机系统>这门课的实验中没有遇到什么大问题,学习内容与上周实验<linux基础入门>有相似之处.本实验中的内容比较贴 ...

  9. T4教程1 T4模版引擎之基础入门

    T4模版引擎之基础入门   额,T4好陌生的名字,和NuGet一样很悲催,不为世人所熟知,却又在背后默默无闻的奉献着,直到现在我们项目组的人除了我之外,其它人还是对其豪无兴趣,基本上是连看一眼都懒得看 ...

随机推荐

  1. Andriod(3)——Understanding Android Resources

    Now, we will follow that introduction with an in-depth look at Android SDK fundamentals and cover re ...

  2. LNMP-day2-进阶

    部署LNMP环境 http://www.cnblogs.com/wazy/p/8386493.html 安装部署wordpress #下载wordpress [root@locahost downlo ...

  3. [Codeup 25481] swan

    莫名其妙还找到了另一个铟炔锶烃的OJ : Codeup墓地 25481: swan 时间限制: 1 Sec  内存限制: 128 MB献花: 86  解决: 13[献花][花圈][TK题库] 题目描述 ...

  4. Socket Tools的使用

    1.启动工具 Socket Tools.exe , 分享:链接:http://pan.baidu.com/s/1dFiuEHz 密码:1sv9 2.在本地创建TCP Server,自动启动监听 3.在 ...

  5. 关于javascript的单线程和异步的一些问题

    关于js单线程和异步方面突然就糊涂了,看别人的文章越看越糊涂,感觉这方面是个坑,跳进去就不好跳出来.再去看,看着看着感觉自己明白了一些东西,也不知道对不对,反正是暂时把自己说服了,这样理解能理解的通, ...

  6. PHP笔记09:PHP之 MVC理解

    1. 首先通俗地说说我对MVC的理解: Model是负责干活的,它干的活主要是从数据库中获取需要的数据以及对获取的数据按照业务逻辑进行加工处理,至于为什么要干某件活,何时干某件活它一概不管,而这正是C ...

  7. Springboot中使用ibatis输出日志

    logging.level.org.apache.ibatis=DEBUG logging.level.org.mybatis=DEBUG logging.level.java.sql.Connect ...

  8. 卢卡斯定理Lucas

    卢卡斯定理Lucas 在数论中,\(Lucas\)定理用于快速计算\(C^m_n ~ \% ~p\),即证明\(C^m_n = \prod_{i = 0} ^kC^{m_i}_{n_i}\)其中\(m ...

  9. 使用Docker发布.NET CORE API

    1.使用VS 2015 新建了一个Core API项目,然后把他的依赖升级到最新(我机器VS 2015默认的包都是rc版本),然后publish. 2.在publish目录的同级目录下,新建Docke ...

  10. CC2540 OSAL 学习其中原理,以及 给任务 添加 一个事件(定时发送串口消息)

    参考学习大神博客: http://blog.csdn.net/feilusia/article/details/51083953 : http://blog.csdn.net/xiaoleiacmer ...