文章目录
上篇文章:ARM 嵌入式编译系列 1 – GNU/GCC/GDB/GNU binutils介绍
下篇文章:ARM 嵌入式 编译系列 2.1 – GCC 编译参数学习
1.1 GCC 编译介绍
GCC,全称GNU Compiler Collection(GNU编译器套件),起初是GNU项目的一部分,为Unix和类Unix系统(包括Linux)提供一个免费的编译器,现已成为大多数类Unix系统的标准编译器。
GCC是一个跨平台的编译器,能够支持多种编程语言,如C、C++、Objective-C、Fortran、Ada、D等,而且还能为多种处理器架构生成目标代码,包括x86、ARM、MIPS、PowerPC等。
GCC主要由四部分组成:预处理器(cpp)、编译器本身(cc1)、汇编器(as)和链接器(ld)。
1.1.1 gcc-arm-none-eabi toolchain 介绍及安装
gcc-arm-none-eabi
是一个开源的ARM开发工具链,适用于Arm Cortex-M 和 Coretex-A 系列处理器,我们可以从从 ARM 官方下载链接选择合适的版本下载。
关于 gcc arm 更多的内容请见文章ARM 嵌入式编译系列 1 – GNU/GCC/GDB/GNU binutils介绍
注意:编译前需要指定交叉编译工具的路径,可以使用下面方法设置,在linux环境下需要根据自己的目录设定绝对路径
$ vim ~/.bashrc //add the below
export CROSS_COMPILE
declare -x CROSS_COMPILE=/.../gcc-arm-none-eabi/bin/arm-none-eabi-"
在终端中输入:
$ source ~/.bashrc
然后输入命令 :
arm-none
然后按三下 Tab
(一定不要输入全部),检查系统是否可以自动补全。
1.1.2 编译过程
一个完整的GCC编译过程通常包括预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)这四个步骤。
预处理:预处理器拿到源代码后,会处理源代码中的预处理指令,如宏定义(#define
)、头文件包含(#include
)、条件编译(#ifdef
,#ifndef
,#if
等)等。预处理器的输出是一个纯文本的源码文件。
例如,以下命令将执行预处理步骤:
gcc -E source.c -o source.i
这将生成一个预处理后的文件source.i
。
编译:编译器会把预处理后的源码编译成汇编语言代码。编译器在这一步会进行词法分析、语法分析、语义分析和优化等操作。
例如,以下命令将执行编译步骤:
gcc -S source.i -o source.s
这将生成一个汇编语言文件source.s
。
汇编:汇编器会将汇编代码转换成机器指令,即目标代码。输出的文件叫做目标文件(Object File),这是一个二进制文件。
例如,以下命令将执行汇编步骤:
gcc -c source.s -o source.o
这将生成一个目标文件source.o
。
链接:链接器会把多个目标文件和库文件链接成一个可执行文件。链接器需要处理全局变量和函数的引用,并决定它们在可执行文件中的地址。
例如,以下命令将执行链接步骤:
gcc source.o -o output
这将生成一个可执行文件output。
通常,我们会一次性执行这四个步骤,如以下命令:
gcc source.c -o output
这将会编译source.c
并生成一个可执行文件output
。
由于编译涉及内容过多,所以通常会将编译相关的部分单独写入一个脚本中,如py
脚本或者shell
脚本,或者几个脚本中。如下的编译参数是 rt-thread
中在一个 python 脚本 rtconfig.py
中配置的:
if PLATFORM == 'gcc':
# toolchains
PREFIX = 'arm-none-eabi-' //指定交叉编译工具为arm-noe-eabi-
CC = PREFIX + 'gcc'
AS = PREFIX + 'gcc' //用于编译
AR = PREFIX + 'ar' //用于lib/.a文件的处理:添加/删除/生成/解压等
CXX = PREFIX + 'g++'
LINK = PREFIX + 'gcc'
TARGET_EXT = 'elf' //用于读取elf 头信息
SIZE = PREFIX + 'size'
OBJDUMP = PREFIX + 'objdump' //用于对elf文件进行反汇编等操作
OBJCPY = PREFIX + 'objcopy' //主要用于将elf文文件copy为binary文件
TOOLS_PATH = TOP_ROOT + '/tools/misc' //指定编译工具所在的路径
GENIMG = TOOLS_PATH + '/gen_rtos_img.sh' //指定mkimage工具所在的脚本
DEVICE = ' -mcpu=cortex-m7 -mthumb -mfpu=fpv5-d16 -mfloat-abi=hard \
-ffunction-sections -fdata-sections' //设定编译参数
CFLAGS = DEVICE + ' -Dgcc'
AFLAGS = ' -c' + DEVICE + ' -x assembler-with-cpp -Wa,-mimplicit-it=thumb '
LFLAGS = DEVICE + ' -Wl,--gc-sections,-Map=rtthread.map,-cref,
-u,Reset_Handler -T board/link.lds' \
CPATH = ''
LPATH = ''
if BUILD == 'debug':
CFLAGS += ' -O0 -gdwarf-2 -g'
AFLAGS += ' -gdwarf-2' // 调试信息的存放格式按照 dwarf2标准来
else:
CFLAGS += ' -O2'
CXXFLAGS = CFLAGS
CFLAGS += ' -std=gnu99' //指定支持的C标准
CFLAGS += ' -Werror' // 将将警告升级为错误
由于使用工具 gcc-arm-none-eabi
编译出来的是个可执行的 elf
文件,里面包含了很多信息,可以通过objcpy
将一些不需要的信息去掉,然后再通过 mkimage
给生成 .bin 文件添加上 header,
POST_ACTION = OBJCPY + ' -O binary $TARGET rtthread.bin\n' + \
GENIMG + ' 0x0 rtthread.bin rtthread.img\n' + \
SIZE + ' $TARGET\n'
目标文件的内容是编译后的机器指令代码,数据,符号表,调试信息,字符串等,同时,是分别以"段"的形式存储。
objcopy 是 GNU的一个工具,其作用是将 elf 目标文件的一部分或者全部内容拷贝为一个二进制binary文件,或者进行目标文件的格式转换。
- 通过
arm-none-eabi-objcompy
将rtthread.bin
生成为rtthread.elf
- 通过
gen_rtthraed_img.sh
调用mkimage
为rtthread.bin
添加header
,生成rtthread.bin
1.2 ELF 文件
ELF
代表"可执行和可链接格式"(Executable and Linkable Format
),是一种常见的对象文件格式。在 Linux 和许多其他类Unix系统中,ELF是标准的二进制文件格式。
GCC 创建的可执行文件、目标文件、共享库以及核心转储文件都是ELF格式的。
GCC使用链接器(通常是ld)来链接ELF目标文件,并可以使用 objdump
、readelf
、nm
等工具来查看ELF
文件的内容和结构。
1.2.1 ELF 文件组成
ELF文件主要由以下部分组成:
-
ELF头(ELF Header):包含了文件的类型(如可执行文件、共享库或目标文件)、文件的体系结构(如x86或ARM)、段表和节表的位置等信息。
-
程序头表(Program Header Table):这部分在可执行文件和共享库中存在,描述了系统如何创建进程映像。
-
节区头表(Section Header Table):这部分在目标文件中存在,描述了文件的节区结构。
- text section 里装载了可执行代码;
- data section 里面装载了被初始化的数据;
- bss section 里面装载了未被初始化的数据;
- 以 .rec 打头的 sections 里面装载了重定位条目;
- symtab 或者 .dynsym section 里面装载了符号信息;
- strtab 或者 .dynstr section 里面装载了字符串信息。
-
数据(Data):包含了代码和数据等。
这是一个使用GCC创建ELF文件的例子:
gcc -o output source.c
在这个命令中,GCC将源文件"source.c
"编译并链接成一个可执行的ELF文件,文件名为"output
"。
你可以使用file
命令来查看一个文件的类型,例如:
file output
如果"output
"是一个ELF文件,你将会看到类似"ELF 64-bit LSB executable
"的输出。
1.3 bin 文件
bin 文件和 elf文件 基本上都是可执行文件,但它们的格式和使用方式有一些重要的区别。
elf 文件是一种复杂的二进制格式,它包含了很多元数据,如程序的入口点、程序需要的共享库、程序的段和节的信息等。因此,elf
文件不仅可以被执行,还可以被调试和反向工程等。在Linux系统中,大多数可执行文件、库和驱动都是elf格式的。GCC默认生成的就是elf
格式的可执行文件。
bin文件通常是一种简单的二进制格式,它通常只包含了程序的机器代码和数据,而没有elf文件那么多的元数据。bin文件通常用在嵌入式系统或者引导加载程序等,这些环境中可能没有操作系统来解析elf文件的元数据。bin文件可以被直接加载到内存中并执行,但是它们不能被轻易的调试或反向工程。
转换 elf
文件到 bin
文件,通常可以使用 objcopy
这个工具,如下命令:
objcopy -O binary input.elf output.bin
在这个命令中,-O binary
指定了输出文件的格式为 binary,input.elf 是输入的 elf文件,output.bin 是输出的 bin文件。
Linux下可以使用下面命令查看:
hexdump -C *.bin
1.3.1 elf 文件到 .image 文件
GCC 编译之后会先生成 xxx.elf
,然后再通过 arm-none-eabi-objcopy
加上相应的参数生成 xxx.bin,
最后再通过mkimage
工具对 xxx.bin
添加 header 生成 xxx.img
1.4 map 文件
在使用GCC编译链接程序时,可以使用 -Wl, -Map, output.map
选项来生成一个名为 output.map
的链接映射文件(Map文件)。Map文件包含了编译链接后的程序中各个模块在内存中的布局信息:
- 模块的地址分配;
- 各个函数在程序中的地址;
- 全局变量的地址等。
该文件对于调试程序、分析程序的内存使用很有帮助。
例如,你可以使用以下命令来生成一个Map文件:
gcc -o output source.c -Wl,-Map,output.map
在这个命令中,-Wl,-Map,output.map
选项告诉GCC生成一个名为output.map
的Map文件。output
是最终生成的可执行文件,source.c
是源代码文件。
生成的Map文件中,主要包含以下几个部分:
-
头文件列表(Header File List):列出了参与编译的所有头文件。
-
文件成员列表(File Members List):列出了参与链接的所有目标文件(Object Files)。
-
段列表(Sections List):列出了程序中的所有段(Sections),如.text、.data、.bss等。
-
符号列表(Symbols List):列出了程序中的所有全局符号(如函数和全局变量),以及它们在内存中的地址。
注意,Map文件中的信息可能会暴露程序的一些内部细节,因此在发布软件时,通常不会包含Map文件。
上篇文章:ARM 嵌入式编译系列 1 – GNU/GCC/GDB/GNU binutils介绍
下篇文章:ARM 嵌入式 编译系列 2.1 – GCC 编译参数学习