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>指定的路径.

参考

GCC项目的文件组织和编译步骤分解的更多相关文章

  1. Linux-编译器gcc/g++编译步骤

    gcc和g++现在是gnu中最主要和最流行的c&c++编译器.g++是c++的命令,以.cpp为主:对于c语言后缀名一般为.c,这时候命令换做gcc即可.编译器是根据gcc还是g++来确定是按 ...

  2. 腾讯开源项目phxpaxos的编译步骤

    #paxos的一般编译流程在项目文档<中文详细编译手册>里面已经有介绍,这里重点介绍一下编译samples目录下的代码: #我的环境是ubuntu; #设置paxos根目录 phx_dir ...

  3. GCC编译步骤

    gcc -E t1.c -o t1.i 预处理gcc -S t1.i -o t1.s 转成汇编语言gcc -c t1.s -o t1.o 转成机器码gcc t1.o -o t1.exe 链接 直接使用 ...

  4. Java native代码编译步骤简书

    Java native代码编译步骤简书 目的:防止java代码反编译获取密码算法 (1)编写实现类com.godlet.PasswordAuth.java (2)编译java代码javac Passw ...

  5. gcc/g++ 链接库的编译与链接

    GCC编译步骤 gcc -E t1.c -o t1.i 预处理 gcc -S t1.i -o t1.s 转成汇编语言 gcc -c t1.s -o t1.o 转成机器码 gcc t1.o -o t1. ...

  6. 开源项目导入eclipse的一般步骤

    开源项目导入eclipse的一般步骤 周银辉 下载到开源项目后,我们还是希望导入到eclipse中还看,这样要方便点,一般的步骤是这样的 打开源代码目录, 如果看到里面有.calsspath .pro ...

  7. Android 源码编译 步骤

    添加资源后编译步骤 1:lunch 112:mmm frameworks/base/core/res/生成Install: out/target/product/hammerhead/system/f ...

  8. zlib库VS2015编译步骤

    [点击这里下载zlib1.2.8源码](http://zlib.net/zlib128.zip) [点击这里下载zlib1.2.8编译动态库](http://zlib.net/zlib128-dll. ...

  9. IDEA上的项目托管到码云步骤

    IDEA上的项目托管到码云步骤:1.安装Git2.idea上配置Git    Setting-Version Control-Git    把git.exe改为安装的Git的执行路径如:D:\Prog ...

  10. 用.NET CORE做项目,VS里编译碰到‘。。。。包降级。。。。’错误

    用.NET CORE做项目,VS里编译碰到‘....包降级....’错误 本地开发机:WIN10+VS2017 15.7.3 ,用CORE2.1版本的建立一个项目,做好了,传到gitee上 今天有新同 ...

随机推荐

  1. Laravel : 模糊查询 where orWhere

    Banner::where('title', 'like', "%{$keyword}%")->orWhere('introduce', 'like', "%{$k ...

  2. 云服务器搭建自己的GitServer!

    云服务器搭建自己的GitServer! 如果你有一台云服务器并想在上面搭建自己的 Git 服务器,你可以使用 Git 自带的 git-shell ,也可以使用像 Gitea.GitLab.Gogs 这 ...

  3. [转帖]shell编程:变量的数值计算实践(五)

    https://www.cnblogs.com/luoahong/articles/9224495.html 算术运算符 变量的数值(整数)计算   1)(())用法:(此方法很常用)** 范例:sh ...

  4. Python学习之十二_tkinter的学习与使用

    Python学习之十二_tkinter的学习与使用 摘要 本来想说会用QT5进行界面编程 但是发现比较繁琐 还是先学习使用 tkinter的方式进行界面化的编写和学习了 基础知识 tkinter是一个 ...

  5. [转帖]sqlserver on linux vs windows

    简单对比下sqlserver on windows与linux的特点,发现新的继续添加 对比项 sqlserver on windows sqlserver on Linux 备注 费用 需要wind ...

  6. [转帖]Windows磁盘性能压测(2)-Fio

    http://www.manongjc.com/detail/59-qftscgqzitmxpaw.html 目录 一.腾讯云官网硬盘性能指标 二.使用fio测试硬盘性能指标 1. 测试工具相关 2. ...

  7. [转帖]Spring Cloud Alibaba Nacos 注册中心使用教程

    一. 什么是Nacos Nacos是一个更易于构建云原生应用的动态服务发现(Nacos Discovery ).服务配置(Nacos Config)和服务管理平台,集注册中心+配置中心+服务管理于一身 ...

  8. C# WPF 开发一个 Emoji 表情查看软件

    微软在发布 Windows 11 系统的时候,发布过一个开源的 Emoji 表情 fluentui-emoji .因为我经常需要里面的一些表情图片,在仓库一个个查找特别的不方便,所以我做了一个表情查看 ...

  9. vs code python(Pylance server) crash

    The Pylance server crashed 5 times in the last 3 minutes. The server will not be restarted. See the ...

  10. MedicalGPT:基于LLaMA-13B的中英医疗问答模型(LoRA)

    MedicalGPT:基于LLaMA-13B的中英医疗问答模型(LoRA).实现包括二次预训练.有监督微调.奖励建模.强化学习训练[LLM:含Ziya-LLaMA]. **** 训练医疗大模型,实现包 ...