diff --git a/Meta/gn/secondary/Userland/Libraries/LibJS/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibJS/BUILD.gn index eb84c7e41f..f77e404f55 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibJS/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibJS/BUILD.gn @@ -6,6 +6,7 @@ shared_library("LibJS") { # FIXME: Why does LibSyntax need to depend on WindowServer headers? "//Userland", ] + cflags_cc = [ "-fno-omit-frame-pointer" ] deps = [ "//AK", "//Userland/Libraries/LibCore", diff --git a/Userland/Libraries/LibJS/Bytecode/Interpreter.cpp b/Userland/Libraries/LibJS/Bytecode/Interpreter.cpp index 0701b9cc9a..a1bb4227d0 100644 --- a/Userland/Libraries/LibJS/Bytecode/Interpreter.cpp +++ b/Userland/Libraries/LibJS/Bytecode/Interpreter.cpp @@ -56,13 +56,6 @@ void Interpreter::visit_edges(Cell::Visitor& visitor) } } -Optional Interpreter::instruction_stream_iterator() const -{ - if (m_current_executable && m_current_executable->native_executable()) - return m_current_executable->native_executable()->instruction_stream_iterator(*m_current_executable); - return m_pc; -} - // 16.1.6 ScriptEvaluation ( scriptRecord ), https://tc39.es/ecma262/#sec-runtime-semantics-scriptevaluation ThrowCompletionOr Interpreter::run(Script& script_record, JS::GCPtr lexical_environment_override) { @@ -371,6 +364,8 @@ Interpreter::ValueAndFrame Interpreter::run_and_return_frame(Executable& executa else push_call_frame(make(), executable.number_of_registers); + vm().execution_context_stack().last()->executable = &executable; + if (auto native_executable = executable.get_or_create_native_executable()) { native_executable->run(vm()); diff --git a/Userland/Libraries/LibJS/Bytecode/Interpreter.h b/Userland/Libraries/LibJS/Bytecode/Interpreter.h index bb98c8f746..05772b1e4f 100644 --- a/Userland/Libraries/LibJS/Bytecode/Interpreter.h +++ b/Userland/Libraries/LibJS/Bytecode/Interpreter.h @@ -78,7 +78,7 @@ public: Executable& current_executable() { return *m_current_executable; } Executable const& current_executable() const { return *m_current_executable; } BasicBlock const& current_block() const { return *m_current_block; } - Optional instruction_stream_iterator() const; + Optional instruction_stream_iterator() const { return m_pc; } void visit_edges(Cell::Visitor&); diff --git a/Userland/Libraries/LibJS/CMakeLists.txt b/Userland/Libraries/LibJS/CMakeLists.txt index 20a04b05c4..68d93adbec 100644 --- a/Userland/Libraries/LibJS/CMakeLists.txt +++ b/Userland/Libraries/LibJS/CMakeLists.txt @@ -271,3 +271,4 @@ target_link_libraries(LibJS PRIVATE LibCore LibCrypto LibFileSystem LibRegex Lib if("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64") target_link_libraries(LibJS PRIVATE LibX86) endif() +target_compile_options(LibJS PRIVATE -fno-omit-frame-pointer) diff --git a/Userland/Libraries/LibJS/JIT/NativeExecutable.cpp b/Userland/Libraries/LibJS/JIT/NativeExecutable.cpp index ee21feb599..1ddeffedb0 100644 --- a/Userland/Libraries/LibJS/JIT/NativeExecutable.cpp +++ b/Userland/Libraries/LibJS/JIT/NativeExecutable.cpp @@ -12,15 +12,6 @@ #include #include -#if __has_include() -# include -# define EXECINFO_BACKTRACE -#endif - -#if defined(AK_OS_ANDROID) && (__ANDROID_API__ < 33) -# undef EXECINFO_BACKTRACE -#endif - namespace JS::JIT { NativeExecutable::NativeExecutable(void* code, size_t size, Vector mapping) @@ -159,32 +150,20 @@ BytecodeMapping const& NativeExecutable::find_mapping_entry(size_t native_offset return m_mapping[nearby_index]; } -Optional NativeExecutable::instruction_stream_iterator([[maybe_unused]] Bytecode::Executable const& executable) const +Optional NativeExecutable::get_source_range(Bytecode::Executable const& executable, FlatPtr address) const { -#ifdef EXECINFO_BACKTRACE - void* buffer[10]; - auto count = backtrace(buffer, 10); auto start = bit_cast(m_code); auto end = start + m_size; - for (auto i = 0; i < count; i++) { - auto address = bit_cast(buffer[i]); - if (address < start || address >= end) - continue; - // return address points after the call - // let's subtract 1 to make sure we don't hit the next bytecode - // (in practice that's not necessary, because our native_call() sequence continues) - auto offset = address - start - 1; - auto& entry = find_mapping_entry(offset); - if (entry.block_index < executable.basic_blocks.size()) { - auto const& block = *executable.basic_blocks[entry.block_index]; - if (entry.bytecode_offset < block.size()) { - // This is rather clunky, but Interpreter::instruction_stream_iterator() gives out references, so we need to keep it alive. - m_instruction_stream_iterator = make(block.instruction_stream(), &executable, entry.bytecode_offset); - return *m_instruction_stream_iterator; - } + if (address < start || address >= end) + return {}; + auto const& entry = find_mapping_entry(address - start - 1); + if (entry.block_index < executable.basic_blocks.size()) { + auto const& block = *executable.basic_blocks[entry.block_index]; + if (entry.bytecode_offset < block.size()) { + auto iterator = Bytecode::InstructionStreamIterator { block.instruction_stream(), &executable, entry.bytecode_offset }; + return iterator.source_range(); } } -#endif return {}; } diff --git a/Userland/Libraries/LibJS/JIT/NativeExecutable.h b/Userland/Libraries/LibJS/JIT/NativeExecutable.h index d8790618de..62003b15fb 100644 --- a/Userland/Libraries/LibJS/JIT/NativeExecutable.h +++ b/Userland/Libraries/LibJS/JIT/NativeExecutable.h @@ -34,7 +34,7 @@ public: void run(VM&) const; void dump_disassembly(Bytecode::Executable const& executable) const; BytecodeMapping const& find_mapping_entry(size_t native_offset) const; - Optional instruction_stream_iterator(Bytecode::Executable const& executable) const; + Optional get_source_range(Bytecode::Executable const& executable, FlatPtr address) const; ReadonlyBytes code_bytes() const { return { m_code, m_size }; } diff --git a/Userland/Libraries/LibJS/Runtime/Error.cpp b/Userland/Libraries/LibJS/Runtime/Error.cpp index 4c238b2749..07b3121b93 100644 --- a/Userland/Libraries/LibJS/Runtime/Error.cpp +++ b/Userland/Libraries/LibJS/Runtime/Error.cpp @@ -75,13 +75,13 @@ ThrowCompletionOr Error::install_error_cause(Value options) void Error::populate_stack() { - auto& vm = this->vm(); - m_traceback.ensure_capacity(vm.execution_context_stack().size()); - for (ssize_t i = vm.execution_context_stack().size() - 1; i >= 0; i--) { - auto context = vm.execution_context_stack()[i]; + auto stack_trace = vm().stack_trace(); + m_traceback.ensure_capacity(stack_trace.size()); + for (auto& element : stack_trace) { + auto* context = element.execution_context; UnrealizedSourceRange range = {}; - if (context->instruction_stream_iterator.has_value()) - range = context->instruction_stream_iterator->source_range(); + if (element.source_range.has_value()) + range = element.source_range.value(); TracebackFrame frame { .function_name = context->function_name, .source_range_storage = range, diff --git a/Userland/Libraries/LibJS/Runtime/ExecutionContext.h b/Userland/Libraries/LibJS/Runtime/ExecutionContext.h index ae61de84c3..55de4aec24 100644 --- a/Userland/Libraries/LibJS/Runtime/ExecutionContext.h +++ b/Userland/Libraries/LibJS/Runtime/ExecutionContext.h @@ -51,9 +51,16 @@ public: MarkedVector local_variables; bool is_strict_mode { false }; + RefPtr executable; + // https://html.spec.whatwg.org/multipage/webappapis.html#skip-when-determining-incumbent-counter // FIXME: Move this out of LibJS (e.g. by using the CustomData concept), as it's used exclusively by LibWeb. size_t skip_when_determining_incumbent_counter { 0 }; }; +struct StackTraceElement { + ExecutionContext* execution_context; + Optional source_range; +}; + } diff --git a/Userland/Libraries/LibJS/Runtime/VM.cpp b/Userland/Libraries/LibJS/Runtime/VM.cpp index c292eff63b..ad020f2e81 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.cpp +++ b/Userland/Libraries/LibJS/Runtime/VM.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -1135,4 +1136,66 @@ void VM::pop_execution_context() on_call_stack_emptied(); } +#if ARCH(X86_64) +struct [[gnu::packed]] NativeStackFrame { + NativeStackFrame* prev; + FlatPtr return_address; +}; +#endif + +Vector VM::get_native_stack_trace() const +{ + Vector buffer; +#if ARCH(X86_64) + // Manually walk the stack, because backtrace() does not traverse through JIT frames. + auto* frame = bit_cast(__builtin_frame_address(0)); + while (bit_cast(frame) < m_stack_info.top() && bit_cast(frame) >= m_stack_info.base()) { + buffer.append(frame->return_address); + frame = frame->prev; + } +#endif + return buffer; +} + +static Optional get_source_range(ExecutionContext const* context, Vector const& native_stack) +{ + // native function + if (!context->executable) + return {}; + + auto const* native_executable = context->executable->native_executable(); + if (!native_executable) { + // Interpreter frame + if (context->instruction_stream_iterator.has_value()) + return context->instruction_stream_iterator->source_range(); + return {}; + } + + // JIT frame + for (auto address : native_stack) { + auto range = native_executable->get_source_range(*context->executable, address); + if (range.has_value()) { + auto realized = range->realize(); + return range; + } + } + + return {}; +} + +Vector VM::stack_trace() const +{ + auto native_stack = get_native_stack_trace(); + Vector stack_trace; + for (ssize_t i = m_execution_context_stack.size() - 1; i >= 0; i--) { + auto* context = m_execution_context_stack[i]; + stack_trace.append({ + .execution_context = context, + .source_range = get_source_range(context, native_stack).value_or({}), + }); + } + + return stack_trace; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/VM.h b/Userland/Libraries/LibJS/Runtime/VM.h index 1c7008c13c..78f0bad1bc 100644 --- a/Userland/Libraries/LibJS/Runtime/VM.h +++ b/Userland/Libraries/LibJS/Runtime/VM.h @@ -254,6 +254,8 @@ public: // NOTE: This is meant as a temporary stopgap until everything is bytecode. ThrowCompletionOr execute_ast_node(ASTNode const&); + Vector stack_trace() const; + private: using ErrorMessages = AK::Array; @@ -277,6 +279,8 @@ private: void set_well_known_symbols(WellKnownSymbols well_known_symbols) { m_well_known_symbols = move(well_known_symbols); } + Vector get_native_stack_trace() const; + HashMap> m_string_cache; HashMap> m_deprecated_string_cache; diff --git a/Userland/Libraries/LibJS/Tests/builtins/Error/Error.prototype.stack.js b/Userland/Libraries/LibJS/Tests/builtins/Error/Error.prototype.stack.js index 0929819d27..36eb449821 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Error/Error.prototype.stack.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Error/Error.prototype.stack.js @@ -5,7 +5,7 @@ const stackSetter = stackDescriptor.set; describe("getter - normal behavior", () => { test("basic functionality", () => { const stackFrames = [ - /^ at .*Error \(.*\/Error\.prototype\.stack\.js:\d+:\d+\)$/, + /^ at .*Error$/, /^ at .+\/Error\/Error\.prototype\.stack\.js:\d+:\d+$/, /^ at test \(.+\/test-common.js:\d+:\d+\)$/, /^ at .+\/Error\/Error\.prototype\.stack\.js:6:9$/,