在本篇介绍 NullPointerException在 ART种的实现。本篇基础:ART异常处理机制(1) - SIGSEGV信号的拦截和处理
在 ART异常处理机制1 中,我们已经知道了 ART会注册信号处理函数,优先尝试处理 SIGSEGV信号。ART中总共又 4 种 handler 享有优先处理 SIGSEGV信号的权利。
在 StackOverflowHandler 发现处理不了当前的 SIGSEGV后,再交给 NullPointerHandler 尝试处理。本篇主要分析下 NullPointerHandler 以及 NullPointerException 的检测以及抛出。
NullPointerHandler 的实现就在其Action函数当中:
bool NullPointerHandler::Action(int sig ATTRIBUTE_UNUSED, siginfo_t* info, void* context) {
if (!IsValidImplicitCheck(info)) {
return false;
}
// The code that looks for the catch location needs to know the value of the
// ARM PC at the point of call. For Null checks we insert a GC map that is immediately after
// the load/store instruction that might cause the fault. However the mapping table has
// the low bits set for thumb mode so we need to set the bottom bit for the LR
// register in order to find the mapping.
// Need to work out the size of the instruction that caused the exception.
struct ucontext *uc = reinterpret_cast<struct ucontext*>(context);
struct sigcontext *sc = reinterpret_cast<struct sigcontext*>(&uc->uc_mcontext);
uint8_t* ptr = reinterpret_cast<uint8_t*>(sc->arm_pc);
uint32_t instr_size = GetInstructionSize(ptr);
uintptr_t gc_map_location = (sc->arm_pc + instr_size) | 1;
// Push the gc map location to the stack and pass the fault address in LR.
sc->arm_sp -= sizeof(uintptr_t);
*reinterpret_cast<uintptr_t*>(sc->arm_sp) = gc_map_location;
sc->arm_lr = reinterpret_cast<uintptr_t>(info->si_addr);
sc->arm_pc = reinterpret_cast<uintptr_t>(art_quick_throw_null_pointer_exception_from_signal);
// Pass the faulting address as the first argument of
// art_quick_throw_null_pointer_exception_from_signal.
VLOG(signals) << "Generating null pointer exception";
return true;
}
从前面几部分的分析我们可以知道一个ART虚拟机进程收到 SIGSEGV之后,会先经过 StackOverflowHandler 尝试能否处理这个SIGSEGV,假如StackOverflowHandler没有能够处理它的话,那么接下来一个想要尝试身手的就是NullPointerHandler了。与前面的 StackOverflowHandler 类似,它总要先判断下自己这两把刷子能不能刷的掉这个SIGSEGV,否则上来一通乱刷岂不乱套。
这个判断就是通过 IsValidImplicitCheck(info)这个函数来进行的:
static bool IsValidImplicitCheck(siginfo_t* siginfo) {
// Our implicit NPE checks always limit the range to a page.
// Note that the runtime will do more exhaustive checks (that we cannot
// reasonably do in signal processing code) based on the dex instruction
// faulting.
return CanDoImplicitNullCheckOn(reinterpret_cast<uintptr_t>(siginfo->si_addr));
}
这个函数里有说明:隐式的Null Check 限制在一个page之内.
什么意思呢?接着看下去,其调用 CanDoImplicitNullCheckOn函数来判断,其参数 siginfo->si_addr 是指本次发生segment fult 时的 fault_addr,通过 SIGSEGV 信号的 siginfo_t 来传递。下面是其声明:
typedef struct siginfo {
int si_signo;
int si_errno;
int si_code;
....
union {
...
struct {
void __user *_addr; /* faulting insn/memory ref. */
#ifdef __ARCH_SI_TRAPNO
int _trapno; /* TRAP # which caused the signal */
#endif
short _addr_lsb; /* LSB of the reported address */
} _sigfault;
....
} _sifields;
....
} __ARCH_SI_ATTRIBUTES siginfo_t;
#define si_addr _sifields._sigfault._addr
接下来真正判断的函数CanDoImplicitNullCheckOn就使用这个 fault_addr 来判断能否处理这个 SIGSEGV:
// Returns whether the given memory offset can be used for generating
// an implicit null check.
static inline bool CanDoImplicitNullCheckOn(uintptr_t offset) {
return offset < kPageSize;
}
可以看到,判断的情况是,当 fault_addr 小于 4 KB 时,就认为需要处理这个SIGSEGV。需要关注的时,在此之前,已经经过 IsInGeneratedCode()函数判断过当前这个segment fault是否是发生在 generated code。当这两个条件都满足时,就判定为NullPointerException.
那么问题来了:
1.IsInGeneratedCode判断,说明了什么问题 ?
答:说明发生在 generated code中的 SIGSEGV 才会进行隐式的 NullPointer check,那么Interpreter模式的NullPointer Check应该就是另外一回事了。
2.为什么是 4KB,这个数字怎么来的 ?
答:目前还没不知道,但可以先推测一下。一般发生一个 NullPointerException 时,是我们准备访问一个对象的成员变量或者调用其成员函数时抛出的这个Exception。其一,访问成员变量时发生,因为成员变量在内存中的存放是紧挨着该对象的Object指针的,所以这个 4 KB 的意思是,Object 的大小一定是小于 4KB的?所以当发生NullPointer时,由于 Object 地址是0x0,访问成员时的 offset小于 4K,所以fault addr 一定小于 4K?感觉有点问题,Object大小应该可以大于 4KB 的,后续探究。其二,调用成员函数的情况,执行一个java对象的成员函数是这么个流程,先从对象地址处(class的对应对象的offset是0)取出其class地址,然后从改地址的一定偏移取出 ArtMethod地址,然后再从ArtMethod地址偏移几十个字节,获得对应函数的 quick code入口,然后跳转过去执行;那么如果当前object是null,在第一步从对象地址处取出 class 地址时,就会触发 segment fault,此时的 fault_addr = 0 (null) + 0 (offset) =0,显然是小于4K 的。
这两个问题可以留着思考与调查。我们现在假设当前 SIGSEGV 确实是 NullPointer 触发的(fault_addr < 4K),看下其接下来会怎么处理:从NullPointerHandler::Action我们看到,接下来计算出 gc_map_location,并push到栈上,把 fault_addr 赋值给 lr,然后跳转
到 art_quick_throw_null_pointer_exception_from_signal 函数,准备抛出一个 NullPointer 异常:
ENTRY art_quick_throw_null_pointer_exception_from_signal
// The fault handler pushes the gc map address, i.e. "return address", to stack
// and passes the fault address in LR. So we need to set up the CFI info accordingly.
.cfi_def_cfa_offset __SIZEOF_POINTER__
.cfi_rel_offset lr, 0
push {r0-r12} @ 13 words of callee saves and args; LR already saved.
.cfi_adjust_cfa_offset 52
.cfi_rel_offset r0, 0
.cfi_rel_offset r1, 4
.cfi_rel_offset r2, 8
.cfi_rel_offset r3, 12
.cfi_rel_offset r4, 16
.cfi_rel_offset r5, 20
.cfi_rel_offset r6, 24
.cfi_rel_offset r7, 28
.cfi_rel_offset r8, 32
.cfi_rel_offset r9, 36
.cfi_rel_offset r10, 40
.cfi_rel_offset r11, 44
.cfi_rel_offset ip, 48
@ save all registers as basis for long jump context
SETUP_SAVE_EVERYTHING_FRAME_CORE_REGS_SAVED r1
mov r0, lr @ pass the fault address stored in LR by the fault handler.
mov r1, r9 @ pass Thread::Current
bl artThrowNullPointerExceptionFromSignal @ (Thread*)
END art_quick_throw_null_pointer_exception_from_signal
实际跳转到 artThrowNullPointerExceptionFromSignal:
// Installed by a signal handler to throw a NPE exception.
extern "C" NO_RETURN void artThrowNullPointerExceptionFromSignal(uintptr_t addr, Thread* self)
REQUIRES_SHARED(Locks::mutator_lock_) {
ScopedQuickEntrypointChecks sqec(self);
ThrowNullPointerExceptionFromDexPC(/* check_address */ true, addr);
self->QuickDeliverException();
}
这个函数的第一个参数 addr 是 fault addr,所以接下来就根据 addr来创建一个 NullPointerException,然后DeliverException即可.
在 ThrowNullPointerExceptionFromDexPC 中:
- 找到当前正在执行的 ArtMethod,以及正在执行的 dex instruction
- 再次检测是否是真正的NullPointerException:再次检测 addr是否小于4K,以及针对不同数据访问的不同检测,比如IGET,IPUT指令,会检测要访问的成员的field_offset 是否与 fault addr相等
- 检测成功后,抛出NullPointerException,ErrorMessage根据dex instruction 类别来决定,比如instruction是在调用函数,message描述调用函数时发生了NullPointer;如果在访问对象成员,也给出相应的 Message
1.编译器在generated code中安插的 Null Check,举个例子:
编译到 OAT文件中如下:public Account[] getSharedAccounts(UserHandle user) { try { return mService.getSharedAccountsAsUser(user.getIdentifier()); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } }
48: android.accounts.Account[] android.accounts.AccountManager.getSharedAccounts(android.os.UserHandle) (dex_method_idx=608) DEX CODE: 0x0000: 5431 f914 | iget-object v1, v3, Landroid/accounts/IAccountManager; android.accounts.AccountManager.mService // field@5369 0x0002: 6e10 eec8 0400 | invoke-virtual {v4}, int android.os.UserHandle.getIdentifier() // method@51438 0x0005: 0a02 | move-result v2 0x0006: 7220 cd03 2100 | invoke-interface {v1, v2}, android.accounts.Account[] android.accounts.IAccountManager.getSharedAccountsAsUser(int) // method@973 0x0009: 0c01 | move-result-object v1 0x000a: 1101 | return-object v1 0x000b: 0d00 | move-exception v0 0x000c: 6e10 1ec7 0000 | invoke-virtual {v0}, java.lang.RuntimeException android.os.RemoteException.rethrowFromSystemServer() // method@50974 0x000f: 0c01 | move-result-object v1 0x0010: 2701 | throw v1
CODE: (code_offset=0x01ac6ca5 size_offset=0x01ac6ca0 size=154)... 0x01ac6ca4: f5ad5c00 sub r12, sp, #8192 0x01ac6ca8: f8dcc000 ldr.w r12, [r12, #0] StackMap [native_pc=0x1ac6cad] (dex_pc=0x0, native_pc_offset=0x8, dex_register_map_offset=0xffffffff, inline_info_offset=0xffffffff, register_mask=0x0, stack_mask=0b0000000000000000000000) 0x01ac6cac: e92d41e0 push {r5, r6, r7, r8, lr} 0x01ac6cb0: b087 sub sp, sp, #28 0x01ac6cb2: 9000 str r0, [sp, #0] 0x01ac6cb4: 910d str r1, [sp, #52] 0x01ac6cb6: 920e str r2, [sp, #56] 0x01ac6cb8: f8b9c000 ldrh.w r12, [r9, #0] ; state_and_flags 0x01ac6cbc: f1bc0f00 cmp.w r12, #0 0x01ac6cc0: d12c bne +88 (0x01ac6d1c) 0x01ac6cc2: 698d ldr r5, [r1, #24] 0x01ac6cc4: b392 cbz r2, +100 (0x01ac6d2c) 0x01ac6cc6: 460e mov r6, r1 0x01ac6cc8: 4611 mov r1, r2 0x01ac6cca: 4617 mov r7, r2 0x01ac6ccc: 6808 ldr r0, [r1, #0] 0x01ac6cce: f8d000b8 ldr.w r0, [r0, #184] 0x01ac6cd2: f8d0e020 ldr.w lr, [r0, #32] 0x01ac6cd6: 47f0 blx lr StackMap [native_pc=0x1ac6cd9] (dex_pc=0x2, native_pc_offset=0x34, dex_register_map_offset=0x0, inline_info_offset=0xffffffff, register_mask=0xe0, stack_mask=0b0000000110000000000000) v1: in register (5) [entry 0] v3: in register (6) [entry 1] v4: in register (7) [entry 2] 0x01ac6cd8: b36d cbz r5, +90 (0x01ac6d36) 0x01ac6cda: 4629 mov r1, r5 0x01ac6cdc: 4602 mov r2, r0 0x01ac6cde: 4680 mov r8, r0 0x01ac6ce0: f2403ccd movw r12, #973 0x01ac6ce4: 6808 ldr r0, [r1, #0] 0x01ac6ce6: f8d00084 ldr.w r0, [r0, #132] 0x01ac6cea: 6b40 ldr r0, [r0, #52] 0x01ac6cec: f8d0e020 ldr.w lr, [r0, #32] 0x01ac6cf0: 47f0 blx lr StackMap [native_pc=0x1ac6cf3] (dex_pc=0x6, native_pc_offset=0x4e, dex_register_map_offset=0x3, inline_info_offset=0xffffffff, register_mask=0xe0, stack_mask=0b0000000110000000000000) v1: in register (5) [entry 0] v2: in register (8) [entry 3] v3: in register (6) [entry 1] v4: in register (7) [entry 2] 0x01ac6cf2: e010 b +32 (0x01ac6d16) StackMap [native_pc=0x1ac6cf5] (dex_pc=0xb, native_pc_offset=0x50, dex_register_map_offset=0xffffffff, inline_info_offset=0xffffffff, register_mask=0x0, stack_mask=0b0000000000000000000000) 0x01ac6cf4: f8d91084 ldr.w r1, [r9, #132] ; exception 0x01ac6cf8: f04f0c00 mov.w r12, #0 0x01ac6cfc: f8c9c084 str.w r12, [r9, #132] 0x01ac6d00: 460d mov r5, r1 0x01ac6d02: 6808 ldr r0, [r1, #0] 0x01ac6d04: f8d000e8 ldr.w r0, [r0, #232] 0x01ac6d08: f8d0e020 ldr.w lr, [r0, #32] 0x01ac6d0c: 47f0 blx lr StackMap [native_pc=0x1ac6d0f] (dex_pc=0xc, native_pc_offset=0x6a, dex_register_map_offset=0x6, inline_info_offset=0xffffffff, register_mask=0x20, stack_mask=0b0000000110000000000000) v0: in register (5) [entry 0] v3: in stack (52) [entry 4] v4: in stack (56) [entry 5] 0x01ac6d0e: 4606 mov r6, r0 0x01ac6d10: f8d9e2ac ldr.w lr, [r9, #684] ; pDeliverException 0x01ac6d14: 47f0 blx lr StackMap [native_pc=0x1ac6d17] (dex_pc=0x10, native_pc_offset=0x72, dex_register_map_offset=0x9, inline_info_offset=0xffffffff, register_mask=0x60, stack_mask=0b0000000110000000000000) v0: in register (5) [entry 0] v1: in register (6) [entry 1] v3: in stack (52) [entry 4] v4: in stack (56) [entry 5] 0x01ac6d16: b007 add sp, sp, #28 0x01ac6d18: e8bd81e0 pop {r5, r6, r7, r8, pc} 0x01ac6d1c: 9103 str r1, [sp, #12] 0x01ac6d1e: 9204 str r2, [sp, #16] 0x01ac6d20: f8d9e2a8 ldr.w lr, [r9, #680] ; pTestSuspend 0x01ac6d24: 47f0 blx lr StackMap [native_pc=0x1ac6d27] (dex_pc=0x0, native_pc_offset=0x82, dex_register_map_offset=0xc, inline_info_offset=0xffffffff, register_mask=0x0, stack_mask=0b0000000110000000011000) v3: in stack (12) [entry 6] v4: in stack (16) [entry 7] 0x01ac6d26: 9903 ldr r1, [sp, #12] 0x01ac6d28: 9a04 ldr r2, [sp, #16] 0x01ac6d2a: e7ca b -108 (0x01ac6cc2) 0x01ac6d2c: 9103 str r1, [sp, #12] 0x01ac6d2e: 9204 str r2, [sp, #16] 0x01ac6d30: f8d9e2bc ldr.w lr, [r9, #700] ; pThrowNullPointer 0x01ac6d34: 47f0 blx lr StackMap [native_pc=0x1ac6d37] (dex_pc=0x2, native_pc_offset=0x92, dex_register_map_offset=0xe, inline_info_offset=0xffffffff, register_mask=0x20, stack_mask=0b0000000110000000011000) v1: in register (5) [entry 0] v3: in stack (12) [entry 6] v4: in stack (16) [entry 7] 0x01ac6d36: 9003 str r0, [sp, #12] 0x01ac6d38: f8d9e2bc ldr.w lr, [r9, #700] ; pThrowNullPointer 0x01ac6d3c: 47f0 blx lr StackMap [native_pc=0x1ac6d3f] (dex_pc=0x6, native_pc_offset=0x9a, dex_register_map_offset=0x11, inline_info_offset=0xffffffff, register_mask=0xe0, stack_mask=0b0000000110000000000000) v1: in register (5) [entry 0] v2: in stack (12) [entry 6] v3: in register (6) [entry 1] v4: in register (7) [entry 2]
可以看到这个函数被编译出来的 quick code中,由两处显示的 Null Check,分别在下面两处代码处:0x01ac6cc4: b392 cbz r2, +100 (0x01ac6d2c)0x01ac6cd8: b36d cbz r5, +90 (0x01ac6d36)第一处,是检查参数 user是否为 Null,因为要调用 user.getIdentifier(),如果user 是Null,则会跳转到 0x01ac6d2c处:0x01ac6d2c: 9103 str r1, [sp, #12] 0x01ac6d2e: 9204 str r2, [sp, #16] 0x01ac6d30: f8d9e2bc ldr.w lr, [r9, #700] ; pThrowNullPointer 0x01ac6d34: 47f0 blx lr
这里会先保存 this,user(此时是 null)到栈上,然后调用 thread->pThrowNullPointer 函数抛出一个NullPointer异常。第二处,r5看起来是mService,因为要调用 mService.getSharedAccountsAsUser(),所以检测的 r5是 null,会跳转到 0x01ac6d36 处:0x01ac6d36: 9003 str r0, [sp, #12] 0x01ac6d38: f8d9e2bc ldr.w lr, [r9, #700] ; pThrowNullPointer 0x01ac6d3c: 47f0 blx lr
这里保存的 user.getIdentifier()的返回值到栈上,然后也调用 thread->pThrowNullPointer 函数来抛出一个 NullPointer异常。而代码中:qpoints->pThrowNullPointer = art_quick_throw_null_pointer_exception;
所以实际是跳转到 art_quick_throw_null_pointer_exception 函数来抛出异常的。(补充:qpoints->pXXX 都是在ART thread 创建后,Init()过程中初始化的,被赋值为 art_quick_throw_null_pointer_exception,而在quick code中,是从thread 的一定 offset 获取 qpoints->pXXX 的值,然后跳转过去,也就是说编译器在编译的时候病不知道具体实现函数的地址)又会跳转到 artThrowNullPointerExceptionFromCode 函数:/* * Called by managed code to create and deliver a NullPointerException. */ NO_ARG_RUNTIME_EXCEPTION_SAVE_EVERYTHING art_quick_throw_null_pointer_exception, artThrowNullPointerExceptionFromCode
接下来的事情就和上面的 artThrowNullPointerExceptionFromSignal() 函数基本相同了,只不过此处调用 ThrowNullPointerExceptionFromDexPC()函数时传递的参数是 false 和 0,也就是不用经过再次检测确认是否是 NullPointerException了,因为这个是显示的 Null Check,是明确的NPE,所以才进行Throw Exception的。// Called by generated code to throw a NPE exception. extern "C" NO_RETURN void artThrowNullPointerExceptionFromCode(Thread* self) REQUIRES_SHARED(Locks::mutator_lock_) { ScopedQuickEntrypointChecks sqec(self); // We come from an explicit check in the generated code. This path is triggered // only if the object is indeed null. ThrowNullPointerExceptionFromDexPC(/* check_address */ false, 0U); self->QuickDeliverException(); }
目前我们能确认的就是 编译器会在编译java 函数时适当的插入显示的 Null Check函数,什么是适当?牵扯到编译规则,后续分析编译器的时候再详细分析。
2.已提到的两种都是在 generated code中发生/检测的 Null Pointer Exception,所以代码如果解释执行的话,定然会在解释执行的过程进行 Null Check,如下这个三个函数里会直接调用 ThrowNullPointerExceptionFromDexPC()来抛出异常:DoIGetQuick():在所有的 iget_xxx指令会调用这个函数,在这个函数内检查符合会跳转到 ThrowNullPointerExceptionFromDexPC()DoIPutQuick():在所有的 iput_xxx指令会调用这个函数,在这个函数内检查符合会跳转到 ThrowNullPointerExceptionFromDexPC()
ThrowNullPointerExceptionFromInterpreter():这个函数只在 monitor_enter/exit,array_length,aget_xxx,aput_xxx,i/a_get_object 这些指令的时候才会判断并跳转到 ThrowNullPointerExceptionFromDexPC() 函数。上面这些看起来都是 field 访问时的 NPE 检测和抛出。而函数调用时的 Null Check则比较隐秘,找了好久才找到:dex 指令中的函数调用指令都是: invoke-virtual / invoke-direct / invoke-static 等,查找这些指令的实现,看在这个实现过程中是否进行了 Null检查:看下 op_invoke_virtual.S在 invoke.S中:%include "arm/invoke.S" { "helper":"MterpInvokeVirtual" }
所以这里会通过 bl $helper 指令跳转到 MterpInvokeVirtual() 函数,MterpInvokeVirtual() 的实现如下:%default { "helper":"UndefinedInvokeHandler" } /* * Generic invoke handler wrapper. */ /* op vB, {vD, vE, vF, vG, vA}, class@CCCC */ /* op {vCCCC..v(CCCC+AA-1)}, meth@BBBB */ .extern $helper EXPORT_PC mov r0, rSELF add r1, rFP, #OFF_FP_SHADOWFRAME mov r2, rPC mov r3, rINST bl $helper cmp r0, #0 beq MterpException FETCH_ADVANCE_INST 3 bl MterpShouldSwitchInterpreters cmp r0, #0 bne MterpFallback GET_INST_OPCODE ip GOTO_OPCODE ip
在 MterpInvokeVirtual 中又调用 DoFastInvoke<kVirtuall>() 函数:extern "C" size_t MterpInvokeVirtual(Thread* self, ShadowFrame* shadow_frame, uint16_t* dex_pc_ptr, uint16_t inst_data) REQUIRES_SHARED(Locks::mutator_lock_) { JValue* result_register = shadow_frame->GetResultRegister(); const Instruction* inst = Instruction::At(dex_pc_ptr); return DoFastInvoke<kVirtual>( self, *shadow_frame, inst, inst_data, result_register); }
template<InvokeType type> static inline bool DoFastInvoke(Thread* self, ShadowFrame& shadow_frame, const Instruction* inst, uint16_t inst_data, JValue* result) { const uint32_t method_idx = inst->VRegB_35c(); const uint32_t vregC = inst->VRegC_35c(); ObjPtr<mirror::Object> receiver = (type == kStatic) ? nullptr : shadow_frame.GetVRegReference(vregC); ArtMethod* sf_method = shadow_frame.GetMethod(); ArtMethod* const called_method = FindMethodFromCode<type, false>( method_idx, &receiver, sf_method, self); .... }
在 DoFastInvoke() 函数中,调用 FindMethodFormCode<kVirtual, false>() 函数来查找 invoke-virtual 想要调用的函数:在查找的过程中,就有个 Null check:假如当前的 this object 是 null,且这个invoke指令不是 invoke-static指令,且不是 String.<init>函数,那么就会抛出一个 NullPointerException。template<InvokeType type, bool access_check> inline ArtMethod* FindMethodFromCode(uint32_t method_idx, ObjPtr<mirror::Object>* this_object, ArtMethod* referrer, Thread* self) { .... // Next, null pointer check. if (UNLIKELY(*this_object == nullptr && type != kStatic)) { if (UNLIKELY(resolved_method->GetDeclaringClass()->IsStringClass() && resolved_method->IsConstructor())) { // Hack for String init: // // We assume that the input of String.<init> in verified code is always // an unitialized reference. If it is a null constant, it must have been // optimized out by the compiler. Do not throw NullPointerException. } else { // Maintain interpreter-like semantics where NullPointerException is thrown // after potential NoSuchMethodError from class linker. ThrowNullPointerExceptionForMethodAccess(method_idx, type); return nullptr; // Failure. } } .... }
到这里,我们知道了:
- 隐式的 Null Check,通过 artThrowNullPointerExceptionFromSignal 来抛出 NPE;
- 编译器安插到 generated code中的显式的 Null Check,通过 artThrowNullPointerExceptionFromCode 来抛出 NPE
- 在 Interpreter 执行过程中的 Null Check,通过 ThrowNullPointerExceptionFromDexPC(field访问时) 和 ThrowNullPointerExceptionForMethodAccess() 这两个函数来抛出 NPE
仍然遗留的问题有:
- Implicit Null Check 时的 4K 这个数字到底是怎么来的?
- 在 generated code中 Null Check的安插点并不多,那么显式的 Null Check 安插的规则是怎样的
- 根据 qpoints->pThrowNullPointer 引出的 thread quick entry points的知识,有兴趣的读者可以研究下