ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

x86电脑如何编译ARM程序?交叉编译原理与工具链实战

x86电脑如何编译ARM程序?交叉编译原理与工具链实战 为什么x86电脑能编译ARM程序这件事还得从我第一次在x86的Ubuntu上敲出arm-linux-gnueabihf-gcc hello.c -o hello说起。那会儿我盯着生成的文件死活想不明白我手里这台CPU明明是Intel的凭什么能吐出一个给ARM板子用的可执行文件后来把编译原理、工具链、链接器这些东西串起来才意识到“x86编译ARM”这件事本质上一点也不玄乎——编译器本身就是个运行在x86上的普通程序它只是把源代码“翻译”成ARM机器码而已。这篇文章我想把这件事彻底讲透。你会搞清楚交叉编译到底是什么、编译器和CPU之间到底是什么关系、工具链里那一大堆带前缀的gcc、ld、objcopy都是干嘛的以及在实际操作中怎么在x86机器上编译出能在ARM设备上跑的程序。无论你是做嵌入式、搞Android ROM、给树莓派交叉编译应用还是纯粹好奇“为什么编译出来的东西能跨平台”这篇都适合你。1. 先搞明白CPU架构、编译器和“跨平台”到底隔离在哪1.1 编译器不是“翻译官”而是“程序员翻译官”很多入门资料喜欢把编译器比作翻译官说它能把C语言“翻译”成机器码。这个类比大体没错但少了一个关键点翻译官本人必须活在目标环境里。你想让英国人看懂中文得找个懂中文的英国人或者至少找个懂中文的人把话翻译成英文——而编译器干的事是“懂C语言的程序员把代码翻译成某种CPU能看懂的机器码”。这里的“某种CPU”就是关键。同一句a b c;x86用mov、add指令ARM用ldr、str、add指令RISC-V又完全是另一套。机器码是跟指令集绑定的而指令集就是CPU的“母语”。所以当你用gcc hello.c -o hello在x86电脑上编译时gcc默认产生的机器码是x86的——因为这台机器的CPU就是x86gcc的默认目标就是“本机”。这没有任何魔法纯粹是默认参数。如果你告诉gcc“别管你现在跑在什么CPU上你给我生成ARM的机器码”它同样能做到。这就是交叉编译的起点。1.2 交叉编译器到底改了什么交叉编译器英文叫 cross compiler工作分两个阶段看才清晰。第一阶段编译器本身的执行。你在x86机器上跑arm-linux-gnueabihf-gcc这个程序本身必须是一个x86可执行文件否则x86的CPU根本跑不起来它。也就是说编译器这个“工具”是运行在宿主架构上的。第二阶段编译器输出的内容。它读取C/C源代码经过词法分析、语法分析、中间表示优化最后在代码生成阶段根据“目标架构”参数生成ARM指令序列。这个“目标架构”是由编译器构建时配置好的比如--targetarm-linux-gnueabihf。所以交叉编译既不神秘也不违反物理定律。它就相当于你请了一个只会说英语的英国工程师x86上的编译器程序但这个工程师懂ARM汇编目标后端能给你写出ARM指令的文档机器码。工程师本人站在哪里不重要重要的是他写出来的“作业”是给谁看的。提示这里可以顺带记住一个专业术语——宿主host和目标target。编译时跑编译器的机器叫宿主编译出来的程序要运行的机器叫目标。交叉编译就是“宿主的体系结构 ≠ 目标的体系结构”的编译方式。2. 交叉编译工具链是怎么一套组合拳只靠一个gcc是编不出能跑的可执行文件的。一个完整的可执行文件要经过预处理、编译、汇编、链接四个阶段每个阶段都有专门的工具。交叉编译的精髓其实是“一整条目标为ARM的工具链”。2.1 工具链命名规则一眼看穿目标平台如果你去装交叉编译工具链会发现有一堆带前缀的命令arm-linux-gnueabihf-gcc、arm-linux-gnueabihf-g、arm-linux-gnueabihf-ld、arm-linux-gnueabihf-objcopy、arm-linux-gnueabihf-strip。这个前缀就是目标平台的“三要素”依次拆开是arm目标CPU架构是ARMlinux目标操作系统是Linux这会影响可执行文件格式ELF、系统调用接口等多方面gnueabihf用的是glibc库且启用了硬件浮点hf hard float。ARM的浮点传参有两种约定软浮点用普通寄存器传参硬浮点用浮点寄存器传参这两者编译出来的程序不兼容所以工具链命名里必须写清楚。如果你看到aarch64-linux-gnu-gcc那就是ARM 64位AArch64目标看到arm-none-eabi-gcc就是ARM 32位、没有操作系统的裸机目标常用于STM32等单片机。前缀不是随便起的它直接决定了你编出来的东西跑在什么环境。2.2 sysrootARM世界的头文件和动静态库交叉编译最容易被忽略、也最容易出问题的地方是“头文件和库的版本归属”。你在x86机器上写程序#include stdio.h时gcc会去/usr/include找头文件链接时去/usr/lib/x86_64-linux-gnu找libc.so。这些库文件都是x86架构编译出来的你的交叉编译器绝不能碰它们——它链接的libc必须是ARM版本的。所以交叉工具链会内置一个“虚根目录”叫 sysroot。arm-linux-gnueabihf-gcc的 sysroot 一般在/usr/arm-linux-gnueabihf里面有一套完整的ARM世界视图usr/include、usr/lib、lib都是ARM版本的库和头文件。编译时交叉编译器会自动去这个sysroot里找东西而不是去宿主机的/usr。自己做嵌入式开发板时芯片厂商给的SDK里通常也会带一个sysroot目录比如里面有arm-buildroot-linux-gnueabihf/sysroot。交叉编译时用--sysroot路径指过去就行。很多人交叉编译死活链不上库十有八九是sysroot没配对。2.3 顺带说一嘴模拟器与binfmt_misc交叉编译解决了“生成不同架构程序”的问题但它不解决“在x86上运行ARM程序”的问题。如果你在x86上直接执行ARM可执行文件内核只会甩给你一个“Exec format error”。但我们还是能在x86上跑ARM程序靠的是另一套机制QEMU用户态模拟 binfmt_misc。QEMU有一个用户态模式比如qemu-arm它能在一个普通进程里动态翻译ARM指令把ARM程序变成宿主CPU能执行的指令流。配合Linux内核的binfmt_misc模块内核看到ARM格式的ELF文件时会自动调用qemu-arm来执行它。这个用处在交叉编译的测试阶段特别大。你可以不把程序拷到板子上直接在x86上用qemu跑一下验证逻辑有没有问题。不过这只是测试手段别把性能期望值放太高——动态翻译的损耗通常是数量级的。3. 实操在x86 Linux上编译并运行ARM程序理论说了一大堆现在上手。下面我用一台x86_64的Ubuntu 22.04作为宿主分别做“编译ARM程序”和“在x86上运行ARM程序”两件事目标架构选 ARM 32位硬浮点armhf和 ARM 64位aarch64两种。3.1 环境准备与工具链安装Debian/Ubuntu系直接装工具链sudo apt update sudo apt install -y gcc-arm-linux-gnueabihf g-arm-linux-gnueabihf sudo apt install -y gcc-aarch64-linux-gnu g-aarch64-linux-gnu sudo apt install -y qemu-user qemu-user-static binfmt-support装完先验证一下工具链是否可用arm-linux-gnueabihf-gcc --version aarch64-linux-gnu-gcc --version这两行命令会打印编译器版本同时你能在输出里看到Target:这一项分别写着arm-linux-gnueabihf和aarch64-linux-gnu。这就是它俩的“目标产品线”。然后写一个简单的Hello程序#include stdio.h int main(void) { printf(Hello from ARM!\n); return 0; }分别编译arm-linux-gnueabihf-gcc hello.c -o hello_arm aarch64-linux-gnu-gcc hello.c -o hello_aarch64编译完成后用file命令查看产物file hello_arm file hello_aarch64你会看到类似下面的输出hello_arm: ELF 32-bit LSB executable, ARM, EABI5, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0 hello_aarch64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0这两行信息量很大。注意“dynamically linked”和“interpreter”两处——这说明程序是动态链接的运行时需要对应架构的动态链接器。而arm-linux-gnueabihf和aarch64-linux-gnu的编译器会默认将解释器路径写进可执行文件。这就是后续“在纯x86上跑不了”和“在qemu下能跑”的关键。3.2 从Hello World到工程化make/CMake交叉编译超小工程直接敲命令没问题但真正做项目交叉编译要配置到构建系统里。这里我给一个常用的CMake方案。先准备项目文件。CMakeLists.txtcmake_minimum_required(VERSION 3.16) project(cross_demo C) set(CMAKE_C_STANDARD 11) add_executable(demo main.c)main.c#include stdio.h int main(void) { printf(Cross compile with CMake\n); return 0; }然后写一个交叉编译工具链文件toolchain-arm.cmakeset(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g) set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)这里的核心是CMAKE_SYSTEM_NAME和CMAKE_SYSTEM_PROCESSOR告诉CMake“我们不是在给当前机器编译”然后CMAKE_C_COMPILER指定交叉编译器。后面三个CMAKE_FIND_ROOT_PATH_MODE_*是在告诉CMake找程序的时候到宿主机找比如find_package找构建工具但找库和头文件时只能搜索sysrootARM那个世界不然就会误用x86的库。构建时执行mkdir build-arm cd build-arm cmake .. -DCMAKE_TOOLCHAIN_FILE../toolchain-arm.cmake make file demo构建完之后demo就是ARM的可执行文件。在cmake输出里你会看到类似-- The C compiler identification is GNU 11.4.0这样一行重点是后面跟着的 “-- Check for working C compiler: .../arm-linux-gnueabihf-gcc” 等等确认它用的确实是交叉编译器。3.3 在x86上直接运行ARM程序QEMU用户态模拟不把程序拷到板子上怎么先跑一下验证我用qemu-user来跑。前面已经安装了qemu-user和binfmt-support在部分系统上安装之后内核会自动注册binfmt处理规则。先手动跑一下./hello_arm预期会报错cannot execute binary file: Exec format error。这个报错是内核拒绝的因为当前CPU是x86不认识ARM的ELF。然后加上qemuqemu-arm ./hello_arm如果工具链是动态链接的qemu-arm需要找到ARM版的动态链接器。Debian/Ubuntu上安装的libc6-armhf-cross会把ARM的ld放到/usr/arm-linux-gnueabihf/lib/ld-linux-armhf.so.3。为了简化也可以直接用刚才编译的静态版本试。先编译一个静态版arm-linux-gnueabihf-gcc -static hello.c -o hello_arm_static qemu-arm ./hello_arm_static输出Hello from ARM!这时候你实际上完成了“在x86电脑上运行ARM程序”的全流程。动态链接版本也能跑但需要确认qemu能找到正确路径。推荐用-L选项指定ARM的sysrootqemu-arm -L /usr/arm-linux-gnueabihf ./hello_armqemu-aarch64 -L /usr/aarch64-linux-gnu ./hello_aarch64注意qemu用户态模拟对浮点数、动态链接库的加载路径、堆栈布局这些细节有严格要求跑简单程序问题不大但跑复杂的GUI程序或依赖特殊设备的程序不如直接在板子上跑来得稳定。4. Windows上那些“见怪不怪”的跨架构场景说到x86编译ARM可不只是Linux的专利。Windows下搞嵌入式、安卓、车机应用的人更多很多“怪现象”其实都跟跨架构编译有关。4.1 Keil MDK与ARM Compiler 5.06/6做STM32、Cortex-M单片机开发的人对“ARM Compiler 5.06”“ARM Compiler 6”这些名字应该不陌生。Keil MDK跑在Windows上但编译出来的目标程序是跑在ARM Cortex-M内核上的裸机程序。Keil里用的编译器实际上是ARM官方出的armccARM Compiler 5或armclangARM Compiler 6。MDK是一个集成开发环境它本身是x86程序但编译器后端的目标处理器是ARM。这就是典型的交叉编译——你在x86 Windows上用Keil点一下编译出来的.axf、.hex、.bin都是ARM机器码。很多人问“为什么我换个ARM编译器版本编译结果就不一样”原因有两个一是不同版本的内核指令选择策略不同比如5.06偏向传统的ARM指令选择6基于LLVM后端优化策略完全不同二是5和6对C语言标准的支持、内嵌汇编的语法也不同。特别是从ARMCC 5.06迁移到ARMCLANG 6时旧的__asm内嵌汇编写法基本都要改成__asm__或者单独的函数属性声明。4.2 PowerShell提示npm.ps1无法加载别急着怪架构很多人在Windows上装完Node.js后执行npm命令会遇到一个莫名其妙的问题npm : 无法加载文件 d:\program files (x86)\nodejs\npm.ps1因为在此系统上禁止运行脚本。有关详细信息请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Policies。这个“无法加载”其实跟x86、ARM完全没关系是PowerShell的脚本执行策略默认禁用了.ps1脚本。npm在Windows下是一个.ps1包装脚本PowerShell出于安全考虑默认不会执行它。解决办法不是去换Node.js的ARM版或x86版而是执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser这个报错和“x86编译ARM”的联系在于很多人一看到Program Files (x86)就以为问题出在架构上其实只是路径带了个x86而已。类似的还有“dependencies工具x86下载”“Java JDK1.8.0_66 x86路径找不到”这些都得先分清到底是架构问题还是环境变量、执行策略、JDK模块化带来的坑。比如Maven编译报Missing class com.sun.image.codec.jpeg.JPEGCodec那不是ARM或x86的问题而是JDK 9以后把rt.jar里的内部类移到了java.desktop模块并删掉了这些旧类。你要做的不是换ARM JDK而是换图像处理库。4.3 镜像、JDK和工具链下载时怎么选ARM版随着ARM服务器如鲲鹏、Ampere等越来越常见很多开源中间件都开始提供ARM版下载。如果你在x86电脑上下载了一个ARM版的CentOS、麒麟系统的包想在本地x86上直接用大概率是不行的但如果你是想交叉编译某个组件那就需要搞清楚目标机器的架构。以JDK为例。x86_64的Windows对应jdk-11_x64_windows.zip而ARM 64的Windows对应jdk-11_arm64_windows.zip。如果你在树莓派ARM64 Linux上开发就选linux-aarch64版本。选错了只能说明目标平台搞错了不是“能编译”还是“不能编译”的问题。这里再顺势提一个概念ARM版的镜像包括容器镜像。在x86的Docker上拉取arm64v8/centos镜像你是无法直接运行的因为Docker镜像里的程序是ARM架构。但如果你注册了binfmt_misc并使用qemu-user-static作为模拟器Docker可以透明地在x86上运行ARM容器支持跨架构构建镜像。这就是很多CI/CD平台“在x86服务器上构建ARM容器镜像”的原理。实际交叉构建时还可以用docker buildx直接并行构建多架构镜像。5. 常见问题与排查技巧实录交叉编译最大的痛苦不是“编不出来”而是“编出来了跑不了”和“链接时各种找不到东西”。下面这些坑我基本都踩过一遍列出来供你排查时参考。5.1 编译产物在板子上报“无法执行二进制文件”场景你在x86编译出hello_arm拷贝到ARM板子上执行结果shell提示cannot execute binary file: Exec format error排查思路依次是先确认架构是否匹配file hello_arm看是ARM还是ARM aarch64和板子uname -m对比。注意32位ARM程序要在64位ARM内核上跑需要内核开启CONFIG_COMPAT支持一般发行版默认开。再确认浮点ABI是否匹配arm-linux-gnueabihf编译的硬浮点程序在只支持软浮点的板子上起不来。最简单的方式是用readelf -A hello_arm | grep Tag_ABI_VFP_args看到VFP registers就是硬浮点standard是软浮点。最后看动态链接器是否存在readelf -l hello_arm | grep interpreter查看解释器路径。ARM版的glibc路径一般是/lib/ld-linux-armhf.so.3如果板子文件系统里没有这个文件程序就起不来。这种场景下直接编译静态版本通常是最省事的选择。5.2 链接阶段找不到库交叉编译时#include xxx.h找得到但链接报cannot find -lxxx这基本是sysroot里没有对应的ARM库而不是“库路径没配置”。x86宿主机上的/usr/lib/x86_64-linux-gnu/libxxx.so对ARM交叉链接器没有任何意义它不会去搜索这个目录。多数时候你需要单独为ARM目标安装库的交叉编译版本。比如在Debian/Ubuntu上ARM版的库包通常叫libxxx-dev:armhf或libxxx-dev:arm64用dpkg的多架构机制安装sudo dpkg --add-architecture armhf sudo apt update sudo apt install libssl-dev:armhf装完后库文件会出现在/usr/arm-linux-gnueabihf/lib或/usr/lib/arm-linux-gnueabihf。如果你的工具链用的是自定义sysroot路径不在这条系统路径里就需要在CMake里手动指定搜索路径set(CMAKE_C_FLAGS --sysroot/path/to/your/sysroot) set(CMAKE_EXE_LINKER_FLAGS -L/path/to/arm/libs)注意不要轻易用-I和-L指向x86宿主机的/usr/include和/usr/lib交叉编译器用x86的头文件或库做出来的东西轻则编译期报类型不匹配重则链接出一个混合架构的程序运行时直接段错误。5.3 编译大型项目Qt、FFmpeg、Wails等时的三个坑大型项目的交叉编译坑主要集中在三个方面。第一configure阶段的架构探测。很多autotools项目的configure脚本会默认编译一个小程序并运行它用来检测系统特性。交叉编译时这种运行必然失败所以需要告诉它“我是在交叉编译”。通常用--hostarm-linux-gnueabihf、--buildx86_64-linux-gnu来区分。build是构建机host是运行目标。在CMake项目里类似的概念是CMAKE_CROSSCOMPILING变量交叉编译时CMake会把它设为TRUE许多模组依赖这个变量决定要不要运行本地探测程序。第二自带依赖的下载和构建。Qt、WebKit这类大项目会拉很多第三方依赖如果依赖库本身也要交叉编译得确保依赖库的工具链和目标一致不然会出现“主体是ARM依赖却是x86”的尴尬局面。最稳妥的方式是构建一个完整的交叉工具链环境比如用OpenEmbedded、Buildroot或ARM的交叉容器镜像把整个sysroot和工具链固定下来。第三目标运行时的文件路径和配置。很多程序编译时会在代码里写死前缀路径比如/usr/local交叉编译时如果你指定了--prefix/opt/myapp运行时也得在目标系统的这个路径下放置文件和库否则启动时会报“找不到共享库”或“找不到配置文件”。如果编译的是Go项目比如Wails、一些CLI工具交叉编译反而异常简单因为Go的交叉编译不依赖目标系统的库只需要设置环境变量GOOSlinux GOARCHarm64 go build -o myapp .但这只对纯Go代码有效一旦涉及cgo、依赖C库照样要走完整的交叉工具链。5.4 检查宿主架构与目标架构的几条命令日常开发中快速确认架构的命令建议收藏目的命令查看当前系统架构uname -m查看可执行文件架构file ./binary查看ELF头部信息readelf -h ./binary查看依赖的动态库readelf -d ./binary | grep NEEDED查看动态链接器路径readelf -l ./binary | grep interpreter查看ARM浮点ABI特性readelf -A ./binary | grep Tag_ABI_VFP_args查看程序里包含的架构字符串strings ./binary | grep -i armuname -m在x86_64机器上输出x86_64在ARM64机器上输出aarch64在ARM32机器上输出armv7l或armv8l取决于内核编译配置。Windows下可以用echo %PROCESSOR_ARCHITECTURE%或者 PowerShell 的$env:PROCESSOR_ARCHITECTURE命令行下跑32位进程时可能会显示x86跑64位进程显示AMD64这个区别要留意。另外提一句现代Windows系统在ARM硬件上能跑x86程序靠的是系统内置的仿真层这和交叉编译是两码事别混为一谈。6. 结尾一些来自实操的体会交叉编译这东西概念上搞通了剩下就是工具链和环境管理的问题。我自己做了几年嵌入式相关开发最大的体会是不要试图在宿主机上“攒”一套完美的交叉环境而是要用工程化的方式固定下来。Docker里放一个带交叉工具链、sysroot、构建脚本的镜像比每次手动apt install各种依赖要可靠得多。真遇到“我又没动代码怎么构建就挂了”的问题多半是宿主机某个库升级了或者sysroot被污染了。另外一个小技巧交叉编译的程序尽量在编译时就把-static留一个可选项。生成静态版本虽然体积大点但在目标板上跑起来几乎不会遇到动态链接器路径、共享库版本不匹配这些问题特别适合做启动阶段的最小验证。等到整个系统能跑了再换回动态链接来减体积、方便共享库更新。最后再说一句可能不太中听但很实在的话交叉编译能帮你解决“代码能不能在目标平台上跑”的问题但解决不了“目标平台上跑得好不好”的问题。性能、功耗、外设适配这些东西最终还是得回到真机上去调。x86编译ARM只是开发流程的第一步别指望它能替代你手里的开发板。
返回列表