7

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

 1 year ago
source link: https://zhangguanzhang.github.io/2023/03/07/qemu-binfmt_misc/#/%E5%8F%82%E8%80%83
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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



字数统计: 2.4k阅读时长: 10 min
 2023/03/07  77  Share

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

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

$ 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 是虚拟化技术,可以完全模拟一个虚拟机,如果你安装 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 的用户模式,下面是一个示例:

$ cat arch.go
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("Runtime:", runtime.GOOS)
fmt.Println("CPU Architecture:", runtime.GOARCH)
}

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

$ 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 模式运行

# 你也可以用包管理安装,有些系统的自带的源里没有静态编译的 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 更加强大的地方是,它不光可以通过文件的扩展名来判断的,还可以通过文件开始位置的特殊的字节(Magic Byte)来判断。

binfmt_misc 开启

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

# 内核编译的选项 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 是否启用

注册格式很简单:

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

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 导入到内存里
  5. 然后在 x86_64 上执行其他架构的镜像,会被 binfmt_misc 识别,调用内存里的 /usr/bin/qemu-*-static 翻译执行

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

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

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

$ 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:

# 提前所有 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 交叉编译,第二阶段是最终架构:

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 ,所以此刻宿主机上也可以执行:

$ ./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 才行
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

构建后测试

$ 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 去了

$ 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

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK