构建C工程中的一些疑惑解答
#include “.c”文件 可行吗?
#include 指令执行在预处理阶段
主要用于包含头文件(.h 文件),以便在编译时插入对应的声明和宏定义等内容
理论上可行,但不推荐。要按照规范,.h用于声明,.c用于实现,利于阅读、模块化组织
链接脚本与汇编源文件、C源文件的联系
链接脚本是用于指示链接器如何链接代码、数据的描述文件
- 四字节对齐:要求数据的存放起始地址必须是4的倍数
作用:
1、数据结构按照4字节对齐,使得处理器更有效访问,32位系统可以一次读完一个4字节长度变量
2、某些硬件要求数据需要在特定的边界上对齐,否则可能出现异常
如STM32,Flash的写入、DMA访问、结构体成员、线程栈PSP、指针变量都需要考虑字节对齐
3、结构体本身没有4字节对齐,进行特定的跳转读取,可能导致异常
与编译链接相关之实用小知识
printf重定向至串口
开发调试时可以通过printf进行信息打印输出,但是通常编译器默认将printf重定向至屏幕,可以通过重定向至串口来输出:
printf
是C标准库用于格式化输出到标准输出(stdout)的函数,其在C标准库里内会调用到接口_write
,该接口是个弱函数,默认实现是输出至屏幕。- 那么用户可以自行实现
_write
函数,将数据输出至串口。编译器链接时会链接到用户自行实现的_write
函数,而非默认的弱符号修饰的_write
函数。
当然,不同C库的实现方式也不同,这里以gcc nano
为例。(其它C库如 ARM-microlib,或者 newlib 等,其重定向时不一定为重写_write
函数,可能是_io_putchar
函数、_putchar
、fputc
函数等)
1 | int _write(int fd, char* data, int len) |
非典型知识
.a静态库文件和.o目标文件、.elf文件
ELF文件格式: 一种广泛使用的文件格式,主要用于UNIX和类UNIX系统,包括Linux。ELF文件格式是Linux系统中程序编译、链接和加载的基础。它允许开发者创建模块化、可重定位的程序,并支持动态链接和加载
类型:可执行文件、可重定位文件(.o)、共享对象文件(.so 动态库)
ELF头部(ELF Header):描述了文件的总体特性,如文件类型、目标机器架构、程序入口地址、程序头表和节头表的位置等。
程序头表(Program Header Table):列举了所有的段(segments),以及它们的属性,如加载到内存的地址、在文件中的偏移量、大小等。
节头表(Section Header Table):包含了文件中各个节(sections)的描述,每个节包含了程序的一部分,如代码、数据或符号表。
节(Sections):包含了实际的程序代码和数据,以及调试信息、符号表等辅助信息。
段(Segments):在可执行文件或动态链接库中,节被组织成段,每个段代表一个内存区域,如文本段(代码)、数据段(初始化数据)和BSS段(未初始化数据)。
.elf文件: .elf 文件是遵循 ELF 格式的二进制文件,其是链接器处理多个 .o 文件后的产物。.o 文件是构成 .elf 文件的基本单元,多个 .o 文件通过链接器链接成一个 .elf 文件
内容:包含完整的可执行或可链接的代码和数据,包括.o文件的内容、重定位信息、节区信息、动态链接信息、调试信息等
作用:.elf 文件可以直接在支持ELF格式的操作系统上运行,或者作为动态库被其他程序加载。
特点:.elf 文件拥有实际的内存地址信息,因为它经过了链接器的地址分配和重定位
在嵌入式设备上运行时,往往需要将其转换为不含调试信息且更适合加载到目标硬件的 .bin 或 .hex 格式
.o文件: 格式由目标体系结构决定,通常为ELF
,.o 文件是编译器(compiler)处理源代码文件(如 .c 或 .cpp)后的结果
内容:
- 文件头:记录了文件的类型、大小、目标体系结构、编译器版本等
- 代码段:包含了函数的代码,如操作码、数据段、代码段
- 数据段:包含了函数的静态数据,如全局变量、静态变量
- 符号表:记录了.o文件中所有符号的名称和地址,其可以是函数、变量、常量等
- 重定位表:记录了.o文件之间相互引用的符号的地址
如:如果一个.o文件中有一个函数foo(),另一个.o文件中有一个变量a,而变量a的值由函数foo()返回,则需要在两个.o文件之间建立重定位关系
.a文件格式: 静态库文件,由多个.o文件链接而成
- .o文件的列表
- 符号表:记录了文件中所有符号的名称和地址。符号可以是函数、变量、常量等
- 重定位表:用于记录两个文件之间相互引用的符号的地址
程序链接时,链接器会根据.a文件中的符号表和重定位表,将.a文件中的代码和数据链接到程序中
nm命令:用于查看目标文件(.o文件)和库文件(.a文件)中的符号表(-a:显示所有符号,-g:显示符号类型、地址、符号表中索引,-r:显示符号的重定位信息)
nm XXX.a
objdump命令:objdump命令用于反汇编目标文件(.o文件)和库文件(.a文件)(-d:反汇编代码段,-s:显示数据段,-t:显示符号表)
objdump -t 1.o
readelf命令:readelf命令用于查看目标文件(.o文件)和库文件(.a文件)的文件头、代码段、数据段等信息(-h:显示文件头信息,-S:显示代码段信息,-D:显示数据段信息)
readelf -h XXX.a
C程序开发注意点
头文件包含
- 避免冗余的头文件包含,只有当用到时才添加其头文件
- 头文件在预处理阶段会被复制到源文件中,过多包含不必要头文件会使得编译时间增加
- 头文件中定义了相同变量、函数等,可能会引起不必要的冲突;或会使得链接器产出些潜在的错误链接问题
- 尽量不要在头文件中包含头文件
修改编译优化级别
改变编译等级可能会导致程序运行出错
- 提高优化级别时出错,可考虑以下:
程序中没有放置正确的内存屏障,优化导致的乱序执行可能有问题
部分代码设计需要严谨考虑时序性
可能会忽略了链接脚本中指定的四字节对齐 - 降低优化级别时出错:
线程的堆栈可能偏小,导致栈溢出
- 提高优化级别时出错,可考虑以下:
修改编译等级后导致运行出错的问题排查思路
C编译过程
编译器的主要工作流程如下:
源程序(source code)→预处理器(preprocessor)→编译器(compiler)→汇编程序(assembler)→目标程序(object code)→连接器(链接器,Linker)→可执行程序(executables)
- 配置
- 确定标准库和头文件位置
- 确定依赖关系
- 头文件预编译
- 预处理
- 编译
- 链接
- 安装