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命令其实依次执行了四步操作
- 预处理(Preprocessing),
- 编译(Compilation),
- 汇编(Assemble),
- 链接(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 -v
或gcc -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
andCPLUS_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
GCC项目的文件组织和编译步骤分解的更多相关文章
- Linux-编译器gcc/g++编译步骤
gcc和g++现在是gnu中最主要和最流行的c&c++编译器.g++是c++的命令,以.cpp为主:对于c语言后缀名一般为.c,这时候命令换做gcc即可.编译器是根据gcc还是g++来确定是按 ...
- 腾讯开源项目phxpaxos的编译步骤
#paxos的一般编译流程在项目文档<中文详细编译手册>里面已经有介绍,这里重点介绍一下编译samples目录下的代码: #我的环境是ubuntu; #设置paxos根目录 phx_dir ...
- 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 链接 直接使用 ...
- Java native代码编译步骤简书
Java native代码编译步骤简书 目的:防止java代码反编译获取密码算法 (1)编写实现类com.godlet.PasswordAuth.java (2)编译java代码javac Passw ...
- 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. ...
- 开源项目导入eclipse的一般步骤
开源项目导入eclipse的一般步骤 周银辉 下载到开源项目后,我们还是希望导入到eclipse中还看,这样要方便点,一般的步骤是这样的 打开源代码目录, 如果看到里面有.calsspath .pro ...
- Android 源码编译 步骤
添加资源后编译步骤 1:lunch 112:mmm frameworks/base/core/res/生成Install: out/target/product/hammerhead/system/f ...
- zlib库VS2015编译步骤
[点击这里下载zlib1.2.8源码](http://zlib.net/zlib128.zip) [点击这里下载zlib1.2.8编译动态库](http://zlib.net/zlib128-dll. ...
- IDEA上的项目托管到码云步骤
IDEA上的项目托管到码云步骤:1.安装Git2.idea上配置Git Setting-Version Control-Git 把git.exe改为安装的Git的执行路径如:D:\Prog ...
- 用.NET CORE做项目,VS里编译碰到‘。。。。包降级。。。。’错误
用.NET CORE做项目,VS里编译碰到‘....包降级....’错误 本地开发机:WIN10+VS2017 15.7.3 ,用CORE2.1版本的建立一个项目,做好了,传到gitee上 今天有新同 ...
随机推荐
- JVM内存参数的学习之三
JVM内存参数的学习之三 背景 研究启动性能时, 顺便看到了jmap -heap 1 的部分信息 看到: MinHeapFreeRatio.MaxHeapFreeRatio 自己突然以为是 Perce ...
- Oceanbase部分参数学习与验证
Oceanbase部分参数学习与验证 字符集等参数查看 yum install obclient -y 然后使用客户端连接: obclient -h172.24.110.175 -P2881 -uro ...
- [转帖]linux系统上free命令看到的buff/cache到底是什么
https://zhuanlan.zhihu.com/p/645904515 上周二一大早,小智准备早点去公司肝一篇技术文分享给大家的,哪成想,一到公司就被测试部的"卷王"拉去看问 ...
- [转帖]jmeter SSL证书相关配置
在实际工作中,我们大多数接口都是用的HTTPS来保证安全,使用jmeter测试HTTPS请求是如何配置证书呢? 1.最简单的方法,在选项里选择SSL管理器,然后选择相应的证书即可 在弹出的选择框选择证 ...
- [转帖]《Linux性能优化实战》笔记(22)—— 网络丢包问题分析
所谓丢包,是指在网络数据的收发过程中,由于种种原因,数据包还没传输到应用程序中,就被丢弃了.这些被丢弃包的数量,除以总的传输包数,也就是我们常说的丢包率.丢包率是网络性能中最核心的指标之一.丢包通常会 ...
- [转帖] q命令-用SQL分析文本文件
https://www.cnblogs.com/codelogs/p/16060830.html 简介# 在Linux上分析文本文件时,一般会使用到grep.sed.awk.sort.uniq等命令, ...
- [转帖]A17再次证明苹果才是王者,组装芯片的安卓手机给它提鞋都不配
http://news.sohu.com/a/653472711_121124371 在挤了两代牙膏之后,苹果终于拿出了性能大幅提升的A17处理器,外媒传出A17处理器的性能提升幅度至少超过四成,相比 ...
- buildkit ctr 与 k3s的简单学习
摘要 前面一部分学习了 buildkit的简单搭建 也学习会了如果build images的简单处理 但是搭建镜像只是万里长征第一步. 如何进行微服务部署,才是关键的第二步. 公司最近使用基于K3S的 ...
- DashBoard in k8s 简单使用
DashBoard in k8s 简单使用 第一部分 拉取分发镜像 没办法的事情,公司网络实在是太垃圾了, dockerhub 又不让多次docker pull 找一台网络表好的机器 执行如下命令: ...
- [译]深入了解现代web浏览器(三)
本文是根据Mariko Kosaka在谷歌开发者网站上的系列文章https://developer.chrome.com/blog/inside-browser-part3/ 翻译而来,共有四篇,该篇 ...