| //===-- TraceDumper.cpp ---------------------------------------------------===// | |
| // | |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| // | |
| //===----------------------------------------------------------------------===// | |
| #include "lldb/Target/TraceDumper.h" | |
| #include "lldb/Core/Module.h" | |
| #include "lldb/Symbol/CompileUnit.h" | |
| #include "lldb/Symbol/Function.h" | |
| #include "lldb/Target/ExecutionContext.h" | |
| #include "lldb/Target/Process.h" | |
| #include "lldb/Target/SectionLoadList.h" | |
| #include <optional> | |
| using namespace lldb; | |
| using namespace lldb_private; | |
| using namespace llvm; | |
| /// \return | |
| /// The given string or \b std::nullopt if it's empty. | |
| static std::optional<llvm::StringRef> ToOptionalString(llvm::StringRef s) { | |
| if (s.empty()) | |
| return std::nullopt; | |
| return s; | |
| } | |
| static llvm::StringRef GetModuleName(const SymbolContext &sc) { | |
| if (!sc.module_sp) | |
| return {}; | |
| return sc.module_sp->GetFileSpec().GetFilename(); | |
| } | |
| /// \return | |
| /// The module name (basename if the module is a file, or the actual name if | |
| /// it's a virtual module), or \b nullptr if no name nor module was found. | |
| static llvm::StringRef GetModuleName(const TraceDumper::TraceItem &item) { | |
| if (!item.symbol_info) | |
| return {}; | |
| return GetModuleName(item.symbol_info->sc); | |
| } | |
| // This custom LineEntry validator is neded because some line_entries have | |
| // 0 as line, which is meaningless. Notice that LineEntry::IsValid only | |
| // checks that line is not LLDB_INVALID_LINE_NUMBER, i.e. UINT32_MAX. | |
| static bool IsLineEntryValid(const LineEntry &line_entry) { | |
| return line_entry.IsValid() && line_entry.line > 0; | |
| } | |
| /// \return | |
| /// \b true if the provided line entries match line, column and source file. | |
| /// This function assumes that the line entries are valid. | |
| static bool FileLineAndColumnMatches(const LineEntry &a, const LineEntry &b) { | |
| if (a.line != b.line) | |
| return false; | |
| if (a.column != b.column) | |
| return false; | |
| return a.GetFile() == b.GetFile(); | |
| } | |
| /// Compare the symbol contexts of the provided \a SymbolInfo | |
| /// objects. | |
| /// | |
| /// \return | |
| /// \a true if both instructions belong to the same scope level analized | |
| /// in the following order: | |
| /// - module | |
| /// - symbol | |
| /// - function | |
| /// - inlined function | |
| /// - source line info | |
| static bool | |
| IsSameInstructionSymbolContext(const TraceDumper::SymbolInfo &prev_insn, | |
| const TraceDumper::SymbolInfo &insn, | |
| bool check_source_line_info = true) { | |
| // module checks | |
| if (insn.sc.module_sp != prev_insn.sc.module_sp) | |
| return false; | |
| // symbol checks | |
| if (insn.sc.symbol != prev_insn.sc.symbol) | |
| return false; | |
| // function checks | |
| if (!insn.sc.function && !prev_insn.sc.function) | |
| return true; // This means two dangling instruction in the same module. We | |
| // can assume they are part of the same unnamed symbol | |
| else if (insn.sc.function != prev_insn.sc.function) | |
| return false; | |
| Block *inline_block_a = | |
| insn.sc.block ? insn.sc.block->GetContainingInlinedBlock() : nullptr; | |
| Block *inline_block_b = prev_insn.sc.block | |
| ? prev_insn.sc.block->GetContainingInlinedBlock() | |
| : nullptr; | |
| if (inline_block_a != inline_block_b) | |
| return false; | |
| // line entry checks | |
| if (!check_source_line_info) | |
| return true; | |
| const bool curr_line_valid = IsLineEntryValid(insn.sc.line_entry); | |
| const bool prev_line_valid = IsLineEntryValid(prev_insn.sc.line_entry); | |
| if (curr_line_valid && prev_line_valid) | |
| return FileLineAndColumnMatches(insn.sc.line_entry, | |
| prev_insn.sc.line_entry); | |
| return curr_line_valid == prev_line_valid; | |
| } | |
| class OutputWriterCLI : public TraceDumper::OutputWriter { | |
| public: | |
| OutputWriterCLI(Stream &s, const TraceDumperOptions &options, Thread &thread) | |
| : m_s(s), m_options(options) { | |
| m_s.Format("thread #{0}: tid = {1}\n", thread.GetIndexID(), thread.GetID()); | |
| }; | |
| void NoMoreData() override { m_s << " no more data\n"; } | |
| void FunctionCallForest( | |
| const std::vector<TraceDumper::FunctionCallUP> &forest) override { | |
| for (size_t i = 0; i < forest.size(); i++) { | |
| m_s.Format("\n[call tree #{0}]\n", i); | |
| DumpFunctionCallTree(*forest[i]); | |
| } | |
| } | |
| void TraceItem(const TraceDumper::TraceItem &item) override { | |
| if (item.symbol_info) { | |
| if (!item.prev_symbol_info || | |
| !IsSameInstructionSymbolContext(*item.prev_symbol_info, | |
| *item.symbol_info)) { | |
| m_s << " "; | |
| llvm::StringRef module_name = GetModuleName(item); | |
| if (module_name.empty()) | |
| m_s << "(none)"; | |
| else if (!item.symbol_info->sc.function && !item.symbol_info->sc.symbol) | |
| m_s.Format("{0}`(none)", module_name); | |
| else | |
| item.symbol_info->sc.DumpStopContext( | |
| &m_s, item.symbol_info->exe_ctx.GetTargetPtr(), | |
| item.symbol_info->address, | |
| /*show_fullpaths=*/false, | |
| /*show_module=*/true, /*show_inlined_frames=*/false, | |
| /*show_function_arguments=*/true, | |
| /*show_function_name=*/true); | |
| m_s << "\n"; | |
| } | |
| } | |
| if (item.error && !m_was_prev_instruction_an_error) | |
| m_s << " ...missing instructions\n"; | |
| m_s.Format(" {0}: ", item.id); | |
| if (m_options.show_timestamps) { | |
| if (item.timestamp) | |
| m_s << formatv("[{0:3} ns]", *item.timestamp); | |
| else | |
| m_s << "[unavailable]"; | |
| } | |
| if (item.event) { | |
| m_s << "(event) " << TraceCursor::EventKindToString(*item.event); | |
| switch (*item.event) { | |
| case eTraceEventCPUChanged: | |
| m_s.Format(" [new CPU={0}]", | |
| item.cpu_id ? std::to_string(*item.cpu_id) : "unavailable"); | |
| break; | |
| case eTraceEventHWClockTick: | |
| m_s.Format(" [{0}]", item.hw_clock ? std::to_string(*item.hw_clock) | |
| : "unavailable"); | |
| break; | |
| case eTraceEventDisabledHW: | |
| case eTraceEventDisabledSW: | |
| break; | |
| case eTraceEventSyncPoint: | |
| m_s.Format(" [{0}]", item.sync_point_metadata); | |
| break; | |
| } | |
| } else if (item.error) { | |
| m_s << "(error) " << *item.error; | |
| } else { | |
| m_s.Format("{0:x+16}", item.load_address); | |
| if (item.symbol_info && item.symbol_info->instruction) { | |
| m_s << " "; | |
| item.symbol_info->instruction->Dump( | |
| &m_s, /*max_opcode_byte_size=*/0, | |
| /*show_address=*/false, | |
| /*show_bytes=*/false, m_options.show_control_flow_kind, | |
| &item.symbol_info->exe_ctx, &item.symbol_info->sc, | |
| /*prev_sym_ctx=*/nullptr, | |
| /*disassembly_addr_format=*/nullptr, | |
| /*max_address_text_size=*/0); | |
| } | |
| } | |
| m_was_prev_instruction_an_error = (bool)item.error; | |
| m_s << "\n"; | |
| } | |
| private: | |
| void | |
| DumpSegmentContext(const TraceDumper::FunctionCall::TracedSegment &segment) { | |
| if (segment.GetOwningCall().IsError()) { | |
| m_s << "<tracing errors>"; | |
| return; | |
| } | |
| const SymbolContext &first_sc = segment.GetFirstInstructionSymbolInfo().sc; | |
| first_sc.DumpStopContext( | |
| &m_s, segment.GetFirstInstructionSymbolInfo().exe_ctx.GetTargetPtr(), | |
| segment.GetFirstInstructionSymbolInfo().address, | |
| /*show_fullpaths=*/false, | |
| /*show_module=*/true, /*show_inlined_frames=*/false, | |
| /*show_function_arguments=*/true, | |
| /*show_function_name=*/true); | |
| m_s << " to "; | |
| const SymbolContext &last_sc = segment.GetLastInstructionSymbolInfo().sc; | |
| if (IsLineEntryValid(first_sc.line_entry) && | |
| IsLineEntryValid(last_sc.line_entry)) { | |
| m_s.Format("{0}:{1}", last_sc.line_entry.line, last_sc.line_entry.column); | |
| } else { | |
| last_sc.DumpStopContext( | |
| &m_s, segment.GetFirstInstructionSymbolInfo().exe_ctx.GetTargetPtr(), | |
| segment.GetLastInstructionSymbolInfo().address, | |
| /*show_fullpaths=*/false, | |
| /*show_module=*/false, /*show_inlined_frames=*/false, | |
| /*show_function_arguments=*/false, | |
| /*show_function_name=*/false); | |
| } | |
| } | |
| void DumpUntracedContext(const TraceDumper::FunctionCall &function_call) { | |
| if (function_call.IsError()) { | |
| m_s << "tracing error"; | |
| } | |
| const SymbolContext &sc = function_call.GetSymbolInfo().sc; | |
| llvm::StringRef module_name = GetModuleName(sc); | |
| if (module_name.empty()) | |
| m_s << "(none)"; | |
| else if (!sc.function && !sc.symbol) | |
| m_s << module_name << "`(none)"; | |
| else | |
| m_s << module_name << "`" << sc.GetFunctionName(); | |
| } | |
| void DumpFunctionCallTree(const TraceDumper::FunctionCall &function_call) { | |
| if (function_call.GetUntracedPrefixSegment()) { | |
| m_s.Indent(); | |
| DumpUntracedContext(function_call); | |
| m_s << "\n"; | |
| m_s.IndentMore(); | |
| DumpFunctionCallTree(function_call.GetUntracedPrefixSegment()->GetNestedCall()); | |
| m_s.IndentLess(); | |
| } | |
| for (const TraceDumper::FunctionCall::TracedSegment &segment : | |
| function_call.GetTracedSegments()) { | |
| m_s.Indent(); | |
| DumpSegmentContext(segment); | |
| m_s.Format(" [{0}, {1}]\n", segment.GetFirstInstructionID(), | |
| segment.GetLastInstructionID()); | |
| segment.IfNestedCall([&](const TraceDumper::FunctionCall &nested_call) { | |
| m_s.IndentMore(); | |
| DumpFunctionCallTree(nested_call); | |
| m_s.IndentLess(); | |
| }); | |
| } | |
| } | |
| Stream &m_s; | |
| TraceDumperOptions m_options; | |
| bool m_was_prev_instruction_an_error = false; | |
| }; | |
| class OutputWriterJSON : public TraceDumper::OutputWriter { | |
| /* schema: | |
| error_message: string | |
| | { | |
| "event": string, | |
| "id": decimal, | |
| "tsc"?: string decimal, | |
| "cpuId"? decimal, | |
| } | { | |
| "error": string, | |
| "id": decimal, | |
| "tsc"?: string decimal, | |
| | { | |
| "loadAddress": string decimal, | |
| "id": decimal, | |
| "hwClock"?: string decimal, | |
| "syncPointMetadata"?: string, | |
| "timestamp_ns"?: string decimal, | |
| "module"?: string, | |
| "symbol"?: string, | |
| "line"?: decimal, | |
| "column"?: decimal, | |
| "source"?: string, | |
| "mnemonic"?: string, | |
| "controlFlowKind"?: string, | |
| } | |
| */ | |
| public: | |
| OutputWriterJSON(Stream &s, const TraceDumperOptions &options) | |
| : m_s(s), m_options(options), | |
| m_j(m_s.AsRawOstream(), | |
| /*IndentSize=*/options.pretty_print_json ? 2 : 0) { | |
| m_j.arrayBegin(); | |
| }; | |
| ~OutputWriterJSON() { m_j.arrayEnd(); } | |
| void FunctionCallForest( | |
| const std::vector<TraceDumper::FunctionCallUP> &forest) override { | |
| for (size_t i = 0; i < forest.size(); i++) { | |
| m_j.object([&] { DumpFunctionCallTree(*forest[i]); }); | |
| } | |
| } | |
| void DumpFunctionCallTree(const TraceDumper::FunctionCall &function_call) { | |
| if (function_call.GetUntracedPrefixSegment()) { | |
| m_j.attributeObject("untracedPrefixSegment", [&] { | |
| m_j.attributeObject("nestedCall", [&] { | |
| DumpFunctionCallTree( | |
| function_call.GetUntracedPrefixSegment()->GetNestedCall()); | |
| }); | |
| }); | |
| } | |
| if (!function_call.GetTracedSegments().empty()) { | |
| m_j.attributeArray("tracedSegments", [&] { | |
| for (const TraceDumper::FunctionCall::TracedSegment &segment : | |
| function_call.GetTracedSegments()) { | |
| m_j.object([&] { | |
| m_j.attribute("firstInstructionId", | |
| std::to_string(segment.GetFirstInstructionID())); | |
| m_j.attribute("lastInstructionId", | |
| std::to_string(segment.GetLastInstructionID())); | |
| segment.IfNestedCall( | |
| [&](const TraceDumper::FunctionCall &nested_call) { | |
| m_j.attributeObject( | |
| "nestedCall", [&] { DumpFunctionCallTree(nested_call); }); | |
| }); | |
| }); | |
| } | |
| }); | |
| } | |
| } | |
| void DumpEvent(const TraceDumper::TraceItem &item) { | |
| m_j.attribute("event", TraceCursor::EventKindToString(*item.event)); | |
| switch (*item.event) { | |
| case eTraceEventCPUChanged: | |
| m_j.attribute("cpuId", item.cpu_id); | |
| break; | |
| case eTraceEventHWClockTick: | |
| m_j.attribute("hwClock", item.hw_clock); | |
| break; | |
| case eTraceEventDisabledHW: | |
| case eTraceEventDisabledSW: | |
| break; | |
| case eTraceEventSyncPoint: | |
| m_j.attribute("syncPointMetadata", item.sync_point_metadata); | |
| break; | |
| } | |
| } | |
| void DumpInstruction(const TraceDumper::TraceItem &item) { | |
| m_j.attribute("loadAddress", formatv("{0:x}", item.load_address)); | |
| if (item.symbol_info) { | |
| m_j.attribute("module", ToOptionalString(GetModuleName(item))); | |
| m_j.attribute( | |
| "symbol", | |
| ToOptionalString( | |
| item.symbol_info->sc.GetFunctionName().AsCString(nullptr))); | |
| if (lldb::InstructionSP instruction = item.symbol_info->instruction) { | |
| ExecutionContext exe_ctx = item.symbol_info->exe_ctx; | |
| m_j.attribute("mnemonic", | |
| ToOptionalString(instruction->GetMnemonic(&exe_ctx))); | |
| if (m_options.show_control_flow_kind) { | |
| lldb::InstructionControlFlowKind instruction_control_flow_kind = | |
| instruction->GetControlFlowKind(&exe_ctx); | |
| m_j.attribute("controlFlowKind", | |
| ToOptionalString( | |
| Instruction::GetNameForInstructionControlFlowKind( | |
| instruction_control_flow_kind))); | |
| } | |
| } | |
| if (IsLineEntryValid(item.symbol_info->sc.line_entry)) { | |
| m_j.attribute( | |
| "source", | |
| ToOptionalString( | |
| item.symbol_info->sc.line_entry.GetFile().GetPath().c_str())); | |
| m_j.attribute("line", item.symbol_info->sc.line_entry.line); | |
| m_j.attribute("column", item.symbol_info->sc.line_entry.column); | |
| } | |
| } | |
| } | |
| void TraceItem(const TraceDumper::TraceItem &item) override { | |
| m_j.object([&] { | |
| m_j.attribute("id", item.id); | |
| if (m_options.show_timestamps) | |
| m_j.attribute("timestamp_ns", item.timestamp | |
| ? std::optional<std::string>( | |
| std::to_string(*item.timestamp)) | |
| : std::nullopt); | |
| if (item.event) { | |
| DumpEvent(item); | |
| } else if (item.error) { | |
| m_j.attribute("error", *item.error); | |
| } else { | |
| DumpInstruction(item); | |
| } | |
| }); | |
| } | |
| private: | |
| Stream &m_s; | |
| TraceDumperOptions m_options; | |
| json::OStream m_j; | |
| }; | |
| static std::unique_ptr<TraceDumper::OutputWriter> | |
| CreateWriter(Stream &s, const TraceDumperOptions &options, Thread &thread) { | |
| if (options.json) | |
| return std::unique_ptr<TraceDumper::OutputWriter>( | |
| new OutputWriterJSON(s, options)); | |
| else | |
| return std::unique_ptr<TraceDumper::OutputWriter>( | |
| new OutputWriterCLI(s, options, thread)); | |
| } | |
| TraceDumper::TraceDumper(lldb::TraceCursorSP cursor_sp, Stream &s, | |
| const TraceDumperOptions &options) | |
| : m_cursor_sp(std::move(cursor_sp)), m_options(options), | |
| m_writer_up(CreateWriter( | |
| s, m_options, *m_cursor_sp->GetExecutionContextRef().GetThreadSP())) { | |
| if (m_options.id) | |
| m_cursor_sp->GoToId(*m_options.id); | |
| else if (m_options.forwards) | |
| m_cursor_sp->Seek(0, lldb::eTraceCursorSeekTypeBeginning); | |
| else | |
| m_cursor_sp->Seek(0, lldb::eTraceCursorSeekTypeEnd); | |
| m_cursor_sp->SetForwards(m_options.forwards); | |
| if (m_options.skip) { | |
| m_cursor_sp->Seek((m_options.forwards ? 1 : -1) * *m_options.skip, | |
| lldb::eTraceCursorSeekTypeCurrent); | |
| } | |
| } | |
| TraceDumper::TraceItem TraceDumper::CreatRawTraceItem() { | |
| TraceItem item = {}; | |
| item.id = m_cursor_sp->GetId(); | |
| if (m_options.show_timestamps) | |
| item.timestamp = m_cursor_sp->GetWallClockTime(); | |
| return item; | |
| } | |
| /// Find the symbol context for the given address reusing the previous | |
| /// instruction's symbol context when possible. | |
| static SymbolContext | |
| CalculateSymbolContext(const Address &address, | |
| const SymbolContext &prev_symbol_context) { | |
| lldb_private::AddressRange range; | |
| if (prev_symbol_context.GetAddressRange(eSymbolContextEverything, 0, | |
| /*inline_block_range*/ true, range) && | |
| range.Contains(address)) | |
| return prev_symbol_context; | |
| SymbolContext sc; | |
| address.CalculateSymbolContext(&sc, eSymbolContextEverything); | |
| return sc; | |
| } | |
| /// Find the disassembler for the given address reusing the previous | |
| /// instruction's disassembler when possible. | |
| static std::tuple<DisassemblerSP, InstructionSP> | |
| CalculateDisass(const TraceDumper::SymbolInfo &symbol_info, | |
| const TraceDumper::SymbolInfo &prev_symbol_info, | |
| const ExecutionContext &exe_ctx) { | |
| if (prev_symbol_info.disassembler) { | |
| if (InstructionSP instruction = | |
| prev_symbol_info.disassembler->GetInstructionList() | |
| .GetInstructionAtAddress(symbol_info.address)) | |
| return std::make_tuple(prev_symbol_info.disassembler, instruction); | |
| } | |
| if (symbol_info.sc.function) { | |
| if (DisassemblerSP disassembler = | |
| symbol_info.sc.function->GetInstructions(exe_ctx, nullptr)) { | |
| if (InstructionSP instruction = | |
| disassembler->GetInstructionList().GetInstructionAtAddress( | |
| symbol_info.address)) | |
| return std::make_tuple(disassembler, instruction); | |
| } | |
| } | |
| // We fallback to a single instruction disassembler | |
| Target &target = exe_ctx.GetTargetRef(); | |
| const ArchSpec arch = target.GetArchitecture(); | |
| lldb_private::AddressRange range(symbol_info.address, | |
| arch.GetMaximumOpcodeByteSize()); | |
| DisassemblerSP disassembler = Disassembler::DisassembleRange( | |
| arch, /*plugin_name=*/nullptr, | |
| /*flavor=*/nullptr, /*cpu=*/nullptr, /*features=*/nullptr, target, range); | |
| return std::make_tuple( | |
| disassembler, | |
| disassembler ? disassembler->GetInstructionList().GetInstructionAtAddress( | |
| symbol_info.address) | |
| : InstructionSP()); | |
| } | |
| static TraceDumper::SymbolInfo | |
| CalculateSymbolInfo(const ExecutionContext &exe_ctx, lldb::addr_t load_address, | |
| const TraceDumper::SymbolInfo &prev_symbol_info) { | |
| TraceDumper::SymbolInfo symbol_info; | |
| symbol_info.exe_ctx = exe_ctx; | |
| symbol_info.address.SetLoadAddress(load_address, exe_ctx.GetTargetPtr()); | |
| symbol_info.sc = | |
| CalculateSymbolContext(symbol_info.address, prev_symbol_info.sc); | |
| std::tie(symbol_info.disassembler, symbol_info.instruction) = | |
| CalculateDisass(symbol_info, prev_symbol_info, exe_ctx); | |
| return symbol_info; | |
| } | |
| std::optional<lldb::user_id_t> TraceDumper::DumpInstructions(size_t count) { | |
| ThreadSP thread_sp = m_cursor_sp->GetExecutionContextRef().GetThreadSP(); | |
| SymbolInfo prev_symbol_info; | |
| std::optional<lldb::user_id_t> last_id; | |
| ExecutionContext exe_ctx; | |
| thread_sp->GetProcess()->GetTarget().CalculateExecutionContext(exe_ctx); | |
| for (size_t insn_seen = 0; insn_seen < count && m_cursor_sp->HasValue(); | |
| m_cursor_sp->Next()) { | |
| last_id = m_cursor_sp->GetId(); | |
| TraceItem item = CreatRawTraceItem(); | |
| if (m_cursor_sp->IsEvent() && m_options.show_events) { | |
| item.event = m_cursor_sp->GetEventType(); | |
| switch (*item.event) { | |
| case eTraceEventCPUChanged: | |
| item.cpu_id = m_cursor_sp->GetCPU(); | |
| break; | |
| case eTraceEventHWClockTick: | |
| item.hw_clock = m_cursor_sp->GetHWClock(); | |
| break; | |
| case eTraceEventDisabledHW: | |
| case eTraceEventDisabledSW: | |
| break; | |
| case eTraceEventSyncPoint: | |
| item.sync_point_metadata = m_cursor_sp->GetSyncPointMetadata(); | |
| break; | |
| } | |
| m_writer_up->TraceItem(item); | |
| } else if (m_cursor_sp->IsError()) { | |
| item.error = m_cursor_sp->GetError(); | |
| m_writer_up->TraceItem(item); | |
| } else if (m_cursor_sp->IsInstruction() && !m_options.only_events) { | |
| insn_seen++; | |
| item.load_address = m_cursor_sp->GetLoadAddress(); | |
| if (!m_options.raw) { | |
| SymbolInfo symbol_info = | |
| CalculateSymbolInfo(exe_ctx, item.load_address, prev_symbol_info); | |
| item.prev_symbol_info = prev_symbol_info; | |
| item.symbol_info = symbol_info; | |
| prev_symbol_info = symbol_info; | |
| } | |
| m_writer_up->TraceItem(item); | |
| } | |
| } | |
| if (!m_cursor_sp->HasValue()) | |
| m_writer_up->NoMoreData(); | |
| return last_id; | |
| } | |
| void TraceDumper::FunctionCall::TracedSegment::AppendInsn( | |
| const TraceCursorSP &cursor_sp, | |
| const TraceDumper::SymbolInfo &symbol_info) { | |
| m_last_insn_id = cursor_sp->GetId(); | |
| m_last_symbol_info = symbol_info; | |
| } | |
| lldb::user_id_t | |
| TraceDumper::FunctionCall::TracedSegment::GetFirstInstructionID() const { | |
| return m_first_insn_id; | |
| } | |
| lldb::user_id_t | |
| TraceDumper::FunctionCall::TracedSegment::GetLastInstructionID() const { | |
| return m_last_insn_id; | |
| } | |
| void TraceDumper::FunctionCall::TracedSegment::IfNestedCall( | |
| std::function<void(const FunctionCall &function_call)> callback) const { | |
| if (m_nested_call) | |
| callback(*m_nested_call); | |
| } | |
| const TraceDumper::FunctionCall & | |
| TraceDumper::FunctionCall::TracedSegment::GetOwningCall() const { | |
| return m_owning_call; | |
| } | |
| TraceDumper::FunctionCall & | |
| TraceDumper::FunctionCall::TracedSegment::CreateNestedCall( | |
| const TraceCursorSP &cursor_sp, | |
| const TraceDumper::SymbolInfo &symbol_info) { | |
| m_nested_call = std::make_unique<FunctionCall>(cursor_sp, symbol_info); | |
| m_nested_call->SetParentCall(m_owning_call); | |
| return *m_nested_call; | |
| } | |
| const TraceDumper::SymbolInfo & | |
| TraceDumper::FunctionCall::TracedSegment::GetFirstInstructionSymbolInfo() | |
| const { | |
| return m_first_symbol_info; | |
| } | |
| const TraceDumper::SymbolInfo & | |
| TraceDumper::FunctionCall::TracedSegment::GetLastInstructionSymbolInfo() const { | |
| return m_last_symbol_info; | |
| } | |
| const TraceDumper::FunctionCall & | |
| TraceDumper::FunctionCall::UntracedPrefixSegment::GetNestedCall() const { | |
| return *m_nested_call; | |
| } | |
| TraceDumper::FunctionCall::FunctionCall( | |
| const TraceCursorSP &cursor_sp, | |
| const TraceDumper::SymbolInfo &symbol_info) { | |
| m_is_error = cursor_sp->IsError(); | |
| AppendSegment(cursor_sp, symbol_info); | |
| } | |
| void TraceDumper::FunctionCall::AppendSegment( | |
| const TraceCursorSP &cursor_sp, | |
| const TraceDumper::SymbolInfo &symbol_info) { | |
| m_traced_segments.emplace_back(cursor_sp, symbol_info, *this); | |
| } | |
| const TraceDumper::SymbolInfo & | |
| TraceDumper::FunctionCall::GetSymbolInfo() const { | |
| return m_traced_segments.back().GetLastInstructionSymbolInfo(); | |
| } | |
| bool TraceDumper::FunctionCall::IsError() const { return m_is_error; } | |
| const std::deque<TraceDumper::FunctionCall::TracedSegment> & | |
| TraceDumper::FunctionCall::GetTracedSegments() const { | |
| return m_traced_segments; | |
| } | |
| TraceDumper::FunctionCall::TracedSegment & | |
| TraceDumper::FunctionCall::GetLastTracedSegment() { | |
| return m_traced_segments.back(); | |
| } | |
| const std::optional<TraceDumper::FunctionCall::UntracedPrefixSegment> & | |
| TraceDumper::FunctionCall::GetUntracedPrefixSegment() const { | |
| return m_untraced_prefix_segment; | |
| } | |
| void TraceDumper::FunctionCall::SetUntracedPrefixSegment( | |
| TraceDumper::FunctionCallUP &&nested_call) { | |
| m_untraced_prefix_segment.emplace(std::move(nested_call)); | |
| } | |
| TraceDumper::FunctionCall *TraceDumper::FunctionCall::GetParentCall() const { | |
| return m_parent_call; | |
| } | |
| void TraceDumper::FunctionCall::SetParentCall( | |
| TraceDumper::FunctionCall &parent_call) { | |
| m_parent_call = &parent_call; | |
| } | |
| /// Given an instruction that happens after a return, find the ancestor function | |
| /// call that owns it. If this ancestor doesn't exist, create a new ancestor and | |
| /// make it the root of the tree. | |
| /// | |
| /// \param[in] last_function_call | |
| /// The function call that performs the return. | |
| /// | |
| /// \param[in] symbol_info | |
| /// The symbol information of the instruction after the return. | |
| /// | |
| /// \param[in] cursor_sp | |
| /// The cursor pointing to the instruction after the return. | |
| /// | |
| /// \param[in,out] roots | |
| /// The object owning the roots. It might be modified if a new root needs to | |
| /// be created. | |
| /// | |
| /// \return | |
| /// A reference to the function call that owns the new instruction | |
| static TraceDumper::FunctionCall &AppendReturnedInstructionToFunctionCallForest( | |
| TraceDumper::FunctionCall &last_function_call, | |
| const TraceDumper::SymbolInfo &symbol_info, const TraceCursorSP &cursor_sp, | |
| std::vector<TraceDumper::FunctionCallUP> &roots) { | |
| // We omit the current node because we can't return to itself. | |
| TraceDumper::FunctionCall *ancestor = last_function_call.GetParentCall(); | |
| for (; ancestor; ancestor = ancestor->GetParentCall()) { | |
| // This loop traverses the tree until it finds a call that we can return to. | |
| if (IsSameInstructionSymbolContext(ancestor->GetSymbolInfo(), symbol_info, | |
| /*check_source_line_info=*/false)) { | |
| // We returned to this symbol, so we are assuming we are returning there | |
| // Note: If this is not robust enough, we should actually check if we | |
| // returning to the instruction that follows the last instruction from | |
| // that call, as that's the behavior of CALL instructions. | |
| ancestor->AppendSegment(cursor_sp, symbol_info); | |
| return *ancestor; | |
| } | |
| } | |
| // We didn't find the call we were looking for, so we now create a synthetic | |
| // one that will contain the new instruction in its first traced segment. | |
| TraceDumper::FunctionCallUP new_root = | |
| std::make_unique<TraceDumper::FunctionCall>(cursor_sp, symbol_info); | |
| // This new root will own the previous root through an untraced prefix segment. | |
| new_root->SetUntracedPrefixSegment(std::move(roots.back())); | |
| roots.pop_back(); | |
| // We update the roots container to point to the new root | |
| roots.emplace_back(std::move(new_root)); | |
| return *roots.back(); | |
| } | |
| /// Append an instruction to a function call forest. The new instruction might | |
| /// be appended to the current segment, to a new nest call, or return to an | |
| /// ancestor call. | |
| /// | |
| /// \param[in] exe_ctx | |
| /// The exeuction context of the traced thread. | |
| /// | |
| /// \param[in] last_function_call | |
| /// The chronologically most recent function call before the new instruction. | |
| /// | |
| /// \param[in] prev_symbol_info | |
| /// The symbol information of the previous instruction in the trace. | |
| /// | |
| /// \param[in] symbol_info | |
| /// The symbol information of the new instruction. | |
| /// | |
| /// \param[in] cursor_sp | |
| /// The cursor pointing to the new instruction. | |
| /// | |
| /// \param[in,out] roots | |
| /// The object owning the roots. It might be modified if a new root needs to | |
| /// be created. | |
| /// | |
| /// \return | |
| /// A reference to the function call that owns the new instruction. | |
| static TraceDumper::FunctionCall &AppendInstructionToFunctionCallForest( | |
| const ExecutionContext &exe_ctx, | |
| TraceDumper::FunctionCall *last_function_call, | |
| const TraceDumper::SymbolInfo &prev_symbol_info, | |
| const TraceDumper::SymbolInfo &symbol_info, const TraceCursorSP &cursor_sp, | |
| std::vector<TraceDumper::FunctionCallUP> &roots) { | |
| if (!last_function_call || last_function_call->IsError()) { | |
| // We create a brand new root | |
| roots.emplace_back( | |
| std::make_unique<TraceDumper::FunctionCall>(cursor_sp, symbol_info)); | |
| return *roots.back(); | |
| } | |
| lldb_private::AddressRange range; | |
| if (symbol_info.sc.GetAddressRange( | |
| eSymbolContextBlock | eSymbolContextFunction | eSymbolContextSymbol, | |
| 0, /*inline_block_range*/ true, range)) { | |
| if (range.GetBaseAddress() == symbol_info.address) { | |
| // Our instruction is the first instruction of a function. This has | |
| // to be a call. This should also identify if a trampoline or the linker | |
| // is making a call using a non-CALL instruction. | |
| return last_function_call->GetLastTracedSegment().CreateNestedCall( | |
| cursor_sp, symbol_info); | |
| } | |
| } | |
| if (IsSameInstructionSymbolContext(prev_symbol_info, symbol_info, | |
| /*check_source_line_info=*/false)) { | |
| // We are still in the same function. This can't be a call because otherwise | |
| // we would be in the first instruction of the symbol. | |
| last_function_call->GetLastTracedSegment().AppendInsn(cursor_sp, | |
| symbol_info); | |
| return *last_function_call; | |
| } | |
| // Now we are in a different symbol. Let's see if this is a return or a | |
| // call | |
| const InstructionSP &insn = last_function_call->GetLastTracedSegment() | |
| .GetLastInstructionSymbolInfo() | |
| .instruction; | |
| InstructionControlFlowKind insn_kind = | |
| insn ? insn->GetControlFlowKind(&exe_ctx) | |
| : eInstructionControlFlowKindOther; | |
| switch (insn_kind) { | |
| case lldb::eInstructionControlFlowKindCall: | |
| case lldb::eInstructionControlFlowKindFarCall: { | |
| // This is a regular call | |
| return last_function_call->GetLastTracedSegment().CreateNestedCall( | |
| cursor_sp, symbol_info); | |
| } | |
| case lldb::eInstructionControlFlowKindFarReturn: | |
| case lldb::eInstructionControlFlowKindReturn: { | |
| // We should have caught most trampolines and linker functions earlier, so | |
| // let's assume this is a regular return. | |
| return AppendReturnedInstructionToFunctionCallForest( | |
| *last_function_call, symbol_info, cursor_sp, roots); | |
| } | |
| default: | |
| // we changed symbols not using a call or return and we are not in the | |
| // beginning of a symbol, so this should be something very artificial | |
| // or maybe a jump to some label in the middle of it section. | |
| // We first check if it's a return from an inline method | |
| if (prev_symbol_info.sc.block && | |
| prev_symbol_info.sc.block->GetContainingInlinedBlock()) { | |
| return AppendReturnedInstructionToFunctionCallForest( | |
| *last_function_call, symbol_info, cursor_sp, roots); | |
| } | |
| // Now We assume it's a call. We should revisit this in the future. | |
| // Ideally we should be able to decide whether to create a new tree, | |
| // or go deeper or higher in the stack. | |
| return last_function_call->GetLastTracedSegment().CreateNestedCall( | |
| cursor_sp, symbol_info); | |
| } | |
| } | |
| /// Append an error to a function call forest. The new error might be appended | |
| /// to the current segment if it contains errors or will create a new root. | |
| /// | |
| /// \param[in] last_function_call | |
| /// The chronologically most recent function call before the new error. | |
| /// | |
| /// \param[in] cursor_sp | |
| /// The cursor pointing to the new error. | |
| /// | |
| /// \param[in,out] roots | |
| /// The object owning the roots. It might be modified if a new root needs to | |
| /// be created. | |
| /// | |
| /// \return | |
| /// A reference to the function call that owns the new error. | |
| TraceDumper::FunctionCall &AppendErrorToFunctionCallForest( | |
| TraceDumper::FunctionCall *last_function_call, TraceCursorSP &cursor_sp, | |
| std::vector<TraceDumper::FunctionCallUP> &roots) { | |
| if (last_function_call && last_function_call->IsError()) { | |
| last_function_call->GetLastTracedSegment().AppendInsn( | |
| cursor_sp, TraceDumper::SymbolInfo{}); | |
| return *last_function_call; | |
| } else { | |
| roots.emplace_back(std::make_unique<TraceDumper::FunctionCall>( | |
| cursor_sp, TraceDumper::SymbolInfo{})); | |
| return *roots.back(); | |
| } | |
| } | |
| static std::vector<TraceDumper::FunctionCallUP> | |
| CreateFunctionCallForest(TraceCursorSP &cursor_sp, | |
| const ExecutionContext &exe_ctx) { | |
| std::vector<TraceDumper::FunctionCallUP> roots; | |
| TraceDumper::SymbolInfo prev_symbol_info; | |
| TraceDumper::FunctionCall *last_function_call = nullptr; | |
| for (; cursor_sp->HasValue(); cursor_sp->Next()) { | |
| if (cursor_sp->IsError()) { | |
| last_function_call = &AppendErrorToFunctionCallForest(last_function_call, | |
| cursor_sp, roots); | |
| prev_symbol_info = {}; | |
| } else if (cursor_sp->IsInstruction()) { | |
| TraceDumper::SymbolInfo symbol_info = CalculateSymbolInfo( | |
| exe_ctx, cursor_sp->GetLoadAddress(), prev_symbol_info); | |
| last_function_call = &AppendInstructionToFunctionCallForest( | |
| exe_ctx, last_function_call, prev_symbol_info, symbol_info, cursor_sp, | |
| roots); | |
| prev_symbol_info = symbol_info; | |
| } else if (cursor_sp->GetEventType() == eTraceEventCPUChanged) { | |
| // TODO: In case of a CPU change, we create a new root because we haven't | |
| // investigated yet if a call tree can safely continue or if interrupts | |
| // could have polluted the original call tree. | |
| last_function_call = nullptr; | |
| prev_symbol_info = {}; | |
| } | |
| } | |
| return roots; | |
| } | |
| void TraceDumper::DumpFunctionCalls() { | |
| ThreadSP thread_sp = m_cursor_sp->GetExecutionContextRef().GetThreadSP(); | |
| ExecutionContext exe_ctx; | |
| thread_sp->GetProcess()->GetTarget().CalculateExecutionContext(exe_ctx); | |
| m_writer_up->FunctionCallForest( | |
| CreateFunctionCallForest(m_cursor_sp, exe_ctx)); | |
| } |