25、MIPS交叉编译实战:以MIPS32为例,处理大小端与ABI问题

MIPS架构,说实话,在嵌入式领域是个老面孔了。从路由器到机顶盒,从工业控制到物联网模组,你总能碰到它。但MIPS有个让人头疼的特点——大小端和ABI的排列组合实在太多了。

我刚开始接触MIPS交叉编译时,就被这个坑过。明明代码编译通过了,烧到板子上就是跑不起来。后来才发现,工具链的大小端选错了,ABI也不匹配。嗯,这节课我们就来彻底搞定它。

MIPS32的核心概念:大小端与ABI

先理清楚几个关键概念。MIPS32是32位的MIPS架构,它有两种字节序:

  • 小端(Little-Endian):低地址存低字节。MIPSEL,也就是MIPS Little Endian。
  • 大端(Big-Endian):低地址存高字节。MIPSEB,也就是MIPS Big Endian。

ABI呢,全称是Application Binary Interface。它定义了函数调用约定、寄存器使用规则、栈布局等。MIPS常见的ABI有:

ABI名称 特点 适用场景
o32 最老、最通用,32位调用约定 传统MIPS Linux系统
n32 64位寄存器但32位指针,效率高 某些高性能嵌入式
n64 纯64位ABI 64位MIPS系统
eabi 嵌入式ABI,无操作系统场景 裸机、RTOS

对于MIPS32,我们最常用的是o32 ABI。我个人习惯,只要不是特殊需求,一律用o32,兼容性最好。

工具链的选择与命名规则

MIPS的交叉编译工具链,命名规则其实很直观。你看这个:

mips-linux-gnu-gcc        # 大端,o32 ABI
mipsel-linux-gnu-gcc      # 小端,o32 ABI
mips64-linux-gnuabi64-gcc # 64位大端,n64 ABI

命名格式是:架构-厂商-系统-ABI-gcc。其中:

  • mipsmipsel 决定了大小端
  • linux-gnu 表示目标系统是Linux
  • 后面的 abi64eabi 表示ABI类型

我在项目中遇到过,有人拿了大端工具链去编译小端程序,结果链接器报了一堆莫名其妙的错误。其实只要看一眼工具链名字里的 mips 还是 mipsel,就能避免这个问题。

实战:编写MIPS32的CMake工具链文件

好,我们直接上手。假设目标平台是MIPS32小端,o32 ABI,运行Linux系统。下面是我写的一个工具链文件:

# mipsel-linux-gnu.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR mipsel)

# 指定交叉编译器
set(CMAKE_C_COMPILER mipsel-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER mipsel-linux-gnu-g++)

# 设置大小端标志
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -EL")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -EL")

# 设置ABI为o32
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mabi=32")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mabi=32")

# 查找目标系统的根文件系统
set(CMAKE_FIND_ROOT_PATH /usr/mipsel-linux-gnu)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

这里有几个关键点:

  • -EL 强制小端,-EB 是大端。我建议显式写出来,别依赖默认值。
  • -mabi=32 指定o32 ABI。如果你用n32,就写 -mabi=n32
  • CMAKE_FIND_ROOT_PATH 指向目标系统的sysroot,这样CMake才能找到正确的头文件和库。
注意: 大小端和ABI必须与目标硬件完全一致。我曾经帮一个客户调试,他用的芯片是大端MIPS,但工具链默认是小端。编译出来的程序在板子上跑,所有多字节数据都是反的。排查了整整两天才发现是大小端问题。

编译与验证

工具链文件写好了,怎么用呢?很简单:

mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../mipsel-linux-gnu.cmake
make

编译完成后,用 file 命令检查生成的可执行文件:

$ file hello
hello: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld.so.1, for GNU/Linux 3.2.0, not stripped

看到 LSB 了吗?那就是小端(Least Significant Byte first)。如果是大端,会显示 MSB。另外 MIPS32 也确认了是32位架构。

你还可以用 readelf 查看更详细的信息:

$ mipsel-linux-gnu-readelf -h hello | grep -E "Class|Machine|Flags"
  Class:                             ELF32
  Machine:                           MIPS R3000
  Flags:                             0x50001007, noreorder, pic, cpic, o32, mips32

看到 o32 标志了吗?这就对了。如果这里显示的是 n32eabi,说明你的ABI设置有问题。

大小端与ABI的常见陷阱

说几个我踩过的坑:

  1. 混合大小端链接:如果你链接了一个大端的静态库,但主程序是小端编译的,链接器会报 relocation truncated to fit 之类的错误。解决办法是确保所有目标文件的大小端一致。
  2. ABI不匹配导致函数调用崩溃:o32和n32的寄存器使用规则不同。如果你用o32编译了主程序,但链接了一个n32的库,函数调用时参数传递会乱掉。程序可能直接段错误。
  3. sysroot路径错误:CMake的 CMAKE_FIND_ROOT_PATH 如果指向了错误的sysroot,它会找到宿主机的头文件。编译出来的程序在目标板上运行时,可能因为glibc版本不匹配而崩溃。
我的建议: 在项目初期,先写一个简单的hello world程序,用 filereadelf 确认大小端和ABI都正确。这一步花不了5分钟,但能省下后面几天的调试时间。

知识体系图:MIPS交叉编译决策流程

下面这张图总结了MIPS交叉编译时,你需要做的关键决策和检查点:

MIPS交叉编译决策流程 确定目标硬件 大小端? 大端: -EB 小端: -EL ABI? o32: -mabi=32 n32: -mabi=n32 编译后验证: file + readelf 部署到目标板

这张图的核心逻辑是:先确定硬件的大小端,再选择合适的ABI,最后一定要用 filereadelf 验证。每一步都不能跳过。

总结

MIPS交叉编译,说白了就是三件事:选对大小端、选对ABI、验证结果。工具链文件里显式指定 -EL-EB,以及 -mabi=32,是最稳妥的做法。

我个人习惯,每次新建一个MIPS项目,都会先写一个最小的CMakeLists.txt,只编译一个hello.c。确认工具链文件没问题后,再往里面加业务代码。这样即使出了问题,也知道是工具链的问题还是代码的问题。

嗯,这节课就到这里。记住,MIPS的坑大多出在大小端和ABI上,只要把这两个点卡死,后面的路就顺了。

核心要点回顾:

  • 工具链命名中 mips 是大端,mipsel 是小端
  • MIPS32最常用o32 ABI,用 -mabi=32 指定
  • 编译后务必用 filereadelf 验证
  • sysroot路径必须指向目标系统的根文件系统

公众号:蓝海资料掘金营,微信deep3321