异架构相关

sky123

环境基础

qemu 仿真

QEMU(Quick Emulator) 是一个开源的通用虚拟化和仿真框架,该框架主要有如下分类:

  • qemu-user:提供用户态的简单仿真。适用于对一些相对简单程序的进行仿真。

  • qemu-system:提供完整的系统级仿真。例如有些固件的运行需要一整套复杂的环境,单纯使用 qemu-user 仿真比较麻烦并且容易出问题,因此一个比较简单的方法是把整个 IOT 设备的文件系统扔到对应架构的一个完整虚拟机中然后 chroot 切换到 IOT 设备的文件系统的根目录进行仿真,此时需要使用 qemu-system 运行整个虚拟机。

  • qemu-utils:qemu 的一些配套工具,比如制作磁盘镜像的 qemu-img

安装命令:

1
2
3
4
sudo apt install qemu -y
sudo apt install qemu-user qemu-user-static -y
sudo apt install qemu-system -y
sudo apt install qemu-utils -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
2
3
4
5
6
7
➜  ~ cat /proc/sys/fs/binfmt_misc/qemu-arm  
enabled
interpreter /usr/bin/qemu-arm-static
flags: OCF
offset 0
magic 7f454c4601010100000000000000000002002800
mask ffffffffffffff00fffffffffffffffffeffffff

然而对于动态链接的程序,qemu 可以正常加载程序,但是动态库却会默认使用本机的动态库导致程序崩溃,因此需要 -L 参数指定 ld 的前缀径。另外如果将交叉编译工具链添加到对应的 qemu-binfmt,则 qemu-user 在运行程序时能加载正确的动态链接库,不需要指定路径。

1
2
3
4
5
6
7
8
sudo mkdir /etc/qemu-binfmt
sudo ln -s /usr/arm-linux-gnueabi /etc/qemu-binfmt/arm
sudo ln -s /usr/mipsel-linux-gnu/ /etc/qemu-binfmt/mipsel
sudo ln -s /usr/aarch64-linux-gnu /etc/qemu-binfmt/aarch64
sudo ln -s /usr/powerpc-linux-gnu /etc/qemu-binfmt/ppc
sudo ln -s /usr/mips64-linux-gnuabi64 /etc/qemu-binfmt/mips64
sudo ln -s /usr/mips64el-linux-gnuabi64 /etc/qemu-binfmt/mips64el
sudo ln -s /usr/mips-linux-gnu /etc/qemu-binfmt/mips

qemu-system

qemu-system 仿真需要提供系统内核和文件系统,我们可以在这个网站下载所需结构的内核和文件系统。

qemu-system 启动命令如下,具体启动参数还要参考镜像对应的 README 作相应的调整。

1
2
3
4
5
6
7
8
9
qemu-system-arm \
-M versatilepb \
-kernel vmlinuz-3.2.0-4-versatile \
-hda debian_wheezy_armel_standard.qcow2 \
-initrd initrd.img-3.2.0-4-versatile \
-append "root=/dev/sda1 console=tty0" \
-net nic \
-net tap,ifname=tap0,script=no,downscript=no \
-nographic
  • -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
2
3
4
5
6
sudo apt install uml-utilities

if ! ip link show tap0 &>/dev/null; then
tunctl -t tap0 -u $(whoami)
fi
ifconfig tap0 192.168.2.1/24

qemu 虚拟机启动后需要再虚拟机中分配 ip 。

1
ifconfig eth0 192.168.2.2/24

之后就可以通过 scp 向虚拟机传文件或者 ssh 登录虚拟机。

1
2
3
sudo apt install sshpass
sshpass -p root scp squashfs-root.tar.xz root@192.168.2.2:~
sshpass -p root ssh root@192.168.2.2

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
2
3
4
5
6
7
8
9
10
sudo apt update
sudo apt install -y \
build-essential gawk git wget curl rsync \
bison flex texinfo help2man gperf \
libncurses-dev \
autoconf automake libtool libtool-bin \
patch unzip xz-utils bzip2 \
python3 python3-dev \
file ca-certificates \
bc

下载并安装 crosstool-ng(编译在用户目录,避免污染系统)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mkdir -p ~/toolchains/src
cd ~/toolchains/src

# 下载 crosstool-ng 1.26.0
wget -O crosstool-ng-1.26.0.tar.xz \
http://crosstool-ng.org/download/crosstool-ng/crosstool-ng-1.26.0.tar.xz

tar -xf crosstool-ng-1.26.0.tar.xz
cd crosstool-ng-1.26.0

./configure --prefix="$HOME/.local"
make -j"$(nproc)"
make install

# 让 ct-ng 可用
export PATH="$HOME/.local/bin:$PATH"
ct-ng version

建议统一用这些目录:

1
2
3
4
5
6
7
~/work/
src/ # 源码下载解压
build/ # out-of-tree 构建目录
staging/ # uclibc/交叉库安装前缀(libiconv/libunwind 等)
out/ # 最终产物打包输出
toolchains/ # crosstool-ng 构建目录(可选)
mkdir -p ~/work/{src,build,staging,out,toolchains}

编译工具链

为什么使用 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 noteLINUX_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
2
3
4
5
6
mkdir -p ~/work/toolchains/ctng-x86_64-uclibc
cd ~/work/toolchains/ctng-x86_64-uclibc

ct-ng list-samples | egrep -i 'x86_64.*uclibc|uclibc.*x86_64' || true
# 如果有 sample,直接选;没有就用 menuconfig 手工配置 target
ct-ng x86_64-unknown-linux-uclibc || ct-ng menuconfig

进入 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
2
3
4
export UCLIBC64_TC="$HOME/x-tools/x86_64-unknown-linux-uclibc"
export PATH="$UCLIBC64_TC/bin:$PATH"
export CROSS64="x86_64-unknown-linux-uclibc-"
export SYSROOT64="$(${CROSS64}gcc -print-sysroot)"

i686-uclibc 工具链

同理:

1
2
3
4
5
6
7
mkdir -p ~/work/toolchains/ctng-i686-uclibc
cd ~/work/toolchains/ctng-i686-uclibc

ct-ng list-samples | egrep -i 'i[3-6]86.*uclibc|uclibc.*i[3-6]86' || true
ct-ng i686-unknown-linux-uclibc || ct-ng menuconfig

ct-ng build

设置变量:

1
2
3
4
export UCLIBC32_TC="$HOME/x-tools/i686-unknown-linux-uclibc"
export PATH="$UCLIBC32_TC/bin:$PATH"
export CROSS32="i686-unknown-linux-uclibc-"
export SYSROOT32="$(${CROSS32}gcc -print-sysroot)"

编译项目

busybox

BusyBox 默认启用了几个 applet/feature,它们会调用 getrandom / setns / syncfs 这三个接口;但如果目标环境是 Linux 2.6.32 + uClibc-ng 静态库,这三者在你的组合里要么 内核根本没有该 syscall,要么 uClibc-ng(静态 libc.a)没有提供对应符号,于是最终链接阶段直报错。

对应的链接错误是:

  • seedrng_main: undefined reference to getrandom

    BusyBox 的 miscutils/seedrng.c 里直接调用 getrandom()(不是 syscall()),所以 只要 libc 没提供 getrandom 符号,静态链接必失败

    另外,getrandom() 这个 syscall 在 Linux 3.17 才出现;如果目标内核是 2.6.32,即使你“强行”把它链接过去,运行时也只能走 ENOSYS 路径(BusyBox seedrng 里面确实有 ENOSYS fallback),但前提是要先能链接成功。

    并且 Buildroot 专门有个补丁提到一个典型坑:在 uClibc 场景里,getrandom() 可能“声明了能编过”,但最终链接时发现 libc 并没有定义它

  • nsenter_main: undefined reference to setns

    BusyBox 的 util-linux/nsenter.c,它是靠 setns() 进入别的进程 namespace 的。setns()Linux 3.0 才引入;目标内核 2.6.32 根本没有这个 syscall。

  • sync_common: undefined reference to syncfs

    BusyBox 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
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
29
30
cd ~/work/src/busybox-1.36.1
make distclean
make defconfig

# uclibc 下通常倾向静态(看你需求,若要动态,别开 CONFIG_STATIC)
sed -i 's/# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config

# 为避免 uclibc 缺符号导致链接失败:禁用 3 个典型项
# 1) seedrng -> getrandom
sed -i 's/^CONFIG_SEEDRNG=y/# CONFIG_SEEDRNG is not set/' .config || true
grep -q '^CONFIG_SEEDRNG=' .config || echo '# CONFIG_SEEDRNG is not set' >> .config

# 2) nsenter -> setns
sed -i 's/^CONFIG_NSENTER=y/# CONFIG_NSENTER is not set/' .config || true
grep -q '^CONFIG_NSENTER=' .config || echo '# CONFIG_NSENTER is not set' >> .config

# 3) fancy sync -> syncfs
sed -i 's/^CONFIG_FEATURE_SYNC_FANCY=y/# CONFIG_FEATURE_SYNC_FANCY is not set/' .config || true
grep -q '^CONFIG_FEATURE_SYNC_FANCY=' .config || echo '# CONFIG_FEATURE_SYNC_FANCY is not set' >> .config

yes "" | make oldconfig

# 编译
make -j"$(nproc)" CROSS_COMPILE="$CROSS64" ARCH=x86_64

mkdir -p ~/work/out/uclibc-x86_64
cp -a busybox ~/work/out/uclibc-x86_64/

file ~/work/out/uclibc-x86_64/busybox
readelf -l ~/work/out/uclibc-x86_64/busybox | grep -i interpreter && echo "NOT static" || echo "OK: static"

32 位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cd ~/work/src/busybox-1.36.1
make distclean
make defconfig
sed -i 's/# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config

sed -i 's/^CONFIG_SEEDRNG=y/# CONFIG_SEEDRNG is not set/' .config || true
grep -q '^CONFIG_SEEDRNG=' .config || echo '# CONFIG_SEEDRNG is not set' >> .config

sed -i 's/^CONFIG_NSENTER=y/# CONFIG_NSENTER is not set/' .config || true
grep -q '^CONFIG_NSENTER=' .config || echo '# CONFIG_NSENTER is not set' >> .config

sed -i 's/^CONFIG_FEATURE_SYNC_FANCY=y/# CONFIG_FEATURE_SYNC_FANCY is not set/' .config || true
grep -q '^CONFIG_FEATURE_SYNC_FANCY=' .config || echo '# CONFIG_FEATURE_SYNC_FANCY is not set' >> .config

yes "" | make oldconfig

make -j"$(nproc)" CROSS_COMPILE="$CROSS32" ARCH=i386

mkdir -p ~/work/out/uclibc-i686
cp -a busybox ~/work/out/uclibc-i686/
file ~/work/out/uclibc-i686/busybox

strace

下载 strace 源码:

1
2
3
4
cd ~/work/src
wget -c https://strace.io/files/5.10/strace-5.10.tar.xz
tar -xf strace-5.10.tar.xz
cd strace-5.10

如果直接用 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/strace

32 位:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cd ~/work/src
wget -c https://download-mirror.savannah.gnu.org/releases/libunwind/libunwind-1.6.2.tar.gz
tar -xf libunwind-1.6.2.tar.gz

mkdir -p ~/work/staging/uclibc-x86_64
cd ~/work/src/libunwind-1.6.2
make distclean 2>/dev/null || true

./configure \
--host=x86_64-unknown-linux-uclibc \
--build="$(gcc -dumpmachine)" \
--prefix="$HOME/work/staging/uclibc-x86_64" \
--disable-shared --enable-static \
CC="${CROSS64}gcc" AR="${CROSS64}ar" RANLIB="${CROSS64}ranlib" \
CFLAGS="--sysroot=$SYSROOT64 -Os" \
LDFLAGS="--sysroot=$SYSROOT64"

make -j"$(nproc)"
make install

编译 strace:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cd ~/work/src/strace-5.10
make distclean 2>/dev/null || true

./configure \
--host=x86_64-unknown-linux-uclibc \
--build="$(gcc -dumpmachine)" \
CC="${CROSS64}gcc" AR="${CROSS64}ar" RANLIB="${CROSS64}ranlib" \
CPPFLAGS="--sysroot=$SYSROOT64 -I$HOME/work/staging/uclibc-x86_64/include" \
CFLAGS="--sysroot=$SYSROOT64 -Os" \
LDFLAGS="--sysroot=$SYSROOT64 -L$HOME/work/staging/uclibc-x86_64/lib -static -pthread" \
--disable-mpers \
--enable-stacktrace \
--with-libunwind

make -j"$(nproc)"
${CROSS64}strip -s strace

mkdir -p ~/work/out/uclibc-x86_64
cp -a strace ~/work/out/uclibc-x86_64/

~/work/out/uclibc-x86_64/strace -h | grep -- '-k' || true
file ~/work/out/uclibc-x86_64/strace

gdbserver

首先下载 gdbserver 源码:

1
2
3
cd ~/work/src
wget -c https://ftp.gnu.org/gnu/gdb/gdb-17.1.tar.xz
tar -xf gdb-17.1.tar.xz

如果直接用 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/gdbserver

32 位为:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cd ~/work/src
wget -c https://ftp.gnu.org/gnu/libiconv/libiconv-1.17.tar.gz
tar -xf libiconv-1.17.tar.gz

mkdir -p ~/work/staging/uclibc-x86_64
cd ~/work/src/libiconv-1.17
make distclean 2>/dev/null || true

./configure \
--host=x86_64-unknown-linux-uclibc \
--build="$(gcc -dumpmachine)" \
--prefix="$HOME/work/staging/uclibc-x86_64" \
--disable-shared --enable-static --disable-nls \
CC="${CROSS64}gcc" AR="${CROSS64}ar" RANLIB="${CROSS64}ranlib" \
CFLAGS="--sysroot=$SYSROOT64 -Os" \
LDFLAGS="--sysroot=$SYSROOT64"

make -j"$(nproc)"
make install

ls -l ~/work/staging/uclibc-x86_64/include/iconv.h
ls -l ~/work/staging/uclibc-x86_64/lib/libiconv.a ~/work/staging/uclibc-x86_64/lib/libcharset.a

32 位对应为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mkdir -p ~/work/staging/uclibc-i686
cd ~/work/src/libiconv-1.17
make distclean 2>/dev/null || true

./configure \
--host=i686-unknown-linux-uclibc \
--build="$(gcc -dumpmachine)" \
--prefix="$HOME/work/staging/uclibc-i686" \
--disable-shared --enable-static --disable-nls \
CC="${CROSS32}gcc" AR="${CROSS32}ar" RANLIB="${CROSS32}ranlib" \
CFLAGS="--sysroot=$SYSROOT32 -Os" \
LDFLAGS="--sysroot=$SYSROOT32"

make -j"$(nproc)"
make install

由于 uclibc 与 thread_db 存在兼容性问题,因此我们在编译前需要禁用 thread_db 相关功能。

1
2
3
4
5
6
7
8
cd ~/work/src/gdb-17.1

# 兼容两种路径:顶层 gdbserver/ 或 gdb/gdbserver/
[ -f gdbserver/configure.srv ] && sed -i 's/srv_linux_thread_db=yes/srv_linux_thread_db=no/g' gdbserver/configure.srv
[ -f gdb/gdbserver/configure.srv ] && sed -i 's/srv_linux_thread_db=yes/srv_linux_thread_db=no/g' gdb/gdbserver/configure.srv

# 验证
grep -R --line-number 'srv_linux_thread_db=' gdbserver/configure.srv gdb/gdbserver/configure.srv 2>/dev/null || true

最后构建:

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
mkdir -p ~/work/build/gdbserver-uclibc-x86_64
cd ~/work/build/gdbserver-uclibc-x86_64

export ICONV64="$HOME/work/staging/uclibc-x86_64"

CC="${CROSS64}gcc" CXX="${CROSS64}g++" AR="${CROSS64}ar" RANLIB="${CROSS64}ranlib" \
CPPFLAGS="--sysroot=$SYSROOT64 -I$ICONV64/include" \
CFLAGS="--sysroot=$SYSROOT64 -Os" \
CXXFLAGS="--sysroot=$SYSROOT64 -Os" \
LDFLAGS="--sysroot=$SYSROOT64 -L$ICONV64/lib -static" \
LIBS="-liconv -lcharset -ldl -pthread -lrt" \
~/work/src/gdb-17.1/configure \
--host=x86_64-unknown-linux-uclibc \
--build="$(gcc -dumpmachine)" \
--disable-gdb \
--disable-werror \
--with-libiconv-prefix="$ICONV64"

make -j"$(nproc)" all-gdbserver

GDBSERVER_BIN="$(find . -type f -name gdbserver | head -n 1)"
echo "$GDBSERVER_BIN"
${CROSS64}strip -s "$GDBSERVER_BIN"

mkdir -p ~/work/out/uclibc-x86_64
cp -a "$GDBSERVER_BIN" ~/work/out/uclibc-x86_64/gdbserver
file ~/work/out/uclibc-x86_64/gdbserver
readelf -l ~/work/out/uclibc-x86_64/gdbserver | grep -i interpreter && echo "NOT static" || echo "OK: static"

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 作为区分不同指令集架构模式的标识,提出了 AArch32AArch64 两个明确的模式:

  • 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–D7D16–D31调用者保存
  • 参数/返回:硬浮点下,浮点参数优先走 s/d/q;返回用 s0/d0(或 q0 某视图)。

汇编指令

调用约定

  • 参数寄存器r0–r3;多余参数走栈(按 4 字节对齐)。

  • 返回r0(64 位放 r0–r1;结构体可能经隐藏指针)。

  • 易失/保存

    • caller‑savedr0–r3r12;VFP 的 D0–D7D16–D31
    • callee‑savedr4–r11(以及 VFP 的 D8–D15)。
  • 满递减(向低地址生长),跨调用点需 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 写返回地址到 x30RETx30 返回。调用者如需保留需入栈。
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
2
3
4
5
stp   x29, x30, [sp, #-16]!
mov x29, sp
... // 保存 x19–x28 / v8–v15(按需)
ldp x29, x30, [sp], #16
ret

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
2
3
4
5
6
7
8
9
10
11
mkdir -p ~/toolchains/build/ctng-mips-uclibc
cd ~/toolchains/build/ctng-mips-uclibc

# 看看有哪些 sample
ct-ng list-samples | grep -i mips

# 选一个最接近的(通常会有这个)
ct-ng mips-unknown-linux-uclibc

# 进入配置界面
ct-ng menuconfig

可以通过下面几个命令收集目标机器上的二进制程序的信息:

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 更保守。

B) Operating System

  • OS 选 linux

C) Kernel

  • Kernel headers 选 Linux kernel
  • Kernel version:填 2.6.32.11
  • 如果界面没有 2.6.32 这么老的可选项:
    • 选择 custom tarballcustom 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
2
3
4
5
6
export TOOLCHAIN="$HOME/x-tools/mips-unknown-linux-uclibc"
export PATH="$TOOLCHAIN/bin:$PATH"
export CROSS="mips-unknown-linux-uclibc-"
export SYSROOT="$(${CROSS}gcc -print-sysroot)"

echo "SYSROOT=$SYSROOT"${CROSS}gcc -v

先做一个最小静态 hello world 验证工具链能跑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mkdir -p ~/mips_static_test && cd ~/mips_static_test

cat > hello.c <<'EOF'
#include <stdio.h>
int main() {
puts("hello uclibc static");
return 0;
}
EOF

${CROSS}gcc -Os -static hello.c -o hello${CROSS}strip -s hello

file hello
readelf -l hello | grep -i interpreter || echo "OK: no PT_INTERP (static)"

hello 拷到目标机运行,确认没问题,再继续后面三个工具。

编译 busybox

下载 BusyBox 源码,usyBox 1.36.1 源码包在 busybox 官方下载目录。

1
2
3
4
5
6
7
8
mkdir -p ~/mips_build/src
cd ~/mips_build/src

wget -O busybox-1.36.1.tar.bz2 \
https://busybox.net/downloads/busybox-1.36.1.tar.bz2

tar -xf busybox-1.36.1.tar.bz2
cd busybox-1.36.1

配置并静态编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
make distclean
make defconfig

# 1) 静态链接(等价于 menuconfig 里 Busybox Settings -> Build Options -> Build BusyBox as a static binary)
sed -i 's/# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config

# 2) 禁用 nsenter(避免 setns)
sed -i 's/^CONFIG_NSENTER=y/# CONFIG_NSENTER is not set/' .config || true
grep -q '^CONFIG_NSENTER=' .config || echo '# CONFIG_NSENTER is not set' >> .config

# 3) 禁用 seedrng(避免 getrandom)
sed -i 's/^CONFIG_SEEDRNG=y/# CONFIG_SEEDRNG is not set/' .config || true
grep -q '^CONFIG_SEEDRNG=' .config || echo '# CONFIG_SEEDRNG is not set' >> .config

# 4) 禁用 sync 的 fancy(避免 syncfs,但保留 sync())
sed -i 's/^CONFIG_FEATURE_SYNC_FANCY=y/# CONFIG_FEATURE_SYNC_FANCY is not set/' .config || true
grep -q '^CONFIG_FEATURE_SYNC_FANCY=' .config || echo '# CONFIG_FEATURE_SYNC_FANCY is not set' >> .config

# 让配置生效并补齐依赖项
yes "" | make oldconfig

编译安装到 _install

1
2
3
4
5
6
7
8
make -j"$(nproc)" CROSS_COMPILE="$CROSS" ARCH=mips
make CROSS_COMPILE="$CROSS" ARCH=mips CONFIG_PREFIX="$PWD/_install" install

# 可选:strip(注意:strip 会去掉符号,调试不方便)
${CROSS}strip -s _install/bin/busybox

file _install/bin/busybox
readelf -l _install/bin/busybox | grep -i interpreter || echo "OK: static"

最终的 busybox 位于 _install/bin/busybox

编译 strace

下载 strace 5.10,strace 5.10 的 tarball 在 strace 官方文件目录。

1
2
3
4
5
6
7
cd ~/mips_build/src

wget -O strace-5.10.tar.xz \
https://strace.io/files/5.10/strace-5.10.tar.xz

tar -xf strace-5.10.tar.xz
cd strace-5.10

用 libunwind 让 strace 支持 -k

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
mkdir -p ~/mips_build/src ~/mips_build/staging
cd ~/mips_build/src

# 1) 下载 libunwind
wget -c https://download-mirror.savannah.gnu.org/releases/libunwind/libunwind-1.6.2.tar.gz

# 2) 解压
tar -xf libunwind-1.6.2.tar.gz
cd libunwind-1.6.2

# 3) 交叉编译 + 只要静态库(disable shared / enable static)
# prefix 装到 staging,避免污染 sysroot
./configure \
--host=mips-unknown-linux-uclibc \
--build="$(gcc -dumpmachine)" \
--prefix="$HOME/mips_build/staging" \
--disable-shared --enable-static \
CFLAGS="--sysroot=$SYSROOT -Os" \
LDFLAGS="--sysroot=$SYSROOT"

make -j"$(nproc)"
make install

# 4) 快速确认安装产物(至少要有这些东西之一)
ls -l ~/mips_build/staging/include/libunwind* || true
ls -l ~/mips_build/staging/lib/libunwind*.a || true

--enable-stacktrace --with-libunwind 配置 strace。

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
cd ~/mips_build/src/strace-5.10

# 清理旧配置
make distclean 2>/dev/null || true

# 重新 configure:打开 stacktrace,并指定 libunwind
# 注意:这里把 staging 的 include/lib 路径塞进去
./configure \
--host=mips-unknown-linux-uclibc \
--build="$(gcc -dumpmachine)" \
CC="${CROSS}gcc" AR="${CROSS}ar" RANLIB="${CROSS}ranlib" STRIP="${CROSS}strip" \
CPPFLAGS="--sysroot=$SYSROOT -I$HOME/mips_build/staging/include" \
CFLAGS="--sysroot=$SYSROOT -Os" \
LDFLAGS="--sysroot=$SYSROOT -L$HOME/mips_build/staging/lib -static -pthread" \
--disable-mpers \
--enable-stacktrace \
--with-libunwind

make -j"$(nproc)"

# 生成的是 ./strace
${CROSS}strip -s strace

# 检查是否静态(无 interpreter)
readelf -l strace | grep -i interpreter || echo "OK: static"

strace 拷到目标机后,跑:

1
2
./strace -h | grep -- '-k' || true
./strace -k -p 1 # 试试看(需要 root/ptrace 权限)

如果 -k 在帮助里能看到,说明编译选项生效了;跑起来能打印栈,说明回溯后端工作正常。

编译 gdbserver

1
2
3
4
mkdir -p ~/mips_build/src
cd ~/mips_build/src
wget -c https://ftp.gnu.org/gnu/gdb/gdb-17.1.tar.xz
tar -xf gdb-17.1.tar.xz

gdbserver 在较新版本里确实开始依赖 iconv,并且 configure 会强制检查,没有就直接报错退出。这个依赖是刻意加入的:GDB 邮件列表里提到,gdbserver 因为“过滤 Linux 线程名里的非法编码”开始使用 iconv,并且又加了 configure 探测 libiconv 的提交,所以如果你的 libc(uClibc-ng)没带 iconv,就必须额外提供 GNU libiconv。

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
29
30
31
32
33
mkdir -p ~/mips_build/src
cd ~/mips_build/src

# 主站(推荐先试这个)
wget -c https://ftp.gnu.org/gnu/libiconv/libiconv-1.17.tar.gz

# 备选镜像(如果上面很慢/连不上再用)
# wget -c https://mirrors.aliyun.com/gnu/libiconv/libiconv-1.17.tar.gz

tar -xf libiconv-1.17.tar.gz

mkdir -p ~/mips_build/staging
cd ~/mips_build/src/libiconv-1.17

# 清理(防止你重复编)
make distclean 2>/dev/null || true

./configure \
--host=mips-unknown-linux-uclibc \
--build="$(gcc -dumpmachine)" \
--prefix="$HOME/mips_build/staging" \
--disable-shared --enable-static \
--disable-nls \
CC="${CROSS}gcc" AR="${CROSS}ar" RANLIB="${CROSS}ranlib" \
CFLAGS="--sysroot=$SYSROOT -Os" \
LDFLAGS="--sysroot=$SYSROOT"

make -j"$(nproc)"
make install

ls -l ~/mips_build/staging/include/iconv.h
ls -l ~/mips_build/staging/lib/libiconv.a
ls -l ~/mips_build/staging/lib/libcharset.a

一定要有 libcharset.a**。静态链接时经常需要 -liconv -lcharset 两个一起带上(顺序也重要:iconv 在前,charset 在后)。

之后把这些库也添加到环境变量里面:

1
2
3
4
5
6
7
8
9
10
export TOOLCHAIN="$HOME/x-tools/mips-unknown-linux-uclibc"
export PATH="$TOOLCHAIN/bin:$PATH"
export CROSS="mips-unknown-linux-uclibc-"
export SYSROOT="$(${CROSS}gcc -print-sysroot)"

# 你之前装 libiconv 的 staging(如果你按我之前教程装过)
export ICONV_PREFIX="$HOME/mips_build/staging"

echo "SYSROOT=$SYSROOT"
ls -l "$ICONV_PREFIX/include/iconv.h" "$ICONV_PREFIX/lib/libiconv.a" "$ICONV_PREFIX/lib/libcharset.a"

**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
2
3
4
5
6
7
8
cd ~/mips_build/src/gdb-17.1

rm -rf build-mips-gdbserver-top
mkdir -p build-mips-gdbserver-top
cd build-mips-gdbserver-top

# 确认顶层 configure 存在(你之前跑过 ./configure,所以这里肯定有)
ls -l ../configure

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export CC="${CROSS}gcc"
export CXX="${CROSS}g++"
export AR="${CROSS}ar"
export RANLIB="${CROSS}ranlib"
export STRIP="${CROSS}strip"

# 让 configure / make 能找到 iconv(你已经让 iconv check 变成 yes)
export CPPFLAGS="--sysroot=$SYSROOT -I$ICONV_PREFIX/include"
export CFLAGS="--sysroot=$SYSROOT -Os"
export CXXFLAGS="--sysroot=$SYSROOT -Os"
export LDFLAGS="--sysroot=$SYSROOT -L$ICONV_PREFIX/lib -static"
# 保险起见把 iconv 两个库显式塞进去(静态常用)
export LIBS="-liconv -lcharset -ldl -pthread -lrt"

../configure \
--build="$(gcc -dumpmachine)" \
--host=mips-unknown-linux-uclibc \
--disable-gdb \
--disable-werror \
--with-libiconv-prefix="$ICONV_PREFIX"

按官方目标构建:make all-gdbserver

1
make -j"$(nproc)" all-gdbserver

编完以后,gdbserver 的位置通常会是以下之一(不同版本/目录布局可能略有差异):

  • ./gdbserver/gdbserver
  • ./gdb/gdbserver/gdbserver

用 find 直接定位最省事:

1
2
find . -type f -name gdbserver -print
file $(find . -type f -name gdbserver | head -n 1)

假设找到的路径是 ./gdbserver/gdbserver

1
2
3
4
5
6
GDBSERVER_BIN="$(find . -type f -name gdbserver | head -n 1)"${CROSS}strip -s "$GDBSERVER_BIN"

file "$GDBSERVER_BIN"

# 静态验证:不应该有 PT_INTERP
readelf -l "$GDBSERVER_BIN" | grep -i interpreter && echo "NOT static" || echo "OK: static"

汇编基础

寄存器

通用寄存器 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
  • $at1)默认被汇编器用于展开伪指令(如li/la等),除非你显式.setnoat并自行管理。1)默认被汇编器用于展开伪指令(如 `li/la` 等),除非你显式 `.set noat` 并自行管理。
HI/LO(乘除结果寄存器)

传统 MIPS 乘除法会把 64 位结果放到两个特殊寄存器:

  • **LO**:乘法结果低 32 位;除法商
  • **HI**:乘法结果高 32 位;除法余数

相关指令:

1
2
3
4
5
6
7
mult  $s0, $s1     # 有符号乘法,64-bit -> HI/LO
multu $s0, $s1 # 无符号
mflo $t0 # 读 LO
mfhi $t1 # 读 HI

div $s0, $s1 # 有符号除法:LO=商 HI=余
divu $s0, $s1 # 无符号

备注:在较新的 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, rsaddu rd, rs, $zero
  • nopsll $zero, $zero, 0
  • li rd, imm32lui/ori(或其他)
  • la rd, symbol → 取决于重定位与链接方式(可能用 $gp、可能用 lui/addiu 等)
  • b labelbeq $zero,$zero,label
数据搬运:Load/Store(基址+偏移)

地址模式基本只有一种:**offset(base)**,其中 offset16 位有符号(范围 -32768..32767 字节)。

常用指令:

1
2
3
4
5
6
7
8
9
10
lw   $t0,  0($sp)    # 读 32-bit
sw $t0, 4($sp) # 写 32-bit

lb $t1, 0($a0) # 读 8-bit 有符号扩展
lbu $t1, 0($a0) # 读 8-bit 无符号扩展
lh $t2, 2($a0) # 读 16-bit 有符号扩展
lhu $t2, 2($a0) # 读 16-bit 无符号扩展

sb $t1, 0($a0)
sh $t2, 2($a0)

注意对齐(alignment):

  • 多数实现要求 lw/sw 地址 4 字节对齐,lh/sh 2 字节对齐;否则可能触发异常(或用慢路径/特殊指令处理)。
  • 有些 ISA/实现提供非对齐访问相关指令(如 lwl/lwr 等),是否建议用取决于平台。
算术/逻辑/移位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
addu  $t0, $t1, $t2   # 无溢出异常的加法
add $t0, $t1, $t2 # 有溢出异常(trap on overflow)
subu $t0, $t1, $t2
sub $t0, $t1, $t2

addi $t0, $t1, 123 # 有溢出异常
addiu $t0, $t1, 123 # 不触发溢出异常(常用于地址/栈指针运算)

and $t0, $t1, $t2
or $t0, $t1, $t2
xor $t0, $t1, $t2
nor $t0, $t1, $t2

sll $t0, $t1, 4 # 逻辑左移
srl $t0, $t1, 4 # 逻辑右移
sra $t0, $t1, 4 # 算术右移(保留符号位)
立即数与常量构造(lui/ori 等)

因为 I 型只有 16 位立即数,装载 32 位常量通常分两步:

1
2
lui  $t0, 0x1234      # $t0 = 0x12340000
ori $t0, $t0, 0x5678 # $t0 = 0x12345678

很多时候你写:

1
li   $t0, 0x12345678

这是伪指令,汇编器会自动展开为 lui/ori(或更复杂序列)。

比较(无条件码):slt 家族
1
2
3
4
slt   $t0, $s0, $s1    # 有符号:$t0 = ($s0 < $s1) ? 1 : 0
sltu $t0, $s0, $s1 # 无符号
slti $t0, $s0, 10
sltiu $t0, $s0, 10

典型“如果 (a<b) 跳转”:

1
2
slt  $t0, $a0, $a1
bne $t0, $zero, less
分支与跳转(PC 相对/区域跳转)

常用分支:

1
2
3
4
5
6
beq  $t0, $t1, label
bne $t0, $t1, label
blez $t0, label # <=0
bgtz $t0, label # >0
bltz $t0, label # <0
bgez $t0, label # >=0

分支位移范围:

  • imm16 是“指令偏移”,实际字节偏移 = signext(imm16) << 2
  • 所以可跳范围约 ±128KB(字节级)

常用跳转:

1
2
3
4
j    label
jal func # 跳转并写返回地址到 $ra
jr $ra # 返回
jalr $t9 # 通过寄存器间接调用,$ra=返回地址

J 型跳转范围:

  • target26 << 2PC+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
2
3
4
5
6
高地址
| 传入的第 5、6... 个参数 | 16($sp_entry)+ ...
| 16B 参数保存区 (a0-a3 spill area) | 0..15($sp_entry)
+-----------------------------------+ <-- $sp_entry(函数入口处的 sp,尚未分配本地栈帧)
| (被调函数分配的局部变量、保存寄存器等) |
低地址
函数序言/尾声模板

其他特性

分支/跳转延迟槽(Delay Slot)

经典 MIPS(MIPS I~MIPS32 早期)有 branch delay slot:分支/跳转指令后面紧跟的下一条指令仍会执行一次(无论是否跳转),这条位置就是“延迟槽”。

例:

1
2
beq  $t0, $t1, L
addiu $s0, $s0, 1 # 这条在经典 MIPS 上仍会执行(延迟槽)

工具链可能自动重排填槽(除非 .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.
Comments