GCC编译器基础入门

导语

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;
}

编译源代码:linux

$ gcc helloworld.c

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

$ gcc -Wall helloworld.c -o helloworld

-o: output 该选项用于指定存储机器码的输出文件;
-Wall: Warning all 该选项用于打开全部最经常使用的编译警告;shell

$ ./helloworld
Hello, world!

多个源文件

若是将源码分为多个源文件,咱们也可使用如下命令进行编译:编程

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

对于包含多个源文件的工程,咱们能够将编译过程分为两个阶段:小程序

第一阶段:全部 .c .h 源文件通过编译,分别生成对应的 .o 对象文件;
第二阶段:全部 .o 对象文件通过连接生成最终的可执行文件。数组

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

  • 源文件生成对象文件编程语言

    $ 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 命令也可以用于检查共享库自己,能够跟踪共享库依赖链。

参考资料

相关文章
相关标签/搜索