Linux编译器 —— gcc和g++

目录

前言

1. gcc和g++的基本理解

2. gcc编译选项


前言

在了解编译器之前,首先需要了解有关程序编译的流程,详见

程序的编译过程(简述)_编译流程_七月不远.的博客-CSDN博客

1. gcc和g++的基本理解

首先要知道,GCC和gcc是两个不同的东西 

  • GCC:GNU Compiler Collection(GUN 编译器集合),最初是作为C语言的编译器(GNU C Compiler),现在可以编译C、C++、JAVA、Fortran、Pascal、Object-C、Ada等语言
  • gcc是GCC中的GUN C Compiler(C 编译器)
  • g++是GCC中的GUN C++ Compiler(C++编译器)

gcc和g++本质上并不是编译器,只是一个驱动器(driver),通过调用GCC编译器来完成对不同高级语言的编译工作,比如,用gcc编译一个.c文件的话,会有以下几个步骤:

  1. 调用预处理器 比如 cpp
  2. 调用真正的编译器 比如 cc or cc1
  3. 调用汇编器 比如 as
  4. 调用链接器 比如 ld

gcc 和 g++ 的区别无非就是调用的编译器不同, 并且传递给链接器的参数不同,更准确的说法是:gcc调用了C compiler,而g++调用了C++ compiler


g++:

  • g++ 会把 .c 文件当做是 C++ 语言 (在 .c 文件前后分别加上 -xc++ 和 -xnone, 强行变成 C++), 从而调用 cc1plus 进行编译
  • g++ 遇到 .cpp 文件也会当做是 C++, 调用 cc1plus 进行编译
  • g++ 还会默认告诉链接器, 让它链接上 C++ 标准库

gcc:

  • gcc 会把 .c 文件当做是 C 语言. 从而调用 cc1 进行编译
  • gcc 遇到 .cpp 文件, 会处理成 C++ 语言. 调用 cc1plus 进行编译
  • gcc 默认不会链接上 C++ 标准库

只要是 GCC 支持编译的程序代码,都可以使用 gcc 命令完成编译。可以这样理解,gcc 是 GCC 编译器的通用编译指令,因为根据程序文件的后缀名,gcc 指令可以自行判断出当前程序所用编程语言的类别 

但如果使用 g++ 指令,则无论目标文件的后缀名是什么,该指令都一律按照编译 C++ 代码的方式编译该文件。也就是说,对于 .c 文件来说,gcc 指令以 C 语言代码对待,而 g++ 指令会以 C++ 代码对待。但对于 .cpp 文件来说,gcc 和 g++ 都会以 C++ 代码的方式编译

因此并不能说gcc只能编译C代码,g++只能编译C++代码

不能用gcc编译C++程序的原因是:gcc在链接时不能自动和C++程序使用的库联接,因此只能使用g++来完成链接过程 

在Linux下,使用 gcc -v 命令可以查看gcc版本(g++类似),一般云服务器不经配置默认版本为4.8.5

使用 sudo yum install -y gcc-g++ 可以安装g++

2. gcc编译选项

假设我们有一个test.c源文件,使用gcc进行编译的最简单方式就是 gcc + .c文件名,默认生成一个a.out的可执行文件

gcc test.c

为了更好地控制编译过程,介绍如下选项

选项 作用
-x 指明输入文件的语言(否则gcc会根据文件后缀进行默认选择)
-E 在预处理阶段后停止,不运行编译操作
-S 在编译阶段后停止,不进行汇编操作
-c 编译或者汇编源文件,不进行链接操作
-o 后面接文件名,表示结果输出到哪个文件中
-v 打印编译各阶段执行的命令,以及gcc的版本号
-ansi 对.c文件,等价于-std=c90;对.cpp文件,等价于-std=c++98
-std= 确定语言标准,只在编译C和C++文件时使用
-w 不生成任何警告信息
-Wall 生成所有警告信息
-Werror 把警告信息作为错误处理
-I 指定头文件的搜索路径
-L 指定库文件的搜索路径
-l 指定链接库文件名
-g 生成调试信息
-O 优化等级,可以取值为0、1、2、3
-D 定义宏
-static 此选项对生成的文件采用静态链接
-shared 此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库

以下所有代码源文件均为test.c 

-x

#include 

int main()
{
    class A {};

    return 0;
}

对于这样一个C++文件,其后缀名却是.c,如果不在编译时添加指明语言的选项,使用如下编译命令会报错

[hqs@VM-8-2-centos 1]$ gcc test.c 
test.c: In function ‘main’:
test.c:5:5: error: unknown type name ‘class’
    5 |     class A {};
      |     ^~~~~
test.c:5:13: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘{’ token
    5 |     class A {};

 使用 -x 选项能够成功编译

[hqs@VM-8-2-centos 1]$ gcc -x c++ test.c 
[hqs@VM-8-2-centos 1]$ ls
a.out  test.c

  -E -S -c -o

gcc -E test.c -o test.i    // 预编译完停下来,生成test.i文件
gcc -S test.i -o test.s    // 编译完停下来,生成test.s文件
gcc -c testis -o test.o    // 汇编完停下来,生成test.o文件
gcc test.o -o test.out     // 编译完成,生成test.out文件

-D

举个例子来介绍 -D 选项,对下面的test.c文件使用不同的命令进行编译

#include 

#ifdef MAX 
    int x = 1;
#endif

int main()
{
    printf("%d\n", x);                                                                                                                                               
    return 0;
}
gcc test.c -o test -D MAX  // 正确输出 1

gcc test.c -o test         // 报错

-O

-O选项可以对源文件进行优化,不同的数字代表不同的优化等级,数字越大,优化等级越高,在Linux中默认的优化等级是 -O0,就是说如果不带优化选项-O,编译码在编译时默认不进行优化

以下面test.c代码为例,通过查看生成的可执行文件的汇编代码,来观察不同优化等级下的表现

// 源代码
#include 

int main()
{
    int left = 10;
    int right = 20;
    int sum = left + right;
    printf("%d\n", sum);

    return 0;
}

-O0(默认选项)

0000000000401132 
: 401132: 55 push %rbp 401133: 48 89 e5 mov %rsp,%rbp 401136: 48 83 ec 10 sub $0x10,%rsp 40113a: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp) 401141: c7 45 f8 14 00 00 00 movl $0x14,-0x8(%rbp) 401148: 8b 55 fc mov -0x4(%rbp),%edx 40114b: 8b 45 f8 mov -0x8(%rbp),%eax 40114e: 01 d0 add %edx,%eax 401150: 89 45 f4 mov %eax,-0xc(%rbp) 401153: 8b 45 f4 mov -0xc(%rbp),%eax 401156: 89 c6 mov %eax,%esi 401158: bf 10 20 40 00 mov $0x402010,%edi 40115d: b8 00 00 00 00 mov $0x0,%eax 401162: e8 c9 fe ff ff callq 401030 401167: b8 00 00 00 00 mov $0x0,%eax 40116c: c9 leaveq 40116d: c3 retq 40116e: 66 90 xchg %ax,%ax

在-O0级别的优化中,编译器十分“老实”,依次完成了如下操作:

  1. 先将10和20分别放进内存,再赋值给寄存器
  2. 在寄存器中完成加法,将结果写回内存
  3. 将结果从内存中取出,放到寄存器%eax中
  4. 再将%eax中的结果传给%esi作为printf函数的参数
  5. 调用printf函数输出

在这种情况下生成的代码与源代码几乎是一样的,只是在生成的代码中会添加一些调试信息,以便于调试程序,因此常用于开发和调试阶段,在生产环境中,通常会使用其他优化级别,以便生成更高效的代码,提高程序的性能

-O1和-O3

-O1选项表示编译器进行基本的优化,包括删除未使用的代码、复制传递参数、移动循环不变表达式等简单的优化。这种优化不会显著增加编译时间,但会略微提高代码的性能

0000000000401132 
: 401132: 48 83 ec 08 sub $0x8,%rsp 401136: be 1e 00 00 00 mov $0x1e,%esi 40113b: bf 10 20 40 00 mov $0x402010,%edi 401140: b8 00 00 00 00 mov $0x0,%eax 401145: e8 e6 fe ff ff callq 401030 40114a: b8 00 00 00 00 mov $0x0,%eax 40114f: 48 83 c4 08 add $0x8,%rsp 401153: c3 retq 401154: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 40115b: 00 00 00 40115e: 66 90 xchg %ax,%ax

可以看到生成的可执行文件汇编代码量明显减少,编译器的行为是:

  1. 直接计算出了10+20的结果,并把这个结果30放到寄存器%esi中作为参数(-O0中第4步)
  2. 调用printf输出

对于-O3选项,编译器进行更高级别的优化,包括函数内联、循环展开、算法改进等复杂的优化。这种优化可以显著提高代码的性能,但会导致编译时间显著增加

-O3选项下的汇编代码如下,可以看到会比-O1选项的代码更短

0000000000401060 
: 401060: 48 83 ec 08 sub $0x8,%rsp 401064: be 1e 00 00 00 mov $0x1e,%esi 401069: bf 10 20 40 00 mov $0x402010,%edi 40106e: 31 c0 xor %eax,%eax 401070: e8 bb ff ff ff callq 401030 401075: 31 c0 xor %eax,%eax 401077: 48 83 c4 08 add $0x8,%rsp 40107b: c3 retq

因此,一般来说,如果开发者注重编译时间,则可以选择-O1选项,如果注重程序的性能,则可以选择-O3选项。当然,在具体的应用场景中,不同的优化级别可能会产生不同的效果,需要根据实际情况进行选择

你可能感兴趣的:(linux,linux,汇编)