在本篇介绍 StackOverflowError 在 ART种的实现。本篇基础:ART异常处理机制(1) - SIGSEGV信号的拦截和处理
在 ART异常处理机制1 中,我们已经知道了 ART会注册信号处理函数,优先尝试处理 SIGSEGV信号。ART中总共有 4 种 handler 享有优先处理 SIGSEGV信号的权利。而StackOverflowHandler排在第二位,由于SuspendHandler是 Disable的,所以实际上第一个处理 SIGSEGV的是 StackOverflowHandler。今天我们主要分析下它的实现以及 StackOverflowError的检测和抛出。
先看下StackOverflowHandler实现的注释:
// Stack overflow fault handler.
//
// This checks that the fault address is equal to the current stack pointer
// minus the overflow region size (8K typically). The instruction sequence
// that generates this signal is:
//
// sub r12,sp,#8192
// ldr.w r12,[r12,#0]
//
// The second instruction will fault if r12 is inside the protected region
// on the stack.
//
// If we determine this is a stack overflow we need to move the stack pointer
// to the overflow region below the protected region.
说的实际就是 stack overflow检测方法,意思是如果 sp-8k在 stack上的 protected region 空间内,那么在执行第二条指令 ldr.w r12, [r12, #0] 时,就会产生一个 SIGSEGV信号。这个8k 取决于 stack reserved page的大小,也有可能是 16k,说到这里,我们还是需要一个清晰的 java stack的布局,才能更明了的分析这个话题了。下图是一个 ART thread的布局:
图的说明:
- ART中的thread也是通过 pthread创建的,所以 pthread 开头的标注都是pthread内部的数据成员
- 以ART开头的标注都是 ART内部/ART thread内部的数据成员
- 在低地址的前两个page,都是 ---p权限,就是用来做 stack overflow检测的,分别是 pthread和ART创建的 guard page
- 接下来黄色部分描述的 8KB 是为了抛出 StackOverflow Exception 预留的栈空间,因为如果不预留的话,在overflow的时候,就没有栈空间用来执行抛出 StackOverflow 异常了
- 再下面的绿色部分,是线程真正能够使用的栈空间
- 最下面的部分,用来存储 pthread_internal_t 来描述当前 pthread,并有个 alignment保证下面的数据 16 byte对齐
7f8000d000-7f8000e000 ---p 00000000 00:00 0 [anon:thread stack guard page]
7f8000e000-7f8000f000 ---p 00000000 00:00 0
7f8000f000-7f8010a000 rw-p 00000000 00:00 0 [stack:7496]
现在就比较明了了,在栈的末尾,先有 8KB的 protect page,都是 ---p权限,所以任何的尝试从 protected page 读取或者写入数据的指令,都会触发 segement fault,从而产生一个 SIGSEGV 信号。
一般情况下,线程运行的过程中,SP都是在图中的浅绿色部分栈空间中,认为栈空间还充足。但只要 SP超出浅绿色部分栈空间,剩余的栈空间就不到 8+4+4=16KB了。此时认为当前线程发生了 stack overflow问题;那么此时 SP- 8192(reserved) 计算得出的地址,就会在 protected page范围内,此时再执行 ldr.w r12, [r12, #0] 就会产生一个SIGSEGV信号,且 fault_addr = SP-8192。而在没有发生 overflow的情况下,SP还在浅绿色部分栈空间,SP-8192的地址肯定是可以被访问的,就不会发生 segement fault。
这样,我们在StackOverflowHander的Action中,只需要检查当前这个 SIGSEGV错误中的 fault_addr是否是等于 SP - 8192,如果相等,这说明当前线程发生了 stack overflow错误,需要抛出StackOverflow异常。
需要指明的是,这样的检测代码都在一个java 函数对应的 generated code的最开始位置,也即在跳转到一个Java 函数后,需要先检查是否发生了 overflow,因为接下来的代码中很快就会减小 SP来使用栈空间。比如:
34: void sun.util.logging.PlatformLogger.warning(java.lang.String, java.lang.Object[]) (dex_method_idx=26617)
DEX CODE:
0x0000: 5420 4b27 | iget-object v0, v2, Lsun/util/logging/PlatformLogger$LoggerProxy; sun.util.logging.PlatformLogger.loggerProxy // field@10059
0x0002: 6201 3c27 | sget-object v1, Lsun/util/logging/PlatformLogger$Level; sun.util.logging.PlatformLogger$Level.WARNING // field@10044
0x0004: 6e40 d267 1043 | invoke-virtual {v0, v1, v3, v4}, void sun.util.logging.PlatformLogger$LoggerProxy.doLog(sun.util.logging.PlatformLogger$Level, java.lang.String, java.lang.Object[]) // method@26578
CODE: (code_offset=0x0094ffb4 size_offset=0x0094ffb0 size=144)...
0x0094ffb4: d1400bf0 sub x16, sp, #0x2000 (8192)
0x0094ffb8: b940021f ldr wzr, [x16]
StackMap [native_pc=0x94ffbc] (dex_pc=0x0, native_pc_offset=0x8, dex_register_map_offset=0xffffffff, inline_info_offset=0xffffffff, register_mask=0x0, stack_mask=0b0000000000000000)
0x0094ffbc: f8190fe0 str x0, [sp, #-112]!
0x0094ffc0: a90457f4 stp x20, x21, [sp, #64]
0x0094ffc4: a9055ff6 stp x22, x23, [sp, #80]
0x0094ffc8: a9067bf8 stp x24, lr, [sp, #96]
....
bool StackOverflowHandler::Action(int sig ATTRIBUTE_UNUSED, siginfo_t* info ATTRIBUTE_UNUSED,
void* context) {
struct ucontext* uc = reinterpret_cast<struct ucontext*>(context);
struct sigcontext *sc = reinterpret_cast<struct sigcontext*>(&uc->uc_mcontext);
uintptr_t sp = sc->arm_sp;
uintptr_t fault_addr = sc->fault_address;
uintptr_t overflow_addr = sp - GetStackOverflowReservedBytes(kArm);
// Check that the fault address is the value expected for a stack overflow.
if (fault_addr != overflow_addr) {
VLOG(signals) << "Not a stack overflow";
return false;
}
VLOG(signals) << "Stack overflow found";
sc->arm_pc = reinterpret_cast<uintptr_t>(art_quick_throw_stack_overflow);
// The kernel will now return to the address in sc->arm_pc.
return true;
}
在这里发现 fault addr 匹配 SP-Reserved 后,就会跳转到 art_quick_throw_stack_overflow 去抛出异常:
/*
* Called by managed code to create and deliver a StackOverflowError.
*/
NO_ARG_RUNTIME_EXCEPTION art_quick_throw_stack_overflow, artThrowStackOverflowFromCode
看下这个宏:
.macro NO_ARG_RUNTIME_EXCEPTION c_name, cxx_name
.extern \cxx_name
ENTRY \c_name
SETUP_SAVE_ALL_CALLEE_SAVES_FRAME r0 @ save all registers as basis for long jump context
mov r0, r9 @ pass Thread::Current
bl \cxx_name @ \cxx_name(Thread*)
END \c_name
.endm
展开后应该是: .extern artThrowStackOverflowFromCode
ENTRY art_quick_throw_stack_overflow
SETUP_SAVE_ALL_CALLEE_SAVES_FRAME r0 @ save all registers as basis for long jump context
mov r0, r9 @ pass Thread::Current
bl artThrowStackOverflowFromCode @ \cxx_name(Thread*)
END art_quick_throw_stack_overflow
所以最终是跳转到 artThrowStackOverflowFromCode函数,进行异常的抛出:extern "C" NO_RETURN void artThrowStackOverflowFromCode(Thread* self)
REQUIRES_SHARED(Locks::mutator_lock_) {
ScopedQuickEntrypointChecks sqec(self);
ThrowStackOverflowError(self);
self->QuickDeliverException();
}
ThrowStackOverflowError()函数,会创建一个 StackOverflowError类对象,并设置好 error message,stack trace等信息,然后使用 self->SetException(e),把这个Throwable对象设置到当前线程的 tlsPtr_.exception 成员中,以便后续通过self->GetException()进行获取。void ThrowStackOverflowError(Thread* self) {
self->SetStackEndForStackOverflow(); // Allow space on the stack for constructor to execute.
JNIEnvExt* env = self->GetJniEnv();
std::string msg("stack size ");
msg += PrettySize(self->GetStackSize());
std::string error_msg;
// Allocate an uninitialized object.
ScopedLocalRef<jobject> exc(env,
env->AllocObject(WellKnownClasses::java_lang_StackOverflowError));
if (exc.get() != nullptr) {
ScopedLocalRef<jstring> s(env, env->NewStringUTF(msg.c_str()));
if (s.get() != nullptr) {
env->SetObjectField(exc.get(), WellKnownClasses::java_lang_Throwable_detailMessage, s.get());
// cause.
env->SetObjectField(exc.get(), WellKnownClasses::java_lang_Throwable_cause, exc.get());
// suppressedExceptions.
ScopedLocalRef<jobject> emptylist(env, env->GetStaticObjectField(
WellKnownClasses::java_util_Collections,
WellKnownClasses::java_util_Collections_EMPTY_LIST));
CHECK(emptylist.get() != nullptr);
env->SetObjectField(exc.get(),
WellKnownClasses::java_lang_Throwable_suppressedExceptions,
emptylist.get());
// stackState is set as result of fillInStackTrace. fillInStackTrace calls
// nativeFillInStackTrace.
ScopedLocalRef<jobject> stack_state_val(env, nullptr);
{
ScopedObjectAccessUnchecked soa(env);
stack_state_val.reset(soa.Self()->CreateInternalStackTrace<false>(soa));
}
if (stack_state_val.get() != nullptr) {
env->SetObjectField(exc.get(),
WellKnownClasses::java_lang_Throwable_stackState,
stack_state_val.get());
// stackTrace.
ScopedLocalRef<jobject> stack_trace_elem(env, env->GetStaticObjectField(
WellKnownClasses::libcore_util_EmptyArray,
WellKnownClasses::libcore_util_EmptyArray_STACK_TRACE_ELEMENT));
env->SetObjectField(exc.get(),
WellKnownClasses::java_lang_Throwable_stackTrace,
stack_trace_elem.get());
} else {
error_msg = "Could not create stack trace.";
}
// Throw the exception.
self->SetException(self->DecodeJObject(exc.get())->AsThrowable());
} else {
// Could not allocate a string object.
error_msg = "Couldn't throw new StackOverflowError because JNI NewStringUTF failed.";
}
} else {
error_msg = "Could not allocate StackOverflowError object.";
}
if (!error_msg.empty()) {
LOG(WARNING) << error_msg;
CHECK(self->IsExceptionPending());
}
bool explicit_overflow_check = Runtime::Current()->ExplicitStackOverflowChecks();
self->ResetDefaultStackEnd(); // Return to default stack size.
// And restore protection if implicit checks are on.
if (!explicit_overflow_check) {
self->ProtectStack();
}
}
需要说明的是 SetStackEndForStackOverflow()函数设置 tlsPtr_.stack_end = tlsPtr_.stack_begin;表示正在处理 stack overflow,并且把 ART设置的那 4KB的protected region设置为可读写(这么做的原因是增加4KB的可使用栈空间,以满足 stack overflow抛出过程的使用)。下面一部分代码就是 error msg,stacktrace填充的逻辑。业务逻辑完成后,再次把 tlsPtr_.stack_end还原到 Reserved page之前的位置,并把那 4KB region重新protect 起来。
而在ThrowStackOverflowError函数之后的 QuickDeliverException函数的功能就是尝试从当前 thread 的 stack 上找到当前 Exception对应的 catch block,交给其去处理。
分析过程中的几个疑问
1.一个线程发生 StackOverflow,触发 segement fault时,这个线程的状态是怎样的?
答:解释不太好,先说下暂时的理解,待后续研究CPU异常处理机制。访问不可读的内存时,应该会产生page fault(此时应该在内核态了),在后续page fault处理流程中,发现该page fault符合 SIGSEGV信号的条件,然后给发生异常的线程发送 SIGSEGV信号。所以,触发 segment fault时,处于内核态。
2.我们知道产生的 SIGSEGV 会由ART处理,那么是哪个线程处理的?
答:从第一点的答案可以知道,内核会把 SIGSEGV信号发送给发生异常的那个线程处理,处理时机应该是CPU异常处理机制执行完成,从内核态切换到用户态的时候,检查到有pending signal,然后调用信号处理函数去处理这个信号。相当于发生异常的线程会调用 SigChain::Hander函数来处理 SIGSEGV信号。
3.检测 StackOverflow的位置除了上面了解的一种,还有哪些位置会检测 StackOverflow?
上面提到的这种是generated code中,在函数的入口位置(此时这个函数还没有开辟当前frame的栈空间),进行检测stack overflow。
那么在 Interpreter 模式,或者从quick模式切换到 Interpreter 模式时,显然没有 generated code了,我们自己设想的话,这个检测应该在切换的过程中 (还没有开始给准备执行的java 函数分配栈空间)完成。检查了一下代码,发现有这么些地方会检查 stack overflow问题:
- reflection.cc:InvokeWithVarArgs() / InvokeWithJValues() / InvokeVirtualOrInterfaceWithJValues() / InvokeVirtualOrInterfaceWithVarArgs() / InvokeMethod(),在这几个函数的入口位置都会进行 stack overflow检测
- interpreter.cc:EnterInterpreterFromInvoke() / EnterInterpreterFromEntryPoint() / ArtInterpreterToInterpreterBridge() 在这几个函数入口 也会检查
- art_method.cc:void ArtMethod::Invoke() 这个函数入口也会进行检查
总的来讲,原理就是在执行将要跳转到的函数的栈空间开辟之前,完成 stack overflow异常的检测。
Java stack overflow 异常说到这里。
补充: StackSize:
stack_size >= 1MB + 8k + 8k
static size_t FixStackSize(size_t stack_size) { // A stack size of zero means "use the default". if (stack_size == 0) { stack_size = Runtime::Current()->GetDefaultStackSize(); } // Dalvik used the bionic pthread default stack size for native threads, // so include that here to support apps that expect large native stacks. stack_size += 1 * MB; // It's not possible to request a stack smaller than the system-defined PTHREAD_STACK_MIN. if (stack_size < PTHREAD_STACK_MIN) { stack_size = PTHREAD_STACK_MIN; } if (Runtime::Current()->ExplicitStackOverflowChecks()) { // It's likely that callers are trying to ensure they have at least a certain amount of // stack space, so we should add our reserved space on top of what they requested, rather // than implicitly take it away from them. stack_size += GetStackOverflowReservedBytes(kRuntimeISA); } else { // If we are going to use implicit stack checks, allocate space for the protected // region at the bottom of the stack. stack_size += Thread::kStackOverflowImplicitCheckSize + GetStackOverflowReservedBytes(kRuntimeISA); } // Some systems require the stack size to be a multiple of the system page size, so round up. stack_size = RoundUp(stack_size, kPageSize); return stack_size; }