1. qemu-user 是什么
本來, 對於 QEmu, 我只知道它是一個模擬器, 可以像 VirtualBox/VMWare 那樣跑一個操作系統, 只不過 QEmu 可以在 AMD64 上面跑針對 PowerPC, ARM 的操作系統, 當然, CPU 指令是解釋執行的, 相對來說比較慢.
但是前幾天折騰 CentOS/Fedora 上面的rpm構建工具mock時才發現, 原來 QEmu 還有一種運行方式, 那就是跟wine
的運行方式相同: 直接運行程序文件.在這種模式下, 這個針對 PowerPC或者ARM編譯的程序, 就比較像一個本地程序, 它跟本機的Linux內核打交道, 進行系統調用, 訪問本地文件(其實是通過qemu進行)和本地設備.
在 QEmu 的術語中, 前面那種運行整個操作系統的方式, 稱為"full system emulation", 在 Ubuntu/CentoS 由軟件包 qemu-system-xxx
(比如qemu-system-ppc
, qemu-system-aarch64
, qemu-system-arm
)提供功能;后面這種運行單個程序文件的方式, 稱為"user mode emulation", 由軟件包qemu-user
或者qemu-user-static
提供功能(注意沒有細分為qemu-user-ppc
, qemu-user-arm
, 不過這也許只是因為這些模擬器文件都不大, 就揉到了一個包里面.至於qemu-user
和qemu-user-static
的區別, 現在只需要知道后者是靜態鏈接版本, 至於在什么場景下需要用到哪一種, 以后再來說).
1.1. 舉個例子
這里舉個例子說明一下應用場景:在樹莓派 2 (CPU是armv7) 上面跑針對 i386 編譯的linux程序.
我在命令行上工作是, 喜歡用一個叫做 fzf 的小程序 (這類程序我以前介紹過: 命令行上的narrowing(隨着輸入逐步減少備選項)工具 - 巴蠻子 - 博客園 ), 但它的早期有個問題: 我日常用的比較多的Linux是在樹莓派上的Raspbian 8, 但fzf自己提供的預編譯版本在Linux上只有amd64
和386
兩個版本, 沒有針對arm
的.這個工具又是用Go語言寫的, 我對這個語言不熟, 也不想去折騰安裝工具鏈在樹莓派上自行編譯.於是就可以試試這條路: 跑i386的版本
$ sudo apt install qemu-user
$ wget -c 'https://github.com/junegunn/fzf-bin/releases/download/0.16.3/fzf-0.16.3-linux_386.tgz'
$ tar xvf fzf-0.16.3-linux_386.tgz
$ file fzf-0.16-3-linux_386
fzf-0.16.3-linux_386: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped
$ qemu-i386 fzf-0.16.3-linux_386 -h
usage: fzf [options]
Search
-x, --extended Extended-search mode
(enabled by default; +x or --no-extended to disable)
[...]
$ history | qemu-i386 fzf-0.16.3-linux_386
上面倒數第三個命令是檢查程序文件fzf-0.16.3-linux_386
的類型, 從結果看它的確是針對386的ELF文件, 並且是靜態鏈接的;倒數 第二個命令 qemu-i386 fzf-0.16.3-linux_386 -h
是試着運行一下, 程序成功地跑起來, 打印除了幫助信息.最后一個命令history | qemu-i386 fzf-0.16.3-linux_386
是真正在使用fzf這個程序的功能.
2. 用binfmt_misc機制來讓啟動運行更方便
上面雖然把這個程序運行起來了, 但命令行上需要將qemu-i386
放在前面, 也就是說實際啟動的qemu-i386
這個程序, 它再把fzf
跑起來.這樣並不太方便, 尤其fzf
這個程序一般都不是直接使用, 而是通過fzf-tmux, fkill等封裝腳本來使用, 腳本里面准備好備選數據后再調用fzf
程序文件來讓用戶挑選, 我們要一一修改這些腳本就太麻煩了.
perl/python腳本就不需要這樣, 只要第一行是#!/usr/bin/perl
或者#!/usr/bin/env python
就可以了.我們能借用這個方法嗎?fzf-0.16.3-linux_386
是個二進制的可執行程序, 我們沒辦法去修改所謂的"第一行";
對了, 有沒有注意到, 安裝wine之后, 命令行上直接輸入notepad.exe
也是可以直接啟動"記事本"程序的, 並不一定需要wine notepad.exe
才能啟動, 這是怎么實現的呢?
這就需要一種叫做binfmt_misc
的機制.
binfmt_misc
是Linux內核說提供的一種擴展機制, 使得更多類型的文件得以成為"可執行"文件.Linux內核本身支持ELF、a.out、腳本(也就是上面所說的第一行#!
指定解釋器的方式)這集中"可執行文件".但它還提供了一個稱為binfmt_misc
的內核模塊, 通過這個模塊可以動態注冊一些"可執行文件格式",注冊之后我們就可以直接"執行"這個程序文件了.
其實上面用apt install qemu-user-static
安裝這個包時, 它的postinstall腳本已經在binfmt_misc
中注冊了相應的配置, 我們可以通過下面的方式檢查一下:
$ lsmod | grep binfmt
binfmt_misc 6306 1
$ ls /proc/sys/fs/binfmt_misc/
python2.7 qemu-cris qemu-mips qemu-ppc64abi32 qemu-sh4eb qemu-x86_64
python3.4 qemu-i386 qemu-mipsel qemu-ppc64le qemu-sparc register
qemu-alpha qemu-m68k qemu-ppc qemu-s390x qemu-sparc32plus status
qemu-armeb qemu-microblaze qemu-ppc64 qemu-sh4 qemu-sparc64
$ cat /proc/sys/fs/binfmt_misc/qemu-i386
enabled
interpreter /usr/bin/qemu-i386-static
flags: OC
offset 0
magic 7f454c4601010100000000000000000002000300
mask fffffffffffefefffffffffffffffffffeffffff
$ xxd fzf-0.16.3-linux_386 | head -2
0000000: 7f45 4c46 0101 0100 0000 0000 0000 0000 .ELF............
0000010: 0200 0300 0100 0000 d090 0908 3400 0000 ............4...
$ ./fzf-0.16.3-linux_386 -h
usage: fzf [options]
Search
-x, --extended Extended-search mode
(enabled by default; +x or --no-extended to disable)
[....]
解釋一下上面幾條命令:
lsmod | grep binfmt
: 這是檢查內核模塊binfmt_misc
是否已經加載, 有內容輸出說明已經加載了.如果沒有加載, 則可以用modprobe binfmt_misc
來加載它(在當前的很多Linux發行版中, 一般可以通過sudo systemctl restart systemd-binfmt
來啟動/重啟它, 修改了注冊配置也可以通過這條命令來重新加載)ls /proc/sys/fs/binfmt_misc/
: 這是檢查內核中目前注冊了哪些格式(register
和status
這兩個除外)cat /proc/sys/fs/binfmt_misc/qemu-i386
: 這是在檢查我們所關心的與qemu-i386相關的配置, 從輸出中可以看到, 對於以7f454c4601010100000000000000000002000300
開頭的文件, 可以調用/usr/bin/qemu-i386-static
來執行(各字段的詳細解釋可以參見binfmt_misc - Wikipedia)xxd fzf-0.16.3-linux_386 | head -2
: 這是檢查一下我們所想運行的程序文件的開頭幾個字節是怎樣的, 從輸出可以看出, 它與上面所注冊的信息是匹配的./fzf-0.16.3-linux_386 -h
: 這是直接運行了這個i386程序, 可以看到它能夠正確打印出幫助信息
關於binfmt_misc
的一些相關鏈接:
- binfmt_misc - Wikipedia
- How programs get run - LWN.net
- Linux binfmt_misc - LuaJIT Wiki: 如何直接執行LuaJIT字節碼
- Using Go as a scripting language in Linux: 因為Go語言不允許第一行用
#!/usr/bin/env go
, 所以需要另外找一個方法來將*.go作為腳本運行 - binfmt_misc for Java - ArchWiki
3. 補充說明:現實並沒有那么簡單/美好
雖然在上面我們成功運行了fzf-0.16.3-linux_386
, 但如果你多實驗幾個程序, 就會發現失敗幾率是比較高的.因為大多數程序都會環境有很多依賴, 比如動態庫依賴、數據文件/配置文件、子進程調用、CPU擴展指令集、環境變量、設備文件等等, 它們的缺失或者錯誤都可能導致程序無法正常運行.很少有只需要單個程序文件就能跑起來的(上面運行的fzf-0.16.3-linux_386
是個靜態鏈接版本, Go語言寫的工具一般都是靜態鏈接的).
對於動態庫依賴、數據文件/配置文件這類文件系統層面的問題, 雖然表面上可以想辦法把文件補齊, 比如Debian/Ubuntu考慮了多架構並存, 但其它Linux發行版並沒有考慮這個問題(有的考慮了x86_64與x86並存), 混合安裝也會給問題定位帶來諸多困難.所以在實際使用中, qemu-user
大都是通過chroot在一個獨立的文件系統中運行的.關於qemu與chroot配合的話題下次再展開吧
Debian/Ubuntu的動態庫都安裝在
.../lib/<target>-<vendor>-<abi>
目錄下, 比如同樣一個動態庫libncurses.so.5.9
, 通過libncurses5:armhf
包提供的動態庫安裝在/lib/arm-linux-gnueabihf/libncurses.so.5.9
,通過libncurses5:i386
包提供的動態庫安裝在/lib/i386-linux-gnu/libncurses.so.5.9
(通過dpkg --add-architecture armhf && apt-get update && apt-get install libc6:armhf
這種方式可以並行安裝多種架構的包)
qemu-user
有一個-L path
選項, 可以用來變更動態庫查找路徑(/set the elf interpreter prefix to 'path'/): 將程序所需要的動態庫都放置到/home/bamanzi/i386-libs/lib
目錄下, 然后用qemu-user -L /home/bamanzi/i386-libs ./prog
來啟動程序, 就會優先到/home/bamanzi/i386-libs/lib
查找prog
所需要的動態庫, 而不是主機里面/etc/ld.so.conf
里面設定的路徑(那些路徑里存放的都是針對主機的動態庫, 在我這個例子里面, 就是針對