Linux 上的交叉编译

1. C/C++ 编译的四个步骤

从源代码到可执行文件,通常分为:预处理 → 编译 → 汇编 → 链接。

1.1 预处理(Preprocessing)

处理以 # 开头的预处理指令,例如展开 #include 引入的头文件、替换宏、处理条件编译,并去掉注释。

bash 复制代码
gcc -E main.c -o main.i

-E 表示只进行预处理。C 的预处理结果通常以 .i 结尾,C++ 通常以 .ii 结尾。

1.2 编译(Compilation)

对预处理后的代码进行语法、语义检查和优化,生成目标架构的汇编代码。

bash 复制代码
gcc -S main.i -o main.s

-S 表示执行到生成汇编代码为止。这里的"编译"指四个步骤中的这一步;平时也会用"编译"泛指整个构建过程。

1.3 汇编(Assembly)

把汇编代码转换成机器指令,生成目标文件。

bash 复制代码
gcc -c main.s -o main.o

-c 表示执行到生成目标文件为止,不进行链接。.o 文件通常包含机器代码、符号表、重定位信息等,还不能直接作为程序运行。

1.4 链接(Linking)

把目标文件与其他目标文件、所需库以及启动代码组合起来,解析函数和变量等符号的引用,完成必要的重定位,生成可执行文件。

以 test.c 和 v.c 两个源文件为例,先分别生成目标文件,再将它们链接为一个程序:

bash 复制代码
# 分别生成目标文件(完成预处理、编译和汇编,不进行链接)
gcc -c test.c -o test.o
gcc -c v.c -o v.o

# 将两个目标文件链接成可执行文件 test
gcc test.o v.o -o test

# 运行程序(要求架构与当前系统兼容)
./test

其中,test.o 和 v.o 是链接的输入,-o test 指定生成的可执行文件名。

例如,test.c 中使用了在 v.c 中定义的全局变量或函数,需要在 test.c 中提供相应声明。分别生成目标文件后,对这些外部符号的引用通常仍待解析;链接器会在 v.o 中找到对应定义,把两个目标文件组合成完整程序。如果漏掉 v.o,且其他输入也没有提供这些定义,通常会报 undefined reference(未定义的引用)。

代码中调用 printf() 等库函数时,链接还需要处理对相应库的引用。动态链接的程序在运行时还需要相应的动态库。

1.5 一条命令完成整个过程

bash 复制代码
# C 程序
gcc main.c -o main

# C++ 程序
g++ main.cpp -o main

# 运行生成的程序(要求架构与当前系统兼容)
./main

上面的单条构建命令会完成四个步骤,默认不会保留中间的 .i、.s、.o 文件。C++ 通常使用 g++,它会自动链接常用的 C++ 标准库。

记忆:预处理展开代码,编译生成汇编,汇编生成目标文件,链接生成可执行文件。

2. 交叉编译工具链(以 i.MX6ULL 为例)

2.1 什么是交叉编译

交叉编译是指:在一种平台上编译,生成供另一种平台运行的程序。

例如,在 x86_64 架构的 Linux 电脑上编译代码,生成能在 i.MX6ULL 开发板上运行的 ARM 程序。电脑是开发主机,开发板是目标平台。

i.MX6ULL 是 NXP 的处理器,采用 32 位 ARM Cortex-A7 内核 。因此,为它编译程序时,需要使用适合目标 ARM 平台的工具链。参考:NXP i.MX6ULL 产品资料

2.2 工具链一般从哪里获取

一般优先使用芯片厂商或开发板厂商随 BSP/SDK 提供、推荐的交叉编译工具链。

  • BSP(板级支持包):包含与开发板相关的启动、内核、驱动及构建配置等内容。
  • SDK(软件开发套件):通常包含交叉编译工具链、目标系统的头文件和库,以及环境配置脚本。

以 i.MX6ULL 为例,先查看 NXP 或所用开发板厂商的配套资料,使用与板上系统版本相匹配的工具链和 SDK。工具链不仅要匹配 CPU 架构,还要考虑 ABI(如浮点调用约定)、C 库及目标系统的库版本;仅看到名称里有 arm,还不足以确认兼容。

根据本次提供的百问网开发包目录,实际使用的工具链前缀是 arm-buildroot-linux-gnueabihf-,后续示例采用这一前缀。

2.3 为什么叫"工具链"

它是一组配合完成编译、汇编、链接等工作的工具。以当前开发包的 arm-buildroot-linux-gnueabihf- 前缀为例:

工具名称 主要用途
arm-buildroot-linux-gnueabihf-gcc C 编译驱动,也可调度汇编、链接等步骤
arm-buildroot-linux-gnueabihf-g++ C++ 编译驱动
arm-buildroot-linux-gnueabihf-as 汇编器
arm-buildroot-linux-gnueabihf-ld 链接器
arm-buildroot-linux-gnueabihf-ar 创建静态库
arm-buildroot-linux-gnueabihf-objcopy 复制、提取或转换目标文件内容

生成的 test 应复制到匹配的 i.MX6ULL Linux 开发板上运行;在普通 x86_64 主机上通常不能直接运行。

记忆:在电脑上编译,在开发板上运行;工具链优先选厂商配套版本。

3. ELF 文件格式

3.1 什么是 ELF

ELF(Executable and Linkable Format,可执行与可链接格式)是 Linux 平台上广泛使用的二进制文件格式。 前面生成的 .o 目标文件、最终的可执行程序,以及 .so 共享库,都可以采用 ELF 格式。

ELF 文件不仅保存机器指令和数据,还保存文件头、加载信息、符号表、调试信息等,供链接器、系统加载器和调试器使用。Linux 执行 ELF 程序时,需要根据其中的加载信息建立程序的内存映像。

3.2 ELF 文件结构示意图

下图按课堂照片右侧的内容重绘,保留各层名称和分组:

重点记住:

  • 文件头与头表:记录文件基本信息、各部分的位置及加载方式。
  • .text:存放机器指令。
  • .rodata:存放只读数据,如字符串常量。
  • .data 与 .bss :存放全局变量和静态变量;初值非零的通常放在 .data,未显式初始化或初值为零的通常放在 .bss。
  • 符号与调试信息 :.symtab 记录函数、变量等符号,图中的 .stab 保存调试信息。

记忆:ELF 不只是代码和数据,还包含供链接、加载和调试使用的信息。

4. file 和 readelf 命令

4.1 用两种编译器编译同一个 test.c

在 Linux 开发主机上,进入 test.c 所在目录,分别使用普通 gcc 和开发板配套的交叉编译器编译同一份源码。以下假设交叉编译工具链的 bin 已加入 PATH。

bash 复制代码
# 普通 gcc:生成供当前 Linux 主机运行的程序
gcc test.c -o test_pc

# i.MX6ULL 配套交叉编译器:生成供开发板运行的程序
arm-buildroot-linux-gnueabihf-gcc test.c -o test_arm

两个命令使用同一个 test.c,分别输出 test_pc 和 test_arm。

4.2 分别使用 file 查看两个文件

成功生成两个文件后,在同一目录执行:

bash 复制代码
file test_pc
file test_arm

file 用于识别文件类型。重点比较输出中的位数和目标架构:

对比项 test_pc test_arm
位数 64-bit,64 位 32-bit,32 位
目标架构 x86-64,面向当前电脑 ARM,面向 i.MX6ULL 开发板
类型描述 shared object executable
动态加载器 /lib64/ld-linux-x86-64.so.2 /lib/ld-linux-armhf.so.3

两者都是 ELF 文件,LSB 表示小端字节序,dynamically linked 表示动态链接。

结论:同一个 test.c,普通 gcc 生成 x86-64 程序,交叉编译器生成 ARM 程序;两者面向不同平台,通常不能直接互换运行。

4.3 使用 readelf -h 查看文件头

readelf -h 用于查看 ELF 文件头 ,比 file 提供更详细的信息。分别查看两个文件:

bash 复制代码
readelf -h test_pc
readelf -h test_arm

这里的 -h 表示查看文件头。也可使用工具链配套的 arm-buildroot-linux-gnueabihf-readelf -h test_arm 查看 ARM 程序。

test_pc 的实际输出:

test_arm 的实际输出:

作用:确认程序的位数、目标架构、文件类型和入口等信息,帮助检查是否编译成了目标平台需要的程序。

字段 test_pc test_arm
Class(位数) ELF64,64 位 ELF32,32 位
Machine(架构) x86-64 ARM
Type(类型) DYN,结合前面的编译方式,可能为 PIE 程序 EXEC,可执行文件
Flags(架构相关标志) 0x0 EABI5、hard-float ABI(硬浮点调用约定)

Entry point address 是程序入口,通常对应启动代码,不能直接当作 main() 的地址。

5. BIN 文件与 ELF 的区别

这里的 BIN 指原始二进制镜像(Raw Binary),通常保存程序的机器指令和数据,没有 ELF 文件头、程序头表、节头表等结构信息,常用于裸机程序或固件烧录。

对比 ELF BIN(原始二进制镜像)
内容 代码、数据,以及加载、链接等结构信息,可含符号和调试信息 主要是代码和数据的原始字节
加载方式 加载器可以从文件头和程序头等获取加载位置与入口 加载位置和入口通常由启动流程或外部约定指定
常见用途 Linux 应用程序、目标文件、共享库及调试 裸机程序、固件镜像及烧录
信息查看 可用 readelf 查看内部结构 没有 ELF 结构,不能直接用 readelf 解析

两者都可以包含 CPU 执行的机器指令。 BIN 也需要正确加载和启动;把普通 Linux 应用的 ELF 转成 BIN,并不会自动变成能独立运行的裸机程序。GNU objcopy 的原始二进制输出说明可参考 官方文档。

记忆:ELF 带有结构和描述信息,BIN 是按约定使用的原始字节镜像。 .bin 只是常见扩展名,不能仅凭后缀确定实际格式。

6. 交叉编译工具链中常用的工具

6.1 arm-buildroot-linux-gnueabihf-size 工具

作用:统计目标文件或可执行文件中代码、数据等节的大小,帮助了解程序的空间占用。

bash 复制代码
arm-buildroot-linux-gnueabihf-size test_arm

实际输出:

大小的单位都是字节。

列名 本次值 含义
text 865 程序指令和只读内容(如字符串)占多大
data 288 静态区,全局变量
bss 4 程序启动时要清零的数据占多大,如没写初值的全局变量
dec 1157 前面三项加起来,用十进制表示
hex 485 同一个总数,换成十六进制表示
filename test_arm 查看的是哪个文件

6.2 arm-buildroot-linux-gnueabihf-nm 工具

作用:查看目标文件或可执行文件的符号表,了解函数、变量的名称、类型和地址等信息。

bash 复制代码
arm-buildroot-linux-gnueabihf-nm test_arm

可以把符号表理解成程序的**"索引表",主要记录函数、变量等的名字、位置和类别**,方便链接器、调试器查找。

例如图中的 0001040c T main:

  • main:函数的名字。
  • 0001040c:这个函数在该程序中的地址,用十六进制表示。
  • T:表示符号位于代码区。

图中的 U puts@GLIBC_2.4 表示:程序使用了 puts,但它的定义由外部库提供,当前文件中没有定义。

普通符号表通常不影响已链接程序的正常运行。 链接完成后,CPU 按机器指令执行,调用函数不需要再查找 main 等名字。因此,去掉普通符号表(.symtab)通常仍能运行,只是查看函数名、调试和定位问题会更困难。动态链接所需的动态符号表(.dynsym)仍需保留。

6.3 arm-buildroot-linux-gnueabihf-strip 工具

作用:去掉程序中运行时不需要的符号、调试信息等,减小文件体积。

bash 复制代码
# 查看处理前的文件信息
file test_arm

# 使用交叉工具链的 strip,直接修改 test_arm
arm-buildroot-linux-gnueabihf-strip test_arm

# 再次查看文件信息
file test_arm
  • 处理前 not stripped:尚未剥离普通符号表等信息。
  • 处理后 stripped:已剥离这些信息,文件通常会变小。

图中处理前后仍是 32 位 ARM ELF 可执行文件,动态链接方式也没有改变。去掉的是运行时不需要的辅助信息,通常不影响程序正常运行。

6.4 arm-buildroot-linux-gnueabihf-objdump 工具

作用:反汇编,把程序中的机器码显示成汇编指令,方便查看程序实际执行的指令。

bash 复制代码
arm-buildroot-linux-gnueabihf-objdump -d test_arm

-d 表示反汇编,查看 test_arm 中的机器码所对应的汇编指令。

6.5 arm-buildroot-linux-gnueabihf-objcopy 工具

作用:复制、提取或转换目标文件中的内容,例如把 ELF 文件转换成 BIN 文件。

bash 复制代码
arm-buildroot-linux-gnueabihf-objcopy -O binary test_arm test_arm.bin

-O binary 指定输出为原始二进制格式,test_arm 是输入文件,test_arm.bin 是生成的文件。格式转换不会让这个 Linux 应用自动变成能独立运行的裸机程序。

为什么转成 BIN 后,仍不能直接脱离 Linux 运行?

因为 objcopy 只转换文件格式,不改变程序原来的依赖。当前的 test_arm 是 Linux 动态链接程序,依赖 Linux 的加载器、库和系统服务,转成 BIN 后这些需求仍然存在。

转换有什么意义?

BIN 方便交给按地址写入原始字节的烧录工具或加载程序使用。裸机开发中,程序会事先配好启动代码、链接地址和必要的硬件初始化,先生成 ELF 供调试,再转换成 BIN 供烧录或加载:

text 复制代码
按裸机要求编写的程序 → ELF(供链接、调试)→ BIN(供烧录、加载)

因此,本次把 Linux 应用转成 BIN,主要是练习格式转换;它不会因此获得裸机运行能力。能否脱离操作系统,取决于程序怎么编写和构建,而不是文件后缀。

相关推荐
蒲公英内测分发1 小时前
智能门锁量产时,工厂测试 App 一直在更新,产线怎么避免装错 APK?
服务器·人工智能·github·智能硬件·智能手表
垆边人似月.1 小时前
Python中如何使用get函数实现频次查询
服务器·前端·python
小张成长计划..1 小时前
【Linux】27:日志
linux·运维·服务器
时间售卖铺1 小时前
Nginx 反向代理
运维·nginx·云原生
life码农1 小时前
Nginx location 优先级详解:匹配规则、修饰符与完整代码示例
运维·nginx
别动我齐刘海2 小时前
机器人 C++ / 系统联调补强2——异常处理、Protobuf
linux·c++·人工智能·学习·算法·机器学习·机器人
Lsetea2 小时前
curl -I检查HTTPS返回405:HEAD、GET与证书验收分层排查
linux·http·https·ssl证书·curl
我命由我123452 小时前
Python 开发 - Python 虚拟环境
运维·开发语言·后端·python·http·https·java-ee
dlhto2 小时前
Windows 使用 Rufus 制作 AlmaLinux 10.2 启动 U 盘:从 ISO 下载到服务器安装
运维·服务器·windows