GCC笔记
小故事
通过在编译器里面做手脚,给程序加后门 TODO
编译器
作用:把代码 翻译成可执行程序
![](/assets/image-20210716002956629.CpydhtOb.webp)
如何编译?如何运行?这两个阶段干了什么?
GCC编译器
GCC文档:https://gcc.gnu.org/onlinedocs/gcc-8.5.0/gcc/#toc-GCC-Command-Options
Note:多版本并行开发.最新版本已经到了GCC11了, 老版本7.X也还在更新; 和普通微信及其他app开发有区别.
之前在开阳开发时版本4.9.x, (GCC 4.9.4 released [2016-08-03])
GCC全称GNU Compiler Collection, GCC是一个工具集合,在编译代码的时候也是团队协作,跟我们开发一样,不同的阶段交给不同的人去处理. 比如:预处理器,链接器(linker:ld)
编译四步骤:
- 预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法
- 编译:检查语法,将预处理后文件编译生成汇编文件
- 汇编:将汇编文件生成目标文件(二进制文件)
- 链接:将目标文件链接为可执行程序
# 1.预处理
$ gcc -E main.c -o main.i
$ gcc -E -C main.c -o main.i # 保留注释
# 2.编译
$ gcc -S main.c -o main.s
$ gcc -S main.c -o main1.s -fverbose-asm # verbose
# 3.汇编
$ gcc -c main.s -o main.o
# 4.链接
$ gcc main.c -o a.out
实际项目编译,从输出日志可以分为两步:
取自cmake编译缓存文件;如果你要定要一个gcc编译器的公式的话,就是下面这些了(还有生成库)
# 第一步:1.2.3.把项目里面单个.c文件编译成.o文件:gcc -o main.c.o -C main.c
<COMPILER> <DEFINES> <INCLUDES> <FLAGS> -o <OBJECT> -c <SOURCE>
# 第四步:4.链接,把上面编译出来的.o文件链接在一起,如果有库也会链接
<COMPILER> <FLAGS> <CMAKE_C_LINK_FLAGS> <LINK_FLAGS> <OBJECTS> -o <TARGET> <LINK_LIBRARIES>
对于我们来说,要告诉编译器哪些:
- 编译哪些源代码文件
- 头文件搜索路径(默认/相对路径/三方库)
- 库搜索路径(默认/相对路径/三方库)
- 编译器/链接器选项 (语言标准, 优化选项, 警告)
- 编译器/链接器特性
- 链接库
- ......
GCC怎么去解决这个问题:
- 命令参数
- 命令选项
- 特定变量
命令参数
gcc编译哪些源代码,库
命令选项:
选项大纲文档:https://gcc.gnu.org/onlinedocs/gcc-8.5.0/gcc/Option-Summary.html#Option-Summary
- -Wall: 开启大部分警告提示(建议使用),turns on all the most commonly-used compiler warnings
- -std: 指定编译语言标准
- -o: 指定输出文件名(o:output)
- -Idir: 指定gcc找头文件的目录(大写i:include首字母)
- -Ldir: 将dir目录加入搜索库的目录路径(大写l:library首字母)
- -llib: 连接lib库(小写l:library首字母)
- -O: 程序优化级别(O: optimization)
- -static: 禁止使用共享连接
- -shared: 生成共享目标文件, 通常用在建立共享库时使用
- -g: debug调试用,保存额外的信息到程序中
- -v: 显示编译详情(v: verbose)
-Wall: 开启大部分警告提示(建议使用)
gcc提供的对代码检查的选项,
-std:指定编译标准
# 使用示例
$ gcc -std=c11 main.c
-Idir: 指定gcc找头文件的目录
https://gcc.gnu.org/onlinedocs/cpp/Search-Path.html
# 查看gcc找头文件的目录
$ cpp -v /dev/null -o /dev/null
...
$ cpp -I/home/shibin/repo -v /dev/null -o /dev/null
# 使用示例
$ gcc -I/home/shibin/repo main.c
-Ldir: 指定gcc找库的目录
查找库详情:man ld
The linker uses the following search paths to locate required shared libraries:
- Any directories specified by -rpath-link options.
- Any directories specified by -rpath options. The difference between -rpath and -rpath-link is that directories specified by -rpath options are included in the xecutable and used at runtime, whereas the -rpath-link option is only effective at link time. Searching -rpath in this way is only supported by native linkers and cross linkers which have been configured with the --with-sysroot option.
- On an ELF system, for native linkers, if the -rpath and -rpath-link options were not used, search the contents of the environment variable "LD_RUN_PATH".
- On SunOS, if the -rpath option was not used, search any directories specified using -L options.
- For a native linker, search the contents of the environment variable "LD_LIBRARY_PATH".
- For a native ELF linker, the directories in "DT_RUNPATH" or "DT_RPATH" of a shared library are searched for shared libraries needed by it. The "DT_RPATH" entries re ignored if "DT_RUNPATH" entries exist.
- The default directories, normally /lib and /usr/lib.
- For a native linker on an ELF system, if the file /etc/ld.so.conf exists, the list of directories found in that file. If the required shared library is not found, the linker will issue a warning and continue with the link.
-llib: 连接lib库
# 使用示例
$ gcc main.c -lpthread -lm -lrt
-o: 程序优化级别(o: optimization)
https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options
An Introduction to GCC: 6 Compiling with optimization
- -O0 不优化 (default)
- -O1 优化.对于大函数,优化编译占用稍微多的时间和相当大的内存.
- -O2 多优化一些.除了涉及空间和速度交换的优化选项,执行几乎所有的优化工作.例如不进行循环展开(loop unrolling)和函数内嵌(inlining). 和-O选项比较,这个选项既增加了编译时间,也提高了生成代码的 运行效果.
- -O3 优化的更多.除了打开-O2所做的一切,它还打开了-finline-functions选项
推荐使用: -O2(经验)
-static: 禁止使用共享连接
一般不用; 当为了方便移植,可以使用
-shared: 生成共享目标文件, 通常用在建立共享库时使用
后面介绍
-g: debug调试用,保存额外的信息到程序中
debug调试时用,会把代码,代码行号...打包到程序中,所以Debug版本的程序会比Release版本的程序大不少
$ gcc -g main.c
-v: 显示编译详情(v: verbose)
编译出问题,不复合预期时候用. 会告诉你gcc是怎么把程序编译出来的,有哪些选项参数,用的什么标准,开启了哪些选项
# gcc
$ gcc -v main.c
# make
$ make VERBOSE=1
# cmake
set(CMAKE_VERBOSE_MAKEFILE ON)
特定变量:
LIBRARY_PATH:编译时需要的动态链接库
LD_LIBRARY_PATH:运行时能够自动找到需要的动态链接库
# 查看程序依赖哪些共享库
$ ldd /bin/ls
linux-vdso.so.1 (0x00007ffcc3563000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f87e5459000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f87e5254000)
libc.so.6 => /lib64/libc.so.6 (0x00007f87e4e92000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f87e4c22000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f87e4a1e000)
/lib64/ld-linux-x86-64.so.2 (0x00005574bf12e000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007f87e4817000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f87e45fa000)
$ export LD_LIBRARY_PATH=/home/shibin/repo/foo/target/lib ./a.out
库
复用的代码
编译时,主要发生在程序的链接阶段; 运行
静态库
目标文件的归档文档;可以理解成一组目标文件的集合; 用ar工具去打包,后缀名为.a,(archive file)
格式:
- Unix: libNAME.a
- Windows: libNAME.lib
- macOS: libNAME.a
特征:
- 文件格式是非可执行程序格式(elf格式)
- 链接时,目标可执行程序只取我需要的部分,合并到可执行文件中.(可移植性高);
- 运行时会被全部加载到内存(内存占用高)
# 打包成静态库
$ gcc -c add.c sub.c
$ ar crs libname.a file1.o file2.o file3.o
# 查看静态库的内容表(table of content)
$ ar t libname.a
# 使用静态库
$ gcc main.o -o a.out libname.a
$ gcc main.o -o a.out -L. -lname
动态库(共享库)
也叫共享库shared object,顾名思义,可以多个程序共享; 文件后缀名为.so,
格式:
- Unix: libNAME.so
- Windows: libNAME.dll
- macOS: libNAME.dylib
特征:
- 文件格式是可执行程序格式(elf格式)
- 链接时,目标可执行程序只记录相应的库信息(函数表).(可移植性低)(ldd executable_name)
- 运行时调用该库函数时才会被全部加载到内存;并且多个程序只会加载一份到内存中(多个程序同时去年时.内存占用低)
# 打包动态库
$ gcc -fpic -c file1.c file2.c
$ gcc -shared -o libname.so file1.o file2.o
# 查看动态库中有哪些函数
$ readelf -Ws /lib/x86_64-linux-gnu/libselinux.so.1
# 使用动态库
$ gcc main.o -o a.out -L. -lname
静态库 vs 动态库:
- 移植性:静态库可移植性好,移植的时候拷贝程序就可以了
- 更新,部署:动态库更新只需要更新单独的库;而静态库需要编译整个程序
- 内存占用:动态库被一个或多个进程共享(代码段);静态库所在的每个进程,都会加载一份到内存
运行时:
ldd:查看程序依赖的共享库
# Linux
$ ldd /bin/ls
linux-vdso.so.1 (0x00007ffcc3563000)
libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f87e5459000)
libcap.so.2 => /lib64/libcap.so.2 (0x00007f87e5254000)
libc.so.6 => /lib64/libc.so.6 (0x00007f87e4e92000)
libpcre.so.1 => /lib64/libpcre.so.1 (0x00007f87e4c22000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f87e4a1e000)
/lib64/ld-linux-x86-64.so.2 (0x00005574bf12e000)
libattr.so.1 => /lib64/libattr.so.1 (0x00007f87e4817000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f87e45fa000)
# macOS
$ otool -L /bin/ls
/bin/ls:
/usr/lib/libutil.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libncurses.5.4.dylib (compatibility version 5.4.0, current version 5.4.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.100.3)
思考问题:
- 多个程序使用同一个共享库时,共享哪些内容? (代码段,只读数据),
- 如果同时安装了一个库的动态库和静态库,你的程序会link哪个库? 如果有动态库,默认就链接动态库
其它工具:
UNIX 系统的本地工具, GNU Binutils
工具 | 描述 |
---|---|
ar | 这是一个程序,可通过从文档中增加、删除和析取文件来维护库文件。通常使用该工具是为了创建和管理连接程序使用的目标库文档。该程序是 binutils 包的一部分 |
as | GNU 汇编器。实际上它是一族汇编器,因为它可以被编泽或能够在各种不同平台上工作。 该程序是 binutils 包的一部分 |
ld | GNU 连接程序。该程序将目标文件的集合组合成可执行程序。该程序是 binutils 包的一部 |
gcov | gprof 使用的配置工具,用来确定程序运行的时候哪一部分耗时最大 |
gdb | GNU 调试器,可用于检查程序运行时的值和行为 |
gprof | 该程序会监督编泽程序的执行过程,并报告程序中各个函数的运行时间,可以根据所提供 的配置文件来优化程序。该程序是 binutils 包的一部分 |
make | 一个工具程序,它会读 makefile 脚木来确定程序中的哪个部分需要编泽和连接,然后发布 必要的命令。它读出的脚木(叫做 makefile 或 Makefile)定义了文件关系和依赖关系 |
objdump | 显示一个或多个目标文件中保存的多种不同信息。该程序是 binutils 包的一部分 |
readelf | 从 ELF 格式的目标文件显示信息。该程序是 binutils 包的一部分 |
size | 列出目标文件中每个部分的名字和尺寸。该程序是 binutils 包的一部分 |
strings | 浏览所有类型的文件,析取出用于显示的字符串。该程序是 binutils 包的一部分 |
strip | 从目标文件或文档库中去掉符号表,以及其他调试所需的信息。该程序是 binutils 包的一部 |
常见问题
兼容问题:
- 系统平台差异
- C标准库实现差异,不同的实现glibc,uClibc,FreeBSD libc
- 同一个库不同版差异
- 不同语言版本标准问题
- 不同编译器版本
预处理时错误信息:
- No such file or directory
编译时错误信息:
- ‘variable’ undeclared (first use in this function)
- warning: implicit declaration of function ‘...’
- warning: unused variable ‘...’
- warning: unused parameter ‘...’
链接时错误信息:
- file not recognized: File format not recognized
- undefined reference to ‘foo’ 找不到函数
运行时错误信息:
- error while loading shared libraries: cannot open shared object file: No such file or directory
段错误Segmentation fault:
- dereferencing a null pointer or uninitialized pointer
- out-of-bounds array access
- incorrect use of malloc, free and related functions
- use of scanf with invalid arguments
交叉编译工具链
在一种计算机环境中运行的编译程序, 能编译出在另外一种环境下运行的代码, 这个编译过程就叫交叉编译
简单地说,就是在一个平台上生成另一个平台上的可执行代码.比如,X86机器上生成arm机器的程序
交叉编译工具链是一个由编译器、连接器和解释器组成的综合开发环境,交叉编译工具链主要由binutils、gcc和glibc三个部分组成。有时出于减小 libc 库大小的考虑,也可以用别的 c 库来代替 glibc,例如 uClibc、dietlibc 和 newlib
分类和说明
从授权上,分为免费授权版和付费授权版。
免费版目前有三大主流工具商提供,第一是GNU(提供源码,自行编译制作),第二是 Codesourcery,第三是Linora。
收费版有ARM原厂提供的armcc、IAR提供的编译器等等,因为这些价格都比较昂贵,不适合学习用户使用,所以不做讲述。
- arm-none-linux-gnueabi-gcc:是 Codesourcery 公司(目前已经被Mentor收购)基于GCC推出的的ARM交叉编译工具。可用于交叉编译ARM(32位)系统中所有环节的代码,包括裸机程序、u-boot、Linux kernel、filesystem和App应用程序。
- arm-linux-gnueabihf-gcc:是由 Linaro 公司基于GCC推出的的ARM交叉编译工具。可用于交叉编译ARM(32位)系统中所有环节的代码,包括裸机程序、u-boot、Linux kernel、filesystem和App应用程序。
- aarch64-linux-gnu-gcc:是由 Linaro 公司基于GCC推出的的ARM交叉编译工具。可用于交叉编译ARMv8 64位目标中的裸机程序、u-boot、Linux kernel、filesystem和App应用程序。
- arm-none-elf-gcc:是 Codesourcery 公司(目前已经被Mentor收购)基于GCC推出的的ARM交叉编译工具。可用于交叉编译ARM MCU(32位)芯片,如ARM7、ARM9、Cortex-M/R芯片程序。
- arm-none-eabi-gcc:是 GNU 推出的的ARM交叉编译工具。可用于交叉编译ARM MCU(32位)芯片,如ARM7、ARM9、Cortex-M/R芯片程序。
命名规则
交叉编译工具链的命名规则为:arch [-vendor] [-os] [-(gnu)eabi]
- arch – 体系架构,如ARM,MIPS(通过交叉编译工具生成的可执行文件或系统镜像的运行平台或环境)
- vendor – 工具链提供商
- os – 目标操作系统(host主要操作平台,也就是编译时的系统)
- eabi – 嵌入式应用二进制接口(Embedded Application Binary Interface)
根据对操作系统的支持与否,ARM GCC可分为支持和不支持操作系统,如
- arm-none-eabi:这个是没有操作系统的,自然不可能支持那些跟操作系统关系密切的函数,比如fork(2)。他使用的是newlib这个专用于嵌入式系统的C库。
- arm-none-linux-eabi:用于Linux的,使用Glibc
实例
1. arm-none-eabi-gcc
(ARM architecture,no vendor,not target an operating system,complies with the ARM EABI) 用于编译 ARM 架构的裸机系统(包括 ARM Linux 的 boot、kernel,不适用编译 Linux 应用 Application),一般适合 ARM7、Cortex-M 和 Cortex-R 内核的芯片使用,所以不支持那些跟操作系统关系密切的函数,比如fork(2),他使用的是 newlib 这个专用于嵌入式系统的C库。
2. arm-none-linux-gnueabi-gcc
(ARM architecture, no vendor, creates binaries that run on the Linux operating system, and uses the GNU EABI)
主要用于基于ARM架构的Linux系统,可用于编译 ARM 架构的 u-boot、Linux内核、linux应用等。arm-none-linux-gnueabi基于GCC,使用Glibc库,经过 Codesourcery 公司优化过推出的编译器。arm-none-linux-gnueabi-xxx 交叉编译工具的浮点运算非常优秀。一般ARM9、ARM11、Cortex-A 内核,带有 Linux 操作系统的会用到。
3. arm-eabi-gcc
Android ARM 编译器。
4. armcc
ARM 公司推出的编译工具,功能和 arm-none-eabi 类似,可以编译裸机程序(u-boot、kernel),但是不能编译 Linux 应用程序。armcc一般和ARM开发工具一起,Keil MDK、ADS、RVDS和DS-5中的编译器都是armcc,所以 armcc 编译器都是收费的(爱国版除外,呵呵~~)。
5. arm-none-uclinuxeabi-gcc 和 arm-none-symbianelf-gcc
arm-none-uclinuxeabi 用于uCLinux,使用Glibc。
arm-none-symbianelf 用于symbian,没用过,不知道C库是什么 。
Codesourcery
Codesourcery推出的产品叫Sourcery G++ Lite Edition,其中基于command-line的编译器是免费的,在官网上可以下载,而其中包含的IDE和debug 工具是收费的,当然也有30天试用版本的。
目前CodeSourcery已经由明导国际(Mentor Graphics)收购,所以原本的网站风格已经全部变为 Mentor 样式,但是 Sourcery G++ Lite Edition 同样可以注册后免费下载。
Codesourcery一直是在做ARM目标 GCC 的开发和优化,它的ARM GCC在目前在市场上非常优秀,很多 patch 可能还没被gcc接受,所以还是应该直接用它的(而且他提供Windows下[mingw交叉编译的]和Linux下的二进制版本,比较方便;如果不是很有时间和兴趣,不建议下载 src 源码包自己编译,很麻烦,Codesourcery给的shell脚本很多时候根本没办法直接用,得自行提取关键的部分手工执行,又费精力又费时间,如果想知道细节,其实不用自己编译一遍,看看他是用什么步骤构建的即可,如果你对交叉编译器感兴趣的话。
ABI 和 EABI
ABI:二进制应用程序接口(Application Binary Interface (ABI) for the ARM Architecture)。在计算机中,应用二进制接口描述了应用程序(或者其他类型)和操作系统之间或其他应用程序的低级接口。
EABI:嵌入式ABI。嵌入式应用二进制接口指定了文件格式、数据类型、寄存器使用、堆积组织优化和在一个嵌入式软件中的参数的标准约定。开发者使用自己的汇编语言也可以使用 EABI 作为与兼容的编译器生成的汇编语言的接口。
两者主要区别是,ABI是计算机上的,EABI是嵌入式平台上(如ARM,MIPS等)。
arm-linux-gnueabi-gcc 和 arm-linux-gnueabihf-gcc
两个交叉编译器分别适用于 armel 和 armhf 两个不同的架构,armel 和 armhf 这两种架构在对待浮点运算采取了不同的策略(有 fpu 的 arm 才能支持这两种浮点运算策略)