動手實現程式碼虛擬機器
Author:[email protected]
0x00 什麼是程式碼虛擬化
虛擬化實際上我認為就是使用一套自定義的位元組碼來替換掉程式中原有的native指令,而位元組碼在執行的時候又由程式中的直譯器來解釋執行。自定義的位元組碼是隻有直譯器才能識別的,所以一般的工具是無法識別我們自定義的位元組碼,也是因為這一點,基於虛擬機器的保護相對其他保護而言要更加難破解。但是直譯器一般都是native程式碼,這樣才能使直譯器執行起來解釋執行位元組碼。其中的關係就像很多的解釋型語言一樣,不是系統的可執行檔案,不能直接在系統中執行,需要相應的直譯器才能執行,如python。
0x01 為什麼研究程式碼虛擬化
目前很多地方都會用到虛擬化技術,比如sandbox、程式保護殼等。很多時候為了防止惡意程式碼對我們的系統造成破壞,我們需要一個sandbox,使程式執行在sandbox中,即使惡意程式碼破壞系統也只是破壞了sandbox而不會對我們的系統造成影響。還有如vmp,shielden這些加密殼就是內建了一個虛擬機器來實現對程式程式碼的保護,基於虛擬機器的保護相對其他保護而言破解起來會更加困難,因為使用現有的工具也是不能識別虛擬機器的位元組碼。在見識過這類保護殼的威力之後,也萌生出了自己動手寫一個的衝動,所以才有了本文。
0x02 基於虛擬機器的程式碼混淆
基於虛擬機器的程式碼保護也可以算是程式碼混淆技術的一種。程式碼混淆的目的就是防止程式碼被逆向分析,但是所有的混淆技術都不是完全不能被分析出來,只是增加了分析的難度或者加長了分析的時間,雖然這些技術對保護程式碼很有效果,但是也存在著副作用,比如會或多或少的降低程式效率,這一點在基於虛擬機器的保護中格外突出,所以大多基於虛擬機器的保護都只是保護了其中比較重要的部分。在基於虛擬機器的程式碼保護中可以大致分為兩種:
使用虛擬機器解釋執行解殼程式碼。這種混淆是為了隱藏原始碼是如何被加密的,又是如何被解殼程式碼解密的。這種方式對於靜態分析來說比較有效,但是對於動態除錯效果不大。因為動態除錯的時候完全可以等到解殼程式碼解密初原始碼之後進行脫殼。只有配合其他保護技術才會有比較強的保護效果。
把需要保護的程式原始碼轉換為自定義位元組碼,再使用虛擬機器解釋執行被轉換後的程式位元組碼,而程式的原始碼是不會出現在程式中的。這種方式不管靜態還是動態都可以有效的保護。
可以看出兩種保護的區別就是,第一種只保護解殼程式碼,沒有保護原始碼。第二種直接保護了所有原始碼。所以第一種的強度也小於第二種。本文則是以第二種方式來實現保護,也就是保護所有原始碼。
在基於虛擬機器的保護技術中,通常自定義的位元組碼與native指令都存在著對映關係,也就是說一條或多條位元組碼對應於一條native指令。至於為什麼需要多條位元組碼對應同一條native指令,其實是為了增加虛擬機器保護被破解的難度,這樣在對被保護的程式碼進行轉換的時候就可以隨機生成出多套位元組碼不同,但執行效果相同的程式,導致逆向分析時的難度增加。
0x03 需要實現什麼?
首先了解過程式碼虛擬化的原理之後,知道了其中的原理就是自定義一套位元組碼,然後使用一個直譯器解釋執行位元組碼。所以,目標分為兩部分:
定義位元組碼
位元組碼只是一個標識,可以隨意定義,以下是我定義的位元組碼,其中每條指令標識都對應於一個位元組
#!c++ /* * opcode enum */ enum OPCODES { MOV = 0xa0, // mov 指令位元組碼對應 0xa0 XOR = 0xa1, // xor 指令位元組碼對應 0xa1 CMP = 0xa2, // cmp 指令位元組碼對應 0xa2 RET = 0xa3, // ret 指令位元組碼對應 0xa3 SYS_READ = 0xa4, // read 系統呼叫位元組碼對應 0xa4 SYS_WRITE = 0xa5, // write 系統呼叫位元組碼對應 0xa5 JNZ = 0xa6 // jnz 指令位元組碼對應 0xa0 };
因為我的demo只是一個簡單的crackme,所以只定義了幾個常用的指令。如果有需要,可以在這個基礎上繼續定義出更多的位元組碼來豐富虛擬機器功能。
實現直譯器
在定義好指令對應的位元組碼之後,就可以實現一個直譯器用來解釋上面定義的指令位元組碼了。在實現虛擬機器直譯器之前需要先搞清楚我們都要虛擬出一些什麼。一個虛擬機器其實就是虛擬出一個程式(自定義的位元組碼)執行的環境,其實這裡的虛擬機器在解釋執行位元組碼時與我們真實處理器執行很相似。在物理機中的程式都需要一個執行指令的處理器、棧、堆等環境才可以執行起來,所以首當其衝需要虛擬出一個處理器,處理器中需要有一些暫存器來輔助計算,以下是我定義的虛擬處理器
#!c /* * virtual processor */ typedef struct processor_t { int r1; // 虛擬暫存器r1 int r2; // 虛擬暫存器r2 int r3; // 虛擬暫存器r3 int r4; // 虛擬暫存器r4 int flag; // 虛擬標誌暫存器flag,作用類似於eflags unsigned char *eip; // 虛擬機器暫存器eip,指向正在解釋的位元組碼地址 vm_opcode op_table[OPCODE_NUM]; // 位元組碼列表,存放了所有位元組碼與對應的處理函式 } vm_processor; /* * opcode struct */ typedef struct opcode_t { unsigned char opcode; // 位元組碼 void (*func)(void *); // 與位元組碼對應的處理函式 } vm_opcode;
上面結構中r1~r4是4個通用暫存器,用來傳引數和返回值。eip則指向當前正在執行的位元組碼地址。op_table中存放了所有位元組碼指令的處理函式。上面的兩個虛擬出的結構就是虛擬機器的核心,之後直譯器在解釋位元組碼的時候都是圍繞著以上兩個結構的。因為程式邏輯簡單,所以只需要虛擬出一個處理器就可以了,堆和棧都不是必須的。程式中的資料我用了一個buffer來儲存,也可以把整個buffer理解成堆或者是棧。
有了上面兩個結構之後,就可以來動手寫直譯器了。直譯器的工作其實就是判斷當前解釋的位元組碼是否可以解析,如果可以就把相應引數傳遞給相應的處理函式,讓處理函式來解釋執行這一條指令。以下是直譯器程式碼
#!c void vm_interp(vm_processor *proc) { /* eip指向被保護程式碼的第一個位元組 * target_func + 4是為了跳過編譯器生成的函式入口的程式碼 */ proc->eip = (unsigned char *) target_func + 4; // 迴圈判斷eip指向的位元組碼是否為返回指令,如果不是就呼叫exec_opcode來解釋執行 while (*proc->eip != RET) { exec_opcode(proc); } }
其中target_func是自定義位元組碼編寫的目標函式,是eip指向目標函式的第一個位元組,準備解釋執行。當碰到RET指令就結束,否則呼叫exec_opcode執行位元組碼。以下是exec_opcode程式碼
#!c void exec_opcode(vm_processor *proc) { int flag = 0; int i = 0; // 查詢eip指向的正在解釋的位元組碼對應的處理函式 while (!flag && i < OPCODE_NUM) { if (*proc->eip == proc->op_table[i].opcode) { flag = 1; // 查詢到之後,呼叫本條指令的處理函式,由處理函式來解釋 proc->op_table[i].func((void *) proc); } else { i++; } } }
解釋位元組碼時首先判斷是哪一個指令需要執行,接著呼叫它的處理函式。以下是target_func的虛擬碼。虛擬碼的邏輯就是首先從標準輸入中讀取0x12個位元組,然後前8位與0x29異或,最後逐位與記憶體中8個位元組比較,全部相同則輸出success,失敗輸出error。以下的程式碼完全可以改成迴圈結構來實現,但是這裡我偷懶了,全部是複製貼上。
#!c /* mov r1, 0x00000000 mov r2, 0x12 call vm_read ; 輸入 mov r1, input[0] mov r2, 0x29 xor r1, r2 ; 異或 cmp r1, flag[0] ; 比較 jnz ERROR ; 如果不相同就跳轉到輸出錯誤的程式碼 ; 同上 mov r1, input[1] xor r1, r2 cmp r1, flag[1] jnz ERROR mov r1, input[2] xor r1, r2 cmp r1, flag[2] jnz ERROR mov r1, input[3] xor r1, r2 cmp r1, flag[3] jnz ERROR mov r1, input[4] xor r1, r2 cmp r1, flag[4] jnz ERROR mov r1, input[5] xor r1, r2 cmp r1, flag[5] jnz ERROR mov r1, input[6] xor r1, r2 cmp r1, flag[6] jnz ERROR mov r1, input[7] xor r1, r2 cmp r1, flag[7] jnz ERROR */
相應處理函式程式碼在後文的完整程式碼中。有了以上關鍵函式,一個簡單的虛擬機器就可以執行了。在虛擬機器中,還可以建立虛擬機器堆疊以及更完整的暫存器來豐富虛擬機器支援的指令。因為本程式相對簡單所以沒有用到堆疊,所有引數都透過暫存器傳遞,或者隱含在位元組碼中。有興趣可以自己修改。
0x04 直譯器解釋執行過程
這裡就用demo中的第一條位元組碼做演示來說明虛擬機器中直譯器解釋執行時的過程,首先可以從上面看到直譯器vm_interp執行時eip會指向target_func + 4,也就是target_func中內聯彙編中定義的第一個位元組0xa0,之後會判斷eip指向的位元組碼是否為ret指令,ret指令是0xa3,所以不是eip指向的不是ret,進入exec_opcode函式進行位元組碼解釋。
進入exec_opcode後開始在虛擬處理器的op_table中查詢eip指向的位元組碼,當前就是0xa0,找到之後就呼叫它的解釋函式。
位元組碼與解釋函式的初始化在init_vm_proc中
可以看出0xa0就對應著mov指令,所以當直譯器遇到0xa0就會呼叫vm_mov函式來解釋mov指令。
在vm_mov函式中首先把eip + 1處的一個位元組和eip + 2處4個位元組分別儲存在dest和src中,dest是暫存器標識,在後面的switch中判斷dest是哪個暫存器,在這個例子中dest是0x10,也就是r1暫存器,在case 0x10分支中就把*src賦值給r1。總體來看,前6個位元組就是第一條mov指令,對應著mov r1, xxxx,xxxx就是這6個位元組中的後4個,在這個例子中就是0x00000000。
從這個例子中就可以大致瞭解一個直譯器在解釋執行位元組碼時的過程,其實很簡單,就是透過一個位元組碼和解釋函式的關係來呼叫相應的函式,或者透過一個很長的switch來判斷每個位元組碼,並呼叫相應函式。而解釋函式則透過執行相應的操作來模擬出一個指令。最後,把這些指令串聯在一起就可以執行完一個完整的邏輯。
0x05 程式碼執行效果
0x06 虛擬機器保護效果
靜態分析
在靜態分析基於虛擬機器保護的程式碼時,一般的工具都是沒有效果的,因為位元組碼都是我們自己定義的,只有直譯器能夠識別。所以在用ida分析時位元組碼只是一段不能識別的資料。
這就是ida識別到的target_func的程式碼,已經做到了對抗靜態分析。不過還是可以靜態分析我們的直譯器,在分析直譯器的時候,直譯器中的控制流會比源程式的控制流複雜很多,這樣也是會增加分析難度。
動態除錯
在動態除錯的時候位元組碼依然不能被識別,而且處理器也不會真正的去執行這些不被識別的東西。因為這些位元組碼都是被我們的虛擬處理器透過直譯器執行的,而我們的直譯器都是native指令,所以可以靜態分析,也可以動態除錯。但是動態除錯的時候只是在除錯直譯器,在除錯過程中只能看到在不斷的呼叫各個指令的解釋函式。所以想要真正還原出原始碼就需要在除錯過程中找到所有位元組碼對應的native指令的對映關係,最後,透過這個對映關係來把位元組碼轉換成native指令,當然也可以修復出一個完全脫殼並且可以執行的native程式,只是過程會比較繁瑣。
0x07 完整程式碼
以下是demo的完整程式碼,已經在linux中測試透過。
xvm.h
#!c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define OPCODE_NUM 7 // opcode number
#define HEAP_SIZE_MAX 1024
char *heap_buf; // vm heap
/*
* opcode enum
*/
enum OPCODES
{
MOV = 0xa0, // mov 指令位元組碼對應 0xa0
XOR = 0xa1, // xor 指令位元組碼對應 0xa1
CMP = 0xa2, // cmp 指令位元組碼對應 0xa2
RET = 0xa3, // ret 指令位元組碼對應 0xa3
SYS_READ = 0xa4, // read 系統呼叫位元組碼對應 0xa4
SYS_WRITE = 0xa5, // write 系統呼叫位元組碼對應 0xa5
JNZ = 0xa6 // jnz 指令位元組碼對應 0xa0
};
enum REGISTERS
{
R1 = 0x10,
R2 = 0x11,
R3 = 0x12,
R4 = 0x13,
EIP = 0x14,
FLAG = 0x15
};
/*
* opcode struct
*/
typedef struct opcode_t
{
unsigned char opcode; // 位元組碼
void (*func)(void *); // 與位元組碼對應的處理函式
} vm_opcode;
/*
* virtual processor
*/
typedef struct processor_t
{
int r1; // 虛擬暫存器r1
int r2; // 虛擬暫存器r2
int r3; // 虛擬暫存器r3
int r4; // 虛擬暫存器r4
int flag; // 虛擬標誌暫存器flag,作用類似於eflags
unsigned char *eip; // 虛擬機器暫存器eip,指向正在解釋的位元組碼地址
vm_opcode op_table[OPCODE_NUM]; // 位元組碼列表,存放了所有位元組碼與對應的處理函式
} vm_processor;
xvm.c
#!c
#include "xvm.h"
void target_func()
{
__asm__ __volatile__(".byte 0xa0, 0x10, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x11, 0x12, 0x00, 0x00, 0x00, 0xa4, 0xa0, 0x14, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x11, 0x29, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x20, 0xa6, 0x5b, 0xa0, 0x14, 0x01, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x21, 0xa6, 0x50, 0xa0, 0x14, 0x02, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x22, 0xa6, 0x45, 0xa0, 0x14, 0x03, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x23, 0xa6, 0x3a, 0xa0, 0x14, 0x04, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x24, 0xa6, 0x2f, 0xa0, 0x14, 0x05, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x25, 0xa6, 0x24, 0xa0, 0x14, 0x06, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x26, 0xa6, 0x19, 0xa0, 0x14, 0x07, 0x00, 0x00, 0x00, 0xa1, 0xa2, 0x27, 0xa6, 0x0f, 0xa0, 0x10, 0x30, 0x00, 0x00, 0x00, 0xa0, 0x11, 0x09, 0x00, 0x00, 0x00, 0xa5, 0xa3, 0xa0, 0x10, 0x40, 0x00, 0x00, 0x00, 0xa0, 0x11, 0x07, 0x00, 0x00, 0x00, 0xa5, 0xa3");
/*
mov r1, 0x00000000
mov r2, 0x12
call vm_read ; 輸入
mov r1, input[0]
mov r2, 0x29
xor r1, r2 ; 異或
cmp r1, flag[0] ; 比較
jnz ERROR ; 如果不相同就跳轉到輸出錯誤的程式碼
; 同上
mov r1, input[1]
xor r1, r2
cmp r1, flag[1]
jnz ERROR
mov r1, input[2]
xor r1, r2
cmp r1, flag[2]
jnz ERROR
mov r1, input[3]
xor r1, r2
cmp r1, flag[3]
jnz ERROR
mov r1, input[4]
xor r1, r2
cmp r1, flag[4]
jnz ERROR
mov r1, input[5]
xor r1, r2
cmp r1, flag[5]
jnz ERROR
mov r1, input[6]
xor r1, r2
cmp r1, flag[6]
jnz ERROR
mov r1, input[7]
xor r1, r2
cmp r1, flag[7]
jnz ERROR
*/
}
/*
* xor 指令解釋函式
*/
void vm_xor(vm_processor *proc)
{
// 異或的兩個資料分別存放在r1,r2暫存器中
int arg1 = proc->r1;
int arg2 = proc->r2;
// 異或結果存在r1中
proc->r1 = arg1 ^ arg2;
// xor指令只佔一個位元組,所以解釋後,eip向後移動一個位元組
proc->eip += 1;
}
/*
* cmp 指令解釋函式
*/
void vm_cmp(vm_processor *proc)
{
// 比較的兩個資料分別存放在r1和buffer中
int arg1 = proc->r1;、
// 位元組碼中包含了buffer的偏移
char *arg2 = *(proc->eip + 1) + heap_buf;
// 比較並對flag暫存器置位,1為相等,0為不等
if (arg1 == *arg2) {
proc->flag = 1;
} else {
proc->flag = 0;
}
// cmp指令佔兩個位元組,eip向後移動2個位元組
proc->eip += 2;
}
/*
* jnz 指令解釋函式
*/
void vm_jnz(vm_processor *proc)
{
// 獲取位元組碼中需要的地址相距eip當前地址的偏移
unsigned char arg1 = *(proc->eip + 1);
// 透過比較flag的值來判斷之前指令的結果,如果flag為零說明之前指令不想等,jnz跳轉實現
if (proc->flag == 0) {
// 跳轉可以直接修改eip,偏移就是上面獲取到的偏移
proc->eip += arg1;
} else {
proc->flag = 0;
}
// jnz 指令佔2個位元組,所以eip向後移動兩個位元組
proc->eip += 2;
}
/*
* ret 指令解釋函式
*/
void vm_ret(vm_processor *proc)
{
}
/*
* read 系統呼叫解釋函式
*/
void vm_read(vm_processor *proc)
{
// read系統呼叫有兩個引數,分別存放在r1,r2暫存器中,r1中是儲存讀入資料的buf的偏移,r2為希望讀入的長度
char *arg2 = heap_buf + proc->r1;
int arg3 = proc->r2;
// 直接呼叫read
read(0, arg2, arg3);
// read系統呼叫佔1個位元組,所以eip向後移動1個位元組
proc->eip += 1;
}
/*
* write 系統呼叫解釋函式
*/
void vm_write(vm_processor *proc)
{
// 與read系統呼叫相同,r1中是儲存寫出資料的buf的偏移,r2為希望寫出的長度
char *arg2 = heap_buf + proc->r1;
int arg3 = proc->r2;
// 直接呼叫write
write(1, arg2, arg3);
// write系統呼叫佔1個位元組,所以eip向後移動1個位元組
proc->eip += 1;
}
/*
* mov 指令解釋函式
*/
void vm_mov(vm_processor *proc)
{
// mov 指令兩個引數都隱含在位元組碼中了,指令標識後的第一個位元組是暫存器的標識,指令標識後的第二到第五個位元組是要mov的立即數,目前只實現了mov一個立即數到一個暫存器中和mov一個buffer中的內容到一個r1暫存器
unsigned char *dest = proc->eip + 1;
int *src = (int *) (proc->eip + 2);
// 前4個case分別對應r1~r4,最後一個case中,*src儲存的是buffer的一個偏移,實現了把buffer中的一個位元組賦值給r1
switch (*dest) {
case 0x10:
proc->r1 = *src;
break;
case 0x11:
proc->r2 = *src;
break;
case 0x12:
proc->r3 = *src;
break;
case 0x13:
proc->r4 = *src;
break;
case 0x14:
proc->r1 = *(heap_buf + *src);
break;
}
// mov指令佔6個位元組,所以eip向後移動6個位元組
proc->eip += 6;
}
/*
* 執行位元組碼
*/
void exec_opcode(vm_processor *proc)
{
int flag = 0;
int i = 0;
// 查詢eip指向的正在解釋的位元組碼對應的處理函式
while (!flag && i < OPCODE_NUM) {
if (*proc->eip == proc->op_table[i].opcode) {
flag = 1;
// 查詢到之後,呼叫本條指令的處理函式,由處理函式來解釋
proc->op_table[i].func((void *) proc);
} else {
i++;
}
}
}
/*
* 虛擬機器的直譯器
*/
void vm_interp(vm_processor *proc)
{
/* eip指向被保護程式碼的第一個位元組
* target_func + 4是為了跳過編譯器生成的函式入口的程式碼
*/
proc->eip = (unsigned char *) target_func + 4;
// 迴圈判斷eip指向的位元組碼是否為返回指令,如果不是就呼叫exec_opcode來解釋執行
while (*proc->eip != RET) {
exec_opcode(proc);
}
}
/*
* 初始化虛擬機器處理器
*/
void init_vm_proc(vm_processor *proc)
{
proc->r1 = 0;
proc->r2 = 0;
proc->r3 = 0;
proc->r4 = 0;
proc->flag = 0;
// 把指令位元組碼與解釋函式關聯起來
proc->op_table[0].opcode = MOV;
proc->op_table[0].func = (void (*)(void *)) vm_mov;
proc->op_table[1].opcode = XOR;
proc->op_table[1].func = (void (*)(void *)) vm_xor;
proc->op_table[2].opcode = CMP;
proc->op_table[2].func = (void (*)(void *)) vm_cmp;
proc->op_table[3].opcode = SYS_READ;
proc->op_table[3].func = (void (*)(void *)) vm_read;
proc->op_table[4].opcode = SYS_WRITE;
proc->op_table[4].func = (void (*)(void *)) vm_write;
proc->op_table[5].opcode = RET;
proc->op_table[5].func = (void (*)(void *)) vm_ret;
proc->op_table[6].opcode = JNZ;
proc->op_table[6].func = (void (*)(void *)) vm_jnz;
// 建立buffer
heap_buf = (char *) malloc(HEAP_SIZE_MAX);
// 初始化buffer
memcpy(heap_buf + 0x20, "syclover", 8);
memcpy(heap_buf + 0x30, "success!\n", 9);
memcpy(heap_buf + 0x40, "error!\n", 7);
}
// flag: ZPJEF_L[
int main()
{
vm_processor proc = {0};
// initial vm processor
init_vm_proc(&proc);
// execute target func
vm_interp(&proc);
return 0;
}
0x08 總結
以上程式為學習程式碼虛擬化之後的總結,其中有很多理解不正確的地方希望大牛指正。這只是最簡單的實現,僅用於學習使用,想要深入學習虛擬化技術還是非常複雜,需要積累更多知識才能理解到位,這篇文章就當是拋磚引玉。在學習的過程也有很多問題沒有解決,比如:如果想實現一個基於虛擬機器的保護殼,必定需要把源程式中的native指令首先轉換為自定義位元組碼,但是不知道用什麼方法來轉換比較好。
在很多國外文章裡也看到另一種虛擬機器保護,是基於LLVM-IR的虛擬機器保護,有興趣也可以繼續深入研究一下。
0x09 參考
http://www.cs.rhul.ac.uk/home/kinder/papers/wcre12.pdf
相關文章
- Python動態規劃實現虛擬機器部署2021-08-01Python動態規劃虛擬機
- 虛擬主機php程式碼實現強制https2021-01-31PHPHTTP
- 使用DiskGenius工具來實現物理機遷移虛擬機器,實現虛擬化2024-04-30虛擬機
- 手動在虛擬機器之間建立信任2018-09-11虛擬機
- 深入理解python虛擬機器:偵錯程式實現原理與原始碼分析2023-04-26Python虛擬機原始碼
- Ubuntu 20.04.2 KVM虛擬機器動態遷移實現(下)2021-07-26Ubuntu虛擬機
- Ubuntu 20.04.2 KVM虛擬機器動態遷移實現(上)2021-07-22Ubuntu虛擬機
- Python 如何實現以太坊虛擬機器2018-10-18Python虛擬機
- Dalvik虛擬機器、Java虛擬機器與ART虛擬機器2018-08-22虛擬機Java
- ida 在虛擬機器中實現linuxremote debugging2020-11-05虛擬機LinuxREM
- 深入理解 python 虛擬機器:原來虛擬機器是這麼實現閉包的2023-10-07Python虛擬機
- java虛擬機器和Dalvik虛擬機器2020-04-04Java虛擬機
- Android 虛擬機器 Vs Java 虛擬機器2018-12-30Android虛擬機Java
- 深入理解虛擬機器之虛擬機器位元組碼執行引擎2018-05-12虛擬機
- 深入學習Java虛擬機器——虛擬機器位元組碼執行引擎2018-08-31Java虛擬機
- Java虛擬機器是怎麼實現synchronized的2018-10-13Java虛擬機synchronized
- 一機實現All in one,NAS如何玩轉虛擬機器!2024-05-28虛擬機
- 虛擬機器(三)虛擬機器配置靜態Ip2024-04-16虛擬機
- 分享Python以太坊虛擬機器實現Py-EVM2019-02-16Python虛擬機
- vmware虛擬機器linux重置密碼2020-12-26虛擬機Linux密碼
- Flutter之Dart虛擬機器啟動2020-05-05FlutterDart虛擬機
- 雲端自動化虛擬機器2022-05-13虛擬機
- Golang實現JAVA虛擬機器-指令集和直譯器2024-01-11GolangJava虛擬機
- PD虛擬機器 18 for Mac(Mac虛擬機器軟體)2022-09-15虛擬機Mac
- 深入理解 Python 虛擬機器:整型(int)的實現原理及原始碼剖析2023-03-13Python虛擬機原始碼
- 深入理解 Python 虛擬機器:列表(list)的實現原理及原始碼剖析2023-03-08Python虛擬機原始碼
- 深入理解Java虛擬機器(程式編譯與程式碼優化)2019-06-29Java虛擬機編譯優化
- JVM 虛擬機器2020-07-16JVM虛擬機
- JVM虛擬機器2019-03-21JVM虛擬機
- Neo 虛擬機器2018-12-27虛擬機
- VMware虛擬機器2024-08-30虛擬機
- 虛擬機器arm虛擬環境搭建2018-09-08虛擬機
- Virtualbox 虛擬機器實現與本地、網際網路互通2024-03-29虛擬機
- Golang實現JAVA虛擬機器-執行時資料區2023-12-25GolangJava虛擬機
- xshell能ping通虛擬機器,不能連線虛擬機器2018-08-24虛擬機
- 虛擬機器軟體Parallels Desktop 19 for Mac虛擬機器 19.0.02023-10-09虛擬機ParallelMac
- 虛擬機器位元組碼執行引擎2018-03-29虛擬機
- VirtualBox虛擬機器U盤啟動方法2018-08-31虛擬機