自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

CPU虛擬化:虛擬機(jī)切入和退出

運(yùn)維 系統(tǒng)運(yùn)維
本文重點(diǎn)討論了虛擬機(jī)CPU如何在Host模式和Guest模式之間切換,以及在Host模式和Guest模式切換時(shí),KVM及物理CPU是如何保存虛擬CPU的上下文的。

本文重點(diǎn)討論了虛擬機(jī)CPU如何在Host模式和Guest模式之間切換,以及在Host模式和Guest模式切換時(shí),KVM及物理CPU是如何保存虛擬CPU的上下文的。

一、GCC內(nèi)聯(lián)匯編

KVM模塊中切入Guest模式的代碼使用GCC的內(nèi)聯(lián)匯編編寫,為了理解這段代碼,我們需要簡(jiǎn)要地介紹一下這段內(nèi)聯(lián)匯編涉及的語(yǔ)法,其基本語(yǔ)法模板如下:

  1. asm volatile ( assembler template  
  2.     : output operands                  /* optional */ 
  3.     : input operands                   /* optional */ 
  4.     : list of clobbered registers      /* optional */ 
  5.     ); 

1. 關(guān)鍵字asm和volatile

asm為GCC關(guān)鍵字,表示接下來(lái)要嵌入?yún)R編代碼,如果asm與程序中其他命名沖突,可以使用__asm__。

volatile為可選關(guān)鍵字,表示不需要GCC對(duì)下面的匯編代碼做任何優(yōu)化,類似的,GCC也支持__volatile__。

2. 匯編指令(assembler template)

這部分即要嵌入的匯編指令,由于是在C語(yǔ)言中內(nèi)聯(lián)匯編代碼,因此須用雙引號(hào)將命令括起來(lái)。如果內(nèi)嵌多行匯編指令,則每條指令占用1行,每行指令使用雙引號(hào)括起來(lái),以后綴\n\t結(jié)尾,其中\(zhòng)n為newline的縮寫,\t為tab的縮寫。由于GCC將每條指令以字符串的形式傳遞給匯編器AS,所以我們使用\n\t分隔符來(lái)分隔每一條指令,示例代碼如下:

  1. __asm__ ("movl %eax, %ebx \n\t" 
  2.           "movl $56, %esi \n\t" 
  3.           "movl %ecx, $label(%edx,%ebx,$4) \n\t" 
  4.           "movb %ah, (%ebx) \n\t"); 

當(dāng)使用擴(kuò)展模式,即包含output、input和clobber list部分時(shí),匯編指令中需要使用兩個(gè)“%”來(lái)引用寄存器,比如%%rax;使用一個(gè)“%”來(lái)引用輸入、輸出操作數(shù),比如%1,以便幫助GCC區(qū)分寄存器和由C語(yǔ)言提供的操作數(shù)。

3. 輸出操作數(shù)(output operands)

內(nèi)聯(lián)匯編有零個(gè)或多個(gè)輸出操作數(shù),用來(lái)指示內(nèi)聯(lián)匯編指令修改了C代碼中的變量。如果有多個(gè)輸出參數(shù),則需要對(duì)每個(gè)輸出參數(shù)進(jìn)行分隔。每個(gè)輸出操作數(shù)的格式為:

  1. [[asmSymbolicName]] constraint (cvariablename) 

我們可以為輸出操作數(shù)指定一個(gè)名字asmSymbolicName,匯編指令中可以使用這個(gè)名字引用輸出操作數(shù)。

除了使用名字引用操作數(shù)外,還可以使用序號(hào)引用操作數(shù)。比如輸出操作數(shù)有兩個(gè),那么可以用%0引用第1個(gè)輸出操作數(shù),%1引用第2個(gè)操作數(shù),以此類推。

輸出操作數(shù)的約束部分必須以“=”或者“+”作為前綴,“=”表示只寫,“+”表示讀寫。在前綴之后,就可以是各種約束了,比如“=a”表示先將結(jié)果輸出至rax/eax寄存器,然后再由rax/eax寄存器更新相應(yīng)的輸出變量。

cvariablename為代碼中的C變量名字,需要使用括號(hào)括起來(lái)。

4. 輸入操作數(shù)(input operands)

內(nèi)聯(lián)匯編可以有零個(gè)或多個(gè)輸入操作數(shù),輸入操作數(shù)來(lái)自C代碼中的變量或者表達(dá)式,作為匯編指令的輸入,每個(gè)輸入操作數(shù)的格式如下:

  1. [[asmSymbolicName]] constraint (cexpression) 

同輸出操作數(shù)相同,也可以為每個(gè)輸入操作數(shù)指定名字asmSymbolicName,匯編指令中可以使用這個(gè)名字引用輸入操作數(shù)。

除了使用名字引用輸入操作數(shù)外,還可以使用序號(hào)引用輸入操作數(shù)。輸入操作數(shù)的序號(hào)以最后一個(gè)輸出操作數(shù)的序號(hào)加1開始,比如輸出操作數(shù)有兩個(gè),輸入操作數(shù)有3個(gè),那么需要使用%2引用第1個(gè)輸入操作數(shù),%3引用第2個(gè)輸入操作數(shù),以此類推。

除了不必以“=”或者“+”前綴開頭外,輸入操作數(shù)的前綴與輸出操作數(shù)基本相同。除了寄存器約束外,在后面的代碼中我們還會(huì)看到“i”這個(gè)約束,表示這個(gè)輸入操作數(shù)是個(gè)立即數(shù)(immediate integer)。

cexpression為代碼中的C變量或者表達(dá)式,需要使用括號(hào)括起來(lái)。

5. clobber list

某些匯編指令執(zhí)行后會(huì)有一些副作用,可能會(huì)隱性地影響某些寄存器或者內(nèi)存的值,如果被影響的寄存器或者內(nèi)存并沒有在輸入、輸出操作數(shù)中列出來(lái),那么需要將這些寄存器或者內(nèi)存列入clobber list。通過這種方式,內(nèi)聯(lián)匯編告知GCC,需要GCC“照顧”好這些被影響的寄存器或者內(nèi)存,比如必要時(shí)需要在執(zhí)行內(nèi)聯(lián)匯編指令前保存好寄存器,而在執(zhí)行內(nèi)聯(lián)匯編指令后恢復(fù)寄存器的值。

接下來(lái)我們來(lái)看一個(gè)具體的例子。這個(gè)例子是一個(gè)加法運(yùn)算,一個(gè)加數(shù)是val,值為100,另外一個(gè)加數(shù)是一個(gè)立即數(shù)400,計(jì)算結(jié)果保存到變量sum中:

  1. int val = 100sum = 0
  2.  
  3.  
  4.  asm ("movl %1, %%rax; \n\t" 
  5.        "movl %c[addend], %%rbx; \n\t" 
  6.        "addl %%rbx, %%rax; \n\t" 
  7.        “movl %%rax, %0; \n\t” 
  8.  
  9.  
  10.        : “=”(sum) 
  11.       : (c)(val), [addend]”i”(400) 
  12.        : “rbx” 
  13.       ); 

我們先來(lái)看第3行的匯編指令。因?yàn)榇嬖诩拇嫫饕煤屯ㄟ^序號(hào)引用的操作數(shù),所以使用兩個(gè)“%”引用寄存器。%1引用的是輸入操作數(shù)val,其中c表示使用rcx寄存器保存val,也就是說(shuō)在執(zhí)行這條匯編指令前,首先將val的值賦值到rcx寄存器中,然后匯編指令再將rcx寄存器的值賦值到rax寄存器中。

第4行的匯編指令引用的addend是第2個(gè)輸入操作數(shù)的符號(hào)名字,因?yàn)檫@是一個(gè)立即數(shù),所以這個(gè)變量前面使用了c修飾符。這是GCC的一個(gè)語(yǔ)法,表示后面是個(gè)立即數(shù)。

第5條指令求rbx寄存器和rax寄存器的和,并將結(jié)果保存到rax寄存器中。

第6條指令中的%0引用的是輸出操作數(shù)sum,這是C代碼中的變量,因?yàn)閟um是只寫的輸出操作數(shù),所以使用約束“=”。所以第6行的匯編指令是將計(jì)算的結(jié)果存儲(chǔ)到變量sum中。

從這段代碼中我們看到,在匯編代碼中使用了rbx寄存器,而rbx寄存器沒有出現(xiàn)在輸出、輸入操作數(shù)中,所以內(nèi)聯(lián)匯編需要把rbx寄存器列入clobber list中,見第10行代碼,告訴GCC匯編指令污染了rbx寄存器,如果有必要,則需要在執(zhí)行內(nèi)聯(lián)匯編指令前自行保存rbx寄存器,執(zhí)行內(nèi)聯(lián)匯編指令后再自行恢復(fù)rbx寄存器。

二、虛擬機(jī)切入和退出及相關(guān)的上下文保存

了解了內(nèi)聯(lián)匯編的語(yǔ)法后,接下來(lái)我們開始探討虛擬機(jī)切入和退出部分的內(nèi)聯(lián)匯編指令:

  1. static void vmx_vcpu_run(struct kvm_vcpu *vcpu) 
  2.     struct vcpu_vmx *vmx = to_vmx(vcpu); 
  3.     … 
  4.     asm( 
  5.         /* Store host registers */ 
  6.         "push %%"R"dx; push %%"R"bp;" 
  7.         "push %%"R"cx \n\t" 
  8.         "cmp %%"R"sp, %c[host_rsp](%0) \n\t" 
  9.         "je 1f \n\t" 
  10.         "mov %%"R"sp, %c[host_rsp](%0) \n\t" 
  11.         __ex(ASM_VMX_VMWRITE_RSP_RDX) "\n\t" 
  12.         "1: \n\t" 
  13.         /* Reload cr2 if changed */ 
  14.         "mov %c[cr2](%0), %%"R"ax \n\t" 
  15.         "mov %%cr2, %%"R"dx \n\t" 
  16.         "cmp %%"R"ax, %%"R"dx \n\t" 
  17.         "je 2f \n\t" 
  18.         "mov %%"R"ax, %%cr2 \n\t" 
  19.         "2: \n\t" 
  20.         /* Check if vmlaunch of vmresume is needed */ 
  21.         "cmpl $0, %c[launched](%0) \n\t" 
  22.         /* Load guest registers.  Don't clobber flags. */ 
  23.         "mov %c[rax](%0), %%"R"ax \n\t" 
  24.         "mov %c[rbx](%0), %%"R"bx \n\t" 
  25.         … 
  26.         "mov %c[rcx](%0), %%"R"cx \n\t" /* kills %0 (ecx) */ 
  27.  
  28.  
  29.         /* Enter guest mode */ 
  30.         "jne .Llaunched \n\t" 
  31.         __ex(ASM_VMX_VMLAUNCH) "\n\t" 
  32.         "jmp .Lkvm_vmx_return \n\t" 
  33.         ".Llaunched: " __ex(ASM_VMX_VMRESUME) "\n\t" 
  34.         ".Lkvm_vmx_return: " 
  35.         /* Save guest registers, load host registers, keep …*/ 
  36.         "xchg %0,     (%%"R"sp) \n\t" 
  37.         "mov %%"R"ax, %c[rax](%0) \n\t" 
  38.         "mov %%"R"bx, %c[rbx](%0) \n\t" 
  39.         "pop"Q" %c[rcx](%0) \n\t" 
  40.         "mov %%"R"dx, %c[rdx](%0) \n\t" 
  41.         … 
  42.         "mov %%cr2, %%"R"ax   \n\t" 
  43.         "mov %%"R"ax, %c[cr2](%0) \n\t" 
  44.  
  45.  
  46.         "pop  %%"R"bp; pop  %%"R"dx \n\t" 
  47.         "setbe %c[fail](%0) \n\t" 
  48.           : : "c"(vmx), "d"((unsigned long)HOST_RSP), 
  49.         [launched]"i"(offsetof(struct vcpu_vmx, launched)), 
  50.         [fail]"i"(offsetof(struct vcpu_vmx, fail)), 
  51.         [host_rsp]"i"(offsetof(struct vcpu_vmx, host_rsp)), 
  52.         [rax]"i"(offsetof(struct vcpu_vmx,  
  53.                    vcpu.arch.regs[VCPU_REGS_RAX])), 
  54.         [rbx]"i"(offsetof(struct vcpu_vmx,  
  55.                    vcpu.arch.regs[VCPU_REGS_RBX])), 
  56.         … 
  57.         [cr2]"i"(offsetof(struct vcpu_vmx, vcpu.arch.cr2)) 
  58.           : "cc", "memory" 
  59.         , R"ax", R"bx", R"di", R"si" 
  60. #ifdef CONFIG_X86_64 
  61.         , "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15" 
  62. #endif 
  63.           ); 
  64.     … 

CPU從Host模式切換到Guest模式時(shí),并不會(huì)自動(dòng)保存部分寄存器,典型的比如通用寄存器。因此,第7行代碼KVM將宿主機(jī)的通用寄存器保存到棧中。當(dāng)發(fā)生VM退出時(shí),KVM從棧中將這些保存的宿主機(jī)的通用寄存器恢復(fù)到CPU的物理寄存器中。這里,宏R在64位下值為r,32位下為e,所以通過定義這個(gè)宏,從編碼層面更簡(jiǎn)潔地支持64位和32位。但是讀者可能有疑問,為什么這里只保存這兩個(gè)寄存器?事實(shí)上,KVM最初的實(shí)現(xiàn)是將所有的通用寄存器都?jí)喝霔V辛?。后?lái)使用了GCC內(nèi)聯(lián)匯編的clobber list特性,將所有可能會(huì)被內(nèi)聯(lián)匯編代碼影響的寄存器都寫入clobber list中,GCC自己負(fù)責(zé)保存和恢復(fù)操作這些寄存器的內(nèi)容。代碼第57~61行就是clobber list。這里面有兩個(gè)特殊的寄存器:rdx/edx和rbp/ebp,其中rdx/edx寄存器是GCC保留的regparm特性,不能放在clobber list中,另外一個(gè)rbp/ebp寄存器也不生效,所以KVM手動(dòng)保存了這兩個(gè)寄存器。

此外,KVM在第8行代碼保存了rcx/ecx寄存器,這里的rcx/ecx寄存器有著特殊的使命。當(dāng)從Guest退出到Host時(shí),CPU不會(huì)自動(dòng)保存Guest的一些寄存器,典型的如通用寄存器,KVM手動(dòng)將其保存到了結(jié)構(gòu)體vcpu_vmx中的子結(jié)構(gòu)體中。因此,在Guest退出的那一刻,首先必須要獲取結(jié)構(gòu)體vcpu_vmx的實(shí)例,也就是第3行代碼中的變量vmx,將CPU寄存器中的狀態(tài)保存到這個(gè)vmx中,也就是說(shuō),在保存完Guest的狀態(tài)后,才能進(jìn)行其他操作,避免破壞Guest的狀態(tài)。于是,每次從Host切入Guest前的最后一刻,KVM將vmx的地址壓入棧頂,然后在Guest退出時(shí)從棧頂?shù)谝粫r(shí)間取出vmx。那么如何將vmx壓入棧頂呢?參見第47行代碼,這里使用了GCC內(nèi)聯(lián)匯編的input約束,即在執(zhí)行匯編代碼前,告訴編譯器將變量vmx加載到rcx/ecx寄存器,那么在執(zhí)行第8行代碼,即將rcx/ecx寄存器的內(nèi)容壓入棧時(shí),實(shí)際上是將變量vmx壓入棧頂了。

在Guest退出時(shí),CPU會(huì)自動(dòng)將VMCS中Host的rsp/esp寄存器恢復(fù)到物理CPU的rsp/esp寄存器中,所以此時(shí)可以訪問VCPU線程在Host態(tài)下的棧。在Guest退出后的第1行代碼,即第36行代碼,調(diào)用xchg指令將棧頂?shù)闹岛托蛱?hào)%0指代的變量進(jìn)行交換,根據(jù)第47行代碼可見,%0指代變量vmx,對(duì)應(yīng)的寄存器是rcx/ecx,也就是說(shuō),這行代碼將切入Guest之前保存到棧頂?shù)淖兞縱mx的地址恢復(fù)到了rcx/ecx寄存器中,%0引用的也是這個(gè)地址,那么就可以使用%0引用這個(gè)地址保存Guest的寄存器了。

讀者可能會(huì)問,Guest沒有使用變量vmx,也沒有破壞它,那么Host是否可以直接使用這個(gè)變量呢?事實(shí)上,從底層來(lái)看,對(duì)于存放在棧中的變量vmx,GCC通常使用棧幀基址指針rbp/ebp或寄存器引用。但是,在Guest退出的第一時(shí)間,除了專用寄存器,這些通用寄存器中保存的都是Guest的狀態(tài),所以自然也無(wú)法通過rbp/ebp加偏移的方式來(lái)引用vmx。因?yàn)橥顺鯣uest時(shí)CPU自動(dòng)恢復(fù)Host的棧頂指針,所以KVM巧妙地利用了這一點(diǎn),借助棧頂保存vmx。然后,通過交換棧頂?shù)淖兞亢蛂cx/ecx寄存器,實(shí)現(xiàn)了在rcx/ecx寄存器中引用vmx的同時(shí),又將Guest的rcx/ecx寄存器的狀態(tài)保存到了棧中。

獲取到了保存Guest狀態(tài)的地址,接下來(lái)保存Guest的狀態(tài),見代碼第37~43行。

退出Guest后的第1行代碼(即第36行)將Guest的rcx/ecx寄存器的值保存到了棧中,所以第39行代碼從棧頂彈出Guest的rcx/ecx的值到保存Guest狀態(tài)的內(nèi)存中rcx/ecx相應(yīng)的位置。

并不是每次Guest退出到切入,Host的棧都會(huì)發(fā)生變化,因此Host的rsp/esp也無(wú)須每次都更新。只有rsp/esp變化了,才需要更新VMCS中Host的rsp/esp字段,以減少不必要的寫VMCS操作。所以KVM在VCPU中記錄了host_rsp的值,用來(lái)比較rsp/esp是否發(fā)生了變化,見代碼第9~13行。

將Host的rsp/esp寫入VMCS中的指令是:

  1. ASM_VMX_VMWRITE_RSP_RDX 

寫VMCS的指令有兩個(gè)參數(shù),一個(gè)指明寫VMCS中哪個(gè)字段,另外一個(gè)是寫入的值。rsp/esp很好理解,指明寫入的值在rsp/esp寄存器里。那么rdx是什么呢?見第47行代碼對(duì)寄存器rdx/edx的約束:

  1. "d"((unsigned long)HOST_RSP) 

結(jié)合宏HOST_RSP的定義:

  1. /* VMCS Encodings */ 
  2. enum vmcs_field { 
  3.     … 
  4.     HOST_RSP                        = 0x00006c14
  5.     … 
  6. }; 

可見,ASM_VMX_VMWRITE_RSP_RDX就是將rsp/esp的值寫入VMCS中Host的rsp字段。

VMX沒有定義CPU自動(dòng)保存cr2寄存器,但是事實(shí)上,Host可能更改cr2的值,以下面這段代碼為例:

  1. commit 1c696d0e1b7c10e1e8b34cb6c797329e3c33f262 
  2. KVM: VMX: Simplify saving guest rcx in vmx_vcpu_run 
  3. linux.git/arch/x86/kvm/x86.c 
  4.  
  5.  
  6. void kvm_inject_page_fault(struct kvm_vcpu *vcpu, …) 
  7.     ++vcpu->stat.pf_guest; 
  8.     vcpu->arch.cr2 = fault->address; 
  9.     kvm_queue_exception_e(vcpu, PF_VECTOR, fault->error_code); 

所以,在切入Guest前,KVM檢測(cè)物理CPU的cr2寄存器與VCPU中保存的Guest的cr2寄存器是否相同,如果不同,則需要使用Guest的cr2寄存器更新物理CPU的cr2寄存器,見第14~20行代碼。但是絕大數(shù)情況下,從Guest退出到下一次切入Guest,cr2寄存器的值不會(huì)發(fā)生變化,另一方面,加載cr2寄存器的開銷很大,所以只有在cr2寄存器發(fā)生變化時(shí)才需要重新加載cr2寄存器。

有些Guest的退出是由頁(yè)面異常引起的,比如通過MMIO方式訪問外設(shè)的I/O,而頁(yè)面異常的地址會(huì)記錄在cr2寄存器中,因此在Guest退出時(shí),KVM需要保存Guest的cr2,見代碼第42~43行。由于指令格式的限制,mov指令不支持控制寄存器到內(nèi)存地址的復(fù)制,因此需要通過rax/eax寄存器中轉(zhuǎn)一下。

在切入Guest前,除了加載cr2寄存器外,還需要加載那些物理CPU不會(huì)自動(dòng)加載的通用寄存器,見代碼第24~27行。

考慮到xchg是個(gè)原子操作,會(huì)鎖住地址總線,因此為了提高效率,后來(lái)KVM摒棄了這條指令,設(shè)計(jì)了一種新的方案。KVM在VCPU的棧中為Guest的rcx/ecx寄存器分配了一個(gè)位置。這樣,當(dāng)Guest退出時(shí),在使用rcx/ecx寄存器引用變量vmx前,可以將Guest的rcx/ecx寄存器臨時(shí)保存到VCPU的棧中為其預(yù)留的位置:

  1. commit 40712faeb84dacfcb3925a88231daa08b3624d34 
  2. KVM: VMX: Avoid atomic operation in vmx_vcpu_run 
  3. linux.git/arch/x86/kvm/vmx.c 
  4.  
  5.  
  6.  static void vmx_vcpu_run(struct kvm_vcpu *vcpu) 
  7.  { 
  8.      … 
  9.      asm( 
  10.          /* Store host registers */ 
  11.          "push %%"R"dx; push %%"R"bp;" 
  12.          "push %%"R"cx \n\t" /* placeholder for guest rcx */ 
  13.          "push %%"R"cx \n\t" 
  14.          … 
  15.          ".Lkvm_vmx_return: " 
  16.          /* Save guest registers, load host registers, …*/ 
  17.          "mov %0, %c[wordsize](%%"R"sp) \n\t" 
  18.          "pop %0 \n\t" 
  19.          "mov %%"R"ax, %c[rax](%0) \n\t" 
  20.          "mov %%"R"bx, %c[rbx](%0) \n\t" 
  21.          "pop"Q" %c[rcx](%0) \n\t" 
  22.      … 
  23.          [wordsize]"i"(sizeof(ulong)) 
  24.      … 
  25.  } 

第7行代碼就是KVM為Guest的rcx/ecx寄存器在棧上預(yù)留的空間,第8行代碼是將變量vmx壓入棧中。

在Guest退出的那一刻,CPU的rcx/ecx寄存器中存儲(chǔ)的是Guest的狀態(tài),所以使用rcx/ecx寄存器前,需要將Guest的狀態(tài)保存起來(lái)。保存的位置就是進(jìn)入Guest前,KVM為其在棧上預(yù)留的位置,即棧頂?shù)南乱粋€(gè)位置,見第12行代碼,即棧頂加上一個(gè)字(word)的偏移。

保存好Guest的值后,rcx/ecx寄存器就可以使用了,第13行代碼將棧頂?shù)闹导磛mx彈出到rcx/ecx寄存器中。彈出棧頂?shù)膙mx后,下面就是Guest的rcx/ecx寄存器了,所以第16行代碼將Guest的rcx/ecx寄存器保存到結(jié)構(gòu)體VCPU中的相關(guān)寄存器數(shù)組中。

 

責(zé)任編輯:趙寧寧 來(lái)源: 今日頭條
相關(guān)推薦

2018-03-13 15:08:19

虛擬機(jī)CPU虛擬化

2012-05-18 10:22:23

2010-07-26 09:02:38

2013-07-17 09:32:58

2010-12-27 14:11:55

虛擬機(jī)配置CPU

2013-04-07 09:52:40

Ubuntu虛擬機(jī)虛擬化軟件

2020-06-18 16:39:10

KVM虛擬化虛擬機(jī)

2010-08-30 10:51:38

2011-02-16 14:49:17

虛擬機(jī)

2010-11-19 16:53:14

桌面虛擬化虛擬機(jī)

2023-04-26 07:51:36

虛擬機(jī)操作系統(tǒng)進(jìn)程

2012-08-22 15:07:45

虛擬化

2009-06-29 19:36:07

虛擬機(jī)備份虛擬環(huán)境

2009-07-29 17:19:02

hypervisor-container-b

2014-05-19 16:46:00

虛擬化技術(shù)虛擬機(jī)

2012-08-17 11:36:23

虛擬化

2020-01-17 10:52:37

無(wú)服務(wù)器容器技術(shù)

2023-09-03 17:05:20

虛擬機(jī)

2012-04-10 10:29:29

2022-04-30 17:11:55

Testcloud虛擬機(jī)云端自動(dòng)化
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)