文章目录

  • 系列文章推荐
  • 前言
  • GCC 编译工具链
    • GCC 编译器
    • Binutils 工具集
    • glibc 库
  • X86_64平台、Ubuntu系统下的HelloWorld
    • 创建工作目录和文件
    • 编译并执行
  • ARM 平台、Debian 系统下的 HelloWorld
    • 安装 GCC 编译工具链
    • 创建工作目录和文件
    • 编译并执行
  • GCC编译过程
    • 基本语法
      • 编译过程
      • 预处理阶段
      • 编译阶段
      • 汇编阶段
      • 链接阶段

系列文章推荐

Linux文件系统目录结构
Linux必备基础
Linux构建一个deb软件安装包

前言

本文主要来自正点原子、野火Linux教程及本人理解,若有侵权请及时联系本人删除。如果本篇对您有帮助的话希望能一键三连,万分感谢。

GCC 编译工具链

GCC 编译工具链(toolchain)是指以 GCC 编译器为核心的一整套工具,用于把源代码转化成可执行应用程序。它主要包含以下三部分内容:

  • gcc-core:即 GCC 编译器,用于完成预处理和编译过程,例如把 C 代码转换成汇编代码。
  • Binutils :除 GCC 编译器外的一系列小工具包括了链接器 ld,汇编器 as、目标文件格式查看器 readelf 等。
  • glibc:包含了主要的 C 语言标准函数库,C 语言中常常使用的打印函数 printf、malloc 函数就在 glibc 库中。

在很多场合下会直接用 GCC 编译器来指代整套 GCC 编译工具链。

GCC 编译器

GCC(GNU Compiler Collection)是由 GNU 开发的编程语言编译器。GCC 最初代表“GNU C Compiler”,当时只支持 C 语言。后来又扩展能够支持更多编程语言,包括 C++、Fortran 和 Java 等。因此,GCC 也被重新定义为“GNU Compiler Collection”,成为历史上最优秀的编译器,其执行效率与一般的编译器相比平均效率要高 20%~30%。

GCC 的官网地址为:https://gcc.gnu.org/,在 Ubuntu 系统下系统默认已经安装好 GCC 编译器,可以通过如下命令查看 Ubuntu 系统中 GCC 编译器的版本及安装路径:

# 在主机上执行如下命令
gcc -v # 查看 gcc 编译器版本
which gcc # 查看 gcc 的安装路径

图中的两处信息说明如下:

  • “Target:x86_64-linux-gnu”表示该 GCC 的目标平台为 x86_64 架构(Intel、AMD 的 CPU),表示它编译生成的应用程序只适用于 x86 架构,不适用于 ARM 开发板平台。
  • “gcc version 7.4.0”表明该 GCC 的版本为 7.4.0,部分程序可能会对编译器版本有要求,不过我们演示使用的应用程序比较简单,兼容性好,一开始可以不用在乎这个,而编译指定版本的 uboot、Linux 内核的时候可能会对 GCC 有版本要求。

Binutils 工具集

Binutils(bin utility),是 GNU 二进制工具集,通常跟 GCC 编译器一起打包安装到系统,它的官方说明网站地址为:https://www.gnu.org/software/binutils/ 。

在进行程序开发的时候通常不会直接调用这些工具,而是在使用 GCC 编译指令的时候由 GCC 编译器间接调用。下面是其中一些常用的工具:

  • as:汇编器,把汇编语言代码转换为机器码(目标文件)。
  • ld:链接器,把编译生成的多个目标文件组织成最终的可执行程序文件。
  • readelf:可用于查看目标文件或可执行程序文件的信息。
  • nm :可用于查看目标文件中出现的符号。
  • objcopy:可用于目标文件格式转换,如.bin 转换成.elf 、.elf 转换成.bin 等。
  • objdump:可用于查看目标文件的信息,最主要的作用是反汇编。
  • size:可用于查看目标文件不同部分的尺寸和总尺寸,例如代码段大小、数据段大小、使用的静态内存、总大小等。

系统默认的 Binutils 工具集位于/usr/bin 目录下,可使用如下命令查看系统中存在的 Binutils 工具集:

# 在Ubuntu上执行如下命令
ls /usr/bin/ | grep linux-gnu

图中列出的是 Binutils 工具的完整名字,在终端中使用时通常直接使用它们的别名即可,在后面的讲解我们会使用到 readelf 工具。

glibc 库

glibc 库是 GNU 组织为 GNU 系统以及 Linux 系统编写的 C 语言标准库,因为绝大部分 C 程序都依赖该函数库,该文件甚至会直接影响到系统的正常运行,例如常用的文件操作函数 read、write、open,打印函数 printf、动态内存申请函数 malloc 等。

在 Ubuntu 系统下,libc.so.6 是 glibc 的库文件,可直接执行该库文件查看版本,在主机上执行如下命令:

# 在 Ubantu 上执行如下命令
# 以下是 Ubuntu 64 位机的 glibc 库文件路径,可直接执行
/lib/x86_64-linux-gnu/libc.so.6


图中表示本系统中使用的 glibc 是 2.27 版本,是由 GCC 7.3.0 版本的编译器编译出来的。

学习 C 语言的时候,可能好奇printf、malloc之类的函数是如何实现的,但是在 Windows下的 C 库是不开源的,无法查看,而在 Linux 下,则可以直接研究 glibc 的源代码,甚至加入开发社区贡献自己的代码,glibc 的官网地址为:https://www.gnu.org/software/libc/ ,可在该网站中下载源代码来学习。

X86_64平台、Ubuntu系统下的HelloWorld

创建工作目录和文件

为方便进行后面的各种实验,首先建立一个工作目录 workdir/example,并在其下建立本章使用的hello_c 目录:

# 在 Ubantu 上执行如下命令
mkdir -p ~/workdir/example/hello_c # 创建 hello_c 目录

使用编辑器新建一个名为 hello.c 的文件

编译并执行

Ubuntu 默认安装 GCC 编译工具链,写好程序后可以直接进行编译,请尝试执行以下命令:

# 在 Ubantu 的 hello_c 目录下执行如下命令
gcc hello.c –o hello # 使用 gcc 把 hello.c 编译成 hello 程序ls # 查看目录下的文件
./hello # 执行生成的 hello 程序# 若提示权限不够或不是可执行文件,执行如下命令再运行 hello 程序
chmod u+x hello # 给 hello 文件添加可执行权限

如下图:

ARM 平台、Debian 系统下的 HelloWorld

安装 GCC 编译工具链

野火提供的 Debian 系统镜像默认不带 gcc 编译器,开发板启动以后,执行以下命令安装 GCC 编译工具链

sudo apt install gcc -y

如下图:

GCC 工具链包括了 binutils、readelf 工具, 因此 GCC 安装完成后,binutils、readelf 等工具也可以直接使用。运行以下指令查看 GCC 版本与安装路径。

gcc -v # 查看 gcc 编译器版本
which gcc # 查看 gcc 的安装路径

如下图:

与前面介绍的 Ubuntu 中 GCC 不一样的是,开发板中 gcc 编译工具链的目标平台是 arm 架构的,表示它生成的应用程序只能运行于 ARM 平台的开发板,而不适合用于 X86 平台。

创建工作目录和文件

先在当前用户下创建一个本章节使用的工作目录 workdir/example/hello_c。使用编辑器新建一个名为 hello.c 的文件

编译并执行

# 在 Debian 的 hello_c 目录下执行如下命令
gcc hello.c –o hello # 使用 gcc 把 hello.c 编译成 hello 程序ls # 查看目录下的文件
./hello # 执行生成的 hello 程序# 若提示权限不够或不是可执行文件,执行如下命令再运行 hello 程序
chmod u+x hello # 给 hello 文件添加可执行权限

如下图:

运行结果与前面 X86_64、Ubantu 系统的 HelloWorld 执行结果一致。这就是在 Linux 下使用 GCC 开发简单 C 应用程序并运行的基本流程

GCC编译过程

基本语法

GCC 使用的命令语法如下:gcc [选项] 输入的文件名
常用选项:

  • -o:小写字母“o”,指定生成的可执行文件的名字,不指定的话生成的可执行文件名为 a.out。
  • -E:只进行预处理,既不编译,也不汇编。
  • -S:只编译,不汇编。
  • -c:编译并汇编,但不进行链接。
  • -g:生成的可执行文件带调试信息,方便使用 gdb 进行调试。
  • -Ox:大写字母“O”加数字,设置程序的优化等级,如“-O0”“-O1”“-O2”“-O3”,数字越大代码的优化等级越高,编译出来的程序一般会越小,但有可能会导致程序不正常运行。

编译过程

若不了解程序的编译过程,那么 GCC 的编译选项会让人一头雾水。下面以 X86_64 平台下 Ubuntu的编译过程为例进行初步讲解,ARM 平台下 Debian 的编译过程也是类似的,不再进行分析。

GCC 编译选项除了-g 和-Ox 选项,其它选项实际上都是编译的分步骤,即只进行某些编译过程。

# 直接编译成可执行文件
gcc hello.c -o hello# 以上命令等价于执行以下全部操作
# 预处理,可理解为把头文件的代码汇总成 C 代码,把 *.c 转换得到 *.i 文件
gcc –E hello.c –o hello.i# 编译,可理解为把 C 代码转换为汇编代码,把 *.i 转换得到 *.s 文件
gcc –S hello.i –o hello.s# 汇编,可理解为把汇编代码转换为机器码,把 *.s 转换得到 *.o,即目标文件
gcc –c hello.s –o hello.o# 链接,把不同文件之间的调用关系链接起来,把一个或多个 *.o 转换成最终的可执行文件
gcc hello.o –o hello

GCC 编译工具链在编译一个 C 源文件时需要经过以下 4 步:

  1. 预处理,在预处理过程中,对源代码文件中的文件包含 (include)、预编译语句 (如宏定义define 等) 进行展开,生成.i 文件。可理解为把头文件的代码、宏之类的内容转换成更纯粹的 C 代码,不过生成的文件以.i 为后缀。
  2. 编译,把预处理后的.i 文件通过编译成为汇编语言,生成.s 文件,即把代码从 C 语言转换成汇编语言,这是 GCC 编译器完成的工作。
  3. 汇编,将汇编语言文件经过汇编,生成目标文件.o 文件,每一个源文件都对应一个目标文件。即把汇编语言的代码转换成机器码,这是 as 汇编器完成的工作。
  4. 链接,最后将每个源文件对应的.o 文件链接起来,就生成一个可执行程序文件,这是链接器 ld 完成的工作。

以上一节的 hello.c 为例,后面括号代表的是 gcc 的参数,分步骤编译过程如下图所示。

预处理阶段

使用 GCC 的参数“-E”,可以让编译器生成.i 文件,参数“-o”,可以指定输出文件的名字。具体执行命令如下:

# 预处理,可理解为把头文件的代码汇总成 C 代码,把 *.c 转换得到 *.i 文件
gcc –E hello.c –o hello.i

直接用编辑器打开生成的 hello.i,可以看到如下的内容

# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "hello.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 1 3 4
# 33 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 424 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4
# 427 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 428 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/long-double.h" 1 3 4
# 429 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 425 "/usr/include/features.h" 2 3 4
# 448 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 1 3 4
# 10 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 2 3 4
# 449 "/usr/include/features.h" 2 3 4
# 34 "/usr/include/x86_64-linux-gnu/bits/libc-header-start.h" 2 3 4
# 28 "/usr/include/stdio.h" 2 3 4
# 1 "/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h" 1 3 4
# 216 "/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h" 3 4
# 216 "/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h" 3 4
typedef long unsigned int size_t;
# 34 "/usr/include/stdio.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/types.h" 1 3 4
# 27 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 28 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4
typedef unsigned char __u_char;
typedef unsigned short int __u_short;... 中间省略部分内容...int main(void)
{printf("hello, world! This is a C program.\n");
for(int i = 0; i < 10; i++)
{printf("output i = %d\n", i );
}
return 0;
}

文件中以“#”开头的是注释,可看到有非常多的类型定义、函数声明被加入到文件中,这些就是预处理阶段完成的工作,相当于它把原 C 代码中包含的头文件中引用的内容汇总到一处。如果原 C 代码有宏定义,还可以更直观地看到它把宏定义展开成具体的内容(如宏定义代表的数字)。

编译阶段

GCC 可以使用-S 选项,让编译程序生成汇编语言的代码文件(.s 后缀)。在这个过程,GCC 会检查各个源文件的语法,即使我们调用了一个没有定义的函数,也不会报错。具体命令如下,生成的 hello.s 文件可直接使用编辑器打开。

# 编译,可理解为把 C 代码转换为汇编代码,把 *.i 转换得到 *.s 文件
gcc –S hello.i –o hello.s# 也可以直接以 C 文件作为输入进行编译,与上面的命令是等价的
gcc –S hello.c –o hello.s

编译生成的 hello.s 文件内容如下:

.file "hello.c"
.text
.section .rodata
.align 8
.LC0:
.string "hello, world! This is a C program."
.LC1:
.string "output i = %d\n"
.text
.globl main
.type main, @function
main:
中间省略部分内容
.L2:
cmpl $9, -4(%rbp)
jle .L3
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0"
.section .note.GNU-stack,"",@progbits

汇编语言是跟平台相关的,由于本示例的 GCC 目标平台是 x86,所以此处生成的汇编文件是 x86的汇编代码。

汇编阶段

GCC 的参数“c”表示只编译 (compile) 源文件但不链接,会将源程序编译成目标文件(.o 后缀)。计算机只认识 0 或者 1,不懂得 C 语言,也不懂得汇编语言,经过编译汇编之后,生成的目标文件包含着机器代码,这部分代码就可以直接被计算机执行。一般情况下,可以直接使用参数“c”,跳过上述的两个过程,具体命令如下:

# 汇编,可理解为把汇编代码转换为机器码,把 *.s 转换得到 *.o,即目标文件
gcc –c hello.s –o hello.o# 也可以直接以 C 文件作为输入进行汇编,与上面的命令是等价的
gcc –c hello.c –o hello.o

.o 是为了让计算机阅读的,所以不像前面生成的.i 和 *.s 文件直接使用字符串来记录,如果直接使用编辑器打开,只会看到乱码,如下图。

Linux 下生成的 *.o 目标文件、 *.so 动态库文件以及下一小节链接阶段生成最终的可执行文件都是 elf 格式的,可以使用“readelf”工具来查看它们的内容。
请亲自尝试执行如下命令:

# 在 hello.o 所在的目录执行如下命令
readelf -a hello.o


从 readelf 的工具输出的信息,可以了解到目标文件包含 ELF 头、程序头、节等内容,对于*.o 目标文件或 *.so 库文件,编译器在链接阶段利用这些信息把多个文件组织起来,对于可执行文件,系统在运行时根据这些信息加载程序运行。

链接阶段

链接过程,是将汇编过程生成的所有目标文件进行链接,生成可执行文件。

例如一个工程里包含了 A 和 B 两个代码文件,编译后生成了各自的 A.o 和 B.o 目标文件,如果在代码 A 中调用了 B 中的某个函数 fun,那么在 A 的代码中只要包含了 fun 的函数声明,编译就会通过,而不管 B 中是否真的定义了 fun 函数(当然,如果函数声明都没有,编译也会报错)。也就是说 A.o 和 B.o 目标文件在编译阶段是独立的,而在链接阶段,链接过程需要把 A 和 B 之间的函数调用关系理顺,也就是说要告诉 A 在哪里能够调用到 fun 函数,建立映射关系,所以称之为链接。若链接过程中找不到 fun 函数的具体定义,则会链接报错。

虽然本示例只有一个 hello.c 文件,但它调用了 C 标准代码库的 printf 函数,所以链接器会把它和printf 函数链接起来,生成最终的可执行文件。

链接分为两种:

  • 动态链接,GCC 编译时的默认选项。动态是指在应用程序运行时才去加载外部的代码库,例如 printf 函数的 C 标准代码库 *.so 文件存储在 Linux 系统的某个位置,hello 程序执行时调用库文件 *.so 中的内容,不同的程序可以共用代码库。所以动态链接生成的程序比较小,占用较少的内存。
  • 静态链接,链接时使用选项“–static”,它在编译阶段就会把所有用到的库打包到自己的可执行程序中。所以静态链接的优点是具有较好的兼容性,不依赖外部环境,但是生成的程序比较大。

请尝试执行如下命令体验静态链接与动态链接的区别:

# 在 hello.o 所在的目录执行如下命令
# 动态链接,生成名为 hello 的可执行文件gcc hello.o –o hello# 也可以直接使用 C 文件一步生成,与上面的命令等价
gcc hello.c -o hello# 静态链接,使用--static 参数,生成名为 hello_static 的可执行文件
gcc hello.o –o hello_static --static# 也可以直接使用 C 文件一步生成,与上面的命令等价
gcc hello.c -o hello_static --static


从图中可以看到,使用动态链接生成的 hello 程序才 8.2KB,而使用静态链接生成的 hello_static 程序则高达 825KB。

在 Ubuntu 下,可以使用 ldd 工具查看动态文件的库依赖,尝试执行如下命令:

# 在 hello 所在的目录执行如下命令
ldd hello
ldd hello_static


可以看到,动态链接生成的 hello 程序依赖于库文件 linux-vdso.so.1、libc.so.6 以及 ld-linux-x86-64.so.2,其中的 libc.so.6 就是我们常说的 C 标准代码库,我们的程序中调用了它的 printf 库函数。

静态链接生成的 hello_static 没有依赖外部库文件。

GCC和HelloWorld相关推荐

  1. C指针原理(43)-helloworld的C程序汇编剖析

    一.汇编基础 1.指令码与数据处理 当计算机处理应用程序运行指令码时,数据指针指示处理器如何在内存的数据区域寻找要处理的数据,这块区域也称为堆栈,指令码放在另外的指令区,此外,还有指令指针机制,当处理 ...

  2. C/C++之Gcc常用参数

    gcc是GNU旗舰产品,目前基本上就是和Unix捆绑在一起分发的.这个东西功能强大,有多达上千个选项,其用户手册也有将近一万行.虽然其中的多数选项平时很少用到,但是不管装软件还是写程序,往往都会用到g ...

  3. gcc编译C++程序

     单个源文件生成可执行程序 下面是一个保存在文件 helloworld.cpp 中一个简单的 C++ 程序的代码:  /* helloworld.cpp */ #include <iostr ...

  4. Linux vim编写编译运行一个.c文件(centeos 8 HelloWorld.c)

    1.先在一个编译器里,编辑HelloWorld.c的代码,这里以dev为例. #include<stdio.h> int main() {pintf("Hello World!\ ...

  5. [GCC for C]编译选项---IDE掩盖下的天空

    编译选项 ---------IDE掩盖下的天空 /***************************************  * gcc for c language  ************ ...

  6. 转:gcc编译C++程序

    转:http://blog.csdn.net/liujiayu2/article/details/49864381 单个源文件生成可执行程序 下面是一个保存在文件 helloworld.cpp 中一个 ...

  7. GCC一些有用的技巧

    为什么80%的码农都做不了架构师?>>>    Linux上的gcc编译器有需要命令选项,我们不可能全部记住它们,今天我们介绍几个不常用但是非常有用的选项.以下的示例实在GCC 4. ...

  8. gcc学习与实践(2)

    主要学习如下gcc选项: -C.-M.-MM.-MD.-MMD.编译优化选项(-O0.-O1.-O2.-O3).-Wall 8.-C 选项 在预处理的时候,不删除注释信息,一般和-E一起使用.有时候分 ...

  9. makefile编写helloworld

    相信在unix下编程的没有不知道makefile的,刚开始学习unix平台 下的东西,了解了下makefile的制作,觉得有点东西可以记录下. 下面是一个极其简单的例子: 现在我要编译一个Hello ...

最新文章

  1. C# 调Win32 API SendMessage简单用法及wMsg常量
  2. 新版 C# 高效率编程指南
  3. 动态规划:从新手到专家
  4. 前端为什么有的接口明明是成功回调却执行了.catch失败回调_前端进阶高薪必看-手写源码篇(高频技术点)...
  5. ECMAScript 6学习总结(2)——ECMAScript 6常用方法总结
  6. linux interfaces配置文件详解
  7. 大规模分布式系统概念介绍
  8. 从零开始学centos(一)
  9. hadoop配置启动historyserver
  10. 抢滩新零售混战 实力战将才不惧双十一 附:双十一红包雨时间表
  11. speedoffice表格如何对内容进行分组?
  12. A Surface Defect Detection Method Based on Positive Samples
  13. Bugku-web-秋名山老司机
  14. 使用命令符关闭笔记本自带键盘命令
  15. 可以真正带你理清同步阻塞与同步非阻塞与异步阻塞与异步非阻塞的文章
  16. 【折腾服务器 1】妖板 Intel N5105 + i226 主板安装 ESXi 7.0 教程
  17. 与文本内容无关的说话人识别概述——从特征到超矢量
  18. [附源码]计算机毕业设计4S店汽车售后服务管理系统Springboot程序
  19. GMap.net 自定义Marker
  20. 计算机控制的节能路灯系统,LED路灯节能控制系统的设计与应用

热门文章

  1. 英语交流加我微信号Williamwhc
  2. Android BLE低功耗蓝牙开发(下) BLE客户端(中央设备)与GATT服务的通讯
  3. 全网唯一文字版:2022中国互联网公司100强排名
  4. 华为 IPv6过渡技术
  5. 经典语录: 新世相《我们终将改变潮水的方向》
  6. 设计分享|单片机交通灯仿真(汇编)
  7. 011 数学期望、方差及方差性质
  8. Go 工程师必学:Go 大杀器之跟踪剖析 trace
  9. H5热门游戏介绍!H5的游戏叠罗汉玩法详解
  10. File Nesting