主要参考野火电子:http://doc.embedfire.com/linux/imx6/base/zh/latest/index.html
1 GCC
编译工具链
1.1 简介
GCC
编译工具链(toolchain
)是指以 GCC
编译器为核心的一整套工具,用于把源代码转化成可执行应用程序。它主要包含以下三部分内容:
-
gcc-core :即GCC编译器,用于完成预处理和编译过程,例如把C代码转换成汇编代码。
-
Binutils :除GCC编译器外的一系列小工具包括了链接器
ld
,汇编器as
、目标文件格式查看器readelf
等。 -
glibc :包含了主要的
C
语言标准函数库,C
语言中常常使用的打印函数printf
、malloc
函数就在glibc
库中。
在很多场合下会直接用 GCC
编译器 来指代整套 GCC
编译工具链。
1.2 GCC
编译器
GCC
(GNU Compiler Collection
)是由 GNU
开发的编程语言编译器。 GCC
最初代表 “GNU C Compiler”
,当时只支持C语言。 后来又扩展能够支持更多编程语言,包括 C++
、Fortran
和 Java
等。 因此,GCC
也被重新定义为 “GNU Compiler Collection”
,成为历史上最优秀的编译器, 其执行效率与一般的编译器相比平均效率要高 20%~30%。GCC的官网地址
在 Ubuntu
系统下系统默认已经安装好 GCC
编译器,可以通过命令查看 Ubuntu
系统中 GCC
编译器的版本及安装路径:
1.3 Binutils
工具集
Binutils
(bin utility
),是 GNU
二进制工具集,通常跟 GCC
编译器一起打包安装到系统。Binutils官方说明
在进行程序开发的时候通常不会直接调用这些工具,而是在使用 GCC
编译指令的时候由 GCC
编译器间接调用。下面是其中一些常用的工具:
as
:汇编器,把汇编语言代码转换为机器码(目标文件)。ld
:链接器,把编译生成的多个目标文件组织成最终的可执行程序文件。readelf
:可用于查看目标文件或可执行程序文件的信息。nm
: 可用于查看目标文件中出现的符号。objcopy
: 可用于目标文件格式转换,如.bin
转换成.elf
、.elf
转换成.bin
等。objdump
:可用于查看目标文件的信息,最主要的作用是反汇编。size
:可用于查看目标文件不同部分的尺寸和总尺寸,例如代码段大小、数据段大小、使用的静态内存、总大小等。
系统默认的 Binutils
工具集位于 /usr/bin
目录下。
1.4 glibc
库
glibc
库是 GNU
组织为 GNU
系统以及 Linux
系统编写的 C
语言标准库,因为绝大部分 C
程序都依赖该函数库,该文件甚至会直接影响到系统的正常运行,例如常用的文件操作函数 read
、write
、open
,打印函数 printf
、动态内存申请函数 malloc
等。
在 Ubuntu
系统下,libc.so.6
是 glibc
的库文件,可直接执行该库文件查看版本:
2 GCC
编译过程
一个 C/C++
文件要经过预处理、编译、汇编和链接等 4 步才能变成可执行文件。在日常中通常使用“编译”统称这 4 个步骤。
2.1 基本语法
2.1.1 GCC
使用的命令语法
$ gcc [option] 文件名
常用选项:
-o
:小写字母“o”,指定生成的可执行文件的名字,不指定的话生成的可执行文件名为a.out。-E
:只进行预处理,既不编译,也不汇编。-S
:只编译,不汇编。-c
:编译并汇编,但不进行链接。-g
:生成的可执行文件带调试信息,方便使用gdb进行调试。-Ox
:大写字母“O”加数字,设置程序的优化等级,如“-O0”“-O1” “-O2” “-O3”, 数字越大代码的优化等级越高,编译出来的程序一般会越小,但有可能会导致程序不正常运行。I
:指定头文件目录L
:指定链接时库文件目录l
:指定链接哪一个库文件
2.1.2 编译过程
GCC
编译工具链在编译一个 C
源文件时需要经过以下 4 步:
- 预处理:在预处理过程中,对源代码文件中的文件包含(
include
)、 预编译语句(如宏定义define
等)进行展开,生成.i
文件。 可理解为把头文件的代码、宏之类的内容转换成更纯粹的C代码,不过生成的文件以.i
为后缀。 - 编译:把预处理后的
.i
文件通过编译成为汇编语言,生成.s
文件,即把代码从C语言转换成汇编语言,这是GCC
编译器完成的工作。 - 汇编:将汇编语言文件经过汇编,生成目标文件
.o
文件,每一个源文件都对应一个目标文件。即把汇编语言的代码转换成机器码,这是as
汇编器完成的工作。 - 链接:最后将每个源文件对应的
.o
文件链接起来,就生成一个可执行程序文件,这是链接器ld
完成的工作。
源码:src/hello.c
,具体命令如下:
#直接编译成可执行文件
$ 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
2.2 编译阶段分析
2.2.1 预处理阶段
-
命令
# 预处理,可理解为把头文件的代码汇总成C代码,把*.c转换得到*.i文件 $ gcc -E hello.c -o hello.i
-
结果
$ vim 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 # 此处省略了很多 # 3 "hello.c" int main(int argc, char *argv[]) { printf("hello world!\n"); return 0; }
2.2.2 编译阶段
-
命令
# 编译,可理解为把C代码转换为汇编代码,把*.i转换得到*.s文件 $ gcc -S hello.i -o hello.s # 也可以直接以C文件作为输入进行编译,与上面的命令是等价的 $ gcc -S hello.c -o hello.s
-
结果
$ vim hello.s .file "hello.c" .text .section .rodata .LC0: .string "hello world!" .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 %edi, -4(%rbp) movq %rsi, -16(%rbp) leaq .LC0(%rip), %rdi call puts@PLT movl $0, %eax leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0" .section .note.GNU-stack,"",@progbits
2.2.3 汇编阶段
-
命令
# 汇编,可理解为把汇编代码转换为机器码,把*.s转换得到*.o,即目标文件,elf格式 $ gcc -c hello.s -o hello.o # 也可以直接以C文件作为输入进行汇编,与上面的命令是等价的 $ gcc -c hello.c -o hello.o
-
结果
$ readelf -a hello.o ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: REL (Relocatable file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x0 Start of program headers: 0 (bytes into file) Start of section headers: 728 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 0 (bytes) Number of program headers: 0 Size of section headers: 64 (bytes) Number of section headers: 13 Section header string table index: 12 # 省略了很多
2.2.4 链接阶段
-
命令
# 动态链接 # 生成名为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
-
结果
$ gcc hello.o -o hello $ gcc hello.o -o hello_static -static $ ls -lh total 872K -rwxr-xr-x 1 root root 8.2K Jun 23 01:33 hello # 比较小,执行的时候会去找到动态库执行,所以要保证库文件目录有对应的库 -rw-r--r-- 1 root root 95 Jun 23 01:21 hello.c -rw-r--r-- 1 root root 18K Jun 23 01:23 hello.i -rw-r--r-- 1 root root 1.6K Jun 23 01:26 hello.o -rw-r--r-- 1 root root 516 Jun 23 01:25 hello.s -rwxr-xr-x 1 root root 826K Jun 23 01:33 hello_static # 把相关的函数打包进了可执行文件里,执行时不用担心库的问题 # 使用ldd工具查看动态文件的库依赖 $ ldd hello linux-vdso.so.1 (0x00007ffd95d72000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f86eeeaa000) /lib64/ld-linux-x86-64.so.2 (0x00007f86ef49d000) $ ldd hello_static not a dynamic executable # 不是动态可执行文件
3 其他用法
3.1 怎么编译多个文件
3.1.1 源码
-
main.c
#include <stdio.h> #include "sub.h" int main(int argc, char *argv[]) { int i; printf("Main fun!\n"); sub_fun(); return 0; }
-
sub.c
#include "sub.h" void sub_fun(void) { printf("Sub fun!\n"); }
-
sub.h
#ifndef _SUB_H #define _SUB_H void sub_fun(void); #endif /* _SUB_H */
3.1.2 编译
-
一起编译,链接
$ gcc -o test main.c sub.c
-
分开编译,统一链接
$ gcc -c -o main.o main.c $ gcc -c -o sub.o sub.c $ gcc -o test main.o sub.o
3.2 制作动态库
3.2.1 制作
-
制作动态库
# 可以使用多个 *.o 生成动态库 $ gcc -c -o sub.o sub.c $ gcc -shared -o libsub.so sub.o
-
生成可执行文件
$ gcc -c -o main.o main.c $ gcc -o test main.o -L./ -lsub # 使用 -L 指定库文件目录
3.2.2 运行
-
把
libusb.so
放到PC
或板子上的/lib
目录,然后就可以运行test
程序。 -
如果不想把
libusb.so
放到/lib
,也可以放在某个目录比如/a
,然后如下执行$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/a $ ./test Main fun! Sub fun!
3.3 制作静态库
3.3.1 制作
-
制作静态库
# 可以使用多个 *.o 生成静态库 $ gcc -c -o sub.o sub.c $ ar crs libsub.a sub.o
-
生成可执行文件
$ gcc -c -o main.o main.c $ gcc -o test main.o libsub.a # 如果 libsub.a 不在当前目录下,需要指定它的绝对或相对路径
3.3.2 运行
-
不需要把静态库
libsub.a
放到板子上$ ./test Main fun! Sub fun!