ITPub博客

首页 > 应用开发 > C/C++ > gcc c语言编译流程

gcc c语言编译流程

原创 C/C++ 作者:LinuxDevqqqqqq 时间:2018-11-22 16:40:20 0 删除 编辑

1  前言

 

最近群里讨论个 C 语言的小程序,看起来都不是很难,但是大家对答案有争论,所以想讨论编译原理,做嵌入式要对编译原理有一定的了解,所以转了这篇文章。

我们之前讨论的问题如下代码

#include

#include

 

#define WEIQIFA 0;

 

int main ( void ) 

{        

     int i  = WEIQIFA;        

    i  = i ++ ;

    i ++ ;        

    printf( "%d\n" ,i);

     return ;

}

 

原来是没有那个宏 WEIQIFA 的,但是我为了举例编译原理,特意加上去,编译的第一步就是做宏替换

预编译后变成下面这样

 

int main(void)

{

 

    int i = 0;;

    i = i++;

    i++;

    printf("%d\n",i);

    return 0;

}

用 g++ -g -Wstrict-prototypes -Wall -Wunused -o test test001.c  编译

然后用 objdump -j .text -Sl test | more  查看代码可以看到汇编代码如下

 

main():

/data/weiqifa/c/bianyiyuanli/test001.c:7

#include

 

#define WEIQIFA 0;

 

int main(void) 

{

  400526:      55                      push   %rbp

  400527:      48 89 e5                mov    %rsp,%rbp

  40052a:      48 83 ec 10             sub    $0x10,%rsp

/data/weiqifa/c/bianyiyuanli/test001.c:9

 

    int i = WEIQIFA;

  40052e:      c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)

/data/weiqifa/c/bianyiyuanli/test001.c:11

 

    i = i++;

  400535:      8b 45 fc                mov    -0x4(%rbp),%eax

  400538:      8d 50 01                lea    0x1(%rax),%edx

  40053b:      89 55 fc                mov    %edx,-0x4(%rbp)

  40053e:      89 45 fc                mov    %eax,-0x4(%rbp)

/data/weiqifa/c/bianyiyuanli/test001.c:12

    i++;

  400541:      83 45 fc 01             addl   $0x1,-0x4(%rbp)

/data/weiqifa/c/bianyiyuanli/test001.c:14

 

    printf("%d\n",i);

  400545:      8b 45 fc                mov    -0x4(%rbp),%eax

  400548:      89 c6                   mov    %eax,%esi

  40054a:      bf e4 05 40 00          mov    $0x4005e4,%edi

  40054f:      b8 00 00 00 00          mov    $0x0,%eax

  400554:      e8 a7 fe ff ff          callq  400400 <printf@plt>

/data/weiqifa/c/bianyiyuanli/test001.c:16

 

    return 0;

  400559:      b8 00 00 00 00          mov    $0x0,%eax

/data/weiqifa/c/bianyiyuanli/test001.c:17

}

当时大家讨论很激烈

 

2  正文


大家肯定都知道计算机程序设计语言通常分为机器语言、汇编语言和高级语言三类。高级语言需要通过翻译成机器语言才能执行,而翻译的方式分为两种,一种是编译型,另一种是解释型,因此我们基本上将高级语言分为两大类,一种是编译型语言,例如 C , C++ , Java ,另一种是解释型语言,例如 Python 、 Ruby 、 MATLAB  、 JavaScript 。

 

本文将介绍如何将高层的 C/C++ 语言编写的程序转换成为处理器能够执行的二进制代码的过程,包括四个步骤:

预处理( Preprocessing )

编译( Compilation )

汇编( Assembly )

链接( Linking )

 


  1 、 GCC  工具链介绍

GCC ( GNU Compiler Collection , GNU 编译器套件),是由 GNU  开发的编程语言编译器。它是以 GPL 许可证所发行的自由软件,也是 GNU 计划的关键部分。 GCC 原本作为 GNU 操作系统的官方编译器,现已被大多数类 Unix 操作系统(如 Linux 、 BSD 、 Mac OS X 等)采纳为标准的编译器, GCC 同样适用于微软的 Windows 。 GCC 是自由软件过程发展中的著名例子,由自由软件基金会以 GPL 协议发布。

 

C 运行库

C 语言标准主要由两部分组成:一部分描述 C 的语法,另一部分描述 C 标准库。 C 标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义,譬如常见的 printf 函数便是一个 C 标准库函数,其原型定义在 stdio 头文件中。

C 语言标准仅仅定义了 C 标准库函数原型,并没有提供实现。因此, C 语言编译器通常需要一个 C 运行时库( C Run Time Libray , CRT )的支持。 C 运行时库又常简称为 C 运行库。与 C 语言类似, C++ 也定义了自己的标准,同时提供相关支持库,称为 C++ 运行时库。

2 、举个例子

由于 GCC 工具链主要是在 Linux 环境中进行使用,因此本文也将以 Linux 系统作为工作环境。为了能够演示编译的整个过程,本节先准备一个 C 语言编写的简单 Hello 程序作为示例,其源代码如下所示:

#include

#include

 

#define WEIQIFA 0;

 

int main(void)

{

int i = WEIQIFA;

i = i++;

i++;

printf("%d\n",i);

return 0;

}  

3 、编译过程

 

3.1  预处理

预处理的过程主要包括以下过程:

1 、将所有的 #define 删除,并且展开所有的宏定义,并且处理所有的条件预编译指令,比如 #if #ifdef #elif #else #endif 等。

2 、处理 #include 预编译指令,将被包含的文件插入到该预编译指令的位置。

3 、删除所有注释 “//” 和 “/* */” 。

4 、添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。

5 、保留所有的 #pragma 编译器指令,后续编译过程需要使用它们。

使用 gcc 进行预处理的命令如下:

$gcc -E test001.c -o test001.i //  将源文件 test001.c 文件预处理生成 test001.i

// GCC 的选项 -E 使 GCC 在进行完预处理后即停止

test001.i 文件可以作为普通文本文件打开进行查看,其代码片段如下所示:

extern char *__stpncpy (char *__restrict __dest,

   const char *__restrict __src, size_t __n)

     __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1, 2)));

extern char *stpncpy (char *__restrict __dest,

        const char *__restrict __src, size_t __n)

     __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1, 2)));

# 658 "/usr/include/string.h" 3 4

 

# 3 "test001.c" 2

 

 

# 6 "test001.c"

int main(void)

{

 

    int i = 0;;

 

    i = i++;

    i++;

 

    printf("%d\n",i);

 

    return 0;

}

 

3.2  编译

编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。

使用 gcc 进行编译的命令如下:

$ gcc -S test001.i -o test001.s //  将预处理生成的 test001.i 文件编译生成汇编程序 test001.s

// GCC 的选项 -S 使 GCC 在执行完编译后停止,生成汇编程序

上述命令生成的汇编程序 test001.s 的代码片段如下所示,其全部为汇编代码。

weiqifa@ubuntu:~/c/bianyiyuanli$ cat test001.s

        .file  "test001.c"

        .section        .rodata

.LC0:

        .string "%d\n"

        .text

        .globl main

        .type  main, @function

main:

.LFB0:

        .cfi_startproc

        pushq  %rbp

        .cfi_def_cfa_offset 16

        .cfi_offset 6, -16

        movq   %rsp, %rbp

        .cfi_def_cfa_register 6

        subq   $16, %rsp

        movl   $0, -4(%rbp)

        movl   -4(%rbp), %eax

        leal   1(%rax), %edx

        movl   %edx, -4(%rbp)

        movl   %eax, -4(%rbp)

        addl   $1, -4(%rbp)

        movl   -4(%rbp), %eax

        movl   %eax, %esi

        movl   $.LC0, %edi

        movl   $0, %eax

        call   printf

        movl   $0, %eax

        leave

        .cfi_def_cfa 7, 8

        ret

        .cfi_endproc

.LFE0:

        .size  main, .-main

        .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609"

        .section        .note.GNU-stack,"",@progbits

weiqifa@ubuntu:~/c/bianyiyuanli$

 

3.3  汇编

汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为 .o 的目标文件中。由于每一个汇编语句几乎都对应一条处理器指令,因此,汇编相对于编译过程比较简单,通过调用 Binutils 中的汇编器 as 根据汇编指令和处理器指令的对照表一一翻译即可。

当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成 .o 目标文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部分了,但是在链接之前还不能执行。

使用 gcc 进行汇编的命令如下:

$ gcc -c test001.s -o test001.o //  将编译生成的 test001.s 文件汇编生成目标文件 test001.o

// GCC 的选项 -c 使 GCC 在执行完汇编后停止,生成目标文件

// 或者直接调用 as 进行汇编

$ as -c test001.s -o test001.o // 使用 as 将 hello.s 文件汇编生成目标文件

注意: test001.o 目标文件为 ELF ( Executable and Linkable Format )格式的可重定向文件。

3.4  链接

链接也分为静态链接和动态链接,其要点如下:

1 、静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。链接器将函数的代码从其所在地(不同的目标文件或静态链接库中)拷贝到最终的可执行程序中。为创建可执行文件,链接器必须要完成的主要任务是:符号解析(把目标文件中符号的定义和引用联系起来)和重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用)。

2 、动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。

2.1 、在 Linux 系统中, gcc 编译链接时的动态库搜索路径的顺序通常为:首先从 gcc 命令的参数 -L 指定的路径寻找;再从环境变量 LIBRARY_PATH 指定的路径寻址;再从默认路径 /lib 、 /usr/lib 、 /usr/local/lib 寻找。

2.2 、在 Linux 系统中,执行二进制文件时的动态库搜索路径的顺序通常为:首先搜索编译目标代码时指定的动态库搜索路径;再从环境变量 LD_LIBRARY_PATH 指定的路径寻址;再从配置文件 /etc/ld.so.conf 中指定的动态库搜索路径;再从默认路径 /lib 、 /usr/lib 寻找。

3 、在 Linux 系统中,可以用 ldd 命令查看一个可执行程序依赖的共享库。

 

由于链接动态库和静态库的路径可能有重合,所以如果在路径中有同名的静态库文件和动态库文件,比如 libtest.a 和 libtest.so , gcc 链接时默认优先选择动态库,会链接 libtest.so ,如果要让 gcc 选择链接 libtest.a 则可以指定 gcc 选项 -static ,该选项会强制使用静态库进行链接。以 Hello World 为例: 4 、如果使用命令 “gcc hello.c -o hello” 则会使用动态库进行链接,生成的 ELF 可执行文件的大小(使用 size 命令查看)和链接的动态库(使用 ldd 命令查看)如下所示:

weiqifa@ubuntu:~/c/bianyiyuanli$ gcc test001.c -o test001

weiqifa@ubuntu:~/c/bianyiyuanli$ size test001

   text   data     bss     dec    hex filename

   1208    552       8    1768    6e8 test001

weiqifa@ubuntu:~/c/bianyiyuanli$ ldd test001// 可以看出该可执行文件链接了很多其他动态库,主要是 Linux 的 glibc 动态库

        linux-vdso.so.1 =>  (0x00007ffe90dbd000)

        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f930be92000)

        /lib64/ld-linux-x86-64.so.2 (0x00007f930c25c000)

weiqifa@ubuntu:~/c/bianyiyuanli$

5 、如果使用命令 “gcc -static hello.c -o hello” 则会使用静态库进行链接,生成的 ELF 可执行文件的大小(使用 Binutils 的 size 命令查看)和链接的动态库(使用 Binutils 的 ldd 命令查看)如下所示:

$ gcc -static test001.c -o test001

$ size test001 // 使用 size 查看大小

text data bss dec hex filename

823382 7284 6360 837026 cc5a2 test001 // 可以看出 text 的代码尺寸变得极大

$ ldd test001

not a dynamic executable // 说明没有链接动态库

链接器链接后生成的最终文件为 ELF 格式可执行文件,一个 ELF 可执行文件通常被链接为不同的段,常见的段譬如 .text 、 .data 、 .rodata 、 .bss 等段。

 

4 、 ELF 文件分析

4.1 ELF 文件的段

ELF 文件格式如下图所示,位于 ELF Header 和 Section Header Table 之间的都是段( Section )。一个典型的 ELF 文件包含下面几个段:

text :已编译程序的指令代码段。

rodata : ro 代表 read only ,即只读数据(譬如常数 const )。

data :已初始化的 C 程序全局变量和静态局部变量。

bss :未初始化的 C 程序全局变量和静态局部变量。

debug :调试符号表,调试器用此段的信息帮助调试。

如果对 ELF 文件特别感兴趣的,可以看看这个网站讲的非常详细非常好

 

 


  可以使用 readelf -S 查看其各个 section 的信息如下:

 

weiqifa@ubuntu:~/c/bianyiyuanli$ readelf -S test001

There are 33 section headers, starting at offset 0xde500:

 

Section Headers:

  [Nr] Name              Type             Address           Offset

       Size              EntSize          Flags Link  Info  Align

  [ 0]                   NULL             0000000000000000  00000000

       0000000000000000  0000000000000000           0    0     0

  [ 1] .note.ABI-tag     NOTE             0000000000400190  00000190

       0000000000000020  0000000000000000   A      0     0     4

  [ 2] .note.gnu.build-i NOTE             00000000004001b0  000001b0

       0000000000000024  0000000000000000   A      0     0     4

  [ 3] .rela.plt         RELA             00000000004001d8  000001d8

       00000000000000f0  0000000000000018  AI      0    24     8

  [ 4] .init             PROGBITS         00000000004002c8  000002c8

       000000000000001a  0000000000000000  AX      0     0     4

  [ 5] .plt              PROGBITS         00000000004002f0  000002f0

       00000000000000a0  0000000000000000  AX      0     0     16

  [ 6] .text             PROGBITS         0000000000400390  00000390

       000000000009e5b4  0000000000000000  AX      0     0     16

  [ 7] __libc_freeres_fn PROGBITS         000000000049e950  0009e950

       0000000000002529  0000000000000000  AX      0     0     16

  [ 8] __libc_thread_fre PROGBITS         00000000004a0e80  000a0e80

       00000000000000de  0000000000000000  AX      0     0     16

  [ 9] .fini             PROGBITS         00000000004a0f60  000a0f60

       0000000000000009  0000000000000000  AX      0     0     4

  [10] .rodata           PROGBITS         00000000004a0f80  000a0f80

       000000000001d244  0000000000000000   A      0     0     32

  [11] __libc_subfreeres PROGBITS         00000000004be1c8  000be1c8

       0000000000000050  0000000000000000   A      0     0    8

  [12] __libc_atexit     PROGBITS         00000000004be218  000be218

       0000000000000008  0000000000000000   A      0     0     8

  [13] .stapsdt.base     PROGBITS         00000000004be220  000be220

       0000000000000001  0000000000000000   A      0     0     1

  [14] __libc_thread_sub PROGBITS         00000000004be228  000be228

       0000000000000008  0000000000000000   A      0     0     8

  [15] .eh_frame         PROGBITS         00000000004be230  000be230

       000000000000af5c  0000000000000000   A      0     0     8

  [16] .gcc_except_table PROGBITS         00000000004c918c  000c918c

       00000000000000a3  0000000000000000   A      0     0     1

  [17] .tdata            PROGBITS         00000000006c9eb8  000c9eb8

       0000000000000020  0000000000000000 WAT       0    0     8

  [18] .tbss             NOBITS           00000000006c9ed8  000c9ed8

       0000000000000030  0000000000000000 WAT       0    0     8

  [19] .init_array       INIT_ARRAY       00000000006c9ed8  000c9ed8

       0000000000000010  0000000000000000  WA      0     0     8

  [20] .fini_array       FINI_ARRAY       00000000006c9ee8  000c9ee8

       0000000000000010  0000000000000000  WA      0     0     8

  [21] .jcr              PROGBITS         00000000006c9ef8  000c9ef8

       0000000000000008  0000000000000000  WA      0     0     8

  [22] .data.rel.ro      PROGBITS         00000000006c9f00  000c9f00

       00000000000000e4  0000000000000000  WA      0     0     32

  [23] .got              PROGBITS         00000000006c9fe8  000c9fe8

       0000000000000010  0000000000000008  WA      0     0     8

  [24] .got.plt          PROGBITS         00000000006ca000  000ca000

       0000000000000068  0000000000000008  WA      0     0     8

  [25] .data             PROGBITS         00000000006ca080  000ca080

       0000000000001ad0  0000000000000000  WA      0     0     32

  [26] .bss              NOBITS           00000000006cbb60  000cbb50

       0000000000001878  0000000000000000  WA      0     0    32

  [27] __libc_freeres_pt NOBITS           00000000006cd3d8  000cbb50

       0000000000000030  0000000000000000  WA      0     0     8

  [28] .comment          PROGBITS         0000000000000000  000cbb50

       0000000000000035  0000000000000001  MS      0     0     1

  [29] .note.stapsdt     NOTE             0000000000000000  000cbb88

       0000000000000f18  0000000000000000           0    0     4

  [30] .shstrtab         STRTAB           0000000000000000  000de392

       0000000000000169  0000000000000000           0    0     1

  [31] .symtab           SYMTAB           0000000000000000  000ccaa0

       000000000000b0a0  0000000000000018          32  710     8

  [32] .strtab           STRTAB           0000000000000000  000d7b40

       0000000000006852  0000000000000000           0    0     1

Key to Flags:

  W (write), A (alloc), X (execute), M (merge), S (strings), l (large)

  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)

  O (extra OS processing required) o (OS specific), p (processor specific)

weiqifa@ubuntu:~/c/bianyiyuanli$

 

 

4.2  反汇编 ELF

由于 ELF 文件无法被当做普通文本文件打开,如果希望直接查看一个 ELF 文件包含的指令和数据,需要使用反汇编的方法。

使用 objdump -D 对其进行反汇编如下:

 

$ objdump -D test001

 eeb:  00 6c 69 62             add    %ch,0x62(%rcx,%rbp,2)

 eef:  63 00                   movslq (%rax),%eax

 ef1:  6c                      insb   (%dx),%es:(%rdi)

 ef2:  6f                      outsl  %ds:(%rsi),(%dx)

 ef3:  6e                      outsb  %ds:(%rsi),(%dx)

 ef4:  67 6a 6d                addr32 pushq $0x6d

 ef7:  70 5f                   jo     f58 <data.8386+0xf10>

 ef9:  74 61                   je     f5c <data.8386+0xf14>

 efb:  72 67                   jb     f64 <data.8386+0xf1c>

 efd:  65 74 00                gs je  f00 <data.8386+0xeb8>

 f00:  38 40 25                cmp    %al,0x25(%rax)

 f03:  72 64                   jb     f69 <data.8386+0xf21>

 f05:  69 20 2d 34 40 25       imul   $0x2540342d,(%rax),%esp

 f0b:  65 61                   gs (bad) 

 f0d:  78 20                   js     f2f <data.8386+0xee7>

 f0f:  38 40 25                cmp    %al,0x25(%rax)

 f12:  72 64                   jb     f78 <data.8386+0xf30>

 f14:  78 00                   js     f16 <data.8386+0xece>

 

使用 objdump -S 将其反汇编并且将其 C 语言源代码混合显示出来:

 weiqifa@ubuntu:~/c/bianyiyuanli$ gcc -o test001 -g test001.c 

weiqifa@ubuntu:~/c/bianyiyuanli$ objdump -S test001          

 

test001:     file format elf64-x86-64

 

0000000000400526

 

 


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31561984/viewspace-2221194/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
微信公众号-嵌入式Linux 嵌入式开发有10余年,从刚开始的STC89C51单片机,STM32单片机,ARM,Linux ,Nordic ble ,Android系统开发,在学习和开发过程中,积累了很多经验,希望这些学习经验跟在码农路上一起奋斗的同学们一起分享。

注册时间:2018-11-13

  • 博文量
    61
  • 访问量
    133474