线程栈
Linux机器上线程的栈(Stack)大小默认为8M。维护着这个线程里函数的调用关系,每个函数对应一帧(Frame)。
x86_64栈帧
具体看下这段C程序在x86_64体系的CPU上栈帧是怎么维护的。
int sum(int i, int j) {
return i + j;
}
int main(int argc, char* args[]) {
int x = sum(1, 2);
return 0;
}
以上程序编译后,通过objdump -d导出汇编。main函数部分如下
000000000040050d <_Z3sumii>:
40050d: 55 push %rbp //把上一帧基址rbp压栈(注意这一步会改变rsp)
40050e: 48 89 e5 mov %rsp,%rbp //把rbp指向上一帧的rsp(从这里可以看出保存上一个帧rbp的位置不算在这一帧里的)
400511: 89 7d fc mov %edi,-0x4(%rbp) //参数1
400514: 89 75 f8 mov %esi,-0x8(%rbp) //参数2
400517: 8b 45 f8 mov -0x8(%rbp),%eax //add参数寄存器
40051a: 8b 55 fc mov -0x4(%rbp),%edx //add参数寄存器
40051d: 01 d0 add %edx,%eax //add结果在eax
40051f: 5d pop %rbp //把rsp指向rbp(也就是销毁这一帧)
400520: c3 retq //把栈顶值压入rip(也就是执行return address)
0000000000400521 <main>:
400521: 55 push %rbp
400522: 48 89 e5 mov %rsp,%rbp
400525: 48 83 ec 20 sub $0x20,%rsp
400529: 89 7d ec mov %edi,-0x14(%rbp)
40052c: 48 89 75 e0 mov %rsi,-0x20(%rbp)
400530: be 02 00 00 00 mov $0x2,%esi //参数2
400535: bf 01 00 00 00 mov $0x1,%edi //参数1
40053a: e8 ce ff ff ff callq 40050d <_Z3sumii> //push %rip(return address) + jump sum
40053f: 89 45 fc mov %eax,-0x4(%rbp)
400542: b8 00 00 00 00 mov $0x0,%eax
400547: c9 leaveq
400548: c3 retq
400549: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
汇编指令callq、retq、push、pop和rbp(栈基地址寄存器)、rsp(栈顶地址寄存器)、rip(指令寄存器)一起配合构造起了程序的运行栈。
x86-64指令集见https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/24592.pdf
插个题外话。
retq指令是完全信任并且去执行上一帧里保存的return地址的。这在正常情况下是没有问题的。
但是,栈是从高地址到低地址分配的,而帧上的局部变量是从低地址开始写的。如果局部变量因为内存越界,它其实覆盖的是高地址区域也就是上一帧的内容。在某种精心的设计下,可能就刚好把return地址改到了一个恶意的地址上。所以内存越界不只是简单的程序bug,更是严重的安全漏洞,是给黑客的特洛伊木马开了一道门。
Java栈帧
上面x86_64指令集维护函数栈的过程,JVM中通过C++代码也做了类似的一个事情。
关于Java栈构造的代码在
jdk\src\hotspot\share\runtime\javaCalls.cpp
构造Java栈的最重要工作是JavaCallWrapper类完成的
// A JavaCallWrapper is constructed before each JavaCall and destructed after the call.
// Its purpose is to allocate/deallocate a new handle block and to save/restore the last
// Java fp/sp. A pointer to the JavaCallWrapper is stored on the stack.
class JavaCallWrapper: StackObj {
friend class VMStructs;
private:
JavaThread* _thread; // the thread to which this call belongs
JNIHandleBlock* _handles; // the saved handle block
Method* _callee_method; // to be able to collect arguments if entry frame is top frame
oop _receiver; // the receiver of the call (if a non-static call)
JavaFrameAnchor _anchor; // last thread anchor state that we must restore
JavaValue* _result; // result value
}
Java调用都是从JavaCalls::call_helper开始的。
void JavaCalls::call(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS)
这个函数的工作
- 检查必要的上下文
- 检查method是否已编译,没有则编译
- 构造Java栈,JavaCallWrapper::JavaCallWrapper
Java栈的地址和指令保存在JavaFrameAnchor对象上,JavaThread上有记录。 - 调用底层的Stub接口StubRoutines::call_stub执行method.entry_point
- 销毁Java栈,JavaCallWrapper::~JavaCallWrapper
构造Java栈的代码
JavaCallWrapper::JavaCallWrapper(const methodHandle& callee_method, Handle receiver, JavaValue* result, TRAPS) {
JavaThread* thread = (JavaThread *)THREAD;
bool clear_pending_exception = true;
guarantee(thread->is_Java_thread(), "crucial check - the VM thread cannot and must not escape to Java code");
assert(!thread->owns_locks(), "must release all locks when leaving VM");
guarantee(thread->can_call_java(), "cannot make java calls from the native compiler");
_result = result;
// Allocate handle block for Java code. This must be done before we change thread_state to _thread_in_Java_or_stub,
// since it can potentially block.
JNIHandleBlock* new_handles = JNIHandleBlock::allocate_block(thread);
// After this, we are official in JavaCode. This needs to be done before we change any of the thread local
// info, since we cannot find oops before the new information is set up completely.
ThreadStateTransition::transition(thread, _thread_in_vm, _thread_in_Java);
// Make sure that we handle asynchronous stops and suspends _before_ we clear all thread state
// in JavaCallWrapper::JavaCallWrapper(). This way, we can decide if we need to do any pd actions
// to prepare for stop/suspend (flush register windows on sparcs, cache sp, or other state).
if (thread->has_special_runtime_exit_condition()) {
thread->handle_special_runtime_exit_condition();
if (HAS_PENDING_EXCEPTION) {
clear_pending_exception = false;
}
}
// Make sure to set the oop's after the thread transition - since we can block there. No one is GC'ing
// the JavaCallWrapper before the entry frame is on the stack.
_callee_method = callee_method();
_receiver = receiver();
#ifdef CHECK_UNHANDLED_OOPS
THREAD->allow_unhandled_oop(&_receiver);
#endif // CHECK_UNHANDLED_OOPS
_thread = (JavaThread *)thread;
_handles = _thread->active_handles(); // save previous handle block & Java frame linkage
// For the profiler, the last_Java_frame information in thread must always be in
// legal state. We have no last Java frame if last_Java_sp == NULL so
// the valid transition is to clear _last_Java_sp and then reset the rest of
// the (platform specific) state.
_anchor.copy(_thread->frame_anchor());
_thread->frame_anchor()->clear();
debug_only(_thread->inc_java_call_counter());
_thread->set_active_handles(new_handles); // install new handle block and reset Java frame linkage
assert (_thread->thread_state() != _thread_in_native, "cannot set native pc to NULL");
// clear any pending exception in thread (native calls start with no exception pending)
if(clear_pending_exception) {
_thread->clear_pending_exception();
}
if (_anchor.last_Java_sp() == NULL) {
_thread->record_base_of_stack_pointer();
}
}
销毁Java栈的代码
JavaCallWrapper::~JavaCallWrapper() {
assert(_thread == JavaThread::current(), "must still be the same thread");
// restore previous handle block & Java frame linkage
JNIHandleBlock *_old_handles = _thread->active_handles();
_thread->set_active_handles(_handles);
_thread->frame_anchor()->zap();
debug_only(_thread->dec_java_call_counter());
if (_anchor.last_Java_sp() == NULL) {
_thread->set_base_of_stack_pointer(NULL);
}
// Old thread-local info. has been restored. We are not back in the VM.
ThreadStateTransition::transition_from_java(_thread, _thread_in_vm);
// State has been restored now make the anchor frame visible for the profiler.
// Do this after the transition because this allows us to put an assert
// the Java->vm transition which checks to see that stack is not walkable
// on sparc/ia64 which will catch violations of the reseting of last_Java_frame
// invariants (i.e. _flags always cleared on return to Java)
_thread->frame_anchor()->copy(&_anchor);
// Release handles after we are marked as being inside the VM again, since this
// operation might block
JNIHandleBlock::release_block(_old_handles, _thread);
}
最终的Java栈结构如下
ZGC在初始标记阶段,扫描GC Roots对象就是从每个JavaThread线程的_anchor对象开始遍历的。
微信扫描下方的二维码阅读本文
0 Comments