我們都知道在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)
查看完整文章請點擊閱讀原文。