zhangguanzhang's Blog

利用 qemu user 模式和 binfmt_misc 构建其他架构的 docker 镜像

字数统计: 2.5k阅读时长: 10 min
2023/03/07

docker 的 buildx 需要内核支持,不升级内核的情况下,实际上可以利用 qemu 和 binfmt_misc 在 x86_64 上构建其他架构容器,之前这块也是没大概看组合原理,这次龙芯 loongarch64 适配的时候正好理解

由来

借着这次在 x86_64 上构建龙芯的 docker 镜像理解了下 qemu 和 binfmt_misc 的组合大概,相信不少人使用过下面的,运行一个 qemu-user-static 的容器后,就可以在 x86_64 机器上执行其他架构容器

1
2
3
4
5
6
7
8
9
10
$ uname -m
x86_64

$ docker run --rm -t arm64v8/ubuntu uname -m
standard_init_linux.go:211: exec user process caused "exec format error"

$ docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

$ docker run --rm -t arm64v8/ubuntu uname -m
aarch64

原理讲解

这次文章就是介绍运行这个特权容器到底干了啥和用了啥科技

qemu

qemu 是虚拟化技术,可以完全模拟一个虚拟机,如果你安装 RHEL 的 gui 系统或者使用过 proxmox,能看到默认就带 qemu 和 kvm,kvm 是内核模块工作在内核态,它大部分承担硬件翻译执行能力, qemu 和 kvm 是互相弥补不足的,组合起来模拟性能更强和模拟的方面更全面。

qemu 分为两种模式:

  • 模拟/系统模式(System Mode):模拟整个计算机系统,包括中央处理器及其他周边设备,它使能为跨平台编写的程序进行测试及排错工作变得容易。其亦能用来在一部主机上虚拟数个不同的虚拟计算机,类似我们平常使用的Vmare、VirtualBox等。运行的二进制是 qemu-system-$arch
  • 用户模式(User Mode):在 os 上直接启动运行非本机器架构的 Linux 程序,因为 qemu-user 有内置了系统调用翻译,运行的二进制是 qemu-$archqemu-$arch-static

qemu user 模式

看上面使用到的镜像名字里有 qemu-user 字样,说明利用到的是 qemu 的用户模式,下面是一个示例:

1
2
3
4
5
6
7
8
9
10
$ cat arch.go
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("Runtime:", runtime.GOOS)
fmt.Println("CPU Architecture:", runtime.GOARCH)
}

编译上面两个文件后,可以看到是不同架构的二进制文件,当然你也可以其他语言,例如 c 语言写个类似的,然后交叉编译工具编译出来:

1
2
3
4
5
6
7
8
9
10
$ CGO_ENABLED=0 go build -o arch_amd64 arch.go
$ CGO_ENABLED=0 GOARCH=arm64 go build -o arch_arm64 arch.go
$ file arch_*
arch_arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID=yWXdW8xWC_151uR99u6q/2Dejwde0vtiPXdDCC-kL/4vOP0RCfaeaWtMVrus3U/_3s13dNK6GQJhzaszUVM, with debug_info, not stripped
arch_amd64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=lmGyI5HbkHtM1qoM4Tzh/dX3opcS3RVhsbDro_SJ6/ZjSvKC84nJJnLhTMfSRC/yRfK-axzCadYFm4tDep5, with debug_info, not stripped
$ ./arch_amd64
Runtime: linux
CPU Architecture: amd64
$ ./arch_arm64
-bash: ./arch_arm64: cannot execute binary file

直接执行是执行不了其他架构的,报错也可能是 exec format error 之类的,下面就是使用 qemu-user 模式运行

1
2
3
4
5
6
7
8
9
# 你也可以用包管理安装,有些系统的自带的源里没有静态编译的 qemu-$arch
# 所以这里我直接下载别人编译好的静态二进制
$ wget https://github.com/multiarch/qemu-user-static/releases/download/v7.2.0-1/qemu-aarch64-static
$ chmod a+x qemu-aarch64-static
$ ./qemu-aarch64-static arch_arm64
Runtime: linux
CPU Architecture: arm64
$ ./arch_arm64
-bash: ./arch_arm64: cannot execute binary file

如果需要运行的程序不是静态链接的,需要宿主机行支持,或者 chroot 进去后,把 qemu-user-static 拷贝进去执行。

Linux 的 binfmt_misc

windows 上可以设置不同的后缀文件使用不同软件打开,在 Linux 上,也有类似的功能。Linux的内核从很早开始就引入了一个叫做 Miscellaneous Binary Format(binfmt_misc)的机制,可以通过要打开文件的特性来选择到底使用哪个程序来打开。比 Windows 更加强大的地方是,它不光可以通过文件的扩展名来判断的,还可以通过文件开始位置的特殊的字节(ELF Magic Byte)来判断。

binfmt_misc 开启

使用下面命令启用这个功能:

1
2
3
4
# 内核编译的选项 Executable file formats / Emulations  ---> <M> Kernel support for MISC binaries
modprobe binfmt_misc
# 也是 /procfs 注册开启
mount binfmt_misc /proc/sys/fs/binfmt_misc

binfmt_misc 的注册格式

然后就可以在 /proc/sys/fs/binfmt_misc 里面看到两个文件

  • register:该文件只能写入,不可读取,写入注册格式就能注册
  • status:读取它可以看到当前 binfmt_misc 是否启用

注册格式很简单:

1
2
:name:type:offset:magic:mask:interpreter:flags
# 字段冒号分隔,某些字段有默认值,默认值情况下也要保留对应位置的冒号分隔符
  • name:名字,用来标识这条记录的,理论上可以取任何名字,只要不重名就可以了
  • type:
    • M 表示目标文件的内容 magic 来识别的,
    • E 则是认文件后缀
  • offset 在 type 为 M 的时候有用,指定识别的时候的偏移位置,默认是 0 。
  • magic 即用来识别的具体 magic 内容
  • mask 在 type 为 M 的时候用,默认值全是 0xFF 的 bitmask,二进制下某一位为1,则表示文件的 ELF magic 必须和 magic对应的位匹配。magic 长度一般是 40,具体可以去看 qemu-binfmt-conf.sh 脚本,获取二进制文件的 magic 可以使用例如下面的 shell:
    • xxd hello_arm64 | awk '{for(i=2;i<NF;i++){a=a$i;c++;if(c>9){print a;exit;}}}' 该示例是 40 个
  • interpreter 具体用来执行的解释器,必须用绝对路径。不能超过127个字符。
  • flags 可选的,用来控制 interpreter 打开文件的行为:
    • P 用于保存用户于命令行中输入的原程序名(通过将程序名添加到argv);interpreter 必须知悉到此标记才能正确将此额外函数作为其argv[0]传递至解释程序。
    • O 用于打开程序文件并将其文档描述符传递至interpreter以读取用户无法读取的文件(对于无读取权限的用户而言)。
    • C 用于根据程序文件而非 interpreter 文件决定新进程凭证(参见setuid);此值默认为O。
    • F 最常用的,白话讲就是配置的时候会把 interpreter 文件导入到内存里,后续用户空间和 chroot 里都没这个文件也可以执行。
  • 一些注意事项
    • offset + size(magic) 一定要少于 128 位
    • 后添加的会先被匹配

取消注册

1
2
3
4
5
6
7
# 全部
echo -1 > /proc/sys/fs/binfmt_misc/status
# 单个
echo -1 > /proc/sys/fs/binfmt_misc/xxx
# 禁用与开启
echo 0 > /proc/sys/fs/binfmt_misc/status
echo 1 > /proc/sys/fs/binfmt_misc/status

multiarch/qemu-user-static 做了啥

1
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

实际上运行上面的特权容器,等同于:

  1. 特权容器下,容器内的 /proc 和 mount 是和宿主机一致的
  2. 入口 register.sh 脚本 挂载 binfmt_misc
  3. shift 掉 --reset 后,剩下参数传递执行 qemu 的 qemu-binfmt-conf.sh 脚本,注册各种架构二进制的打开方式为对应的 qemu-$arch-static
  4. 因为 multiarch/qemu-user-static 镜像里有 COPY qemu-*-static /usr/bin/,然后把 -p yes 传递给最终在脚本里,也就是注册的时候会开 flags 的 F ,会把这些 /usr/bin/qemu-*-static 导入到内存里。这个需要内核支持,centos 3.10内核就不支持
  5. 然后在 x86_64 上执行其他架构的镜像,会被 binfmt_misc 识别,调用内存里的 /usr/bin/qemu-*-static 翻译执行

还有个 multiarch/qemu-user-static:register 的镜像,运行是:

1
docker run --rm --privileged multiarch/qemu-user-static:register --reset

因为这个镜像里没 /usr/bin/qemu-$arch-static ,所以也不会用 -p yes ,这种使用方式就需要你挂载 qemu 到 /usr/bin/ 里:

1
2
$ docker run --rm -t -v $PWD/qemu-aarch64-static:/usr/bin/qemu-aarch64-static arm64v8/ubuntu uname -m
aarch64

或者制作 docker 镜像的时候,内部 /usr/bin/qemu-$arch-static 存在。例如我们业务的 Dockerfile_arm64:

1
2
3
4
5
# 提前所有 jenkins 上执行 docker run --rm --privileged multiarch/qemu-user-static:register --reset
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
FROM arm64v8/ubuntu
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin/
RUN xxxx

或者 第一阶段是 golang 交叉编译,第二阶段是最终架构:

1
2
3
4
5
6
7
8
FROM go:xxx as build
RUN GOARCH=arm64 go build -o /xxx cmd/main.go

FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
FROM arm64v8/ubuntu
COPY --from=build /xxx /xxx
COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin/
RUN apt-get ...

当然,也不是只有 docker,因为是内核拦截的 execute 的 syscall ,所以此刻宿主机上也可以执行:

1
2
3
$ ./arch_arm64
Runtime: linux
CPU Architecture: arm64

一些注意点

  • register.sh 脚本 里默认是把 QEMU_BIN_DIR 设置为 /usr/bin/ ,直接使用 qemu 的脚本默认则是 /usr/local/bin/
  • register.sh 脚本 里默认设置了 --qemu-suffix "-static",意味着最终的 interpreter 会带有 -static 后缀
  • multiarch/qemu-user-static 镜像不一定更新及时,可能需要自己替换里面的一些文件,例如我这几天搞的 loongarch64 ,龙芯官方提供的 qemu-loongarch64 + 最新的 qemu-binfmt-conf.sh 才行
1
2
3
4
5
FROM multiarch/qemu-user-static
# 因为设置了 --qemu-suffix "-static" 所以拷贝进去名字要对应
COPY qemu-loongarch64 /usr/bin/qemu-loongarch64-static
ADD https://raw.githubusercontent.com/qemu/qemu/master/scripts/qemu-binfmt-conf.sh /qemu-binfmt-conf.sh
RUN chmod a+x /qemu-binfmt-conf.sh

构建后测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ docker build -t multiarch/qemu-user-static-2 .
$ docker run --rm --privileged multiarch/qemu-user-static-2 --reset -p yes
$ docker run --rm -ti cr.loongnix.cn/library/alpine:3.11.11 uname -m
WARNING: The requested image's platform (linux/loong64) does not match the detected host platform (linux/amd64) and no specific platform was requested
loongarch64
$ uname -m
x86_64
$ cat /proc/sys/fs/binfmt_misc/qemu-loongarch64
enabled
interpreter /usr/bin/qemu-loongarch64-static
flags: F
offset 0
magic 7f454c4602010100000000000000000002000201
mask fffffffffffffffc00fffffffffffffffeffffff

之前使用 multiarch/qemu-user-static 的 qemu-loongarch64-static 会报错 Function not implemented,龙芯官方给我的才可以正常使用,这块后续得等龙芯他们合并到 qemu 去了

1
2
3
$ docker run --rm -ti cr.loongnix.cn/library/alpine:3.11.11 ls
WARNING: The requested image's platform (linux/loong64) does not match the detected host platform (linux/amd64) and no specific platform was requested
ls: .: Function not implemented

龙芯的 qemu 下载

env

发现 dockerfile 里这样会影响 qemu 执行

1
ARG QEMU_VERSION

参考

CATALOG
  1. 1. 由来
  2. 2. 原理讲解
    1. 2.1. qemu
      1. 2.1.1. qemu user 模式
    2. 2.2. Linux 的 binfmt_misc
      1. 2.2.1. binfmt_misc 开启
      2. 2.2.2. binfmt_misc 的注册格式
    3. 2.3. multiarch/qemu-user-static 做了啥
    4. 2.4. 一些注意点
      1. 2.4.1. env
  3. 3. 参考