| 1 | //===-- CPPLanguageRuntime.cpp---------------------------------------------===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | |
| 9 | #include <cstring> |
| 10 | #include <iostream> |
| 11 | |
| 12 | #include <memory> |
| 13 | |
| 14 | #include "CPPLanguageRuntime.h" |
| 15 | |
| 16 | #include "llvm/ADT/StringRef.h" |
| 17 | |
| 18 | #include "lldb/Symbol/Block.h" |
| 19 | #include "lldb/Symbol/Variable.h" |
| 20 | #include "lldb/Symbol/VariableList.h" |
| 21 | |
| 22 | #include "lldb/Core/PluginManager.h" |
| 23 | #include "lldb/Core/UniqueCStringMap.h" |
| 24 | #include "lldb/Symbol/CompileUnit.h" |
| 25 | #include "lldb/Target/ABI.h" |
| 26 | #include "lldb/Target/ExecutionContext.h" |
| 27 | #include "lldb/Target/RegisterContext.h" |
| 28 | #include "lldb/Target/SectionLoadList.h" |
| 29 | #include "lldb/Target/StackFrame.h" |
| 30 | #include "lldb/Target/StackFrameRecognizer.h" |
| 31 | #include "lldb/Target/ThreadPlanRunToAddress.h" |
| 32 | #include "lldb/Target/ThreadPlanStepInRange.h" |
| 33 | #include "lldb/Utility/Timer.h" |
| 34 | |
| 35 | using namespace lldb; |
| 36 | using namespace lldb_private; |
| 37 | |
| 38 | static ConstString g_this = ConstString("this" ); |
| 39 | // Artificial coroutine-related variables emitted by clang. |
| 40 | static ConstString g_promise = ConstString("__promise" ); |
| 41 | static ConstString g_coro_frame = ConstString("__coro_frame" ); |
| 42 | |
| 43 | char CPPLanguageRuntime::ID = 0; |
| 44 | |
| 45 | /// A frame recognizer that is installed to hide libc++ implementation |
| 46 | /// details from the backtrace. |
| 47 | class LibCXXFrameRecognizer : public StackFrameRecognizer { |
| 48 | std::array<RegularExpression, 2> m_hidden_regex; |
| 49 | RecognizedStackFrameSP m_hidden_frame; |
| 50 | |
| 51 | struct LibCXXHiddenFrame : public RecognizedStackFrame { |
| 52 | bool ShouldHide() override { return true; } |
| 53 | }; |
| 54 | |
| 55 | public: |
| 56 | LibCXXFrameRecognizer() |
| 57 | : m_hidden_regex{ |
| 58 | // internal implementation details in the `std::` namespace |
| 59 | // std::__1::__function::__alloc_func<void (*)(), std::__1::allocator<void (*)()>, void ()>::operator()[abi:ne200000] |
| 60 | // std::__1::__function::__func<void (*)(), std::__1::allocator<void (*)()>, void ()>::operator() |
| 61 | // std::__1::__function::__value_func<void ()>::operator()[abi:ne200000]() const |
| 62 | // std::__2::__function::__policy_invoker<void (int, int)>::__call_impl[abi:ne200000]<std::__2::__function::__default_alloc_func<int (*)(int, int), int (int, int)>> |
| 63 | // std::__1::__invoke[abi:ne200000]<void (*&)()> |
| 64 | // std::__1::__invoke_void_return_wrapper<void, true>::__call[abi:ne200000]<void (*&)()> |
| 65 | RegularExpression{R"(^std::__[^:]*::__)" }, |
| 66 | // internal implementation details in the `std::ranges` namespace |
| 67 | // std::__1::ranges::__sort::__sort_fn_impl[abi:ne200000]<std::__1::__wrap_iter<int*>, std::__1::__wrap_iter<int*>, bool (*)(int, int), std::__1::identity> |
| 68 | RegularExpression{R"(^std::__[^:]*::ranges::__)" }, |
| 69 | }, |
| 70 | m_hidden_frame(new LibCXXHiddenFrame()) {} |
| 71 | |
| 72 | std::string GetName() override { return "libc++ frame recognizer" ; } |
| 73 | |
| 74 | lldb::RecognizedStackFrameSP |
| 75 | RecognizeFrame(lldb::StackFrameSP frame_sp) override { |
| 76 | if (!frame_sp) |
| 77 | return {}; |
| 78 | const auto &sc = frame_sp->GetSymbolContext(resolve_scope: lldb::eSymbolContextFunction); |
| 79 | if (!sc.function) |
| 80 | return {}; |
| 81 | |
| 82 | // Check if we have a regex match |
| 83 | for (RegularExpression &r : m_hidden_regex) { |
| 84 | if (!r.Execute(string: sc.function->GetNameNoArguments())) |
| 85 | continue; |
| 86 | |
| 87 | // Only hide this frame if the immediate caller is also within libc++. |
| 88 | lldb::ThreadSP thread_sp = frame_sp->GetThread(); |
| 89 | if (!thread_sp) |
| 90 | return {}; |
| 91 | lldb::StackFrameSP parent_frame_sp = |
| 92 | thread_sp->GetStackFrameAtIndex(idx: frame_sp->GetFrameIndex() + 1); |
| 93 | if (!parent_frame_sp) |
| 94 | return {}; |
| 95 | const auto &parent_sc = |
| 96 | parent_frame_sp->GetSymbolContext(resolve_scope: lldb::eSymbolContextFunction); |
| 97 | if (!parent_sc.function) |
| 98 | return {}; |
| 99 | if (parent_sc.function->GetNameNoArguments().GetStringRef().starts_with( |
| 100 | Prefix: "std::" )) |
| 101 | return m_hidden_frame; |
| 102 | } |
| 103 | |
| 104 | return {}; |
| 105 | } |
| 106 | }; |
| 107 | |
| 108 | CPPLanguageRuntime::CPPLanguageRuntime(Process *process) |
| 109 | : LanguageRuntime(process) { |
| 110 | if (process) |
| 111 | process->GetTarget().GetFrameRecognizerManager().AddRecognizer( |
| 112 | recognizer: StackFrameRecognizerSP(new LibCXXFrameRecognizer()), module: {}, |
| 113 | symbol: std::make_shared<RegularExpression>(args: "^std::__[^:]*::" ), |
| 114 | /*mangling_preference=*/symbol_mangling: Mangled::ePreferDemangledWithoutArguments, |
| 115 | /*first_instruction_only=*/false); |
| 116 | } |
| 117 | |
| 118 | bool CPPLanguageRuntime::IsAllowedRuntimeValue(ConstString name) { |
| 119 | return name == g_this || name == g_promise || name == g_coro_frame; |
| 120 | } |
| 121 | |
| 122 | llvm::Error CPPLanguageRuntime::GetObjectDescription(Stream &str, |
| 123 | ValueObject &object) { |
| 124 | // C++ has no generic way to do this. |
| 125 | return llvm::createStringError(Fmt: "C++ does not support object descriptions" ); |
| 126 | } |
| 127 | |
| 128 | llvm::Error |
| 129 | CPPLanguageRuntime::GetObjectDescription(Stream &str, Value &value, |
| 130 | ExecutionContextScope *exe_scope) { |
| 131 | // C++ has no generic way to do this. |
| 132 | return llvm::createStringError(Fmt: "C++ does not support object descriptions" ); |
| 133 | } |
| 134 | |
| 135 | bool contains_lambda_identifier(llvm::StringRef &str_ref) { |
| 136 | return str_ref.contains(Other: "$_" ) || str_ref.contains(Other: "'lambda'" ); |
| 137 | } |
| 138 | |
| 139 | CPPLanguageRuntime::LibCppStdFunctionCallableInfo |
| 140 | line_entry_helper(Target &target, const SymbolContext &sc, Symbol *symbol, |
| 141 | llvm::StringRef first_template_param_sref, bool has_invoke) { |
| 142 | |
| 143 | CPPLanguageRuntime::LibCppStdFunctionCallableInfo optional_info; |
| 144 | |
| 145 | Address address = sc.GetFunctionOrSymbolAddress(); |
| 146 | |
| 147 | Address addr; |
| 148 | if (target.ResolveLoadAddress(load_addr: address.GetCallableLoadAddress(target: &target), |
| 149 | so_addr&: addr)) { |
| 150 | LineEntry line_entry; |
| 151 | addr.CalculateSymbolContextLineEntry(line_entry); |
| 152 | |
| 153 | if (contains_lambda_identifier(str_ref&: first_template_param_sref) || has_invoke) { |
| 154 | // Case 1 and 2 |
| 155 | optional_info.callable_case = lldb_private::CPPLanguageRuntime:: |
| 156 | LibCppStdFunctionCallableCase::Lambda; |
| 157 | } else { |
| 158 | // Case 3 |
| 159 | optional_info.callable_case = lldb_private::CPPLanguageRuntime:: |
| 160 | LibCppStdFunctionCallableCase::CallableObject; |
| 161 | } |
| 162 | |
| 163 | optional_info.callable_symbol = *symbol; |
| 164 | optional_info.callable_line_entry = line_entry; |
| 165 | optional_info.callable_address = addr; |
| 166 | } |
| 167 | |
| 168 | return optional_info; |
| 169 | } |
| 170 | |
| 171 | CPPLanguageRuntime::LibCppStdFunctionCallableInfo |
| 172 | CPPLanguageRuntime::FindLibCppStdFunctionCallableInfo( |
| 173 | lldb::ValueObjectSP &valobj_sp) { |
| 174 | LLDB_SCOPED_TIMER(); |
| 175 | |
| 176 | LibCppStdFunctionCallableInfo optional_info; |
| 177 | |
| 178 | if (!valobj_sp) |
| 179 | return optional_info; |
| 180 | |
| 181 | // Member __f_ has type __base*, the contents of which will hold: |
| 182 | // 1) a vtable entry which may hold type information needed to discover the |
| 183 | // lambda being called |
| 184 | // 2) possibly hold a pointer to the callable object |
| 185 | // e.g. |
| 186 | // |
| 187 | // (lldb) frame var -R f_display |
| 188 | // (std::__1::function<void (int)>) f_display = { |
| 189 | // __buf_ = { |
| 190 | // … |
| 191 | // } |
| 192 | // __f_ = 0x00007ffeefbffa00 |
| 193 | // } |
| 194 | // (lldb) memory read -fA 0x00007ffeefbffa00 |
| 195 | // 0x7ffeefbffa00: ... `vtable for std::__1::__function::__func<void (*) ... |
| 196 | // 0x7ffeefbffa08: ... `print_num(int) at std_function_cppreference_exam ... |
| 197 | // |
| 198 | // We will be handling five cases below, std::function is wrapping: |
| 199 | // |
| 200 | // 1) a lambda we know at compile time. We will obtain the name of the lambda |
| 201 | // from the first template pameter from __func's vtable. We will look up |
| 202 | // the lambda's operator()() and obtain the line table entry. |
| 203 | // 2) a lambda we know at runtime. A pointer to the lambdas __invoke method |
| 204 | // will be stored after the vtable. We will obtain the lambdas name from |
| 205 | // this entry and lookup operator()() and obtain the line table entry. |
| 206 | // 3) a callable object via operator()(). We will obtain the name of the |
| 207 | // object from the first template parameter from __func's vtable. We will |
| 208 | // look up the objects operator()() and obtain the line table entry. |
| 209 | // 4) a member function. A pointer to the function will stored after the |
| 210 | // we will obtain the name from this pointer. |
| 211 | // 5) a free function. A pointer to the function will stored after the vtable |
| 212 | // we will obtain the name from this pointer. |
| 213 | ValueObjectSP member_f_(valobj_sp->GetChildMemberWithName(name: "__f_" )); |
| 214 | |
| 215 | if (member_f_) { |
| 216 | ValueObjectSP sub_member_f_(member_f_->GetChildMemberWithName(name: "__f_" )); |
| 217 | |
| 218 | if (sub_member_f_) |
| 219 | member_f_ = sub_member_f_; |
| 220 | } |
| 221 | |
| 222 | if (!member_f_) |
| 223 | return optional_info; |
| 224 | |
| 225 | lldb::addr_t member_f_pointer_value = member_f_->GetValueAsUnsigned(fail_value: 0); |
| 226 | |
| 227 | optional_info.member_f_pointer_value = member_f_pointer_value; |
| 228 | |
| 229 | if (!member_f_pointer_value) |
| 230 | return optional_info; |
| 231 | |
| 232 | ExecutionContext exe_ctx(valobj_sp->GetExecutionContextRef()); |
| 233 | Process *process = exe_ctx.GetProcessPtr(); |
| 234 | |
| 235 | if (process == nullptr) |
| 236 | return optional_info; |
| 237 | |
| 238 | uint32_t address_size = process->GetAddressByteSize(); |
| 239 | Status status; |
| 240 | |
| 241 | // First item pointed to by __f_ should be the pointer to the vtable for |
| 242 | // a __base object. |
| 243 | lldb::addr_t vtable_address = |
| 244 | process->ReadPointerFromMemory(vm_addr: member_f_pointer_value, error&: status); |
| 245 | |
| 246 | if (status.Fail()) |
| 247 | return optional_info; |
| 248 | |
| 249 | lldb::addr_t vtable_address_first_entry = |
| 250 | process->ReadPointerFromMemory(vm_addr: vtable_address + address_size, error&: status); |
| 251 | |
| 252 | if (status.Fail()) |
| 253 | return optional_info; |
| 254 | |
| 255 | lldb::addr_t address_after_vtable = member_f_pointer_value + address_size; |
| 256 | // As commented above we may not have a function pointer but if we do we will |
| 257 | // need it. |
| 258 | lldb::addr_t possible_function_address = |
| 259 | process->ReadPointerFromMemory(vm_addr: address_after_vtable, error&: status); |
| 260 | |
| 261 | if (status.Fail()) |
| 262 | return optional_info; |
| 263 | |
| 264 | Target &target = process->GetTarget(); |
| 265 | |
| 266 | if (!target.HasLoadedSections()) |
| 267 | return optional_info; |
| 268 | |
| 269 | Address vtable_first_entry_resolved; |
| 270 | |
| 271 | if (!target.ResolveLoadAddress(load_addr: vtable_address_first_entry, |
| 272 | so_addr&: vtable_first_entry_resolved)) |
| 273 | return optional_info; |
| 274 | |
| 275 | Address vtable_addr_resolved; |
| 276 | SymbolContext sc; |
| 277 | Symbol *symbol = nullptr; |
| 278 | |
| 279 | if (!target.ResolveLoadAddress(load_addr: vtable_address, so_addr&: vtable_addr_resolved)) |
| 280 | return optional_info; |
| 281 | |
| 282 | target.GetImages().ResolveSymbolContextForAddress( |
| 283 | so_addr: vtable_addr_resolved, resolve_scope: eSymbolContextEverything, sc); |
| 284 | symbol = sc.symbol; |
| 285 | |
| 286 | if (symbol == nullptr) |
| 287 | return optional_info; |
| 288 | |
| 289 | llvm::StringRef vtable_name(symbol->GetName().GetStringRef()); |
| 290 | bool found_expected_start_string = |
| 291 | vtable_name.starts_with(Prefix: "vtable for std::__1::__function::__func<" ); |
| 292 | |
| 293 | if (!found_expected_start_string) |
| 294 | return optional_info; |
| 295 | |
| 296 | // Given case 1 or 3 we have a vtable name, we are want to extract the first |
| 297 | // template parameter |
| 298 | // |
| 299 | // ... __func<main::$_0, std::__1::allocator<main::$_0> ... |
| 300 | // ^^^^^^^^^ |
| 301 | // |
| 302 | // We could see names such as: |
| 303 | // main::$_0 |
| 304 | // Bar::add_num2(int)::'lambda'(int) |
| 305 | // Bar |
| 306 | // |
| 307 | // We do this by find the first < and , and extracting in between. |
| 308 | // |
| 309 | // This covers the case of the lambda known at compile time. |
| 310 | size_t first_open_angle_bracket = vtable_name.find(C: '<') + 1; |
| 311 | size_t first_comma = vtable_name.find(C: ','); |
| 312 | |
| 313 | llvm::StringRef first_template_parameter = |
| 314 | vtable_name.slice(Start: first_open_angle_bracket, End: first_comma); |
| 315 | |
| 316 | Address function_address_resolved; |
| 317 | |
| 318 | // Setup for cases 2, 4 and 5 we have a pointer to a function after the |
| 319 | // vtable. We will use a process of elimination to drop through each case |
| 320 | // and obtain the data we need. |
| 321 | if (target.ResolveLoadAddress(load_addr: possible_function_address, |
| 322 | so_addr&: function_address_resolved)) { |
| 323 | target.GetImages().ResolveSymbolContextForAddress( |
| 324 | so_addr: function_address_resolved, resolve_scope: eSymbolContextEverything, sc); |
| 325 | symbol = sc.symbol; |
| 326 | } |
| 327 | |
| 328 | // These conditions are used several times to simplify statements later on. |
| 329 | bool has_invoke = |
| 330 | (symbol ? symbol->GetName().GetStringRef().contains(Other: "__invoke" ) : false); |
| 331 | auto calculate_symbol_context_helper = [](auto &t, |
| 332 | SymbolContextList &sc_list) { |
| 333 | SymbolContext sc; |
| 334 | t->CalculateSymbolContext(&sc); |
| 335 | sc_list.Append(sc); |
| 336 | }; |
| 337 | |
| 338 | // Case 2 |
| 339 | if (has_invoke) { |
| 340 | SymbolContextList scl; |
| 341 | calculate_symbol_context_helper(symbol, scl); |
| 342 | |
| 343 | return line_entry_helper(target, sc: scl[0], symbol, first_template_param_sref: first_template_parameter, |
| 344 | has_invoke); |
| 345 | } |
| 346 | |
| 347 | // Case 4 or 5 |
| 348 | if (symbol && !symbol->GetName().GetStringRef().starts_with(Prefix: "vtable for" ) && |
| 349 | !contains_lambda_identifier(str_ref&: first_template_parameter) && !has_invoke) { |
| 350 | optional_info.callable_case = |
| 351 | LibCppStdFunctionCallableCase::FreeOrMemberFunction; |
| 352 | optional_info.callable_address = function_address_resolved; |
| 353 | optional_info.callable_symbol = *symbol; |
| 354 | |
| 355 | return optional_info; |
| 356 | } |
| 357 | |
| 358 | std::string func_to_match = first_template_parameter.str(); |
| 359 | |
| 360 | auto it = CallableLookupCache.find(Key: func_to_match); |
| 361 | if (it != CallableLookupCache.end()) |
| 362 | return it->second; |
| 363 | |
| 364 | SymbolContextList scl; |
| 365 | |
| 366 | CompileUnit *vtable_cu = |
| 367 | vtable_first_entry_resolved.CalculateSymbolContextCompileUnit(); |
| 368 | llvm::StringRef name_to_use = func_to_match; |
| 369 | |
| 370 | // Case 3, we have a callable object instead of a lambda |
| 371 | // |
| 372 | // TODO |
| 373 | // We currently don't support this case a callable object may have multiple |
| 374 | // operator()() varying on const/non-const and number of arguments and we |
| 375 | // don't have a way to currently distinguish them so we will bail out now. |
| 376 | if (!contains_lambda_identifier(str_ref&: name_to_use)) |
| 377 | return optional_info; |
| 378 | |
| 379 | if (vtable_cu && !has_invoke) { |
| 380 | lldb::FunctionSP func_sp = |
| 381 | vtable_cu->FindFunction(matching_lambda: [name_to_use](const FunctionSP &f) { |
| 382 | auto name = f->GetName().GetStringRef(); |
| 383 | if (name.starts_with(Prefix: name_to_use) && name.contains(Other: "operator" )) |
| 384 | return true; |
| 385 | |
| 386 | return false; |
| 387 | }); |
| 388 | |
| 389 | if (func_sp) { |
| 390 | calculate_symbol_context_helper(func_sp, scl); |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | if (symbol == nullptr) |
| 395 | return optional_info; |
| 396 | |
| 397 | // Case 1 or 3 |
| 398 | if (scl.GetSize() >= 1) { |
| 399 | optional_info = line_entry_helper(target, sc: scl[0], symbol, |
| 400 | first_template_param_sref: first_template_parameter, has_invoke); |
| 401 | } |
| 402 | |
| 403 | CallableLookupCache[func_to_match] = optional_info; |
| 404 | |
| 405 | return optional_info; |
| 406 | } |
| 407 | |
| 408 | lldb::ThreadPlanSP |
| 409 | CPPLanguageRuntime::GetStepThroughTrampolinePlan(Thread &thread, |
| 410 | bool stop_others) { |
| 411 | ThreadPlanSP ret_plan_sp; |
| 412 | |
| 413 | lldb::addr_t curr_pc = thread.GetRegisterContext()->GetPC(); |
| 414 | |
| 415 | TargetSP target_sp(thread.CalculateTarget()); |
| 416 | |
| 417 | if (!target_sp->HasLoadedSections()) |
| 418 | return ret_plan_sp; |
| 419 | |
| 420 | Address pc_addr_resolved; |
| 421 | SymbolContext sc; |
| 422 | Symbol *symbol; |
| 423 | |
| 424 | if (!target_sp->ResolveLoadAddress(load_addr: curr_pc, so_addr&: pc_addr_resolved)) |
| 425 | return ret_plan_sp; |
| 426 | |
| 427 | target_sp->GetImages().ResolveSymbolContextForAddress( |
| 428 | so_addr: pc_addr_resolved, resolve_scope: eSymbolContextEverything, sc); |
| 429 | symbol = sc.symbol; |
| 430 | |
| 431 | if (symbol == nullptr) |
| 432 | return ret_plan_sp; |
| 433 | |
| 434 | llvm::StringRef function_name(symbol->GetName().GetCString()); |
| 435 | |
| 436 | // Handling the case where we are attempting to step into std::function. |
| 437 | // The behavior will be that we will attempt to obtain the wrapped |
| 438 | // callable via FindLibCppStdFunctionCallableInfo() and if we find it we |
| 439 | // will return a ThreadPlanRunToAddress to the callable. Therefore we will |
| 440 | // step into the wrapped callable. |
| 441 | // |
| 442 | bool found_expected_start_string = |
| 443 | function_name.starts_with(Prefix: "std::__1::function<" ); |
| 444 | |
| 445 | if (!found_expected_start_string) |
| 446 | return ret_plan_sp; |
| 447 | |
| 448 | AddressRange range_of_curr_func; |
| 449 | sc.GetAddressRange(scope: eSymbolContextEverything, range_idx: 0, use_inline_block_range: false, range&: range_of_curr_func); |
| 450 | |
| 451 | StackFrameSP frame = thread.GetStackFrameAtIndex(idx: 0); |
| 452 | |
| 453 | if (frame) { |
| 454 | ValueObjectSP value_sp = frame->FindVariable(name: g_this); |
| 455 | |
| 456 | CPPLanguageRuntime::LibCppStdFunctionCallableInfo callable_info = |
| 457 | FindLibCppStdFunctionCallableInfo(valobj_sp&: value_sp); |
| 458 | |
| 459 | if (callable_info.callable_case != LibCppStdFunctionCallableCase::Invalid && |
| 460 | value_sp->GetValueIsValid()) { |
| 461 | // We found the std::function wrapped callable and we have its address. |
| 462 | // We now create a ThreadPlan to run to the callable. |
| 463 | ret_plan_sp = std::make_shared<ThreadPlanRunToAddress>( |
| 464 | args&: thread, args&: callable_info.callable_address, args&: stop_others); |
| 465 | return ret_plan_sp; |
| 466 | } else { |
| 467 | // We are in std::function but we could not obtain the callable. |
| 468 | // We create a ThreadPlan to keep stepping through using the address range |
| 469 | // of the current function. |
| 470 | ret_plan_sp = std::make_shared<ThreadPlanStepInRange>( |
| 471 | args&: thread, args&: range_of_curr_func, args&: sc, args: nullptr, args: eOnlyThisThread, |
| 472 | args: eLazyBoolYes, args: eLazyBoolYes); |
| 473 | return ret_plan_sp; |
| 474 | } |
| 475 | } |
| 476 | |
| 477 | return ret_plan_sp; |
| 478 | } |
| 479 | |
| 480 | bool CPPLanguageRuntime::IsSymbolARuntimeThunk(const Symbol &symbol) { |
| 481 | llvm::StringRef mangled_name = |
| 482 | symbol.GetMangled().GetMangledName().GetStringRef(); |
| 483 | // Virtual function overriding from a non-virtual base use a "Th" prefix. |
| 484 | // Virtual function overriding from a virtual base must use a "Tv" prefix. |
| 485 | // Virtual function overriding thunks with covariant returns use a "Tc" |
| 486 | // prefix. |
| 487 | return mangled_name.starts_with(Prefix: "_ZTh" ) || mangled_name.starts_with(Prefix: "_ZTv" ) || |
| 488 | mangled_name.starts_with(Prefix: "_ZTc" ); |
| 489 | } |
| 490 | |