Linux: qemu-user-static 是如何工作的?
文章目录
- 1. 前言
- 2. qemu-user-static 的工作原理
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. qemu-user-static 的工作原理
我们通常使用交叉编译的方式来构建 ARM 程序,譬如有一个 hello.c
程序,其代码如下:
// hello.c
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
在 x86
系统下将其交叉编译为 aarch64
程序:
$ aarch64-linux-gnu-gcc -o hello hello.c
$ file hello
hello: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=c4367fc3135655c88a487baf4a4ac0f04bcff318, for GNU/Linux 3.7.0, not stripped
试着运行:
$ ./hello
bash: ./hello: 无法执行二进制文件: 可执行文件格式错误
安装 qemu-user-static
后再运行:
$ sudo apt-get install qemu-user-static
$ ./hello
aarch64-binfmt-P: Could not open '/lib/ld-linux-aarch64.so.1': No such file or directory
看到变化了吗?虽然仍然不能运行,但是错误提示不一样了:安装 qemu-user-static
之前,提示的是 可执行文件格式错误
,即系统不支持 aarch64
格式的二进制文件执行;安装 qemu-user-static
之后,系统已经支持aarch64
格式的二进制文件执行,只是提示在 x86 系统中找不到动态链接库 /lib/ld-linux-aarch64.so.1
。这是显然的,ld-linux-aarch64.so.1
是 aarch64
架构的动态链接解释器程序,默认 x86
系统下是不存在 /lib/ld-linux-aarch64.so.1
文件的。既然是找不到这个动态链接库,有两个办法解决,第一个是放置一个这个文件,第二个是静态链接,我们采用第二种:
$ aarch64-linux-gnu-gcc -static -o hello hello.c
$ file hello
hello: ELF 64-bit LSB executable, ARM aarch64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=7bb03bb527641bda2d2ee1f0a1a5e9e02d932c5f, for GNU/Linux 3.7.0, not stripped
$ ./hello
Hello, World!
程序居然能执行了!这是什么情况?是不是有点颠覆认知?原来一切的秘密,来自于 Linux
内核的 binfmt_misc
模块功能,也就是允许用户自定义可执行二进制文件格式和其翻译执行程序(如 qemu-user-static
),然后通过翻译执行。如我们的例子中,就是这样一个工作过程:
qemu-user-static
hello (aarch64 程序) ---------------------------> 在 x86 上执行
翻译 aarch64 指令为 x86 指令
Linux 内核 binfmt_misc
模块提供了一个用户空间接口:
$ sudo mount | grep "binfmt"
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=29,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=18043)
binfmt_misc on /proc/sys/fs/binfmt_misc type binfmt_misc (rw,nosuid,nodev,noexec,relatime)
目录 /proc/sys/fs/binfmt_misc
在安装 qemu-user-static
之前,如下:
$ ls /proc/sys/fs/binfmt_misc/ -l
total 0
--w------- 1 root root 0 3月 18 13:40 register
-rw-r--r-- 1 root root 0 3月 18 13:40 status
在安装 qemu-user-static
之后,如下:
$ ls /proc/sys/fs/binfmt_misc
llvm-14-runtime.binfmt qemu-aarch64 qemu-cris qemu-microblaze qemu-mipsel qemu-ppc64 qemu-s390x qemu-sparc32plus register
llvm-18-runtime.binfmt qemu-alpha qemu-hexagon qemu-mips qemu-mipsn32 qemu-ppc64le qemu-sh4 qemu-sparc64 status
python2.7 qemu-arm qemu-hppa qemu-mips64 qemu-mipsn32el qemu-riscv32 qemu-sh4eb qemu-xtensa
python3.10 qemu-armeb qemu-m68k qemu-mips64el qemu-ppc qemu-riscv64 qemu-sparc qemu-xtensaeb
可以看到,除了之前的 register
和 status
文件外,多了很多其它程序。但即使还不理解这里的细节,我想读者也能猜测到,安装 qemu-user-static
后,可以支持在 x86
系统下通过 qemu-user-static
运行 alpha,arm,riscv,......
等等平台的程序了。
那这一切是怎么做到的呢?我们来看下 binfmt_misc
的内核源码。首先,模块注册了 binfmt_misc
类型的文件系统:
/* fs/binfmt_misc.c */
static struct file_system_type bm_fs_type = {
.owner = THIS_MODULE,
.name = "binfmt_misc",
.mount = bm_mount,
.kill_sb = kill_litter_super,
};
MODULE_ALIAS_FS("binfmt_misc");
static int __init init_misc_binfmt(void)
{
int err = register_filesystem(&bm_fs_type);
...
return err;
}
然后在将 binfmt_misc
文件系统挂载到 /proc/sys/fs
下时,建立了文件节点 register
和 status
:
/* fs/binfmt_misc.c */
// bm_mount() -> ... -> bm_fill_super()
static const struct file_operations bm_status_operations = {
.read = bm_status_read,
.write = bm_status_write,
.llseek = default_llseek,
};
/* Superblock handling */
static const struct super_operations s_ops = {
.statfs = simple_statfs,
.evict_inode = bm_evict_inode,
};
static int bm_fill_super(struct super_block *sb, void *data, int silent)
{
int err;
static const struct tree_descr bm_files[] = {
[2] = {"status", &bm_status_operations, S_IWUSR|S_IRUGO},
[3] = {"register", &bm_register_operations, S_IWUSR},
/* last one */ {""}
};
err = simple_fill_super(sb, BINFMTFS_MAGIC, bm_files);
if (!err)
sb->s_op = &s_ops;
return err;
}
static struct dentry *bm_mount(struct file_system_type *fs_type,
int flags, const char *dev_name, void *data)
{
return mount_single(fs_type, flags, data, bm_fill_super);
}
于是用户空间看到了 /proc/sys/fs/binfmt_misc/{register,status}
两个节点。status
是可读写
的,用来控制开启和关闭 binfmt_misc
模块功能;register
是只写的,用来注册用户自定义的可执行二进制格式和翻译执行程序,如:
echo ':qemu-aarch64:M::\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00:\xff\xff\xff\xff\xff\xff\xff\xfc\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/bin/qemu-aarch64-static:OC' | sudo tee /proc/sys/fs/binfmt_misc/register
这里是注册 aarch64
的解释执行程序为 /usr/bin/qemu-aarch64-static
。这个细节在函数 bm_register_write()
中:
/* fs/binfmt_misc.c */
static LIST_HEAD(entries); /* 自定义可执行二进制文件格式处理接口信息列表 */
/* /register */
static ssize_t bm_register_write(struct file *file, const char __user *buffer,
size_t count, loff_t *ppos)
{
Node *e;
struct inode *inode;
...
/*
* 设定翻译解释器程序等信息,如:
* /usr/libexec/qemu-binfmt/aarch64-binfmt-P
*/
e = create_entry(buffer, count);
...
inode = bm_get_inode(sb, S_IFREG | 0644);
...
/* 一种新的格式对应一个文件节点,如 /proc/sys/fs/binfmt_misc/qemu-aarch64 */
e->dentry = dget(dentry);
inode->i_private = e;
inode->i_fop = &bm_entry_operations;
d_instantiate(dentry, inode);
write_lock(&entries_lock);
list_add(&e->list, &entries);
write_unlock(&entries_lock);
...
}
看一下注册的 qemu-aarch64
类型注册信息:
$ cat /proc/sys/fs/binfmt_misc/qemu-aarch64
enabled
interpreter /usr/libexec/qemu-binfmt/aarch64-binfmt-P
flags: POCF
offset 0
magic 7f454c460201010000000000000000000200b700
mask ffffffffffffff00fffffffffffffffffeffffff
好了,现在注册了自定义可执行二进制格式的翻译解释程序,那是怎么被触发的呢?看一下代码细节。首先向系统注册 misc_format
二进制格式:
/* fs/binfmt_misc.c */
static struct linux_binfmt misc_format = {
.module = THIS_MODULE,
.load_binary = load_misc_binary,
};
static int __init init_misc_binfmt(void)
{
int err = register_filesystem(&bm_fs_type);
if (!err)
insert_binfmt(&misc_format); /* 注册 misc_format */
return err;
}
/* include/linux/binfmts.h */
/* Same as above, but adds a new binfmt at the top of the list */
static inline void insert_binfmt(struct linux_binfmt *fmt)
{
__register_binfmt(fmt, 1);
}
/* fs/exec.c */
static LIST_HEAD(formats);
static DEFINE_RWLOCK(binfmt_lock);
void __register_binfmt(struct linux_binfmt * fmt, int insert)
{
BUG_ON(!fmt);
if (WARN_ON(!fmt->load_binary))
return;
write_lock(&binfmt_lock);
insert ? list_add(&fmt->lh, &formats) :
list_add_tail(&fmt->lh, &formats);
write_unlock(&binfmt_lock);
}
然后在执行二进制格式时,如果不匹配到任何其它已指格式和架构的时候,在执行 misc_format
中注册的、匹配的某种自定义格式的翻译执行程序:
do_execveat_common()
exec_binprm()
search_binary_handler()
int search_binary_handler(struct linux_binprm *bprm)
{
...
retry:
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {
...
retval = fmt->load_binary(bprm); /* load_misc_binary() */
...
}
...
}
/* fs/binfmt_misc.c */
static int load_misc_binary(struct linux_binprm *bprm)
{
Node *fmt;
struct file *interp_file = NULL;
int retval;
int fd_binary = -1;
...
/* to keep locking time low, we copy the interpreter string */
read_lock(&entries_lock);
fmt = check_file(bprm); /* 找到匹配的格式 */
if (fmt)
dget(fmt->dentry);
read_unlock(&entries_lock);
...
if (fmt->flags & MISC_FMT_OPEN_FILE) {
interp_file = filp_clone_open(fmt->interp_file);
if (!IS_ERR(interp_file))
deny_write_access(interp_file);
} else {
interp_file = open_exec(fmt->interpreter);
}
/* 重置解释器程序,如更新为 /usr/libexec/qemu-binfmt/aarch64-binfmt-P */
bprm->file = interp_file;
...
/* 用新的解释器程序 (如 /usr/libexec/qemu-binfmt/aarch64-binfmt-P) 来执行程序 */
retval = search_binary_handler(bprm);
...
}
到此,整个 qemu-user-static
的主干工作流程已经分析完毕。