0%

【构建】编译/链接原理及其在开发中之体现

构建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函数、_putcharfputc函数等)

1
2
3
4
5
int _write(int fd, char* data, int len)
{
drv_uart_write(DEV_UART1, (uint8_t *)data, len); // 由开发者自行实现此接口
return 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)

  • 配置
  • 确定标准库和头文件位置
  • 确定依赖关系
  • 头文件预编译
  • 预处理
  • 编译
  • 链接
  • 安装

参考站点