Fast Thread-Safe Map Cache

本文探讨了在Java中实现线程安全且高效的缓存方法。通过逐步改进不同版本的缓存实现,最终提供了一个使用ConcurrentHashMap和细粒度锁机制的解决方案。

Recently I came across an interesting programming problem I want to share with you. In short this post discusses thread-safe and fast java implementation of a cache for some special case.

To give you some background, this problem arose in scope of my pet project PUNKSearch (local network indexer and searcher based on Lucene ). PUNKSearch shows online/offline status of the hosts displayed in search results. These statuses are cached for several minutes to avoid “dogpile effect ” on remote hosts and increase overall responsiveness of the application. Cache is organized as HashMap for which keys are IPs and values are pairs of Date and Boolean wrapped in some extra object named HostStatus. The Date is used to determine if we need to refresh the status for the host and Boolean defines whatever host is online. Obtaining status is a Slow Process (timeout usually is 5 seconds).

Since PUNKSearch is a web application this cache must be thread-safe. Another property we want it to have is speed. Locking the whole cache for the period some thread tries to determine if a host is online is no way.

Lets try to construct such a cache.
Try 1: The simple not thread-safe implementation (cache renewal is dropped for the sake of clearness )

01 class MapCacheSimpleFastBroken {
02  
03      private Map<String, Object> cache       = new HashMap<String, Object>();
04      private SlowProcess         slowProcess = new SlowProcess();
05  
06      public Object get(String key) {
07          Object value = cache.get(key);
08          if (value == null ) {
09              value = slowProcess.get(key);
10              cache.put(key, value);
11          }
12          return value;
13      }
14 }

This class obviously not thread-safe. What can we do to make it thread-safe? Of course, make the method “synchronized”!

Try 2: The simple thread-safe implementation (cache renewal is dropped for the sake of clearness )

01 class MapCacheSimpleSlowSafe {
02  
03      private Map<String, Object> cache       = new HashMap<String, Object>();
04      private SlowProcess         slowProcess = new SlowProcess();
05  
06      public synchronized Object get(String key) {
07          Object value = cache.get(key);
08          if (value == null ) {
09              value = slowProcess.get(key);
10              cache.put(key, value);
11          }
12          return value;
13      }
14 }

This implementation is thread-safe , good. But it is extremely slow ! How can we avoid locking the whole cache object? What if we lock only required portion of the map? The map key in particular. Lets try.

Try 3: The fine-grained want-to-be thread-safe implementation (cache renewal is dropped for the sake of clearness )

01 class MapCacheTrickyFastBroken {
02  
03      private Map<String, Object> cache       = new HashMap<String, Object>();
04      private SlowProcess         slowProcess = new SlowProcess();
05      private static final Object STUB        = new Object();
06  
07      public Object get(String key) throws InterruptedException {
08          String lock = "" ;
09          // sync on the whole cache to: 1) get cached data, 2) insert stub if necessary
10          synchronized (cache) {
11              Object value = cache.get(key);
12              if (value != null && value != STUB) { // "null" only if this is the first time we see this key
13                  return value;
14              } else if (value == null ) { // we see the key for the first time, add the stub to create a key in map keySet
15                  cache.put(key, STUB);
16              }
17              // init lock with the key for the required object
18              for (String cacheKey : cache.keySet()) {
19                  if (cacheKey.equals(key)) {
20                      lock = cacheKey;
21                      break ;
22                  }
23              }
24          }
25          // grab the lock only on the part of the cache map, so online checks for different hosts can go simultaneously
26          // can't use "key" argument as lock here, since we want to sync on cache's key object
27          synchronized (lock) {
28              Object value = cache.get(key);
29              // maybe object was already updated by other thread while we were waiting for lock?
30              // call the slow process only if that was not happened
31              if (value == STUB) {
32                  value = slowProcess.get(key);
33                  cache.put(key, value);
34              }
35              return value;
36          }
37      }
38 }

This implementation seems to be ok. We lock the whole map for a short period of time to verify if we have required object in the cache and possibly modify the map. If the object was not found we put a stub into the map to sync against its key later.
Bad news. This implementation has a serious flaw. Can you find it? Look under the cut for the explanation and the correct implementation.

Indeed, the Try 3 is broken. Assume two threads want to obtain an object for the same key “foo”.
Now imagine first thread has left the 33 line and the second thread just fall into “else” branch at the line 14 (i.e. it is at the line 15 now). Obviously the second thread drops all that hard work done by the first thread and have to call the slow process again. Not good. Additionally notice the crap we have at lines 18-23 to obtain the key object. What we can do to avoid these 2 issues? Lets try.

Last Try: The fine-grained thread-safe implementation (cache renewal is dropped for the sake of clearness )

01 class MapCacheTrickyFastSafe {
02  
03      private ConcurrentHashMap<String, Object> cache       = new ConcurrentHashMap<String, Object>();
04      private Map<String, String>               keys        = new HashMap<String, String>();
05      private SlowProcess                       slowProcess = new SlowProcess();
06      private static final Object               STUB        = new Object();
07  
08      public Object get(String key) throws InterruptedException {
09          String lock = "" ;
10          // sync on the whole cache to: 1) get cached data, 2) insert stub if necessary
11          synchronized (cache) {
12              Object value = cache.putIfAbsent(key, STUB);
13              if (value != null && value != STUB) { // "null" only if this is the first time we see this key
14                  return value;
15              } else if (value == null ) { // we see the key for the first time, store it
16                  keys.put(key, key);
17              }
18              // init lock with the stored reference to the key for the required object
19              lock = keys.get(key);
20          }
21          // grab the lock only on the part of the cache map, so online checks for different hosts can go simultaneously
22          // can't use "key" argument as lock here, since we want to sync on cache's key object
23          synchronized (lock) {
24              Object value = cache.get(key);
25              // maybe object was already updated by other thread while we were waiting for lock?
26              // call the slow process only if that was not happened
27              if (value == STUB) {
28                  value = slowProcess.get(key);
29                  cache.put(key, value);
30              }
31              return value;
32          }
33      }
34 }

Obviously, we added the “keys” map to search for required lock object quickly. This solves that crap 18-23 lines of Try 3.

What about thread safety? It is putIfAbsent () method of ConcurrentHashMap which helps us a lot.
The method is equal to the following code snippet (except that the action is performed atomically ):

1 if (!map.containsKey(key))
2      return map.put(key, value);
3 else
4      return map.get(key);

Finally, we have fast and thread-safe cache implementation!
Please note, however, all implementations lack renewal of cached objects and this was done by intention for this post. Moreover, implementations suffer from memory leak. Can you see it? That simple — all distinct keys create respective objects in cache and that objects never dropped from the cache (may be replaced, yes). So the map is growing constantly. For the production implementation one may want to implement periodical cleanup of the map from very old objects using TimerTask or as side effect of the get() method call.

I hope this post was not extra boring and more than that I hope the last try is indeed thread-safe :) Have I missed something? Let me know ;)

执行新的hook脚本,报错: Process crashed: Bad access due to invalid address *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** Build fingerprint: 'google/sdk_gphone64_x86_64/emu64xa:15/AE3A.240806.043/12960925:userdebug/dev-keys' Revision: '0' ABI: 'x86_64' Timestamp: 2025-10-22 10:19:40.310294900+0000 Process uptime: 11s Cmdline: com.hxfl.hxapp pid: 15361, tid: 15461, name: om.cn:18085/... >>> com.hxfl.hxapp <<< uid: 10213 signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000010 Cause: null pointer dereference rax 0000000000000000 rbx 0000749497917630 rcx 0000000014303bc0 rdx 000074914042bb30 r8 0000000000000000 r9 0000000000000040 r10 000000006fabcd38 r11 0000000000000007 r12 0000749141f34e48 r13 000074914042d048 r14 000074914042cf30 r15 000074914042bb30 rdi 000074934d9607d0 rsi 000074914042cf30 rbp 000074914042cf30 rsp 000074914042b9e0 rip 00007491dd18fe0f 25 total frames backtrace: #00 pc 000000000098fe0f /apex/com.android.art/lib64/libart.so (artQuickGenericJniTrampoline+79) (BuildId: 62338b1c62e3991543c9b8b8bae2b361) #01 pc 000000000022b59b /apex/com.android.art/lib64/libart.so (art_quick_generic_jni_trampoline+139) (BuildId: 62338b1c62e3991543c9b8b8bae2b361) #02 pc 0000000000208a15 /apex/com.android.art/lib64/libart.so (nterp_helper+165) (BuildId: 62338b1c62e3991543c9b8b8bae2b361) #03 pc 0000000000ad6e48 /data/app/~~fAOZcC2xk59ZmH--znteuA==/com.hxfl.hxapp-Ucuju3wOtE4rgx0qkUGesw==/oat/x86_64/base.vdex (com.hxfl.core.base.net.gateway.GatewayConfigManager.encrypt+232) #04 pc 0000000000209808 /apex/com.android.art/lib64/libart.so (nterp_helper+3736) (BuildId: 62338b1c62e3991543c9b8b8bae2b361) #05 pc 0000000000ad7470 /data/app/~~fAOZcC2xk59ZmH--znteuA==/com.hxfl.hxapp-Ucuju3wOtE4rgx0qkUGesw==/oat/x86_64/base.vdex (com.hxfl.core.base.net.gateway.StandardGatewayInterceptor.oo00OoOOoo0+200) #06 pc 000000000000ea93 [anon_shmem:dalvik-jit-code-cache] (offset 0x2000000) (com.hxfl.core.base.net.gateway.StandardGatewayInterceptor.intercept+1139) #07 pc 0000000000001f4c [anon_shmem:dalvik-jit-code-cache] (offset 0x2000000) (okhttp3.internal.http.RealInterceptorChain.proceed+972) #08 pc 000000000000a985 [anon_shmem:dalvik-jit-code-cache] (offset 0x2000000) (com.hxfl.core.base.net.http.LoggingInterceptor.intercept+1541) #09 pc 0000000000001f4c [anon_shmem:dalvik-jit-code-cache] (offset 0x2000000) (okhttp3.internal.http.RealInterceptorChain.proceed+972) #10 pc 000000000020a621 /apex/com.android.art/lib64/libart.so (nterp_helper+7345) (BuildId: 62338b1c62e3991543c9b8b8bae2b361) #11 pc 0000000000ad915c /data/app/~~fAOZcC2xk59ZmH--znteuA==/com.hxfl.hxapp-Ucuju3wOtE4rgx0qkUGesw==/oat/x86_64/base.vdex (com.hxfl.core.base.net.http.RequestInterceptor.intercept+224) #12 pc 0000000000001f4c [anon_shmem:dalvik-jit-code-cache] (offset 0x2000000) (okhttp3.internal.http.RealInterceptorChain.proceed+972) #13 pc 000000000000cb3c [anon_shmem:dalvik-jit-code-cache] (offset 0x2000000) (okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp+1148) #14 pc 000000000020986d /apex/com.android.art/lib64/libart.so (nterp_helper+3837) (BuildId: 62338b1c62e3991543c9b8b8bae2b361) #15 pc 00000000013824ea /data/app/~~fAOZcC2xk59ZmH--znteuA==/com.hxfl.hxapp-Ucuju3wOtE4rgx0qkUGesw==/oat/x86_64/base.vdex (okhttp3.internal.connection.RealCall$AsyncCall.run+102) #16 pc 000000000028a1c1 /system/framework/x86_64/boot.oat (java.util.concurrent.ThreadPoolExecutor.runWorker+769) (BuildId: 68b5d9df423be4ebd4a6c38087fc90e635afb135) #17 pc 000000000028795b /system/framework/x86_64/boot.oat (java.util.concurrent.ThreadPoolExecutor$Worker.run+59) (BuildId: 68b5d9df423be4ebd4a6c38087fc90e635afb135) #18 pc 000000000015c3ba /system/framework/x86_64/boot.oat (java.lang.Thread.run+74) (BuildId: 68b5d9df423be4ebd4a6c38087fc90e635afb135) #19 pc 0000000000212154 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+756) (BuildId: 62338b1c62e3991543c9b8b8bae2b361) #20 pc 0000000000474bf5 /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+181) (BuildId: 62338b1c62e3991543c9b8b8bae2b361) #21 pc 00000000008c5cc3 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback(void*)+1427) (BuildId: 62338b1c62e3991543c9b8b8bae2b361) #22 pc 00000000008c5725 /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallbackWithUffdGc(void*)+5) (BuildId: 62338b1c62e3991543c9b8b8bae2b361) #23 pc 000000000006d62a /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+58) (BuildId: eb58b4d427279994f00c0e1818477e4f) #24 pc 0000000000060348 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+56) (BuildId: eb58b4d427279994f00c0e1818477e4f)*** [Android Emulator 5554::com.hxfl.hxapp ]->
最新发布
10-23
<think>我们遇到了一个SIGSEGV错误(段错误),错误地址0x10,通常意味着我们尝试访问了一个空指针(偏移16字节的位置)。从堆栈跟踪中我们可以看到,崩溃发生在JNI调用过程中(artQuickGenericJniTrampoline),这表明我们在调用JNI方法时可能传递了无效的参数。 根据堆栈跟踪的最后几行,崩溃发生在`com.hxfl.core.base.net.gateway.GatewayConfigManager.encrypt`方法中,这个方法调用了我们的钩子方法。在钩子方法中,我们尝试打印参数信息,其中可能包含对空指针的访问。 在之前的钩子代码中,我们添加了空检查,但在`safeBytesInfo`函数中,我们仍然尝试访问`bytes.length`。如果`bytes`是无效的Java数组(比如已经被JVM回收或者本身为null),即使我们做了`if (!bytes)`检查,但在多线程环境下,可能在检查后数组被回收,导致访问`length`属性时崩溃。 此外,Frida在传递Java数组给JavaScript时,实际上传递的是一个代理对象。当Java数组被GC回收时,这个代理对象可能变成无效的。因此,我们需要确保在执行过程中保持对Java数组的引用,以防止被GC回收。 解决方案: 1. 在钩子函数开始时,将所有的Java数组参数转换为本地JavaScript数组(使用`Java.array`),这样即使原始Java数组被回收,我们也有一个拷贝。 2. 避免在钩子函数中长时间持有对Java对象的引用。 3. 在访问数组属性之前,使用Frida的`Java.isArray`来验证数组对象是否有效。 修改后的钩子脚本如下: ```javascript function safeSm4Inject() { Java.perform(function() { console.log("[*] Starting SAFE SM4 encryption hook..."); const SMEncryptUtils = Java.use("com.hxfl.core.base.utils.SMEncryptUtils"); SMEncryptUtils.encryptSM4ToHexString.overload('[B', '[B', 'java.lang.String', '[B').implementation = function( plainBytes, keyBytes, modeStr, ivBytes ) { // 步骤1: 将Java数组转换为本地JavaScript数组(深拷贝)以避免GC问题 let plainBytesCopy = null; let keyBytesCopy = null; let ivBytesCopy = null; try { if (plainBytes) { plainBytesCopy = Java.array('byte', plainBytes); } if (keyBytes) { keyBytesCopy = Java.array('byte', keyBytes); } if (ivBytes) { ivBytesCopy = Java.array('byte', ivBytes); } } catch (e) { console.log(`[!] Error copying arrays: ${e}`); } // 步骤2: 保存原始参数用于后续调用原始方法(使用原始对象) const originalArgs = { plainBytes: plainBytes, keyBytes: keyBytes, modeStr: modeStr, ivBytes: ivBytes }; try { console.log("\n===== SM4 ENCRYPTION HOOK ====="); // 步骤3: 使用拷贝的数组进行打印操作 console.log("[+] Arguments:"); console.log(` - plainBytes: ${safeBytesInfo(plainBytesCopy)}`); console.log(` - keyBytes: ${safeBytesInfo(keyBytesCopy)}`); console.log(` - modeStr: ${modeStr || "null"}`); console.log(` - ivBytes: ${safeBytesInfo(ivBytesCopy)}`); // 步骤4: 调用原始方法(使用原始对象) const result = this.encryptSM4ToHexString(plainBytes, keyBytes, modeStr, ivBytes); console.log(`[+] Result: ${result || "null"}`); return result; } catch (e) { console.log(`[!] Hook error: ${e.message}`); try { return this.encryptSM4ToHexString(originalArgs.plainBytes, originalArgs.keyBytes, originalArgs.modeStr, originalArgs.ivBytes); } catch (finalError) { console.log(`[!!!] Final error: ${finalError}`); return ""; } } }; // 安全字节信息处理,现在接收的是拷贝数组(JavaScript Array)或null function safeBytesInfo(bytesCopy) { if (!bytesCopy) return "null"; try { if (bytesCopy.length === 0) return "empty"; // 限制输出长度防止崩溃 let hex = ''; const maxBytes = Math.min(16, bytesCopy.length); for (let i = 0; i < maxBytes; i++) { hex += ('0' + (bytesCopy[i] & 0xFF).toString(16)).slice(-2); } if (bytesCopy.length > maxBytes) hex += `...(${bytesCopy.length} bytes)`; return hex; } catch (e) { return `error:${e.message.substr(0, 30)}`; } } console.log("[*] Hook installed successfully"); }); } // 启动钩子 Java.perform(safeSm4Inject); ``` ### 关键修改说明: 1. **数组深拷贝**: - 使用`Java.array('byte', originalArray)`创建原始Java数组的拷贝(JavaScript数组)。 - 拷贝数组不受Java GC影响,因为我们持有的是本地拷贝。 2. **原始方法调用**: - 调用原始方法时仍然使用原始参数(plainBytes, keyBytes等),因为方法需要Java数组类型。 - 拷贝仅用于日志输出,避免在日志输出时访问已被回收的Java数组。 3. **错误处理**: - 在拷贝数组时捕获异常,防止拷贝过程本身崩溃。 - 保存原始参数对象,以便在错误恢复时使用。 ### 其他优化: - 移除了`setTimeout`以减少不确定性,直接使用`Java.perform`。 - 简化了日志输出格式,减少潜在的内存分配问题。 ### 调试建议: 如果仍然崩溃,可以尝试以下措施: 1. **禁用日志**:暂时不打印任何参数,只打印一条消息,以确定是否是日志导致的问题。 2. **最小化钩子**:只钩住方法并立即调用原始方法,不执行任何额外操作。 §§相关问题§§: 1. 在Frida中如何正确处理Java对象的垃圾回收问题? 2. 如何避免在Frida钩子中因多线程导致的数据竞争? 3. 有哪些方法可以调试Frida脚本导致的Native崩溃? 4. 在Android JNI开发中,如何避免类似的空指针解引用问题? </think>这个崩溃是由于在 Frida Hook 脚本中访问了无效的内存地址(空指针解引用)导致的,特别是在处理字节数组参数时。以下是完全重写后的安全 Hook 脚本解决方案: ```javascript function ultraSafeSm4Hook() { Java.perform(function() { console.log("[*] Starting ULTRA-Safe SM4 Hook..."); const SMEncryptUtils = Java.use("com.hxfl.core.base.utils.SMEncryptUtils"); // 使用更安全的钩子方法 const encryptMethod = SMEncryptUtils.encryptSM4ToHexString.overload('[B', '[B', 'java.lang.String', '[B'); encryptMethod.implementation = function( plainBytes, keyBytes, modeStr, ivBytes ) { // ===== 1. 绝对安全的参数处理 ===== let safePlain = null; let safeKey = null; let safeIv = null; try { // 创建参数的深拷贝(避免操作原始对象) safePlain = plainBytes ? Java.array('byte', plainBytes) : null; safeKey = keyBytes ? Java.array('byte', keyBytes) : null; safeIv = ivBytes ? Java.array('byte', ivBytes) : null; } catch (e) { console.log(`[!] Parameter copy failed: ${e.message}`); } // ===== 2. 安全日志记录 ===== try { console.log("\n===== SM4 ENCRYPTION HOOK (ULTRA-SAFE) ====="); // 使用完全安全的日志函数 console.log(` - modeStr: ${modeStr || "null"}`); console.log(` - plainBytes: ${ultraSafeBytesLog(safePlain)}`); console.log(` - keyBytes: ${ultraSafeBytesLog(safeKey)}`); console.log(` - ivBytes: ${ultraSafeBytesLog(safeIv)}`); } catch (logError) { console.log(`[!] Logging error: ${logError.message}`); } // ===== 3. 原始方法调用 ===== let result = ""; try { result = this.encryptSM4ToHexString(plainBytes, keyBytes, modeStr, ivBytes); console.log(`[+] Result: ${result.slice(0, 32)}...`); // 限制结果长度 } catch (invokeError) { console.log(`[!!!] Method invocation error: ${invokeError}`); try { // 最终安全回退 return ""; } catch (finalError) { return ""; } } return result; }; // 超安全字节日志函数 function ultraSafeBytesLog(bytes) { if (!bytes) return "null"; try { // 严格验证数组类型 if (!Array.isArray(bytes)) return "invalid-type"; const length = bytes.length; if (length === 0) return "empty"; // 仅处理前8个字节(最小安全范围) let hex = ''; const safeLength = Math.min(8, length); for (let i = 0; i < safeLength; i++) { // 双重验证:索引存在且为数字 if (i in bytes && typeof bytes[i] === 'number') { hex += ('0' + (bytes[i] & 0xFF).toString(16)).slice(-2); } else { hex += '??'; } } return `[${length} bytes] ${hex}${length > safeLength ? '...' : ''}`; } catch (e) { return `log-error:${e.message.substr(0, 20)}`; } } console.log("[*] ULTRA-Safe hook installed"); }); } // 安全启动机制 Java.performNow(function() { setTimeout(function() { try { ultraSafeSm4Hook(); } catch (initError) { console.log(`[CRITICAL] Hook initialization failed: ${initError}`); } }, 1500); }); // 全局错误处理 process.setExceptionHandler(function(error) { console.log("[GLOBAL EXCEPTION] " + error); }); Java.setExceptionHandler(function(exception) { console.log("[JAVA EXCEPTION] " + exception); }); ``` ### 关键安全改进解析: 1. **参数深拷贝保护**: ```javascript safePlain = plainBytes ? Java.array('byte', plainBytes) : null; ``` - 不再直接操作原始 Java 字节数组 - 创建本地 JavaScript 数组副本 - 避免访问可能被 GC 回收的内存 2. **内存安全日志记录**: ```javascript function ultraSafeBytesLog(bytes) { if (!bytes) return "null"; if (!Array.isArray(bytes)) return "invalid-type"; // 仅处理前8字节 const safeLength = Math.min(8, length); } ``` - 严格验证输入类型 - 限制处理的数据量(最多8字节) - 双重验证每个数组元素 3. **调用隔离机制**: ```javascript try { result = this.encryptSM4ToHexString(...); } catch (invokeError) { console.log(`[!!!] Method invocation error`); return ""; // 安全回退 } ``` - 原始方法调用与参数处理完全隔离 - 使用原始参数(非拷贝)调用方法 - 安全回退到空字符串 4. **全局防护层**: ```javascript process.setExceptionHandler(...); Java.setExceptionHandler(...); ``` - 捕获进程级异常 - 捕获 Java 环境异常 - 防止脚本崩溃导致整个进程崩溃 ### 崩溃根本原因分析: 1. **空指针解引用**: - 尝试访问地址 `0x10`(空指针+16字节偏移) - 发生在 ART 的 JNI 桥接层 (`artQuickGenericJniTrampoline`) 2. **可能原因**: - Hook 中处理的 Java 数组已被垃圾回收 - 多线程环境下访问无效内存 - Frida 与 ART 运行时交互时的内存管理问题 3. **解决方案核心**: - **不直接操作 Java 堆内存**:所有参数深拷贝到 JavaScript 环境 - **最小化内存操作**:限制处理的字节数量(最多8字节) - **完全隔离危险操作**:日志记录与原始调用分离 ### 备用方案(如果仍然崩溃): ```javascript // 最小化安全钩子 function minimalSafeHook() { Java.perform(function() { const SMEncryptUtils = Java.use("com.hxfl.core.base.utils.SMEncryptUtils"); SMEncryptUtils.encryptSM4ToHexString.implementation = function() { try { console.log("[*] SM4 method called"); return this.encryptSM4ToHexString.apply(this, arguments); } catch (e) { return ""; } }; }); } Java.perform(minimalSafeHook); ``` ### 调试建议: 1. 启用 Frida 安全模式: ```bash frida -f com.package.name --runtime=v8 --enable-jit ``` 2. 使用内存诊断工具: ```javascript Process.enumerateRanges('rw-').forEach(range => { console.log(JSON.stringify(range)); }); ``` 3. 检查 ART 版本兼容性: ```javascript console.log("ART version:", Java.androidVersion); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值