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等参数优化. 例如

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

头文件, 静态库(.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 -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>指定的路径.

参考

  • GCC介绍, 以及分步的编译过程 https://www3.ntu.edu.sg/home/ehchua/programming/cpp/gcc_make.html
  • GCC减小尺寸的优化 https://stackoverflow.com/questions/6687630/how-to-remove-unused-c-c-symbols-with-gcc-and-ld
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值