| // Copyright 2018 the V8 project authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "include/v8-unwinder-state.h" |
| #include "include/v8.h" |
| #include "src/api/api-inl.h" |
| #include "src/builtins/builtins.h" |
| #include "src/execution/isolate.h" |
| #include "src/heap/spaces.h" |
| #include "src/objects/code-inl.h" |
| #include "test/cctest/cctest.h" |
| |
| namespace v8 { |
| namespace internal { |
| namespace test_unwinder_code_pages { |
| |
| namespace { |
| |
| #define CHECK_EQ_VALUE_REGISTER(uiuntptr_value, register_value) \ |
| CHECK_EQ(reinterpret_cast<void*>(uiuntptr_value), register_value) |
| |
| #ifdef V8_TARGET_ARCH_X64 |
| // How much the JSEntry frame occupies in the stack. |
| constexpr int kJSEntryFrameSpace = 3; |
| |
| // Offset where the FP, PC and SP live from the beginning of the JSEntryFrame. |
| constexpr int kFPOffset = 0; |
| constexpr int kPCOffset = 1; |
| constexpr int kSPOffset = 2; |
| |
| // Builds the stack from {stack} as x64 expects it. |
| // TODO(solanes): Build the JSEntry stack in the way the builtin builds it. |
| void BuildJSEntryStack(uintptr_t* stack) { |
| stack[0] = reinterpret_cast<uintptr_t>(stack + 0); // saved FP. |
| stack[1] = 100; // Return address into C++ code. |
| stack[2] = reinterpret_cast<uintptr_t>(stack + 2); // saved SP. |
| } |
| |
| // Dummy method since we don't save callee saved registers in x64. |
| void CheckCalleeSavedRegisters(const RegisterState& register_state) {} |
| |
| #elif V8_TARGET_ARCH_ARM |
| // How much the JSEntry frame occupies in the stack. |
| constexpr int kJSEntryFrameSpace = 27; |
| |
| // Offset where the FP, PC and SP live from the beginning of the JSEntryFrame. |
| constexpr int kFPOffset = 24; |
| constexpr int kPCOffset = 25; |
| constexpr int kSPOffset = 26; |
| |
| // Builds the stack from {stack} as it is explained in frame-constants-arm.h. |
| void BuildJSEntryStack(uintptr_t* stack) { |
| stack[0] = -1; // the bad frame pointer (0xF..F) |
| // Set d8 = 150, d9 = 151, ..., d15 = 157. |
| for (int i = 0; i < 8; ++i) { |
| // Double registers occupy two slots. Therefore, upper bits are zeroed. |
| stack[1 + i * 2] = 0; |
| stack[1 + i * 2 + 1] = 150 + i; |
| } |
| // Set r4 = 160, ..., r10 = 166. |
| for (int i = 0; i < 7; ++i) { |
| stack[17 + i] = 160 + i; |
| } |
| stack[24] = reinterpret_cast<uintptr_t>(stack + 24); // saved FP. |
| stack[25] = 100; // Return address into C++ code (i.e lr/pc) |
| stack[26] = reinterpret_cast<uintptr_t>(stack + 26); // saved SP. |
| } |
| |
| // Checks that the values in the calee saved registers are the same as the ones |
| // we saved in BuildJSEntryStack. |
| void CheckCalleeSavedRegisters(const RegisterState& register_state) { |
| CHECK_EQ_VALUE_REGISTER(160, register_state.callee_saved->arm_r4); |
| CHECK_EQ_VALUE_REGISTER(161, register_state.callee_saved->arm_r5); |
| CHECK_EQ_VALUE_REGISTER(162, register_state.callee_saved->arm_r6); |
| CHECK_EQ_VALUE_REGISTER(163, register_state.callee_saved->arm_r7); |
| CHECK_EQ_VALUE_REGISTER(164, register_state.callee_saved->arm_r8); |
| CHECK_EQ_VALUE_REGISTER(165, register_state.callee_saved->arm_r9); |
| CHECK_EQ_VALUE_REGISTER(166, register_state.callee_saved->arm_r10); |
| } |
| |
| #elif V8_TARGET_ARCH_ARM64 |
| // How much the JSEntry frame occupies in the stack. |
| constexpr int kJSEntryFrameSpace = 22; |
| |
| // Offset where the FP, PC and SP live from the beginning of the JSEntryFrame. |
| constexpr int kFPOffset = 11; |
| constexpr int kPCOffset = 12; |
| constexpr int kSPOffset = 21; |
| |
| // Builds the stack from {stack} as it is explained in frame-constants-arm64.h. |
| void BuildJSEntryStack(uintptr_t* stack) { |
| stack[0] = -1; // the bad frame pointer (0xF..F) |
| // Set x19 = 150, ..., x28 = 159. |
| for (int i = 0; i < 10; ++i) { |
| stack[1 + i] = 150 + i; |
| } |
| stack[11] = reinterpret_cast<uintptr_t>(stack + 11); // saved FP. |
| stack[12] = 100; // Return address into C++ code (i.e lr/pc) |
| // Set d8 = 160, ..., d15 = 167. |
| for (int i = 0; i < 8; ++i) { |
| stack[13 + i] = 160 + i; |
| } |
| stack[21] = reinterpret_cast<uintptr_t>(stack + 21); // saved SP. |
| } |
| |
| // Dummy method since we don't save callee saved registers in arm64. |
| void CheckCalleeSavedRegisters(const RegisterState& register_state) {} |
| |
| #else |
| // Dummy constants for the rest of the archs which are not supported. |
| constexpr int kJSEntryFrameSpace = 1; |
| constexpr int kFPOffset = 0; |
| constexpr int kPCOffset = 0; |
| constexpr int kSPOffset = 0; |
| |
| // Dummy methods to be able to compile. |
| void BuildJSEntryStack(uintptr_t* stack) { UNREACHABLE(); } |
| void CheckCalleeSavedRegisters(const RegisterState& register_state) { |
| UNREACHABLE(); |
| } |
| #endif // V8_TARGET_ARCH_X64 |
| |
| } // namespace |
| |
| static const void* fake_stack_base = nullptr; |
| |
| TEST(Unwind_BadState_Fail_CodePagesAPI) { |
| JSEntryStubs entry_stubs; // Fields are intialized to nullptr. |
| RegisterState register_state; |
| size_t pages_length = 0; |
| MemoryRange* code_pages = nullptr; |
| |
| bool unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, fake_stack_base); |
| CHECK(!unwound); |
| // The register state should not change when unwinding fails. |
| CHECK_NULL(register_state.fp); |
| CHECK_NULL(register_state.sp); |
| CHECK_NULL(register_state.pc); |
| } |
| |
| // Unwind a middle JS frame (i.e not the JSEntry one). |
| TEST(Unwind_BuiltinPCInMiddle_Success_CodePagesAPI) { |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate); |
| |
| JSEntryStubs entry_stubs = isolate->GetJSEntryStubs(); |
| MemoryRange code_pages[v8::Isolate::kMinCodePagesBufferSize]; |
| size_t pages_length = |
| isolate->CopyCodePages(arraysize(code_pages), code_pages); |
| CHECK_LE(pages_length, arraysize(code_pages)); |
| RegisterState register_state; |
| |
| // {stack} here mocks the stack, where the top of the stack (i.e the lowest |
| // addresses) are represented by lower indices. |
| uintptr_t stack[3]; |
| void* stack_base = stack + arraysize(stack); |
| // Index on the stack for the topmost fp (i.e the one right before the C++ |
| // frame). |
| const int topmost_fp_index = 0; |
| stack[0] = reinterpret_cast<uintptr_t>(stack + 2); // saved FP. |
| stack[1] = 202; // Return address into C++ code. |
| stack[2] = reinterpret_cast<uintptr_t>(stack + 2); // saved SP. |
| |
| register_state.sp = stack; |
| register_state.fp = stack; |
| |
| // Put the current PC inside of a valid builtin. |
| Code builtin = i_isolate->builtins()->builtin(Builtins::kStringEqual); |
| const uintptr_t offset = 40; |
| CHECK_LT(offset, builtin.InstructionSize()); |
| register_state.pc = |
| reinterpret_cast<void*>(builtin.InstructionStart() + offset); |
| |
| bool unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, stack_base); |
| CHECK(unwound); |
| CHECK_EQ_VALUE_REGISTER(stack[topmost_fp_index], register_state.fp); |
| CHECK_EQ_VALUE_REGISTER(stack[topmost_fp_index + 1], register_state.pc); |
| CHECK_EQ_VALUE_REGISTER(stack[topmost_fp_index + 2], register_state.sp); |
| } |
| |
| // The unwinder should be able to unwind even if we haven't properly set up the |
| // current frame, as long as there is another JS frame underneath us (i.e. as |
| // long as the PC isn't in JSEntry). This test puts the PC at the start |
| // of a JS builtin and creates a fake JSEntry frame before it on the stack. The |
| // unwinder should be able to unwind to the C++ frame before the JSEntry frame. |
| TEST(Unwind_BuiltinPCAtStart_Success_CodePagesAPI) { |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate); |
| |
| JSEntryStubs entry_stubs = isolate->GetJSEntryStubs(); |
| MemoryRange code_pages[v8::Isolate::kMinCodePagesBufferSize]; |
| RegisterState register_state; |
| |
| const size_t code_length = 40; |
| uintptr_t code[code_length] = {0}; |
| |
| // We use AddCodeRange so that |code| is inserted in order. |
| i_isolate->AddCodeRange(reinterpret_cast<Address>(code), |
| code_length * sizeof(uintptr_t)); |
| size_t pages_length = |
| isolate->CopyCodePages(arraysize(code_pages), code_pages); |
| CHECK_LE(pages_length, arraysize(code_pages)); |
| |
| uintptr_t stack[6]; |
| void* stack_base = stack + arraysize(stack); |
| stack[0] = 101; |
| // Return address into JS code. It doesn't matter that this is not actually in |
| // JSEntry, because we only check that for the top frame. |
| stack[1] = reinterpret_cast<uintptr_t>(code + 10); |
| // Index on the stack for the topmost fp (i.e the one right before the C++ |
| // frame). |
| const int topmost_fp_index = 2; |
| stack[2] = reinterpret_cast<uintptr_t>(stack + 5); // saved FP. |
| stack[3] = 303; // Return address into C++ code. |
| stack[4] = reinterpret_cast<uintptr_t>(stack + 4); |
| stack[5] = 505; |
| |
| register_state.sp = stack; |
| register_state.fp = stack + 2; // FP to the JSEntry frame. |
| |
| // Put the current PC at the start of a valid builtin, so that we are setting |
| // up the frame. |
| Code builtin = i_isolate->builtins()->builtin(Builtins::kStringEqual); |
| register_state.pc = reinterpret_cast<void*>(builtin.InstructionStart()); |
| |
| bool unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, stack_base); |
| |
| CHECK(unwound); |
| CHECK_EQ_VALUE_REGISTER(stack[topmost_fp_index], register_state.fp); |
| CHECK_EQ_VALUE_REGISTER(stack[topmost_fp_index + 1], register_state.pc); |
| CHECK_EQ_VALUE_REGISTER(stack[topmost_fp_index + 2], register_state.sp); |
| } |
| |
| const char* foo_source = R"( |
| function foo(a, b) { |
| let x = a * b; |
| let y = x ^ b; |
| let z = y / a; |
| return x + y - z; |
| }; |
| %PrepareFunctionForOptimization(foo); |
| foo(1, 2); |
| foo(1, 2); |
| %OptimizeFunctionOnNextCall(foo); |
| foo(1, 2); |
| )"; |
| |
| bool PagesContainsAddress(size_t length, MemoryRange* pages, |
| Address search_address) { |
| byte* addr = reinterpret_cast<byte*>(search_address); |
| auto it = std::find_if(pages, pages + length, [addr](const MemoryRange& r) { |
| const byte* page_start = reinterpret_cast<const byte*>(r.start); |
| const byte* page_end = page_start + r.length_in_bytes; |
| return addr >= page_start && addr < page_end; |
| }); |
| return it != pages + length; |
| } |
| |
| // Check that we can unwind when the pc is within an optimized code object on |
| // the V8 heap. |
| TEST(Unwind_CodeObjectPCInMiddle_Success_CodePagesAPI) { |
| FLAG_allow_natives_syntax = true; |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate); |
| HandleScope scope(i_isolate); |
| |
| JSEntryStubs entry_stubs = isolate->GetJSEntryStubs(); |
| MemoryRange code_pages[v8::Isolate::kMinCodePagesBufferSize]; |
| RegisterState register_state; |
| |
| uintptr_t stack[3]; |
| void* stack_base = stack + arraysize(stack); |
| // Index on the stack for the topmost fp (i.e the one right before the C++ |
| // frame). |
| const int topmost_fp_index = 0; |
| stack[0] = reinterpret_cast<uintptr_t>(stack + 2); // saved FP. |
| stack[1] = 202; // Return address into C++ code. |
| stack[2] = reinterpret_cast<uintptr_t>(stack + 2); // saved SP. |
| |
| register_state.sp = stack; |
| register_state.fp = stack; |
| |
| // Create an on-heap code object. Make sure we run the function so that it is |
| // compiled and not just marked for lazy compilation. |
| CompileRun(foo_source); |
| v8::Local<v8::Function> local_foo = v8::Local<v8::Function>::Cast( |
| env.local()->Global()->Get(env.local(), v8_str("foo")).ToLocalChecked()); |
| Handle<JSFunction> foo = |
| Handle<JSFunction>::cast(v8::Utils::OpenHandle(*local_foo)); |
| |
| // Put the current PC inside of the created code object. |
| AbstractCode abstract_code = foo->abstract_code(); |
| // We don't produce optimized code when run with --no-opt. |
| if (!abstract_code.IsCode() && FLAG_opt == false) return; |
| CHECK(abstract_code.IsCode()); |
| |
| Code code = abstract_code.GetCode(); |
| // We don't want the offset too early or it could be the `push rbp` |
| // instruction (which is not at the start of generated code, because the lazy |
| // deopt check happens before frame setup). |
| const uintptr_t offset = code.InstructionSize() - 20; |
| CHECK_LT(offset, code.InstructionSize()); |
| Address pc = code.InstructionStart() + offset; |
| register_state.pc = reinterpret_cast<void*>(pc); |
| |
| // Get code pages from the API now that the code obejct exists and check that |
| // our code objects is on one of the pages. |
| size_t pages_length = |
| isolate->CopyCodePages(arraysize(code_pages), code_pages); |
| CHECK_LE(pages_length, arraysize(code_pages)); |
| CHECK(PagesContainsAddress(pages_length, code_pages, pc)); |
| |
| bool unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, stack_base); |
| CHECK(unwound); |
| CHECK_EQ_VALUE_REGISTER(stack[topmost_fp_index], register_state.fp); |
| CHECK_EQ_VALUE_REGISTER(stack[topmost_fp_index + 1], register_state.pc); |
| CHECK_EQ_VALUE_REGISTER(stack[topmost_fp_index + 2], register_state.sp); |
| } |
| |
| // If the PC is within JSEntry but we haven't set up the frame yet, then we |
| // cannot unwind. |
| TEST(Unwind_JSEntryBeforeFrame_Fail_CodePagesAPI) { |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| |
| JSEntryStubs entry_stubs = isolate->GetJSEntryStubs(); |
| MemoryRange code_pages[1]; |
| size_t pages_length = 1; |
| RegisterState register_state; |
| |
| const size_t code_length = 40; |
| uintptr_t code[code_length] = {0}; |
| code_pages[0].start = code; |
| code_pages[0].length_in_bytes = code_length * sizeof(uintptr_t); |
| |
| // Pretend that it takes 5 instructions to set up the frame in JSEntry. |
| entry_stubs.js_entry_stub.code.start = code + 10; |
| entry_stubs.js_entry_stub.code.length_in_bytes = 10 * sizeof(uintptr_t); |
| |
| uintptr_t stack[10]; |
| void* stack_base = stack + arraysize(stack); |
| stack[0] = 101; |
| stack[1] = 111; |
| stack[2] = 121; |
| stack[3] = 131; |
| stack[4] = 141; |
| stack[5] = 151; // Here's where the saved fp would be. We are not going to be |
| // unwinding so we do not need to set it up correctly. |
| stack[6] = 100; // Return address into C++ code. |
| stack[7] = 303; // Here's where the saved SP would be. |
| stack[8] = 404; |
| stack[9] = 505; |
| |
| register_state.sp = &stack[5]; |
| register_state.fp = &stack[9]; |
| |
| // Put the current PC inside of JSEntry, before the frame is set up. |
| uintptr_t* jsentry_pc_value = code + 12; |
| register_state.pc = jsentry_pc_value; |
| bool unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, stack_base); |
| CHECK(!unwound); |
| // The register state should not change when unwinding fails. |
| CHECK_EQ_VALUE_REGISTER(&stack[9], register_state.fp); |
| CHECK_EQ_VALUE_REGISTER(&stack[5], register_state.sp); |
| CHECK_EQ(jsentry_pc_value, register_state.pc); |
| |
| // Change the PC to a few instructions later, after the frame is set up. |
| jsentry_pc_value = code + 16; |
| register_state.pc = jsentry_pc_value; |
| unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, stack_base); |
| // TODO(petermarshall): More precisely check position within JSEntry rather |
| // than just assuming the frame is unreadable. |
| CHECK(!unwound); |
| // The register state should not change when unwinding fails. |
| CHECK_EQ_VALUE_REGISTER(&stack[9], register_state.fp); |
| CHECK_EQ_VALUE_REGISTER(&stack[5], register_state.sp); |
| CHECK_EQ(jsentry_pc_value, register_state.pc); |
| } |
| |
| // Creates a fake stack with two JS frames on top of a C++ frame and checks that |
| // the unwinder correctly unwinds past the JS frames and returns the C++ frame's |
| // details. |
| TEST(Unwind_TwoJSFrames_Success_CodePagesAPI) { |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| |
| JSEntryStubs entry_stubs = isolate->GetJSEntryStubs(); |
| MemoryRange code_pages[1]; |
| size_t pages_length = 1; |
| RegisterState register_state; |
| |
| // Use a fake code range so that we can initialize it to 0s. |
| const size_t code_length = 40; |
| uintptr_t code[code_length] = {0}; |
| code_pages[0].start = code; |
| code_pages[0].length_in_bytes = code_length * sizeof(uintptr_t); |
| |
| // Our fake stack has three frames - one C++ frame and two JS frames (on top). |
| // The stack grows from high addresses to low addresses. |
| uintptr_t stack[5 + kJSEntryFrameSpace]; |
| void* stack_base = stack + arraysize(stack); |
| stack[0] = 101; |
| stack[1] = 111; |
| stack[2] = reinterpret_cast<uintptr_t>(stack + 5); // saved FP. |
| // The fake return address is in the JS code range. |
| const void* jsentry_pc = code + 10; |
| stack[3] = reinterpret_cast<uintptr_t>(jsentry_pc); |
| stack[4] = 141; |
| const int top_of_js_entry = 5; |
| BuildJSEntryStack(&stack[top_of_js_entry]); |
| |
| register_state.sp = stack; |
| register_state.fp = stack + 2; |
| |
| // Put the current PC inside of the code range so it looks valid. |
| register_state.pc = code + 30; |
| |
| // Put the PC in the JSEntryRange. |
| entry_stubs.js_entry_stub.code.start = jsentry_pc; |
| entry_stubs.js_entry_stub.code.length_in_bytes = sizeof(uintptr_t); |
| |
| bool unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, stack_base); |
| |
| CHECK(unwound); |
| CHECK_EQ_VALUE_REGISTER(stack[top_of_js_entry + kFPOffset], |
| register_state.fp); |
| CHECK_EQ_VALUE_REGISTER(stack[top_of_js_entry + kPCOffset], |
| register_state.pc); |
| CHECK_EQ_VALUE_REGISTER(stack[top_of_js_entry + kSPOffset], |
| register_state.sp); |
| CheckCalleeSavedRegisters(register_state); |
| } |
| |
| // If the PC is in JSEntry then the frame might not be set up correctly, meaning |
| // we can't unwind the stack properly. |
| TEST(Unwind_JSEntry_Fail_CodePagesAPI) { |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate); |
| |
| JSEntryStubs entry_stubs = isolate->GetJSEntryStubs(); |
| MemoryRange code_pages[v8::Isolate::kMinCodePagesBufferSize]; |
| size_t pages_length = |
| isolate->CopyCodePages(arraysize(code_pages), code_pages); |
| CHECK_LE(pages_length, arraysize(code_pages)); |
| RegisterState register_state; |
| |
| Code js_entry = i_isolate->heap()->builtin(Builtins::kJSEntry); |
| byte* start = reinterpret_cast<byte*>(js_entry.InstructionStart()); |
| register_state.pc = start + 10; |
| |
| bool unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, fake_stack_base); |
| CHECK(!unwound); |
| // The register state should not change when unwinding fails. |
| CHECK_NULL(register_state.fp); |
| CHECK_NULL(register_state.sp); |
| CHECK_EQ(start + 10, register_state.pc); |
| } |
| |
| // Tries to unwind a middle frame (i.e not a JSEntry frame) first with a wrong |
| // stack base, and then with the correct one. |
| TEST(Unwind_StackBounds_Basic_CodePagesAPI) { |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| |
| JSEntryStubs entry_stubs = isolate->GetJSEntryStubs(); |
| MemoryRange code_pages[1]; |
| size_t pages_length = 1; |
| RegisterState register_state; |
| |
| const size_t code_length = 10; |
| uintptr_t code[code_length] = {0}; |
| code_pages[0].start = code; |
| code_pages[0].length_in_bytes = code_length * sizeof(uintptr_t); |
| |
| uintptr_t stack[3]; |
| stack[0] = reinterpret_cast<uintptr_t>(stack + 2); // saved FP. |
| stack[1] = 202; // saved PC. |
| stack[2] = 303; // saved SP. |
| |
| register_state.sp = stack; |
| register_state.fp = stack; |
| register_state.pc = code; |
| |
| void* wrong_stack_base = reinterpret_cast<void*>( |
| reinterpret_cast<uintptr_t>(stack) - sizeof(uintptr_t)); |
| bool unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, wrong_stack_base); |
| CHECK(!unwound); |
| |
| // Correct the stack base and unwinding should succeed. |
| void* correct_stack_base = stack + arraysize(stack); |
| unwound = |
| v8::Unwinder::TryUnwindV8Frames(entry_stubs, pages_length, code_pages, |
| ®ister_state, correct_stack_base); |
| CHECK(unwound); |
| } |
| |
| TEST(Unwind_StackBounds_WithUnwinding_CodePagesAPI) { |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| |
| JSEntryStubs entry_stubs = isolate->GetJSEntryStubs(); |
| MemoryRange code_pages[1]; |
| size_t pages_length = 1; |
| RegisterState register_state; |
| |
| // Use a fake code range so that we can initialize it to 0s. |
| const size_t code_length = 40; |
| uintptr_t code[code_length] = {0}; |
| code_pages[0].start = code; |
| code_pages[0].length_in_bytes = code_length * sizeof(uintptr_t); |
| |
| // Our fake stack has two frames - one C++ frame and one JS frame (on top). |
| // The stack grows from high addresses to low addresses. |
| uintptr_t stack[9 + kJSEntryFrameSpace]; |
| void* stack_base = stack + arraysize(stack); |
| stack[0] = 101; |
| stack[1] = 111; |
| stack[2] = 121; |
| stack[3] = 131; |
| stack[4] = 141; |
| stack[5] = reinterpret_cast<uintptr_t>(stack + 9); // saved FP. |
| const void* jsentry_pc = code + 20; |
| stack[6] = reinterpret_cast<uintptr_t>(jsentry_pc); // JS code. |
| stack[7] = 303; // saved SP. |
| stack[8] = 404; |
| const int top_of_js_entry = 9; |
| BuildJSEntryStack(&stack[top_of_js_entry]); |
| // Override FP and PC |
| stack[top_of_js_entry + kFPOffset] = |
| reinterpret_cast<uintptr_t>(stack) + |
| (9 + kJSEntryFrameSpace + 1) * sizeof(uintptr_t); // saved FP (OOB). |
| stack[top_of_js_entry + kPCOffset] = |
| reinterpret_cast<uintptr_t>(code + 20); // JS code. |
| |
| register_state.sp = stack; |
| register_state.fp = stack + 5; |
| |
| // Put the current PC inside of the code range so it looks valid. |
| register_state.pc = code + 30; |
| |
| // Put the PC in the JSEntryRange. |
| entry_stubs.js_entry_stub.code.start = jsentry_pc; |
| entry_stubs.js_entry_stub.code.length_in_bytes = sizeof(uintptr_t); |
| |
| // Unwind will fail because stack[9] FP points outside of the stack. |
| bool unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, stack_base); |
| CHECK(!unwound); |
| |
| // Change the return address so that it is not in range. We will not range |
| // check the stack's FP value because we have finished unwinding and the |
| // contents of rbp does not necessarily have to be the FP in this case. |
| stack[top_of_js_entry + kPCOffset] = 202; |
| unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, stack_base); |
| CHECK(unwound); |
| CheckCalleeSavedRegisters(register_state); |
| } |
| |
| TEST(PCIsInV8_BadState_Fail_CodePagesAPI) { |
| void* pc = nullptr; |
| size_t pages_length = 0; |
| MemoryRange* code_pages = nullptr; |
| |
| CHECK(!v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| } |
| |
| TEST(PCIsInV8_ValidStateNullPC_Fail_CodePagesAPI) { |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| |
| void* pc = nullptr; |
| |
| MemoryRange code_pages[v8::Isolate::kMinCodePagesBufferSize]; |
| size_t pages_length = |
| isolate->CopyCodePages(arraysize(code_pages), code_pages); |
| CHECK_LE(pages_length, arraysize(code_pages)); |
| |
| CHECK(!v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| } |
| |
| void TestRangeBoundaries(size_t pages_length, MemoryRange* code_pages, |
| byte* range_start, size_t range_length) { |
| void* pc = range_start - 1; |
| CHECK(!v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| pc = range_start; |
| CHECK(v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| pc = range_start + 1; |
| CHECK(v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| pc = range_start + range_length - 1; |
| CHECK(v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| pc = range_start + range_length; |
| CHECK(!v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| pc = range_start + range_length + 1; |
| CHECK(!v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| } |
| |
| TEST(PCIsInV8_InAllCodePages_CodePagesAPI) { |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| |
| MemoryRange code_pages[v8::Isolate::kMinCodePagesBufferSize]; |
| size_t pages_length = |
| isolate->CopyCodePages(arraysize(code_pages), code_pages); |
| CHECK_LE(pages_length, arraysize(code_pages)); |
| |
| for (size_t i = 0; i < pages_length; i++) { |
| byte* range_start = |
| const_cast<byte*>(reinterpret_cast<const byte*>(code_pages[i].start)); |
| size_t range_length = code_pages[i].length_in_bytes; |
| TestRangeBoundaries(pages_length, code_pages, range_start, range_length); |
| } |
| } |
| |
| // PCIsInV8 doesn't check if the PC is in JSEntry directly. It's assumed that |
| // the CodeRange or EmbeddedCodeRange contain JSEntry. |
| TEST(PCIsInV8_InJSEntryRange_CodePagesAPI) { |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate); |
| |
| MemoryRange code_pages[v8::Isolate::kMinCodePagesBufferSize]; |
| size_t pages_length = |
| isolate->CopyCodePages(arraysize(code_pages), code_pages); |
| CHECK_LE(pages_length, arraysize(code_pages)); |
| |
| Code js_entry = i_isolate->heap()->builtin(Builtins::kJSEntry); |
| byte* start = reinterpret_cast<byte*>(js_entry.InstructionStart()); |
| size_t length = js_entry.InstructionSize(); |
| |
| void* pc = start; |
| CHECK(v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| pc = start + 1; |
| CHECK(v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| pc = start + length - 1; |
| CHECK(v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| } |
| |
| // Large code objects can be allocated in large object space. Check that this is |
| // inside the CodeRange. |
| TEST(PCIsInV8_LargeCodeObject_CodePagesAPI) { |
| FLAG_allow_natives_syntax = true; |
| LocalContext env; |
| v8::Isolate* isolate = env->GetIsolate(); |
| Isolate* i_isolate = reinterpret_cast<Isolate*>(isolate); |
| HandleScope scope(i_isolate); |
| |
| // Create a big function that ends up in CODE_LO_SPACE. |
| const int instruction_size = Page::kPageSize + 1; |
| CHECK_GT(instruction_size, MemoryChunkLayout::MaxRegularCodeObjectSize()); |
| std::unique_ptr<byte[]> instructions(new byte[instruction_size]); |
| |
| CodeDesc desc; |
| desc.buffer = instructions.get(); |
| desc.buffer_size = instruction_size; |
| desc.instr_size = instruction_size; |
| desc.reloc_size = 0; |
| desc.constant_pool_size = 0; |
| desc.unwinding_info = nullptr; |
| desc.unwinding_info_size = 0; |
| desc.origin = nullptr; |
| Handle<Code> foo_code = |
| Factory::CodeBuilder(i_isolate, desc, CodeKind::WASM_FUNCTION).Build(); |
| |
| CHECK(i_isolate->heap()->InSpace(*foo_code, CODE_LO_SPACE)); |
| byte* start = reinterpret_cast<byte*>(foo_code->InstructionStart()); |
| |
| MemoryRange code_pages[v8::Isolate::kMinCodePagesBufferSize]; |
| size_t pages_length = |
| isolate->CopyCodePages(arraysize(code_pages), code_pages); |
| CHECK_LE(pages_length, arraysize(code_pages)); |
| |
| void* pc = start; |
| CHECK(v8::Unwinder::PCIsInV8(pages_length, code_pages, pc)); |
| } |
| |
| #ifdef USE_SIMULATOR |
| // TODO(v8:10026): Make this also work without the simulator. The part that |
| // needs modifications is getting the RegisterState. |
| class UnwinderTestHelper { |
| public: |
| explicit UnwinderTestHelper(const std::string& test_function) |
| : isolate_(CcTest::isolate()) { |
| CHECK(!instance_); |
| instance_ = this; |
| v8::HandleScope scope(isolate_); |
| v8::Local<v8::ObjectTemplate> global = v8::ObjectTemplate::New(isolate_); |
| global->Set(v8_str("TryUnwind"), |
| v8::FunctionTemplate::New(isolate_, TryUnwind)); |
| LocalContext env(isolate_, nullptr, global); |
| CompileRun(v8_str(test_function.c_str())); |
| } |
| |
| ~UnwinderTestHelper() { instance_ = nullptr; } |
| |
| private: |
| static void TryUnwind(const v8::FunctionCallbackInfo<v8::Value>& args) { |
| instance_->DoTryUnwind(); |
| } |
| |
| void DoTryUnwind() { |
| // Set up RegisterState. |
| v8::RegisterState register_state; |
| SimulatorHelper simulator_helper; |
| if (!simulator_helper.Init(isolate_)) return; |
| simulator_helper.FillRegisters(®ister_state); |
| // At this point, the PC will point to a Redirection object, which is not |
| // in V8 as far as the unwinder is concerned. To make this work, point to |
| // the return address, which is in V8, instead. |
| register_state.pc = register_state.lr; |
| |
| JSEntryStubs entry_stubs = isolate_->GetJSEntryStubs(); |
| MemoryRange code_pages[v8::Isolate::kMinCodePagesBufferSize]; |
| size_t pages_length = |
| isolate_->CopyCodePages(arraysize(code_pages), code_pages); |
| CHECK_LE(pages_length, arraysize(code_pages)); |
| |
| void* stack_base = reinterpret_cast<void*>(0xffffffffffffffffL); |
| bool unwound = v8::Unwinder::TryUnwindV8Frames( |
| entry_stubs, pages_length, code_pages, ®ister_state, stack_base); |
| // Check that we have successfully unwound past js_entry_sp. |
| CHECK(unwound); |
| CHECK_GT(register_state.sp, |
| reinterpret_cast<void*>(CcTest::i_isolate()->js_entry_sp())); |
| } |
| |
| v8::Isolate* isolate_; |
| |
| static UnwinderTestHelper* instance_; |
| }; |
| |
| UnwinderTestHelper* UnwinderTestHelper::instance_; |
| |
| TEST(Unwind_TwoNestedFunctions_CodePagesAPI) { |
| i::FLAG_allow_natives_syntax = true; |
| const char* test_script = |
| "function test_unwinder_api_inner() {" |
| " TryUnwind();" |
| " return 0;" |
| "}" |
| "function test_unwinder_api_outer() {" |
| " return test_unwinder_api_inner();" |
| "}" |
| "%NeverOptimizeFunction(test_unwinder_api_inner);" |
| "%NeverOptimizeFunction(test_unwinder_api_outer);" |
| "test_unwinder_api_outer();"; |
| |
| UnwinderTestHelper helper(test_script); |
| } |
| #endif |
| |
| #undef CHECK_EQ_VALUE_REGISTER |
| } // namespace test_unwinder_code_pages |
| } // namespace internal |
| } // namespace v8 |