Skip to content
Published at:

GCC笔记

小故事

通过在编译器里面做手脚,给程序加后门 TODO

编译器

作用:把代码 翻译成可执行程序

如何编译?如何运行?这两个阶段干了什么?

GCC编译器

官网:https://gcc.gnu.org/

GCC文档:https://gcc.gnu.org/onlinedocs/gcc-8.5.0/gcc/#toc-GCC-Command-Options

书籍推荐:An Introduction to GCC

Note:多版本并行开发.最新版本已经到了GCC11了, 老版本7.X也还在更新; 和普通微信及其他app开发有区别.

之前在开阳开发时版本4.9.x, (GCC 4.9.4 released [2016-08-03])

GCC全称GNU Compiler Collection, GCC是一个工具集合,在编译代码的时候也是团队协作,跟我们开发一样,不同的阶段交给不同的人去处理. 比如:预处理器,链接器(linker:ld)

编译四步骤:

  1. 预处理:宏定义展开、头文件展开、条件编译,这里并不会检查语法
  2. 编译:检查语法,将预处理后文件编译生成汇编文件
  3. 汇编:将汇编文件生成目标文件(二进制文件)
  4. 链接:将目标文件链接为可执行程序
bash
# 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编译器的公式的话,就是下面这些了(还有生成库)

bash
# 第一步: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:指定编译标准

bash
# 使用示例
$ gcc -std=c11 main.c

-Idir: 指定gcc找头文件的目录

https://gcc.gnu.org/onlinedocs/cpp/Search-Path.html

bash
# 查看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:

  1. Any directories specified by -rpath-link options.
  2. 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.
  3. 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".
  4. On SunOS, if the -rpath option was not used, search any directories specified using -L options.
  5. For a native linker, search the contents of the environment variable "LD_LIBRARY_PATH".
  6. 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.
  7. The default directories, normally /lib and /usr/lib.
  8. 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库

bash
# 使用示例
$ 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版本的程序大不少

bash
$ gcc -g main.c

-v: 显示编译详情(v: verbose)

编译出问题,不复合预期时候用. 会告诉你gcc是怎么把程序编译出来的,有哪些选项参数,用的什么标准,开启了哪些选项

bash
# gcc
$ gcc -v main.c

# make
$ make VERBOSE=1

# cmake
set(CMAKE_VERBOSE_MAKEFILE ON)

特定变量:

LIBRARY_PATH:编译时需要的动态链接库

LD_LIBRARY_PATH:运行时能够自动找到需要的动态链接库

bash
# 查看程序依赖哪些共享库
$ 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格式)
  • 链接时,目标可执行程序只取我需要的部分,合并到可执行文件中.(可移植性高);
  • 运行时会被全部加载到内存(内存占用高)
bash
# 打包成静态库
$ 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)
  • 运行时调用该库函数时才会被全部加载到内存;并且多个程序只会加载一份到内存中(多个程序同时去年时.内存占用低)
bash
# 打包动态库
$ 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:查看程序依赖的共享库

bash
# 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)

思考问题:

  1. 多个程序使用同一个共享库时,共享哪些内容? (代码段,只读数据),
  2. 如果同时安装了一个库的动态库和静态库,你的程序会link哪个库? 如果有动态库,默认就链接动态库

其它工具:

UNIX 系统的本地工具, GNU Binutils

工具描述
ar这是一个程序,可通过从文档中增加、删除和析取文件来维护库文件。通常使用该工具是为了创建和管理连接程序使用的目标库文档。该程序是 binutils 包的一部分
asGNU 汇编器。实际上它是一族汇编器,因为它可以被编泽或能够在各种不同平台上工作。 该程序是 binutils 包的一部分
ldGNU 连接程序。该程序将目标文件的集合组合成可执行程序。该程序是 binutils 包的一部
gcovgprof 使用的配置工具,用来确定程序运行的时候哪一部分耗时最大
gdbGNU 调试器,可用于检查程序运行时的值和行为
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 才能支持这两种浮点运算策略)

Updated at: