GCC项目的文件组织和编译步骤分解

C项目的文件组织和编译

C项目的代码, 由头文件(.h后缀)和C文件(.c后缀)组成

  • C语言的函数和变量, 分声明定义两个阶段
  • 头文件和C文件是等价的, 相当于C文件的一部分, 其功能由人为划分, 用于变量和函数的声明, 头文件也可以用于变量和函数的定义(但是不推荐)
  • 同一个编译中, 函数在一处定义, 处处可用(除非使用static关键字)
    • 在A.c中定义后, 在B.c中用extern声明这个函数, 就可以调用
    • 将A.c中的函数声明提取到A.h, 在B.c中include A.h, 或者通过B.c include B.h, B.h include A.h, 都可以实现函数引用
  • C的编译, 是按文件编译的, 每个C文件会编译为一个目标文件
  • 头文件不单独编译, 与include这个头文件的C文件, 在预编译阶段展开, 之后在C文件中编译
  • 编译需要知道C文件的列表和头文件的目录列表
  • 编译会依次编译C文件列表中的每个文件, 不管最终是否用到

C项目结构示例

定义一个头文件 inc.h,声明两个函数func1和func2, 将定义写在func1.c和func2.c. 在main.c中通过main.h引用inc.h, 调用这些函数, 程序目录结构如下

├── inc
│   ├── func1.c
│   ├── func2.c
│   └── inc.h
├── main.c
├── main.h
└── obj

main.c

#include <stdio.h>
#include "main.h"

int main()
{
  uint8_t a = 0x08;
  uint8_t b = func1(a);
  printf("%X", b);
  return 0;
}

main.h

#ifndef MAIN_H
#define MAIN_H

#include "inc.h"

#endif

inc.h

#ifndef INC_H
#define INC_H

typedef unsigned char uint8_t;

uint8_t func1(uint8_t a);
uint8_t func2(uint8_t a);

#endif

func1.c

#include "inc.h"

uint8_t func1(uint8_t a)
{
  a = a << 1;
  return a;
}

func2.c

#include "inc.h"

uint8_t func2(uint8_t a)
{
  a = a >> 1;
  return a;
}

gcc的编译过程

gcc命令其实依次执行了四步操作

  1. 预处理(Preprocessing),
  2. 编译(Compilation),
  3. 汇编(Assemble),
  4. 链接(Linking)

1.预处理(Preprocessing)

预处理用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。gcc的预处理是预处理器cpp来完成的,你可以通过如下命令对 main.c进行预处理:

gcc -E -I./inc main.c -o obj/main.i
# or
$ cpp main.c -I./inc -o obj/main.i

-E是让编译器在预处理之后就退出,不进行后续编译过程; -I指定头文件目录, -o指定输出文件名.

经过预处理之后代码体积会大很多, main.c只有10行, 但是main.i有749行, 预处理之后的文件可以用文本编辑器查看

2.编译(Compilation)

这一步的编译将经过预处理之后的程序转换成特定汇编代码的过程, 编译的命令如下:

$ gcc -S -I./inc main.c -o obj/main.s

-S让编译器在编译之后停止. 这一步会生成程序的汇编代码, 内容如下:

	.file	"main.c"
	.text
	.section	.rodata
.LC0:
	.string	"%X"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movb	$8, -2(%rbp)
	movzbl	-2(%rbp), %eax
	movl	%eax, %edi
	call	func1@PLT
	movb	%al, -1(%rbp)
	movzbl	-1(%rbp), %eax
	movl	%eax, %esi
	leaq	.LC0(%rip), %rdi
	movl	$0, %eax
	call	printf@PLT
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	 1f - 0f
	.long	 4f - 1f
	.long	 5
0:
	.string	 "GNU"
1:
	.align 8
	.long	 0xc0000002
	.long	 3f - 2f
2:
	.long	 0x3
3:
	.align 8
4:

3.汇编(Assemble)

汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生了二进制的目标文件, gcc汇编过程通过as命令完成

as obj/main.s -o obj/main.o
# por
gcc -c obj/main.s -o obj/main.o

这一步需要给每一个源文件产生一个目标文件, 以便后面link

gcc -c -I./inc inc/func1.c -o obj/func1.o
gcc -c -I./inc inc/func2.c -o obj/func2.o

4.链接(Linking)

通过上面的步骤, 在obj目录下已经有main.o, func1.o和func2.o这三个目标文件, 现在需要通过linker将这些目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)

命令如下

gcc -o obj/main obj/main.o obj/func1.o obj/func2.o

这时候在obj目录下就会生成可执行文件main

链接并不会忽略未使用的目标文件
上面的编译产生的main文件大小为16824字节, 不管在main中是否调用了func1或者func2.
如果在link中去掉func2.o (因为main中未调用func2, 所以不会产生错误), 这样产生的main文件为16760字节

gcc -o obj/main obj/main.o obj/func1.o

如果需要减小尺寸, 可以使用 -fdata-sections -ffunction-sections -Wl --gc-sections -Os等参数优化.

  • -fdata-sections -ffunction-sections 这两个是编译阶段的参数, 在编译时, 将每个函数和数据在结果对象文件中分别放置
  • --gc-sections 是连接阶段的参数, 在连接时对未被引用的数据和代码进行回收

例如

gcc -Os -fdata-sections -ffunction-sections test.cpp -o test -Wl,--gc-sections

关于-fdata-sections -ffunction-sections gc-sections这几个参数的说明: https://gcc.gnu.org/onlinedocs/gnat_ugn/Compilation-options.html

The operation of eliminating the unused code and data from the final executable is directly performed by the linker.
In order to do this, it has to work with objects compiled with the following options: -ffunction-sections -fdata-sections.
These options are usable with C and Ada files. They will place respectively each function or data in a separate section in the resulting object file.
Once the objects and static libraries are created with these options, the linker can perform the dead code elimination. You can do this by setting the -Wl,--gc-sections option to gcc command or in the -largs section of gnatmake. This will perform a garbage collection of code and data never referenced.
If the linker performs a partial link (-r linker option), then you will need to provide the entry point using the -e / --entry linker option.
Note that objects compiled without the -ffunction-sections and -fdata-sections options can still be linked with the executable. However, no dead code elimination will be performed on those objects (they will be linked as is).
The GNAT static library is now compiled with -ffunction-sections and -fdata-sections on some platforms. This allows you to eliminate the unused code and data of the GNAT library from your executable.

5.优化参数

GNU C有多种优化级别

  • -O0: 不优化, 这种方式编译时间最短, 产生的二进制最大, 会保留完整的debug信息
  • -O, -O1: 这两者是一样的, 能完成大部分的优化
  • -Og: 保留debug体验的优化, 常用于标准的 编辑-编译-debug 循环. 相对于-O0提供了合理的优化, 较快的编译速度, 但是保留了debug功能. 和-O0一样单独的优化指令不起作用, 其它情况和 -O1 级别的优化项基本一样, 除掉个别影响debug的优化项.
  • -O2: 在 -O1 基础上的进一步优化, 增加了不涉及空间换速度的优化项. 相对于 -O1, 编译时间更长, 执行性能更好.
  • -O3: 在 -O2 基础上进一步优化
  • -Os: 针对二进制尺寸的优化, 在 -O2 的优化基础上去掉那些会增加二进制大小的优化项
  • -Oz: 进一步针对二进制尺寸的优化, 会优先选择最后编码尺寸小的指令哪怕这些指令需要更多的执行次数, 与 -Os 相似但是会开启大部分 -O2 的优化项

头文件, 静态库(.lib, .a) 和动态库(.dll, .so)

静态库 vs 动态库

库文件就是已经预编译好的目标文件, 只需要link到你的程序里就可以用了, 例如常见的方法 printf() and sqrt(). 库文件有两种类型: 静态库和动态库(也叫共享库).

静态库 在Linux下使用扩展名.a, 在Windows下使用扩展名.lib, 当link静态库时, 这些对象文件的机器码会被复制到你的可执行文件中.
动态库 在Linux下使用扩展每.so, 在Windows下使用扩展名.dll, 当你的程序link静态库时, 只会在你的程序可执行文件中添加一个表, 在运行你的程序之前, 操作系统会将这些外部方法的机器码载入进来. 这种方式可以节约磁盘资源, 让程序更小, 另外大多数操作系统也运行内存中的一份动态库在多个运行的程序中共享. 动态库升级时无需重新编译执行程序.

GCC默认情况下以动态库方式link. 要查看库内容, 可以用命令nm filename

编译中定位包含头文件和库文件 (-I, -L and -l)

当编译项目时, 编译器需要头文件的信息, linker需要库文件解决外部依赖.
对于项目中include的头文件, 编译器会去搜索相应的路径, 这些路径通过 -Idir 参数 ( 或者环境变量 CPATH) 指定, 因为头文件的文件名是已知的, 所以编译器只需要知道路径.
对于linker, 会去搜索库路径, 这个通过 -Ldir 参数 (大写 'L' 后面是路径) (或者环境变量 LIBRARY_PATH). 另外你需要指定库名称. 在Unix系统中, 库文件 libxxx.a 通过参数 -lxxx 指定 (小写字符 'l' 不带lib前缀, 不带.a扩展名). 在Windows下, 需要提供文件全名, 例如 -lxxx.lib. 路径和文件名都需要指定.

默认的 Include-paths, Library-paths 和 Libraries

可以通过cpp -vgcc -v命令列出:

> cpp -v
......
#include "..." search starts here:
#include <...> search starts here:
 /usr/lib/gcc/x86_64-pc-cygwin/6.4.0/include
 /usr/include
 /usr/lib/gcc/x86_64-pc-cygwin/6.4.0/../../../../lib/../include/w32api

在编译时, 加入-v参数开启verbose mode, 可以了解系统中使用到的库路径(-L)以及库明细(-l)

> gcc -v -o hello.exe hello.c
......
-L/usr/lib/gcc/x86_64-pc-cygwin/6.4.0
-L/usr/x86_64-pc-cygwin/lib
-L/usr/lib
-L/lib
-lgcc_s     // libgcc_s.a
-lgcc       // libgcc.a
-lcygwin    // libcygwin.a
-ladvapi32  // libadvapi32.a
-lshell32   // libshell32.a
-luser32    // libuser32.a
-lkernel32  // libkernel32.a

Eclipse CDT 在 Eclipse CDT 中, 可以在项目上右键, 点击project ⇒ Properties ⇒ C/C++ General ⇒ Paths and Symbols, 在标签页"Includes", "Library Paths" and "Libraries"下, 设置 include path, library paths 和 libraries.

GCC环境变量

GCC 使用下列环境变量:

  • PATH: 用于搜索可执行文件和运行时的动态链接库(.dll, .so).
  • CPATH: 用于搜索头文件包含路径. 优先级低于直接用-I<dir>指定的路径. C_INCLUDE_PATH and CPLUS_INCLUDE_PATH可分别用于指定C和C++的头文件路径.
  • LIBRARY_PATH: 用于搜索库文件的路径, 优先级低于用-L<dir>指定的路径.

参考

posted on 2021-08-24 18:38  Milton  阅读(559)  评论(0编辑  收藏  举报

导航