技術分享 | 探索QEMU-KVM中PIO處理的奧秘

2022-01-01 安全客

我們都知道在kvm/qemu的虛擬機中向埠讀寫輸入會陷入kvm中(絕大部分埠)。但是其具體過程是怎麼樣的,虛擬機、kvm和qemu這三者的關係在這個過程中又是如何相互聯繫來完成這一模擬過程的。本文就是對這一問題的探索,通過對kvm進行調試來了解其中的奧秘。

零.  準備工作

工欲善其事,必先利其器。為了了解kvm如何對PIO進行截獲處理,首先需要調試kvm,這需要 配置雙機調試環境,網上很多例子,需要注意的是,4.x內核清除kernel text的可防寫有點問題。 所以本文還是用的3.x內核,具體為3.10.105。所以我們的環境是target為3.10.105的內核,debugger隨意。

如果我們直接用kvm/qemu調試,由於一個完整的環境會有非常多的vm exit,會干擾我們的分析。 這裡我們只需要建立一個使用kvm api建立起一個最簡易虛擬機的例子,在虛擬機中執行in/out 指令即可。網上也有很多這種例子。比如使用KVM API實現Emulator Demo, Linux KVM as a Learning Tool.

這裡我們使用第一個例子,首先從

https://github.com/soulxu/kvmsample   

把代碼clone下來,直接make,如果加載了kvm應該就可以看到輸出了,kvm的api用法這裡不表,仔細看看 前兩篇文章之一就可以了,qemu雖然複雜,本質上也是這樣運行的。這個例子中的guest是向埠輸出數據。

一.  IO埠在KVM中的註冊

首先我們需要明確的一點是,IO port 這個東西是CPU用來與外設進行數據交互的,也不是所有CPU都有。 在虛擬機看來是沒有IO port這個概念的,所以是一定要在vm exit中捕獲的。

對於是否截獲IO指令,是由vmcs中的VM-Execution controls中的兩個域決定的。 參考intel SDM 24.6.2:

我們可以看到如果設置了Use I/O bitmpas這一位,Unconditional I/O exiting就無效了,如果在IO bitmap 中某一位被設置為1,則訪問該埠就會發生vm exit,否則客戶機可以直接訪問。 IO bitmap的地址存在vmcs中的I/O-Bitmap Addresses域中,事實上,有兩個IO bitmap,我們叫做A和B。 再來看看SDM

每一個bitmap包含4kb,也就是一個頁,bitmap A包含了埠0000H到7FFFFH(4*1024*8),第二個埠包含了8000H到 FFFFH。

好了,我們已經從理論上對IO port有了了解了,下面看看kvm中的代碼。

首先我們看到arch/x86/kvm/vmx.c中,定義了兩個全局變量表示bitmap A和B的地址。 在vmx_init函數中這兩個指針都被分配了一個頁大小的空間,之後所有位都置1,然後在bitmap A中對第 80位進行了清零,也就是客戶機訪

這個0x80埠不會發生vm exit。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

static unsigned long *vmx_io_bitmap_a;

static unsigned long *vmx_io_bitmap_b;

static int __init vmx_init(void)

{

    vmx_io_bitmap_a = (unsigned long *)__get_free_page(GFP_KERNEL);

    vmx_io_bitmap_b = (unsigned long *)__get_free_page(GFP_KERNEL);

     

    

    /*

    * Allow direct access to the PC debug port (it is often used for I/O

    * delays, but the vmexits simply slow things down).

    */

    memset(vmx_io_bitmap_a, 0xff, PAGE_SIZE);

    clear_bit(0x80, vmx_io_bitmap_a);

    memset(vmx_io_bitmap_b, 0xff, PAGE_SIZE);

    ...

}

在同一個文件中,我們看到在對vcpu進行初始化的時候會把這個bitmap A和B的地址寫入到vmcs中去,這樣 就建立了對IO port的訪問的截獲。

1

2

3

4

5

6

7

static int vmx_vcpu_setup(struct vcpu_vmx *vmx)

{

    /* I/O */

    vmcs_write64(IO_BITMAP_A, __pa(vmx_io_bitmap_a));

    vmcs_write64(IO_BITMAP_B, __pa(vmx_io_bitmap_b));

    return 0;

}

二.  PIO中out的處理流程

本節我們來探討一下kvm中out指令的處理流程。首先,將上一節中的test.S代碼改一下,只out一次。

1

2

3

4

5

6

7

8

.globl _start

    .code16

_start:

    xorw %ax, %ax

    mov  $0x0a,%al

    out %ax, $0x10

    inc %ax

    hlt

kvm中guest發送vm exit之後會根據發送exit的原因調用各種handler。這也在vmx.c中

1

2

3

4

5

6

7

8

static int (*const kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = {

    [EXIT_REASON_EXCEPTION_NMI]           = handle_exception,

    [EXIT_REASON_EXTERNAL_INTERRUPT]      = handle_external_interrupt,

    [EXIT_REASON_TRIPLE_FAULT]            = handle_triple_fault,

    [EXIT_REASON_NMI_WINDOW]          = handle_nmi_window,

    [EXIT_REASON_IO_INSTRUCTION]          = handle_io,

    ...

}

對應這裡,處理IO的回調是handle_io。我們在target中執行:

1

root@ubuntu:/home/test# echo g >/proc/sysrq-trigger

這樣調試機中的gdb會斷下來,給handle_io下個斷點:

1

2

3

4

5

6

7

8

9

10

11

(gdb) b handle_io

Breakpoint 1 at 0xffffffff81037dca: file arch/x86/kvm/vmx.c, line 4816.

(gdb) c

接著,我們用gdb啟動target中的kvmsample,並且在main.c的84行下個斷點。

test@ubuntu:~/kvmsample$ gdb ./kvmsample 

...

Reading symbols from ./kvmsample...done.

(gdb) b ma

main        main.c      malloc      malloc@plt  

(gdb) b main.c:84

Breakpoint 1 at 0x400cac: file main.c, line 84.

第84行恰好是從ioctl KVM_RUN中返回回來的時候。

好了,開始r,會發現debugger已經斷下來了:

1

2

3

4

Thread 434 hit Breakpoint 1, handle_io (vcpu=0xffff8800ac528000)

at arch/x86/kvm/vmx.c:4816

4816    {

(gdb)

從handle_io的代碼我們可以看出,首先會從vmcs中讀取exit的一些信息,包括訪問這個埠是in還是out, 大小,以及埠號port等。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

static int handle_io(struct kvm_vcpu *vcpu)

{

    unsigned long exit_qualification;

    int size, in, string;

    unsigned port;

    exit_qualification = vmcs_readl(EXIT_QUALIFICATION);

    string = (exit_qualification & 16) != 0;

    in = (exit_qualification & 8) != 0;

    ++vcpu->stat.io_exits;

    if (string || in)

        return emulate_instruction(vcpu, 0) == EMULATE_DONE;

    port = exit_qualification >> 16;

    size = (exit_qualification & 7) + 1;

    skip_emulated_instruction(vcpu);

    return kvm_fast_pio_out(vcpu, size, port);

}

之後通過skip_emulated_instruction增加guest的rip之後調用kvm_fast_pio_out,在該函數中, 我們可以看到首先讀取guest的rax,這個值放的是向埠寫入的數據,這裡是,0xa

1

2

3

4

5

6

7

8

9

int kvm_fast_pio_out(struct kvm_vcpu *vcpu, int size, unsigned short port)

{

    unsigned long val = kvm_register_read(vcpu, VCPU_REGS_RAX);

    int ret = emulator_pio_out_emulated(&vcpu->arch.emulate_ctxt,

                        size, port, &val, 1);

    /* do not return to emulator after return from userspace */

    vcpu->arch.pio.count = 0;

    return ret;

}

我們可以對比gdb中看看數據:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

Thread 434 hit Breakpoint 1, handle_io (vcpu=0xffff8800ac528000)

    at arch/x86/kvm/vmx.c:4816

4816    {

(gdb) n

4821        exit_qualification = vmcs_readl(EXIT_QUALIFICATION);

(gdb) n

4825        ++vcpu->stat.io_exits;

(gdb) n

4827        if (string || in)

(gdb) n

4832        skip_emulated_instruction(vcpu);

(gdb) n

[New Thread 3654]

4834        return kvm_fast_pio_out(vcpu, size, port);

(gdb) s

kvm_fast_pio_out (vcpu=0xffff8800ac528000, size=16, port=16)

    at arch/x86/kvm/x86.c:5086

5086    {

(gdb) n

[New Thread 3656]

5087        unsigned long val = kvm_register_read(vcpu, VCPU_REGS_RAX);

(gdb) n

[New Thread 3657]

5088        int ret = emulator_pio_out_emulated(&vcpu->arch.emulate_ctxt,

(gdb) p /x val

$1 = 0xa

(gdb)

再往下,我們看到在emulator_pio_out_emulated,把值拷貝到了vcpu->arch.pio_data中,接著調用 emulator_pio_in_out。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

static int emulator_pio_out_emulated(struct x86_emulate_ctxt *ctxt,

                    int size, unsigned short port,

                    const void *val, unsigned int count)

{

    struct kvm_vcpu *vcpu = emul_to_vcpu(ctxt);

    memcpy(vcpu->arch.pio_data, val, size * count);

    return emulator_pio_in_out(vcpu, size, port, (void *)val, count, false);

}

static int emulator_pio_in_out(struct kvm_vcpu *vcpu, int size,

                unsigned short port, void *val,

                unsigned int count, bool in)

{

    trace_kvm_pio(!in, port, size, count);

    vcpu->arch.pio.port = port;

    vcpu->arch.pio.in = in;

    vcpu->arch.pio.count  = count;

    vcpu->arch.pio.size = size;

    if (!kernel_pio(vcpu, vcpu->arch.pio_data)) {

        vcpu->arch.pio.count = 0;

        return 1;

    }

    vcpu->run->exit_reason = KVM_EXIT_IO;

    vcpu->run->io.direction = in ? KVM_EXIT_IO_IN : KVM_EXIT_IO_OUT;

    vcpu->run->io.size = size;

    vcpu->run->io.data_offset = KVM_PIO_PAGE_OFFSET * PAGE_SIZE;

    vcpu->run->io.count = count;

    vcpu->run->io.port = port;

    return 0;

}

在後一個函數中,我們可以看到vcpu->run->io.data_offset設置為4096了,我們可以看到之前已經把我們 向埠寫的值通過memcpy拷貝到了vpuc->arch.pio_data中去了,通過調試我們可以看出其中的端倪。 vcpu->arch.pio_data就在kvm_run後面一個頁的位置。這也可以從kvm_vcpu_init中看出來。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

4405        vcpu->run->io.size = size;

(gdb) n

[New Thread 3667]

4406        vcpu->run->io.data_offset = KVM_PIO_PAGE_OFFSET * PAGE_SIZE;

(gdb) n

4407        vcpu->run->io.count = count;

(gdb) n

4408        vcpu->run->io.port = port;

(gdb) p count

$7 = 1

(gdb) n

4410        return 0;

(gdb) x /2b 0xffff88002a2a2000+0x1000

0xffff88002a2a3000: 0x0a    0x00

(gdb) p vcpu->run

$9 = (struct kvm_run *) 0xffff88002a2a2000

(gdb) p vcpu->arch.pio_data

$10 = (void *) 0xffff88002a2a3000

(gdb)

查看完整文章請點擊閱讀原文。

相關焦點

  • Qemu,KVM,Virsh傻傻的分不清
    命令qemu命令qemu-kvm命令qemu-system-x86_64命令這些之間是什麼關係呢?由於所有的指令都要從Qemu裡面過一手,因而性能比較差按照上一次的理論,完全虛擬化是非常慢的,所以要使用硬體輔助虛擬化技術Intel-VT,AMD-V,所以需要CPU硬體開啟這個標誌位,一般在BIOS裡面設置。
  • Qemu-KVM的CPUID初始化和自定義CPU Model顯示
    想要修改這些寄存器,首先得先看看CPUID指令在Qemu裡是怎麼處理的:經過一些搜索,發現KVM提供了一個接口KVM_SET_CPUID2,通過這個接口,可以在用戶空間設置需要模擬的CPUID的信息,而我們使用Qemu,肯定是會打開KVM加速的,因此,只需要看看Qemu在這方面是怎麼處理的就可以了。
  • 在KVM加速的Qemu中運行Android Oreo
    你將需要用到以下軟體:Linux Mint 19.1 (x86_64) 作為我們的主機系統(內核中內置了KVM支持)Qemu(https://github.com/qemu/qemu)Android 8.1.0(https://www.fosshub.com/Android-x86.html)Burp Suite community
  • Linux虛擬化技術KVM
    yum install -y qemu-kvm libvirt virt-install bridge-utils這麼多軟體都是什麼作用?軟體作用qemu-kvm整合了QEMU 和 KVM 的一個軟體。
  • 一文讀懂 Qemu 模擬器
    正因為 Qemu 是純軟體實現的,所有的指令都要經 Qemu 過一手,性能非常低,所以,在生產環境中,大多數的做法都是配合 KVM 來完成虛擬化工作,因為 KVM 是硬體輔助的虛擬化技術,主要負責 比較繁瑣的 CPU 和內存虛擬化,而 Qemu 則負責 I/O 虛擬化,兩者合作各自發揮自身的優勢,相得益彰。
  • 使用qemu安裝虛擬機
    最後看有沒有qemu安裝。這個每個發行版的可執行文件的名字或許有差別,但基本都以qemu開頭。有的叫qemu-kvm, qemu-system-x86_64等,當然也有特別的就是叫kvm。具體在你的發行版上如何命名,可以使用包管理軟體搜索qemu來確認和安裝。創建虛擬機的虛擬磁碟這個就和我們裝物理機,要求物理機上有硬碟一樣。我們安裝的虛擬機也是要有虛擬磁碟的。
  • 使用 qemu-kvm 安裝和運行 Vagrant | Linux 中國
    引用自它的網站:Vagrant 是用於在單工作流程中構建和管理虛擬機環境的工具。憑藉簡單易用的工作流程並專注於自動化,Vagrant 降低了開發環境的設置時間,提高了生產效率,並使「在我的機器上可以工作」的藉口成為過去。如果你已經熟悉 Vagrant 的基礎知識,那麼該文檔為所有的功能和內部結構提供了更好的參考。
  • qemu-pwn-基礎知識
    /qemu-system-x86_64 \-m 1G \-device strng \-hda my-disk.img \-hdb my-seed.img \-nographic \-L pc-bios/ \-enable-kvm \-device e1000,netdev=net0 \-netdev user,id=net0,hostfwd=tcp::5555-:22qemu虛擬機對應的內存為
  • KVM虛擬化
    KVM,是一個開源的系統虛擬化模塊,自 Linux 2.6.20 之後集成在Linux的各個主要發行版本中。 它使用 Linux自身的調度器進行管理,所以相對於Xen,其核心源碼很少。KVM目前已成為學術界的主流VMM(虛擬機監控器)之一。KVM的虛擬化需要硬體支持(如 Intel VT技術戒者 AMD V技術)。是基於硬體的完全虛擬化。
  • 怎樣在 ubuntu 和 debian 中通過命令行管理 KVM
    $ sudo apt-get install qemu-kvm libvirt-bin安裝期間,libvirtd 用戶組(在 debian 上是 libvirtd-qemu 用戶組)將會被創建,並且你的用戶 id 將會被自動添加到該組中。這樣做的目的是讓你可以以一個普通用戶而不是 root 用戶的身份去管理虛擬機。
  • Linux虛擬化KVM-Qemu分析(一)
    虛擬化是一種資源管理技術,在非虛擬化系統中,單個作業系統管理和使用所有的硬體資源,而在虛擬化系統中,硬體資源可以被抽象和分割成多個虛擬的實體用於支持多個作業系統,多個作業系統可以共享所有的實體硬體資源,從而達到物理資源的最大化利用;Virtual Machine Monitor(VMM),虛擬機監控器,也叫Hypervisor,向下管理實際的物理資源,向上給不同的虛擬機提供邏輯資源;Virtual
  • 如何使用 virsh 命令創建、還原和刪除 KVM 虛擬機快照 | Linux 中國
    注意:我們只能對磁碟格式為 Qcow2 的虛擬機的進行快照,並且 kvm 的 virsh 命令不支持 raw 磁碟格式,請使用以下命令將原始磁碟格式轉換為 qcow2。# qemu-img convert -f raw -O qcow2 image-name.img image-name.qcow2創建 KVM 虛擬機(域)快照我假設 KVM 管理程序已經在 CentOS 7 / RHEL 7 機器上配置好了,並且有虛擬機正在運行。
  • Linux運維基礎 - KVM(接上篇)
    工作中如果要創建100個虛擬機可以使用如下腳本:[root@localhost ~]#!/bin/bashif [ !/etc/libvirt/qemu ##查看KVM虛擬機配置文件kvm_ctos6-1.xml kvm_ctos6-3.xml kvm_ctos6-5.xmlkvm_ctos6-2.xml kvm_ctos6-4.xml networks[root@localhost ~]#[root@localhost ~]# virsh list --all ##查看所有KVM虛擬 Id 名稱
  • Linux系統——KVM虛擬機安裝與管理
    qemu-kvm-tools virt-manager libvirt -yKVM:它是linux系統內核的一個模塊qemu:虛擬化軟體qemu-kvm:管理工具(管理網卡等一些設備)創建一個磁碟-x86_64.raw', fmt=raw size=10737418240 創建完成後,我們可以看看這時的磁碟狀態信息[root@apache ~]# qemu-img info /opt/kvm.rawimage: /opt/kvm.raw
  • 在deepin作業系統上使用KVM虛擬機
    KVMKernel-based Virtual Machine基於內核的虛擬機,配合QEMU(處理器虛擬軟體),需要CPU支持虛擬化技術在amd64架構的deepin系統上安裝qemu以及virsh虛擬機管理工具:
  • CentOS下KVM虛擬化學習筆記
    全稱Kernel-based Virtual Machine, 其實kvm只是一個內核模塊,提供虛擬cpu和內存管理的模塊,至於其它的設備是由qemu模擬的,如網卡,顯卡,磁碟等。後來redhat聯合IBM以及Linux社區創造了libvirt,模擬的設備性能要比qemu的好很多,並提供了一系列的管理工具和api,整個集成了kvm虛擬化的解決方案。Linux(redhat系)裝載kvm模塊後,妖神一變成為了VM Monitor,也稱為Hypervisor,部署使用簡單,需要硬體支持虛擬化。一.
  • linux安裝kvm虛擬機軟體
    大家好我是sean,今天給大家帶來的是linux安裝kvm虛擬機軟體,廢話不多說,教程開始1.首先你需要查看下你的系統版本
  • 虛擬機管理KVM基本操作
    \一、KVM簡介1、基於內核的虛擬機 Kernel-based Virtual Machine(KVM)是一種內建於 Linux® 中的開源虛擬化技術。具體而言,KVM 可幫助您將 Linux 轉變為虛擬機監控程序,使主機計算機能夠運行多個隔離的虛擬環境,即虛擬客戶機或虛擬機(VM)。2、KVM 是 Linux 的一部分。
  • kvm虛擬機的virsh日常管理和配置
    [root@localhost ~]# virsh list Id 名稱 狀態-- 1     centos7                        runnin     2.關閉某一臺KVM虛擬機(正在啟動過程中是無法關閉的)
  • Linux虛擬化-KVM-虛擬機安裝
    1、KVM介紹虛擬化技術類型:仿真虛擬化:對系統硬體沒有要求,性能最低;半虛擬化: