我见过最全的剖析QEMU原理的文章[Z]_no qemu-程序员宅基地

技术标签: qemu 原理  QEMU  

 

 

转自:

http://people.cs.nctu.edu.tw/~chenwj/dokuwiki/doku.php?id=qemu

 

 

How To Become A Hacker 写给新手程序员的一封信

目录

QEMU 的簡介請見 QEMU internals

建置 QEMU

QEMU 1.0 預設會編譯成 PIE,這對舊版的 GDB 會有影響。強制開啟 IO thread。使用 Clang 編譯 QEMU 會出現以下訊息1)2)3)

In file included from /z/tmp/chenwj/qemu-1.0/user-exec.c:21:/z/tmp/chenwj/qemu-1.0/dyngen-exec.h:64:20: error: global register variables are not supportedregister CPUState *env asm(AREG0);                   ^1 warning and 1 error generated.

TCI 沒有用到 global register variable。

$ ../qemu-1.0/configure --target-list=i386-bsd-user --enable-tcg-interpreter --cc=clang \  --extra-cflags="-v" --disable-smartcard-nss        

User Mode

目前 QEMU 將 glib2.0 列為必要,The GTK+ Project4)

  1. 如果系統沒有安裝 glib,則自行安裝。

    $ apt-get install zlib1g-dev pkg-config libglib2.0-dev$ wget http://ftp.gnome.org/pub/gnome/sources/glib/2.28/glib-2.28.0.tar.gz; tar xvf glib-2.28.0.tar.gz$ mkdir build; cd build$ ../glib-2.28.0/configure --prefix=$INSTALL$ make install                    

  2. 下載 QEMU。

                            # http://wiki.qemu.org/download/qemu-0.15.1.tar.gz$ git clone git://git.qemu.org/qemu.git

  3. 修改 configure。

                            # glib support probe#if $pkg_config --modversion gthread-2.0 > /dev/null 2>&1 ; then    # 用 pkg-config 查詢    glib_cflags="-pthread -I$INSTALL/include/glib-2.0/ -I$INSTALL/lib/glib-2.0/include/"    glib_libs="-L$INSTALL/lib -pthread -lgthread-2.0 -lrt -lglib-2.0"    LIBS="$glib_libs $LIBS"    libs_qga="$glib_libs $libs_qga"#else# echo "glib-2.0 required to compile QEMU"# exit 1#fi                    

  4. 編譯並安裝 QEMU。

                            # config.log 可以用來檢查設定 QEMU 時哪裡出錯$ mkdir build; cd build$ ../qemu/configure --prefix=$INSTALL --target-list=i386-linux-user --enable-debug$ make; make install                    

  5. 如果 glib 是自行安裝。

    $ export LD_LIBRARY_PATH=$INSTALL/lib/# -m32 會編譯 32-bit 執行檔$ gcc -m32 hello.c -o hello$ qemu-i386 hello

SPARC

關於 SPARC V8 (32-bit)、V8PLUS (64-bit with 32-bit ABI) 和 V9 (64-bit) 架構的差異請見 ABI Compliance and Global Registers Usage in SPARC V8 and V9 Architecture。QEMU 對 v9 支援最完整 5)。64-bit with 32-bit ABI 和在 x86_64 上運行 x86 binary 不一樣。64-bit with 32-bit ABI 代表程式可以存取 64-bit 硬體資源,但其內存空間仍只侷限在 32-bit; x86 binary 運行在 x86_64 上不能存取 64-bit 硬體資源 6)

$ uname -aLinux sparc 2.6.37-rc5-git #1 SMP Tue Dec 21 17:03:53 CST 2010 sparc64 sun4v UltraSparc T2 (Niagara2) GNU/Linux$ file /bin/ls/bin/ls: ELF 32-bit MSB executable, SPARC32PLUS, V8+ Required, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.9, stripped# 雖然機器是 64-bit,但是裝的是 32-bit OS# --sparc_cpu=v9 改用 sparc64-linux-gcc$ configure --prefix=$INSTALL --target-list=i386-linux-user --sparc_cpu=v8plus

PowerPC

因為 PS3 記憶體不足,可能導致 GCC 編譯 QEMU 失敗 7)8)。改以 debug 模式編譯。

  1. 註解 assert。

                            --- qemu-0.13.0/tcg/tcg.c.orig 2010-10-16 04:56:09.000000000 +0800+++ qemu-0.13.0/tcg/tcg.c 2011-02-24 09:46:28.796899001 +0800@@ -1031,7 +1031,7 @@         def = &tcg_op_defs[op]; #if defined(CONFIG_DEBUG_TCG)         /* Duplicate entry in op definitions? */- assert(!def->used);+ //assert(!def->used);         def->used = 1; #endif         nb_args = def->nb_iargs + def->nb_oargs;

  2. 以 debug 模式編譯,或是參考 http://cruxppc.org/viewvc/opt/branches/2.7/qemu/Pkgfile?revision=2267&view=markup9)

    $ configure --prefix=$INSTALL --target-list=i386-linux-user --enable-debug$ make install                    

  3. 拷貝 x86 函式庫供 powerpc 上的 qemu-i386 使用。函式庫必須放在以 lib 為名的目錄底下。

    $ mkdir $HOME/tools/lib$ cp /lib32/ld-linux.so.2 $HOME/tools/lib$ cp /lib32/libc.so.6 $HOME/tools/lib$ export LD_LIBRARY_PATH=$HOME/tools/lib$ qemu-i386 -L $HOME/tools hello

ARM

  1. 安裝交叉工具鏈。使用預編譯好的工具鏈。可以到 Sourcery G++ Lite Edition 下載。或者使用 crosstool-NG 建立工具鏈。

    1. 下載並安裝 crosstool-ng。

                                      # 預設安裝在 $HOME/x-tools 下。# 注意運行 QEMU 平台的內核,這裡要選較其為舊的內核建立工具鏈。# 請記得將 crosstool.config 更名為 .config。$ wget http://ymorin.is-a-geek.org/download/crosstool-ng/crosstool-ng-1.9.2.tar.bz2; tar xvf crosstool-ng-1.9.2.tar.bz2$ cd crosstool-ng-1.9.2;$ ./configure --prefix=$INSTALL$ make install                            

    2. 建置交叉工具鏈。

      $ mkdir toolchain-build$ cp $INSTALL/lib/ct-ng-1.9.2/samples/arm-unknown-linux-gnueabi/* .$ mv crosstool.config .config$ ct-ng menuconfig$ ct-ng build

  2. 安裝運行時期函式庫。如果要安裝預編譯好的運行時期函式庫。可以到 linux-arm.org 下載。

                            # mount cramfs 要有 root 權限。可以改採下面的作法,使用 fakeroot-ng。$ wget http://www.linux-arm.org/git?p=ael.git;a=blob_plain;f=filesystem/bin/alip-ael-armv6-full-reduced.cramfs;hb=2010q4$ fakeroot-ng# /sbin/cramfsck filesystem_bin_alip-ael-armv6-full-reduced.cramfs -x runtime/# cp runtime/lib/* /path/to/runtime/lib# exit$

    也可以使用 crosstool-NG,在建立工具鏈時同時也會得到運行時期函式庫。預設安裝在 $HOME/x-tools/arm-unknown-linux-gnueabi/arm-unknown-linux-gnueabi/sys-root/lib。PowerPC64 需要為 sys-root/lib64 和 sys-root/usr/lib64 建立軟鏈結,請 chmod u+w sys-root 和 chmod u+w sys-root/usr。

  3. 執行 QEMU。假設 runtime 裝在 /path/to/runtime/lib。

    $ qemu-arm -L /path/to/runtime hello$ qemu-ppc64 $HOME/x-tools/powerpc64-unknown-linux-gnu/powerpc64-unknown-linux-gnu/sys-root hello

System Mode

  1. 編譯 QEMU。

    $ wget http://download.savannah.gnu.org/releases/qemu/qemu-0.14.1.tar.gz; tar xvf qemu-0.14.1.tar.gz$ mkdir build; cd build# i386-softmmu 執行檔名稱為 qemu。將在 QEMU 1.0 修正。$ ../qemu-0.14.1  --prefix=$INSTALL --target-list=i386-softmmu,arm-softmmu$ make && make install                    

  2. 編譯內核映像。

    • Lab 2 編譯 kernel

                                      # 取得 arch/i386/boot/bzImage 和 vmlinux 兩個文件# kernel hacking 開啟 debug$ wget http://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.20.tar.gz; tar xvf linux-2.6.20.tar.gz# http://blog.csdn.net/livingpark/article/details/3732679# 在 scripts/mod/sumversion.c 加上 #include <limits.h>$ cd linux-2.6.20# make mrproper 清理原始檔和設定$ make mrproper; make ARCH=i386 menuconfig; make ARCH=i386

  3. 準備 root file system。如果沒有 root 權限的話,可以安裝 fakeroot 或是 fakeroot-ng,或是用 buildroot

    • Lab 3 製作 root filesystem

    • Creating a Root File System for Linux on OMAP35x

    • Kernel與initrd的基本建造

    • Create root filesystem step by step

      $ wget http://busybox.net/downloads/busybox-1.18.5.tar.bz2; tar xvf busybox-1.18.5.tar.bz2$ cd busybox-1.18.5# Enable Busybox setting/Build Options/Build busybox as static binary$ make menuconfig$ make; make install$ cd ..; fakeroot-ng;# mkdir root; cd root# mkdir dev bin# mknod dev/console c 5 1# mknod dev/tty2 c 4 2# mknod dev/tty3 c 4 3# mknod dev/tty4 c 4 4# cp ../busybox-1.18.5/busybox bin# ln -s bin/busybox init# cd bin# ln -s busybox sh# cd ..# find . | cpio -o -H newc | gzip > ../initramfs                            

      或是直接使用 QEMU 官網上的測試檔。

      $ wget http://wiki.qemu.org/download/arm-test-0.2.tar.gz; tar xvf arm-test-0.2.tar.gz$ cd arm-test# zImage.integrator 是內核文件。arm_root.img 是根文件系统。# gzip -dc arm_root.img | cpio -idv$ qemu-system-arm -kernel zImage.integrator -initrd arm_root.img -nographic -append "console=ttyAMA0"$ wget http://wiki.qemu.org/download/minix204.tar.bz2; tar xvf minix204.tar.bz2$ cd minix204$ qemu -m 8M -fda vfloppya.img -hda minix204.img -boot c -vnc 0.0.0.0:1$ wget http://wiki.qemu.org/download/linux-0.2.img.bz2$ bunzip2 linux-0.2.img.bz2# 使用 vncviwer 連至 server:1 即可得到輸出。# 使用非常類似的 2.6.20 config。# http://bellard.org/jslinux/linuxstart-20110820.tar.gz#qemu -S -kernel bzImage -hda linux-0.2.img -append "root=/dev/hda" -vnc 0.0.0.0:1$ qemu linux-0.2.img -vnc 0.0.0.0:1                            

  • 可以用 E2tools 或 guestfish 修改硬盤映像。或是在 output/targets 放置檔案,再重新 make

    $ /sbin/e2fsck -f rootfs.ext2# 先把 rootfs 放大$ /sbin/resize2fs rootfs.ext2 5G$ e2mkdir rootfs.ext2:/root/tmp/$ e2cp SPEC_CPU2006_v1.1.tgz rootfs.ext2:/root/tmp/                    

  • -kernel <kernel image> : -kernel 参数后面接内核映像文件名,如果不指定路径,则在当前目录下寻找。-kernel bzImage 表示指定 bzImage 作为内核镜像文件。

  • -initrd <initial ram disk image> : 需要为内核加在的初始 RAM 磁盘,里面包含文件系统。

  • -hda linux-0.2.img 表示 linux-0.2.img 中的文件系统都放在 hda 中。

  • -nographic : 由于这里仿真的目标板没有图形设备,所以我们也没必要让 QEMU 仿真图形设备,所以这里使用此参数。

  • -append <parameters> : 一般的,我们在启动 linux 时,我们有时需要给内核传递一些参数,比如 init=/etc/init 指定内核启动后,最后将从运行位于 /etc 目录下的 init 文件 (该文件默认位于 /sbin/ 目录下)。这里,我们设置 console=ttyAMA0 表示内核使用 /dev/ttyAMA0 设备作为控制台,该设备由 QEMU 创建,这样目标板就有了一个作为终端的伪串行设备。-append "root=/dev/hda" :-append 后接命令行参数,这里表示根文件系统设备使用 hda 。

退出 QEMU 操作为:Ctrl + A ,然后按下 X 键。注意,它不会提醒你是否要退出,而是直接退出,所以操作时要小心。

QEMU 的 LInux 映像檔裡包含 NBench,分數越高越好。要切換到 nbench 的目錄底下執行 nbench 這隻測試程式,否則會噴出以下訊息:

CPU:NNET--error in opening file!

製作 QEMU 官網上的硬盤映像。

  1. Create the RAM disk whose size is 64M.

    $ dd if=/dev/zero of=disk.img bs=4096 count=16384                    

  2. Partition the disk.

    $ /sbin/fdisk -C 16065 -H 255 -S 63 disk.img

  3. Format the filesystem.

     

  4. Copy file system.

     

  5. Build the bootloader by grub.

     

  6. Boot system with your new disk.

     

shutdown 和 reboot 不會導致重開機,exit 登出 shell 會導致重開機。linux-0.11 登出後不會重開機10)

# -no-shutdown -no-reboot: 登出後,出現 "machine restart" 之後停住。(qemu) system_powerdown 無反應(qemu) system_reset QEMU 停止運作# -no-reboot: 登出後,離開 QEMU。(exit instead of rebooting)(qemu) system_powerdown 無反應(qemu) system_reset 關閉 QEMU# -no-shutdown: 登出後,重啟系統。(stop before shutdown)(qemu) system_powerdown 無反應(qemu) system_reset 重啟系統# 不加 -no-shutdown -no-reboot(qemu) system_powerdown 無反應(qemu) system_reset 重啟系統

安裝系統

$ wget http://releases.ubuntu.com/12.04/ubuntu-12.04-server-amd64.iso$ qemu-img create -f qcow2 ubuntu_x86_64.qcow2 3G$ qemu-system-x86_64 -m 1024M -boot d -cdrom ubuntu-12.04-server-amd64.iso \  -hda ubuntu_x86_64.qcow2 -vnc 0.0.0.0:1$ qemu-system-x86_64 -m 1024M -hda ubuntu_x86_64.qcow2 -vnc 0.0.0.0:1        
$ wget http://people.debian.org/~aurel32/qemu/armel/vmlinuz-2.6.32-5-versatile$ wget http://people.debian.org/~aurel32/qemu/armel/initrd.img-2.6.32-5-versatile$ wget http://people.debian.org/~aurel32/qemu/armel/debian_squeeze_armel_desktop.qcow2$ qemu-system-arm -M versatilepb -kernel vmlinuz-2.6.32-5-versatile \  -initrd initrd.img-2.6.32-5-versatile \  -hda debian_squeeze_armel_desktop.qcow2 -append "root=/dev/sda1" -vnc 0.0.0.0:1# 請到 http://ftp.de.debian.org/debian/dists/ 找類似 Debian6.0.4/main/installer-armel/current/images/versatile/netboot# 的路徑,下載 initrd.gz。$ qemu-img create -f qcow2 hda.img 3G$ qemu-system-arm -M versatilepb \  -kernel vmlinuz-2.6.32-5-versatile -initrd initrd.gz \  -hda hda.img -append "root=/dev/ram" -vnc 0.0.0.0:1# 安裝完後,關閉 QEMU。並改用底下指令開機。$ qemu-system-arm -M versatilepb \  -initrd initrd.img-2.6.32-5-versatile \  -hda hda.img -append "root=/dev/sda1" -vnc 0.0.0.0:1# 或是改從 console 輸出。$ qemu-system-arm -M versatilepb \  -initrd initrd.img-2.6.32-5-versatile \  -hda hda.img -append "root=/dev/sda1 console=ttyAMA0" -nographic# 下載 linux kernel tarball,解開後在 arch/arm/configs 找 config。$ wget 'http://linux-arm.org/git?p=ael.git;a=blob_plain;f=kernel/config/config-ael-2011.06.00-realview-v7-smp-thumb;hb=2011.06'        

Buildroot

            # http://buildroot.uclibc.org/downloads/snapshots/buildroot-snapshot.tar.bz2$ wget http://buildroot.uclibc.org/downloads/buildroot-2011.11.tar.gz; tar vxf buildroot-2011.11.tar.gz$ cd buildroot-2011.11# for kernel,請見 arch/i386/defconfig 或 arch/arm/configs。make help 可以看到更多資訊。# 先產生 qemu 用的 default config。$ make qemu_x86_defconfig# 設定 linux kernel version。最後生成的 rfs 格式,initramfs。# 注意編譯 kernel 時所用的 config 從哪裡來。## - gzip, tar## - rpm## - dropbear: openssh 會一直要求改密碼。#$ make menuconfig$ make# 使用 QEMU 內部的 DHCP 以便存取網路。$ vi output/target/etc/network/interfacesauto eth0iface eth0 inet dhcp$ cd output/images/# 建議先轉成 qcow2 以便動態增長硬盤大小。rootfs.qcow2 只能增長成原本硬盤映像的大小。$ qemu-img convert -O qcow2 rootfs.ext2 rootfs.qcow2$ qemu-img resize rootfs.qcow2 +3G$ qemu-img info rootfs.qcow2image: rootfs.qcow2file format: qcow2virtual size: 3.2G (3399145472 bytes)disk size: 159Mcluster_size: 65536$ qemu-system-i386 -kernel bzImage -initrd rootfs.cpio -vnc 0.0.0.0:1$ qemu-system-i386 -kernel bzImage -hda rootfs.ext2 -append "root=/dev/sda console=ttyAMA0" -nographic \  -redir tcp:2222::22 \  -redir tcp:3333::3333# 登入後,修改密碼。$ passwd new_passwd# 修改密碼若失敗,請修改 /etc/sshd_config,在其中將 UsePrivilegeSeparation 開啟:# UsePrivilegeSeparation yes$ ssh -p 2222 root@localhost

有些套件需要 wchar 支援。12)

Monitor

# 有時候會遇到 vnc 無法開啟的情況,使用 screendump 輸出螢幕內容。# 再用 http://www.online-convert.com 轉成 jpeg 觀看。(qemu) screendump screenshot.ppm
(qemu) info jitTranslation buffer state:gen code size       3785504/4089856TB count            11244/32768TB avg target size  14 max=595 bytesTB avg host size    336 bytes (expansion ratio: 23.4)cross page TB count 25 (0%) // guest TB 對映的 guest binary 跨 guest page 的個數和比例。direct jump count   7569 (67%) (2 jumps=5173 46%)Statistics:TB flush count      0TB invalidate count 2737TLB flush count     13869
# 印出客戶機虛擬位址處的內容。(qemu) x /2 0x08d3100808d31008: 0x64636261 0x00006665# 印出客戶機物理位址處的內容。(qemu) xp /2 0x016030080000000001603008: 0x64636261 0x00006665# 顯示內存、MMIO、IO 地址分佈。# 以縮排顯示階層關係。(qemu) info mtreeI/O0000000000000000-000000000000ffff (prio 0): io  0000000000000020-0000000000000021 (prio 0): pic

使用 Ctrl + Alt + 2 组合键切换到 QEMU 终端 (QEMU Monitor),然后输入 gdbserver ,启动 gdbserver 服务。 这时启动另外一个终端窗口,输入 gdb vmlinux 命令进行调试:

            (gdb) target remote localhost:1234               /*使用 gdbserver 进行调试命令*/

或是將 QEMU 終端導至標準輸出。

$ qemu -hda linux-0.2.img -vnc 0.0.0.0:1 -monitor stdio

Snapshot

  1. 先確定裝置格式支援 snapshot。raw 不支援 snapshot。Monitor

    (qemu) info blockide0-hd0: removable=0 io-status=ok file=linux-0.2.img ro=0 drv=raw encrypted=0ide1-cd0: removable=1 locked=0 tray-open=0 io-status=ok [not inserted]floppy0: removable=1 locked=0 tray-open=0 [not inserted]sd0: removable=1 locked=0 tray-open=0 [not inserted](qemu) savevmDevice 'ide0-hd0' is writable but does not support snapshots.

  2. 用 qemu-img 轉換硬盤映像成 qcow2 格式。

    $ qemu-img convert -O qcow2 linux-0.2.img linux-0.2.qcow2

  3. 在 monitor 在下 savevm 把 RAM、device state 和 disk 存到當前使用的硬盤映像13)

    (qemu) savevm(qemu) loadvm(qemu) info snapshots

Network

QEMU 可以透過虛擬網卡連至 vlan,vlan 之間可以透過 slirp,socket,tap 或是 vde 連接。在 Documentation/Networking 提到的 virtual network device 即為虛擬網卡,network backend 負責將虛擬網卡送出的資料放到真實網路上,可以是 slirp,socket,tap 或是 vde。QEMU Networking 裡面的 usermode network stack 即為 slirp,slirp 為預設網路後端,見下圖。QEMU 內部會啟動一個 DHCP 伺服器,宿主作業系統不可見。

  1. 存取宿主機的資料。

                            # 在 host 執行底下命令。$ cd path/to/shared/files && python -m SimpleHTTPServer# 在 guest 執行底下命令。$ wget http://10.0.2.2:8000/README

  2. 存取客戶機的資料。

                            # 將 host TCP 連線 127.0.0.1:8000 導到 guest 內部 DHCP 預設 IP 埠 8000。$ qemu-system-x86_64 -m 1024M -hda ubuntu_x86_64.qcow2 \  -net user,hostfwd=tcp:127.0.0.1:8000-:8000 \  -vnc 0.0.0.0:1$ wget localhost:8000/README

$ qemu-system-i386 -kernel bzImage-x86 -hda disk-x86.raw -append "root=/dev/sda" \  -net nic -net tap,ifname=tap0,script=no -vnc :3 -monitor stdio

Tracing

請見 docs/tracing.txt。

$ wget http://download.savannah.gnu.org/releases/qemu/qemu-0.14.0.tar.gz$ tar xvf qemu-0.14.0.tar.gz$ vim qemu-0.14.0/trace-events # qemu-malloc.cqemu_malloc(size_t size, void *ptr) "size %zu ptr %p"qemu_realloc(void *ptr, size_t size, void *newptr) "ptr %p size %zu newptr %p"qemu_free(void *ptr) "ptr %p" $ make install build; cd build;$ ../qemu-0.14.0/configure --prefix=/path/to/install --target-list=i386-linux-user --enable-trace-backend=stderr$ ./path/to/install/qemu-i386 hello

Cross Compile

  1. 需要 zlib 的 ARM binary。

    $ wget http://zlib.net/zlib-1.2.5.tar.gz; tar xvf zlib-1.2.5.tar.gz# export PATH=/PATH/TO/CROSSTOOL:$PATH$ CC=arm-none-linux-gnueabi-gcc ./configure \  --prefix=$INSTALL                    

  2. 執行底下腳本。

                            #!/bin/bash SOURCE_DIR=../user-modeZLIB_PREBUILT=$HOME/x-toolsTARGET_LIST=arm-linux-userCROSS_PREFIX=arm-none-linux-gnueabi-CPU=armv7lCFLAGS=-I$ZLIB_PREBUILT/includeLDFLAGS=-L$ZLIB_PREBUILT/libPREFIX=`pwd`OPT_FLAGS="--disable-sdl"DEBUG=--disable-stripSTATIC=--static $SOURCE_DIR/configure --target-list=$TARGET_LIST \  --cpu=$CPU \  --extra-ldflags=$LDFLAGS  \  --extra-cflags=$CFLAGS \  --cross-prefix=$CROSS_PREFIX \  --prefix=$PREFIX                    

Debug & Testing

$ git clone git://git.qemu.org/qemu-test.git$ cd qemu-test$ git submodule update --init$ make        

舊版 GDB 無法處理 PIE。

$ ../qemu-1.0/configure --prefix=$INSTALL --target-list=i386-softmmu --enable-debug --disable-pie        
            (gdb) handle SIG38 noprint pass# 定位感興趣的 guest pc 以便查找其在 code cache 中的位址。(gdb) b gen_intermediate_code_internal if tb->pc == 0x801a3bc# 此時已生成 host binary,查找 qemu.log。(gdb) b tb_find_fast

QEMU 內部會註冊 SIGNAL 供自己使用,例如 IO thread 執行完畢之後會發送 SIGNAL 給 vcpu thread。

  1. 在 qemu_tcg_init_cpu_signals (cpus.c) 註冊 QEMU 內部使用的 SIGNAL。

                            static void qemu_tcg_init_cpu_signals(void){    sigset_t set;    struct sigaction sigact;     memset(&sigact, 0, sizeof(sigact));    sigact.sa_handler = cpu_signal;    sigaction(SIG_IPI, &sigact, NULL);     sigemptyset(&set);    sigaddset(&set, SIG_IPI);    pthread_sigmask(SIG_UNBLOCK, &set, NULL);}                    

  2. cpu_signal 會將執行從 code cache 拉出來,回到 QEMU 處理 singal。

                            static void cpu_signal(int sig){    if (cpu_single_env) {        cpu_exit(cpu_single_env);    }    exit_request = 1;}                    

Internal

QEMU 的簡介請見 QEMU internals

QEMU 中的 target 有兩種意義,

  1. 描述被模擬的硬體

  2. 對 TCG 而言,target 描述產生何種宿主硬體代碼

QEMU 0.9 版以前使用 dyngen,對於 dyngen 的描述可以參考以下文件。QEMU 0.10 以後改採 TCG。可以從下載 QEMU Source Archive 源碼。

請先閱讀 Documentation/GettingStartedDevelopers

  • QEMU does not have a high level design description document - only the source code tells the full story 8-)

  • HACKING 、CODING_STYLE 和 tcg/README。

  • QEMU 使用宏展開。編譯時加上 –extra-cflags="-save-temps" 可以得到宏展開之後的檔案 *.i。

  • 有些註解是 QEMU 0.9 以前的殘留。

  • qemu patchlist 保留早期 QEMU 的 log。

簡單的 patch 請送到 ,請見 Contribute/TrivialPatches

源碼目錄概觀

  • target-ARCH/

    • 定義被模擬硬體,反匯編

  • OS-user/

    • 作業系統相關

  • tcg/

    • 定義如何生成宿主平台指令

Memory

QEMU 1.0 將有變動。請見 HACKING、docs/memory.txt 或 Planning/1.0

請見 memory.[ch]。

  • target_phys_addr_t (targphys.h) 代表客戶機物理地址空間。如果客戶機是 x86 開啟 PAE 的話,target_phys_addr_t 為 64 bit。

  • target_ulong 代表客戶機暫存器大小和虛擬地址空間。如果客戶機是 x86 開啟 PAE 的話,target_ulong 為 32 bit。

  • ram_addr_t (cpu-common.h) 代表宿主機虛擬地址空間。如果宿主機是 x86 的話,ram_addr_t 為 32 bit。

  • tb_page_addr_t (exec-all.h) 在 system mode 中被 typedef 成 ram_addr_t; 在 process mode 中被 typedef 成 abi_ulong,abi_ulong 又被 typedef 成 target_ulong。

以宿主機作業系統的角度來看,QEMU 就是一般的使用者進程。QEMU 會在自己的虛擬位址空間分配內存給客戶機作業系統。以客戶機作業系統的角度來看,該塊內存即是客戶機作業系統的物理內存。該物理內存分成一般使用的內存和內存映射 IO。透過 cpu_register_physical_memory_offset (exec.c) 註冊。hw/* 會依序呼叫 cpu_register_io_memory 註冊 IO 模擬的函式和 cpu_register_physical_memory。因為 QEMU 看不到宿主機物理內存,所以註解中所提到的 physical 代表的是 guest physical。

QEMU 分配給客戶機的內存是以 RAMBlock 和 RAMList 來管理。內存主要分為底下幾類 (cpu-common.h):

  • IO_MEM_RAM: 一般內存。

  • IO_MEM_ROM: ROM。

  • IO_MEM_UNASSIGNED: 初始未指定。

  • IO_MEM_ROMD: 讀的時候視作為 ROM,寫的時候視作為裝置。

  1. 透過 qemu_ram_alloc (exec.c) 申請空間。

    ram_addr_t qemu_ram_alloc(DeviceState *dev, const char *name, ram_addr_t size){    RAMBlock *new_block, *block;     // RAMBlock 會被賦予一個字串名稱。    pstrcat(new_block->idstr, sizeof(new_block->idstr), name);     // 檢視 RAMList 中是否已有具有相同名稱的 RAMBlock。    QLIST_FOREACH(block, &ram_list.blocks, next) {    }     if (mem_path) {    } else {        // 指向宿主的虛擬內存位址。        new_block->host = qemu_vmalloc(size);    }     new_block->offset = find_ram_offset(size); // 該 RAMBlock 在 RAMList 的偏移量。    new_block->length = size; // 該 RAMBlock 的大小。     QLIST_INSERT_HEAD(&ram_list.blocks, new_block, next); // 將新增的 RAMBlock 加入 RAMList。     ram_list.phys_dirty = qemu_realloc(ram_list.phys_dirty,                                       last_ram_offset() >> TARGET_PAGE_BITS);    memset(ram_list.phys_dirty + (new_block->offset >> TARGET_PAGE_BITS),           0xff, size >> TARGET_PAGE_BITS);     return new_block->offset; // 回傳該 RAMBlock 在 RAMList 的偏移量。                    

  2. 透過 cpu_register_physical_memory → cpu_register_physical_memory_offset 註冊該 RAMBlock 的資訊 (跟 QEMU 註冊客戶機物理內存)。target_phys_addr_t 代表客戶機物理內存空間; ram_addr_t 代表宿主機虛擬內存空間。

                            void cpu_register_physical_memory_offset(target_phys_addr_t start_addr,                                         ram_addr_t size,                                         ram_addr_t phys_offset,                                         ram_addr_t region_offset){    PhysPageDesc *p;     for(addr = start_addr; addr != end_addr; addr += TARGET_PAGE_SIZE) {        p = phys_page_find(addr >> TARGET_PAGE_BITS); // 用客戶機物理位址 start_addr 查找 l1_phys_map        if (p && p->phys_offset != IO_MEM_UNASSIGNED) {            // 1_phys_map 中已存在該客戶機物理位址的項目。        } else {          // 針對該客戶機物理位址在 1_phys_map 中配置 PhysPageDesc 並更新相應的欄位。            p = phys_page_find_alloc(addr >> TARGET_PAGE_BITS, 1);       }    }}                    

  3. 使用 MMIO 的裝置會先呼叫 cpu_register_io_memory 註冊 IO 模擬函式。cpu_register_io_memory 返回 io_mem_write/io_mem_read 的索引。該索引被當作 phys_offset 傳給 cpu_register_physical_memory。

                            static void cirrus_init_common(CirrusVGAState * s, int device_id, int is_pci){    s->vga.vga_io_memory = cpu_register_io_memory(cirrus_vga_mem_read,                                                  cirrus_vga_mem_write, s);    cpu_register_physical_memory(isa_mem_base + 0x000a0000, 0x20000,                                 s->vga.vga_io_memory);}                    

PhysPageDesc

PhysPageDesc 用來描述客戶機物理頁面和宿主機虛擬頁面的對映。有一個二級頁表 l1_phys_map 存放 PhysPageDesc。phys_page_find_alloc 用客戶機物理位址查詢 l1_phys_map 取得 PhysPageDesc,視情況配置新的 PhysPageDesc。

  • cpu_register_physical_memory_offset → phys_page_find → phys_page_find_alloc。

                            static PhysPageDesc *phys_page_find_alloc(target_phys_addr_t index, int alloc){    // 取得一級頁表項    lp = l1_phys_map + ((index >> P_L1_SHIFT) & (P_L1_SIZE - 1));     // 視 alloc 是否要分配二級頁表項    for (i = P_L1_SHIFT / L2_BITS - 1; i > 0; i--) {    }     pd = *lp;    if (pd == NULL) {        for (i = 0; i < L2_SIZE; i++) {            pd[i].phys_offset = IO_MEM_UNASSIGNED;            pd[i].region_offset = (index + i) << TARGET_PAGE_BITS;        }    }}                    

PageDesc

PageDesc 維護 TB 和虛擬頁面/客戶機物理頁面之間的關係 (視 process/system mode 而定)。同樣有一個二級頁表 l1_map 存放 PageDesc。page_find_alloc 查詢 l1_map 取得 PageDesc,視情況配置新的 PageDesc。

  • tb_find_slow → tb_gen_code → tb_link_page → tb_alloc_page → page_find_alloc。

                            static PageDesc *page_find_alloc(tb_page_addr_t index, int alloc){    // ALLOC 在 process mode 使用 mmap; 在 system mode 使用 qemu_mallocz。     /* Level 1. Always allocated. */    lp = l1_map + ((index >> V_L1_SHIFT) & (V_L1_SIZE - 1));     /* Level 2..N-1. */    for (i = V_L1_SHIFT / L2_BITS - 1; i > 0; i--) {    }}                    

QEMU 基本上是以 page 為單位將該 page 所屬 TB 清掉。

  • stl_mmu (softmmu_template.h) → io_writel (softmmu_template.h) → notdirty_mem_writel (exec.c) → notdirty_mem_writel → tb_invalidate_phys_page_fast (exec.c) → tb_invalidate_phys_page_range (exec.c) → tb_phys_invalidate (exec.c) 會將屬於某虛擬頁面/客戶機物理頁面的 TB 清掉。

                            void tb_invalidate_phys_page_range(tb_page_addr_t start, tb_page_addr_t end, ...){    p = page_find(start >> TARGET_PAGE_BITS);     /* we remove all the TBs in the range [start, end[ */    tb = p->first_tb;    while (tb != NULL) {    }}                    

  • tb_invalidate_phys_page (exec.c) → tb_phys_invalidate (exec.c)。tb_invalidate_phys_page 僅在 process mode 有定義,用來處理 SMC。

                            #if !defined(CONFIG_SOFTMMU)static void tb_invalidate_phys_page(tb_page_addr_t addr,                                    unsigned long pc, void *puc){    addr &= TARGET_PAGE_MASK;    p = page_find(addr >> TARGET_PAGE_BITS);    // 取得該 page 的第一個 tb。     // tb 末兩位如果是 01 (1),代表 tb 對應的 guest bianry 跨 page。     tb = p->first_tb;     while (tb != NULL) {        n = (long)tb & 3; // 取得 block chaing 的方向         tb = (TranslationBlock *)((long)tb & ~3); // 去掉末兩位的編碼,還原回真正的 tb        tb_phys_invalidate(tb, addr);        tb = tb->page_next[n]; // 取得 tb 所屬 page (或下一個 page) 的下一個 tb    }    p->first_tb = NULL; }                    

  • 最終會呼叫到 tb_phys_invalidate。

                            void tb_phys_invalidate(TranslationBlock *tb, tb_page_addr_t page_addr){    // 將該 tb 從 tb_phys_hash 中移除     phys_pc = tb->page_addr[0] + (tb->pc & ~TARGET_PAGE_MASK); // virtual addr 中 page offset 的部分和 physical addr 一樣     h = tb_phys_hash_func(phys_pc);    tb_remove(&tb_phys_hash[h], tb,              offsetof(TranslationBlock, phys_hash_next));     // 將 tb 從相應的 PageDesc 中移除    if (tb->page_addr[0] != page_addr) {        p = page_find(tb->page_addr[0] >> TARGET_PAGE_BITS);        tb_page_remove(&p->first_tb, tb);        invalidate_page_bitmap(p);    }    if (tb->page_addr[1] != -1 && tb->page_addr[1] != page_addr) {        p = page_find(tb->page_addr[1] >> TARGET_PAGE_BITS);        tb_page_remove(&p->first_tb, tb);        invalidate_page_bitmap(p);    }     tb_invalidated_flag = 1;     // 將 tb 從 tb_jmp_cache 移除     h = tb_jmp_cache_hash_func(tb->pc);    // 因為每一個 env 都有一份自己的 tb_jmp_cache,全部清除。    for(env = first_cpu; env != NULL; env = env->next_cpu) {        if (env->tb_jmp_cache[h] == tb)            env->tb_jmp_cache[h] = NULL;    }     // 處理 tb1 (tb -> tb1)    tb_jmp_remove(tb, 0);    tb_jmp_remove(tb, 1);     // 處理 tb1 (tb1 -> tb)    tb1 = tb->jmp_first;    for(;;) {        n1 = (long)tb1 & 3;        if (n1 == 2) // tb1 末兩位如果為 10 (2),代表 tb1 沒有跳至其它 tb            break;        tb1 = (TranslationBlock *)((long)tb1 & ~3); // 還原回原本的 tb1        tb2 = tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2)        tb_reset_jump(tb1, n1); // 將 tb1 至其它的 tb 的 block chaining 打斷 (code cache)        tb1->jmp_next[n1] = NULL;        tb1 = tb2;    }    tb->jmp_first = (TranslationBlock *)((long)tb | 2); // 將 jmp_first 再次指向自己}                    

    • tb_jmp_remove 將該 tb 移出 circular lists‎。

                                      static inline void tb_jmp_remove(TranslationBlock *tb, int n){    ptb = &tb->jmp_next[n]; // n (0 或 1) 指示 tb 下一個 block chaining 的方向     tb1 = *ptb; // 處理 tb1 (tb -> tb1)    if (tb1) {        /* find tb(n) in circular list */        for(;;) {            tb1 = *ptb;            n1 = (long)tb1 & 3; // 取出 tb1 末兩位              tb1 = (TranslationBlock *)((long)tb1 & ~3); 還原回原本的 tb1            if (n1 == n && tb1 == tb) // 代表 tb 沒有跳至其它 tb                break;            if (n1 == 2) {                ptb = &tb1->jmp_first; // 代表沒有其它 tb 跳至 tb1            } else {                ptb = &tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2)            }        }        /* now we can suppress tb(n) from the list */        *ptb = tb->jmp_next[n];         tb->jmp_next[n] = NULL;    }}                            

    • cpu_exec (cpu-exec.c) 會用到 tb_invalidated_flag。

                                      if (tb_invalidated_flag) {    /* as some TB could have been invalidated because of memory exceptions while generating the code, we must recompute the hash index here */    next_tb = 0;    tb_invalidated_flag = 0;}                            

MemoryRegion

請見 memory.[ch] 和 doc/memory.txt

  • target_phys_addr_t (targphys.h) 代表客戶機物理地址空間。如果客戶機是 x86 開啟 PAE 的話,target_phys_addr_t 為 64 bit。

  • target_ulong 代表客戶機暫存器大小和虛擬地址空間。如果客戶機是 x86 開啟 PAE 的話,target_ulong 為 32 bit。

  • ram_addr_t (cpu-common.h) 代表宿主機虛擬地址空間。如果宿主機是 x86 的話,ram_addr_t 為 32 bit。

  • tb_page_addr_t (exec-all.h) 在 system mode 中被 typedef 成 ram_addr_t; 在 process mode 中被 typedef 成 abi_ulong,abi_ulong 又被 typedef 成 target_ulong。

  • MemoryRegion (memory.h)。

                            struct MemoryRegion {    /* All fields are private - violators will be prosecuted */    const MemoryRegionOps *ops;    void *opaque;    MemoryRegion *parent;    Int128 size;    target_phys_addr_t addr;    void (*destructor)(MemoryRegion *mr);    ram_addr_t ram_addr;    bool subpage;    bool terminates;    bool readable;    bool ram;    bool readonly; /* For RAM regions */    bool enabled;    bool rom_device;    bool warning_printed; /* For reservations */    MemoryRegion *alias;    target_phys_addr_t alias_offset;    unsigned priority;    bool may_overlap;    QTAILQ_HEAD(subregions, MemoryRegion) subregions;    QTAILQ_ENTRY(MemoryRegion) subregions_link;    QTAILQ_HEAD(coalesced_ranges, CoalescedMemoryRange) coalesced;    const char *name;    uint8_t dirty_log_mask;    unsigned ioeventfd_nb;    MemoryRegionIoeventfd *ioeventfds;};                    

System Mode

 

Before QEMU 1.0

以 QEMU 1.0 版以前,qemu (i386-softmmu) 為例,主要流程如下:

main (vl.c) → init_clocks (qemu-timer.c) → module_call_init(MODULE_INIT_MACHINE) (module.c) → cpu_exec_init_all (初始 dynamic translator) (exec.c) → module_call_init(MODULE_INIT_DEVICE) (module.c) → machine→init (初始 machine) (vl.c) → main_loop (vl.c)

  • main_loop (vl.c) → qemu_main_loop_start (cpus.c) → cpu_exec_all (cpus.c) → main_loop_wait (vl.c)

    • cpu_exec_all (cpus.c) → qemu_clock_enable (qemu-timer.c) → qemu_alarm_pending (qemu-timer.c) → any_cpu_has_work (cpus.c)

    • cpu_exec_all (cpus.c) → qemu_cpu_exec (cpus.c) → cpu_x86_exec (cpu-exec.c) → tb_find_fast (cpu-exec.c) → tb_find_slow (cpu-exec.c)

      • tb_find_slow (cpu-exec.c) → get_page_addr_code (exec-all.h)

      • tb_find_slow (cpu-exec.c) → tb_gen_code (exec.c) → cpu_gen_code (translate-all.c) → gen_intermediate_code (target-i386/translate.c) → tcg_gen_code (tcg/tcg.c) → tcg_gen_code_common (tcg/tcg.c)

  • main_loop_wait (vl.c) 處理事件。

check exception -> check interrupt (setjmp) -> tb_find_fast -> tb_exec -> check exception (check interrupt)main_loop_wait -> select (alarm)

QEMU 會設置定時器 (qemu_signal_init),定時發出 SINGALARM 將 QEMU 從 code cache 拉出,去檢查 exception 或 interrupt。

  1. 進入點為 main.c (vl.c)。初始化環境。

                            int main(int argc, char **argv, char **envp){    // QEMU 內部維護三個 clock,分別為: rt_clock,vm_clock 和 host_clock。    // 之後會根據命令行參數將 rtc_clock 設為前述三者之一。    init_clocks();     // module_call_init -> pc_machine_init -> qemu_register_machine    // 會有預設 QEMUMachine,之後處理命令行參數時可被替換。    module_call_init(MODULE_INIT_MACHINE);     /* 處理命令行參數,並初始化環境 */     // 初始 QEMU 會用到的鎖以及使用的 signal number    if (qemu_init_main_loop()) {        fprintf(stderr, "qemu_init_main_loop failed\n");        exit(1);    }     // alarm_timers 數組存放各種 timer 相對應的啟動/終止函式指針,以及其它資料。    // init_timer_alarm 依序呼叫 alarm_timers 數組中各個 timer 的啟動函式。    // dynticks_start_timer 會註冊 SIGALRM 相對應的信號句柄。    if (init_timer_alarm() < 0) {        fprintf(stderr, "could not initialize alarm timer\n");        exit(1);    }     /* init the dynamic translator */    cpu_exec_init_all(tb_size * 1024 * 1024);     // drive_init_func 最後會呼叫到 paio_init 註冊 SIGUSR2 的信號句柄。    if (qemu_opts_foreach(&qemu_drive_opts, drive_init_func, &machine->use_scsi, 1) != 0)        exit(1);     // 初始化設備    module_call_init(MODULE_INIT_DEVICE);     // 建立 QEMUMachine (hw/pc_piix.c) 並呼叫 machine->init (pc_init_pci) 初始化。    machine->init(ram_size, boot_devices,                  kernel_filename, kernel_cmdline, initrd_filename, cpu_model);     /* 初始化剩下的設備以及輸出設備 */     main_loop(); // 主要執行迴圈    quit_timers();    net_cleanup();     return 0;}                    

    • dynticks_start_timer 所註冊的 SIGALRM 的信號句柄是 host_alarm_handler。當宿主機作業系統發出 SIGALRM 時,host_alarm_handler 視情況會呼叫 qemu_notify_event。qemu_notify_event 用 cpu_exit 將 QEMU 從當前 code cache 中拉出來檢查 IO。關於 clock 請見 [Qemu-devel] Question on kvm_clock working ...

    • cpu_exec_init_all 的代碼如下:

                                      /* Must be called before using the QEMU cpus. 'tb_size' is the size (in bytes) allocated to the translation buffer. Zero means default size. */void cpu_exec_init_all(unsigned long tb_size){    cpu_gen_init();    code_gen_alloc(tb_size);    code_gen_ptr = code_gen_buffer;    page_init();#if !defined(CONFIG_USER_ONLY)    io_mem_init(); // 註冊 MMIO 回掉函式#endif#if !defined(CONFIG_USER_ONLY) || !defined(CONFIG_USE_GUEST_BASE)    /* There's no guest base to take into account, so go ahead and initialize the prologue now. */    tcg_prologue_init(&tcg_ctx);#endif}                            

    • pc_init_pci (hw/pc_piix.c) 呼叫 pc_init1 (hw/pc_piix.c) 進行 PC 機器的初始化。

                                      /* PC hardware initialisation */static void pc_init1(ram_addr_t ram_size, ...){    // 呼叫 pc_new_cpu (hw/pc.c) -> cpu_init/cpu_x86_init (target-i386/helper.c) 初始化 CPU。    pc_cpus_init(cpu_model);     // 配置客戶機內存,載入 BIOS。    // 這部分在 QEMU 1.0 會用 memory API 改寫。    // http://lists.gnu.org/archive/html/qemu-devel/2011-07/msg02716.html    pc_memory_init(ram_size, kernel_filename, kernel_cmdline, initrd_filename,                   &below_4g_mem_size, &above_4g_mem_size);     // 呼叫 qemu_allocate_irqs (hw/irq.c) 設置中斷處理常式。    cpu_irq = pc_allocate_cpu_irq();     pc_vga_init(pci_enabled? pci_bus: NULL);     /* init basic PC hardware */    pc_basic_device_init(isa_irq, &floppy_controller, &rtc_state);     pc_vga_init(pci_enabled? pci_bus: NULL);     /* init basic PC hardware */    pc_basic_device_init(isa_irq, &floppy_controller, &rtc_state);}                            

      • 第一部分 qemu oldworld mac(heathrow)的初始化

      • Features/RamAPI

                                                void pc_memory_init(ram_addr_t ram_size, ...){    // 透過 qemu_ram_alloc 跟 QEMU 申請內存空間。QEMU 以 RAMBlock 為單位分配內存,並以 RAMList 管理所有 RAMBlock。    // QEMU 依命令行參數的不同,會從檔案或是跟宿主機作業系統申請 (posix_memalign) 配置空間。    // 回傳的是 RAMBlock 在 RAMList 的偏移量。    ram_addr = qemu_ram_alloc(NULL, "pc.ram",                              below_4g_mem_size + above_4g_mem_size);    // 所有類型的 RAM (一般內存、內存映射 IO) 皆要透過 cpu_register_physical_memory 跟 QEMU 註冊。    // 將該資訊記錄在 PhysPageDesc。    cpu_register_physical_memory(0, 0xa0000, ram_addr);    cpu_register_physical_memory(0x100000,                 below_4g_mem_size - 0x100000,                 ram_addr + 0x100000);}                                    

  2. main_loop (vl.c) 是主要的執行迴圈。

                            static void main_loop(void){    // 若是沒有開啟 IO 執行緒的話,無作用。    qemu_main_loop_start();     // 主要執行的無窮迴圈。    for (;;) {        do {            bool nonblocking = false; #ifndef CONFIG_IOTHREAD            nonblocking = cpu_exec_all(); // 翻譯並執行客戶端代碼#endif            main_loop_wait(nonblocking); // 處理 IO        } while (vm_can_run()); // 如果此虛擬機沒有收到關機或是重開機等諸如此類的請求,則繼續執行。        /* 檢查系統是否收到關機或是重開機的要求。若是關機,則跳離此無窮迴圈 */    }    bdrv_close_all(); // 關閉所有設備    pause_all_vcpus(); // 暫無作用}                    

  3. 翻譯並執行客戶端代碼是由 cpu_exec_all (cpus.c) 負責。

    bool cpu_exec_all(void){    // 依序檢視虛擬處理器    for (; next_cpu != NULL && !exit_request; next_cpu = next_cpu->next_cpu) {        CPUState *env = next_cpu;         qemu_clock_enable(vm_clock,                          (env->singlestep_enabled & SSTEP_NOTIMER) == 0);         if (qemu_alarm_pending())            break;        if (cpu_can_run(env)) {            // qemu_cpu_exec 以 process mode 的路徑執行。            // cpu_x86_exec (cpu-exec.c) → tb_find_fast (cpu-exec.c) → tb_find_slow (cpu-exec.c)            // cpu_exec 執行完後會返回 exception_index 狀態,狀態定義在 cpu-defs.h。            if (qemu_cpu_exec(env) == EXCP_DEBUG) {                break;            }        } else if (env->stop) {            break;        }    }    exit_request = 0;    return any_cpu_has_work();}                    

    • qemu_cpu_exec 基本上只額外多做計數。

  4. 處理 IO 是由 main_loop_wait (vl.c) 負責。How to use the select(), an I/O Multiplexer

                            void main_loop_wait(int nonblocking){    nfds = -1;    FD_ZERO(&rfds);    FD_ZERO(&wfds);    FD_ZERO(&xfds);    QLIST_FOREACH(ioh, &io_handlers, next) {      // 將欲處理的設備加入上述的 file set    }     // 根據 nonblocking 與否計算 select 等待時間    tv.tv_sec = timeout / 1000;    tv.tv_usec = (timeout % 1000) * 1000;     // 將設備以 file descriptor 來處理    qemu_mutex_unlock_iothread();    // 用 select 由設備描述符中選擇一個能立即處理的設備    // select 參數代表的意義分別是: 欲處理的設備個數,要處理的輸入設備的檔案描述詞的集合,要處理的輸出設備的檔案描述詞的集合,    // 有突發狀態發生的設備的檔案描述詞的集合和要求 select 等待的時間。    ret = select(nfds + 1, &rfds, &wfds, &xfds, &tv);    qemu_mutex_lock_iothread();    if (ret > 0) {        IOHandlerRecord *pioh;         QLIST_FOREACH_SAFE(ioh, &io_handlers, next, pioh) {          /* 處理設備 */        }    }     qemu_run_all_timers();     /* Check bottom-halves last in case any of the earlier events triggered them. */    qemu_bh_poll();}                    

  • QEMU Internals: Overall architecture and threading model

    • QEMU 1.0 之前,預設編譯為 non-iothread。不論 guest OS 是否為 SMP,只有一個 QEMU thread 負責執行 guest code 和 IO 處理。如果開啟 IO thread,每一個 guest CPU 有一個 QEMU thread 對映,加上一個處理 IO 的 thread。因為 TCG 為 non thread safe,以上兩種模式同時都只有一個 thread 在執行。

After QEMU 1.0

QEMU 1.0 開啟 IO thread,無法關閉。仍舊以 qemu-system-i386 為例:

模擬虛擬 CPU 和虛擬外設分為不同的執行緒。開機時至少會看到兩個執行緒,主執行緒處理 IO,另一個則是模擬虛擬 CPU 的執行緒。模擬客戶機 CPU 的流程如下:

  1. cpu_init/cpu_x86_init (target-i386/helper.c) 在初始化虛擬 CPU 時,會呼叫 qemu_init_vcpu

    CPUX86State *cpu_x86_init(const char *cpu_model){    CPUX86State *env;    static int inited;     env = g_malloc0(sizeof(CPUX86State));    cpu_exec_init(env);    env->cpu_model_str = cpu_model;     /* init various static tables used in TCG mode */    if (tcg_enabled() && !inited) {        inited = 1;        optimize_flags_init();#ifndef CONFIG_USER_ONLY        prev_debug_excp_handler =            cpu_set_debug_excp_handler(breakpoint_handler);#endif    }    if (cpu_x86_register(env, cpu_model) < 0) {        cpu_x86_close(env);        return NULL;    }    env->cpuid_apic_id = env->cpu_index;    mce_init(env);     qemu_init_vcpu(env);     return env;}                    

  2. qemu_init_vcpu (cpus.c)

                            void qemu_init_vcpu(void *_env){    CPUState *env = _env;     env->nr_cores = smp_cores;    env->nr_threads = smp_threads;    env->stopped = 1;    if (kvm_enabled()) {        qemu_kvm_start_vcpu(env);    } else {        qemu_tcg_init_vcpu(env);    }}                    

  3. qemu_tcg_init_vcpu (cpus.c)

                            static void qemu_tcg_init_vcpu(void *_env){    CPUState *env = _env;     /* share a single thread for all cpus with TCG */    if (!tcg_cpu_thread) {        env->thread = g_malloc0(sizeof(QemuThread));        env->halt_cond = g_malloc0(sizeof(QemuCond));        qemu_cond_init(env->halt_cond);        tcg_halt_cond = env->halt_cond;        qemu_thread_create(env->thread, qemu_tcg_cpu_thread_fn, env,                           QEMU_THREAD_JOINABLE);#ifdef _WIN32        env->hThread = qemu_thread_get_handle(env->thread);#endif        while (env->created == 0) {            qemu_cond_wait(&qemu_cpu_cond, &qemu_global_mutex);        }        tcg_cpu_thread = env->thread;    } else {        env->thread = tcg_cpu_thread;        env->halt_cond = tcg_halt_cond;    }}                    

  4. qemu_tcg_cpu_thread_fn (cpus.c)

                            static void *qemu_tcg_cpu_thread_fn(void *arg){    CPUState *env = arg;     qemu_tcg_init_cpu_signals();    qemu_thread_get_self(env->thread);     /* signal CPU creation */    qemu_mutex_lock(&qemu_global_mutex);    for (env = first_cpu; env != NULL; env = env->next_cpu) {        env->thread_id = qemu_get_thread_id();        env->created = 1;    }    qemu_cond_signal(&qemu_cpu_cond);     /* wait for initial kick-off after machine start */    while (first_cpu->stopped) {        qemu_cond_wait(tcg_halt_cond, &qemu_global_mutex);    }     while (1) {        tcg_exec_all();        if (use_icount && qemu_clock_deadline(vm_clock) <= 0) {            qemu_notify_event();        }        qemu_tcg_wait_io_event();    }     return NULL;}                    

  5. tcg_exec_all (cpus.c) 執行所有的虛擬 CPU。

                            static void tcg_exec_all(void){    int r;     /* Account partial waits to the vm_clock. */    qemu_clock_warp(vm_clock);     if (next_cpu == NULL) {        next_cpu = first_cpu;    }    for (; next_cpu != NULL && !exit_request; next_cpu = next_cpu->next_cpu) {        CPUState *env = next_cpu;         qemu_clock_enable(vm_clock,                          (env->singlestep_enabled & SSTEP_NOTIMER) == 0);         if (cpu_can_run(env)) {            r = tcg_cpu_exec(env);            if (r == EXCP_DEBUG) {                cpu_handle_guest_debug(env);                break;            }        } else if (env->stop || env->stopped) {            break;        }    }    exit_request = 0;}                    

    • qemu_tcg_cpu_thread_fn (cpus.c) → tcg_exec_all (cpus.c) → tcg_cpu_exec (cpus.c) → cpu_x86_exec (cpu-exec.c)

目前 QEMU 本身即為 IO thread 執行 main_loop_wait,當遇到 block IO 時,會 fork 出 posix-aio-compat.c worker thread 去處理。

  1. main (vl.c)

        cpu_exec_init_all();     /* open the virtual block devices */    if (snapshot)        qemu_opts_foreach(qemu_find_opts("drive"), drive_enable_snapshot, NULL, 0);    if (qemu_opts_foreach(qemu_find_opts("drive"), drive_init_func, &machine->use_scsi, 1) != 0)        exit(1);     qemu_init_cpu_loop();    // qemu_init_main_loop 呼叫 main_loop_init (main-loop.c)    if (qemu_init_main_loop()) {        fprintf(stderr, "qemu_init_main_loop failed\n");        exit(1);    }                    

    • qemu_init_cpu_loop (cpus.c)

                                      void qemu_init_cpu_loop(void){    qemu_init_sigbus();    qemu_cond_init(&qemu_cpu_cond);    qemu_cond_init(&qemu_pause_cond);    qemu_cond_init(&qemu_work_cond);    qemu_cond_init(&qemu_io_proceeded_cond);    qemu_mutex_init(&qemu_global_mutex);     qemu_thread_get_self(&io_thread);}                            

    • main_loop_init (main-loop.c)

                                      int main_loop_init(void){    int ret;     qemu_mutex_lock_iothread();    ret = qemu_signal_init();    if (ret) {        return ret;    }     /* Note eventfd must be drained before signalfd handlers run */    ret = qemu_event_init();    if (ret) {        return ret;    }     return 0;}                            

  2. main_loop (vl.c) 是主要的執行迴圈,IO thread。

                            static void main_loop(void){    bool nonblocking;    int last_io = 0;     do {        nonblocking = !kvm_enabled() && last_io > 0;         last_io = main_loop_wait(nonblocking);     } while (!main_loop_should_exit());}                    

  3. main_loop_wait (main-loop.c)

                            int main_loop_wait(int nonblocking){    fd_set rfds, wfds, xfds;    int ret, nfds;    struct timeval tv;    int timeout;     if (nonblocking) {        timeout = 0;    } else {        timeout = qemu_calculate_timeout();        qemu_bh_update_timeout(&timeout);    }     os_host_main_loop_wait(&timeout);     tv.tv_sec = timeout / 1000;    tv.tv_usec = (timeout % 1000) * 1000;     /* poll any events */    /* XXX: separate device handlers from system ones */    nfds = -1;    FD_ZERO(&rfds);    FD_ZERO(&wfds);    FD_ZERO(&xfds); #ifdef CONFIG_SLIRP    slirp_select_fill(&nfds, &rfds, &wfds, &xfds);#endif    qemu_iohandler_fill(&nfds, &rfds, &wfds, &xfds);    glib_select_fill(&nfds, &rfds, &wfds, &xfds, &tv);     if (timeout > 0) {        qemu_mutex_unlock_iothread();    }     ret = select(nfds + 1, &rfds, &wfds, &xfds, &tv);     if (timeout > 0) {        qemu_mutex_lock_iothread();    }     glib_select_poll(&rfds, &wfds, &xfds, (ret < 0));    qemu_iohandler_poll(&rfds, &wfds, &xfds, ret);#ifdef CONFIG_SLIRP    slirp_select_poll(&rfds, &wfds, &xfds, (ret < 0));#endif     qemu_run_all_timers();     /* Check bottom-halves last in case any of the earlier events triggered them. */    qemu_bh_poll();     return ret;}                    

main (vl.c) → qemu_opts_foreach (qemu-option.c) → qemu_aio_wait (aio.c) → qemu_bh_poll (async.c) → spawn_thread_bh_fn (posix-aio-compat.c) → do_spawn_thread (posix-aio-compat.c)

aio_thread (posix-aio-compat.c) → cond_timedwait (posix-aio-compat.c)

底下腳本可以觀察 QEMU 本身。

$ vi command.gdbset breakpoint pending onfile qemuhandle SIGUSR2 noprint nostopbreak main_looprun linux-0.2.img -vnc 0.0.0.0:1$ gdb -x command.gdb

Reboot

虛擬機重啟 (reboot) 的時候,會重置 virtual cpu 的 reset vector,這樣 virtual cpu 才會跳至開機預設的位址執行。請在 cpu_reset 下斷點,並 reboot 虛擬機 14)

            (gdb) bt#0  cpu_reset (env=0x1251290) at /nfs_home/chenwj/work/svn/qemu-1.0/target-i386/helper.c:37#1  0x0000000000638753 in pc_cpu_reset (opaque=0x1251290) at /nfs_home/chenwj/work/svn/qemu-1.0/hw/pc.c:928#2  0x00000000004fe916 in qemu_system_reset (report=true) at /nfs_home/chenwj/work/svn/qemu-1.0/vl.c:1381#3  0x00000000004feb71 in main_loop_should_exit () at /nfs_home/chenwj/work/svn/qemu-1.0/vl.c:1452#4  0x00000000004fec48 in main_loop () at /nfs_home/chenwj/work/svn/qemu-1.0/vl.c:1485#5  0x0000000000503864 in main (argc=4, argv=0x7fffffffe218, envp=0x7fffffffe240) at /nfs_home/chenwj/work/svn/qemu-1.0/vl.c:3485(gdb)        
  1. 當有 reboot (reset) 的需要時,會呼叫 qemu_system_reset_request (vl.c) 拉起 reset_requested。

                            void qemu_system_reset_request(void){    if (no_reboot) {        shutdown_requested = 1;    } else {        reset_requested = 1;    }    cpu_stop_current();    qemu_notify_event();}                    

    • 以 i386 為例,大約有以下幾處會呼叫 qemu_system_reset_request。前兩者都是當出現 Triple fault 的時候重啟系統,後者是拉起 port 92。

      1. target-i386/op_helper.c

      2. target-i386/helper.c

      3. hw/pc.c

  2. 在 main_loop (vl.c) 中會呼叫 main_loop_should_exit 判斷是否需要跳離主迴圈。

                            static void main_loop(void){    bool nonblocking;    int last_io = 0;     do {        nonblocking = !kvm_enabled() && last_io > 0;         last_io = main_loop_wait(nonblocking);     } while (!main_loop_should_exit());}                    

  3. main_loop_should_exit

                            static bool main_loop_should_exit(void){    RunState r;    if (qemu_debug_requested()) {        vm_stop(RUN_STATE_DEBUG);    }    if (qemu_shutdown_requested()) {        qemu_kill_report();        monitor_protocol_event(QEVENT_SHUTDOWN, NULL);        if (no_shutdown) {            vm_stop(RUN_STATE_SHUTDOWN);        } else {            return true;        }    }    if (qemu_reset_requested()) { // 返回 reset_requested        pause_all_vcpus();        cpu_synchronize_all_states();        qemu_system_reset(VMRESET_REPORT); // 重啟系統        resume_all_vcpus();        if (runstate_check(RUN_STATE_INTERNAL_ERROR) ||            runstate_check(RUN_STATE_SHUTDOWN)) {            runstate_set(RUN_STATE_PAUSED);        }    }    if (qemu_powerdown_requested()) {        monitor_protocol_event(QEVENT_POWERDOWN, NULL);        qemu_irq_raise(qemu_system_powerdown);    }    if (qemu_vmstop_requested(&r)) {        vm_stop(r);    }    return false;}                    

  4.                         void qemu_system_reset(bool report){    QEMUResetEntry *re, *nre;     /* reset all devices */    // 從 reset_handlers 抓出 device 重啟。之前就會用 qemu_register_reset 註冊各個裝置的 reset 回掉函式。    QTAILQ_FOREACH_SAFE(re, &reset_handlers, entry, nre) {        re->func(re->opaque);    }    if (report) {        monitor_protocol_event(QEVENT_RESET, NULL);    }    cpu_synchronize_all_post_reset();}                    

  5. pc_cpu_reset 呼叫 cpu_reset (target-i386/helper.c)。

                            static void pc_cpu_reset(void *opaque){    CPUState *env = opaque;     cpu_reset(env);    env->halted = !cpu_is_bsp(env);}                    

  6. cpu_reset (target-i386/helper.c) 開機或是重啟時會將 CPU 狀態重置。

                            void cpu_reset(CPUX86State *env){    int i;     if (qemu_loglevel_mask(CPU_LOG_RESET)) {        qemu_log("CPU Reset (CPU %d)\n", env->cpu_index);        log_cpu_state(env, X86_DUMP_FPU | X86_DUMP_CCOP);    }     memset(env, 0, offsetof(CPUX86State, breakpoints));     tlb_flush(env, 1);     env->old_exception = -1;     /* init to reset state */ #ifdef CONFIG_SOFTMMU    env->hflags |= HF_SOFTMMU_MASK;#endif    env->hflags2 |= HF2_GIF_MASK;     cpu_x86_update_cr0(env, 0x60000010);    env->a20_mask = ~0x0;    env->smbase = 0x30000;     env->idt.limit = 0xffff;    env->gdt.limit = 0xffff;    env->ldt.limit = 0xffff;    env->ldt.flags = DESC_P_MASK | (2 << DESC_TYPE_SHIFT);    env->tr.limit = 0xffff;    env->tr.flags = DESC_P_MASK | (11 << DESC_TYPE_SHIFT);     cpu_x86_load_seg_cache(env, R_CS, 0xf000, 0xffff0000, 0xffff,                           DESC_P_MASK | DESC_S_MASK | DESC_CS_MASK |                           DESC_R_MASK | DESC_A_MASK);    cpu_x86_load_seg_cache(env, R_DS, 0, 0, 0xffff,                           DESC_P_MASK | DESC_S_MASK | DESC_W_MASK |                           DESC_A_MASK);    cpu_x86_load_seg_cache(env, R_ES, 0, 0, 0xffff,                           DESC_P_MASK | DESC_S_MASK | DESC_W_MASK |                           DESC_A_MASK);    cpu_x86_load_seg_cache(env, R_SS, 0, 0, 0xffff,                           DESC_P_MASK | DESC_S_MASK | DESC_W_MASK |                           DESC_A_MASK);    cpu_x86_load_seg_cache(env, R_FS, 0, 0, 0xffff,                           DESC_P_MASK | DESC_S_MASK | DESC_W_MASK |                           DESC_A_MASK);    cpu_x86_load_seg_cache(env, R_GS, 0, 0, 0xffff,                           DESC_P_MASK | DESC_S_MASK | DESC_W_MASK |                           DESC_A_MASK);     env->eip = 0xfff0;    env->regs[R_EDX] = env->cpuid_version;     env->eflags = 0x2;     /* FPU init */    for(i = 0;i < 8; i++)        env->fptags[i] = 1;    env->fpuc = 0x37f;     env->mxcsr = 0x1f80;     env->pat = 0x0007040600070406ULL;    env->msr_ia32_misc_enable = MSR_IA32_MISC_ENABLE_DEFAULT;     memset(env->dr, 0, sizeof(env->dr));    env->dr[6] = DR6_FIXED_1;    env->dr[7] = DR7_FIXED_1;    cpu_breakpoint_remove_all(env, BP_CPU);    cpu_watchpoint_remove_all(env, BP_CPU);}                    

Software MMU

  • target_phys_addr_t (targphys.h) 代表客戶機物理地址空間。如果客戶機是 x86 開啟 PAE 的話,target_phys_addr_t 為 64 bit。

  • target_ulong 代表客戶機暫存器大小和虛擬地址空間。如果客戶機是 x86 開啟 PAE 的話,target_ulong 為 32 bit。

  • ram_addr_t (cpu-common.h) 代表宿主機虛擬地址空間。如果宿主機是 x86 的話,ram_addr_t 為 32 bit。

  • tb_page_addr_t (exec-all.h) 在 system mode 中被 typedef 成 ram_addr_t; 在 process mode 中被 typedef 成 abi_ulong,abi_ulong 又被 typedef 成 target_ulong。

guest virtual addr (GVA) → guest physical addr (GPA) → host virtual addr (HVA)

  1. GVA → GPA 由客戶機作業系統負責; GPA → HVA 由 QEMU 負責。HVA → HPA 由宿主機作業系統負責

    • CPUTLBEntry

      • GVA → HVA。存放 GVA 相對於 HVA 的偏移量。轉換 GVA 到 HVA 的過程中,會先搜尋 TLB。如果命中,則將 GVA 加上該偏移量得到 HVA。若否,則需搜尋 l1_phys_map 並將 PhysPageDesc 填入 TLB。

    1. guest virtual addr → guest physical addr

    2. 搜尋 l1_phys_map 得到 PhysPageDesc。

    3. 將 phys_ram_base 加上 PhysPageDesc.phys_offset,得到 host virtual addr (physical addr → host virtual addr)

              typedef struct CPUTLBEntry {    // 以下存放 GVA,同時也代表該頁面的權限。tlb_set_page (exec.c) 填入新的 TLB 項目時會做設置。    target_ulong addr_read; // 可讀    target_ulong addr_write; // 可寫    target_ulong addr_code; // 可執行    // HVA 相對於 GVA 的偏移量。    unsigned long addend;} CPUTLBEntry;        
  1. tb_find_slow 是利用 guest pc (GVA) 對映的 guest physical address (GPA) 查找該 guest pc 的 TB。

                            static TranslationBlock *tb_find_slow(target_ulong pc, ...){    /* find translated block using physical mappings */    phys_pc = get_page_addr_code(env, pc);     phys_page1 = phys_pc & TARGET_PAGE_MASK;    phys_page2 = -1;    // 用虛擬位址 pc 對映的物理位址 phys_pc 查找 tb_phys_hash。     h = tb_phys_hash_func(phys_pc);    ptb1 = &tb_phys_hash[h];    for(;;) {        tb = *ptb1;        if (!tb)            goto not_found;        if (tb->pc == pc &&            tb->page_addr[0] == phys_page1 && // 該 TB 所屬物理頁面 (guest code) 是否與 pc 所屬物理頁面相同?            tb->cs_base == cs_base &&            tb->flags == flags) {            /* check next page if needed */            if (tb->page_addr[1] != -1) { // 該 TB 有跨物理頁面                virt_page2 = (pc & TARGET_PAGE_MASK) +                    TARGET_PAGE_SIZE;                phys_page2 = get_page_addr_code(env, virt_page2);                if (tb->page_addr[1] == phys_page2) // 該 TB 所屬的第二個物理頁面是否與 pc 所屬的第二個物理頁面相同?                    goto found;            } else {                goto found;            }        }        ptb1 = &tb->phys_hash_next; // 當 phys_pc 雜湊到同一個 tb_phys_hash 項目時。    } not_found:   /* if no translated code available, then translate it now */    tb = tb_gen_code(env, pc, cs_base, flags, 0);  found:    /* we add the TB in the virtual pc hash table */    env->tb_jmp_cache[tb_jmp_cache_hash_func(pc)] = tb;    return tb;}                    

  2. get_page_addr_code (exec.c) 先查找 TLB。process mode 情況有所不同,此時沒有所謂的 GPA,直接返回 addr。注意! get_page_addr_code 是被 tb_find_slow (cpu-exec.c) 或是 tb_gen_code (exec.c) 這兩個函式呼叫,get_page_addr_code 中的 code 代表存取的地址是一段 code。因此,皆是呼叫到 ld*_code 或是 ldb_cmmu。強烈建議查看 i386-softmmu/exec.i。

                            static inline tb_page_addr_t get_page_addr_code(CPUState *env1, target_ulong addr){    page_index = (addr >> TARGET_PAGE_BITS) & (CPU_TLB_SIZE - 1); // 計算 GVA 對映的 TLB 索引     mmu_idx = cpu_mmu_index(env1);    // TLB 不命中    if (unlikely(env1->tlb_table[mmu_idx][page_index].addr_code !=                 (addr & TARGET_PAGE_MASK))) {        ldub_code(addr);    }    // TLB 命中。檢查欲執行的位址屬於 RAM 之後,計算 GVA 對映的 HVA。    p = (void *)(unsigned long)addr        + env1->tlb_table[mmu_idx][page_index].addend;    // 返回 HVA 在 RAM 中的偏移量。    return qemu_ram_addr_from_host(p);}                    

  3. TLB 不命中。ldub_code (softmmu_header.h) 是個透過宏展開的函式。

                            static inline RES_TYPE glue(glue(ld, USUFFIX), MEMSUFFIX)(target_ulong ptr){    if (unlikely(env->tlb_table[mmu_idx][page_index].ADDR_READ !=                 (addr & (TARGET_PAGE_MASK | (DATA_SIZE - 1))))) {        // ADDR_READ 會視情況被替換成 addr_code 或是 addr_read。這裡因為存取的是 code,         // ADDR_READ 被替換成 addr_code。         res = glue(glue(__ld, SUFFIX), MMUSUFFIX)(addr, mmu_idx);    } else {        physaddr = addr + env->tlb_table[mmu_idx][page_index].addend;        res = glue(glue(ld, USUFFIX), _raw)((uint8_t *)physaddr);    }    return res;}                    

  4. ldb_cmmu (softmmu_template.h),其中的 cmmu 代表存取的是 code。如果是 mmu,代表存取的是 data。

                            /* handle all cases except unaligned access which span two pages */DATA_TYPE REGPARM glue(glue(__ld, SUFFIX), MMUSUFFIX)(target_ulong addr,                                                      int mmu_idx){ // 先查找 TLB redo:    // ADDR_READ 會被替換成 addr_code。    tlb_addr = env->tlb_table[mmu_idx][index].ADDR_READ;    if ((addr & TARGET_PAGE_MASK) == (tlb_addr & (TARGET_PAGE_MASK | TLB_INVALID_MASK))) {        // TLB 命中        if (tlb_addr & ~TARGET_PAGE_MASK) {            /* IO access */            // iotlb 緩存 IO 模擬函式            ioaddr = env->iotlb[mmu_idx][index];        } else if (((addr & ~TARGET_PAGE_MASK) + DATA_SIZE - 1) >= TARGET_PAGE_SIZE) {        do_unaligned_access:            /* slow unaligned access (it spans two pages) */            // 這裡會呼叫 slow_ldb_cmmu 做跨頁存取。       } else {            /* unaligned/aligned access in the same page */            addend = env->tlb_table[mmu_idx][index].addend;            res = glue(glue(ld, USUFFIX), _raw)((uint8_t *)(long)(addr+addend));       }    } else {        /* the page is not in the TLB : fill it */        // GETPC 包裝 __builtin_return_address,請見 http://gcc.gnu.org/onlinedocs/gcc/Return-Address.html。         // 其用途是取得此函式的 return address,藉此可得知從哪個 caller 呼叫到此函式。         // 在 exec.c 的最後已將 GETPC 定為 NULL。         retaddr = GETPC();         // 不同 ISA 分別定義不同的 tlb_fill        tlb_fill(addr, READ_ACCESS_TYPE, mmu_idx, retaddr);        goto redo;    }    return res;}                    

    • 在 exec.c 的最後定義如下的宏,並 include softmmu_template.h 將其中的宏展開。請見 exec.i。

                                      #define MMUSUFFIX _cmmu // load code#define GETPC() NULL // tb_find_slow -> get_page_addr_code -> ldub_code -> __ldb_cmmu#define env cpu_single_env#define SOFTMMU_CODE_ACCESS #define SHIFT 0#include "softmmu_template.h" #define SHIFT 1#include "softmmu_template.h" #define SHIFT 2#include "softmmu_template.h" #define SHIFT 3#include "softmmu_template.h" #undef env                            

      請見 [Qemu-devel] When the tlb_fill will be called from generated code?

  5. tlb_fill (target-i386/op_helper.c)。查找頁表,如果頁存在,將該頁帶進 TLB; 如果頁不存在,發出頁缺失中斷。

                            /* try to fill the TLB and return an exception if error. If retaddr is NULL, it means that the function was called in C code (i.e. not from generated code or from helper.c) */void tlb_fill(target_ulong addr, int is_write, int mmu_idx, void *retaddr){    ret = cpu_x86_handle_mmu_fault(env, addr, is_write, mmu_idx, 1);    if (ret) { // 出包了!        if (retaddr) { // tlb_fill 由 code cache 或是 helper.c 被呼叫。            /* now we have a real cpu fault */            pc = (unsigned long)retaddr;            tb = tb_find_pc(pc);            if (tb) {                /* the PC is inside the translated code. It means that we have a virtual CPU fault */                cpu_restore_state(tb, env, pc, NULL);            }        }        // tlb_fill 以一般的方式 (get_page_addr_code) 被呼叫,並非從 code cache 或是 helper function 被呼叫。         // 發出 guest page fault exception,guest OS 開始 page fault 處理。         raise_exception_err(env->exception_index, env->error_code);    }    // 頁面已在內存。    env = saved_env;}                    

    • retaddr 非 NULL 代表 tlb_fill 並非從 get_page_addr_code 呼叫。例如,target-i386/op_helper.c 或是 code cache。請見 op_helper.i。

                                      uint8_t __ldb_mmu(target_ulong addr, int mmu_idx){        retaddr = ((void *)((unsigned long)__builtin_return_address(0) - 1));         tlb_fill(addr, 0, mmu_idx, retaddr);        goto redo;}                            

  6. cpu_x86_handle_mmu_fault (target-i386/helper.c) 查找頁表。如果該頁已在內存,呼叫 tlb_set_page 將該頁寫入 TLB。

                            /* return value: -1 = cannot handle fault 0 = nothing more to do // 頁面已在內存,填入適當 TLB 項目即可。 1 = generate PF fault // 頁面不在內存,產生頁缺失。 */int cpu_x86_handle_mmu_fault(CPUX86State *env, target_ulong addr, ...){ do_mapping:    tlb_set_page(env, vaddr, paddr, prot, mmu_idx, page_size);    return 0; do_fault:    return 1;}                    

  7. tlb_set_page (exec.c) 填入 TLB 項。

                            void tlb_set_page(CPUState *env, target_ulong vaddr, ...){    CPUTLBEntry *te;     // 回傳 host virtual address    addend = (unsigned long)qemu_get_ram_ptr(pd & TARGET_PAGE_MASK);    // 更新 TLB 項目     te = &env->tlb_table[mmu_idx][index];    te->addend = addend - vaddr; // host virtual address 與 guest virtual address 的偏移量。}                    

請見 [Qemu-devel] When the tlb_fill will be called from generated code?。共有底下檔案:

  • softmmu-semi.h

  • softmmu_defs.h: 宣告 \_\_{ld,st}* 函式原型。

  • softmmu_exec.h: 利用 softmmu_header.h 生成 {ld,st}_{user,kernel,etc} 函式。{ld,st}_{user,kernel,etc} 函式又會呼叫到 \_\_{ld,st}* 函式。

  • softmmu_header.h: 生成 {ld,st}* 函式。{ld,st}* 函式又會呼叫到 \_\_{ld,st}* 函式。

  • softmmu_template.h: 生成 \_\_{ld,st}* 函式。

底下檔案定義相關函式原型。

  • softmmu_defs.h: 宣告給 TCG IR qemu_ld/qemu_st 使用的 \_\_{ld,st}* 函式原型。被 softmmu_exec.h, tcg/xxx/tcg-target.c 和 exec-all.h 所 #include。

底下檔案生成相關函式體。

  • softmmu_template.h: exec.c 和 target-*/op_helper.c 使用 softmmu_template.h 生成 \_\_{ld,st}* 函式。tcg_out_qemu_ld (tcg/xxx/tcg-target.c) 在為 qemu_ld/qemu_st 產生 host binary 時,會呼叫到 softmmu_template.h 生成 \_\_{ld,st}* 函式。

    1. tcg_out_qemu_ld (tcg/i386/tcg-target.c)。

                                      #include "../../softmmu_defs.h"// softmmu_defs.h// uint8_t REGPARM __ldb_mmu(target_ulong addr, int mmu_idx);// void REGPARM __stb_mmu(target_ulong addr, uint8_t val, int mmu_idx); // 內存讀指令。會將該指令指定的虛擬位址透過 software MMU 轉換成物理位址。// softmmu_template.h 會透過宏展開定義相對應的函式。static void *qemu_ld_helpers[4] = {    __ldb_mmu, // load byte    __ldw_mmu, // load word    __ldl_mmu, // load long word    __ldq_mmu, // load quad word}; /* XXX: qemu_ld and qemu_st could be modified to clobber only EDX and EAX. It will be useful once fixed registers globals are less common. */static void tcg_out_qemu_ld(TCGContext *s, const TCGArg *args,                            int opc){    /* 略 */     tcg_out_calli(s, (tcg_target_long)qemu_ld_helpers[s_bits]); // 呼叫上述函式。     /* 略 */}                            

    2. switch_tss (target-i386/op_helper.c)

                                      // #define MMU_MODE0_SUFFIX _kernel// #define MMU_MODE1_SUFFIX _user#include "cpu.h" // softmmu_exec.h 生成 {ld,st}*_{kernel,user,etc} 函式。// #define ACCESS_TYPE 0// #define MEMSUFFIX MMU_MODE0_SUFFIX// #define DATA_SIZE 1// #include "softmmu_header.h"#if !defined(CONFIG_USER_ONLY)#include "softmmu_exec.h"#endif /* !defined(CONFIG_USER_ONLY) */  #define MMUSUFFIX _mmu #define SHIFT 0#include "softmmu_template.h" // softmmu_template.h// SUFFIX 代表資料大小,可以是 b (byte, 8)、w (word, 16)、l (long word, 32) 或 q (quadruple word,64)// MMUSUFFIX 代表存取代碼或是資料,可以是 _cmmu 或 _mmu。DATA_TYPE REGPARM glue(glue(__ld, SUFFIX), MMUSUFFIX)(target_ulong addr,                                                      int mmu_idx){} // target-i386/op_helper.iuint8_t __ldb_mmu(target_ulong addr, int mmu_idx){} # 1 "/tmp/chenwj/qemu/softmmu_exec.h" 1# 27 "/tmp/chenwj/qemu/softmmu_exec.h"# 1 "/tmp/chenwj/qemu/softmmu_header.h" 1# 83 "/tmp/chenwj/qemu/softmmu_header.h"// 存取內核態資料static __attribute__ (( always_inline )) __inline__ uint32_t ldub_kernel(target_ulong ptr){    if (__builtin_expect(!!(env->tlb_table[mmu_idx][page_index].addr_read != (addr & (~((1 << 12) - 1) | (1 - 1)))), 0)                                                               ) {        res = __ldb_mmu(addr, mmu_idx); // softmmu_defs.h 定義函式原型,其函式體由 softmmu_template.h 實現。    } else {        physaddr = addr + env->tlb_table[mmu_idx][page_index].addend;        // softmmu_exec.h 定義函式原型,其函式體由 softmmu_header.h 實現。        res = ldub_p((uint8_t *)(long)(((uint8_t *)physaddr)));    }} static void switch_tss(int tss_selector, ...){    /* 略 */     v1 = ldub_kernel(env->tr.base);    v2 = ldub_kernel(env->tr.base + old_tss_limit_max);     /* 略 */}                            

  • softmmu_exec.h: target-*/op_helper.c #include softmmu_exec.h,softmmu_exec.h 再利用 softmmu_header.h 生成 {ld,st}*_{kernel,user,etc} 函式。

    • softmmu_exec.h #include softmmu_defs.h,softmmu_defs.h 定義函式原型 \_\_{ld,st},其函式體由 softmmu_template.h 實現。softmmu_exec.h 也 #include softmmu_header.h,softmmu_header.h 定義巨集生成 {ld,st}*_{kernel,user,etc} 函式。

                                      // softmmu_defs.huint8_t REGPARM __ldb_mmu(target_ulong addr, int mmu_idx);void REGPARM __stb_mmu(target_ulong addr, uint8_t val, int mmu_idx); // softmmu_template.hDATA_TYPE REGPARM glue(glue(__ld, SUFFIX), MMUSUFFIX)(target_ulong addr,                                                      int mmu_idx){} // softmmu_header.hstatic inline RES_TYPE glue(glue(ld, USUFFIX), MEMSUFFIX)(target_ulong ptr){        /* 略 */         res = glue(glue(__ld, SUFFIX), MMUSUFFIX)(addr, mmu_idx);         /* 略 */}                            

    • helper_fldt (target-i386/op_helper.c)

                                      #if !defined(CONFIG_USER_ONLY)#include "softmmu_exec.h"#endif /* !defined(CONFIG_USER_ONLY) */ static inline floatx80 helper_fldt(target_ulong ptr){    CPU_LDoubleU temp;     temp.l.lower = ldq(ptr);     // #define ldub(p) ldub_data(p) in softmmu_exec.h    temp.l.upper = lduw(ptr + 8);    return temp.d;}                            

  • softmmu_header.h: 定義巨集。被 softmmu_exec.h 和 exec-all.h #include 進而展開巨集,根據 MMU mode 和 data size 定義 inli ne ld/st 函式。

    • softmmu_exec.h: 被 target-xxx/op_helper.c 所 #include。根據 MMU mode (user/kernel) 和 data size 生成 inline ld/st 函式。

                                      // target-xxx/cpu.h 自行定義 MMU_MODE?_SUFFIX。// 以 i386 為例: _kernel,_user。#define MEMSUFFIX MMU_MODE1_SUFFIX                            
                                      // target-i386/op_helper.c#include "cpu.h"#include "softmmu_exec.h" // softmmu_exec.h#include "softmmu_defs.h" #define ACCESS_TYPE 0#define MEMSUFFIX MMU_MODE0_SUFFIX#define DATA_SIZE 1#include "softmmu_header.h" // softmmu_header.h// op_helper.i -> ldub_kernel// ld/st 最後會呼叫到 __ld/__st// kernel mode: env->tlb_table[0]// user mode: env->tlb_table[1]// data: env->tlb_table[(cpu_mmu_index(env))]static inline RES_TYPE glue(glue(ld, USUFFIX), MEMSUFFIX)(target_ulong ptr){}                            

  1. exec-all.h。定義給 code cache 使用的 softmmu 函式。

                            #include "softmmu_defs.h" // uint64_t REGPARM __ldq_mmu(target_ulong addr, int mmu_idx);// void REGPARM __stq_mmu(target_ulong addr, uint64_t val, int mmu_idx); uint8_t REGPARM __ldb_cmmu(target_ulong addr, int mmu_idx);// void REGPARM __stb_cmmu(target_ulong addr, uint8_t val, int mmu_idx); #define ACCESS_TYPE (NB_MMU_MODES + 1)#define MEMSUFFIX _code#define env cpu_single_env #define DATA_SIZE 1#include "softmmu_header.h" // softmmu_header.h#elif ACCESS_TYPE == (NB_MMU_MODES + 1) #define CPU_MMU_INDEX (cpu_mmu_index(env))#define MMUSUFFIX _cmmu #else // 生成 ldub_cmmu -> __ldb_cmmu// env->tlb_table[(cpu_mmu_index(env))]                    

  • cpu-exec.i: {ld, st}{sb, ub}_{kernel, user, data, p} p: 直接讀。

QEMU softmmu 有幾處可以加速15)16)17)

System Call

 

TLB

以 x86 為例,有幾種情況會呼叫 tlb_flush。

  1. cpu_x86_update_crN。在 target-i386/translate.c 中,遇到 mov reg, crN 或是 mov crN, reg 會呼叫 helper_write_crN (target-i386/op_helper.c),helper_write_crN 再視情況呼叫 cpu_x86_update_crN (target-i386/helper.c)。Control register

  2. cpu_register_physical_memory_log

  3. cpu_reset

  4. cpu_x86_set_a20

  1. tlb_flush (exec.c)。

                            void tlb_flush(CPUState *env, int flush_global){    int i;     /* must reset current TB so that interrupts cannot modify the links while we are modifying them */    env->current_tb = NULL;     for(i = 0; i < CPU_TLB_SIZE; i++) {        int mmu_idx;        for (mmu_idx = 0; mmu_idx < NB_MMU_MODES; mmu_idx++) {            env->tlb_table[mmu_idx][i] = s_cputlb_empty_entry;        }    }     // 此時 softmmu (tlb) 失效,GVA -> HVA 的對映不再合法,所以要清空以 GVA (guest pc) 當索引的 tb_jmp_cache。    memset (env->tb_jmp_cache, 0, TB_JMP_CACHE_SIZE * sizeof (void *));     env->tlb_flush_addr = -1;    env->tlb_flush_mask = 0;    tlb_flush_count++;}                    

  2. 這時 QEMU 被迫以 tb_find_slow 改以 GPA 查找是否有以翻譯過的 TranslationBlock,同時會進行額外檢查。注意! 這時候有可能會重翻!

                            static TranslationBlock *tb_find_slow(CPUState *env, ...){    for(;;) {        tb = *ptb1;        if (!tb)            goto not_found;        if (tb->pc == pc &&            tb->page_addr[0] == phys_page1 &&            tb->cs_base == cs_base &&            tb->flags == flags) {            /* check next page if needed */            if (tb->page_addr[1] != -1) {                tb_page_addr_t phys_page2;                 virt_page2 = (pc & TARGET_PAGE_MASK) +                    TARGET_PAGE_SIZE;                phys_page2 = get_page_addr_code(env, virt_page2);                if (tb->page_addr[1] == phys_page2)                    goto found;            } else {                goto found;            }        }        ptb1 = &tb->phys_hash_next;    }}                    

User Mode

可參考底下文件,

  1. main (linux-user/main.c) 為進入點。

                            int main(int argc, char **argv, char **envp){    // 初始化 TCG    cpu_exec_init_all(0);     // 初始化 CPUState    env = cpu_init(cpu_model);     // 傳遞環境變數和命令行參數給 guest program    target_environ = envlist_to_environ(envlist, NULL);     // 初始化 TaskState 数据结构    init_task_state(ts);    /* build Task State */    ts->info = info;    ts->bprm = &bprm;    env->opaque = ts; // 後續透過 env->opaque 取得 TaskState    task_settid(ts);     // 載入 guest program。此時 regs 存放程序進入點,之後將此進入點賦值給 virtual CPU。    // 這樣 virtual CPU 就知道從哪裡開始執行。    ret = loader_exec(filename, target_argv, target_environ, regs,        info, &bprm);     target_set_brk(info->brk); // 設置 guest process 的 brk 指針。    syscall_init();    signal_init();     tcg_prologue_init(&tcg_ctx);     // 設置 CPU#if defined(TARGET_I386)    cpu_x86_set_cpl(env, 3); // 將 x86 virtual CPU 特權級設為 ring 3。     env->cr[0] = CR0_PG_MASK | CR0_WP_MASK | CR0_PE_MASK;    env->hflags |= HF_PE_MASK;    if (env->cpuid_features & CPUID_SSE) {        env->cr[4] |= CR4_OSFXSR_MASK;        env->hflags |= HF_OSFXSR_MASK;    }     /* flags setup : we activate the IRQs by default as in user mode */    env->eflags |= IF_MASK;     /* linux register setup */#ifndef TARGET_ABI32    env->regs[R_EAX] = regs->rax;    env->regs[R_EBX] = regs->rbx;    env->regs[R_ECX] = regs->rcx;    env->regs[R_EDX] = regs->rdx;    env->regs[R_ESI] = regs->rsi;    env->regs[R_EDI] = regs->rdi;    env->regs[R_EBP] = regs->rbp;    env->regs[R_ESP] = regs->rsp;    env->eip = regs->rip;#else    env->regs[R_EAX] = regs->eax;    env->regs[R_EBX] = regs->ebx;    env->regs[R_ECX] = regs->ecx;    env->regs[R_EDX] = regs->edx;    env->regs[R_ESI] = regs->esi;    env->regs[R_EDI] = regs->edi;    env->regs[R_EBP] = regs->ebp;    env->regs[R_ESP] = regs->esp;    env->eip = regs->eip; // 前面已將 guest binary 進入點存進 regs。#endif     // 設置中斷    /* linux interrupt setup */#ifndef TARGET_ABI32    env->idt.limit = 511;#else    env->idt.limit = 255;#endif    env->idt.base = target_mmap(0, sizeof(uint64_t) * (env->idt.limit + 1),                                PROT_READ|PROT_WRITE,                                MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);    idt_table = g2h(env->idt.base); // 取得 guest idt (中斷描述符表) 在 host 的位址。    set_idt(0, 0); // 設定    set_idt(1, 0);     // 開始 guest binary -> TCG IR -> host binary,並執行 host binary。    cpu_loop(env);}                    

    • cpu_exec_init_all 初始化 TCG。

                                      void cpu_exec_init_all(unsigned long tb_size){    // 呼叫 tcg_context_init (tcg/tcg.c) 初始化 TCG。tcg_context_init    // 再呼叫 tcg_target_init (tcg/i386/tcg-target.c) 針對宿主機做相應的設置。    cpu_gen_init();    code_gen_alloc(tb_size); // 使用 mmap 將 code_gen_prologue 和 code_gen_buffer 設為可讀、可寫和可執行。    code_gen_ptr = code_gen_buffer;    page_init(); // 扫描 /proc/self/maps。針對每個頁調用 page_set_flags。}                            

    • page_init (exec.c)。

                                      static void page_init(void){        last_brk = (unsigned long)sbrk(0); // 取得目前 QEMU brk 指針,brk 指向堆 (heap) 的頂端,代表程序資料段的結尾。         f = fopen("/compat/linux/proc/self/maps", "r"); // 取得 QEMU 虛擬內存的映象。        if (f) {            mmap_lock();             do {                // 掃描 maps 的內容,透過 h2g (cpu-all.h) 將 host addr 轉換成 guest addr,透過減去 guest addr base。                   // 並檢查 guest addr 是否落在 guest addr space。                     // page_set_flags 檢視該頁面是否被寫入。若被寫入,則清空所有與該頁面有關的 TB。這裡只是要建該頁面的 PageDesc。                    page_set_flags(startaddr, endaddr, PAGE_RESERVED);            } while (!feof(f));       }}                            

  2. cpu_init 依據不同的 guest CPU 型號進行初始化。如 x86,則呼叫 cpu_x86_init (target-i386/helper.c)。

  3. init_task_state (linux-user/main.c) 每個 guest process 都有一個 TaskState

                            void init_task_state(TaskState *ts){    int i;     ts->used = 1;    ts->first_free = ts->sigqueue_table;    for (i = 0; i < MAX_SIGQUEUE_SIZE - 1; i++) {        ts->sigqueue_table[i].next = &ts->sigqueue_table[i + 1];    }    ts->sigqueue_table[i].next = NULL;}                    

  4. loader_exec (linux-user/linuxload.c) 載入 guest program。檢查其執行檔格式和權限。

    • linux_binprm

    • image_info

                                      // ret = loader_exec(filename, target_argv, target_environ, regs,// info, &bprm);int loader_exec(const char * filename, char ** argv, char ** envp,             struct target_pt_regs * regs, struct image_info *infop,             struct linux_binprm *bprm){    retval = open(filename, O_RDONLY);     retval = prepare_binprm(bprm);     if(retval>=0) {        if (bprm->buf[0] == 0x7f                && bprm->buf[1] == 'E'                && bprm->buf[2] == 'L'                && bprm->buf[3] == 'F') {            retval = load_elf_binary(bprm, regs, infop);        }    }     if(retval>=0) {        /* success. Initialize important registers */        // do_init_thread (linux-user/elfload.c) 呼叫 init_thread (linux-user/elfload.c) 根據不同架構        // 初始化 CPU 暫存器。以 x86_64 為例,        //       // regs->rax = 0;       // regs->rsp = infop->start_stack;       // regs->rip = infop->entry;        do_init_thread(regs, infop);        return retval;    }}                            

    • load_elf_binary (linux-user/elfload.c) 將 guest binary 載入成為 image。

      • ELF 之 Program Loading 教學文件, #3: Segment Type 與 Kernel Space Loader

      • Dynamic Linking

                                                int load_elf_binary(struct linux_binprm * bprm, struct target_pt_regs * regs,                    struct image_info * info){    // 將 guest binary 載入成為 image。    load_elf_image(bprm->filename, bprm->fd, info,                   &elf_interpreter, bprm->buf);     // 如果該 guest binary 為動態連結,載入 dynamic linker。透過檢查 guest binary 的 program header 是否帶有 PT_INTERP。    if (elf_interpreter) {        load_elf_interp(elf_interpreter, &interp_info, bprm->buf);    }     bprm->p = create_elf_tables(bprm->p, bprm->argc, bprm->envc, &elf_ex,                                info, (elf_interpreter ? &interp_info : NULL));    info->start_stack = bprm->p;     // 將進入點設為 dynamic linker。    if (elf_interpreter) {        info->load_bias = interp_info.load_bias;        info->entry = interp_info.entry;        free(elf_interpreter);    }}                                    

    • create_elf_tables (linux-user/elfload.c)

                                      static abi_ulong create_elf_tables(abi_ulong p, int argc, int envc,                                   struct elfhdr *exec,                                   struct image_info *info,                                   struct image_info *interp_info){    info->saved_auxv = sp;     sp = loader_build_argptr(envc, argc, sp, p, 0);    return sp;}                            

    • loader_build_argptr (linux-user/linuxload.c) 在 guest (target) 的棧上放置環境變數和命令行參數。put_user_ual (linux-user/qemu.h) 用來將資料搬移至 guest 內存。

                                      /* Construct the envp and argv tables on the target stack. */abi_ulong loader_build_argptr(int envc, int argc, abi_ulong sp, ...){    TaskState *ts = (TaskState *)thread_env->opaque;     sp -= (envc + 1) * n; // 調整棧指針給予環境變數空間。    envp = sp;    sp -= (argc + 1) * n; // 調整棧指針給予命令行參數空間。    argv = sp;     // 將命令行參數寫至 guest 棧上。     while (argc-- > 0) {        /* FIXME - handle put_user() failures */        put_user_ual(stringp, argv);        argv += n;        stringp += target_strlen(stringp) + 1;    }     // 將環境變數寫至 guest 棧上。    while (envc-- > 0) {        /* FIXME - handle put_user() failures */        put_user_ual(stringp, envp);        envp += n;        stringp += target_strlen(stringp) + 1;    } }                            

  5. syscall_init (linux-user/syscall.c) IOCTLEntry

     

  6. signal_init (linux-user/signal.c) 會處理 guest 跟 host 之間 signal 如何對映和處理。

                            // linux-user/syscall_defs.h 定義 TARGET_XXXstatic uint8_t host_to_target_signal_table[_NSIG] = {    [SIGHUP] = TARGET_SIGHUP,} static uint8_t target_to_host_signal_table[_NSIG]; void signal_init(void){    /* generate signal conversion tables */    for(i = 1; i < _NSIG; i++) {        if (host_to_target_signal_table[i] == 0)            host_to_target_signal_table[i] = i;    }    for(i = 1; i < _NSIG; i++) {        j = host_to_target_signal_table[i];        target_to_host_signal_table[j] = i;    }     /* set all host signal handlers. ALL signals are blocked during the handlers to serialize them. */    memset(sigact_table, 0, sizeof(sigact_table));     sigfillset(&act.sa_mask);    act.sa_flags = SA_SIGINFO;    act.sa_sigaction = host_signal_handler;    for(i = 1; i <= TARGET_NSIG; i++) {        host_sig = target_to_host_signal(i);        sigaction(host_sig, NULL, &oact);        if (oact.sa_sigaction == (void *)SIG_IGN) {            sigact_table[i - 1]._sa_handler = TARGET_SIG_IGN;        } else if (oact.sa_sigaction == (void *)SIG_DFL) {            sigact_table[i - 1]._sa_handler = TARGET_SIG_DFL;        }        /* If there's already a handler installed then something has gone horribly wrong, so don't even try to handle that case. */        /* Install some handlers for our own use. We need at least SIGSEGV and SIGBUS, to detect exceptions. We can not just trap all signals because it affects syscall interrupt behavior. But do trap all default-fatal signals. */        if (fatal_signal (i))            sigaction(host_sig, &act, NULL);    }}                    

  7. 設定 virtual CPU 暫存器、中斷描述符表、全域描述符表和段描述符。

                            static uint64_t *idt_table; static void set_gate(void *ptr, unsigned int type, unsigned int dpl,                     uint32_t addr, unsigned int sel){    uint32_t *p, e1, e2;    e1 = (addr & 0xffff) | (sel << 16);    e2 = (addr & 0xffff0000) | 0x8000 | (dpl << 13) | (type << 8);    p = ptr;    p[0] = tswap32(e1);    p[1] = tswap32(e2);} static void set_idt(int n, unsigned int dpl){    set_gate(idt_table + n, 0, dpl, 0, 0);}                    

  8. cpu_loop (linux-user/main.c) 為主要執行迴圈,呼叫 cpu_x86_exec 進行 guest binary → TCG → host binary 並執行的流程。cpu_x86_exec/cpu_exec (cpu-exec.c) 會返回例外號,cpu_loop 視例外號的不同做相對應的處理,之後處理訊號。不同架構定義不同的 cpu_loop。以 x86 為例,

                            void cpu_loop(CPUX86State *env){    int trapnr;    abi_ulong pc;    target_siginfo_t info;     for(;;) {        trapnr = cpu_x86_exec(env);        switch(trapnr) {        case 0x80:            /* linux syscall from int $0x80 */            env->regs[R_EAX] = do_syscall(env,                                          env->regs[R_EAX],                                          env->regs[R_EBX],                                          env->regs[R_ECX],                                          env->regs[R_EDX],                                          env->regs[R_ESI],                                          env->regs[R_EDI],                                          env->regs[R_EBP],                                          0, 0);            break;         /* different cases */        default:            pc = env->segs[R_CS].base + env->eip;            fprintf(stderr, "qemu: 0x%08lx: unhandled CPU exception 0x%x - aborting\n",                    (long)pc, trapnr);            abort();        }        process_pending_signals(env); // linux-user/signal.c    }}                    

System Call

QEMU user mode 在處理系統呼叫時,不像 system mode 需要翻譯 system call handler。

  1. cpu_loop (linux-user/main.c)。

                            void cpu_loop(CPUX86State *env){    int trapnr;    abi_ulong pc;    target_siginfo_t info;     for(;;) {        trapnr = cpu_x86_exec(env);        switch(trapnr) {        case 0x80:            /* linux syscall from int $0x80 */            env->regs[R_EAX] = do_syscall(env,                                          env->regs[R_EAX],                                          env->regs[R_EBX],                                          env->regs[R_ECX],                                          env->regs[R_EDX],                                          env->regs[R_ESI],                                          env->regs[R_EDI],                                          env->regs[R_EBP],                                          0, 0);            break;          ... 略 ...     }      ... 略 ... }                    

  2. do_syscall (linux-user/syscall.c) 將客戶 system call 做些包裝之後,轉交給宿主 system call。

    abi_long do_syscall(void *cpu_env, int num, abi_long arg1,                    abi_long arg2, abi_long arg3, abi_long arg4,                    abi_long arg5, abi_long arg6, abi_long arg7,                    abi_long arg8){    switch(num) {     ... 略 ...     case TARGET_NR_open:        if (!(p = lock_user_string(arg1)))            goto efault;        ret = get_errno(do_open(cpu_env, p,                                target_to_host_bitmask(arg2, fcntl_flags_tbl),                                arg3));        unlock_user(p, arg1, 0);        break;     ... 略 ...     }     ... 略 ...}                    

  3. do_open (linux-user/syscall.c)。

                            static int do_open(void *cpu_env, const char *pathname, int flags, mode_t mode){    // 前置處理。     // 呼叫 host system call。    return get_errno(open(path(pathname), flags, mode));}                    

Interrupt & Exception Handling

以 x86 為例,

  • cpu-defs.h 定義例外號。

                            #define EXCP_INTERRUPT 0x10000 /* async interruption */#define EXCP_HLT 0x10001 /* hlt instruction reached */#define EXCP_DEBUG 0x10002 /* cpu stopped after a breakpoint or singlestep */#define EXCP_HALTED 0x10003 /* cpu is halted (waiting for external event) */                    

  • 有些函式前面會加上 QEMU_NORETURN (compiler.h),這代表該函式不會返回。

                            #define QEMU_NORETURN __attribute__ ((__noreturn__))                    

  1. cpu_exec (cpu-exec.c)

                            int cpu_exec(CPUState *env){    if (env->halted) { // system mode 才會拉起 env->halted。        if (!cpu_has_work(env)) {            return EXCP_HALTED;        }         env->halted = 0;    }     cpu_single_env = env; // 保存當前 env。待 lonjmp 時,可以用 cpu_single_env 回復 env。     if (unlikely(exit_request)) {        env->exit_request = 1;    } // 不同架構會有不同前置處理。#if defined(TARGET_I386)    CC_SRC = env->eflags & (CC_O | CC_S | CC_Z | CC_A | CC_P | CC_C);    DF = 1 - (2 * ((env->eflags >> 10) & 1));    CC_OP = CC_OP_EFLAGS;    env->eflags &= ~(DF_MASK | CC_O | CC_S | CC_Z | CC_A | CC_P | CC_C);#else#error unsupported target CPU#endif    // cpu_exec 返回值即為 env->exception_index。以 process mode 為例,cpu_loop 在呼叫 cpu_exec 之後,會檢視其返回值並做相應處理。    env->exception_index = -1;     // 進行翻譯並執行的迴圈。    /* prepare setjmp context for exception handling */    for(;;) {        if (setjmp(env->jmp_env) == 0) { // 正常流程。             next_tb = 0; /* force lookup of first TB */            for(;;) {             } /* for(;;) */        } else {            /* Reload env after longjmp - the compiler may have smashed all * local variables as longjmp is marked 'noreturn'. */            env = cpu_single_env;        }    } /* for(;;) */}                    

    • QEMU 支持 precise exception。當例外發生時,執行流程會將舊的 env (cpu_single_env) 存回 env。

  2. 外層迴圈。cpu_exec 利用 setjmp/longjmp 處理例外。cpu_loop_exit (cpu-exec.c) 和 cpu_resume_from_signal (cpu-exec.c) 會呼叫 longjmp 回到 setjmp 設定的例外處理分支。18)

                                for(;;) {        if (setjmp(env->jmp_env) == 0) {            /* if an exception is pending, we execute it here */            if (env->exception_index >= 0) { // exception_index 非 -1 代表有事要處理。                if (env->exception_index >= EXCP_INTERRUPT) { // 來自 cpu_exec 以外的例外。                    /* exit request from the cpu execution loop */                    ret = env->exception_index;                    if (ret == EXCP_DEBUG) {                        cpu_handle_debug_exception(env);                    }                    break;                } else { #if defined(CONFIG_USER_ONLY)                    /* if user mode only, we simulate a fake exception which will be handled outside the cpu execution loop */#if defined(TARGET_I386)                    do_interrupt(env);#endif                    ret = env->exception_index;                    break;#else                }            }         } else {            /* Reload env after longjmp - the compiler may have smashed all * local variables as longjmp is marked 'noreturn'. */            env = cpu_single_env;        }    } /* for(;;) */     /* 回復 env 中的欄位,清空 cpu_single_env,返回 cpu_loop 處理例外 */}                    

    • cpu_loop_exit (cpu-exec.c)

                                      void cpu_loop_exit(CPUState *env){    env->current_tb = NULL;    longjmp(env->jmp_env, 1);}                            

    • cpu_resume_from_signal (cpu-exec.c)

                                      /* exit the current TB from a signal handler. The host registers are restored in a state compatible with the CPU emulator */#if defined(CONFIG_SOFTMMU)void cpu_resume_from_signal(CPUState *env, void *puc){    /* XXX: restore cpu registers saved in host registers */     env->exception_index = -1;    longjmp(env->jmp_env, 1);}#endif                            

  3. user mode 和 system mode 有不同處置。

                            void do_interrupt(CPUState *env1){    CPUState *saved_env;     saved_env = env;    env = env1;#if defined(CONFIG_USER_ONLY)    /* if user mode only, we simulate a fake exception which will be handled outside the cpu execution loop */    do_interrupt_user(env->exception_index,                      env->exception_is_int,                      env->error_code,                      env->exception_next_eip);    /* successfully delivered */    env->old_exception = -1;#else    /* simulate a real cpu exception. On i386, it can trigger new exceptions, but we do not handle double or triple faults yet. */    do_interrupt_all(env->exception_index,                     env->exception_is_int,                     env->error_code,                     env->exception_next_eip, 0);    /* successfully delivered */    env->old_exception = -1;#endif    env = saved_env;}                    

    • user mode。

      • General protection fault

                                                #if defined(CONFIG_USER_ONLY)/* fake user mode interrupt */static void do_interrupt_user(int intno, int is_int, int error_code,                              target_ulong next_eip){    SegmentCache *dt;    target_ulong ptr;    int dpl, cpl, shift;    uint32_t e2;     dt = &env->idt; // 取出中斷描述符表    if (env->hflags & HF_LMA_MASK) {        shift = 4;    } else {        shift = 3;    }    ptr = dt->base + (intno << shift); // 取出中斷處理常式    e2 = ldl_kernel(ptr + 4);     dpl = (e2 >> DESC_DPL_SHIFT) & 3;    cpl = env->hflags & HF_CPL_MASK;    /* check privilege if software int */    if (is_int && dpl < cpl)        raise_exception_err(EXCP0D_GPF, (intno << shift) + 2); // target-i386/cpu.h     /* Since we emulate only user space, we cannot do more than exiting the emulation with the suitable exception and error code */    if (is_int)        EIP = next_eip; // target-i386/cpu.h:#define EIP (env->eip)} #else                                    

      • raise_exception_err (target-i386/op_helper.c) 轉呼叫 raise_interrupt (target-i386/op_helper.c)。

                                                static void QEMU_NORETURN raise_exception_err(int exception_index,                                              int error_code){    raise_interrupt(exception_index, 0, error_code, 0);}                                    

      • raise_interrupt (target-i386/op_helper.c) 最後呼叫 cpu_loop_exit (cpu-exec.c) longjmp 回外層迴圈 else 分支。

                                                /* * Signal an interruption. It is executed in the main CPU loop. * is_int is TRUE if coming from the int instruction. next_eip is the * EIP value AFTER the interrupt instruction. It is only relevant if * is_int is TRUE. */static void QEMU_NORETURN raise_interrupt(int intno, int is_int, int error_code,                                          int next_eip_addend){    if (!is_int) {        helper_svm_check_intercept_param(SVM_EXIT_EXCP_BASE + intno, error_code);        intno = check_exception(intno, &error_code);    } else {        helper_svm_check_intercept_param(SVM_EXIT_SWINT, 0);    }     env->exception_index = intno;    env->error_code = error_code;    env->exception_is_int = is_int;    env->exception_next_eip = env->eip + next_eip_addend;    cpu_loop_exit(env);}                                    

    • system mode。

      • do_interrupt_all (target-i386/op_helper.c)。

                                                static void do_interrupt_all(int intno, int is_int, int error_code,                             target_ulong next_eip, int is_hw){    ... 略 ...     // 檢查當前處於何種模式,交由相對應的函式處理。如果客戶機是 64 bit,還有 do_interrupt_64。    if (env->cr[0] & CR0_PE_MASK) {        if (env->hflags & HF_SVMI_MASK)            handle_even_inj(intno, is_int, error_code, is_hw, 0);        {            do_interrupt_protected(intno, is_int, error_code, next_eip, is_hw);        }    } else {        do_interrupt_real(intno, is_int, error_code, next_eip);    }     ... 略 ...}                                    

      • 內核態與用戶態分別有內核棧和用戶棧。

      • do_interrupt_real

                                                static void do_interrupt_real(int intno, int is_int, int error_code,                              unsigned int next_eip){    ... 略 ...     // 取得內核棧棧頂。    ssp = env->segs[R_SS].base;     esp = ESP; // #define ESP (env->regs[R_ESP]) (target-i386/cpu.h)    ssp = env->segs[R_SS].base;     // 將用戶態資訊放至內核棧。    /* XXX: use SS segment size ? */    PUSHW(ssp, esp, 0xffff, compute_eflags());    PUSHW(ssp, esp, 0xffff, old_cs);    PUSHW(ssp, esp, 0xffff, old_eip);     // 將 env->eip 指向中斷向量。返回 cpu_exec 後便會翻譯中斷向量並執行。    /* update processor state */    ESP = (ESP & ~0xffff) | (esp & 0xffff);    env->eip = offset;    env->segs[R_CS].selector = selector;    env->segs[R_CS].base = (selector << 4);    env->eflags &= ~(IF_MASK | TF_MASK | AC_MASK | RF_MASK);}                                    

      • helper_iret_real。當中斷向量執行完畢後,會執行 iret 返回用戶態。

                                                void helper_iret_real(int shift){    ... 略 ...     // 將用戶態資訊從內核棧取出。    if (shift == 1) {        /* 32 bits */        POPL(ssp, sp, sp_mask, new_eip);        POPL(ssp, sp, sp_mask, new_cs);        new_cs &= 0xffff;        POPL(ssp, sp, sp_mask, new_eflags);    } else {        /* 16 bits */        POPW(ssp, sp, sp_mask, new_eip);        POPW(ssp, sp, sp_mask, new_cs);        POPW(ssp, sp, sp_mask, new_eflags);    }    ESP = (ESP & ~sp_mask) | (sp & sp_mask);    env->segs[R_CS].selector = new_cs;    env->segs[R_CS].base = (new_cs << 4);    env->eip = new_eip; // 返回用戶態後,欲執行的 pc。    if (env->eflags & VM_MASK)        eflags_mask = TF_MASK | AC_MASK | ID_MASK | IF_MASK | RF_MASK | NT_MASK;    else        eflags_mask = TF_MASK | AC_MASK | ID_MASK | IF_MASK | IOPL_MASK | RF_MASK | NT_MASK;    if (shift == 0)        eflags_mask &= 0xffff;    load_eflags(new_eflags, eflags_mask);    env->hflags2 &= ~HF2_NMI_MASK;}                                    

  4. 內層迴圈。

                next_tb = 0; /* force lookup of first TB */            for(;;) {                interrupt_request = env->interrupt_request;                // 檢視 interrupt_request 是何種中斷,並將 interrupt_request 復位。                   // 設置 env->exception_index,再跳至 cpu_loop_exit。                   // cpu_loop_exit 再 longjmp 到外層迴圈 setjmp 的點,跳到處理中斷的分支。                  if (unlikely(interrupt_request)) {                }                 // 檢視 env->exit_request。                if (unlikely(env->exit_request)) {                    env->exit_request = 0;                    env->exception_index = EXCP_INTERRUPT;                    cpu_loop_exit(env);                }                 tb = tb_find_fast(env);                 env->current_tb = tb;                barrier();                // 若無 exit_request,跳入 code cache 開始執行。                if (likely(!env->exit_request)) {                }                env->current_tb = NULL;                /* reset soft MMU for next block (it can currently only be set by a memory fault) */            } /* for(;;) */                    

Precise Exception

Precise exception 要求當某一條 guest 指令發生例外,例如溢位或是頁缺失,此條指令之前的運算必須完成,此條指令之後的運算必須捨棄。當 QEMU 在 code cache 中執行翻譯過後的指令,其中發生例外時,QEMU 必須要能維護例外發生之前 guest 的暫存器和內存內容,並能反查出發生例外的 basic block 相對映開頭的 guest pc。從那個 guest pc 重新開始翻譯直到發生例外的位址,並把這個 guest pc 存回 CPUState,這是因為 QEMU 不會執行完一條 guest 指令就更新 guest pc。

  1. 暫存器: 在有可能發生例外的運算之前,如: guest load/store 或是呼叫 helper function,QEMU 會把 dirty CPUState 寫回內存。

  2. 內存: QEMU 不會將原本指令亂序。

QEMU 會用到底下定義在 translate-all.c 資料結構:

target_ulong gen_opc_pc[OPC_BUF_SIZE]; // 紀錄 guest pc。uint16_t gen_opc_icount[OPC_BUF_SIZE];uint8_t gen_opc_instr_start[OPC_BUF_SIZE]; // 當作標記之用。        

針對 x86,又在 target-i386/translate.c 定義以下資料結構:

            static uint8_t gen_opc_cc_op[OPC_BUF_SIZE]; // 紀錄 condition code。        

QEMU 會以 retaddr 判斷在哪裡發生例外,這用到 GCC Getting the Return or Frame Address of a Function 擴展。注意! QEMU 在兩個地方定義 GETPC,分別位於 exec-all.h 和 exec.c。

  1. exec-all.h。定義給 code cache 使用的 softmmu 函式。

                            // __builtin_return_address 返回當前函式 (參數 0) 的返回位址。#else# define GETPC() ((void *)((unsigned long)__builtin_return_address(0) - 1))#endif #include "softmmu_defs.h" #define ACCESS_TYPE (NB_MMU_MODES + 1)#define MEMSUFFIX _code#define env cpu_single_env #define DATA_SIZE 1#include "softmmu_header.h" #define DATA_SIZE 2#include "softmmu_header.h" #define DATA_SIZE 4#include "softmmu_header.h" #define DATA_SIZE 8#include "softmmu_header.h" #undef ACCESS_TYPE#undef MEMSUFFIX#undef env #endif                    

  2. exec.c。定義給一般 C 函式使用的 softmmu 函式。

                            #define MMUSUFFIX _cmmu#undef GETPC#define GETPC() NULL#define env cpu_single_env#define SOFTMMU_CODE_ACCESS #define SHIFT 0#include "softmmu_template.h" #define SHIFT 1#include "softmmu_template.h" #define SHIFT 2#include "softmmu_template.h" #define SHIFT 3#include "softmmu_template.h" #undef env #endif static inline void svm_load_seg(target_phys_addr_t addr, SegmentCache *sc){    unsigned int flags;     sc->selector = lduw_phys(addr + offsetof(struct vmcb_seg, selector));    sc->base = ldq_phys(addr + offsetof(struct vmcb_seg, base));    sc->limit = ldl_phys(addr + offsetof(struct vmcb_seg, limit));    flags = lduw_phys(addr + offsetof(struct vmcb_seg, attrib));    sc->flags = ((flags & 0xff) << 8) | ((flags & 0x0f00) << 12);}                    

  1. tlb_fill。env 是 dyngen-exec.h 中定義的全域變數,是一個指向 CPUState 的指針,該指針存放在宿主機特定的暫存器20)

                            #if !defined(CONFIG_USER_ONLY)/* try to fill the TLB and return an exception if error. If retaddr is NULL, it means that the function was called in C code (i.e. not from generated code or from helper.c) *//* XXX: fix it to restore all registers */void tlb_fill(CPUState *env1, target_ulong addr, int is_write, int mmu_idx,              void *retaddr){    TranslationBlock *tb;    int ret;    unsigned long pc;    CPUX86State *saved_env;     saved_env = env;  // 備份 global env。    env = env1;       // 將當前 env (env1) 賦值與 global env。     ret = cpu_x86_handle_mmu_fault(env, addr, is_write, mmu_idx);    if (ret) {        if (retaddr) {            /* now we have a real cpu fault */            pc = (unsigned long)retaddr;            tb = tb_find_pc(pc);            if (tb) {                /* the PC is inside the translated code. It means that we have a virtual CPU fault */                cpu_restore_state(tb, env, pc);            }        }        raise_exception_err(env->exception_index, env->error_code);    }    env = saved_env;}#endif                    

    • tb_find_slow → get_page_addr_code → ldub_code → \_\_ldb_cmmu → tlb_fill。此時,retaddr 為 NULL,代表是從一般 C 函式呼叫到 tlb_fill。

    • code cache → \_\_ldl_mmu → tlb_fill。此時,retaddr 不為 NULL,代表是從 code cache 呼叫到 tlb_fill,guest binary 通常在做 guest memory operation。

  2. 透過 tb_find_pc 以發生例外 host binary (retaddr) 位址反查 TranslationBlock。

                            /* find the TB 'tb' such that tb[0].tc_ptr <= tc_ptr < tb[1].tc_ptr. Return NULL if not found */TranslationBlock *tb_find_pc(unsigned long tc_ptr){    int m_min, m_max, m;    unsigned long v;    TranslationBlock *tb;     if (nb_tbs <= 0)        return NULL;    // code_gen_buffer 是 code cache 起始位址,code_gen_ptr 是 code cache 目前生成 host binary 會放的位址。    if (tc_ptr < (unsigned long)code_gen_buffer ||        tc_ptr >= (unsigned long)code_gen_ptr)        return NULL;    /* binary search (cf Knuth) */    m_min = 0;    m_max = nb_tbs - 1;    while (m_min <= m_max) {        m = (m_min + m_max) >> 1;        tb = &tbs[m];        v = (unsigned long)tb->tc_ptr;        // 例外發生所在的 host 位址和該 TranslationBlock 在 code cache 的位址一致。        if (v == tc_ptr)            return tb;        else if (tc_ptr < v) {            m_max = m - 1;        } else {            m_min = m + 1;        }    }    return &tbs[m_max]; // 返回發生例外的 TranslationBlock。}                    

  3. cpu_restore_state (translate-all.c) 重翻該 TranslationBlock (guest binary → TCG IR → host binary),同時附帶上 guest pc 資訊。searched_pc 為發生例外的 host binary 位址。

                            /* The cpu state corresponding to 'searched_pc' is restored. */int cpu_restore_state(TranslationBlock *tb,                      CPUState *env, unsigned long searched_pc){    TCGContext *s = &tcg_ctx;    int j;    unsigned long tc_ptr;     tcg_func_start(s); // 初始 gen_opc_ptr 和 gen_opparam_ptr     // 轉呼叫 gen_intermediate_code_internal,要求在生成 TCG IR 的同時,為其生成相關的 pc 資訊。    gen_intermediate_code_pc(env, tb);     if (use_icount) {        /* Reset the cycle counter to the start of the block. */        env->icount_decr.u16.low += tb->icount;        /* Clear the IO flag. */        env->can_do_io = 0;    }     /* find opc index corresponding to search_pc */    // tc_ptr 指向 host binary 在 code cache 的起始位址。    // 如果 searched_pc 小於 tc_ptr,代表此 tb 並非是負責人。    tc_ptr = (unsigned long)tb->tc_ptr;    if (searched_pc < tc_ptr)        return -1;     s->tb_next_offset = tb->tb_next_offset;#ifdef USE_DIRECT_JUMP    s->tb_jmp_offset = tb->tb_jmp_offset;    s->tb_next = NULL;#else    s->tb_jmp_offset = NULL;    s->tb_next = tb->tb_next;#endif    // 轉呼叫 tcg_gen_code_common (tcg/tcg.c)    j = tcg_gen_code_search_pc(s, (uint8_t *)tc_ptr, searched_pc - tc_ptr);    if (j < 0)        return -1;    /* now find start of instruction before */    while (gen_opc_instr_start[j] == 0)        j--;    env->icount_decr.u16.low -= gen_opc_icount[j];     // 此時,用 j 索引 gen_opc_pc 可以得到對應的 guest pc。    restore_state_to_opc(env, tb, j);     return 0;}                    

    1. gen_intermediate_code_pc (target-i386/translate.c) 轉呼叫 gen_intermediate_code_internal,search_pc 為真。將 guest binary 翻成 TCG IR 的同時,紀錄下 guest pc。

                                      static inline void gen_intermediate_code_internal(CPUState *env,                                                  TranslationBlock *tb,                                                  int search_pc){    for(;;) {        if (search_pc) {            // gen_opc_ptr 為 TCG opcode buffer 目前位址,gen_opc_buf 為 TCG opcode buffer。            j = gen_opc_ptr - gen_opc_buf;            if (lj < j) {                lj++;                while (lj < j)                    gen_opc_instr_start[lj++] = 0; // 不到 j 的部分填零。            }            gen_opc_pc[lj] = pc_ptr; // 紀錄 guest pc。            gen_opc_cc_op[lj] = dc->cc_op; // 紀錄 condition code。            gen_opc_instr_start[lj] = 1; // 填 1 作為標記。            gen_opc_icount[lj] = num_insns;        }    }    if (tb->cflags & CF_LAST_IO)        gen_io_end();    gen_icount_end(tb, num_insns);    *gen_opc_ptr = INDEX_op_end;    /* we don't forget to fill the last values */    if (search_pc) {        j = gen_opc_ptr - gen_opc_buf;        lj++;        while (lj <= j)            gen_opc_instr_start[lj++] = 0;    }}                            

    2. tcg_gen_code_search_pc (tcg/tcg.c) 轉呼叫 tcg_gen_code_common (tcg/tcg.c) 將 TCG IR 翻成 host binary。search_pc 應該改叫 offset。

                                      /* Return the index of the micro operation such as the pc after is < offset bytes from the start of the TB. The contents of gen_code_buf must not be changed, though writing the same values is ok. Return -1 if not found. */static inline int tcg_gen_code_common(TCGContext *s, uint8_t *gen_code_buf,                                      long search_pc){    for(;;) {        switch(opc) {        case INDEX_op_nopn:            args += args[0];            goto next;        case INDEX_op_call:            dead_args = s->op_dead_args[op_index];            args += tcg_reg_alloc_call(s, def, opc, args, dead_args);            goto next;        }        args += def->nb_args;    next:        // 如果 offset (search_pc) 落在目前 code cache 起始位址和 code cache 目前存放 host binary 的位址之間,        // 返回 TCG op index。        if (search_pc >= 0 && search_pc < s->code_ptr - gen_code_buf) {            return op_index;        }        op_index++;    }}                            

    3. restore_state_to_opc (target-i386/translate.c)

                                      void restore_state_to_opc(CPUState *env, TranslationBlock *tb, int pc_pos){    int cc_op;     env->eip = gen_opc_pc[pc_pos] - tb->cs_base;    cc_op = gen_opc_cc_op[pc_pos];    if (cc_op != CC_OP_DYNAMIC)        env->cc_op = cc_op;}                            

  4. raise_exception_err (target-i386/op_helper.c)

                            static void QEMU_NORETURN raise_exception_err(int exception_index,                                              int error_code){    raise_interrupt(exception_index, 0, error_code, 0);}                    

  5. raise_interrupt (target-i386/op_helper.c)

                            /* * Signal an interruption. It is executed in the main CPU loop. * is_int is TRUE if coming from the int instruction. next_eip is the * EIP value AFTER the interrupt instruction. It is only relevant if * is_int is TRUE. */static void QEMU_NORETURN raise_interrupt(int intno, int is_int, int error_code,                                          int next_eip_addend){    if (!is_int) {        // 走這裡。        helper_svm_check_intercept_param(SVM_EXIT_EXCP_BASE + intno, error_code);        intno = check_exception(intno, &error_code);    } else {        helper_svm_check_intercept_param(SVM_EXIT_SWINT, 0);    }     env->exception_index = intno;    env->error_code = error_code;    env->exception_is_int = is_int;    env->exception_next_eip = env->eip + next_eip_addend;    cpu_loop_exit(env);}                    

    • 這裡操作的 env 是 global env,在 tlb_fill 已經同步成當前 env。

  6. helper_svm_check_intercept_param (target-i386/op_helper.c)

  7. cpu_loop_exit (cpu-exec.c)

                            void cpu_loop_exit(CPUState *env){    env->current_tb = NULL;    longjmp(env->jmp_env, 1);}                    

以 linux-0.11 為例,

            (gdb) b gen_intermediate_code_internal if tb->pc == 0xe4c0(gdb) r -m 1024M -boot a -fda linux-0.11/Image -hda rootfs/hdc-0.11-new.img -vnc 0.0.0.0:1        
  1. Breakpoint 1, gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe75b9e60, search_pc=0) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:77317731    {(gdb) bt#0  gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe75b9e60, search_pc=0) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:7731#1  0x0000000000535551 in gen_intermediate_code (env=0x110e5d0, tb=0x7fffe75b9e60) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:7907#2  0x000000000050fdee in cpu_x86_gen_code (env=0x110e5d0, tb=0x7fffe75b9e60, gen_code_size_ptr=0x7fffffffdd30)    at /tmp/chenwj/qemu-0.13.0/translate-all.c:73#3  0x000000000050871f in tb_gen_code (env=0x110e5d0, pc=58560, cs_base=0, flags=2740, cflags=0) at /tmp/chenwj/qemu-0.13.0/exec.c:962#4  0x0000000000510a7a in tb_find_slow (pc=58560, cs_base=0, flags=2740) at /tmp/chenwj/qemu-0.13.0/cpu-exec.c:167

  2. 下 ls。0x4011809f 位在 code cache 之內。

    Breakpoint 1, gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe75b9e60, search_pc=1) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:77317731    {(gdb) bt#0  gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe75b9e60, search_pc=1) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:7731#1  0x000000000053559e in gen_intermediate_code_pc (env=0x110e5d0, tb=0x7fffe75b9e60) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:7912#2  0x000000000050ff4d in cpu_restore_state (tb=0x7fffe75b9e60, env=0x110e5d0, searched_pc=1074888863, puc=0x0)    at /tmp/chenwj/qemu-0.13.0/translate-all.c:130#3  0x000000000054effd in tlb_fill (addr=268513280, is_write=1, mmu_idx=0, retaddr=0x4011809f) at /tmp/chenwj/qemu-0.13.0/target-i386/op_helper.c:4836#4  0x000000000054d4f0 in __stb_mmu (addr=268513280, val=150 '\226', mmu_idx=0) at /tmp/chenwj/qemu-0.13.0/softmmu_template.h:272#5  0x00000000401180a0 in ?? ()

    • ----------------IN:0x0000e4c0:  sub    $0x4,%esp0x0000e4c3:  mov    0x8(%esp),%eax0x0000e4c7:  mov    %al,(%esp)0x0000e4ca:  movzbl (%esp),%eax0x0000e4ce:  mov    0xc(%esp),%edx0x0000e4d2:  mov    %al,%fs:(%edx)  <---0x0000e4d5:  add    $0x4,%esp0x0000e4d8:  ret ---- 0xe4d2 mov_i32 tmp2,edx ld_i32 tmp4,env,$0x84 add_i32 tmp2,tmp2,tmp4 mov_i32 tmp0,eax qemu_st8 tmp0,tmp2,$0x0            <---RESTORE:0x0000: 0000e4c00x0007: 0000e4c30x000d: 0000e4c70x0011: 0000e4ca0x0015: 0000e4ce0x001b: 0000e4d2// spc 是觸發例外的 host binary 位址,等同 retaddr。// eip 是觸發例外的 guest binary 位址。spc=0x401255bf pc_pos=0x1b eip=0000e4d2 cs_base=0Servicing hardware INT=0x20

    • 0x40125577:  mov    %eax,%ebp              // 保存 CPUState0x40125579:  mov    %ebp,%ebx0x4012557b:  mov    0x84(%r14),%r12d0x40125582:  add    %r12d,%ebx0x40125585:  mov    (%r14),%r12d0x40125588:  mov    %ebp,0x8(%r14)0x4012558c:  mov    %ebx,%esi              // 查詢 TLB0x4012558e:  mov    %ebx,%edi0x40125590:  shr    $0x7,%esi0x40125593:  and    $0xfffff000,%edi0x40125599:  and    $0x1fe0,%esi0x4012559f:  lea    0x34c(%r14,%rsi,1),%rsi0x401255a7:  cmp    (%rsi),%edi0x401255a9:  mov    %ebx,%edi0x401255ab:  jne    0x401255b6             // TLB 不命中,跳至 0x401255b6。0x401255ad:  add    0xc(%rsi),%rdi0x401255b1:  mov    %r12b,(%rdi)0x401255b4:  jmp    0x401255c00x401255b6:  mov    %r12d,%esi             // 不命中,呼叫 __stb_mmu0x401255b9:  xor    %edx,%edx0x401255bb:  callq  0x54d38a               // 執行 __stb_mmu 的時候,發生頁缺失例外。0x401255c0:  mov    0x10(%r14),%ebp

  • 執行 \_\_stb_mmu 的時候,會呼叫 tlb_fill。tlb_fill 呼叫 cpu_x86_handle_mmu_fault 查找客戶機頁表,若該頁存在,將其頁表項填入 tlb; 否則觸發頁缺失例外。

數個 tb 可能會相對應 (不同進程的) guest pc。

Breakpoint 1, gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe75d2eb0, search_pc=0) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:77317731    {Breakpoint 1, gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe76232c0, search_pc=0) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:77317731    {
  

TCG Flow

請閱讀以下文章:

TCG 是 dynamic binary tranlation。QEMU 1.0 預計會加入 TCI (tiny code interpreter)。請見 [Qemu-devel] [PATCH 0/8] tcg/interpreter: Add TCG + interpreter for bytecode (virtual machine)。在上下文有提到 TCG 的話,其所指的 target 必定是指 QEMU 運行其上的 host。21)

底下是 TCG 動態翻譯流程中會使用到的緩衝區:

  1. gen_opc_buf 和 gen_opparam_buf (translate-all.c) 是 TCG IR 所在位置。

                            uint16_t gen_opc_buf[OPC_BUF_SIZE]; // 放置 TCG opcode。TCGArg gen_opparam_buf[OPPARAM_BUF_SIZE]; // 放置 TCG opcode 會用到的 operand。                    

  2. 如果使用靜態配置的緩衝區,static_code_gen_buffer (exec.c) 是 translation block code cache (host binary) 所在位置。

                            #ifdef USE_STATIC_CODE_GEN_BUFFERstatic uint8_t static_code_gen_buffer[DEFAULT_CODE_GEN_BUFFER_SIZE]               __attribute__((aligned (CODE_GEN_ALIGN)));#endif                    

  3. 在跳入/出 code cache 執行之前/後,要執行 prologue/epilogue。

                            // 根據不同平台,code_gen_section 用不同的 __attribute__ 修飾 code_gen_prologue。uint8_t code_gen_prologue[1024] code_gen_section;                    

以 qemu-i386 為例,主要流程如下:

main (linux-user/main.c) → cpu_exec_init_all (exec.c) → cpu_init/cpu_x86_init (target-i386/helper.c) → tcg_prologue_init (tcg/tcg.c) → cpu_loop (linux-user/main.c)

  • tcg_prologue_init (tcg/tcg.c) → tcg_target_qemu_prologue (tcg/i386/tcg-target.c)

    • 進 translation block code cache 之前和之後,需要執行 prologue 和 epilogue。請參考 Calling convention 和 Which x86 registers to save in C functions?

                                      void tcg_prologue_init(TCGContext *s){    /* init global prologue and epilogue */    s->code_buf = code_gen_prologue;    s->code_ptr = s->code_buf;    tcg_target_qemu_prologue(s);    flush_icache_range((unsigned long)s->code_buf,                       (unsigned long)s->code_ptr);} static void tcg_target_qemu_prologue(TCGContext *s){    // QEMU -> prologue -> code cache。prologue -> code cache 被當作一個函式。    // 寫入 host binary。    tcg_out_push(s, tcg_target_callee_save_regs[i]); // 將 callee saved 的暫存器入棧     tcg_out_addi(s, TCG_REG_ESP, -stack_addend); // 調整棧指針,加大棧     // OPC_GRP5 (0xff) 為 call,EXT5_JMPN_Ev 是其 opcode extension。   // tcg_target_call_iarg_regs 是函式呼叫負責傳遞參數的暫存器。    tcg_out_modrm(s, OPC_GRP5, EXT5_JMPN_Ev, tcg_target_call_iarg_regs[0]); // 跳至 code cache 執行    // 此時,s->code_ptr 指向 code_gen_prologue 中 prologue 和 jmp to code cache 之後的位址。   // tb_ret_addr 是紀錄 code cache 跳回 code_gen_prologue 的哪個地方。   tb_ret_addr = s->code_ptr;    tcg_out_addi(s, TCG_REG_ESP, stack_addend); // 調整棧指針,縮小棧    tcg_out_pop(s, tcg_target_callee_save_regs[i]); // 回復 callee saved 的暫存器     tcg_out_opc(s, OPC_RET, 0, 0, 0); // 返回 QEMU}                            

  • cpu_loop (linux-user/main.c) → cpu_x86_exec/cpu_exec (cpu-exec.c) → tb_find_fast (cpu-exec.c)

    • tb_find_fast (cpu-exec.c) → tb_find_slow (cpu-exec.c) → tb_gen_code (exec.c) → cpu_gen_code (translate-all.c) → gen_intermediate_code (target-i386/translate.c) → tcg_gen_code (tcg/tcg.c) → tcg_gen_code_common (tcg/tcg.c)

    • tcg_gen_code_common (tcg/tcg.c) → tcg_reg_alloc_op (tcg/tcg.c) → tcg_out_op (tcg/i386/tcg-target.c)

  1. cpu_exec (cpu-exec.c) 為主要執行迴圈。cpu_exec_nocache 是在 system mode 啟用 icount 的時候才會被用到

    next_tb = 0; /* force lookup of first TB */for(;;) {    // 處理中斷。這裡會檢視是何種中斷,並將 interrupt_request 復位。設置 exception_index,再跳至 cpu_loop_exit。     // cpu_loop_exit 再 longjmp 到外層迴圈 setjmp 的點,跳到處理中斷的分支。     if (unlikely(interrupt_request)) {        if (interrupt_request & CPU_INTERRUPT_DEBUG) {            env->interrupt_request &= ~CPU_INTERRUPT_DEBUG;            env->exception_index = EXCP_DEBUG;            cpu_loop_exit();        }    }     tb_find_fast(); // 查詢 TB 是否已存在 code cache。若無,則呼叫 tb_find_slow     // 執行 TB,也就是 tc_ptr 所指到的位址。注意,產生 TCG IR 的過程中,在 block 的最後會是    // exit_tb addr,此 addr 是正在執行的這個 block 起始位址,同時也是 tcg_qemu_tb_exec 的回傳值。     // 該位址後兩位會被填入 0、1 或 2 以指示 block chaining 的方向。    next_tb = tcg_qemu_tb_exec(tc_ptr);}                    

    • tb_find_fast 會以 pc (虛擬位址)試圖尋找已翻譯過的 translation block。

      tb = env->tb_jmp_cache[tb_jmp_cache_hash_func(pc)];                            

      如果失敗,則呼叫 tb_find_slow 以 pc (cs + eip) 對映的物理位址尋找 TB。如果成功,則將該 TB 寫入 tb_jmp_cache; 若否,則進行翻譯。

      not_found:  /* if no translated code available, then translate it now */  tb = tb_gen_code(env, pc, cs_base, flags, 0); found:  /* we add the TB in the virtual pc hash table */  env->tb_jmp_cache[tb_jmp_cache_hash_func(pc)] = tb;  return tb;                            

  2. tb_gen_code 配置內存給 TB,再交由 cpu_gen_code。

                            // 注意! 這裡會將虛擬位址轉成物理位址。phys_pc 將交給之後的 tb_link_page 使用。// get_page_addr_code 在 process mode 直接返回 pc; system mode 則透過查找// env 的 tlb_table 返回 GPA 在客戶機內存中的偏移量。phys_pc = get_page_addr_code(env, pc);tb = tb_alloc(pc);if (!tb) {  // 清空 code cache} // 初始 tb cpu_gen_code(env, tb, &code_gen_size); // 開始 guest binary -> TCG IR -> host binary 的翻譯。 // 將 tb 加入 tb_phys_hash 和二級頁表 l1_map。// phys_pc 和 phys_page2 分別代表 tb (guest pc) 對映的物理位址和所屬的第二個頁面 (如果 tb 代表的 guest binary 跨頁面的話)。tb_link_page(tb, phys_pc, phys_page2);return tb;                    

    • cpu_gen_code 負責 guest binary → TCG IR → host binary 的翻譯。

      tcg_func_start(s); // 初始 gen_opc_ptr 和 gen_opparam_ptr gen_intermediate_code(env, tb); // 呼叫 gen_intermediate_code_internal 產生 TCG IR gen_code_size = tcg_gen_code(s, gen_code_buf); // TCG IR -> host binary                            

      • tcg_func_start (tcg/tcg.c)

                                                void tcg_func_start(TCGContext *s){    int i;    tcg_pool_reset(s);    s->nb_temps = s->nb_globals;    for(i = 0; i < (TCG_TYPE_COUNT * 2); i++)        s->first_free_temp[i] = -1;    s->labels = tcg_malloc(sizeof(TCGLabel) * TCG_MAX_LABELS);    s->nb_labels = 0;    s->current_frame_offset = s->frame_start;     gen_opc_ptr = gen_opc_buf;    gen_opparam_ptr = gen_opparam_buf;}                                    

    • gen_intermediate_code_internal (target-*/translate.c) 初始化並呼叫 disas_insn 反組譯 guest binary 成 TCG IR。

      • disas_insn 呼叫 tcg_gen_xxx (tcg/tcg-op.h) 產生 TCG IR。分別將 opcode 寫入 gen_opc_ptr 指向的緩衝區 (translate-all.c 裡的 gen_opc_buf); operand 寫入 gen_opparam_ptr 指向的緩衝區 (translate-all.c 裡的 gen_opparam_buf )。

    • tcg_gen_code (tcg/tcg.c) 呼叫 tcg_gen_code_common (tcg/tcg.c) 將 TCG IR 轉成 host binary。

      tcg_reg_alloc_start(s); s->code_buf = gen_code_buf;s->code_ptr = gen_code_buf; // host binary 會寫入 TCGContext s 的 code_ptr 所指向的緩衝區。                            

    • tb_link_page (exec.c) 把新的 TB 加進 tb_phys_hash 和 l1_map 二級頁表。tb_find_slow 會用 pc 對映的物理位址的哈希值索引 tb_phys_hash (存放 TranslationBlock *) 取得下一個 TB。

                                      /* add in the physical hash table */h = tb_phys_hash_func(phys_pc);ptb = &tb_phys_hash[h];tb->phys_hash_next = *ptb; // 如果兩個以上的 TB 其 phys_pc 的哈希值相同,則做 chaining。*ptb = tb; // 新加入的 TB 放至 chaining 的開頭。 /* add in the page list */tb_alloc_page(tb, 0, phys_pc & TARGET_PAGE_MASK);if (phys_page2 != -1) // TB 對應的 guest binary 跨頁    tb_alloc_page(tb, 1, phys_page2);else    tb->page_addr[1] = -1; // jmp_first 代表跳至此 TB 的其它 TB 中的頭一個。jmp_first 初值為自己,末兩位為 10 (2)。// 將來做 block chaining 時,jmp_first 指向跳至此 TB 的其它 TB 中的頭一個 tb1,末兩位為 00// 或 01,這代表從 tb1 的哪一個分支跳至此 TB。tb->jmp_first = (TranslationBlock *)((long)tb | 2);// jmp_next[n] 代表此 TB 條件分支的目標 TB。// 注意! 如果目標 TB,tb1,孤身一人,jmp_next 就真的指向 tb1。// 如果其它 TB,tb2,跳至 tb1,則賦值給 tb->jmp_next 的是 tb1 的 jmp_first,也就是 tb1 (末兩位編碼 tb2 跳至 tb1 的方向)。tb->jmp_next[0] = NULL;tb->jmp_next[1] = NULL; // tb_next_offset 代表此 TB 在 code cache 中分支跳轉要被 patch 的位址 (相對於其 code cache 的偏移量),// 為了 direct block chaining 之用。if (tb->tb_next_offset[0] != 0xffff)    tb_reset_jump(tb, 0);if (tb->tb_next_offset[1] != 0xffff)    tb_reset_jump(tb, 1);                            

      • tb_alloc_page 建立新的 PageDesc。(exec.c)。

                                                static inline void tb_alloc_page(TranslationBlock *tb,                                 unsigned int n, tb_page_addr_t page_addr){  // 代表 tb (guest binary) 所屬頁面。  tb->page_addr[n] = page_addr;  // 在 l1_map 中配置一個 PageDesc,返回該 PageDesc。  p = page_find_alloc(page_addr >> TARGET_PAGE_BITS, 1);  tb->page_next[n] = p->first_tb; // 將該頁面目前第一個 TB 串接到此 TB。將來有需要將某頁面所屬所有 TB 清空。  // n 為 1 代表 tb 對應的 guest binary 跨 page。  p->first_tb = (TranslationBlock *)((long)tb | n);  invalidate_page_bitmap(p);}                                    

      • tb_reset_jump 呼叫 tb_set_jmp_target 使得該 TB 不會鏈結到其它 TB。根據是否使用 direct jump 做 block chaining 與否,tb_set_jmp_target 直接修改 TB (code cache) 跳躍目標或是 TB 的 tb_next。

  3. tb_find_fast/slow 傳回 translation block 後,交給 tcg_qemu_tb_exec 執行。

    next_tb = tcg_qemu_tb_exec(tc_ptr);                    

    • tcg_qemu_tb_exec 被定義在 tcg/tcg.h。

                                      // code_gen_prologue(tb_ptr) is casted to a function with one parameter,// in such a way, execute host machine code stored in code_gen_prologue[]#define tcg_qemu_tb_exec(tb_ptr) ((long REGPARM (*)(void *))code_gen_prologue)(tb_ptr)                            

      (long REGPARM (*)(void *)) 將 code_gen_prologue 轉型成函式指針,void * 為該函式的參數,返回值為 long。REGPARM 指示 GCC 此函式透過暫存器而非棧傳遞參數。至此,(long REGPARM (*)(void *)) 將數組指針 code_gen_prologue 轉型成函式指針。tb_ptr 為該函式指針的參數。綜合以上所述,code_gen_prologue 被視為一函式,其參數為 tb_ptr,返回下一個欲執行的 TB。code_gen_prologue 所做的事為一般函式呼叫前的 prologue,之後將控制交由 tc_ptr 指向的 host binary 並開始執行。

有幾種情況會需要打斷 code cache 的執行,將控制權將還給 QEMU。

  • cpu_interrupt: 虛擬外設會發出中斷,最後透過 apic_local_deliver (hw/apic.c) 呼叫 cpu_interrupt 發出中斷給虛擬 CPU。QEMU 0.15 將system mode 和 process mode 的 cpu_interrupt 分離,請見 cpu-all.h 和 exec.c。

  • cpu_exit: system mode 會註冊 host alarm signal handler,該 handler 會呼叫 cpu_exit。其它諸如 gdb stub,DMA,IO thread 都會呼叫到 cpu_exit。

針對 self-modifying code 或是 JIT,在內存生成代碼之後通常需要清空快取22)

Block Chaining

請閱讀一下文章

為了盡量在 code cache 中執行,QEMU 會做 block chaining。block chaining 是將 code cache 中的 translation block 串接起來。有兩種做法: 第一,採用 direct jump。此法直接修改 code cache 中分支指令的跳躍目標,因此依據 host 有不同的 patch 方式。第二,則是透過修改 TB 的 tb_next 欄位達成 block chaining。exec-all.h 中定義那些 host 可以使用 direct jump。

tb = tb_find_fast(env); if (tb_invalidated_flag) {    /* as some TB could have been invalidated because of memory exceptions while generating the code, we must recompute the hash index here */    next_tb = 0; // 注意! next_tb 也被用來控制是否要做 block chaining。    tb_invalidated_flag = 0;} // 注意!! next_tb 的名字會讓人誤解。block chaining 的方向為: next_tb -> tb。// next_tb 不為 NULL 且 tb (guest binary) 不跨頁面的話,做 block chaining。if (next_tb != 0 && tb->page_addr[1] == -1) {    // 這邊利用 TranlationBlock 指針的最低有效位後兩位指引 block chaining 的方向。    tb_add_jump((TranslationBlock *)(next_tb & ~3), next_tb & 3, tb);}   // 執行 TB,也就是 tc_ptr 所指到的位址。注意,產生 TCG IR 的過程中,在 block 的最後會是  // exit_tb addr,此 addr 是正在執行的這個 block 起始位址,同時也是 tcg_qemu_tb_exec 的回傳值。  // 該位址後兩位會被填入 0、1 或 2 以指示 block chaining 的方向。  next_tb = tcg_qemu_tb_exec(env, tc_ptr);        
  • tb_add_jump 呼叫 tb_set_jmp_target 做 block chaining 的 patch。另外,會利用 tb 的 jmp_next 和 tb_next 的 jmp_first 把 block chaining 中的 tb 串成一個 circular list。這邊要注意到,QEMU 利用 TranslatonBlock 位址後兩位必為零的結果做了一些手腳。(exec-all.h)。

                            // block chaining 方向為: tb -> tb_next。n 用來指示 tb 條件分支的方向。static inline void tb_add_jump(TranslationBlock *tb, int n,                               TranslationBlock *tb_next){    // jmp_next[0]/jmp_next[1] 代表 tb 條件分支的目標。    if (!tb->jmp_next[n]) {        /* patch the native jump address */        tb_set_jmp_target(tb, n, (unsigned long)tb_next->tc_ptr);         // tb_jmp_remove 會用到 jmp_next 做 unchain。         // tb_next->jmp_first 初值為自己,末兩位設為 10 (2)。         // 如果已有其它 TB,tb1,跳至 tb_next,則 tb->jmp_next 指向 tb1 (末兩位代表 tb1 跳至 tb_next 的方向)。         // tb_next->jmp_first 改指向 tb。         tb->jmp_next[n] = tb_next->jmp_first;        // tb_next 的 jmp_first 指回 tb,末兩位代表由 tb 哪一個條件分支跳至 tb_next。         tb_next->jmp_first = (TranslationBlock *)((long)(tb) | (n));    }}                    

  • 依據是否採用 direct jump,tb_set_jmp_target (exec-all.h) 有不同做法。採用 direct jump 的話,tb_set_jmp_target 會根據 host 呼叫不同的 tb_set_jmp_target1。tb_set_jmp_target1 會用到 TB 的 tb_jmp_offset。如果不採用 direct jump 做 block chaining,tb_set_jmp_target 會直接修改 TB 的 tb_next。

    • tb_set_jmp_target (exec-all.h)。

                                      static inline void tb_set_jmp_target(TranslationBlock *tb,                                     int n, unsigned long addr){    unsigned long offset;     offset = tb->tb_jmp_offset[n]; // tb 要 patch 的位址相對於 tb->tc_ptr 的偏移量。    tb_set_jmp_target1((unsigned long)(tb->tc_ptr + offset), addr);}                            

    • tb_set_jmp_target1 (exec-all.h)。

                                      #elif defined(__i386__) || defined(__x86_64__)static inline void tb_set_jmp_target1(unsigned long jmp_addr, unsigned long addr){    /* patch the branch destination */    *(uint32_t *)jmp_addr = addr - (jmp_addr + 4); // jmp 的參數為 jmp 下一條指令與目標地址的偏移量。    /* no need to flush icache explicitly */}                            

由 guest binary → TCG IR 的過程中,gen_goto_tb 會做 block chaining 的準備。請見 Porting QEMU to Plan 9: QEMU Internals and Port Strategy 2.2.3 和 2.2.4 節。以 i386 為例: (target-i386/translate.c):

            // tb_num 代表目前 tb block linking 分支情況。eip 代表跳轉目標。static inline void gen_goto_tb(DisasContext *s, int tb_num, target_ulong eip){    TranslationBlock *tb;    target_ulong pc;     // s->pc 代表翻譯至目前 guest binary 的所在位址。tb->pc 表示 guest binary 的起始位址。    // 注意! 這裡 s->cs_base + eip 代表跳轉位址; s->pc 代表目前翻譯到的 guest pc。見 target-i386/translate.c 中的 case 0xe8。    pc = s->cs_base + eip; // 計算跳轉目標的 pc    tb = s->tb; // 目前 tb    /* NOTE: we handle the case where the TB spans two pages here */    // http://lists.nongnu.org/archive/html/qemu-devel/2011-08/msg02249.html    // 滿足底下兩個條件之一,則可以做 direct block linking    // 第一,跳轉目標和目前 tb 起始的 pc 同屬一個 guest page。    // 第二,跳轉目標和目前翻譯到的 pc 同屬一個 guest page。    if ((pc & TARGET_PAGE_MASK) == (tb->pc & TARGET_PAGE_MASK) ||        (pc & TARGET_PAGE_MASK) == ((s->pc - 1) & TARGET_PAGE_MASK))  {        /* jump to same page: we can use a direct jump */        // 如果 guest jump 指令和其跳轉位址同屬一個 guest page,則做 direct block linking。        tcg_gen_goto_tb(tb_num); // 生成準備做 block linking 的 TCG IR。詳情請見之後描述。        // 更新 env 的 eip 使其指向此 block 之後欲執行指令的位址。        // tb_find_fast 會用 eip 查找該 TB 是否已被翻譯過。       gen_jmp_im(eip);       // 最終回到 QEMU tcg_qemu_tb_exec,賦值給 next_tb。        // 注意! tb_num 會被 next_tb & 3 取出,由此可以得知 block chaining 的方向。        tcg_gen_exit_tb((tcg_target_long)tb + tb_num);    } else {        /* jump to another page: currently not optimized */        gen_jmp_im(eip);        gen_eob(s);    }}        
  • tcg_gen_goto_tb 生成 TCG IR。

                            static inline void tcg_gen_goto_tb(int idx){    tcg_gen_op1i(INDEX_op_goto_tb, idx);}                    

  • tcg/i386/tcg-target.c 將 TCG IR 翻成 host binary。注意! 這邊利用 patch jmp 跳轉位址達成 block linking。

                            static inline void tcg_out_op(TCGContext *s, TCGOpcode opc,                              const TCGArg *args, const int *const_args){    case INDEX_op_goto_tb:        if (s->tb_jmp_offset) {            /* direct jump method */            tcg_out8(s, OPC_JMP_long); /* jmp im */            // 紀錄將來要 patch 的地方。              s->tb_jmp_offset[args[0]] = s->code_ptr - s->code_buf;            // jmp 的參數為 jmp 下一個指令與目標的偏移量。              // 如果還沒做 block chaining,則 jmp 0 代表 fall through。              tcg_out32(s, 0);        } else {            /* indirect jump method */            tcg_out_modrm_offset(s, OPC_GRP5, EXT5_JMPN_Ev, -1,                                 (tcg_target_long)(s->tb_next + args[0]));        }        s->tb_next_offset[args[0]] = s->code_ptr - s->code_buf;        break;                    

  • 遇到 guest binary 中的條件分支和直接跳轉都會呼叫 gen_goto_tb (target-i386/translate.c)。以 i386 直接跳轉為例:

                            static void gen_jmp_tb(DisasContext *s, target_ulong eip, int tb_num){    if (s->jmp_opt) { // 使用 direct jump 實現 block chaining        gen_update_cc_op(s);        gen_goto_tb(s, tb_num, eip); // tb_num 指示目前 tb 分支方向,eip 是下一個 tb 位址。        s->is_jmp = DISAS_TB_JUMP;    } else {        gen_jmp_im(eip);        gen_eob(s);    }} static void gen_jmp(DisasContext *s, target_ulong eip){    // 注意! DisasContext 的 pc 值是 eip + cs_base。    // 直接跳轉至 eip。    gen_jmp_tb(s, eip, 0);}                    

請見,

當 TLB (QEMU 替每個 env 維護的 software TLB,負責 GVA → HVA 的轉換) 不命中,會尋訪 guest page table (GVA → GPA),再由 tlb_set_page 將 guest page table entry 帶入 TLB,這裡會將 GPA 換成 HVA 以便之後能做 GVA → HVA 的轉換。當 TLB 的內容經由 tlb_set_page 改變時,代表原本 GVA → HVA 的對映會失效,這代表 guest program 的某個 guest physical page 被 swap out,或是 guest OS 做 task switching。

注意! QEMU 是將 guest binary (位於 guest physical page) 翻譯成 host binary 並執行。因此,當某個 guest physical page 被 swap out,或是 guest OS 做 task switching,則屬於該 guest physical page 的所有 TB 皆屬失效,不應該被執行。QEMU 一次會清空一個 guest physical page 的所有 TB。

假設我們要連結 tb1 和 tb2,也就是 tb1 → tb2。只有在 tb2 (對應的 guest binary) 起始位址和 tb1 (對應的 guest binary) 起始位址或尾巴落在同一個 guest page 才能做 direct block linking。這是因為當 TLB (QEMU 替每個 env 維護的 software TLB) 的內容經由 tlb_set_page 改變時,原本 guest page 內的 TB 必須被沖掉。

如果 block linking 沒有上述限制,則會執行到不該執行的 host binary。

假設跨 guest page 的 tb1 和 tb2 之間沒有 direct block chaining,亦即 tb1 和 tb2 中間會回到 QEMU。QEMU 就可以透過 tb_find_fast → tb_find_slow → get_page_addr_code 查找 TLB 並發出 guest exception。

移除該限制,運行 linux-0.11。登入後,下 bash 會出問題。

            static inline void gen_goto_tb(DisasContext *s, int tb_num, target_ulong eip){    TranslationBlock *tb;    target_ulong pc;     pc = s->cs_base + eip;    tb = s->tb;#if 0    /* NOTE: we handle the case where the TB spans two pages here */    if ((pc & TARGET_PAGE_MASK) == (tb->pc & TARGET_PAGE_MASK) ||        (pc & TARGET_PAGE_MASK) == ((s->pc - 1) & TARGET_PAGE_MASK))  {        /* jump to same page: we can use a direct jump */#endif        tcg_gen_goto_tb(tb_num);        gen_jmp_im(eip);        tcg_gen_exit_tb((long)tb + tb_num);#if 0    } else {        /* jump to another page: currently not optimized */        gen_jmp_im(eip);        gen_eob(s);    }#endif}        
$ git clone git://jcmvbkbc.spb.ru/dumb/qemu-test-kernel.git$ cd qemu-test-kernel$ ./autogen.sh; configure; make        

Block Unchaining

底下幾種情況會做 block unchaining。請見以下討論,

  1. cpu_exit (exec.c) → cpu_unlink_tb(exec.c)。當某些情況需要 QEMU 從 code cache 中跳離出來處理某些事情時,會呼叫到 cpu_exit。例如: Host SIGALRM, DMA, IO Thread, Single step。之所以要將 tb unlink,是不希望一直在 code cache 中執行,需要早點離開 code cache 處理事情。

                            void cpu_exit(CPUState *env){    env->exit_request = 1; // 拉起 env->exit_request。    cpu_unlink_tb(env); // 將 env 的 code cache 中的 tb unlink。}                    

    • cpu_signal。IOThread 完成 IO 之後,透過 cpu_signal 通知 QEMU。

                                      #ifdef CONFIG_IOTHREADstatic void cpu_signal(int sig){    if (cpu_single_env) { // 某些情況下會把 env 備份到 cpu_single_env        cpu_exit(cpu_single_env);    }    exit_request = 1; // 拉起 exit_request,之後會在 cpu_exec (cpu-exec.c) 中把 env->exit_request 拉起。}#endif                            

    • 在 cpu_exec (cpu-exec.c) 中會檢查 env→exit_request。

                                      if (unlikely(env->exit_request)) {    env->exit_request = 0;    env->exception_index = EXCP_INTERRUPT; // 設置 exception_index。longjmp 回 cpu_exec 開頭後會檢查 exception_index。    cpu_loop_exit(); // 將 env->current_tb 設為 NULL,longjmp 回 cpu_exec 開頭。}                            

  2. cpu_interrupt (exec.c) → cpu_unlink_tb (exec.c)。虛擬外設透過 cpu_interrupt (exec.c) 發出中斷。QEMU 0.15 改叫 tcg_handle_interrupt。

                            void cpu_interrupt(CPUState *env, int mask){    // 設置 interrupt_request。cpu_exec 會在內層迴圈處理 interrupt_request。     // 那時會設置 env->exception_index,並呼叫 cpu_loop_exit,longjmp 回 cpu_exec 開頭。     env->interrupt_request |= mask;    cpu_unlink_tb(env);}                    

    • cpu_unlink_tb (exec.c) → tb_reset_jump_recursive。

                                      static void cpu_unlink_tb(CPUState *env){    /* FIXME: TB unchaining isn't SMP safe. For now just ignore the problem and hope the cpu will stop of its own accord. For userspace emulation this often isn't actually as bad as it sounds. Often signals are used primarily to interrupt blocking syscalls. */    TranslationBlock *tb;    static spinlock_t interrupt_lock = SPIN_LOCK_UNLOCKED;     spin_lock(&interrupt_lock);    tb = env->current_tb; // current_tb 代表 env 目前正在執行的 tb    /* if the cpu is currently executing code, we must unlink it and all the potentially executing TB */    if (tb) {        env->current_tb = NULL;        tb_reset_jump_recursive(tb); // 將 tb 的 block chaining 打斷    }    spin_unlock(&interrupt_lock);}                            

    • tb_reset_jump_recursive 呼叫 tb_reset_jump_recursive2 打斷 tb 的 block chaining。

                                      static void tb_reset_jump_recursive(TranslationBlock *tb){    tb_reset_jump_recursive2(tb, 0);    tb_reset_jump_recursive2(tb, 1);}                            

    • tb_reset_jump_recursive2 清除 jmp_first list (TranslationBlock),再呼叫 tb_reset_jump 和 tb_reset_jump_recursive 重設 code cache 中的 jmp address,打斷 block chaining。

                                      static inline void tb_reset_jump_recursive2(TranslationBlock *tb, int n){    TranslationBlock *tb1, *tb_next, **ptb;    unsigned int n1;     tb1 = tb->jmp_next[n]; // tb -> tb1。但有可能有其它 TB,tb2, 跳至 tb1。此時,tb->jmp_next 其值為 tb2。    if (tb1 != NULL) {        /* find head of list */        for(;;) {            n1 = (long)tb1 & 3;            tb1 = (TranslationBlock *)((long)tb1 & ~3);            if (n1 == 2) // 代表 tb 是唯一跳至 tb1 的 TB。                break;            tb1 = tb1->jmp_next[n1]; // 代表有其它跳至 tb1 的 TB。繼續尋訪該串列。        }        /* we are now sure now that tb jumps to tb1 */        tb_next = tb1; // 確定是 tb -> tb1。         /* remove tb from the jmp_first list */        ptb = &tb_next->jmp_first; // jmp_first 指向跳至 tb_next 的所有 TB。        for(;;) {            tb1 = *ptb;            n1 = (long)tb1 & 3;            tb1 = (TranslationBlock *)((long)tb1 & ~3);            if (n1 == n && tb1 == tb) // 在 jmp_first -> jmp_next 構成的串列中找到 tb                break;            ptb = &tb1->jmp_next[n1]; // 繼續在 jmp_first -> jmp_next 構成的串列中找尋 tb        }        *ptb = tb->jmp_next[n]; // 將 tb_next 的 jmp_first 的串列改以下一個 TB 為開頭         tb->jmp_next[n] = NULL;         /* suppress the jump to next tb in generated code */        tb_reset_jump(tb, n);         /* suppress jumps in the tb on which we could have jumped */        tb_reset_jump_recursive(tb_next);    }}                            

    •                                 /* reset the jump entry 'n' of a TB so that it is not chained to another TB */static inline void tb_reset_jump(TranslationBlock *tb, int n){    tb_set_jmp_target(tb, n, (unsigned long)(tb->tc_ptr + tb->tb_next_offset[n]));}                            

  3. tb_phys_invalidate 會呼叫 tb_jmp_remove 做 unchain。

    • tb_phys_invalidate。

                                      void tb_phys_invalidate(TranslationBlock *tb, tb_page_addr_t page_addr){    // 將該 tb 從 tb_phys_hash 中移除     phys_pc = tb->page_addr[0] + (tb->pc & ~TARGET_PAGE_MASK); // virtual addr 中 page offset 的部分和 physical addr 一樣     h = tb_phys_hash_func(phys_pc);    tb_remove(&tb_phys_hash[h], tb,              offsetof(TranslationBlock, phys_hash_next));     // 將 tb 從相應的 PageDesc 中移除    if (tb->page_addr[0] != page_addr) {        p = page_find(tb->page_addr[0] >> TARGET_PAGE_BITS);        tb_page_remove(&p->first_tb, tb);        invalidate_page_bitmap(p);    }    if (tb->page_addr[1] != -1 && tb->page_addr[1] != page_addr) {        p = page_find(tb->page_addr[1] >> TARGET_PAGE_BITS);        tb_page_remove(&p->first_tb, tb);        invalidate_page_bitmap(p);    }     tb_invalidated_flag = 1;     // 將 tb 從 tb_jmp_cache 移除     h = tb_jmp_cache_hash_func(tb->pc);    // 因為每一個 env 都有一份自己的 tb_jmp_cache,全部清除。    for(env = first_cpu; env != NULL; env = env->next_cpu) {        if (env->tb_jmp_cache[h] == tb)            env->tb_jmp_cache[h] = NULL;    }     // 處理 tb1 (tb -> tb1)    tb_jmp_remove(tb, 0);    tb_jmp_remove(tb, 1);     // 處理 tb1 (tb1 -> tb)    tb1 = tb->jmp_first;    for(;;) {        n1 = (long)tb1 & 3;        if (n1 == 2) // tb1 末兩位如果為 10 (2),代表 tb1 沒有跳至其它 tb            break;        tb1 = (TranslationBlock *)((long)tb1 & ~3); // 還原回原本的 tb1        tb2 = tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2)        tb_reset_jump(tb1, n1); // 將 tb1 至其它的 tb 的 block chaining 打斷 (code cache)        tb1->jmp_next[n1] = NULL;        tb1 = tb2;    }    tb->jmp_first = (TranslationBlock *)((long)tb | 2); // 將 jmp_first 再次指向自己}                            

    • tb_jmp_remove 將該 tb (TranslationBlock) 移出 circular list。

                                      static inline void tb_jmp_remove(TranslationBlock *tb, int n){    ptb = &tb->jmp_next[n]; // n (0 或 1) 指示 tb 下一個 block chaining 的方向     tb1 = *ptb; // 處理 tb1 (tb -> tb1)    if (tb1) {        /* find tb(n) in circular list */        for(;;) {            tb1 = *ptb;            n1 = (long)tb1 & 3; // 取出 tb1 末兩位              tb1 = (TranslationBlock *)((long)tb1 & ~3); 還原回原本的 tb1            if (n1 == n && tb1 == tb) // 代表 tb 沒有跳至其它 tb                break;            if (n1 == 2) {                ptb = &tb1->jmp_first; // 代表沒有其它 tb 跳至 tb1            } else {                ptb = &tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2)            }        }        /* now we can suppress tb(n) from the list */        *ptb = tb->jmp_next[n];         tb->jmp_next[n] = NULL;    }}                            

Self-modifying Code

以 Linux 上的 self modifying code 為例 (請見 Self-modifying code),主要是利用 mprotect 修改頁面權限,修改其中代碼。QEMU 從兩個方向偵測 self-modifying code。

  1. 在呼叫 do_syscall 執行 mprotect 系統呼叫時進行相應的檢查。

    • main → cpu_loop → do_syscall → target_mprotect → page_set_flags (如果遇到 SMC,把相應的 tb 沖掉) → tb_invalidate_phys_page (只有 process mode 有定義此函式) → tb_phys_invalidate

                                      /* Modify the flags of a page and invalidate the code if necessary. The flag PAGE_WRITE_ORG is positioned automatically depending on PAGE_WRITE. The mmap_lock should already be held. */void page_set_flags(target_ulong start, target_ulong end, int flags){    for (addr = start, len = end - start;         len != 0;         len -= TARGET_PAGE_SIZE, addr += TARGET_PAGE_SIZE) {        // 反查該 guest pc 對映的頁面。         PageDesc *p = page_find_alloc(addr >> TARGET_PAGE_BITS, 1);         /* If the write protection bit is set, then we invalidate the code inside. */        if (!(p->flags & PAGE_WRITE) &&            (flags & PAGE_WRITE) &&            p->first_tb) {            tb_invalidate_phys_page(addr, 0, NULL);        }        p->flags = flags;    }}                            

  2. tb_invalidate_phys_page_fast (exec.c) 會先檢查客戶機頁面是否有 bitmap。

                            static inline void tb_invalidate_phys_page_fast(tb_page_addr_t start, int len){    PageDesc *p;    int offset, b;     p = page_find(start >> TARGET_PAGE_BITS);    if (!p)        return;    if (p->code_bitmap) {        offset = start & ~TARGET_PAGE_MASK;        b = p->code_bitmap[offset >> 3] >> (offset & 7);        if (b & ((1 << len) - 1))            goto do_invalidate;    } else {    do_invalidate:        tb_invalidate_phys_page_range(start, start + len, 1);    }}                    

  3. tb_invalidate_phys_page (exec.c)。

                            #if !defined(CONFIG_SOFTMMU)static void tb_invalidate_phys_page(tb_page_addr_t addr,                                    unsigned long pc, void *puc){    addr &= TARGET_PAGE_MASK;    p = page_find(addr >> TARGET_PAGE_BITS);    // 取得該 page 的第一個 tb。     // tb 末兩位如果是 01 (1),代表 tb 對應的 guest bianry 跨 page。     tb = p->first_tb;     while (tb != NULL) {        n = (long)tb & 3; // 取得 block chaing 的方向         tb = (TranslationBlock *)((long)tb & ~3); // 去掉末兩位的編碼,還原回真正的 tb        tb_phys_invalidate(tb, addr);        tb = tb->page_next[n]; // 取得 tb 所屬 page (或下一個 page) 的下一個 tb    }    p->first_tb = NULL; }                    

    • 最終會呼叫到 tb_phys_invalidate。

                                      void tb_phys_invalidate(TranslationBlock *tb, tb_page_addr_t page_addr){    // 將該 tb 從 tb_phys_hash 中移除     phys_pc = tb->page_addr[0] + (tb->pc & ~TARGET_PAGE_MASK); // virtual addr 中 page offset 的部分和 physical addr 一樣     h = tb_phys_hash_func(phys_pc);    tb_remove(&tb_phys_hash[h], tb,              offsetof(TranslationBlock, phys_hash_next));     // 將 tb 從相應的 PageDesc 中移除    if (tb->page_addr[0] != page_addr) {        p = page_find(tb->page_addr[0] >> TARGET_PAGE_BITS);        tb_page_remove(&p->first_tb, tb);        invalidate_page_bitmap(p);    }    if (tb->page_addr[1] != -1 && tb->page_addr[1] != page_addr) {        p = page_find(tb->page_addr[1] >> TARGET_PAGE_BITS);        tb_page_remove(&p->first_tb, tb);        invalidate_page_bitmap(p);    }     tb_invalidated_flag = 1;     // 將 tb 從 tb_jmp_cache 移除     h = tb_jmp_cache_hash_func(tb->pc);    // 因為每一個 env 都有一份自己的 tb_jmp_cache,全部清除。    for(env = first_cpu; env != NULL; env = env->next_cpu) {        if (env->tb_jmp_cache[h] == tb)            env->tb_jmp_cache[h] = NULL;    }     // 處理 tb1 (tb -> tb1)    tb_jmp_remove(tb, 0);    tb_jmp_remove(tb, 1);     // 處理 tb1 (tb1 -> tb)    tb1 = tb->jmp_first;    for(;;) {        n1 = (long)tb1 & 3;        if (n1 == 2) // tb1 末兩位如果為 10 (2),代表 tb1 沒有跳至其它 tb            break;        tb1 = (TranslationBlock *)((long)tb1 & ~3); // 還原回原本的 tb1        tb2 = tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2)        tb_reset_jump(tb1, n1); // 將 tb1 至其它的 tb 的 block chaining 打斷 (code cache)        tb1->jmp_next[n1] = NULL;        tb1 = tb2;    }    tb->jmp_first = (TranslationBlock *)((long)tb | 2); // 將 jmp_first 再次指向自己}                            

    • tb_jmp_remove 將該 tb 移出 circular lists‎。

                                      static inline void tb_jmp_remove(TranslationBlock *tb, int n){    ptb = &tb->jmp_next[n]; // n (0 或 1) 指示 tb 下一個 block chaining 的方向     tb1 = *ptb; // 處理 tb1 (tb -> tb1)    if (tb1) {        /* find tb(n) in circular list */        for(;;) {            tb1 = *ptb;            n1 = (long)tb1 & 3; // 取出 tb1 末兩位              tb1 = (TranslationBlock *)((long)tb1 & ~3); 還原回原本的 tb1            if (n1 == n && tb1 == tb) // 代表 tb 沒有跳至其它 tb                break;            if (n1 == 2) {                ptb = &tb1->jmp_first; // 代表沒有其它 tb 跳至 tb1            } else {                ptb = &tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2)            }        }        /* now we can suppress tb(n) from the list */        *ptb = tb->jmp_next[n];         tb->jmp_next[n] = NULL;    }}                            

  4. 當宿主機發出 SIGSEGV 給 QEMU 時,QEMU 會檢視該 signal 並做相應處理。

    • host_signal_handler (linux-user/signal.c) → cpu_x86_signal_handler (user-exec.c) → handle_cpu_signal (user-exec.c) → page_unprotect (exec.c) → tb_invalidate_phys_page (exec.c)

                                      static inline int handle_cpu_signal(unsigned long pc, unsigned long address, ...){    if (is_write && page_unprotect(h2g(address), pc, puc)) {        return 1;    }}                            

    • page_unprotect 將該 guest page 相應的 tb 清掉,將該內存區段設成可寫。

                                      int page_unprotect(target_ulong address, unsigned long pc, void *puc){    p = page_find(address >> TARGET_PAGE_BITS);     /* if the page was really writable, then we change its protection back to writable */    if ((p->flags & PAGE_WRITE_ORG) && !(p->flags & PAGE_WRITE)) {        host_start = address & qemu_host_page_mask;        host_end = host_start + qemu_host_page_size;         prot = 0;        for (addr = host_start ; addr < host_end ; addr += TARGET_PAGE_SIZE) {            p = page_find(addr >> TARGET_PAGE_BITS);            p->flags |= PAGE_WRITE;            prot |= p->flags;             /* and since the content will be modified, we must invalidate the corresponding translated code. */            tb_invalidate_phys_page(addr, pc, puc);        }        mprotect((void *)g2h(host_start), qemu_host_page_size,                 prot & PAGE_BITS);         return 1;    }}                            

      使用 GDB 的時候,會出現 host 發出的 SIGSEGV。

      PAGE(0x8048000): cp <0x804854a>[0..57] <0x80485e1> Program received signal SIGSEGV, Segmentation fault.0x00000000602296ac in static_code_gen_buffer ()(gdb) cContinuing.Hello :-)No endless loop here! Program exited with code 052.

如果看 QEMU in_asm 的 log,會發現 injectHere 所在的區段被翻譯過兩次。如果 TARGET_HAS_PRECISE_SMC 被定義,會有額外的處理,這在 Qemu 0.5.4 被加入。x86 上針對 self-modifying code 會自動偵測並處理,無需程序員用特定指令將快取清空。TARGET_HAS_PRECISE_SMC 是針對某客戶機指令修改該指令所在客戶機內存區段的情況。

\_\_stl_mmu → io_writel → notdirty_mem_writel → tb_invalidate_phys_page_fast → tb_invalidate_phys_page_range

  1. \_\_stl_mmu 中存取 IO 的路徑不完全是 MMIO23)

     

  2.                         void tb_invalidate_phys_page_range(tb_page_addr_t start, tb_page_addr_t end,                                   int is_cpu_write_access){    TranslationBlock *tb, *tb_next, *saved_tb;    CPUState *env = cpu_single_env;    tb_page_addr_t tb_start, tb_end;    PageDesc *p;    int n;#ifdef TARGET_HAS_PRECISE_SMC    int current_tb_not_found = is_cpu_write_access;    TranslationBlock *current_tb = NULL;    int current_tb_modified = 0;    target_ulong current_pc = 0;    target_ulong current_cs_base = 0;    int current_flags = 0;#endif /* TARGET_HAS_PRECISE_SMC */    p = page_find(start >> TARGET_PAGE_BITS);    if (!p)        return;    if (!p->code_bitmap &&        ++p->code_write_count >= SMC_BITMAP_USE_THRESHOLD &&        is_cpu_write_access) {        /* build code bitmap */        build_page_bitmap(p);    }     /* we remove all the TBs in the range [start, end[ */    /* XXX: see if in some cases it could be faster to invalidate all the code */    tb = p->first_tb;    while (tb != NULL) {        n = (long)tb & 3;        tb = (TranslationBlock *)((long)tb & ~3);        tb_next = tb->page_next[n];        /* NOTE: this is subtle as a TB may span two physical pages */        if (n == 0) {            /* NOTE: tb_end may be after the end of the page, but it is not a problem */            tb_start = tb->page_addr[0] + (tb->pc & ~TARGET_PAGE_MASK);            tb_end = tb_start + tb->size;        } else {            tb_start = tb->page_addr[1];            tb_end = tb_start + ((tb->pc + tb->size) & ~TARGET_PAGE_MASK);        }        if (!(tb_end <= start || tb_start >= end)) {#ifdef TARGET_HAS_PRECISE_SMC            if (current_tb_not_found) {                current_tb_not_found = 0;                current_tb = NULL;                if (env->mem_io_pc) {                    /* now we have a real cpu fault */                    current_tb = tb_find_pc(env->mem_io_pc);                }            }            if (current_tb == tb &&                (current_tb->cflags & CF_COUNT_MASK) != 1) {                /* If we are modifying the current TB, we must stop its execution. We could be more precise by checking that the modification is after the current PC, but it would require a specialized function to partially restore the CPU state */                 current_tb_modified = 1;                cpu_restore_state(current_tb, env,                                  env->mem_io_pc, NULL);                cpu_get_tb_cpu_state(env, &current_pc, &current_cs_base,                                     &current_flags);            }#endif /* TARGET_HAS_PRECISE_SMC */            /* we need to do that to handle the case where a signal occurs while doing tb_phys_invalidate() */            saved_tb = NULL;            if (env) {                saved_tb = env->current_tb;                env->current_tb = NULL;            }            tb_phys_invalidate(tb, -1);            if (env) {                env->current_tb = saved_tb;                if (env->interrupt_request && env->current_tb)                    cpu_interrupt(env, env->interrupt_request);            }        }        tb = tb_next;    }#if !defined(CONFIG_USER_ONLY)    /* if no code remaining, no need to continue to use slow writes */    if (!p->first_tb) {        invalidate_page_bitmap(p);        if (is_cpu_write_access) {            tlb_unprotect_code_phys(env, start, env->mem_io_vaddr);        }    }#endif#ifdef TARGET_HAS_PRECISE_SMC    if (current_tb_modified) {        /* we generate a block containing just the instruction modifying the memory. It will ensure that it cannot modify itself */        env->current_tb = NULL;        tb_gen_code(env, current_pc, current_cs_base, current_flags, 1);        cpu_resume_from_signal(env, NULL);    }#endif}                    

  • cpu_get_tb_cpu_state (target-i386/cpu.h)。

                            static inline void cpu_get_tb_cpu_state(CPUState *env, target_ulong *pc,                                        target_ulong *cs_base, int *flags){    *cs_base = env->segs[R_CS].base;    *pc = *cs_base + env->eip;    *flags = env->hflags |        (env->eflags & (IOPL_MASK | TF_MASK | RF_MASK | VM_MASK));}                    

  • mem_io_pc (cpu-defs.h)。

                            static inline DATA_TYPE glue(io_read, SUFFIX)(target_phys_addr_t physaddr,                                              target_ulong addr,                                              void *retaddr){    DATA_TYPE res;    int index;    index = (physaddr >> IO_MEM_SHIFT) & (IO_MEM_NB_ENTRIES - 1);    physaddr = (physaddr & TARGET_PAGE_MASK) + addr;    env->mem_io_pc = (unsigned long)retaddr;    if (index > (IO_MEM_NOTDIRTY >> IO_MEM_SHIFT)            && !can_do_io(env)) {        cpu_io_recompile(env, retaddr);    }     ... 略 ...}                    

TCG IR

TCG IR 分為底下幾類:

  • 暫存器移動: mov, movi

  • 邏輯運算: and, or, xor, shl, shr, …

  • 算術運算: add, sub, mul, div, …

  • 內存操作: qemu_ld, qemu_st。客戶代碼中的內存操作,這裡會透過 mmu (tlb) 做 guest virtual addr 到 guest physical addr 的轉換。Re: [Qemu-devel] When the tlb_fill will be called from generated code?

                            mov %eax, 0x4(%ebx)                    

  • QEMU 內部內存操作: ld, st。QEMU 存取 CPUState 之用。

    movi 0x8000000, 0x20(%r14)          # env->eip = 0x8000000                    

  • 分支指令: jmp, br, brcond

  • Helper function: call。呼叫 helper function。

  • 其它: exit_tb, end。

OP: 右 (源) 至左 (目的)。 OUT_ASM: 左 (源) 至右 (目的)。

QEMU 用 typedef enum TCGOpcode 枚舉所有的 TCG Opcode。可以在 cpu-exec.i 看到宏展開之後的結果,例如: INDEX_op_add_i32。gen_opc_buf 指向存放 TCG opcode 的緩衝區,gen_opparam_buf 指向存放 TCG opcode 所需參數的緩衝區。Opcode end 是用來作為 gen_opc_buf 結尾的標記,Opcode exit_tb 代表 block 結尾,準備跳回 QEMU。

  1. tcg/tcg.c 定義了最基本的函式供 tcg/xxx/tcg-target.c 使用。例如 tcg_out8 是將其參數 (8 bit) 寫入 host binary 緩衝區。

  2. tcg/xxx/tcg-target.c 定義利用 tcg/tcg.c 提供的基本函式客製化自己的 tcg_out_xxx。

  3. 在 TCG 裡提到的 target 都是指宿主機 24)

在 exec.c 的最後。

            #define MMUSUFFIX _cmmu // load code#define GETPC() NULL // tb_find_slow -> get_page_addr_code -> ldub_code -> __ldb_cmmu#define env cpu_single_env#define SOFTMMU_CODE_ACCESS #define SHIFT 0#include "softmmu_template.h" #define SHIFT 1#include "softmmu_template.h" #define SHIFT 2#include "softmmu_template.h" #define SHIFT 3#include "softmmu_template.h" #undef env        

請見 [Qemu-devel] When the tlb_fill will be called from generated code?

  1. softmmu_exec.h: target-*/exec.h 使用 softmmu_exec.h 生成 {ld,st}*_{kernel,user,etc} 函式。

  2. softmmu_template.h: exec.c 和 target-*/op_helper.c 使用 softmmu_template.h 生成 __{ld,st}* 函式。

  1. tcg_gen_exit_tb 呼叫 tcg_gen_op1i 生成 TCG IR,其 op 為 INDEX_op_exit_tb,operand 為 val。

                            static inline void tcg_gen_exit_tb(tcg_target_long val){    // tcg_gen_exit_tb((tcg_target_long)tb + tb_num);    // 將 INDEX_op_exit_tb 寫入 gen_opc_buf; val 寫入 gen_opparam_buf。    tcg_gen_op1i(INDEX_op_exit_tb, val);}                    

  2. tcg/xxx/tcg-target.c 根據 TCG IR 產生對應 host binary。以 i386 為例:

                            static inline void tcg_out_op(TCGContext *s, int opc,                              const TCGArg *args, const int *const_args){    case INDEX_op_exit_tb:        tcg_out_movi(s, TCG_TYPE_I32, TCG_REG_EAX, args[0]); // 將 val 寫進 EAX        // e9 是 jmp 指令,後面的 operand 為相對偏移量,將會加上 eip。          // 總和效果使跳回 code_gen_prologue 中 prologue 以後的位置。          tcg_out8(s, 0xe9); /* jmp tb_ret_addr */        // tb_ret_addr 在 tcg_target_qemu_prologue 初始成指向 code_gen_prologue 中 prologue 以後的位置。         // 生成 host binary 的同時,s->code_ptr 會移向下一個 code buffer 的位址。         tcg_out32(s, tb_ret_addr - s->code_ptr - 4);        break;}                    

    • tcg_out_movi 將 arg 移至 ret 代表的暫存器。

                                      static inline void tcg_out_movi(TCGContext *s, TCGType type,                                int ret, int32_t arg){    if (arg == 0) {        /* xor r0,r0 */        tcg_out_modrm(s, 0x01 | (ARITH_XOR << 3), ret, ret);    } else {        // move arg 至 ret        tcg_out8(s, 0xb8 + ret); // 0xb8 為 move,ret 代表目的暫存器。0xb8 + ret 合成一個 opcode。        tcg_out32(s, arg);    }}                            

    • tcg_out_modrm 是 x86 上對 opcode 的 extension。

                                      static void tcg_out_modrm(TCGContext *s, int opc, int r, int rm){    tcg_out_opc(s, opc, r, rm, 0);    tcg_out8(s, 0xc0 | (LOWREGMASK(r) << 3) | LOWREGMASK(rm));}                            

  1. tcg_gen_goto_tb。

                            static inline void tcg_gen_goto_tb(int idx){    tcg_gen_op1i(INDEX_op_goto_tb, idx);}                    

  1. tcg_gen_code_common 會依據 TCG IR 呼叫不同的函式分配暫存器,tcg_reg_alloc_op (tcg/tcg.c) 是其中之一。

     

  2. 不論 TLB 命中與否,都會呼叫 save_globals (tcg/tcg.c) 將 CPUState 寫回。

                            static void save_globals(TCGContext *s, TCGRegSet allocated_regs){    int i;     for(i = 0; i < s->nb_globals; i++) {        temp_save(s, i, allocated_regs);    }}                    

  3. tcg_out_tlb_load 查詢 guest virtual pc 是否在 TLB 內已有項目。

                            // 用 addrlo_idx 索引 args 得到位址下半部,用 addrlo_idx + 1 索引 args 得到位址上半部。// mem_index 用來索引 CPUState 中的 tlb_table[mem_index]。// s_bits 是欲讀取資料大小以 2 為底的對數。// which 是存取 CPUTLBEntry 其成員的偏移量,應該是 addr_read 或是 addr_write 的偏移。static inline void tcg_out_tlb_load(TCGContext *s, int addrlo_idx,                                    int mem_index, int s_bits,                                    const TCGArg *args,                                    uint8_t **label_ptr, int which){    const int addrlo = args[addrlo_idx];         // 索引 args 得到位址下半部。    const int r0 = tcg_target_call_iarg_regs[0]; // 取得參數傳遞所用的暫存器。    const int r1 = tcg_target_call_iarg_regs[1];     tcg_out_mov(s, type, r1, addrlo); // 分別複製參數 addrlo 至 r0 和 r1    tcg_out_mov(s, type, r0, addrlo);     // 邏輯右移    // TARGET_PAGE_BITS 是 page size 以 2 為底的對數。    // CPU_TLB_ENTRY_BITS 是 CPUTLBEntry 以 2 為底的對數。    tcg_out_shifti(s, SHIFT_SHR + rexw, r1,                   TARGET_PAGE_BITS - CPU_TLB_ENTRY_BITS);     // 取得該位址所在頁,利用 TARGET_PAGE_MASK。    tgen_arithi(s, ARITH_AND + rexw, r0,                TARGET_PAGE_MASK | ((1 << s_bits) - 1), 0);    // CPU_TLB_BITS 是 TLB 大小以 2 為底的對數。    tgen_arithi(s, ARITH_AND + rexw, r1,                (CPU_TLB_SIZE - 1) << CPU_TLB_ENTRY_BITS, 0);     tcg_out_modrm_sib_offset(s, OPC_LEA + P_REXW, r1, TCG_AREG0, r1, 0,                             offsetof(CPUState, tlb_table[mem_index][0])                             + which);     /* cmp 0(r1), r0 */    tcg_out_modrm_offset(s, OPC_CMP_GvEv + rexw, r0, r1, 0);     tcg_out_mov(s, type, r0, addrlo); // r0 = addrlo     /* jne label1 */    // TLB 缺失,跳至 label1     /* TLB Hit. */     /* add addend(r1), r0 */    tcg_out_modrm_offset(s, OPC_ADD_GvEv + P_REXW, r0, r1,                         offsetof(CPUTLBEntry, addend) - which);}                    

  4.                         /* XXX: qemu_ld and qemu_st could be modified to clobber only EDX and EAX. It will be useful once fixed registers globals are less common. */ opc 代表 tcg_out_qemu_ld 是被 INDEX_op_qemu_ld8u (0),INDEX_op_qemu_ld16u (1),等等所呼叫。//static void tcg_out_qemu_ld(TCGContext *s, const TCGArg *args,                            int opc){    addrlo_idx = 1;     // TARGET_LONG_BITS 指被模擬的 guest。TCG_TARGET_REG_BITS 指宿主 host。    // mem_index 用來索引 CPUState 中的 tlb_table[mem_index]。    mem_index = args[addrlo_idx + 1 + (TARGET_LONG_BITS > TCG_TARGET_REG_BITS)];    // QEMU 會利用 opc 第 2 位指明是有號/無號數,這裡取末兩位作為欲讀取資料大小。    s_bits = opc & 3;     // addrlo_idx=1, mem_index=0, s_bits=1, which=0    // 傳遞 label_ptr 給 tcg_out_tlb_load 生成 label。    // label_ptr[0] 為 TLB 命中,label_ptr[1] 為 TLB 缺失。    tcg_out_tlb_load(s, addrlo_idx, mem_index, s_bits, args,                     label_ptr, offsetof(CPUTLBEntry, addr_read));     /* TLB Hit. */    tcg_out_qemu_ld_direct(s, data_reg, data_reg2,                           tcg_target_call_iarg_regs[0], 0, opc);     /* TLB Miss. */    // 準備參數。    tcg_out_movi(s, TCG_TYPE_I32, tcg_target_call_iarg_regs[arg_idx],                 mem_index);    // 呼叫 helper function。    tcg_out_calli(s, (tcg_target_long)qemu_ld_helpers[s_bits]);     // 視情況擴展。    switch(opc) {    }     /* label2: */    *label_ptr[2] = s->code_ptr - label_ptr[2] - 1;}                    

 ---- 0xe86c8 mov_i32 tmp2,edi qemu_ld8u tmp0,tmp2,$0x0 ext8u_i32 tmp12,tmp0 movi_i32 tmp13,$0xffffff00 and_i32 edx,edx,tmp13 or_i32 edx,edx,tmp12OUT: [size=172]0x40000ce0:  mov    0x1c(%r14),%ebp0x40000ce4:  mov    %ebp,%esi               // tcg_out_mov(s, type, r1, addrlo);0x40000ce6:  mov    %ebp,%edi0x40000ce8:  shr    $0x7,%esi               // tcg_out_shifti0x40000ceb:  and    $0xfffff000,%edi        // tgen_arithi,取得目標位址所在頁0x40000cf1:  and    $0x1fe0,%esi            // tgen_arithi,取得 TLB entry index0x40000cf7:  lea    0x348(%r14,%rsi,1),%rsi // tcg_out_modrm_sib_offset,取得 TLB entry 位址0x40000cff:  cmp    (%rsi),%edi             // tcg_out_modrm_offset,將 TLB entry 位址其內容和目標位址所在頁加以比較0x40000d01:  mov    %ebp,%edi               // edi = ebp0x40000d03:  jne    0x40000d0e              // TLB 缺失,跳至 0x40000d0e0x40000d05:  add    0x10(%rsi),%rdi         // TLB 命中。tcg_out_modrm_offset。將 addend (0x10(%rsi)) 加上 %rdi。0x40000d09:  movzbl (%rdi),%ebp             // ebp 是欲讀取的值?0x40000d0c:  jmp    0x40000d180x40000d0e:  xor    %esi,%esi               // TLB 缺失。0x40000d10:  callq  0x54cf8e                // \_\_ldb_mmu0x40000d15:  movzbl %al,%ebp                // 視狀況擴展。0x40000d18:  movzbl %bpl,%ebp0x40000d1c:  mov    0x8(%r14),%ebx
IN:0xc014198d:  test   %ebp,%ebp0xc014198f:  je     0xc0141999OP: ---- 0xc014198d mov_i32 tmp0,ebp mov_i32 tmp1,ebp discard cc_src           // 捨棄 cc_src 暫存器的內容。 and_i32 cc_dst,tmp0,tmp1 ---- 0xc014198f movi_i32 cc_op,$0x18      // 藉由之前計算出的 codition code 決定分支方向。 movi_i32 tmp12,$0x0 brcond_i32 cc_dst,tmp12,eq,$0x0 goto_tb $0x0              // 為 if 預留空間。tcg_gen_goto_tb(tb_num); movi_i32 tmp4,$0xc0141991 // if 分支跳躍目標,0xc0141991。gen_jmp_im(eip); st_i32 tmp4,env,$0x20     // 將該目標寫入 guest pc exit_tb $0x7f042bc5c070   // tcg_gen_exit_tb((tcg_target_long)tb + tb_num); set_label $0x0 goto_tb $0x1              // 為 else 預留空間 movi_i32 tmp4,$0xc0141999 // else 分支跳躍目標,0xc0141999 st_i32 tmp4,env,$0x20     // 將該目標寫入 guest pc exit_tb $0x7f042bc5c071OUT: [size=89]0x40b3cee0:  mov    0x14(%r14),%ebp0x40b3cee4:  mov    0x14(%r14),%ebx0x40b3cee8:  and    %ebx,%ebp0x40b3ceea:  mov    $0x18,%ebx0x40b3ceef:  mov    %ebx,0x30(%r14)0x40b3cef3:  mov    %ebp,0x2c(%r14)0x40b3cef7:  test   %ebp,%ebp0x40b3cef9:  je     0x40b3cf1c0x40b3ceff:  jmpq   0x40b3cf04            // 預留將來 block linking patch 的點0x40b3cf04:  mov    $0xc0141991,%ebp      // if 分支跳躍目標,0xc01419910x40b3cf09:  mov    %ebp,0x20(%r14)       // 將該目標寫入 guest pc0x40b3cf0d:  mov    $0x7f042bc5c070,%rax  // if (藉由後 2 個 bit)0x40b3cf17:  jmpq   0x10eadae             // 返回 prologue/epilogue0x40b3cf1c:  jmpq   0x40b3cf21            // 預留將來 block linking patch 的點0x40b3cf21:  mov    $0xc0141999,%ebp      // else 分支跳躍目標,0xc01419990x40b3cf26:  mov    %ebp,0x20(%r14)       // 將該目標寫入 guest pc0x40b3cf2a:  mov    $0x7f042bc5c071,%rax  // else (藉由後 2 個 bit)0x40b3cf34:  jmpq   0x10eadae             // 返回 prologue/epilogue

Register Allocation

  • struct TCGOpDef 用來定義各個 TCG Op 的相關性質。在不同宿主機上,暫存器分配有不同限制,由 strutc TCGArgConstraint 規範。

                            typedef struct TCGArgConstraint {    uint16_t ct;    uint8_t alias_index;    union {       // 視平台有多少個暫存器,TCGRegSet 被 typedef 成 uint32_t 或是 uint64_t。        // 用來代表宿主機上的暫存器組。        TCGRegSet regs;    } u;} TCGArgConstraint /* Bits for TCGOpDef->flags, 8 bits available. */enum {    /* Instruction defines the end of a basic block. */    TCG_OPF_BB_END       = 0x01,    /* Instruction clobbers call registers and potentially update globals. */    TCG_OPF_CALL_CLOBBER = 0x02,    /* Instruction has side effects: it cannot be removed if its outputs are not used. */    TCG_OPF_SIDE_EFFECTS = 0x04,    /* Instruction operands are 64-bits (otherwise 32-bits). */    TCG_OPF_64BIT        = 0x08,    /* Instruction is optional and not implemented by the host. */    TCG_OPF_NOT_PRESENT  = 0x10,}; typedef struct TCGOpDef {    const char *name;    // 此 TCG Op 的輸出參數,輸入參數,常數參數和參數個數。    uint8_t nb_oargs, nb_iargs, nb_cargs, nb_args;    uint8_t flags;    TCGArgConstraint *args_ct;    int *sorted_args;} TCGOpDef;                    

    • 在不同宿主機上,暫存器分配有不同限制。以 x86 為例,tcg/i386/tcg-target.c 有其規範。

                                      static const TCGTargetOpDef x86_op_defs[] = {    { INDEX_op_exit_tb, { } },    { INDEX_op_goto_tb, { } },    { INDEX_op_call, { "ri" } },    { INDEX_op_jmp, { "ri" } },     ... 略 ... } /* parse target specific constraints */static int target_parse_constraint(TCGArgConstraint *ct, const char **pct_str){    switch(ct_str[0]) {    case 'a':        ct->ct |= TCG_CT_REG;        tcg_regset_set_reg(ct->u.regs, TCG_REG_EAX);        break;     ... 略 ...     default:        return -1;    }    ct_str++;    *pct_str = ct_str;    return 0;}                            

  • tcg/tcg-opc.h 定義各個 TCG Op 的相關性質。

                            /* * DEF(name, oargs, iargs, cargs, flags) */ DEF(qemu_ld8u, 1, 1, 1, TCG_OPF_CALL_CLOBBER | TCG_OPF_SIDE_EFFECTS)DEF(qemu_ld8s, 1, 1, 1, TCG_OPF_CALL_CLOBBER | TCG_OPF_SIDE_EFFECTS)DEF(qemu_ld16u, 1, 1, 1, TCG_OPF_CALL_CLOBBER | TCG_OPF_SIDE_EFFECTS)DEF(qemu_ld16s, 1, 1, 1, TCG_OPF_CALL_CLOBBER | TCG_OPF_SIDE_EFFECTS)                    

  • struct TCGContext 為暫存器分配的中樞。

                            typedef struct TCGTemp {    TCGType base_type;    TCGType type;    int val_type;    int reg;    tcg_target_long val;    int mem_reg;    tcg_target_long mem_offset;    unsigned int fixed_reg:1;    unsigned int mem_coherent:1;    unsigned int mem_allocated:1;    unsigned int temp_local:1; /* If true, the temp is saved across basic blocks. Otherwise, it is not preserved across basic blocks. */    unsigned int temp_allocated:1; /* never used for code gen */    /* index of next free temp of same base type, -1 if end */    int next_free_temp;    const char *name;} TCGTemp; struct TCGContext {    uint8_t *pool_cur, *pool_end;    TCGPool *pool_first, *pool_current, *pool_first_large;    TCGLabel *labels;    int nb_labels;    TCGTemp *temps; /* globals first, temps after */     ... 略 ...};                    

  1. tcg_gen_code_common (tcg/tcg.c) 檢視 TCG IR。

    TCGOpDef tcg_op_defs[] = {#define DEF(s, oargs, iargs, cargs, flags) { #s, oargs, iargs, cargs, iargs + oargs + cargs, flags },#include "tcg-opc.h"#undef DEF}; static inline int tcg_gen_code_common(TCGContext *s, uint8_t *gen_code_buf,                                      long search_pc){    unsigned int dead_args;     tcg_reg_alloc_start(s);     for(;;) {        opc = gen_opc_buf[op_index];         def = &tcg_op_defs[opc];         switch(opc) {        ... 略 ...        default:            /* Sanity check that we've not introduced any unhandled opcodes. */            if (def->flags & TCG_OPF_NOT_PRESENT) {                tcg_abort();            }            /* Note: in order to speed up the code, it would be much faster to have specialized register allocator functions for some common argument patterns */            dead_args = s->op_dead_args[op_index];            tcg_reg_alloc_op(s, def, opc, args, dead_args);            break;        }        args += def->nb_args;    next:        if (search_pc >= 0 && search_pc < s->code_ptr - gen_code_buf) {            return op_index;        }        op_index++;    } the_end:    return -1;}}                    

  2. tcg_reg_alloc_op (tcg/tcg.c) 分配暫存器並呼叫 tcg_out_op 生成 host binary。

                            static void tcg_reg_alloc_op(TCGContext *s,                             const TCGOpDef *def, TCGOpcode opc,                             const TCGArg *args,                             unsigned int dead_args){    TCGRegSet allocated_regs;    int i, k, nb_iargs, nb_oargs, reg;    TCGArg arg;    const TCGArgConstraint *arg_ct;    TCGTemp *ts;    TCGArg new_args[TCG_MAX_OP_ARGS];    int const_args[TCG_MAX_OP_ARGS];     // 取得該 TCG Op 其輸出和輸入參數個數。    nb_oargs = def->nb_oargs;    nb_iargs = def->nb_iargs;     /* copy constants */    // TCGArg 依序放置輸出參數、輸入參數和常數參數。    memcpy(new_args + nb_oargs + nb_iargs,           args + nb_oargs + nb_iargs,           sizeof(TCGArg) * def->nb_cargs);     // 分配暫存器給輸入參數。      // 將 global 存回內存。      // 分配暫存器給輸出參數。      // 生成 host binary。     /* emit instruction */    tcg_out_op(s, opc, new_args, const_args);     /* move the outputs in the correct register if needed */    for(i = 0; i < nb_oargs; i++) {        ts = &s->temps[args[i]];        reg = new_args[i];        if (ts->fixed_reg && ts->reg != reg) {            tcg_out_mov(s, ts->type, ts->reg, reg);        }    }}                    

icount

icount 只有在 system mode 下有作用。以 x86 為例,

  1. cpu-defs.h 定義 icount 相關資料結構。

                            typedef struct icount_decr_u16 {    uint16_t low;    uint16_t high;} icount_decr_u16; #define CPU_COMMON \ int64_t icount_extra; /* Instructions until next timer event. */ \ /* Number of cycles left, with interrupt flag in high bit. \ This allows a single read-compare-cbranch-write sequence to test \ for both decrementer underflow and exceptions. */ \ union { \ uint32_t u32; \ icount_decr_u16 u16; \ } icount_decr;                    

  2. gen_intermediate_code_internal (target-i386/translate.c) 在翻譯 guest binary 的前後呼叫 gen_icount_start 和 gen_icount_end 插入 icount 相關的 TCG IR,兩者只有在開啟 icount 的情況下才有作用。

                            static inline void gen_intermediate_code_internal(CPUState *env, ...){    gen_icount_start();    for(;;) {        pc_ptr = disas_insn(dc, pc_ptr);        num_insns++;    }    if (tb->cflags & CF_LAST_IO)        gen_io_end();    gen_icount_end(tb, num_insns);}                    

    • gen_icount_start (gen-icount.h)

                                      static inline void gen_icount_start(void){    TCGv_i32 count;     if (!use_icount)        return;     icount_label = gen_new_label();    count = tcg_temp_local_new_i32();    tcg_gen_ld_i32(count, cpu_env, offsetof(CPUState, icount_decr.u32));    /* This is a horrid hack to allow fixing up the value later. */    icount_arg = gen_opparam_ptr + 1;    tcg_gen_subi_i32(count, count, 0xdeadbeef); // count -= 0xdeadbeef;     tcg_gen_brcondi_i32(TCG_COND_LT, count, 0, icount_label); // if count < 0 goto icount_label;    tcg_gen_st16_i32(count, cpu_env, offsetof(CPUState, icount_decr.u16.low)); // else count = icount_decr.u16.low    tcg_temp_free_i32(count);}                            

    • gen_icount_end (gen-icount.h)

                                      static void gen_icount_end(TranslationBlock *tb, int num_insns){    if (use_icount) {        *icount_arg = num_insns;        gen_set_label(icount_label); // 設置 label,做為 block 的開頭。如果 counter 小於零,跳至此 label。        tcg_gen_exit_tb((tcg_target_long)tb + 2); // 返回 QEMU,末兩位設為 2 做為返回值。    }}                            

  3. cpu_exec (cpu-exec.c)

                                            if (likely(!env->exit_request)) {                    tc_ptr = tb->tc_ptr;                    /* execute the generated code */                    next_tb = tcg_qemu_tb_exec(env, tc_ptr);                    // 只有當 icount 開啟且 counter expire,next_tb 末兩位才會被設成 2。                    if ((next_tb & 3) == 2) {                        /* Instruction counter expired. */                        int insns_left;                        tb = (TranslationBlock *)(long)(next_tb & ~3);                        /* Restore PC. */                        cpu_pc_from_tb(env, tb); // env->eip = tb->pc - tb->cs_base;                        insns_left = env->icount_decr.u32;                        if (env->icount_extra && insns_left >= 0) {                            /* Refill decrementer and continue execution. */                            env->icount_extra += insns_left;                            if (env->icount_extra > 0xffff) {                                insns_left = 0xffff;                            } else {                                insns_left = env->icount_extra;                            }                            env->icount_extra -= insns_left;                            env->icount_decr.u16.low = insns_left;                        } else {                            if (insns_left > 0) {                                /* Execute remaining instructions. */                                cpu_exec_nocache(env, insns_left, tb);                            }                            env->exception_index = EXCP_INTERRUPT;                            next_tb = 0;                            cpu_loop_exit(env);                        }                    }                    

    • cpu_exec_nocache (cpu-exec.c) 執行完 tb 之後就會把它清空。

                                      /* Execute the code without caching the generated code. An interpreter could be used if available. */static void cpu_exec_nocache(CPUState *env, int max_cycles,                             TranslationBlock *orig_tb){    unsigned long next_tb;    TranslationBlock *tb;     /* Should never happen. We only end up here when an existing TB is too long. */    if (max_cycles > CF_COUNT_MASK)        max_cycles = CF_COUNT_MASK;     tb = tb_gen_code(env, orig_tb->pc, orig_tb->cs_base, orig_tb->flags,                     max_cycles);    env->current_tb = tb;    /* execute the generated code */    next_tb = tcg_qemu_tb_exec(env, tb->tc_ptr);    env->current_tb = NULL;     if ((next_tb & 3) == 2) {        /* Restore PC. This may happen if async event occurs before the TB starts executing. */        cpu_pc_from_tb(env, tb);    }    tb_phys_invalidate(tb, -1);    tb_free(tb);}                            

向量指令

TCG 不支援向量指令,guest 向量指令需透過 helper function 實現。此外,考慮 guest 和 host 有大小端的問題,一般只能以 scalar 處理 guest 向量指令,無法直接使用 host 向量指令實現。

            // target-arm/neon_helper.cuint32_t HELPER(neon_add_u8)(uint32_t a, uint32_t b){    uint32_t mask;    mask = (a ^ b) & 0x80808080u;    a &= ~0x80808080u;    b &= ~0x80808080u;    return (a + b) ^ mask;}        
            static inline int gen_neon_add(int size, TCGv t0, TCGv t1){    switch (size) {    case 0: gen_helper_neon_add_u8(t0, t0, t1); break;    case 1: gen_helper_neon_add_u16(t0, t0, t1); break;    case 2: tcg_gen_add_i32(t0, t0, t1); break;    default: return 1;    }    return 0;}        
            static int disas_neon_data_insn(CPUState * env, DisasContext *s, uint32_t insn){        // 視情況將 128/64 bit vector operation 拆成 4/2 個 helper function call (一次處理 32 bit)。        for (pass = 0; pass < (q ? 4 : 2); pass++) {        case 16:            if (!u) { /* VADD */                if (gen_neon_add(size, tmp, tmp2))                    return 1;            } else { /* VSUB */                switch (size) {                case 0: gen_helper_neon_sub_u8(tmp, tmp, tmp2); break;                case 1: gen_helper_neon_sub_u16(tmp, tmp, tmp2); break;                case 2: tcg_gen_sub_i32(tmp, tmp, tmp2); break;                default: return 1;                }            }            break;        }}        

Coroutine

Coroutine 在 QEMU 上主要是為了做到多執行緒的效果,又不需要付出多執行緒所需要的開銷25)。某些函式在 QEMU 中被標記成 Coroutine,此類函式無法從一般的函式被呼叫。

            static void coroutine_fn foo(void) {  ...}        

QEMU 會使用 Coroutine 記下函式每一次的進入點,下一次呼叫會從上一次的返回點開始執行。

I/O

目前 QEMU 本身即為 IO thread 執行 main_loop_wait,當遇到 block IO 時,會 fork 出 worker thread 去處理。每一個 VCPU 均有對應的 VCPU 執行緒運行。

  1. main (vl.c)

                            int main(int argc, char **argv, char **envp){    ... 略 ...     qemu_init_cpu_loop();    // qemu_init_main_loop 呼叫 main_loop_init (main-loop.c)    if (qemu_init_main_loop()) {        fprintf(stderr, "qemu_init_main_loop failed\n");        exit(1);    }     ... 略 ...     cpu_exec_init_all();     bdrv_init_with_whitelist();     blk_mig_init();     /* open the virtual block devices */    if (snapshot)        qemu_opts_foreach(qemu_find_opts("drive"), drive_enable_snapshot, NULL, 0);    if (qemu_opts_foreach(qemu_find_opts("drive"), drive_init_func, &machine->use_scsi, 1) != 0)        exit(1);     ... 略 ...     resume_all_vcpus();    main_loop();    bdrv_close_all();    pause_all_vcpus();    net_cleanup();    res_free();     return 0;}                    

    • qemu_init_cpu_loop (cpus.c)

                                      void qemu_init_cpu_loop(void){    qemu_init_sigbus();    qemu_cond_init(&qemu_cpu_cond);    qemu_cond_init(&qemu_pause_cond);    qemu_cond_init(&qemu_work_cond);    qemu_cond_init(&qemu_io_proceeded_cond);    qemu_mutex_init(&qemu_global_mutex);     qemu_thread_get_self(&io_thread);}                            

    • qemu_init_main_loop (vl.c) 呼叫 main_loop_init (main-loop.c)。

                                      int main_loop_init(void){    int ret;     qemu_mutex_lock_iothread();    ret = qemu_signal_init();    if (ret) {        return ret;    }     /* Note eventfd must be drained before signalfd handlers run */    ret = qemu_event_init();    if (ret) {        return ret;    }     return 0;}                            

  2. main_loop (vl.c) 是主要的執行迴圈,即 IO thread。

                            static void main_loop(void){    bool nonblocking;    int last_io = 0;     do {        nonblocking = !kvm_enabled() && last_io > 0;         last_io = main_loop_wait(nonblocking);     } while (!main_loop_should_exit());}                    

  3. main_loop_wait (main-loop.c) 等待 work thread 完成任務。

                            int main_loop_wait(int nonblocking){    int ret;    uint32_t timeout = UINT32_MAX;     if (nonblocking) {        timeout = 0;    } else {        qemu_bh_update_timeout(&timeout);    }     /* poll any events */    /* XXX: separate device handlers from system ones */    nfds = -1;    FD_ZERO(&rfds);    FD_ZERO(&wfds);    FD_ZERO(&xfds);     qemu_iohandler_fill(&nfds, &rfds, &wfds, &xfds);    // 1. Waits for file descriptors to become readable or writable.    ret = os_host_main_loop_wait(timeout);    // fd 已便備,處理 IO。     qemu_iohandler_poll(&rfds, &wfds, &xfds, ret);     qemu_run_all_timers();     /* Check bottom-halves last in case any of the earlier events triggered them. */    qemu_bh_poll();     return ret;}                    

  • qemu_iohandler_poll (main-loop.c)。

                            void qemu_iohandler_poll(fd_set *readfds, fd_set *writefds, fd_set *xfds, int ret){    if (ret > 0) {        IOHandlerRecord *pioh, *ioh;         QLIST_FOREACH_SAFE(ioh, &io_handlers, next, pioh) {            if (!ioh->deleted && ioh->fd_read && FD_ISSET(ioh->fd, readfds)) {                ioh->fd_read(ioh->opaque);            }            if (!ioh->deleted && ioh->fd_write && FD_ISSET(ioh->fd, writefds)) {                ioh->fd_write(ioh->opaque);            }             /* Do this last in case read/write handlers marked it for deletion */            if (ioh->deleted) {                QLIST_REMOVE(ioh, next);                g_free(ioh);            }        }    }}                    

    • qemu_bh_poll (async.c) 處理 bh。

                                      struct QEMUBH {    QEMUBHFunc *cb;    void *opaque;    QEMUBH *next;    bool scheduled;    bool idle;    bool deleted;}; int qemu_bh_poll(void){    QEMUBH *bh, **bhp, *next;     ... 略 ...    // 有需要的裝置透過 qemu_bh_new (async.c) 將自己的 handler 加進 BH 等待調用。    // 這裡調用排定好的 bh handler。    for (bh = first_bh; bh; bh = next) {        next = bh->next;        if (!bh->deleted && bh->scheduled) {            bh->scheduled = 0;            if (!bh->idle)                ret = 1;            bh->idle = 0;            bh->cb(bh->opaque);        }    }     ... 略 ...}                            

    • 11.1 Bottom Half Handling

    • 11.1 Bottom Half Handling (任務的延遲處理)

  • ioport.[ch]: port IO 不用做位址轉換

  • MMIO 需要做位址轉換: env→iotlb

  • DMA 使用物理位址。

Timer

QEMUTimer 和

            struct QEMUTimer {    QEMUClock *clock; // 使用特定的 QEMUClock 計時    int64_t expire_time;    QEMUTimerCB *cb; // callback function pointer    void *opaque; // 傳給 callback function 的參數    struct QEMUTimer *next;};        

QEMUClock 有底下幾種,請見 qemu-timer.h:

  1. rt_clock: 只有不會改變虛擬機的事物才能使用 rt_clock,這是因為 rt_clock 即使在虛擬機停止的情況下仍會運作。

  2. vm_clock: vm_clock 只有在虛擬機運行時才會運作。

  3. host_clock: 用來產生 real time source 的虛擬設備使用 host_clock。

rtc_clock 會選擇上述其中一種 clock。

外設與中斷

請見 cpu-all.h,基本上有四類通用中斷:

  1. CPU_INTERRUPT_HARD: 虛擬外設發出的中斷。

  2. CPU_INTERRUPT_EXITTB: 用於某些外設改變其內存映射時,如: A20 line change。要求虛擬 CPU 離開目前的 TB。

  3. CPU_INTERRUPT_HALT: 停止當前的虛擬 CPU。

  4. CPU_INTERRUPT_DEBUG: 除錯之用。

另外留下 CPU_INTERRUPT_TGT_EXT_* 和 CPU_INTERRUPT_TGT_INT_* 給各個 CPU 自行運用。例如: target-i386/cpu.h。

請見 QEMU's new device model qdev 和 QEMU's device model qdev: Where do we go from here?。docs/qdev-device-use.txt。

  • hw/pc.c 一般 PC 周邊。

  • hw/irq.* 中斷之用。

  • hw/apic.c 模擬 APIC,負責發出中斷 (cpu_interrupt)。

  • hw/i8259.c 模擬 PIC。

  • hw/i8254.c 模擬時鐘。

QOM (Qemu Object Model) 用來取代 QDev 26)

虛擬外設發出的 IRQ 以 IRQState 包裝。在 QEMU 中,所有的设备包括总线,桥,一般设备都对应一个设备结构。總線,如 PCI 總線,在 QEMU 中包裝成 PCIBus; 橋,如 PCI 橋,在 QEMU 中包裝成 PCIBridgePCIDeviceInfo

  • pc_init_pci (hw/pc_piix.c) 呼叫 pc_init1 (hw/pc_piix.c) 進行 PC 機器的初始化。

                            /* PC hardware initialisation */static void pc_init1(ram_addr_t ram_size, ...){    /* 初始化 CPU */     /* 初始化內存 */     /* 初始化 PIC */    if (!xen_enabled()) {        cpu_irq = pc_allocate_cpu_irq();        i8259 = i8259_init(cpu_irq[0]);    } else {        i8259 = xen_interrupt_controller_init();    }     /* 初始化 ISA */    isa_irq_state = qemu_mallocz(sizeof(*isa_irq_state));    isa_irq_state->i8259 = i8259;     /* 初始化 IOAPIC */    if (pci_enabled) {        ioapic_init(isa_irq_state); // sysbus_get_default 會創建 main-system-bus    }     /* 初始化 PCI bus,之後即可將外設掛上 PCI bus */    if (pci_enabled) {        pci_bus = i440fx_init(&i440fx_state, &piix3_devfn, isa_irq, ram_size);    } else {        pci_bus = NULL;        i440fx_state = NULL;        isa_bus_new(NULL);    }     /* 初始化其它外設 */}                    

    • i8259 (PIC) 請見 PicState2 和 PicState。請見 8259 PIC 和 Intel 8259

      qemu_irq *i8259_init(qemu_irq parent_irq){    PicState2 *s;     s = qemu_mallocz(sizeof(PicState2));    pic_init1(0x20, 0x4d0, &s->pics[0]); // Master IO port 為 0x20    pic_init1(0xa0, 0x4d1, &s->pics[1]); // Slave IO port 為 0xa0    s->pics[0].elcr_mask = 0xf8;    s->pics[1].elcr_mask = 0xde;    s->parent_irq = parent_irq;    s->pics[0].pics_state = s;    s->pics[1].pics_state = s;    isa_pic = s;    return qemu_allocate_irqs(i8259_set_irq, s, 16);}                            

    • i440fx_init → i440fx_common_init。請見 Intel 440FX 和 PCI IDE ISA Xcelerator

                                      static PCIBus *i440fx_common_init(const char *device_name, ...){    DeviceState *dev;    PCIBus *b;    PCIDevice *d;    I440FXState *s; // 北橋     PIIX3State *piix3; // 南橋 (PCI-ISA)     /* 创建 PCI 主总线设备 */    dev = qdev_create(NULL, "i440FX-pcihost");    s = FROM_SYSBUS(I440FXState, sysbus_from_qdev(dev)); // 請見 hw/sysbus.h 和 osdep.h    /* 创建我们真正的 PCI 总线 */    b = pci_bus_new(&s->busdev.qdev, NULL, 0);    s->bus = b;    /* 初始化主总线设备 */    qdev_init_nofail(dev);    /* 创建主桥 */    d = pci_create_simple(b, 0, device_name);    *pi440fx_state = DO_UPCAST(PCII440FXState, dev, d);    /* 创建 ISA 桥 (南橋) */        piix3 = DO_UPCAST(PIIX3State, dev,                pci_create_simple_multifunction(b, -1, true, "PIIX3"));        pci_bus_irqs(b, piix3_set_irq, pci_slot_get_pirq, piix3,                PIIX_NUM_PIRQS);     /* 连接 8259 中断控制器,IOAPIC 貌似也和在一起 */    piix3->pic = pic;     (*pi440fx_state)->piix3 = piix3;     *piix3_devfn = piix3->dev.devfn;     ram_size = ram_size / 8 / 1024 / 1024;    if (ram_size > 255)        ram_size = 255;    (*pi440fx_state)->dev.config[0x57]=ram_size;     return b; /* 此後可將外設掛在這個 PCI bus */}                            

    • 第一部分 qemu oldworld mac(heathrow)的初始化

    • KVM虚拟机代码揭秘——QEMU的PCI总线与设备(上)

    • KVM虚拟机代码揭秘——QEMU的PCI总线与设备(下)

以 i8259 為例:

            static void i8259_set_irq(void *opaque, int irq, int level){    pic_set_irq1(&s->pics[irq >> 3], irq & 7, level);    pic_update_irq(s);}        
  1. 最後由 apic_local_deliver (Local APIC) 呼叫 cpu_interrupt 送出中斷給 virtual CPU。

Floppy

KVM

QEMU 與 KVM 的協作請見 What is the difference between KVM and QEMU? 和 Kernel-based Virtual Machine Technology

QOM

include/qemu/object.h

Watchpoint

  1. watch_mem_{read, write}

                            static uint64_t watch_mem_read(void *opaque, target_phys_addr_t addr,                               unsigned size){    check_watchpoint(addr & ~TARGET_PAGE_MASK, ~(size - 1), BP_MEM_READ);    switch (size) {    case 1: return ldub_phys(addr);    case 2: return lduw_phys(addr);    case 4: return ldl_phys(addr);    default: abort();    }} static const MemoryRegionOps watch_mem_ops = {    .read = watch_mem_read,    .write = watch_mem_write,    .endianness = DEVICE_NATIVE_ENDIAN,};                    

    • cpu_watchpoint_insert 用來插入 watchpoint。

    • qemu_add_vm_change_state_handler 用來這註冊當 QEMU 狀態有變化時會調用的函式。

  2. io_mem_init

                            static void io_mem_init(void){    memory_region_init_io(&io_mem_ram, &error_mem_ops, NULL, "ram", UINT64_MAX);    memory_region_init_io(&io_mem_rom, &rom_mem_ops, NULL, "rom", UINT64_MAX);    memory_region_init_io(&io_mem_unassigned, &unassigned_mem_ops, NULL,                          "unassigned", UINT64_MAX);    memory_region_init_io(&io_mem_notdirty, &notdirty_mem_ops, NULL,                          "notdirty", UINT64_MAX);    memory_region_init_io(&io_mem_subpage_ram, &subpage_ram_ops, NULL,                          "subpage-ram", UINT64_MAX);    memory_region_init_io(&io_mem_watch, &watch_mem_ops, NULL,                          "watch", UINT64_MAX);}                    

  3. check_watchpoint

                            /* Generate a debug exception if a watchpoint has been hit. */static void check_watchpoint(int offset, int len_mask, int flags){    ... 略 ...     // check_watchpoint 在 watch_mem_read 中被第一次呼叫時,env->watchpoint_hit 為 NULL。    if (env->watchpoint_hit) {        /* We re-entered the check after replacing the TB. Now raise * the debug interrupt so that is will trigger after the * current instruction. */        // 重新執行觸發 watchpoint 的指令,會來到這裡。          // env->interrupt_request 被設為 CPU_INTERRUPT_DEBUG,接著再返回 cpu_exec。(1.b)        cpu_interrupt(env, CPU_INTERRUPT_DEBUG);        return;    }    vaddr = (env->mem_io_vaddr & TARGET_PAGE_MASK) + offset;    // 查詢目前存取的內存位址是否有被監控。     QTAILQ_FOREACH(wp, &env->watchpoints, entry) {        if ((vaddr == (wp->vaddr & len_mask) ||             (vaddr & wp->len_mask) == wp->vaddr) && (wp->flags & flags)) {            wp->flags |= BP_WATCHPOINT_HIT;            // 第一次進到 check_watchpoint,env->watchpoint_hit 為 NULL。              if (!env->watchpoint_hit) {                env->watchpoint_hit = wp;                tb = tb_find_pc(env->mem_io_pc);                if (!tb) {                    cpu_abort(env, "check_watchpoint: could not find TB for "                              "pc=%p", (void *)env->mem_io_pc);                }                cpu_restore_state(tb, env, env->mem_io_pc);                tb_phys_invalidate(tb, -1);                if (wp->flags & BP_STOP_BEFORE_ACCESS) {                    env->exception_index = EXCP_DEBUG;                    cpu_loop_exit(env);                } else {                    // 重新翻譯觸發 watchpoint 的 TB,從觸發 watchpoint 的那一條指令開始翻起。                        // 返回至 cpu_exec 從觸發 watchpoint 的那一條指令開始執行。(1.a)                    cpu_get_tb_cpu_state(env, &pc, &cs_base, &cpu_flags);                    tb_gen_code(env, pc, cs_base, cpu_flags, 1);                    cpu_resume_from_signal(env, NULL);                }            }        } else {            wp->flags &= ~BP_WATCHPOINT_HIT;        }    }}                    

  4. cpu_exec

                            int cpu_exec(CPUArchState *env){    ... 略 ...     for(;;) {        if (setjmp(env->jmp_env) == 0) {            /* if an exception is pending, we execute it here */            if (env->exception_index >= 0) {                if (env->exception_index >= EXCP_INTERRUPT) {                    /* exit request from the cpu execution loop */                    ret = env->exception_index;                    if (ret == EXCP_DEBUG) {                        cpu_handle_debug_exception(env); // 處理 watchpoint。(1.b)                    }                    break;                } else {                    do_interrupt(env);                    env->exception_index = -1;                }            }             next_tb = 0; /* force lookup of first TB */            for(;;) {                interrupt_request = env->interrupt_request;                if (unlikely(interrupt_request)) {                     ... 略 ...                     if (interrupt_request & CPU_INTERRUPT_DEBUG) {                        env->interrupt_request &= ~CPU_INTERRUPT_DEBUG;                        env->exception_index = EXCP_DEBUG;                        cpu_loop_exit(env); // 返回 cpu_exec 外層迴圈。(1.a)                    }                     ... 略 ...                }            }    }     ... 略 ...}                    

            static void notdirty_mem_write(void *opaque, target_phys_addr_t ram_addr,                               uint64_t val, unsigned size){    int dirty_flags;    dirty_flags = cpu_physical_memory_get_dirty_flags(ram_addr);    if (!(dirty_flags & CODE_DIRTY_FLAG)) {#if !defined(CONFIG_USER_ONLY)        tb_invalidate_phys_page_fast(ram_addr, size);        dirty_flags = cpu_physical_memory_get_dirty_flags(ram_addr);#endif    }    switch (size) {    case 1:        stb_p(qemu_get_ram_ptr(ram_addr), val); // ram_addr 是 GPA,qemu_get_ram_ptr 將其轉成對應的 HVA。        break;    case 2:        stw_p(qemu_get_ram_ptr(ram_addr), val);        break;    case 4:        stl_p(qemu_get_ram_ptr(ram_addr), val);        break;    default:        abort();    }    dirty_flags |= (0xff & ~CODE_DIRTY_FLAG);    cpu_physical_memory_set_dirty_flags(ram_addr, dirty_flags);    /* we remove the notdirty callback only if the code has been flushed */    if (dirty_flags == 0xff)        tlb_set_dirty(cpu_single_env, cpu_single_env->mem_io_vaddr);}        
  1. stl_phys_notdirty (exec.c) 寫入 PTE。

                            void stl_phys_notdirty(target_phys_addr_t addr, uint32_t val){    uint8_t *ptr;    MemoryRegionSection *section;     section = phys_page_find(addr >> TARGET_PAGE_BITS);     if (!memory_region_is_ram(section->mr) || section->readonly) {        addr = memory_region_section_addr(section, addr);        if (memory_region_is_ram(section->mr)) {            section = &phys_sections[phys_section_rom];        }        io_mem_write(section->mr, addr, val, 4);    } else {        unsigned long addr1 = (memory_region_get_ram_addr(section->mr)                               & TARGET_PAGE_MASK)            + memory_region_section_addr(section, addr);        ptr = qemu_get_ram_ptr(addr1);        stl_p(ptr, val);         if (unlikely(in_migration)) {            if (!cpu_physical_memory_is_dirty(addr1)) {                /* invalidate code */                tb_invalidate_phys_page_range(addr1, addr1 + 4, 0);                /* set dirty bit */                cpu_physical_memory_set_dirty_flags(                    addr1, (0xff & ~CODE_DIRTY_FLAG));            }        }    }}                    

GDB Stub

  • gdb_handle_packet 處理 client 送來的 request。

                            static int gdb_handle_packet(GDBState *s, const char *line_buf){}                    

重要檔案

  • linux-user/main.c - qemu user main

  • exec.c - virtual page mapping and translated block handling

  • cpu-exec.c- i386 emulator main execution loop

  • target-i386/translate.c - i386 translation

  • tcg/tcg.c - Tiny Code Generator for QEMU

    • tcg_out_xxx 負責將參數 (host binary) 寫入 TCGContext 所指的 code cache。

  • tcg/tcg-op.h - 提供產生 TCG IR 的函式,opcode 寫入 gen_opc_ptr 指向的緩衝區 (translate-all.c 裡的 gen_opc_buf); operand 寫入 gen_opparam_ptr 指向的緩衝區。

    • tcg_gen_xxx 產生 TCG IR。

  • target-i386/op_helper.c - Code snippets called from TCG generated code. Implement more complex operations that gcc gets better than TCG.

  • target-i386/helper.c - Helper functions specific to the CPU, but called from multiple places around QEMU. For example the MMU code belongs here.

  • target-i386/cpu.h 會引用 cpu-defs.h 定義個平台共用的資料結構

  • cpu_x86_exec/cpu_exec 代表呼叫的是 cpu_x86_exec,實際上被呼叫的是 cpu_exec。

                            // target-i386/cpu.h#define cpu_exec cpu_x86_exe                    

  1. 當中斷發生時,translation block 必須 unchain。

    • cpu_interrupt (exec.c)→ cpu_unlink_tb (exec.c)

  2. 不同 ISA 有不同的中斷處理函式。請見 target-xxx/* 中的 do_interrupt_xxx。

重要資料結構

  • CPUX86State

    • CPU_COMMON

      • exception_index 存放中斷號。hw,syscall 都會賦值給 exception_index。QEMU 利用 setjmp/longjmp 處理中斷,jmp_env 用來存放上下文。

      • interrupt_request

      • exit_request。全域變數 exit_request 只有在開啟 IO 執行緒的情況下才會被 cpu_signal 賦值。

      • CPUState *env 會被保存在特定一個 host 暫存器,以加速存取 CPUState。請見 exec.h。

                                                register struct CPUX86State *env asm(AREG0);                                    

      • CPUState 已用 QOM 改寫。[Qemu-devel] [PULL] QOM CPUState for i386

  • TranslationBlock

    • 用來記錄 TB 所需訊息。tc_ptr 指向 translated code cache。TB 和客戶機物理頁面有所聯繫。

      • page_addr 紀錄 TB 所屬頁面。

      • page_next

  • PageDesc

    • 用來查找頁中的 TB

  • DisasContext

    • 反組譯 guest binary 所用的結構

  • TCGContext

    • 存放 TCG IR 所用的結構。

    • static_temps 用來存放運算過程中的中間值。

  • IOHandlerRecord

    • IOHandler 是 callback 函式

  • SegmentCache

    • 段暫存器快取。x86 載入 TR 和 IDTR 其選擇符的時候,會一併把其段基地址、段限長度和描述符屬性載入。cpu_x86_load_seg_cache (target-i386/cpu.h) 負責填充 SegmentCache。

其它

  1. in_asm 代表的是對 guest 的反組譯。out_asm 代表的是針對目標機器生成的組語。op 代表的是 QEMU 自己的 IR。

                            # 修改 linux-user/main.c 裡面的 #define DEBUG_LOGFILE "/tmp/qemu.log"# 修改 exec.c 裡面的 logfilename$ qemu-i386 -d in_asm,out_asm,op hello$ less /tmp/qemu.log

  2. 得知 qemu 在什麼地方做 log。

    $ grep -r qemu_log qemu-0.14.2/*                    

  3. 加入額外的 log 選項。

    1. cpu-all.h 中加入

                                      #define CPU_LOG_IBTC (1 << 10)#define CPU_LOG_TB_FIND (1 << 11)                            

    2. 打印出新增的選項。

                                      const CPULogItem cpu_log_items[] = {   { CPU_LOG_IBTC, "ibtc",     "print ibtc return address" },}                            

    3. 在代碼中使用新增的選項。

                                      #ifdef DEBUG_DISAS  if (qemu_loglevel_mask(CPU_LOG_IBTC)) {      qemu_log("%lu\t%p\n", guest_eip, next_tb->tc_ptr);  }#endif                            

  4. 定義自己的 helper function。例如要為 i386 guest 新增 helper function

    1. target-i386/helper.h

                                      // 傳入 helper function 的參數請見 def-helper.h,也請參考 tcg/README。// DEF_HELPER_FLAGS_? 中的 ? 代表參數個數。參數的意義分別是: helper function 的名稱,修飾子 (TCG_CALL_CONST 表示// 該 helper function 是否會修改到全域變數),回傳執型別和參數型別。// 相對應的函式: void *helper_lookup_ibtc(target_ulong guest_eip, CPUState *env)// 可用 gen_helper_lookup_ibtc(ibtc_host_eip, cpu_T[0]) 產生呼叫該 helper function 的 TCG IR。DEF_HELPER_FLAGS_2(lookup_ibtc, TCG_CALL_CONST, ptr, tl, env)                            

  5. 因為 QEMU 將跳入/出 code cache 當作函式呼叫,它會把 guest call instruction 翻譯成 jmp instruction。如果不這樣做的話,code cache 中的 longjmp (回 QEMU) 會破壞 call stack。

  6. 修改 monitor.[ch] 增加 QEMU Monitor 的功能。

  7. 查看 $BUILD/config-host.mak 得知設定參數。

Makefile

  • Makefile。

                            # configure 中有 echo "TARGET_DIRS=$target_list" >> $config_host_mak# $target_list 即為 --target-list=i386-linux-user 中的 i386-linux-user。include config-host.mak # 將 $(TARGET_DIRS) 中的 % 替換成 subdir-%。SUBDIR_RULES=$(patsubst %,subdir-%, $(TARGET_DIRS)) # $(GENERATED_HEADERS) 是編譯時才產生的標頭檔。subdir-%: $(GENERATED_HEADERS)  $(call quiet-command,$(MAKE) $(SUBDIR_MAKEFLAGS) -C $* V="$(V)" TARGET_DIR="$*/" all,) ifneq ($(wildcard config-host.mak),)include $(SRC_PATH)/Makefile.objsendif $(universal-obj-y) $(common-obj-y): $(GENERATED_HEADERS)subdir-libcacard: $(oslib-obj-y) $(trace-obj-y) qemu-timer-common.o # 從 $(SUBDIR_RULES) 濾出 %-softmmu,% 代表任意長度的字串。$(filter %-softmmu,$(SUBDIR_RULES)): $(universal-obj-y) $(trace-obj-y) $(common-obj-y) subdir-libdis $(filter %-user,$(SUBDIR_RULES)): $(GENERATED_HEADERS) $(universal-obj-y) $(trace-obj-y) subdir-libdis-user subdir-libuser

  • Makefile.target。QEMU_PROG 即是最後生成的執行檔。一般我們會在 $BUILD 目錄底下編譯,與 $SRC 目錄區隔。

                            ########################################################## Linux user emulator target ifdef CONFIG_LINUX_USER # call 負責將參數,在此為 $(SRC_PATH)/linux-user:$(SRC_PATH)/linux-user/$(TARGET_ABI_DIR),# 傳遞給表達式 set-vpath。rules.mak 定義 set-vpath。$(call set-vpath, $(SRC_PATH)/linux-user:$(SRC_PATH)/linux-user/$(TARGET_ABI_DIR)) # 如果 --target-list=i386-linux-user,TARGET_I386 會設成 y,最後成為 obj-y += vm86.o。# 可以把自己的 *.o 加在 obj-y 之後。obj-$(TARGET_I386) += vm86.o

  • rules.mak 27)

                            # 由 *.c 生成 *.o 檔。$@ 代表欲生成的 *.o 檔,@< 代表輸入的檔案,在此為 *.c 檔。# 在此可以新增條件,用 clang 生成 LLVM 的 *.bc 檔。%.o: %.c  $(call quiet-command,$(CC) $(QEMU_INCLUDES) $(QEMU_CFLAGS) $(QEMU_DGFLAGS) $(CFLAGS) -c -o $@ $<," CC $(TARGET_DIR)$@") # V 即為 `make V=1` 中的 V。此時會將執行的命令印在螢幕上,否則 @ 會使得執行的命令不顯示在螢幕上。# $1 即為 $(CC) $(QEMU_INCLUDES) $(QEMU_CFLAGS) $(QEMU_DGFLAGS) $(CFLAGS) -c -o $@ $<# $2 即為 " CC $(TARGET_DIR)$@"quiet-command = $(if $(V),$1,$(if $(2),@echo $2 && $1, @$1)) VPATH_SUFFIXES = %.c %.h %.S %.m %.mak %.texi %.sh # set-vpath 設定 VPATH。VPATH 是變量,vpath 是關鍵字。# VPATH 是變量,告知 make 在 $BUILD 目錄底下若找不到相應的檔案,應該要再找那些路徑。# foreach 是將 $(VPATH_SUFFIXES) 中的變數逐一放至 PATTERN,再執行 $(eval vpath $(PATTERN) $1)。# vpath 為符合 $(PATTERN) 的文件指定搜索目錄 $1,在此即為 $(SRC_PATH)/linux-user:$(SRC_PATH)/linux-user/$(TARGET_ABI_DIR))。set-vpath = $(if $1,$(foreach PATTERN,$(VPATH_SUFFIXES),$(eval vpath $(PATTERN) $1)))                    

  • 8.8 The eval Function

  • GNU Automake

  • [Qemu-devel] one question on the makefile

HowTo

  1. 對照第 72 頁 ABSQ_S.QB 中的 Operation 小節。

                            /* get abs value */static inline int8_t mipsdsp_sat_abs_u8(uint8_t a){    int8_t temp;    temp = a;     if (a == 0x80) {        set_DSPControl_overflow_flag(1, 20);        temp = 0x7f;    } else {        if ((a & 0x80) == 0x80)            temp = -temp;    }     return temp;}                    

Bug

Submitted Patch

請見 Git

$ wget http://wiki.qemu.org/download/linux-user-test-0.3.tar.gz; tar xvf linux-user-test-0.3.tar.gz$ cd linux-user-test-0.3$ vim qemu-linux-user.sh$ make test        

Q & A

  1. QEMU softmmu 做什麼? 
    softmmu 負責 GVA → GPA 的對映,但 QEMU 會用 TLB 做 GVA → HVA 的對映,這是用來加速28)。HVA 之後交由宿主機作業系統利用頁表對映至 HPA。

  2. tb_find_fast 和 tb_find_slow 的區別? 
    tb_find_fast 用 virtual pc 查找,tb_find_slow 則是用 virtual pc 對應的 physical addr 查找 29)。這是因為不同程序會有相同的 virtual pc,如果 tb_find_fast 用 virtual pc 查找得到的 tb 不屬於當前進程,則改用 tb_find_slow 查找。

  3. QEMU 處理 host physical address 嗎? 
    不。QEMU 內部最多用 TLB 存放 GVA → HVA 的對映。

  4. CPUState 中的 iotlb 有什麼用途? 
    Re: How can I understand iotlb (IOMMU)

  5. Block chaining 為何要有限制? 
    以 qemu-i386 為例,有兩處對 block chaining 有所限制30)

  6. 可以用 GDB 監看客戶機作業系統的物理位址嗎? GDB 只能監看虛擬位址,但有其它作法31)。Bochs 可以指定監看虛擬或物理位址。

  7. 多執行緒程序的支援? 還不周全。

  8. target_phys_addr_t 和 ram_addr_t 分別指的是? 
    請見 [Qemu-devel] target_phys_addr_t vs ram_addr_t

    • target_phys_addr_t 代表客戶機物理地址空間。如果客戶機是 x86 開啟 PAE 的話,target_phys_addr_t 為 64 bit。

    • target_ulong 代表客戶機暫存器大小和虛擬地址空間。如果客戶機是 x86 開啟 PAE 的話,target_ulong 為 32 bit。

    • ram_addr_t (uintptr_t) 代表宿主機虛擬地址空間。如果宿主機是 x86 的話,ram_addr_t 為 32 bit。

  9. PageDesc 和 PhysPageDesc 的區別? 
    注意,QEMU 看不到 HPA,所以註解中所提到的 physical 總是指 'guest physical'。PageDesc 和 PhysPageDesc 各有自己的兩層頁表 l1_map 和 l1_phys_map。l1_map 是用來查找某個客戶虛擬頁面中相對應的 TB 和其權限。l1_phys_map 用來查找 GPA → HVA 的對映 32)

  10. 如何新增 guest? 
    請參考 [Qemu-devel] [PATCH v5 00/32] target-xtensa: new target architecture

  11. 如何新增 guest process mode? 
    請參考 [Qemu-devel] [PATCH 0/3] MIPS64 user mode emulation in QEMU with Cavium 和 [Qemu-devel] [PATCH 0/7] MIPS64 user mode emulation in QEMU with Cavium[Qemu-devel] [PATCH 00/17] s390x emulation support

  12. 執行 code cache 發生頁缺失的時候,會呼叫 tb_find_pc 反查指向該塊 code cache 的 guest pc。

  13. [Qemu-devel] op-helper.c vs helper.c Re: [Qemu-devel] [PATCH RFC 00/11] AREG0 elimination

  14. [Qemu-devel] [RFC][PATCH 0/5] backdoor: lightweight guest-to-QEMU backdoor channel

  15. [Qemu-devel] cpu_x86() ?

  16. block unchaing 即使在 single thread process mode 和 system mode 都有潛在問題。[Qemu-devel] linux-user crashing in multi-threaded programs

  17. 記憶體模型。[Qemu-devel] Get only TCG code without execution

  18. [Qemu-devel] Removing indeterminism in qemu execution

  19. 只用 pc 和 cs_base 來區分不同 process 相同 virtual pc 是不夠的33)

  20. [Qemu-devel] copy benchmarks onto qemu

  21. [Qemu-devel] time inside qemu 和 Re: [Qemu-devel] time inside qemu

  22. [Coremu-list-devel] How do you measure timing on COREMU?

  23. [Qemu-devel] Detecting an assembly instruction in QEMU

  24. [Qemu-devel] [PATCH] fix some common typos

相關文章

外部連結


1) http://lists.gnu.org/archive/html/qemu-devel/2011-12/msg02907.html

2) http://lists.gnu.org/archive/html/qemu-devel/2011-05/msg01116.html

3) http://article.gmane.org/gmane.comp.emulators.qemu/102156

4) http://www.cs.nctu.edu.tw/~chenwj/log/QEMU/brad0-2011-11-01.txt

5) http://www.mail-archive.com/[email protected]/msg56517.html

6) http://www.cs.nctu.edu.tw/~chenwj/log/QEMU/rth-2011-11-05.txt

7) http://www.mail-archive.com/[email protected]/msg55898.html

8) http://www.mail-archive.com/[email protected]/msg56274.html

9) http://archives.gentoo.org/gentoo-ppc-user/msg_831759f005c0c38c9a6617a719374f0a.xml

10) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/pm215-2012-01-10.txt

11) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/pm215-2012-04-21.txt

12) http://people.cs.nctu.edu.tw/~chenwj/log/Buildroot/Jacmet-2012-04-27.txt

13) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/stefanha-2011-12-20.txt

14) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/agraf-2012-01-12.txt

15) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/zwu-2012-06-14.txt

16) http://www.mail-archive.com/[email protected]/msg91294.html

17) http://lists.nongnu.org/archive/html/qemu-devel/2012-01/msg00026.html

18) http://lists.gnu.org/archive/html/qemu-devel/2011-07/msg00134.html

19) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/agraf-2012-03-02.txt

20) http://lists.gnu.org/archive/html/qemu-devel/2012-01/msg02495.html

21) http://www.mail-archive.com/[email protected]/msg89014.html

22) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/friggle-2012-01-11.txt

23) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/pm215-2012-05-10.txt

24) http://lists.gnu.org/archive/html/qemu-devel/2011-12/msg00943.html

25) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/afaerber-2012-02-02.txt

26) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/pbrook-2012-01-30.txt

27) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/pm215-2012-05-01.txt

28) http://www.cs.nctu.edu.tw/~chenwj/log/stefanha-softmmu-2011-08-11.txt

29) http://www.cs.nctu.edu.tw/~chenwj/log/pm215-tb_find-2011-08-11.txt

30) http://lists.nongnu.org/archive/html/qemu-devel/2011-08/msg02249.html

31) http://lists.gnu.org/archive/html/qemu-devel/2011-08/msg03262.html

32) http://www.cs.nctu.edu.tw/~chenwj/log/pm215-PhysPageDescriptor-2011-09-06.txt

33) http://people.cs.nctu.edu.tw/~chenwj/log/QEMU/pm215-2012-02-01.txt

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/pkufergus/article/details/18401915

智能推荐

while循环&CPU占用率高问题深入分析与解决方案_main函数使用while(1)循环cpu占用99-程序员宅基地

文章浏览阅读3.8k次,点赞9次,收藏28次。直接上一个工作中碰到的问题,另外一个系统开启多线程调用我这边的接口,然后我这边会开启多线程批量查询第三方接口并且返回给调用方。使用的是两三年前别人遗留下来的方法,放到线上后发现确实是可以正常取到结果,但是一旦调用,CPU占用就直接100%(部署环境是win server服务器)。因此查看了下相关的老代码并使用JProfiler查看发现是在某个while循环的时候有问题。具体项目代码就不贴了,类似于下面这段代码。​​​​​​while(flag) {//your code;}这里的flag._main函数使用while(1)循环cpu占用99

【无标题】jetbrains idea shift f6不生效_idea shift +f6快捷键不生效-程序员宅基地

文章浏览阅读347次。idea shift f6 快捷键无效_idea shift +f6快捷键不生效

node.js学习笔记之Node中的核心模块_node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是-程序员宅基地

文章浏览阅读135次。Ecmacript 中没有DOM 和 BOM核心模块Node为JavaScript提供了很多服务器级别,这些API绝大多数都被包装到了一个具名和核心模块中了,例如文件操作的 fs 核心模块 ,http服务构建的http 模块 path 路径操作模块 os 操作系统信息模块// 用来获取机器信息的var os = require('os')// 用来操作路径的var path = require('path')// 获取当前机器的 CPU 信息console.log(os.cpus._node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是

数学建模【SPSS 下载-安装、方差分析与回归分析的SPSS实现(软件概述、方差分析、回归分析)】_化工数学模型数据回归软件-程序员宅基地

文章浏览阅读10w+次,点赞435次,收藏3.4k次。SPSS 22 下载安装过程7.6 方差分析与回归分析的SPSS实现7.6.1 SPSS软件概述1 SPSS版本与安装2 SPSS界面3 SPSS特点4 SPSS数据7.6.2 SPSS与方差分析1 单因素方差分析2 双因素方差分析7.6.3 SPSS与回归分析SPSS回归分析过程牙膏价格问题的回归分析_化工数学模型数据回归软件

利用hutool实现邮件发送功能_hutool发送邮件-程序员宅基地

文章浏览阅读7.5k次。如何利用hutool工具包实现邮件发送功能呢?1、首先引入hutool依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.19</version></dependency>2、编写邮件发送工具类package com.pc.c..._hutool发送邮件

docker安装elasticsearch,elasticsearch-head,kibana,ik分词器_docker安装kibana连接elasticsearch并且elasticsearch有密码-程序员宅基地

文章浏览阅读867次,点赞2次,收藏2次。docker安装elasticsearch,elasticsearch-head,kibana,ik分词器安装方式基本有两种,一种是pull的方式,一种是Dockerfile的方式,由于pull的方式pull下来后还需配置许多东西且不便于复用,个人比较喜欢使用Dockerfile的方式所有docker支持的镜像基本都在https://hub.docker.com/docker的官网上能找到合..._docker安装kibana连接elasticsearch并且elasticsearch有密码

随便推点

Python 攻克移动开发失败!_beeware-程序员宅基地

文章浏览阅读1.3w次,点赞57次,收藏92次。整理 | 郑丽媛出品 | CSDN(ID:CSDNnews)近年来,随着机器学习的兴起,有一门编程语言逐渐变得火热——Python。得益于其针对机器学习提供了大量开源框架和第三方模块,内置..._beeware

Swift4.0_Timer 的基本使用_swift timer 暂停-程序员宅基地

文章浏览阅读7.9k次。//// ViewController.swift// Day_10_Timer//// Created by dongqiangfei on 2018/10/15.// Copyright 2018年 飞飞. All rights reserved.//import UIKitclass ViewController: UIViewController { ..._swift timer 暂停

元素三大等待-程序员宅基地

文章浏览阅读986次,点赞2次,收藏2次。1.硬性等待让当前线程暂停执行,应用场景:代码执行速度太快了,但是UI元素没有立马加载出来,造成两者不同步,这时候就可以让代码等待一下,再去执行找元素的动作线程休眠,强制等待 Thread.sleep(long mills)package com.example.demo;import org.junit.jupiter.api.Test;import org.openqa.selenium.By;import org.openqa.selenium.firefox.Firefox.._元素三大等待

Java软件工程师职位分析_java岗位分析-程序员宅基地

文章浏览阅读3k次,点赞4次,收藏14次。Java软件工程师职位分析_java岗位分析

Java:Unreachable code的解决方法_java unreachable code-程序员宅基地

文章浏览阅读2k次。Java:Unreachable code的解决方法_java unreachable code

标签data-*自定义属性值和根据data属性值查找对应标签_如何根据data-*属性获取对应的标签对象-程序员宅基地

文章浏览阅读1w次。1、html中设置标签data-*的值 标题 11111 222222、点击获取当前标签的data-url的值$('dd').on('click', function() { var urlVal = $(this).data('ur_如何根据data-*属性获取对应的标签对象

推荐文章

热门文章

相关标签