异架构相关
环境基础
qemu 仿真
QEMU(Quick Emulator) 是一个开源的通用虚拟化和仿真框架,该框架主要有如下分类:
qemu-user:提供用户态的简单仿真。适用于对一些相对简单程序的进行仿真。qemu-system:提供完整的系统级仿真。例如有些固件的运行需要一整套复杂的环境,单纯使用qemu-user仿真比较麻烦并且容易出问题,因此一个比较简单的方法是把整个 IOT 设备的文件系统扔到对应架构的一个完整虚拟机中然后chroot切换到 IOT 设备的文件系统的根目录进行仿真,此时需要使用qemu-system运行整个虚拟机。qemu-utils:qemu 的一些配套工具,比如制作磁盘镜像的qemu-img。
安装命令:
1 | sudo apt install qemu -y |
qemu-user
qemu-user 是 QEMU 的用户态仿真组件,用于在宿主机(例如 x86_64 Linux)上直接运行其他架构(如 ARM/MIPS/RISC-V)的用户态二进制程序。该组件只仿真用户空间指令,不包括系统级仿真,因而被仿真的二进制程序的系统调用由宿主机内核负责。
安装与使用
qemu-user 组件可以单独安装,安装命令如下:
1 | sudo apt install qemu-user qemu-user-static -y |
qemu-user 的基本命令格式如下:
1 | qemu-<arch> [options] <target_program> [args...] |
例如:
在 x86_64 上运行 ARM 二进制
1
qemu-arm ./hello-arm
在 x86_64 上运行 RISC-V 二进制并传递参数
1
qemu-riscv64 ./hello-riscv arg1 arg2
常用参数
qemu-user 支持一些特定参数:
-L <path>:指定目标程序依赖的根文件系统(sysroot)。适合动态链接二进制。
例如:1
qemu-arm -L /path/to/armfs ./hello-arm
注意
有些依赖特定动态库和特定路径的固件程序,需要通过
chroot将根目录切换到解压出的文件系统根目录后再使用qemu-user仿真程序。但如果使用的是动态链接的
qemu-user,在chroot后可能会因为无法找到 qemu-user 自身依赖的动态库 而导致仿真失败。
此时应使用 静态编译版本 的qemu-user-static。-strace:输出系统调用日志,类似于strace,可用于观察程序的系统调用行为。1
qemu-arm -strace ./hello-arm
-g <port>:开启 GDB 远程调试接口(gdbserver),用于指令级调试。1
qemu-arm -g 1234 ./hello-arm
-cpu <model>:指定仿真的 CPU 型号。1
qemu-arm -cpu cortex-a9 ./hello-arm
-E VAR=value:向目标程序注入环境变量。-d in_asm,cpu:同时输出指令与寄存器状态。-d in_asm:输出执行过的指令反汇编。-d cpu:输出 CPU 状态(包括寄存器)。
binfmt
在 Linux 内核中,binfmt(binary format)机制负责决定“某个可执行文件被执行时应由谁来解释 / 运行”。通过配置 binfmt,我们可以“透明”地执行异架构的二进制程序,而不用显式的通过 qemu-user 执行。
这一能力的核心子系统是 **binfmt_misc**。加载 binfmt_misc 模块后,内核会在 /proc/sys/fs/binfmt_misc/ 目录下暴露一组伪文件,用户空间通过向这些文件写入配置即可把“文件格式 → 解释器”映射关系注册给内核。
qemu-user 在安装时会在 /proc/sys/fs/binfmt_misc/ 路径下添加 binfmt_misc 相应的配置文件。因此我们直接运行异架构程序 Linux 会选择正确的 qemu-user-static 程序运行。
1 | ➜ ~ cat /proc/sys/fs/binfmt_misc/qemu-arm |
然而对于动态链接的程序,qemu 可以正常加载程序,但是动态库却会默认使用本机的动态库导致程序崩溃,因此需要 -L 参数指定 ld 的前缀径。另外如果将交叉编译工具链添加到对应的 qemu-binfmt,则 qemu-user 在运行程序时能加载正确的动态链接库,不需要指定路径。
1 | sudo mkdir /etc/qemu-binfmt |
qemu-system
qemu-system 仿真需要提供系统内核和文件系统,我们可以在这个网站下载所需结构的内核和文件系统。
qemu-system 启动命令如下,具体启动参数还要参考镜像对应的 README 作相应的调整。
1 | qemu-system-arm \ |
-M malta:主板类型,参考内核镜像后缀。-kernel vmlinux-3.2.0-4-4kc-malta:内核镜像。-hda debian_wheezy_mipsel_standard.qcow2:虚拟磁盘镜像。-initrd initrd.img-3.2.0-4-versatile:一个临时的文件系统映像,用于在Linux系统引导过程中提供必要的文件和工具,它通常用于初始化和准备真正的根文件系统之前。-append root=/dev/sda1 console=tty0: 内核启动参数。-net nic:添加一个网络接口卡(NIC)到模拟器中,以实现网络功能。-net tap,ifname=tap0,script=no,downscript=no:添加一个 TAP 设备,并将其与模拟器中的网络接口卡关联起来,用于与主机的网络进行通信。-nographic:禁用图形界面输出,只使用命令行界面进行连接和操作。
为了能够与 qemu 虚拟机通信,需要再 qemu 虚拟机启动先在外部主机创建并配置网卡 tap0 。
1 | sudo apt install uml-utilities |
qemu 虚拟机启动后需要再虚拟机中分配 ip 。
1 | ifconfig eth0 192.168.2.2/24 |
之后就可以通过 scp 向虚拟机传文件或者 ssh 登录虚拟机。
1 | sudo apt install sshpass |
crosstool-ng 交叉编译
crosstool-ng 是一个 跨平台交叉编译工具链自动生成器。它通过一套脚本 + Kconfig 菜单界面,自动下载、打补丁、配置并编译 Binutils、GCC、C 库及调试工具,最终生成可重复使用的交叉编译环境(<triplet>-gcc、<triplet>-gdb 等)。
对于一些简单情形的交叉编译,直接下载对应的交叉编译工具链即可。
1
2
3
4
5
6
7
8
9
10
11
12 sudo apt install gcc-arm-linux-gnueabi g++-arm-linux-gnueabi -y
sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu -y
sudo apt install gcc-mips64el-linux-gnuabi64 g++-mips64el-linux-gnuabi64 -y
sudo apt install gcc-mips-linux-gnu g++-mips-linux-gnu -y
sudo apt install gcc-mipsel-linux-gnu g++-mipsel-linux-gnu -y
sudo apt install gcc-mips64-linux-gnuabi64 g++-mips64-linux-gnuabi64 -y
sudo apt install gcc-powerpc-linux-gnu g++-powerpc-linux-gnu -y
sudo apt install gcc-powerpc64-linux-gnu g++-powerpc64-linux-gnu -y
sudo apt install gcc-riscv64-linux-gnu g++-riscv64-linux-gnu -y
sudo apt install gcc-alpha-linux-gnu g++-alpha-linux-gnu -y
sudo apt install gcc-s390x-linux-gnu g++-s390x-linux-gnu -y
sudo apt install gcc-sparc64-linux-gnu g++-sparc64-linux-gnu -y
crosstool-ng 安装
Ubuntu 22.04 安装构建依赖
1 | sudo apt update |
下载并安装 crosstool-ng(编译在用户目录,避免污染系统)
1 | mkdir -p ~/toolchains/src |
建议统一用这些目录:
1 | ~/work/ |
编译工具链
为什么使用 uclibc
无论你写的 main() 多简单,ELF 程序真正的入口是 _start(来自 crt1.o / Scrt1.o 之类的启动对象),它会调用 glibc 的 __libc_start_main(),然后 glibc 会在调用 main() 之前做一堆初始化,例如:
- 解析内核传来的 auxv(auxiliary vector)、环境变量等
- 初始化 TLS / 线程库(NPTL) 的最小运行时
- 初始化 libc 内部的一些锁、malloc、locale(取决于配置)
- 以及非常关键的:做“kernel too old”检查(内核太旧就拒绝启动)
这些动作发生在 main() 之前,所以“还没进 main 就崩”的现象。
glibc 在构建时会把“最低支持的内核版本”编码进去(通过 --enable-kernel=...),并据此在代码里假设某些内核特性一定存在,从而删掉对更老内核的兼容分支。glibc-help 邮件列表里说得很直接:
每个使用某个版本 glibc 编译出来的二进制,都期望某个内核版本或更新;最低内核版本在 glibc 构建时通过
--enable-kernel编码进去,这允许 glibc “期待” 某些内核特性存在。
也就是说:在 Ubuntu 22.04 上拿到的 glibc(或者你用 ct-ng 构出来的 glibc)很可能最低内核版本已经是 3.2 或更高。把它静态链接进程序之后,程序运行时就等同于“带着一份要求更高内核的 libc 一起跑”,在 2.6.32 上自然会出事。
man7.org(TLPI 的 API changes 文档)明确写过:从 glibc 2.24 起,除 i386/x86-64 例外,大多数架构的最低内核要求是 Linux 3.2;而 i386/x86-64 的最低内核可以是 2.6.32。
为什么实际运行时有时候是段错误,有时候会看到
FATAL: kernel too old?glibc 对“当前内核版本”的获取方式在动态/静态场景不同:
- 动态链接的 glibc:优先从 vDSO 的 ELF note 读
LINUX_VERSION_CODE- 静态链接的程序:使用
uname()解析utsname.release(失败才可能走 /proc 回退)- 动态链接器如果找不到 vDSO,也会回退到
uname()- glibc 用这个版本号做两件事:
1)自己的 “kernel too old”拒绝启动检查
2)决定是否跳过加载 ABI tag 要求更高内核版本的 DSO因此:
- 动态场景:经常会非常清晰地报
FATAL: kernel too old- 静态场景:也会做检查,但由于启动路径、输出时机、异常表现差异,有时你看到的是“还没到 main 就崩/段错误/异常退出”
(尤其在嵌入式场景里,stderr/控制台、init 环境、甚至某些异常处理路径不完善时,你看到的现象可能不像 PC 上那么“友好”。)
uClibc/uClibc-ng 主要面向嵌入式、老内核、资源受限环境,通常不会像现代 glibc 那样把最低内核版本抬那么高,也更倾向于保留老 syscall/老行为的兼容路径。因此用 ct-ng 做 uClibc 工具链时,本身就会配合你选的老 kernel headers,生成一套“更像目标系统时代”的用户态 ABI。所以同样一个 -static,uClibc 更可能在 2.6.32 上正常启动并进入 main()。
x86_64-uclibc 工具链
这里的核心是:先生成 uClibc-ng 工具链,然后用这个工具链分别编 BusyBox/strace/gdbserver。
1 | mkdir -p ~/work/toolchains/ctng-x86_64-uclibc |
进入 ct-ng menuconfig,确认关键点:
Target options —>
- Target Architecture (x86)
- Bitness: (64-bit)
Operating System —>
Version of linux (2.6.32.71)
都用 uclibc 编译了,肯定是需要在老版本的 linux 上跑的程序。
C-library —>
- libc: uClibc-ng(建议 1.0.43)
- Add support for locales
- Enable iconv
Companion libraries —>
- Build local libiconv
其它按默认即可
然后构建:
1 | ct-ng build |
最终工具链一般会在:
1 | ls ~/x-tools/ | grep x86_64 |
假设安装到了:
1 | ~/x-tools/x86_64-unknown-linux-uclibc/ |
设置变量:
1 | export UCLIBC64_TC="$HOME/x-tools/x86_64-unknown-linux-uclibc" |
i686-uclibc 工具链
同理:
1 | mkdir -p ~/work/toolchains/ctng-i686-uclibc |
设置变量:
1 | export UCLIBC32_TC="$HOME/x-tools/i686-unknown-linux-uclibc" |
编译项目
busybox
BusyBox 默认启用了几个 applet/feature,它们会调用 getrandom / setns / syncfs 这三个接口;但如果目标环境是 Linux 2.6.32 + uClibc-ng 静态库,这三者在你的组合里要么 内核根本没有该 syscall,要么 uClibc-ng(静态 libc.a)没有提供对应符号,于是最终链接阶段直报错。
对应的链接错误是:
seedrng_main: undefined reference togetrandomBusyBox 的
miscutils/seedrng.c里直接调用getrandom()(不是syscall()),所以 只要 libc 没提供getrandom符号,静态链接必失败。另外,
getrandom()这个 syscall 在 Linux 3.17 才出现;如果目标内核是 2.6.32,即使你“强行”把它链接过去,运行时也只能走ENOSYS路径(BusyBox seedrng 里面确实有ENOSYSfallback),但前提是要先能链接成功。并且 Buildroot 专门有个补丁提到一个典型坑:在 uClibc 场景里,
getrandom()可能“声明了能编过”,但最终链接时发现 libc 并没有定义它。nsenter_main: undefined reference tosetnsBusyBox 的
util-linux/nsenter.c,它是靠setns()进入别的进程 namespace 的。setns()在 Linux 3.0 才引入;目标内核 2.6.32 根本没有这个 syscall。sync_common: undefined reference tosyncfsBusyBox
coreutils/sync.c源码中有说明:FEATURE_SYNC_FANCY(启用sync -d / -f)“requires syncfs(2) in libc”,并且代码里会直接调用syncfs(fd)。而
syncfs()这个在 Linux 2.6.39 才出现;目标内核 2.6.32 没有。虽然可以保留
sync命令,但必须关掉FEATURE_SYNC_FANCY(也就是去掉-d/-f那些花活),这样 BusyBox 的sync就只会调用老的sync(),能在 2.6.32 上正常工作。
因此要想正常编译必须:
- 关掉:
CONFIG_SEEDRNG(解决getrandom) - 关掉:
CONFIG_NSENTER(解决setns) - 关掉:
CONFIG_FEATURE_SYNC_FANCY(解决syncfs,但保留sync基本功能)
64 位:
1 | cd ~/work/src/busybox-1.36.1 |
32 位:
1 | cd ~/work/src/busybox-1.36.1 |
strace
下载 strace 源码:
1 | cd ~/work/src |
如果直接用 glibc 编译则不需要 crosstool-ng。直接编译即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 # 建议先看 configure 支持哪些选项(博客里可以贴一行结果)
./configure --help | egrep -i 'stacktrace|unwind|dw|mpers' || true
# 全功能配置:mpers + stacktrace
# 说明:mpers 在 x86_64 上可以让 strace 跟踪 32-bit 进程(需要 multilib 环境)
./configure \
CFLAGS="-O2 -g" \
--enable-mpers \
--enable-stacktrace \
--with-libunwind \
--with-libdw
make -j"$(nproc)"
mkdir -p ~/work/out/glibc-x86_64
cp -a strace ~/work/out/glibc-x86_64/
# 验证
~/work/out/glibc-x86_64/strace -h | grep -- '-k' || true
file ~/work/out/glibc-x86_64/strace32 位:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 cd ~/work/src/strace-5.10
make distclean 2>/dev/null || true
./configure \
CC="gcc -m32" \
CFLAGS="-m32 -O2 -g" \
LDFLAGS="-m32" \
--disable-mpers \
--enable-stacktrace \
--with-libunwind \
--with-libdw
make -j"$(nproc)"
mkdir -p ~/work/out/glibc-i686
cp -a strace ~/work/out/glibc-i686/
~/work/out/glibc-i686/strace -h | grep -- '-k' || true
file ~/work/out/glibc-i686/strace
交叉编 libunwind。
1 | cd ~/work/src |
编译 strace:
1 | cd ~/work/src/strace-5.10 |
gdbserver
首先下载 gdbserver 源码:
1 | cd ~/work/src |
如果直接用 glibc 编译则不需要 crosstool-ng。直接编译即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 mkdir -p ~/work/build/gdbserver-glibc-x86_64
cd ~/work/build/gdbserver-glibc-x86_64
# 只构建 gdbserver:避免 GMP/MPFR(那是 gdb 本体才需要)
~/work/src/gdb-17.1/configure \
--disable-gdb \
--disable-werror
make -j"$(nproc)" all-gdbserver
# 找到产物
GDBSERVER_BIN="$(find . -type f -name gdbserver | head -n 1)"
echo "gdbserver: $GDBSERVER_BIN"
file "$GDBSERVER_BIN"
mkdir -p ~/work/out/glibc-x86_64
cp -a "$GDBSERVER_BIN" ~/work/out/glibc-x86_64/gdbserver32 位为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 sudo dpkg --add-architecture i386
sudo apt update
sudo apt install -y \
gcc-multilib g++-multilib \
libc6-dev:i386 \
libunwind-dev:i386 \
libdw-dev:i386 libelf-dev:i386
mkdir -p ~/work/build/gdbserver-glibc-i686
cd ~/work/build/gdbserver-glibc-i686
# 这里属于“在 x86_64 上交叉构建 i686 程序”
CC="gcc -m32" CXX="g++ -m32" \
CFLAGS="-m32 -O2 -g" CXXFLAGS="-m32 -O2 -g" LDFLAGS="-m32" \
~/work/src/gdb-17.1/configure \
--host=i686-pc-linux-gnu \
--build="$(gcc -dumpmachine)" \
--disable-gdb \
--disable-werror
make -j"$(nproc)" all-gdbserver
GDBSERVER_BIN="$(find . -type f -name gdbserver | head -n 1)"
file "$GDBSERVER_BIN"
mkdir -p ~/work/out/glibc-i686
cp -a "$GDBSERVER_BIN" ~/work/out/glibc-i686/gdbserver
gdbserver configure 明确要求 iconv,如果uClibc-ng 工具链里没开 iconv(CT_LIBC_UCLIBC_LIBICONV 没开)则需要给每套工具链各编一份静态 GNU libiconv 到 staging。
1 | cd ~/work/src |
32 位对应为:
1 | mkdir -p ~/work/staging/uclibc-i686 |
由于 uclibc 与 thread_db 存在兼容性问题,因此我们在编译前需要禁用 thread_db 相关功能。
1 | cd ~/work/src/gdb-17.1 |
最后构建:
1 | mkdir -p ~/work/build/gdbserver-uclibc-x86_64 |
ARM
ARM 架构体系
ARM的架构体系可以分为三个层次:
- 架构版本(ISA) :这是 ARM 处理器的核心指令集架构,决定了处理器如何执行指令、如何与内存交互。
- 核心版本(Core) :基于架构,ARM 会设计具体的处理器核心,比如 Cortex 系列、Neoverse 等。
- 平台和产品系列 :ARM 设计的不同平台解决方案,比如面向移动设备的平台,面向服务器的 Neoverse 平台等。
ARM 架构版本(ISA)
ARM的架构版本是ARM处理器指令集的基础,从 ARMv4 开始,到现在的 ARMv9 ,ARM经历了几个大的变革:
| 架构版本 | 发布年份 | 特点 | 代表性产品 |
|---|---|---|---|
| ARMv4 | 1994 | 初代架构,支持32位指令集,主打低功耗嵌入式应用。 | ARM7TDMI |
| ARMv5 | 1999 | 加入了增强指令集,优化了性能,支持更高效的指令执行。 | ARM9 |
| ARMv6 | 2001 | 引入了Thumb指令集(16位指令,减小代码体积),支持更复杂的缓存系统。 | ARM11 |
| ARMv7 | 2005 | 分为A/R/M三种子架构,支持硬件虚拟化、浮点运算、SIMD等功能,支持Thumb-2(16/32位混合)。 | Cortex-A8, Cortex-A9 |
| ARMv8 | 2011 | 支持64位架构(AArch64),引入了64位寄存器和指令,提升了性能和寻址能力,同时保持向下兼容32位。 | Cortex-A53, Cortex-A57 |
| ARMv9 | 2021 | 强化了AI加速、安全(TrustZone增强)和虚拟化功能,同时优化了指令集。 | Cortex-A510, Cortex-A710, Neoverse V1 |
其中 ARMv8 架构在引入 64 位计算时,增加了 AArch 作为区分不同指令集架构模式的标识,提出了 AArch32 和 AArch64 两个明确的模式:
- AArch32:保持与 ARMv7 及之前架构兼容,支持 32 位程序执行,且继续支持 Thumb模式(16位指令)和 ARM模式(32位指令)。
- AArch64:是 ARMv8 引入的 64 位指令集,处理器支持 64 位寄存器(X0-X30),支持更大的寻址空间,且指令集被简化为统一的 32 位宽度指令(没有 Thumb 模式)。
ARM核心版本(Core)
ARM 设计了多个处理器核心系列,用于满足不同的应用需求。每个核心基于一个架构版本,提供了不同的性能、功耗、特性。
| 核心系列 | 说明 | 代表核心 |
|---|---|---|
| Cortex-A系列 | 高性能,面向应用处理器,支持运行操作系统(Linux/Android等),适用于手机、平板、笔记本等设备。 | Cortex-A7, Cortex-A9, Cortex-A53, Cortex-A72, Cortex-A76, Cortex-A78 |
| Cortex-R系列 | 实时处理器,强调低延迟和高可靠性,广泛用于汽车、工业控制、实时系统等领域。 | Cortex-R4, Cortex-R5, Cortex-R7 |
| Cortex-M系列 | 超低功耗微控制器,适用于嵌入式系统、物联网、传感器控制等。 | Cortex-M0, Cortex-M0+, Cortex-M3, Cortex-M4, Cortex-M7, Cortex-M33 |
| Neoverse系列 | 针对数据中心、云计算、服务器市场设计,优化了AI和高吞吐量任务。 | Neoverse N1, Neoverse V1 |
ARM 平台和产品系列
ARM 不仅设计了核心,还根据市场需求推出了各种完整的系统平台解决方案。这些平台集成了 ARM 处理器、GPU、内存、I/O 等组件,形成一个完整的 SoC(System-on-Chip)方案。
| 平台 | 说明 | 代表平台/产品 |
|---|---|---|
| ARMv7-A/ARMv8-A平台 | 移动和消费电子产品,支持高性能处理器、图形、音视频处理等。 | Apple A系列,Qualcomm Snapdragon |
| Neoverse平台 | 针对数据中心、云计算、边缘计算等市场设计,提供高性能的ARM处理器,优化了AI计算、并行处理和高吞吐量。 | AWS Graviton,Ampere Altra |
| Automotive平台 | 针对汽车电子、自动驾驶提供优化的处理器和系统方案。 | ARM Cortex-A系列在车载系统中的应用 |
| Arm Total Compute平台 | 针对智能终端设备(手机、平板、嵌入式设备)提供的完整系统平台,集成CPU、GPU、AI加速器等。 | Arm’s solution for mobile devices |
ARM32
交叉编译
汇编基础
寄存器
通用寄存器
| 编号 | 约定名 | 典型角色(AAPCS/EABI 主流) | 保存约定 |
|---|---|---|---|
| r0–r3 | — | 参数 1–4、返回值(r0;64 位返回用 r0–r1),临时值 | 调用者保存(caller‑saved) |
| r4–r11 | —(r11 常作 FP) | 被调方可保留使用的通用寄存器;有的编译器把 r11 当 帧指针 FP | 被调保存(callee‑saved) |
| r12 | IP(Intra‑procedure scratch) | 过程内临时寄存器;链接器 veneer/PLT 经常用它做跳板/中转 | 调用者保存 |
| r13 | SP | 栈指针(各异常模式多数都有独立的 r13,见 1.3) | 特殊 |
| r14 | LR | 返回地址(BL/BLX 写入);函数若继续 call 他人应先入栈保存 |
特殊 |
| r15 | PC | 程序计数器。读到的是“当前指令地址 + 偏移”(ARM≈+8,Thumb≈+4);写入=跳转/切态 | 特殊 |
r9(SB/平台寄存器):AAPCS 指定 r9 可作为“平台寄存器”,常被某些平台/工具链拿来充当 GOT/小数据静态基(SB) 或 TLS 指针(具体由平台约定)。在很多 Linux/EABI + 现代 GCC/Clang 的 PC 相对/PIC 代码里,r9 可能空闲,但也可能被用作 GOT 基址。
程序状态寄存器 CPSR
条件标志:
N(负),Z(零),C(进位),V(溢出),Q(饱和)。控制位:
T(Thumb 状态标志;1=Thumb,0=ARM);E(端序;1=BE,0=LE,平台固定/受控);A/I/F(禁用异步中止/IRQ/FIQ);GE[3:0](SIMD 比较分组结果);IT域(Thumb 的IT块状态);M[4:0](处理器模式位)。
访问:
MRS/MSR CPSR;异常模式还可MRS/MSR SPSR。
浮点/向量寄存器(VFP/NEON)
VFP/NEON 统一寄存器文件:
- Q0–Q15(128 位)↔ D0–D31(64 位)↔ S0–S31(32 位)三套视图互相重叠。
- 有的实现只有 D0–D15(VFPv3‑D16)。
硬浮点 ABI(armhf)保存约定(AAPCS):
- 被调保存:
D8–D15(即S16–S31/Q4–Q7涉及到的片段)。 - 其余(
D0–D7、D16–D31)调用者保存。
- 被调保存:
参数/返回:硬浮点下,浮点参数优先走 s/d/q;返回用
s0/d0(或q0某视图)。
汇编指令
调用约定
参数寄存器:
r0–r3;多余参数走栈(按 4 字节对齐)。返回:
r0(64 位放r0–r1;结构体可能经隐藏指针)。易失/保存:
- caller‑saved:
r0–r3、r12;VFP 的D0–D7、D16–D31; - callee‑saved:
r4–r11(以及 VFP 的D8–D15)。
- caller‑saved:
栈:满递减(向低地址生长),跨调用点需 8 字节对齐。
系统调用(Linux):
r7 = NR;r0..r6 = args;svc 0(返回在 r0,负值为 -errno)。Thumb 下 r7:某些编译器把 r7 用作 帧指针;若手写 syscall(
svc),要确保不与编译器的 r7 用途冲突(常用 内联汇编约束 或禁用 FP)。
其它特性
漏洞相关
shellcode
gadget
ARM64
寄存器
AArch64 共有 31 个 64 位通用寄存器:x0–x30(32 位别名为 w0–w30),外加**栈指针 sp**(有 wsp 别名)与不可直接寻址的 pc。
通用寄存器
| 范围 | 名称 | 角色(要点) |
|---|---|---|
x0–x7 |
参数/返回寄存器 | 函数前 8 个整型/指针参数与返回值使用;caller-saved(被调用者可破坏)。 |
x8 |
间接结果位置寄存器 (IRL) | 当返回“大对象/结构体”时,调用者把返回缓冲区地址放在 x8 传给被调函数;此外在 Linux 系统调用里,x8 还装系统调用号(与 C 调用约定无关)。 |
x9–x15 |
临时(scratch) | caller-saved,函数内部临时值。 |
x16–x17 |
IP0 / IP1 | “过程内调用临时寄存器”,可被链接器的跳板/PLT veneer使用;在普通代码里也可做临时,但要知道随时会被尾调用/跳板改写。caller-saved。 |
x18 |
平台寄存器 | AAPCS64 保留给平台约定:如果平台需要跨调用携带状态(如线程上下文),就用 x18;否则可作临时。可移植汇编最好避免使用。在 Apple 平台强制保留(不要用),Windows/ARM64也保留;Linux/ARM64通常当普通 caller‑saved 使用。 |
x19–x28 |
callee‑saved | 被调用者必须在返回前恢复它们(64 位全宽必须保留)。常用于保存长期活跃的局部变量/指针。 |
x29 |
FP(帧指针) | AAPCS64 允许作为通用 callee‑saved 或维持帧链;Apple 平台要求始终保持有效帧记录(便于回溯)。 |
x30 |
LR(链接寄存器) | BL/BLR 写返回地址到 x30;RET 从 x30 返回。调用者如需保留需入栈。 |
SP |
栈指针 | 16 字节对齐;只能在有限指令形态中参与(如 ADD/SUB (imm) 调整栈,或做访存基址)。**任何通过 SP 的访存都要求 SP % 16 == 0**。 |
XZR/WZR |
零寄存器 | 读恒为 0,写被丢弃;可作为算术“把结果丢弃/与 0 运算”的操作数。注意:编码上与 SP 复用寄存器号 31,具体解释取决于指令语境。 |
PC |
程序计数器 | 不可当通用寄存器直接读写;PC 相对地址由 ADR/ADRP 计算。 |
32 位别名(w0–w30)写入会把对应的 xN 高 32 位清 0(零扩展语义),很多时候你只需对 wN 写就相当于完成了零扩展。
x16/x17 可能被 PLT/Veneer 暂用,不要指望跨函数调用保存其内容(哪怕你没显式用它们)。
汇编指令
调用约定
C 调用:调用者把返回缓冲区地址放入 x8,被调用者把结果写回该地址后以“void”返回;这是 AAPCS64 的间接结果位置寄存器规则。
Linux 系统调用:参数 x0–x5,系统调用号在 x8,返回在 x0,通过 svc #0 进入内核。
1 | stp x29, x30, [sp, #-16]! |
MIPS
MIPS 架构体系
MIPS 的架构体系至少有三层:
- ISA/架构版本:例如 MIPS I/II/III/IV/V,或 MIPS32/64 Release 1…6(常写作 MIPS32R2、MIPS64R6 等)。这是“指令、寄存器、异常/特权语义”的标准。
- 具体 CPU 内核/微架构实现:例如 4Kc、24K、74K、I6400…它们“实现了哪个 ISA/哪些扩展”,以及流水线/Cache/乱序等实现细节。
- ABI:例如 o32 / n32 / n64 / EABI,它决定“C 语言类型大小、参数怎么传、栈怎么对齐、返回值怎么放、系统调用/动态链接怎么约定”。
MIPS 架构版本(ISA)
早期“代际”版本:MIPS I ~ V(历史线)
- MIPS I:最早的经典 MIPS(很多教材里的 MIPS 指的就是这一类语义:load/store、固定 32 位指令、分支延迟槽等)。
- MIPS II / III / IV / V:逐步加入更多指令与 64 位能力(其中 MIPS III 开始引入 64 位寻址/指令语义,是早期 64 位 MIPS 的重要节点)。
这条线今天更多用于理解历史与兼容性。
现代标准线:MIPS32 / MIPS64 Release 1 ~ 6(主流线)
这是后来为了嵌入式与标准化而整理出来的“发布版”体系,常见写法:
- MIPS32R1 / R2 / R3 / R5 / R6
- MIPS64R1 / R2 / R3 / R5 / R6
其中 Release 6(R6)很关键:它引入了一组“无延迟槽(no delay slot)的 compact branch”分支家族,同时也移除了/调整了不少旧指令编码与语义(所以 R6 与 pre-R6 在二进制兼容、汇编指令集上经常需要特别区分)。
MIPS 的 ABI 类型
ABI 主要解决三件事:
- 数据模型:C/C++ 类型大小(int/long/pointer 等)
- 调用约定:参数/返回值用哪些寄存器、栈怎么布局与对齐、哪些寄存器需保存
- 对象文件/动态链接:ELF 标志位、重定位、PLT/GOT 约定等(工具链和系统决定)
MIPS 有三个最常见 ABI:
- o32(O32)
- 经典 32 位 System V ABI;历史包袱重但极其普及(很多嵌入式 Linux 32 位 MIPS 就是它)。
- 典型特征:只用 4 个参数寄存器传参,其余走栈;并且栈上预留“参数溢出区”等传统约定。
- n64(N64)
- 典型 64 位 ABI(LP64),常见于 64 位系统;
- 关键改进:前 8 个参数用寄存器传递,并使用更现代的对齐/寄存器规则。
- n32(N32)
- “介于 32 和 64 之间”的 ILP32 ABI:指针仍是 32 位(省内存/省 cache),但运行在 64 位 CPU 模式下,并继承很多 n64 的调用改进(如 8 个参数寄存器等)。
很实用的一句话:
- o32:纯传统 32 位生态
- n64:真正 64 位生态
- n32:想要“64 位寄存器带来的性能/调用约定优势”,但又想保留 32 位指针的体积优势
MIPS 还有面向嵌入式场景的 MIPS EABI(以及一些提案类 ABI),但在通用 Linux 发行版上最常见还是 o32/n32/n64 这三套。
MIPS 的端序问题
很多 MIPS 处理器可以配置为 大端(BE)或小端(LE)运行;也因此工具链/发行版会把“端序”作为目标三元组/架构名的一部分。
常见命名:
- mips:通常指 大端 32 位(也有项目用 mips 表示“端序未指定”,但 Linux/GNU 生态里常见约定是大端)
- mipsel:小端 32 位
- mips64:大端 64 位
- mips64el:小端 64 位
端序本身不是 ABI 的一部分,但在实际系统里,“同一个 OS/发行版”往往对端序 + ABI 组合做成不同架构端口(例如 Debian 的 mipsel vs mips64el)。你在交叉编译时也要同时选对两者(-EL/-EB + -mabi= / target triple)。
端序影响什么?
- 主要影响 内存中字节的排列顺序(多字节整数/指针/浮点在内存布局)。
- 指令本身是 32 位(或压缩 16 位)编码;CPU 取指后会按当前端序把字节组成指令字再解码——对编译器/汇编器来说,你只需要选对目标(mips vs mipsel),它就会生成正确字节序的机器码与数据段。
MIPS32
交叉编译
编译工具链
配置并构建交叉工具链(默认:mips big-endian + o32)
1 | mkdir -p ~/toolchains/build/ctng-mips-uclibc |
可以通过下面几个命令收集目标机器上的二进制程序的信息:
1
2
3 file hello
mips-unknown-linux-uclibc-readelf -A hello | egrep -i 'Tag_GNU_MIPS_ABI_FP|fp|float' || true
mips-unknown-linux-uclibc-readelf -h hello | egrep 'Class:|Data:|Machine:|Flags:'
在 ct-ng menuconfig 里按下面设置:
A) Target options
- Target Architecture:
mips - Endianness:
big endian - Bitness:
32-bit - ABI:
o32(如果你第 1 步确认是 n32,则改成 n32) - Architecture level / ISA:建议选
mips64r2(和你 file 输出一致)- 如果你担心 “Illegal instruction”,可以选
mips32r2更保守。
- 如果你担心 “Illegal instruction”,可以选
B) Operating System
- OS 选
linux
C) Kernel
- Kernel headers 选
Linux kernel - Kernel version:填
2.6.32.11 - 如果界面没有 2.6.32 这么老的可选项:
- 选择 custom tarball 或 custom location(不同版本菜单叫法略有差异)
- 指向你刚下载的:
~/toolchains/src/linux-2.6.32.11.tar.xz
D) C-library
- 选择
uClibc-ng - 版本选
1.0.43(若可选) - uClibc-ng 选项里确保开启:
threads (NPTL)(gdbserver 常用)
静态链接目标:工具链里只要有 libc 的
.a(静态库)就行。uClibc-ng 默认会有静态库。你最终编译 busybox/strace/gdbserver 时强制-static。
然后开始构建:
1 | ct-ng build |
构建完成后,默认会安装到类似这个目录:
1 | ls ~/x-tools/ |
设置环境变量
假设 ct-ng 输出工具链目录是:
~/x-tools/mips-unknown-linux-uclibc/
配置环境:
1 | export TOOLCHAIN="$HOME/x-tools/mips-unknown-linux-uclibc" |
先做一个最小静态 hello world 验证工具链能跑:
1 | mkdir -p ~/mips_static_test && cd ~/mips_static_test |
把 hello 拷到目标机运行,确认没问题,再继续后面三个工具。
编译 busybox
下载 BusyBox 源码,usyBox 1.36.1 源码包在 busybox 官方下载目录。
1 | mkdir -p ~/mips_build/src |
配置并静态编译:
1 | make distclean |
编译安装到 _install:
1 | make -j"$(nproc)" CROSS_COMPILE="$CROSS" ARCH=mips |
最终的 busybox 位于 _install/bin/busybox。
编译 strace
下载 strace 5.10,strace 5.10 的 tarball 在 strace 官方文件目录。
1 | cd ~/mips_build/src |
用 libunwind 让 strace 支持 -k。
1 | mkdir -p ~/mips_build/src ~/mips_build/staging |
用 --enable-stacktrace --with-libunwind 配置 strace。
1 | cd ~/mips_build/src/strace-5.10 |
把 strace 拷到目标机后,跑:
1 | ./strace -h | grep -- '-k' || true |
如果 -k 在帮助里能看到,说明编译选项生效了;跑起来能打印栈,说明回溯后端工作正常。
编译 gdbserver
1 | mkdir -p ~/mips_build/src |
gdbserver 在较新版本里确实开始依赖 iconv,并且 configure 会强制检查,没有就直接报错退出。这个依赖是刻意加入的:GDB 邮件列表里提到,gdbserver 因为“过滤 Linux 线程名里的非法编码”开始使用 iconv,并且又加了 configure 探测 libiconv 的提交,所以如果你的 libc(uClibc-ng)没带 iconv,就必须额外提供 GNU libiconv。
1 | mkdir -p ~/mips_build/src |
一定要有
libcharset.a**。静态链接时经常需要-liconv -lcharset两个一起带上(顺序也重要:iconv在前,charset在后)。
之后把这些库也添加到环境变量里面:
1 | export TOOLCHAIN="$HOME/x-tools/mips-unknown-linux-uclibc" |
**gdb 10.x 之后(包含 17.1)官方推荐/默认支持的方式已经变成:用“源码顶层 configure”先把 gnulib 等公共组件配置出来,再 make all-gdbserver**。否则就会出现这种报错:
../gnulib/Makefile.gnulib.inc: No such file or directory
因为 Makefile.gnulib.inc 是 gnulib 相关的“生成出来的 Makefile 片段”(模板是 Makefile.gnulib.inc.in,里面有一堆 @...@ 需要 configure 替换),不是你手动进 gdbserver 子目录 configure 就一定会生成/放对位置的。
官方 gdbserver 的 README 写得很明确:要单独构建 gdbserver,请在 obj 目录里跑 顶层 configure(并 --disable-gdb),然后 make all-gdbserver。
1 | cd ~/mips_build/src/gdb-17.1 |
configure 这里的要点:
--disable-gdb:避免去构建 gdb 本体,从而绕开你之前遇到的 GMP/MPFR 检查(gdb 本体才需要那堆数学库)--host=mips-unknown-linux-uclibc:告诉 configure 这是要跑在目标板上的程序(交叉编译)--with-libiconv-prefix=...或者用CPPFLAGS/LDFLAGS/LIBS指向你刚做好的 GNU libiconv(你现在 iconv 已经探测通过了)-static:强制静态链接
--with-libiconv-prefix 这个参数在 GDB 的 configure 选项里就是官方提供的。
1 | export CC="${CROSS}gcc" |
按官方目标构建:make all-gdbserver
1 | make -j"$(nproc)" all-gdbserver |
编完以后,gdbserver 的位置通常会是以下之一(不同版本/目录布局可能略有差异):
./gdbserver/gdbserver- 或
./gdb/gdbserver/gdbserver
用 find 直接定位最省事:
1 | find . -type f -name gdbserver -print |
假设找到的路径是 ./gdbserver/gdbserver:
1 | GDBSERVER_BIN="$(find . -type f -name gdbserver | head -n 1)"${CROSS}strip -s "$GDBSERVER_BIN" |
汇编基础
寄存器
通用寄存器 GPR(32 个,32 位)
MIPS32 有 32 个通用寄存器(GPR),编号 $0~`$31`,常用有“约定俗成”的别名:
| 编号 | 名称 | 典型用途 | 保存约定 |
|---|---|---|---|
| 0 | $zero |
恒为 0(读永远是 0,写入被丢弃) | — |
| 1 | $at |
汇编器临时寄存器(Assembler Temporary) | 不要随便用(可 .set noat) |
| 2–3 | $v0,$v1 |
返回值(或系统调用号等) | caller-saved |
| 4–7 | $a0–$a3 |
前 4 个整数/指针参数 | caller-saved |
| 8–15 | $t0–$t7 |
临时寄存器 | caller-saved |
| 16–23 | $s0–$s7 |
被调用者保存寄存器 | callee-saved |
| 24–25 | $t8,$t9 |
临时寄存器;$t9 常用于 PIC/函数地址调用 |
caller-saved |
| 26–27 | $k0,$k1 |
内核保留(异常/中断) | 仅内核用 |
| 28 | $gp |
global pointer(小数据区基址) | 约定用途 |
| 29 | $sp |
栈指针(向低地址增长) | 约定用途 |
| 30 | $fp/$s8 |
帧指针(可选) | callee-saved |
| 31 | $ra |
返回地址(jal/jalr 写入) |
caller-saved(但 callee 常需保存) |
几个关键点:
- MIPS 没有“标志寄存器/条件码”(不像 x86 有 ZF/CF),比较通常用
slt/sltu产生 0/1 再配合分支。 $zero让很多“伪指令”很自然:比如move常展开成addu rd, rs, $zero。$at(
HI/LO(乘除结果寄存器)
传统 MIPS 乘除法会把 64 位结果放到两个特殊寄存器:
- **
LO**:乘法结果低 32 位;除法商 - **
HI**:乘法结果高 32 位;除法余数
相关指令:
1 | mult $s0, $s1 # 有符号乘法,64-bit -> HI/LO |
备注:在较新的 MIPS32(如 r2 及以后)还出现过把低 32 位直接写回通用寄存器的
mul等指令;是否可用取决于具体 CPU/ISA 版本与工具链配置。
程序计数器 PC
- PC 不是通用寄存器,不能直接
mov。 - 获取“当前 PC”常用技巧:
bal label(branch and link)或jal到下一条附近,利用$ra获得“PC+8”(经典有延迟槽时)。
协处理器寄存器(CP0/CP1)
- CP0(系统控制):异常/中断、虚拟内存、特权态等(如
Status,Cause,EPC等)。常见指令mfc0/mtc0。 - CP1(FPU 浮点):
$f0–$f31(32 个浮点寄存器)。浮点调用约定、是否用硬浮点寄存器传参取决于 hard-float/soft-float ABI。
汇编指令
MIPS 的指令格式主要有 R / I / 三大类,指令定长 32 位。
- R 型(寄存器-寄存器):
op(6)=0 + rs + rt + rd + shamt + funct - I 型(立即数/访存/分支):
op + rs + rt + imm16 - J 型(跳转):
op + target26
MIPS 的一个核心哲学是 Load/Store:
算术/逻辑只在寄存器之间做;内存读写只能用专门的 load/store。
另外 MIPS 汇编器(assembler)会提供语法糖:汇编器在汇编阶段把它展开成一条或多条真实指令(最终机器码里只有真实指令);有的反汇编器也会“合并”几条真实指令,显示成更像源码的伪指令。
因此你写的“看似一条”的指令,可能展开成多条:
move rd, rs→addu rd, rs, $zeronop→sll $zero, $zero, 0li rd, imm32→lui/ori(或其他)la rd, symbol→ 取决于重定位与链接方式(可能用$gp、可能用lui/addiu等)b label→beq $zero,$zero,label
数据搬运:Load/Store(基址+偏移)
地址模式基本只有一种:**offset(base)**,其中 offset 是 16 位有符号(范围 -32768..32767 字节)。
常用指令:
1 | lw $t0, 0($sp) # 读 32-bit |
注意对齐(alignment):
- 多数实现要求
lw/sw地址 4 字节对齐,lh/sh2 字节对齐;否则可能触发异常(或用慢路径/特殊指令处理)。 - 有些 ISA/实现提供非对齐访问相关指令(如
lwl/lwr等),是否建议用取决于平台。
算术/逻辑/移位
1 | addu $t0, $t1, $t2 # 无溢出异常的加法 |
立即数与常量构造(lui/ori 等)
因为 I 型只有 16 位立即数,装载 32 位常量通常分两步:
1 | lui $t0, 0x1234 # $t0 = 0x12340000 |
很多时候你写:
1 | li $t0, 0x12345678 |
这是伪指令,汇编器会自动展开为 lui/ori(或更复杂序列)。
比较(无条件码):slt 家族
1 | slt $t0, $s0, $s1 # 有符号:$t0 = ($s0 < $s1) ? 1 : 0 |
典型“如果 (a<b) 跳转”:
1 | slt $t0, $a0, $a1 |
分支与跳转(PC 相对/区域跳转)
常用分支:
1 | beq $t0, $t1, label |
分支位移范围:
imm16是“指令偏移”,实际字节偏移 =signext(imm16) << 2- 所以可跳范围约 ±128KB(字节级)
常用跳转:
1 | j label |
J 型跳转范围:
target26 << 2与PC+4的高 4 位拼接- 因此
j/jal只能在同一个 256MB 区间内跳;跨区间通常用jr/jalr配合la/lui+ori装地址。
调用约定
MIPS32 常见 ABI 是 o32(32 位传统 ABI)。不同系统可能还有 EABI、以及是否 hard-float/soft-float 的差异,但核心思想类似。
参数传递与返回值
参数(前 4 个)
- 第 1–4 个整数/指针参数:放在
$a0–$a3
第 5 个及之后参数
- 放在栈上(caller 在调用前写好)
- 且 调用点的
$sp之上有一个固定的 16 字节参数保存区(argument save area),用于给被调函数保存$a0–$a3或放置溢出的参数。
常用的口径是:
在被调函数入口处(尚未调整 $sp 之前),第 5 个参数通常位于:
16($sp)(第 5 个)20($sp)(第 6 个)- …
这就是为什么很多资料说“栈上传参从
16($sp)开始”:前面 0–15 是那 16 字节的固定区。
寄存器保存规则
caller-saved(调用者保存)
调用别人之前,如果你还要用这些寄存器的值,就必须自己保存:
$t0–$t9$a0–$a3(参数寄存器随时可被覆盖)$v0–$v1- HI/LO(如果你依赖它们)
$at一般不算你能用的
callee-saved(被调用者保存)
被调函数如果要用这些寄存器,必须先保存、返回前恢复:
$s0–$s7$fp/$s8$ra:只有当该函数还要调用别的函数时才必须保存(leaf function 可不保存)$gp:在很多工具链/ABI 下要求保持(尤其 PIC/全局访问相关),但实现细节会随编译模式变化
这些寄存器在栈上的布局如下:
1 | 高地址 |
函数序言/尾声模板
其他特性
分支/跳转延迟槽(Delay Slot)
经典 MIPS(MIPS I~MIPS32 早期)有 branch delay slot:分支/跳转指令后面紧跟的下一条指令仍会执行一次(无论是否跳转),这条位置就是“延迟槽”。
例:
1 | beq $t0, $t1, L |
工具链可能自动重排填槽(除非 .set noreorder),也可能需要你手动放 nop。
注意:MIPS32 Release 6 取消了传统延迟槽语义(并对分支体系做了较大调整)。所以是否存在/如何处理延迟槽,必须结合“你目标 CPU 的 MIPS32 版本”和汇编器选项来看。
- Title: 异架构相关
- Author: sky123
- Created at : 2022-09-28 11:45:14
- Updated at : 2026-02-03 01:28:46
- Link: https://skyi23.github.io/2022/09/28/异架构相关/
- License: This work is licensed under CC BY-NC-SA 4.0.