ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ubuntu 20.04 ARM汇编踩坑:交叉编译工具链与权限问题详解

Ubuntu 20.04 ARM汇编踩坑:交叉编译工具链与权限问题详解

1. 写在前面:三个报错其实是一条路上的三个坑

如果你是刚接触嵌入式或者操作系统课程,大概率会在Ubuntu 20.04上尝试编译一段ARM汇编。我的实际情况就是这样:按照某本教材敲了个hello.s,然后依次执行arm-linux-objdump -d hello.s和bash hello.s,结果屏幕上蹦出来两条刺眼的报错——arm-linux-objdump: command not found和bash: hello.s: Permission denied。

先说结论:这两个报错不是同一个原因,但都是同一个根源——工具链没配好,文件没有一个正确的处理姿势。arm-linux-objdump属于交叉编译工具链,不是系统自带的原生objdump,需要单独安装;而bash hello.s报权限拒绝,是因为把汇编源码当成了可执行脚本来跑,或者当前目录下的文件没有执行权限。中间还夹了一个“手动安装binutils”的操作,很多人会绕远路去源码编译,其实完全没必要。

这篇文章就围绕这三个问题展开,把背后的原理、排查思路和完整解决步骤全部梳理清楚。适合的人群很明确:正在Ubuntu 20.04上做ARM汇编实验的学生、刚入门嵌入式交叉编译的开发者,以及任何被command not found折磨过的人。我会把每一步怎么查、怎么做、为什么要这样做都写明白。

2. 先摸清三个报错的真实含义

动手解决之前,必须先把报错本身吃透。很多人一看见command not found就去百度“怎么安装objdump”,结果装了半天还是不行,就是因为没搞清楚objdump和arm-linux-objdump的区别。

2.1 objdump到底是什么,系统里默认有吗

objdump是GNU binutils工具集里的一个重要成员,作用是把编译好的目标文件、可执行文件进行反汇编、查看节区信息、导出头部数据等。简单说,它是一个“文件解剖师”——你想知道一个二进制文件内部长什么样,用它就对了。

在x86架构的Ubuntu 20.04上,如果你只是敲objdump,大概率是能用的,因为系统默认安装了binutils。但注意,这个objdump是x86架构的,它只能解析x86的目标文件。你手头编译出来的hello.o是ARM指令集,用x86的objdump去解析,要么报错,要么输出一堆无法识别的数据。

这就是第一个关键点:arm-linux-objdump不是“objdump的ARM版本”,而是“运行在x86上、但目标是ARM的交叉工具”。它的前缀arm-linux-表示目标平台是ARM架构、Linux系统。这一类工具还有arm-linux-gcc、arm-linux-as、arm-linux-ld,全都在交叉编译工具链里。

2.2 “command not found”的常见误判

当我执行arm-linux-objdump -d hello.s得到command not found时,第一反应是“系统里没有objdump,我需要装一个”。当时我还真去手动下载了binutils源码,准备自己编译——这就是标题里“手动安装binutils”的由来。但后来发现这个动作大概率是多余的,而且容易踩坑。

command not found有三层可能:

  1. 命令真的没装,系统PATH里找不到对应的可执行文件。
  2. 命令装了,但安装路径不在PATH环境变量里,shell找不到。
  3. 命令名称不对,你以为叫arm-linux-objdump,实际安装后的名称是arm-linux-gnueabihf-objdump或者其他变体。

第三种情况在交叉编译工具链里极其常见。不同发行版、不同工具链版本,命令前缀的命名规则不一样。Ubuntu的软件源里有一个叫做binutils-arm-linux-gnueabihf的包,装完之后生成的命令是arm-linux-gnueabihf-objdump,而不是arm-linux-objdump。所以你即使装了包,敲原来的名字照样报command not found。

注意:交叉编译工具链的命令前缀不是统一的。arm-linux-、arm-linux-gnueabi-、arm-linux-gnueabihf-、aarch64-linux-gnu-都有可能出现,区别在于ABI调用约定(是软浮点还是硬浮点)以及目标架构位数。看到前缀先别慌,按我说的方式检查一下再动手。

2.3 Permission denied的真相:你在试图运行一个汇编源文件

第二个报错bash: hello.s: Permission denied就更好玩了。如果你敲的是bash hello.s,那你的本意是“用bash来解析并执行这个文件”。但hello.s是ARM汇编源代码,里面是一堆mov、ldr、str之类的指令,不是shell脚本。bash尝试把它当脚本解释,第一行如果是#include或者.global之类的汇编伪指令,bash根本不认识,行为不可预期。再加上文件本身没执行权限,bash会直接拒绝运行,于是弹出Permission denied。

或者你敲的是./hello.s,那就更直白了——你想让内核加载这个文件作为程序执行。但如果文件不是合法的可执行格式(没经过编译链接生成ELF文件),并且没有执行权限,内核会直接返回Permission denied或者Exec format error。

这里必须纠正一个观念:汇编源文件hello.s是一个文本文件,不是一个可执行程序。你不能像运行脚本或程序那样直接“运行”它。正确的流程是:

hello.s --arm-linux-gnueabihf-as--> hello.o --arm-linux-gnueabihf-ld--> hello(ELF可执行文件)

也就是说,必须先汇编、再链接,最后得到的hello才是能在ARM环境(或在QEMU模拟、或通过其他方式)运行的程序。而objdump反汇编的对象,通常是hello.o或者最终的hello,不是.s源文件本身。虽然objdump也能看源文件里的文本,但它的设计目标是处理二进制目标文件。

2.4 一条完整的报错链背后的逻辑

把三个报错连起来看,其实就是一个典型的“新手在Linux上做ARM汇编实验”的标准流程:

  • 写好了hello.s
  • 想反汇编看看指令长啥样 → 用arm-linux-objdump→ 命令不存在
  • 想直接运行 → 用bash hello.s→ 权限不足或格式错误

根本问题不是某个包缺失,而是“工具链没有完整搭建 + 对文件处理流程理解不到位”。这也解释了为什么网上杂七杂八的答案让你“安装binutils”“chmod +x”“改用qemu”都能沾点边,但都解决不了核心矛盾——因为每个人卡住的环节不一样。接下来我就按正确顺序,把每一步的排查方法和解决方案完整写出来。

3. 工具链排查与安装:别急着源码编译,先看系统里有什么

这一节是整个解决过程的重头戏。我们按“先检查、再安装、最后验证”的顺序来。

3.1 检查现状:确认架构、确认已装包、确认PATH

先打开终端,执行下面几条命令,把环境信息摸清楚。

uname -m

这行命令输出你当前系统的CPU架构。Ubuntu 20.04如果是常规安装,大概率是x86_64。这意味着本机原生工具链都是x86体系,ARM交叉工具链一个都不会有。

which objdump which arm-linux-objdump which arm-linux-gnueabihf-objdump

which的作用是告诉你命令在哪。如果objdump有输出,说明binutils已安装;后面两个没有输出,说明对应的交叉工具不存在。

dpkg -l | grep binutils

dpkg -l列出所有已安装的软件包,grep binutils过滤出和binutils相关的。这里会出现几种情况:

  • 只有binutils和binutils-common,说明你装的是x86版binutils,没有交叉版。
  • 有binutils-arm-linux-gnueabihf,说明交叉工具其实已经装了,你只是命令名不对。
  • 什么都没有,说明原生binutils都没装,需要先把基础包补上。

还有一个容易被忽略的检查项——PATH环境变量:

echo $PATH

看看输出里有没有/usr/bin、/usr/local/bin这类常规路径。交叉工具链默认装完都在/usr/bin下,只要这个目录在PATH里,命令基本都能找到。如果你的工具链是手动下载解压到某个自定义目录的,那就得手动把路径加进PATH,否则就会出现“明明装好了却还是command not found”的诡异情况。

3.2 安装方案选择:为什么优先推荐apt而不是源码编译

很多教程会让你去下载binutils源码包,然后 configure、make、make install三连。我当年也这么干过,但说句实在话——在Ubuntu 20.04上做ARM交叉调试,用源码编译binutils纯属给自己找麻烦。

原因有三点:

  1. 源码编译耗时长。binutils的configure和编译在普通机器上少说要好几分钟到十几分钟,中间还可能因为缺少依赖(如gmp、mpfr、libtextstyle等开发库)而失败。
  2. 版本过新未必兼容。源码里往往是最新版本,但你的其他工具(比如gcc、ld)可能停留在系统版本,版本不匹配会产生莫名其妙的链接错误。
  3. 维护麻烦。源码编译的binutils不归apt管理,以后卸载、升级都得手动处理,很容易留下环境垃圾。

而Ubuntu官方源里针对ARM交叉开发提供了一整套现成工具包,安装只需要一条apt install,版本与系统自带gcc匹配,卸载升级都方便。除非你是在做一个需要定制binutils特性的特殊项目(比如修改反汇编输出格式、给特定CPU架构打补丁),否则完全用不到源码编译。

实战心得:我后来查了一下自己当年的笔记,发现“手动安装binutils”的整个过程大概花了四十分钟,而用apt安装只需要三十秒。四十分钟里大部分时间都是在跟make报错搏斗。如果你只是需要arm-linux-objdump看反汇编,别再跳进源码编译的坑了。

3.3 具体执行:安装对应架构的binutils交叉工具包

现在分情况给出命令。注意区分你的目标架构和ABI:

情况一:目标平台是ARM 32位,硬浮点(绝大多数ARM开发板、树莓派默认)

sudo apt update sudo apt install binutils-arm-linux-gnueabihf

装完以后,可用的命令是arm-linux-gnueabihf-objdump、arm-linux-gnueabihf-as、arm-linux-gnueabihf-ld。验证:

arm-linux-gnueabihf-objdump --version

输出类似GNU objdump (GNU Binutils for Ubuntu) ...就说明成功了。

情况二:目标平台是ARM 64位

sudo apt install binutils-aarch64-linux-gnu

命令前缀是aarch64-linux-gnu-。

情况三:你必须要arm-linux-objdump这个名字

如果你手头的教材、脚本全是按照arm-linux-objdump这个名称写的,不想改,可以考虑装binutils-multiarch:

sudo apt install binutils-multiarch

这个包提供arm-linux-objdump这类通用命名的多架构binutils。不过我实测下来,这个包在Ubuntu 20.04上的可用性有时候不太稳定,部分子命令可能缺失或版本偏老。更推荐的做法是:装binutils-arm-linux-gnueabihf,然后给命令做一个软链接。软链接方法很简单:

sudo ln -s /usr/bin/arm-linux-gnueabihf-objdump /usr/local/bin/arm-linux-objdump

这样你再敲arm-linux-objdump,实际上就是调用arm-linux-gnueabihf-objdump,一劳永逸,而且不污染系统包管理。同理,需要arm-linux-as就再链接一个。

3.4 装完还提示 command not found?按这个顺序排查

装完包之后仍然报command not found,我整理了一个排查顺序,建议严格按步骤来:

  1. 先确认包确实装上了:dpkg -l | grep binutils-arm,看状态是不是ii(installed)。如果显示un或没有结果,说明安装失败,回看有没有apt报错。
  2. 确认命令实际名字:ls /usr/bin/ | grep objdump,看看系统里到底有什么前缀的objdump。这一步能直接终结“名字不对”的猜测。
  3. 确认命令所在路径是否在PATH里:如果你命令在/usr/bin/下,正常没问题;如果在自定义目录,就用export PATH=$PATH:/你的目录临时加进去,或者写进~/.bashrc。
  4. 检查是不是bash的hash缓存:有时候bash会缓存命令路径,导致新装的命令找不到。执行hash -r清除缓存再试。
  5. 终极测试:直接输命令的完整路径,比如/usr/bin/arm-linux-gnueabihf-objdump -d hello.o。能跑通,说明就是PATH或名称问题;跑不通,说明二进制本身有问题,可能要检查架构兼容性。

这套排查顺序我后来在很多机器上用过,基本能在五分钟内定位问题。

4. 汇编与链接实操:从 .s 到可执行文件的完整流程

工具链装好了,接下来就是解决hello.s的正确处理方式。很多人卡在这一步是因为搞不清.s、.o、可执行文件三者的关系。我用最直白的方式讲一遍。

4.1 源文件、目标文件、可执行文件,三者到底是什么关系

拿做饭来类比:

  • hello.s是菜谱,上面写的是原料和步骤(ARM指令)。
  • hello.o是备好的菜料,已经切好、配好,但还没下锅(汇编器把指令翻译成机器码,但还没分配好内存地址)。
  • hello(可执行文件)是出锅的菜,可以直接吃(链接器把目标文件组合起来,填充地址,生成可运行的ELF格式文件)。

Android里你没学过也没关系,只要记住:.s文件是给人看的文本,.o和可执行文件是给机器看的二进制。objdump主要分析的是后两者。

4.2 一个可直接照抄的完整示例

假设你的hello.s是正统的ARM汇编(比如在树莓派或QEMU里跑的那种),内容是经典的“Hello, World”。现在执行以下三步:

# 第一步:汇编 arm-linux-gnueabihf-as -o hello.o hello.s # 第二步:链接 arm-linux-gnueabihf-ld -o hello hello.o # 第三步:反汇编验证 arm-linux-gnueabihf-objdump -d hello

如果一切顺利,objdump会输出每个指令的地址、机器码和反汇编文本,类似这样:

00010054 <_start>: 10054: e1a00000 mov r0, #0 10058: e59f0014 ldr r0, [pc, #20] ...

看到这里,恭喜你,工具链已经完全跑通了。

这里有几个细节值得注意:

  • as是汇编器(assembler),对应binutils包里的arm-linux-gnueabihf-as。
  • ld是链接器(linker),对应arm-linux-gnueabihf-ld。
  • 如果你还装了arm-linux-gnueabihf-gcc,也可以直接一步到位:arm-linux-gnueabihf-gcc -o hello hello.s -static。gcc会自动调用汇编器和链接器,但实验教学里通常还是建议先用as和ld手动走一遍,把每一阶段发生了什么看清楚。

4.3 能直接“运行” hello.s 吗?不能,但你可以用这些方式运行编译产物

这里再强调一次:hello.s永远不能直接运行,bash hello.s本身就是错误用法。但是编译链接之后的hello能不能在x86的Ubuntu上直接跑?答案是不能,因为它是ARM指令集,x86 CPU没法原生执行。

那怎么验证程序逻辑对不对?有三种方式:

  1. 用QEMU用户态模拟:qemu-arm ./hello,适用于纯ARM指令、不依赖硬件外设的程序。
  2. 用QEMU系统级模拟:启动一个完整的ARM Linux虚拟机,把编译产物拷贝进去运行。
  3. 在真实ARM开发板上运行:通过NFS、TFTP或者U盘把文件传过去执行。

高校实验课最常见的做法是前两种。这里顺手提一句:如果你机器上装了qemu-user,执行sudo apt install qemu-user后,甚至可以把hello作为“普通程序”直接运行,qemu会在后端做指令翻译。

实操心得:很多教材里的ARM汇编程序其实是“裸机程序”,用ld -Ttext 0x00000000指定了加载地址,这种程序在QEMU用户态里不一定能跑,因为它没有Linux系统调用接口。所以如果你的hello是用ld直接链接出来的裸机格式,别急着怪qemu——先确认程序有没有依赖操作系统服务。

4.4 chmod +x 到底该不该用,什么时候才真正需要

我见过很多人遇到Permission denied时,第一个反应是chmod +x hello.s,然后继续./hello.s,结果冒出Exec format error。这个操作毫无意义,因为.s文本文件即使有了执行权限,也不是合法的可执行格式。

但chmod +x在什么场景下是真的有用呢?如果你写的是一个shell脚本test.sh,内容是#!/bin/bash开头,那么chmod +x test.sh && ./test.sh是完全正确的。关键区别在于:文件内容的第一行必须是指定解释器的shebang行,且文件本身能被对应解释器读取。

所以权限问题的正确解法不是给.s文件加执行权限,而是:把源文件汇编链接成真正的可执行文件,再考虑给那个可执行文件加权限。如果你用ld生成的hello提示Permission denied,那才需要chmod +x hello——因为这时的hello是合法ELF程序,只是文件权限位里没有执行位。

这个区分非常重要,我建议你把它抄在笔记里:

  • 文本源码(.s、.c、.sh):不需要执行权限,靠的是编译器/解释器去读取。
  • 编译产物(ELF可执行文件):需要读权限和执行权限,内核才能加载它。

4.5 一个容易被忽视的问题:32位库缺失

有些新手装完交叉工具链后发现arm-linux-gnueabihf-as可以跑,但一执行arm-linux-gnueabihf-ld就报类似error while loading shared libraries: libc6-i386.so.6: cannot open shared object file的错误。

为什么?因为binutils-arm-linux-gnueabihf这个包里的二进制程序是为i386架构构建的,它需要32位的动态库支持。而你的Ubuntu 20.04系统默认可能只安装了64位库。解决办法:

sudo apt install libc6-i386

有些旧教程还会提到lib32z1、lib32stdc++6,其实在20.04上libc6-i386基本就能覆盖大部分情况。装上之后再跑ld,问题就消失了。

这个问题不算高频,但一旦碰到特别隐蔽,因为报错信息不会直接说“你缺32位库”,而是绕弯子提示某个.so文件不存在。我的建议是看到.so相关的加载错误,先file一下当前可执行文件,再用ldd查看动态库依赖。

5. 手动编译binutils的备用方案与避坑记录

虽然不是首选,但万一你的环境特殊(比如公司内网没有apt源、需要定制binutils版本),手动编译这条路也得会走。这里把要点写清楚。

5.1 什么时候才真正需要走源码编译这条路

我在前面反复强调apt优先,但确实存在apt无法覆盖的场景:

  1. 你需要一个较新版本的binutils,Ubuntu源里版本偏旧。
  2. 你要给特定的CPU型号生成反汇编,默认版本不支持某些扩展指令集。
  3. 公司或学校的内网环境源不完整,但你可以拿到源码包离线编译。
  4. 你在搞交叉编译器for bare-metal(裸机),需要定制--target=arm-none-eabi这类配置选项。

只要命中其中一条,源码编译就是必要的。下面给一个最精简的编译流程。

5.2 以目标平台 arm-linux-gnueabihf 为例的源码编译步骤

先确保基础依赖齐全:

sudo apt install build-essential texinfo flex bison

texinfo是生成文档用的,不加会影响make流程;flex和bison是生成语法解析器用的,binutils源码需要它们来生成汇编器和反汇编器的解析代码。

然后下载源码。binutils的官方源码包可以从GNU官网镜像站获取,或者使用wget直接拉取:

wget https://ftp.gnu.org/gnu/binutils/binutils-2.40.tar.gz tar -xzf binutils-2.40.tar.gz cd binutils-2.40

接着创建独立的构建目录,不要直接在源码目录里编译:

mkdir build-arm cd build-arm ../configure --target=arm-linux-gnueabihf --prefix=/opt/arm-toolchain --disable-werror

这里两个参数很关键:

  • --target=arm-linux-gnueabihf:生成的工具链前缀就叫arm-linux-gnueabihf-。
  • --prefix=/opt/arm-toolchain:指定安装目录,避免装到系统路径污染全局环境。

--disable-werror的作用是把“警告当错误”关掉。老版本源码在较新的gcc下编译时,经常会有一些小警告,如果不关掉这个选项,make会直接失败,这一项能救很多命。

然后:

make -j4 sudo make install

编译过程少则几分钟多则十几分钟,取决于机器性能。装完之后,可执行文件在/opt/arm-toolchain/bin/下。别忘了把路径加进PATH:

export PATH=/opt/arm-toolchain/bin:$PATH

想永久生效,写到~/.bashrc末尾。

5.3 源码编译踩坑实录:我遇到过的三个问题

第一坑:make报ERROR: no makefile。原因是忘了先../configure,或者configure时当前目录不对。记住:必须在build-arm目录里执行../configure,不是在源码根目录里。这个设计是为了不污染源代码目录,很多新手在源码根目录里直接 configure 也能跑,但目录结构比较乱,后续升级不便。

第二坑:编译中途报cannot find -lz。原因是缺少zlib的开发库,执行sudo apt install zlib1g-dev再重新configure即可。

第三坑:make install提示权限不够。如果你没加sudo,那/opt/arm-toolchain目录你根本写不进去。但如果加了sudo,后续所有在这个目录里的操作都要注意权限归属。我的建议是make install不用 sudo,直接把--prefix指向自己的家目录,比如--prefix=$HOME/opt/arm-toolchain,省掉一堆权限问题。

实测心得:源码编译的binutils版本如果和系统gcc版本跨度太大,链接器在后续使用中可能会选择不同的默认格式。这个问题在实验环境里很少见,但一旦你开始做大型项目,建议还是回到apt版本,或者把工具链彻底独立到自定义目录,避免混用。

6. 常见报错排查速查表:一条命令对应一个坑

这里把我积累的典型报错整理成一个速查表,方便你以后直接查。

报错信息大概率原因解决动作
arm-linux-objdump: command not found交叉工具链未安装或命令名不对安装binutils-arm-linux-gnueabihf,或建立arm-linux-objdump软链接
arm-linux-gnueabihf-objdump: command not found包没装成功;或装了但PATH没包含检查dpkg状态;检查/usr/bin目录;加PATH
/usr/bin/arm-linux-gnueabihf-objdump: No such file or directory通常是动态库缺失或文件格式不对用file看文件格式;用ldd查依赖;装libc6-i386
bash: hello.s: Permission denied把汇编源码当可执行文件跑;文件无执行权限按汇编→链接流程生成ELF文件;不要直接运行.s
bash: ./hello: cannot execute binary file: Exec format error二进制是ARM格式,x86系统无法直接执行使用QEMU用户态模拟;或移植到ARM设备执行
arm-linux-gnueabihf-as: error: unrecognized option汇编源文件里有当前版本不支持的指令或伪指令检查CPU架构是否匹配;或更新binutils版本
undefined reference或file format not recognized链接时混用了x86的.o文件或库文件确认汇编和链接全用arm前缀工具,不要混用本地gcc产物

这张表覆盖了我能想到的绝大多数初学者会遇到的场景。如果你遇到的报错不在表里,请先执行file查看目标文件类型,再执行ldd查看依赖,最后用dmesg | tail查看是否有内核拒绝加载的记录——这三板斧基本能解决90%的诡异问题。

7. 最后的实操建议:把流程固化成一串命令

这篇文章写到这里,核心内容已经讲完了。但我想在最后再分享一个我觉得真正有用的习惯:把整个流程固化成一组命令,存成一个shell脚本或写进笔记,这样每次新开环境就不会再摸索一遍。

以我的个人习惯为例,我在一台新Ubuntu 20.04上搭建ARM汇编实验环境,会依次执行:

sudo apt update sudo apt install -y binutils-arm-linux-gnueabihf qemu-user sudo ln -sf /usr/bin/arm-linux-gnueabihf-objdump /usr/local/bin/arm-linux-objdump sudo ln -sf /usr/bin/arm-linux-gnueabihf-as /usr/local/bin/arm-linux-as sudo ln -sf /usr/bin/arm-linux-gnueabihf-ld /usr/local/bin/arm-linux-ld echo "export PATH=/usr/local/bin:\$PATH" >> ~/.bashrc

这套命令做完,arm-linux-objdump、arm-linux-as、arm-linux-ld三条命令全部可用,而且qemu-user保证编译产物能模拟运行。整个过程不超过一分钟。对比我当年手动编译binutils的四十分钟,差距非常明显。

还有一个容易被忽略的习惯:每次写完汇编源码,先跑arm-linux-as -o hello.o hello.s确认汇编阶段没有语法错误,再跑ld链接,最后才用objdump验证。不要跳过任何一步。因为如果汇编阶段就有错误,后面的链接和反汇编报错只会让你更抓狂。

另外建议给hello.s文件开头加上一行注释,写清楚它的目标架构和链接地址,比如/* ARM32, ld -Ttext 0x10054 */。别小看这个习惯,几个月后你翻回来看这段代码,会感激这一行字帮你省了大量回忆时间。

说实话,我在实际踩坑过程中的最大体会是:交叉编译环境的坑多数不是“技术深度”问题,而是“信息错位”问题——你不知道某个包装完以后命令叫什么,不知道某个文件该不该加执行权限,不知道查看反汇编的对象应该是.o而不是.s。这三件事每一件单拎出来都不难,但连在一起就容易让人绕圈子。希望这篇文章能帮你把这三个坑一次性填平,后面再遇到类似报错,对照着速查表查就行。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进