• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 //===-- UnwindPlan.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 "lldb/Symbol/UnwindPlan.h"
10 
11 #include "lldb/Expression/DWARFExpression.h"
12 #include "lldb/Target/Process.h"
13 #include "lldb/Target/RegisterContext.h"
14 #include "lldb/Target/Target.h"
15 #include "lldb/Target/Thread.h"
16 #include "lldb/Utility/ConstString.h"
17 #include "lldb/Utility/Log.h"
18 #include "llvm/DebugInfo/DWARF/DWARFExpression.h"
19 
20 using namespace lldb;
21 using namespace lldb_private;
22 
23 bool UnwindPlan::Row::RegisterLocation::
operator ==(const UnwindPlan::Row::RegisterLocation & rhs) const24 operator==(const UnwindPlan::Row::RegisterLocation &rhs) const {
25   if (m_type == rhs.m_type) {
26     switch (m_type) {
27     case unspecified:
28     case undefined:
29     case same:
30       return true;
31 
32     case atCFAPlusOffset:
33     case isCFAPlusOffset:
34     case atAFAPlusOffset:
35     case isAFAPlusOffset:
36       return m_location.offset == rhs.m_location.offset;
37 
38     case inOtherRegister:
39       return m_location.reg_num == rhs.m_location.reg_num;
40 
41     case atDWARFExpression:
42     case isDWARFExpression:
43       if (m_location.expr.length == rhs.m_location.expr.length)
44         return !memcmp(m_location.expr.opcodes, rhs.m_location.expr.opcodes,
45                        m_location.expr.length);
46       break;
47     }
48   }
49   return false;
50 }
51 
52 // This function doesn't copy the dwarf expression bytes; they must remain in
53 // allocated memory for the lifespan of this UnwindPlan object.
SetAtDWARFExpression(const uint8_t * opcodes,uint32_t len)54 void UnwindPlan::Row::RegisterLocation::SetAtDWARFExpression(
55     const uint8_t *opcodes, uint32_t len) {
56   m_type = atDWARFExpression;
57   m_location.expr.opcodes = opcodes;
58   m_location.expr.length = len;
59 }
60 
61 // This function doesn't copy the dwarf expression bytes; they must remain in
62 // allocated memory for the lifespan of this UnwindPlan object.
SetIsDWARFExpression(const uint8_t * opcodes,uint32_t len)63 void UnwindPlan::Row::RegisterLocation::SetIsDWARFExpression(
64     const uint8_t *opcodes, uint32_t len) {
65   m_type = isDWARFExpression;
66   m_location.expr.opcodes = opcodes;
67   m_location.expr.length = len;
68 }
69 
70 static llvm::Optional<std::pair<lldb::ByteOrder, uint32_t>>
GetByteOrderAndAddrSize(Thread * thread)71 GetByteOrderAndAddrSize(Thread *thread) {
72   if (!thread)
73     return llvm::None;
74   ProcessSP process_sp = thread->GetProcess();
75   if (!process_sp)
76     return llvm::None;
77   ArchSpec arch = process_sp->GetTarget().GetArchitecture();
78   return std::make_pair(arch.GetByteOrder(), arch.GetAddressByteSize());
79 }
80 
DumpDWARFExpr(Stream & s,llvm::ArrayRef<uint8_t> expr,Thread * thread)81 static void DumpDWARFExpr(Stream &s, llvm::ArrayRef<uint8_t> expr, Thread *thread) {
82   if (auto order_and_width = GetByteOrderAndAddrSize(thread)) {
83     llvm::DataExtractor data(expr, order_and_width->first == eByteOrderLittle,
84                              order_and_width->second);
85     llvm::DWARFExpression(data, order_and_width->second, llvm::dwarf::DWARF32)
86         .print(s.AsRawOstream(), llvm::DIDumpOptions(), nullptr, nullptr);
87   } else
88     s.PutCString("dwarf-expr");
89 }
90 
Dump(Stream & s,const UnwindPlan * unwind_plan,const UnwindPlan::Row * row,Thread * thread,bool verbose) const91 void UnwindPlan::Row::RegisterLocation::Dump(Stream &s,
92                                              const UnwindPlan *unwind_plan,
93                                              const UnwindPlan::Row *row,
94                                              Thread *thread,
95                                              bool verbose) const {
96   switch (m_type) {
97   case unspecified:
98     if (verbose)
99       s.PutCString("=<unspec>");
100     else
101       s.PutCString("=!");
102     break;
103   case undefined:
104     if (verbose)
105       s.PutCString("=<undef>");
106     else
107       s.PutCString("=?");
108     break;
109   case same:
110     s.PutCString("= <same>");
111     break;
112 
113   case atCFAPlusOffset:
114   case isCFAPlusOffset: {
115     s.PutChar('=');
116     if (m_type == atCFAPlusOffset)
117       s.PutChar('[');
118     s.Printf("CFA%+d", m_location.offset);
119     if (m_type == atCFAPlusOffset)
120       s.PutChar(']');
121   } break;
122 
123   case atAFAPlusOffset:
124   case isAFAPlusOffset: {
125     s.PutChar('=');
126     if (m_type == atAFAPlusOffset)
127       s.PutChar('[');
128     s.Printf("AFA%+d", m_location.offset);
129     if (m_type == atAFAPlusOffset)
130       s.PutChar(']');
131   } break;
132 
133   case inOtherRegister: {
134     const RegisterInfo *other_reg_info = nullptr;
135     if (unwind_plan)
136       other_reg_info = unwind_plan->GetRegisterInfo(thread, m_location.reg_num);
137     if (other_reg_info)
138       s.Printf("=%s", other_reg_info->name);
139     else
140       s.Printf("=reg(%u)", m_location.reg_num);
141   } break;
142 
143   case atDWARFExpression:
144   case isDWARFExpression: {
145     s.PutChar('=');
146     if (m_type == atDWARFExpression)
147       s.PutChar('[');
148     DumpDWARFExpr(
149         s, llvm::makeArrayRef(m_location.expr.opcodes, m_location.expr.length),
150         thread);
151     if (m_type == atDWARFExpression)
152       s.PutChar(']');
153   } break;
154   }
155 }
156 
DumpRegisterName(Stream & s,const UnwindPlan * unwind_plan,Thread * thread,uint32_t reg_num)157 static void DumpRegisterName(Stream &s, const UnwindPlan *unwind_plan,
158                              Thread *thread, uint32_t reg_num) {
159   const RegisterInfo *reg_info = unwind_plan->GetRegisterInfo(thread, reg_num);
160   if (reg_info)
161     s.PutCString(reg_info->name);
162   else
163     s.Printf("reg(%u)", reg_num);
164 }
165 
166 bool UnwindPlan::Row::FAValue::
operator ==(const UnwindPlan::Row::FAValue & rhs) const167 operator==(const UnwindPlan::Row::FAValue &rhs) const {
168   if (m_type == rhs.m_type) {
169     switch (m_type) {
170     case unspecified:
171     case isRaSearch:
172       return m_value.ra_search_offset == rhs.m_value.ra_search_offset;
173 
174     case isRegisterPlusOffset:
175       return m_value.reg.offset == rhs.m_value.reg.offset;
176 
177     case isRegisterDereferenced:
178       return m_value.reg.reg_num == rhs.m_value.reg.reg_num;
179 
180     case isDWARFExpression:
181       if (m_value.expr.length == rhs.m_value.expr.length)
182         return !memcmp(m_value.expr.opcodes, rhs.m_value.expr.opcodes,
183                        m_value.expr.length);
184       break;
185     }
186   }
187   return false;
188 }
189 
Dump(Stream & s,const UnwindPlan * unwind_plan,Thread * thread) const190 void UnwindPlan::Row::FAValue::Dump(Stream &s, const UnwindPlan *unwind_plan,
191                                      Thread *thread) const {
192   switch (m_type) {
193   case isRegisterPlusOffset:
194     DumpRegisterName(s, unwind_plan, thread, m_value.reg.reg_num);
195     s.Printf("%+3d", m_value.reg.offset);
196     break;
197   case isRegisterDereferenced:
198     s.PutChar('[');
199     DumpRegisterName(s, unwind_plan, thread, m_value.reg.reg_num);
200     s.PutChar(']');
201     break;
202   case isDWARFExpression:
203     DumpDWARFExpr(s,
204                   llvm::makeArrayRef(m_value.expr.opcodes, m_value.expr.length),
205                   thread);
206     break;
207   case unspecified:
208     s.PutCString("unspecified");
209     break;
210   case isRaSearch:
211     s.Printf("RaSearch@SP%+d", m_value.ra_search_offset);
212     break;
213   }
214 }
215 
Clear()216 void UnwindPlan::Row::Clear() {
217   m_cfa_value.SetUnspecified();
218   m_afa_value.SetUnspecified();
219   m_offset = 0;
220   m_register_locations.clear();
221 }
222 
Dump(Stream & s,const UnwindPlan * unwind_plan,Thread * thread,addr_t base_addr) const223 void UnwindPlan::Row::Dump(Stream &s, const UnwindPlan *unwind_plan,
224                            Thread *thread, addr_t base_addr) const {
225   if (base_addr != LLDB_INVALID_ADDRESS)
226     s.Printf("0x%16.16" PRIx64 ": CFA=", base_addr + GetOffset());
227   else
228     s.Printf("%4" PRId64 ": CFA=", GetOffset());
229 
230   m_cfa_value.Dump(s, unwind_plan, thread);
231 
232   if (!m_afa_value.IsUnspecified()) {
233     s.Printf(" AFA=");
234     m_afa_value.Dump(s, unwind_plan, thread);
235   }
236 
237   s.Printf(" => ");
238   for (collection::const_iterator idx = m_register_locations.begin();
239        idx != m_register_locations.end(); ++idx) {
240     DumpRegisterName(s, unwind_plan, thread, idx->first);
241     const bool verbose = false;
242     idx->second.Dump(s, unwind_plan, this, thread, verbose);
243     s.PutChar(' ');
244   }
245   s.EOL();
246 }
247 
Row()248 UnwindPlan::Row::Row()
249     : m_offset(0), m_cfa_value(), m_afa_value(), m_register_locations() {}
250 
GetRegisterInfo(uint32_t reg_num,UnwindPlan::Row::RegisterLocation & register_location) const251 bool UnwindPlan::Row::GetRegisterInfo(
252     uint32_t reg_num,
253     UnwindPlan::Row::RegisterLocation &register_location) const {
254   collection::const_iterator pos = m_register_locations.find(reg_num);
255   if (pos != m_register_locations.end()) {
256     register_location = pos->second;
257     return true;
258   }
259   return false;
260 }
261 
RemoveRegisterInfo(uint32_t reg_num)262 void UnwindPlan::Row::RemoveRegisterInfo(uint32_t reg_num) {
263   collection::const_iterator pos = m_register_locations.find(reg_num);
264   if (pos != m_register_locations.end()) {
265     m_register_locations.erase(pos);
266   }
267 }
268 
SetRegisterInfo(uint32_t reg_num,const UnwindPlan::Row::RegisterLocation register_location)269 void UnwindPlan::Row::SetRegisterInfo(
270     uint32_t reg_num,
271     const UnwindPlan::Row::RegisterLocation register_location) {
272   m_register_locations[reg_num] = register_location;
273 }
274 
SetRegisterLocationToAtCFAPlusOffset(uint32_t reg_num,int32_t offset,bool can_replace)275 bool UnwindPlan::Row::SetRegisterLocationToAtCFAPlusOffset(uint32_t reg_num,
276                                                            int32_t offset,
277                                                            bool can_replace) {
278   if (!can_replace &&
279       m_register_locations.find(reg_num) != m_register_locations.end())
280     return false;
281   RegisterLocation reg_loc;
282   reg_loc.SetAtCFAPlusOffset(offset);
283   m_register_locations[reg_num] = reg_loc;
284   return true;
285 }
286 
SetRegisterLocationToIsCFAPlusOffset(uint32_t reg_num,int32_t offset,bool can_replace)287 bool UnwindPlan::Row::SetRegisterLocationToIsCFAPlusOffset(uint32_t reg_num,
288                                                            int32_t offset,
289                                                            bool can_replace) {
290   if (!can_replace &&
291       m_register_locations.find(reg_num) != m_register_locations.end())
292     return false;
293   RegisterLocation reg_loc;
294   reg_loc.SetIsCFAPlusOffset(offset);
295   m_register_locations[reg_num] = reg_loc;
296   return true;
297 }
298 
SetRegisterLocationToUndefined(uint32_t reg_num,bool can_replace,bool can_replace_only_if_unspecified)299 bool UnwindPlan::Row::SetRegisterLocationToUndefined(
300     uint32_t reg_num, bool can_replace, bool can_replace_only_if_unspecified) {
301   collection::iterator pos = m_register_locations.find(reg_num);
302   collection::iterator end = m_register_locations.end();
303 
304   if (pos != end) {
305     if (!can_replace)
306       return false;
307     if (can_replace_only_if_unspecified && !pos->second.IsUnspecified())
308       return false;
309   }
310   RegisterLocation reg_loc;
311   reg_loc.SetUndefined();
312   m_register_locations[reg_num] = reg_loc;
313   return true;
314 }
315 
SetRegisterLocationToUnspecified(uint32_t reg_num,bool can_replace)316 bool UnwindPlan::Row::SetRegisterLocationToUnspecified(uint32_t reg_num,
317                                                        bool can_replace) {
318   if (!can_replace &&
319       m_register_locations.find(reg_num) != m_register_locations.end())
320     return false;
321   RegisterLocation reg_loc;
322   reg_loc.SetUnspecified();
323   m_register_locations[reg_num] = reg_loc;
324   return true;
325 }
326 
SetRegisterLocationToRegister(uint32_t reg_num,uint32_t other_reg_num,bool can_replace)327 bool UnwindPlan::Row::SetRegisterLocationToRegister(uint32_t reg_num,
328                                                     uint32_t other_reg_num,
329                                                     bool can_replace) {
330   if (!can_replace &&
331       m_register_locations.find(reg_num) != m_register_locations.end())
332     return false;
333   RegisterLocation reg_loc;
334   reg_loc.SetInRegister(other_reg_num);
335   m_register_locations[reg_num] = reg_loc;
336   return true;
337 }
338 
SetRegisterLocationToSame(uint32_t reg_num,bool must_replace)339 bool UnwindPlan::Row::SetRegisterLocationToSame(uint32_t reg_num,
340                                                 bool must_replace) {
341   if (must_replace &&
342       m_register_locations.find(reg_num) == m_register_locations.end())
343     return false;
344   RegisterLocation reg_loc;
345   reg_loc.SetSame();
346   m_register_locations[reg_num] = reg_loc;
347   return true;
348 }
349 
operator ==(const UnwindPlan::Row & rhs) const350 bool UnwindPlan::Row::operator==(const UnwindPlan::Row &rhs) const {
351   return m_offset == rhs.m_offset &&
352       m_cfa_value == rhs.m_cfa_value &&
353       m_afa_value == rhs.m_afa_value &&
354       m_register_locations == rhs.m_register_locations;
355 }
356 
AppendRow(const UnwindPlan::RowSP & row_sp)357 void UnwindPlan::AppendRow(const UnwindPlan::RowSP &row_sp) {
358   if (m_row_list.empty() ||
359       m_row_list.back()->GetOffset() != row_sp->GetOffset())
360     m_row_list.push_back(row_sp);
361   else
362     m_row_list.back() = row_sp;
363 }
364 
InsertRow(const UnwindPlan::RowSP & row_sp,bool replace_existing)365 void UnwindPlan::InsertRow(const UnwindPlan::RowSP &row_sp,
366                            bool replace_existing) {
367   collection::iterator it = m_row_list.begin();
368   while (it != m_row_list.end()) {
369     RowSP row = *it;
370     if (row->GetOffset() >= row_sp->GetOffset())
371       break;
372     it++;
373   }
374   if (it == m_row_list.end() || (*it)->GetOffset() != row_sp->GetOffset())
375     m_row_list.insert(it, row_sp);
376   else if (replace_existing)
377     *it = row_sp;
378 }
379 
GetRowForFunctionOffset(int offset) const380 UnwindPlan::RowSP UnwindPlan::GetRowForFunctionOffset(int offset) const {
381   RowSP row;
382   if (!m_row_list.empty()) {
383     if (offset == -1)
384       row = m_row_list.back();
385     else {
386       collection::const_iterator pos, end = m_row_list.end();
387       for (pos = m_row_list.begin(); pos != end; ++pos) {
388         if ((*pos)->GetOffset() <= static_cast<lldb::offset_t>(offset))
389           row = *pos;
390         else
391           break;
392       }
393     }
394   }
395   return row;
396 }
397 
IsValidRowIndex(uint32_t idx) const398 bool UnwindPlan::IsValidRowIndex(uint32_t idx) const {
399   return idx < m_row_list.size();
400 }
401 
GetRowAtIndex(uint32_t idx) const402 const UnwindPlan::RowSP UnwindPlan::GetRowAtIndex(uint32_t idx) const {
403   if (idx < m_row_list.size())
404     return m_row_list[idx];
405   else {
406     Log *log(GetLogIfAllCategoriesSet(LIBLLDB_LOG_UNWIND));
407     LLDB_LOGF(log,
408               "error: UnwindPlan::GetRowAtIndex(idx = %u) invalid index "
409               "(number rows is %u)",
410               idx, (uint32_t)m_row_list.size());
411     return UnwindPlan::RowSP();
412   }
413 }
414 
GetLastRow() const415 const UnwindPlan::RowSP UnwindPlan::GetLastRow() const {
416   if (m_row_list.empty()) {
417     Log *log(GetLogIfAllCategoriesSet(LIBLLDB_LOG_UNWIND));
418     LLDB_LOGF(log, "UnwindPlan::GetLastRow() when rows are empty");
419     return UnwindPlan::RowSP();
420   }
421   return m_row_list.back();
422 }
423 
GetRowCount() const424 int UnwindPlan::GetRowCount() const { return m_row_list.size(); }
425 
SetPlanValidAddressRange(const AddressRange & range)426 void UnwindPlan::SetPlanValidAddressRange(const AddressRange &range) {
427   if (range.GetBaseAddress().IsValid() && range.GetByteSize() != 0)
428     m_plan_valid_address_range = range;
429 }
430 
PlanValidAtAddress(Address addr)431 bool UnwindPlan::PlanValidAtAddress(Address addr) {
432   // If this UnwindPlan has no rows, it is an invalid UnwindPlan.
433   if (GetRowCount() == 0) {
434     Log *log(GetLogIfAllCategoriesSet(LIBLLDB_LOG_UNWIND));
435     if (log) {
436       StreamString s;
437       if (addr.Dump(&s, nullptr, Address::DumpStyleSectionNameOffset)) {
438         LLDB_LOGF(log,
439                   "UnwindPlan is invalid -- no unwind rows for UnwindPlan "
440                   "'%s' at address %s",
441                   m_source_name.GetCString(), s.GetData());
442       } else {
443         LLDB_LOGF(log,
444                   "UnwindPlan is invalid -- no unwind rows for UnwindPlan '%s'",
445                   m_source_name.GetCString());
446       }
447     }
448     return false;
449   }
450 
451   // If the 0th Row of unwind instructions is missing, or if it doesn't provide
452   // a register to use to find the Canonical Frame Address, this is not a valid
453   // UnwindPlan.
454   if (GetRowAtIndex(0).get() == nullptr ||
455       GetRowAtIndex(0)->GetCFAValue().GetValueType() ==
456           Row::FAValue::unspecified) {
457     Log *log(GetLogIfAllCategoriesSet(LIBLLDB_LOG_UNWIND));
458     if (log) {
459       StreamString s;
460       if (addr.Dump(&s, nullptr, Address::DumpStyleSectionNameOffset)) {
461         LLDB_LOGF(log,
462                   "UnwindPlan is invalid -- no CFA register defined in row 0 "
463                   "for UnwindPlan '%s' at address %s",
464                   m_source_name.GetCString(), s.GetData());
465       } else {
466         LLDB_LOGF(log,
467                   "UnwindPlan is invalid -- no CFA register defined in row 0 "
468                   "for UnwindPlan '%s'",
469                   m_source_name.GetCString());
470       }
471     }
472     return false;
473   }
474 
475   if (!m_plan_valid_address_range.GetBaseAddress().IsValid() ||
476       m_plan_valid_address_range.GetByteSize() == 0)
477     return true;
478 
479   if (!addr.IsValid())
480     return true;
481 
482   if (m_plan_valid_address_range.ContainsFileAddress(addr))
483     return true;
484 
485   return false;
486 }
487 
Dump(Stream & s,Thread * thread,lldb::addr_t base_addr) const488 void UnwindPlan::Dump(Stream &s, Thread *thread, lldb::addr_t base_addr) const {
489   if (!m_source_name.IsEmpty()) {
490     s.Printf("This UnwindPlan originally sourced from %s\n",
491              m_source_name.GetCString());
492   }
493   if (m_lsda_address.IsValid() && m_personality_func_addr.IsValid()) {
494     TargetSP target_sp(thread->CalculateTarget());
495     addr_t lsda_load_addr = m_lsda_address.GetLoadAddress(target_sp.get());
496     addr_t personality_func_load_addr =
497         m_personality_func_addr.GetLoadAddress(target_sp.get());
498 
499     if (lsda_load_addr != LLDB_INVALID_ADDRESS &&
500         personality_func_load_addr != LLDB_INVALID_ADDRESS) {
501       s.Printf("LSDA address 0x%" PRIx64
502                ", personality routine is at address 0x%" PRIx64 "\n",
503                lsda_load_addr, personality_func_load_addr);
504     }
505   }
506   s.Printf("This UnwindPlan is sourced from the compiler: ");
507   switch (m_plan_is_sourced_from_compiler) {
508   case eLazyBoolYes:
509     s.Printf("yes.\n");
510     break;
511   case eLazyBoolNo:
512     s.Printf("no.\n");
513     break;
514   case eLazyBoolCalculate:
515     s.Printf("not specified.\n");
516     break;
517   }
518   s.Printf("This UnwindPlan is valid at all instruction locations: ");
519   switch (m_plan_is_valid_at_all_instruction_locations) {
520   case eLazyBoolYes:
521     s.Printf("yes.\n");
522     break;
523   case eLazyBoolNo:
524     s.Printf("no.\n");
525     break;
526   case eLazyBoolCalculate:
527     s.Printf("not specified.\n");
528     break;
529   }
530   s.Printf("This UnwindPlan is for a trap handler function: ");
531   switch (m_plan_is_for_signal_trap) {
532   case eLazyBoolYes:
533     s.Printf("yes.\n");
534     break;
535   case eLazyBoolNo:
536     s.Printf("no.\n");
537     break;
538   case eLazyBoolCalculate:
539     s.Printf("not specified.\n");
540     break;
541   }
542   if (m_plan_valid_address_range.GetBaseAddress().IsValid() &&
543       m_plan_valid_address_range.GetByteSize() > 0) {
544     s.PutCString("Address range of this UnwindPlan: ");
545     TargetSP target_sp(thread->CalculateTarget());
546     m_plan_valid_address_range.Dump(&s, target_sp.get(),
547                                     Address::DumpStyleSectionNameOffset);
548     s.EOL();
549   }
550   collection::const_iterator pos, begin = m_row_list.begin(),
551                                   end = m_row_list.end();
552   for (pos = begin; pos != end; ++pos) {
553     s.Printf("row[%u]: ", (uint32_t)std::distance(begin, pos));
554     (*pos)->Dump(s, this, thread, base_addr);
555   }
556 }
557 
SetSourceName(const char * source)558 void UnwindPlan::SetSourceName(const char *source) {
559   m_source_name = ConstString(source);
560 }
561 
GetSourceName() const562 ConstString UnwindPlan::GetSourceName() const { return m_source_name; }
563 
GetRegisterInfo(Thread * thread,uint32_t unwind_reg) const564 const RegisterInfo *UnwindPlan::GetRegisterInfo(Thread *thread,
565                                                 uint32_t unwind_reg) const {
566   if (thread) {
567     RegisterContext *reg_ctx = thread->GetRegisterContext().get();
568     if (reg_ctx) {
569       uint32_t reg;
570       if (m_register_kind == eRegisterKindLLDB)
571         reg = unwind_reg;
572       else
573         reg = reg_ctx->ConvertRegisterKindToRegisterNumber(m_register_kind,
574                                                            unwind_reg);
575       if (reg != LLDB_INVALID_REGNUM)
576         return reg_ctx->GetRegisterInfoAtIndex(reg);
577     }
578   }
579   return nullptr;
580 }
581