//===-- LibiptDecoder.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 "LibiptDecoder.h" #include "TraceIntelPT.h" #include "lldb/Target/Process.h" #include using namespace lldb; using namespace lldb_private; using namespace lldb_private::trace_intel_pt; using namespace llvm; bool IsLibiptError(int status) { return status < 0; } bool IsEndOfStream(int status) { assert(status >= 0 && "We can't check if we reached the end of the stream if " "we got a failed status"); return status & pts_eos; } bool HasEvents(int status) { assert(status >= 0 && "We can't check for events if we got a failed status"); return status & pts_event_pending; } // RAII deleter for libipt's decoders auto InsnDecoderDeleter = [](pt_insn_decoder *decoder) { pt_insn_free_decoder(decoder); }; auto QueryDecoderDeleter = [](pt_query_decoder *decoder) { pt_qry_free_decoder(decoder); }; using PtInsnDecoderUP = std::unique_ptr; using PtQueryDecoderUP = std::unique_ptr; /// Create a basic configuration object limited to a given buffer that can be /// used for many different decoders. static Expected CreateBasicLibiptConfig(TraceIntelPT &trace_intel_pt, ArrayRef buffer) { Expected cpu_info = trace_intel_pt.GetCPUInfo(); if (!cpu_info) return cpu_info.takeError(); pt_config config; pt_config_init(&config); config.cpu = *cpu_info; int status = pt_cpu_errata(&config.errata, &config.cpu); if (IsLibiptError(status)) return make_error(status); // The libipt library does not modify the trace buffer, hence the // following casts are safe. config.begin = const_cast(buffer.data()); config.end = const_cast(buffer.data() + buffer.size()); return config; } /// Callback used by libipt for reading the process memory. /// /// More information can be found in /// https://github.com/intel/libipt/blob/master/doc/man/pt_image_set_callback.3.md static int ReadProcessMemory(uint8_t *buffer, size_t size, const pt_asid * /* unused */, uint64_t pc, void *context) { Process *process = static_cast(context); Status error; int bytes_read = process->ReadMemory(pc, buffer, size, error); if (error.Fail()) return -pte_nomap; return bytes_read; } /// Set up the memory image callback for the given decoder. static Error SetupMemoryImage(pt_insn_decoder *decoder, Process &process) { pt_image *image = pt_insn_get_image(decoder); int status = pt_image_set_callback(image, ReadProcessMemory, &process); if (IsLibiptError(status)) return make_error(status); return Error::success(); } /// Create an instruction decoder for the given buffer and the given process. static Expected CreateInstructionDecoder(TraceIntelPT &trace_intel_pt, ArrayRef buffer, Process &process) { Expected config = CreateBasicLibiptConfig(trace_intel_pt, buffer); if (!config) return config.takeError(); pt_insn_decoder *decoder_ptr = pt_insn_alloc_decoder(&*config); if (!decoder_ptr) return make_error(-pte_nomem); PtInsnDecoderUP decoder_up(decoder_ptr, InsnDecoderDeleter); if (Error err = SetupMemoryImage(decoder_ptr, process)) return std::move(err); return decoder_up; } /// Create a query decoder for the given buffer. The query decoder is the /// highest level decoder that operates directly on packets and doesn't perform /// actual instruction decoding. That's why it can be useful for inspecting a /// raw trace without pinning it to a particular process. static Expected CreateQueryDecoder(TraceIntelPT &trace_intel_pt, ArrayRef buffer) { Expected config = CreateBasicLibiptConfig(trace_intel_pt, buffer); if (!config) return config.takeError(); pt_query_decoder *decoder_ptr = pt_qry_alloc_decoder(&*config); if (!decoder_ptr) return make_error(-pte_nomem); return PtQueryDecoderUP(decoder_ptr, QueryDecoderDeleter); } /// Class used to identify anomalies in traces, which should often indicate a /// fatal error in the trace. class PSBBlockAnomalyDetector { public: PSBBlockAnomalyDetector(pt_insn_decoder &decoder, TraceIntelPT &trace_intel_pt, DecodedThread &decoded_thread) : m_decoder(decoder), m_decoded_thread(decoded_thread) { m_infinite_decoding_loop_threshold = trace_intel_pt.GetGlobalProperties() .GetInfiniteDecodingLoopVerificationThreshold(); m_extremely_large_decoding_threshold = trace_intel_pt.GetGlobalProperties() .GetExtremelyLargeDecodingThreshold(); m_next_infinite_decoding_loop_threshold = m_infinite_decoding_loop_threshold; } /// \return /// An \a llvm::Error if an anomaly that includes the last instruction item /// in the trace, or \a llvm::Error::success otherwise. Error DetectAnomaly() { RefreshPacketOffset(); uint64_t insn_added_since_last_packet_offset = m_decoded_thread.GetTotalInstructionCount() - m_insn_count_at_last_packet_offset; // We want to check if we might have fallen in an infinite loop. As this // check is not a no-op, we want to do it when we have a strong suggestion // that things went wrong. First, we check how many instructions we have // decoded since we processed an Intel PT packet for the last time. This // number should be low, because at some point we should see branches, jumps // or interrupts that require a new packet to be processed. Once we reach // certain threshold we start analyzing the trace. // // We use the number of decoded instructions since the last Intel PT packet // as a proxy because, in fact, we don't expect a single packet to give, // say, 100k instructions. That would mean that there are 100k sequential // instructions without any single branch, which is highly unlikely, or that // we found an infinite loop using direct jumps, e.g. // // 0x0A: nop or pause // 0x0C: jump to 0x0A // // which is indeed code that is found in the kernel. I presume we reach // this kind of code in the decoder because we don't handle self-modified // code in post-mortem kernel traces. // // We are right now only signaling the anomaly as a trace error, but it // would be more conservative to also discard all the trace items found in // this PSB. I prefer not to do that for the time being to give more // exposure to this kind of anomalies and help debugging. Discarding the // trace items would just make investigation harded. // // Finally, if the user wants to see if a specific thread has an anomaly, // it's enough to run the `thread trace dump info` command and look for the // count of this kind of errors. if (insn_added_since_last_packet_offset >= m_extremely_large_decoding_threshold) { // In this case, we have decoded a massive amount of sequential // instructions that don't loop. Honestly I wonder if this will ever // happen, but better safe than sorry. return createStringError( inconvertibleErrorCode(), "anomalous trace: possible infinite trace detected"); } if (insn_added_since_last_packet_offset == m_next_infinite_decoding_loop_threshold) { if (std::optional loop_size = TryIdentifyInfiniteLoop()) { return createStringError( inconvertibleErrorCode(), "anomalous trace: possible infinite loop detected of size %" PRIu64, *loop_size); } m_next_infinite_decoding_loop_threshold *= 2; } return Error::success(); } private: std::optional TryIdentifyInfiniteLoop() { // The infinite decoding loops we'll encounter are due to sequential // instructions that repeat themselves due to direct jumps, therefore in a // cycle each individual address will only appear once. We use this // information to detect cycles by finding the last 2 ocurrences of the last // instruction added to the trace. Then we traverse the trace making sure // that these two instructions where the ends of a repeating loop. // This is a utility that returns the most recent instruction index given a // position in the trace. If the given position is an instruction, that // position is returned. It skips non-instruction items. auto most_recent_insn_index = [&](uint64_t item_index) -> std::optional { while (true) { if (m_decoded_thread.GetItemKindByIndex(item_index) == lldb::eTraceItemKindInstruction) { return item_index; } if (item_index == 0) return std::nullopt; item_index--; } return std::nullopt; }; // Similar to most_recent_insn_index but skips the starting position. auto prev_insn_index = [&](uint64_t item_index) -> std::optional { if (item_index == 0) return std::nullopt; return most_recent_insn_index(item_index - 1); }; // We first find the most recent instruction. std::optional last_insn_index_opt = *prev_insn_index(m_decoded_thread.GetItemsCount()); if (!last_insn_index_opt) return std::nullopt; uint64_t last_insn_index = *last_insn_index_opt; // We then find the most recent previous occurrence of that last // instruction. std::optional last_insn_copy_index = prev_insn_index(last_insn_index); uint64_t loop_size = 1; while (last_insn_copy_index && m_decoded_thread.GetInstructionLoadAddress(*last_insn_copy_index) != m_decoded_thread.GetInstructionLoadAddress(last_insn_index)) { last_insn_copy_index = prev_insn_index(*last_insn_copy_index); loop_size++; } if (!last_insn_copy_index) return std::nullopt; // Now we check if the segment between these last positions of the last // instruction address is in fact a repeating loop. uint64_t loop_elements_visited = 1; uint64_t insn_index_a = last_insn_index, insn_index_b = *last_insn_copy_index; while (loop_elements_visited < loop_size) { if (std::optional prev = prev_insn_index(insn_index_a)) insn_index_a = *prev; else return std::nullopt; if (std::optional prev = prev_insn_index(insn_index_b)) insn_index_b = *prev; else return std::nullopt; if (m_decoded_thread.GetInstructionLoadAddress(insn_index_a) != m_decoded_thread.GetInstructionLoadAddress(insn_index_b)) return std::nullopt; loop_elements_visited++; } return loop_size; } // Refresh the internal counters if a new packet offset has been visited void RefreshPacketOffset() { lldb::addr_t new_packet_offset; if (!IsLibiptError(pt_insn_get_offset(&m_decoder, &new_packet_offset)) && new_packet_offset != m_last_packet_offset) { m_last_packet_offset = new_packet_offset; m_next_infinite_decoding_loop_threshold = m_infinite_decoding_loop_threshold; m_insn_count_at_last_packet_offset = m_decoded_thread.GetTotalInstructionCount(); } } pt_insn_decoder &m_decoder; DecodedThread &m_decoded_thread; lldb::addr_t m_last_packet_offset = LLDB_INVALID_ADDRESS; uint64_t m_insn_count_at_last_packet_offset = 0; uint64_t m_infinite_decoding_loop_threshold; uint64_t m_next_infinite_decoding_loop_threshold; uint64_t m_extremely_large_decoding_threshold; }; /// Class that decodes a raw buffer for a single PSB block using the low level /// libipt library. It assumes that kernel and user mode instructions are not /// mixed in the same PSB block. /// /// Throughout this code, the status of the decoder will be used to identify /// events needed to be processed or errors in the decoder. The values can be /// - negative: actual errors /// - positive or zero: not an error, but a list of bits signaling the status /// of the decoder, e.g. whether there are events that need to be decoded or /// not. class PSBBlockDecoder { public: /// \param[in] decoder /// A decoder configured to start and end within the boundaries of the /// given \p psb_block. /// /// \param[in] psb_block /// The PSB block to decode. /// /// \param[in] next_block_ip /// The starting ip at the next PSB block of the same thread if available. /// /// \param[in] decoded_thread /// A \a DecodedThread object where the decoded instructions will be /// appended to. It might have already some instructions. /// /// \param[in] tsc_upper_bound /// Maximum allowed value of TSCs decoded from this PSB block. /// Any of this PSB's data occurring after this TSC will be excluded. PSBBlockDecoder(PtInsnDecoderUP &&decoder_up, const PSBBlock &psb_block, std::optional next_block_ip, DecodedThread &decoded_thread, TraceIntelPT &trace_intel_pt, std::optional tsc_upper_bound) : m_decoder_up(std::move(decoder_up)), m_psb_block(psb_block), m_next_block_ip(next_block_ip), m_decoded_thread(decoded_thread), m_anomaly_detector(*m_decoder_up, trace_intel_pt, decoded_thread), m_tsc_upper_bound(tsc_upper_bound) {} /// \param[in] trace_intel_pt /// The main Trace object that own the PSB block. /// /// \param[in] decoder /// A decoder configured to start and end within the boundaries of the /// given \p psb_block. /// /// \param[in] psb_block /// The PSB block to decode. /// /// \param[in] buffer /// The raw intel pt trace for this block. /// /// \param[in] process /// The process to decode. It provides the memory image to use for /// decoding. /// /// \param[in] next_block_ip /// The starting ip at the next PSB block of the same thread if available. /// /// \param[in] decoded_thread /// A \a DecodedThread object where the decoded instructions will be /// appended to. It might have already some instructions. static Expected Create(TraceIntelPT &trace_intel_pt, const PSBBlock &psb_block, ArrayRef buffer, Process &process, std::optional next_block_ip, DecodedThread &decoded_thread, std::optional tsc_upper_bound) { Expected decoder_up = CreateInstructionDecoder(trace_intel_pt, buffer, process); if (!decoder_up) return decoder_up.takeError(); return PSBBlockDecoder(std::move(*decoder_up), psb_block, next_block_ip, decoded_thread, trace_intel_pt, tsc_upper_bound); } void DecodePSBBlock() { int status = pt_insn_sync_forward(m_decoder_up.get()); assert(status >= 0 && "Synchronization shouldn't fail because this PSB was previously " "decoded correctly."); // We emit a TSC before a sync event to more easily associate a timestamp to // the sync event. If present, the current block's TSC would be the first // TSC we'll see when processing events. if (m_psb_block.tsc) m_decoded_thread.NotifyTsc(*m_psb_block.tsc); m_decoded_thread.NotifySyncPoint(m_psb_block.psb_offset); DecodeInstructionsAndEvents(status); } private: /// Append an instruction and return \b false if and only if a serious anomaly /// has been detected. bool AppendInstructionAndDetectAnomalies(const pt_insn &insn) { m_decoded_thread.AppendInstruction(insn); if (Error err = m_anomaly_detector.DetectAnomaly()) { m_decoded_thread.AppendCustomError(toString(std::move(err)), /*fatal=*/true); return false; } return true; } /// Decode all the instructions and events of the given PSB block. The /// decoding loop might stop abruptly if an infinite decoding loop is /// detected. void DecodeInstructionsAndEvents(int status) { pt_insn insn; while (true) { status = ProcessPTEvents(status); if (IsLibiptError(status)) return; else if (IsEndOfStream(status)) break; // The status returned by pt_insn_next will need to be processed // by ProcessPTEvents in the next loop if it is not an error. std::memset(&insn, 0, sizeof insn); status = pt_insn_next(m_decoder_up.get(), &insn, sizeof(insn)); if (IsLibiptError(status)) { m_decoded_thread.AppendError(IntelPTError(status, insn.ip)); return; } else if (IsEndOfStream(status)) { break; } if (!AppendInstructionAndDetectAnomalies(insn)) return; } // We need to keep querying non-branching instructions until we hit the // starting point of the next PSB. We won't see events at this point. This // is based on // https://github.com/intel/libipt/blob/master/doc/howto_libipt.md#parallel-decode if (m_next_block_ip && insn.ip != 0) { while (insn.ip != *m_next_block_ip) { if (!AppendInstructionAndDetectAnomalies(insn)) return; status = pt_insn_next(m_decoder_up.get(), &insn, sizeof(insn)); if (IsLibiptError(status)) { m_decoded_thread.AppendError(IntelPTError(status, insn.ip)); return; } } } } /// Process the TSC of a decoded PT event. Specifically, check if this TSC /// is below the TSC upper bound for this PSB. If the TSC exceeds the upper /// bound, return an error to abort decoding. Otherwise add the it to the /// underlying DecodedThread and decoding should continue as expected. /// /// \param[in] tsc /// The TSC of the a decoded event. Error ProcessPTEventTSC(DecodedThread::TSC tsc) { if (m_tsc_upper_bound && tsc >= *m_tsc_upper_bound) { // This event and all the remaining events of this PSB have a TSC // outside the range of the "owning" ThreadContinuousExecution. For // now we drop all of these events/instructions, future work can // improve upon this by determining the "owning" // ThreadContinuousExecution of the remaining PSB data. std::string err_msg = formatv("decoding truncated: TSC {0} exceeds " "maximum TSC value {1}, will skip decoding" " the remaining data of the PSB", tsc, *m_tsc_upper_bound) .str(); uint64_t offset; int status = pt_insn_get_offset(m_decoder_up.get(), &offset); if (!IsLibiptError(status)) { err_msg = formatv("{2} (skipping {0} of {1} bytes)", offset, m_psb_block.size, err_msg) .str(); } m_decoded_thread.AppendCustomError(err_msg); return createStringError(inconvertibleErrorCode(), err_msg); } else { m_decoded_thread.NotifyTsc(tsc); return Error::success(); } } /// Before querying instructions, we need to query the events associated with /// that instruction, e.g. timing and trace disablement events. /// /// \param[in] status /// The status gotten from the previous instruction decoding or PSB /// synchronization. /// /// \return /// The pte_status after decoding events. int ProcessPTEvents(int status) { while (HasEvents(status)) { pt_event event; std::memset(&event, 0, sizeof event); status = pt_insn_event(m_decoder_up.get(), &event, sizeof(event)); if (IsLibiptError(status)) { m_decoded_thread.AppendError(IntelPTError(status)); return status; } if (event.has_tsc) { if (Error err = ProcessPTEventTSC(event.tsc)) { consumeError(std::move(err)); return -pte_internal; } } switch (event.type) { case ptev_disabled: // The CPU paused tracing the program, e.g. due to ip filtering. m_decoded_thread.AppendEvent(lldb::eTraceEventDisabledHW); break; case ptev_async_disabled: // The kernel or user code paused tracing the program, e.g. // a breakpoint or a ioctl invocation pausing the trace, or a // context switch happened. m_decoded_thread.AppendEvent(lldb::eTraceEventDisabledSW); break; case ptev_overflow: // The CPU internal buffer had an overflow error and some instructions // were lost. A OVF packet comes with an FUP packet (harcoded address) // according to the documentation, so we'll continue seeing instructions // after this event. m_decoded_thread.AppendError(IntelPTError(-pte_overflow)); break; default: break; } } return status; } private: PtInsnDecoderUP m_decoder_up; PSBBlock m_psb_block; std::optional m_next_block_ip; DecodedThread &m_decoded_thread; PSBBlockAnomalyDetector m_anomaly_detector; std::optional m_tsc_upper_bound; }; Error lldb_private::trace_intel_pt::DecodeSingleTraceForThread( DecodedThread &decoded_thread, TraceIntelPT &trace_intel_pt, ArrayRef buffer) { Expected> blocks = SplitTraceIntoPSBBlock(trace_intel_pt, buffer, /*expect_tscs=*/false); if (!blocks) return blocks.takeError(); for (size_t i = 0; i < blocks->size(); i++) { PSBBlock &block = blocks->at(i); Expected decoder = PSBBlockDecoder::Create( trace_intel_pt, block, buffer.slice(block.psb_offset, block.size), *decoded_thread.GetThread()->GetProcess(), i + 1 < blocks->size() ? blocks->at(i + 1).starting_ip : std::nullopt, decoded_thread, std::nullopt); if (!decoder) return decoder.takeError(); decoder->DecodePSBBlock(); } return Error::success(); } Error lldb_private::trace_intel_pt::DecodeSystemWideTraceForThread( DecodedThread &decoded_thread, TraceIntelPT &trace_intel_pt, const DenseMap> &buffers, const std::vector &executions) { bool has_seen_psbs = false; for (size_t i = 0; i < executions.size(); i++) { const IntelPTThreadContinousExecution &execution = executions[i]; auto variant = execution.thread_execution.variant; // We emit the first valid tsc if (execution.psb_blocks.empty()) { decoded_thread.NotifyTsc(execution.thread_execution.GetLowestKnownTSC()); } else { assert(execution.psb_blocks.front().tsc && "per cpu decoding expects TSCs"); decoded_thread.NotifyTsc( std::min(execution.thread_execution.GetLowestKnownTSC(), *execution.psb_blocks.front().tsc)); } // We then emit the CPU, which will be correctly associated with a tsc. decoded_thread.NotifyCPU(execution.thread_execution.cpu_id); // If we haven't seen a PSB yet, then it's fine not to show errors if (has_seen_psbs) { if (execution.psb_blocks.empty()) { decoded_thread.AppendCustomError( formatv("Unable to find intel pt data a thread " "execution on cpu id = {0}", execution.thread_execution.cpu_id) .str()); } // A hinted start is a non-initial execution that doesn't have a switch // in. An only end is an initial execution that doesn't have a switch in. // Any of those cases represent a gap because we have seen a PSB before. if (variant == ThreadContinuousExecution::Variant::HintedStart || variant == ThreadContinuousExecution::Variant::OnlyEnd) { decoded_thread.AppendCustomError( formatv("Unable to find the context switch in for a thread " "execution on cpu id = {0}", execution.thread_execution.cpu_id) .str()); } } for (size_t j = 0; j < execution.psb_blocks.size(); j++) { const PSBBlock &psb_block = execution.psb_blocks[j]; Expected decoder = PSBBlockDecoder::Create( trace_intel_pt, psb_block, buffers.lookup(execution.thread_execution.cpu_id) .slice(psb_block.psb_offset, psb_block.size), *decoded_thread.GetThread()->GetProcess(), j + 1 < execution.psb_blocks.size() ? execution.psb_blocks[j + 1].starting_ip : std::nullopt, decoded_thread, execution.thread_execution.GetEndTSC()); if (!decoder) return decoder.takeError(); has_seen_psbs = true; decoder->DecodePSBBlock(); } // If we haven't seen a PSB yet, then it's fine not to show errors if (has_seen_psbs) { // A hinted end is a non-ending execution that doesn't have a switch out. // An only start is an ending execution that doesn't have a switch out. // Any of those cases represent a gap if we still have executions to // process and we have seen a PSB before. if (i + 1 != executions.size() && (variant == ThreadContinuousExecution::Variant::OnlyStart || variant == ThreadContinuousExecution::Variant::HintedEnd)) { decoded_thread.AppendCustomError( formatv("Unable to find the context switch out for a thread " "execution on cpu id = {0}", execution.thread_execution.cpu_id) .str()); } } } return Error::success(); } bool IntelPTThreadContinousExecution::operator<( const IntelPTThreadContinousExecution &o) const { // As the context switch might be incomplete, we look first for the first real // PSB packet, which is a valid TSC. Otherwise, We query the thread execution // itself for some tsc. auto get_tsc = [](const IntelPTThreadContinousExecution &exec) { return exec.psb_blocks.empty() ? exec.thread_execution.GetLowestKnownTSC() : exec.psb_blocks.front().tsc; }; return get_tsc(*this) < get_tsc(o); } Expected> lldb_private::trace_intel_pt::SplitTraceIntoPSBBlock( TraceIntelPT &trace_intel_pt, llvm::ArrayRef buffer, bool expect_tscs) { // This follows // https://github.com/intel/libipt/blob/master/doc/howto_libipt.md#parallel-decode Expected decoder_up = CreateQueryDecoder(trace_intel_pt, buffer); if (!decoder_up) return decoder_up.takeError(); pt_query_decoder *decoder = decoder_up.get().get(); std::vector executions; while (true) { uint64_t maybe_ip = LLDB_INVALID_ADDRESS; int decoding_status = pt_qry_sync_forward(decoder, &maybe_ip); if (IsLibiptError(decoding_status)) break; uint64_t psb_offset; int offset_status = pt_qry_get_sync_offset(decoder, &psb_offset); assert(offset_status >= 0 && "This can't fail because we were able to synchronize"); std::optional ip; if (!(pts_ip_suppressed & decoding_status)) ip = maybe_ip; std::optional tsc; // Now we fetch the first TSC that comes after the PSB. while (HasEvents(decoding_status)) { pt_event event; decoding_status = pt_qry_event(decoder, &event, sizeof(event)); if (IsLibiptError(decoding_status)) break; if (event.has_tsc) { tsc = event.tsc; break; } } if (IsLibiptError(decoding_status)) { // We continue to the next PSB. This effectively merges this PSB with the // previous one, and that should be fine because this PSB might be the // direct continuation of the previous thread and it's better to show an // error in the decoded thread than to hide it. If this is the first PSB, // we are okay losing it. Besides that, an error at processing events // means that we wouldn't be able to get any instruction out of it. continue; } if (expect_tscs && !tsc) return createStringError(inconvertibleErrorCode(), "Found a PSB without TSC."); executions.push_back({ psb_offset, tsc, 0, ip, }); } if (!executions.empty()) { // We now adjust the sizes of each block executions.back().size = buffer.size() - executions.back().psb_offset; for (int i = (int)executions.size() - 2; i >= 0; i--) { executions[i].size = executions[i + 1].psb_offset - executions[i].psb_offset; } } return executions; } Expected> lldb_private::trace_intel_pt::FindLowestTSCInTrace(TraceIntelPT &trace_intel_pt, ArrayRef buffer) { Expected decoder_up = CreateQueryDecoder(trace_intel_pt, buffer); if (!decoder_up) return decoder_up.takeError(); pt_query_decoder *decoder = decoder_up.get().get(); uint64_t ip = LLDB_INVALID_ADDRESS; int status = pt_qry_sync_forward(decoder, &ip); if (IsLibiptError(status)) return std::nullopt; while (HasEvents(status)) { pt_event event; status = pt_qry_event(decoder, &event, sizeof(event)); if (IsLibiptError(status)) return std::nullopt; if (event.has_tsc) return event.tsc; } return std::nullopt; }