EDIT: JITCを有効にしたらちゃんとLinuxも起動した。
EDIT: Cygwinのバグ(?)節を新設。ちょっと動きが怪しい。
qemuはCygwinでの動作をサポートしていない。といってもqemuは実はかなりポータビリティが良く、pthreadをサポートしたPOSIXプラットフォームであればかなり動作する。
今回はqemuの本家をパッチしてqemuをCygwinでビルド、ARM64のEFI Shellを動かしてみた。 (Linux kernelはダメだった) Linuxも動いた。
../configure --prefix=/opt/qemu --disable-strip --enable-plugins --disable-capstone --disable-tpm
↑ のようにしてconfigure、ビルドできる。
/opt/qemu/bin/qemu-system-aarch64.exe -m 512 -cpu cortex-a57 -smp 1 -machine virt -kernel k3os-vmlinuz-arm64 -initrd k3os-initrd-arm64 -nographic -serial mon:stdio -append "console=ttyAMA0"
EFIシェルを起動してみる
とりあえず適当なARM64バイナリを動かしてみようということで、linaroのサイトからビルド済のEFIファームウェアをダウンロードして起動してみる。
ここから QEMU_EFI.fd
をダウンロードして↓のようなコマンドラインで起動すると、1分くらいしてEFI Shellが起動する。
/opt/qemu/bin/qemu-system-aarch64.exe -m 256 -cpu cortex-a57 -smp 1 -machine virt -bios QEMU_EFI.fd -nographic -serial mon:stdio
最初にPXEで待たされるのはご愛嬌。。
UEFI Interactive Shell v2.2
EDK II
UEFI v2.70 (EDK II, 0x00010000)
Mapping table
BLK0: Alias(s):
VenHw(93E34C7E-B50E-11DF-9223-2443DFD72085,00)
Press ESC in 1 seconds to skip startup.nsh or any other key to continue.
Shell>
今回オプションとして -serial mon:stdio
を渡しているので、モニタとシリアル出力が同じ端末に出ることになる。これは CTRL+A
→ c
で切り替えができ、他のコンビネーションは CTRL+A
+ h
で出てくる。
C-a h print this help
C-a x exit emulator
C-a s save disk data back to file (if -snapshot)
C-a t toggle console timestamps
C-a b send break (magic sysrq)
C-a c switch between console and monitor
C-a C-a sends C-a
Cygwinのバグ(?)
移植の過程でいくつかCygwinのバグっぽい挙動を見つけた。テストしたバージョンは3.0.7。
O_NONBLOCK
なpipe(2)がブロックする
qemuでは、非同期I/Oのイベント通知にeventfd(2)か古き良きself-pipeテクニックを使っている。このself-pipeは O_NONBLOCK
が設定されているものの、何故かCygwinではブロックしてしまうことがある。
0 0x00007ffcbe9bcc14 in ntdll!ZwWaitForMultipleObjects () from /cygdrive/c/WINDOWS/SYSTEM32/ntdll.dll
1 0x00007ffcbba67ff7 in WaitForMultipleObjectsEx () from /cygdrive/c/WINDOWS/System32/KERNELBASE.dll
2 0x00007ffcbba67ede in WaitForMultipleObjects () from /cygdrive/c/WINDOWS/System32/KERNELBASE.dll
3 0x00000001800485f5 in cygwait (object=<optimized out>, timeout=timeout@entry=0x0, mask=mask@entry=5) at /usr/src/debug/cygwin-3.0.7-1/winsup/cygwin/cygwait.cc:75
4 0x000000018006b3ba in cygwait (mask=5, howlong=4294967295, h=<optimized out>) at /usr/src/debug/cygwin-3.0.7-1/winsup/cygwin/cygwait.h:45
5 cygwait (howlong=4294967295, h=<optimized out>) at /usr/src/debug/cygwin-3.0.7-1/winsup/cygwin/cygwait.h:51
6 fhandler_base_overlapped::wait_overlapped (this=this@entry=0x1803450e8, inres=<optimized out>, writing=writing@entry=true, bytes=bytes@entry=0xff9fbe9c, nonblocking=true, len=8) at /usr/src/debug/cygwin-3.0.7-1/winsup/cygwin/fhandler.cc:2017
7 0x000000018006bb31 in fhandler_base_overlapped::raw_write (this=0x1803450e8, ptr=0x18035dcd8, len=8) at /usr/src/debug/cygwin-3.0.7-1/winsup/cygwin/fhandler.cc:2201
8 0x0000000180065dbe in fhandler_base::write (this=0x1803450e8, ptr=0x100e2eec0 <value>, len=8) at /usr/src/debug/cygwin-3.0.7-1/winsup/cygwin/fhandler.cc:912
9 0x0000000180143123 in write (fd=7, ptr=0x100e2eec0 <value>, len=8) at /usr/src/debug/cygwin-3.0.7-1/winsup/cygwin/syscalls.cc:1342
10 0x0000000180128deb in _sigfe () at sigfe.s:35
11 0x0000000100959b2d in event_notifier_set (e=e@entry=0x600181294) at /home/oku/repos/qemu/util/event_notifier-posix.c:100
12 0x000000010095606e in aio_notify (ctx=0x6001811f0) at /home/oku/repos/qemu/util/async.c:350
13 0x00000001005449bf in gt_recalc_timer (cpu=0x600219070, timeridx=1) at /home/oku/repos/qemu/target/arm/helper.c:2451
14 0x0000000100544d83 in gt_tval_write (env=0x6002221d0, timeridx=1, value=<optimized out>, ri=<optimized out>) at /home/oku/repos/qemu/target/arm/helper.c:2507
15 0x00000001005914a4 in helper_set_cp_reg64 (env=0x6002221d0, rip=0x60024dce0, value=236350) at /home/oku/repos/qemu/target/arm/op_helper.c:693
16 0x000006ffef8186de in ?? ()
diff --git a/util/event_notifier-posix.c b/util/event_notifier-posix.c
index 00d93204f9..7092fc9e21 100644
--- a/util/event_notifier-posix.c
+++ b/util/event_notifier-posix.c
@@ -49,9 +49,15 @@ int event_notifier_init(EventNotifier *e, int active)
if (errno != ENOSYS) {
return -errno;
}
+#ifdef __CYGWIN__
+ if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) < 0) {
+ return -errno;
+ }
+#else
if (qemu_pipe(fds) < 0) {
return -errno;
}
+#endif
ret = fcntl_setfl(fds[0], O_NONBLOCK);
if (ret < 0) {
ret = -errno;
ちゃんと追うのが面倒だったので、ここではsocketpair(2)でお茶を濁している。socketpairを使うと、pipeのような自分自身に接続されたfdをUNIXドメインソケットを使って得ることができる。pipeのようなバグはsocketには無いようだ。
Cygwinのコードでは、同期I/OがブロックするかどうかをWin32 GetLastError
で検出していて、これはPOSIXの errno
同様にThread Local Storageであるためスレッドセーフになっていないのが原因ではないかと思う。ただコレは普通にpipeを使っていれば再現するはずなので外しているかもしれない。
MAP_FIXED
でマップ済みの領域を再マップできない
qemu内部のmmap(2)が EINVAL
で失敗してしまう。こういうときはstrace( http://cygwin.net/cygwin-ug-net/strace.html )でsyscallトレースを取って調べるのが定石なのでやってみると、
3290 378355 [main] qemu-system-aarch64 42663 mmap64: addr 0x0, len 67174400, prot 0x0, flags 0x22, fd -1, off 0x0
836 379191 [main] qemu-system-aarch64 42663 mmap64: 0x6FFEB8C0000 = mmap()
34 379225 [main] qemu-system-aarch64 42663 mmap64: addr 0x6FFEB8C0000, len 67108864, prot 0x3, flags 0x32, fd -1, off 0x0
39 379264 [main] qemu-system-aarch64 42663 mmap_record::map_pages: map_pages (addr=0x6FFEB8C0000, len=67108864)
49 379313 [main] qemu-system-aarch64 42663 __set_errno: bool mmap_record::map_pages(caddr_t, SIZE_T):465 setting errno 22
40 379353 [main] qemu-system-aarch64 42663 mmap64: 0xFFFFFFFFFFFFFFFF = mmap()
このように、 flags 0x32
= MAP_ANONYMOUS | MAP_FIXED | MAP_SHARED
のようにしているところでエラー errno 22
= EINVAL
になっていることが判る。
これは qemu_ram_mmap
でガードページ付きのアロケーションを実施しているところなので、ガードページをガン無視して良ければ(= メモリ領域外部のアクセスをクラッシュにする必要が無ければ) MAP_FIXED
は消しても動く。
というわけで消してしまった。
EINVALが発生する理由はよくわからない。基本的にはこのような上書きmapは合法のはずで、Cygwin側の制約なんじゃないかという気はする。(一旦unmapしてからやる方法ではatomic性が保証できないが、qemuのユースケースではatomic性を要求していないので専用のルーチンを用意すれば解決はするはず)
必要なパッチ
いきなりqemuをチェックアウトしてビルドしても正常にビルドできない。今回用意した cygwin
ブランチではいくつかパッチしている。
Cygwinをビルド可能なプラットフォームとして認識させる
diff --git a/configure b/configure
index 6099be1d84..78c1154e64 100755
--- a/configure
+++ b/configure
@@ -653,6 +653,8 @@ elif check_define __NetBSD__; then
targetos='NetBSD'
elif check_define __APPLE__; then
targetos='Darwin'
+elif check_define __CYGWIN__; then
+ targetos='Cygwin'
else
# This is a fatal error, but don't report it yet, because we
# might be going to just print the --help text, or it might
Cygwinのプラットフォーム検出は __CYGWIN__
(直球) となっている。ちなみに __CYGWIN32__
のようなbitness定義は無いということになっている。
__CYGWIN32__
is not defined in the 64 bit toolchain. This may hit a few projects which are around since before Y2K. Check your project for occurences of__CYGWIN32__
and change them to__CYGWIN__
, which is defined in the Cygwin toolchain since 1998, to get the same Cygwin-specific code changes done.
mlockall
を諦める
Cygwinにはmlockallが無いため諦める。真面目にエミュレーションしても良いかもしれないけど多分ユースケースが無い。
diff --git a/os-posix.c b/os-posix.c
index 86cffd2c7d..2ca183f6c9 100644
--- a/os-posix.c
+++ b/os-posix.c
@@ -352,6 +352,9 @@ bool is_daemonized(void)
int os_mlock(void)
{
+#ifdef __CYGWIN__
+ return 0;
+#else
int ret = 0;
ret = mlockall(MCL_CURRENT | MCL_FUTURE);
JOB_STATUS_PAUSED
の定義の衝突
Cygwinにインスールされる <libusb.h>
は定義の互換のために <windows.h>
をincludeしており、これがWin32 APIの定数とqemuソースコードの enum
との衝突を招く。
In file included from /usr/include/w32api/windows.h:102:0,
from /usr/include/libusb-1.0/libusb.h:70,
from /home/oku/repos/qemu/hw/usb/host-libusb.c:40:
./qapi/qapi-types-job.h:36:5: エラー: expected identifier before numeric constant
JOB_STATUS_PAUSED,
^
CC migration/block-dirty-bitmap.o
CC migration/block.o
make: *** [/home/oku/repos/qemu/rules.mak:69: hw/usb/host-libusb.o] エラー 1
というわけで、安直に #undef
してしまう。。
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index fcf48c0193..eee977bad0 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -38,6 +38,7 @@
#include <poll.h>
#endif
#include <libusb.h>
+#undef JOB_STATUS_PAUSED
これはどちらかというとlibusbのヘッダの方がよくない(環境間でpull-inする定義が多すぎる)が、既存のプロジェクトを壊さないという目的上こうなっているようだ。
/* 'interface' might be defined as a macro on Windows, so we need to
* undefine it so as not to break the current libusb API, because
* libusb_config_descriptor has an 'interface' member
* As this can be problematic if you include windows.h after libusb.h
* in your sources, we force windows.h to be included first. */
#if defined(_WIN32) || defined(__CYGWIN__) || defined(_WIN32_WCE)
#include <windows.h>
#if defined(interface)
#undef interface
#endif
不要な <sys/syscall.h>
のincludeをやめる
signalfd(2)をエミュレートするのに <sys/syscall.h>
のincludeは要らないのでifdefする。
もっとも、実は今年リリースされたCygwin 3.0以降では signalfd
をAPIとしてサポートしている( https://cygwin.com/ml/cygwin-announce/2019-02/msg00010.html )。用意されているcompat関数はsignalfdをスレッドを使ってエミュレートするものなので、ネイティブのAPIを使った方が効率的かもしれない。
このsignalfdの用法はKVMに由来する( https://github.com/okuoku/qemu/commit/dcc38d1cce61e7d986e74dcd9bff2a5efc53a87c )。libc側を使わない理由は謎。
TCGで正しいWin64 ABIを使用する
EDIT: 追記。最初はこれをやっていなくてJITCを使うとクラッシュしていた。
Cygwinでは一般的なELFプラットフォームと異なり、Win64の呼び出し規約を使用する。このとき、破壊されるレジスタやスタックの配置はABIに合わせて対応する必要があるが、qemuは何故かこの判定を _WIN64
のdefineで行っているので、configure
側で追従する必要がある。
diff --git a/configure b/configure
index 7b553b84e5..2145f76303 100755
--- a/configure
+++ b/configure
@@ -894,6 +894,7 @@ Linux)
libudev="yes"
;;
Cygwin)
+ QEMU_CFLAGS="-D_WIN64 $QEMU_CFLAGS"
cygwin="yes"
;;
esac
...実際、このWin64方式のamd64 ABIにはあんまり良い名称が無く(Windows以外ではCygwinとBIOS類くらいでしか採用例が無い)、 _WIN64
で検出してしまうのは止むを得ない気はする。
外部デバイスへの対応
外部と通信できないと面白くないので、USBとEthernetには対応してみた。
TAP-win32に対応する
qemuではL2TP(= UDP)を使ったEthernetフレーム転送をサポートしているためそっちを使えば良いが、準備が面倒なのでTAP-win32を使えるようにした。
- https://github.com/okuoku/qemu/commit/2db0cf765a2fafc708a7452392a0843981626b5c - ビルド周り
-
https://github.com/okuoku/qemu/commit/49ecb0b48d1bd8dce3b38d7c59ed2fbde423a0f3 - self-pipeによる
qemu_add_wait_object
の実装
qemuのTAP-win32実装では、バッファ通知をWin32 Semaphoreで行っている。POSIXでのメインループ util/main-loop.c
はfd以外のものを待つことができないので、Semaphoreとcygwinのfdを繋ぐものが必要になる。今回は独立スレッドを用意してself-pipeした。deleteの実装はサボっている。
usbredir を移植する
TBD: コレは意外と面白そうなので別立てにする。
USBデバイスも使いたかったのでUSBトラフィックをTCP/IPにencapsulateするusbredirをCygwinに移植してみた。元々はqemuとKVMを使用したリモートデスクトップソリューションであるSPICEのために開発されたプロトコル( https://www.spice-space.org/usbredir.html )で、既存のUSB/IPと異なりデバイスのディスカバリのような機能が無い。
↑ のTAP-win32のケースと同じく、usbredirserver
のメインループはfdしか待てないため、独立スレッドを用意してUSBイベントを一括で処理するようにしている。
SPICEでは汎用のフィルタドライバUSBDk( https://github.com/daynix/UsbDk )のサポートも提供しているが、USB-NIC程度であればZadig( https://zadig.akeo.ie/ )からインストールできるlibusbkで十分と言える。
TODO
あんまり真面目に追ってないがそのうちやるかもしれない:
-
Linuxを動かす 。動いた方が面白いけどTCI(インタプリタ側)のバグのような気もする。。やった。 -
JITCするようにする 。移植するのに何が必要なのかまだ調べてない。やった。 - capstone検出を治す 。capstone( http://www.capstone-engine.org/ )は良い逆アセンブラで、たぶん検出側を多少修正すればそのままCygwinでも使えると思う。
- TPMがビルドできないのを治す 。...TPM要る。。?