VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testdriver/btresolver.py@ 106212

Last change on this file since 106212 was 106061, checked in by vboxsync, 3 months ago

Copyright year updates by scm.

  • Property svn:executable set to *
  • Property svn:keywords set to Id Revision
File size: 21.1 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: btresolver.py 106061 2024-09-16 14:03:52Z vboxsync $
3# pylint: disable=too-many-lines
4
5"""
6Backtrace resolver using external debugging symbols and RTLdrFlt.
7"""
8
9__copyright__ = \
10"""
11Copyright (C) 2016-2024 Oracle and/or its affiliates.
12
13This file is part of VirtualBox base platform packages, as
14available from https://www.virtualbox.org.
15
16This program is free software; you can redistribute it and/or
17modify it under the terms of the GNU General Public License
18as published by the Free Software Foundation, in version 3 of the
19License.
20
21This program is distributed in the hope that it will be useful, but
22WITHOUT ANY WARRANTY; without even the implied warranty of
23MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
24General Public License for more details.
25
26You should have received a copy of the GNU General Public License
27along with this program; if not, see <https://www.gnu.org/licenses>.
28
29The contents of this file may alternatively be used under the terms
30of the Common Development and Distribution License Version 1.0
31(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
32in the VirtualBox distribution, in which case the provisions of the
33CDDL are applicable instead of those of the GPL.
34
35You may elect to license modified versions of this file under the
36terms and conditions of either the GPL or the CDDL or both.
37
38SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
39"""
40__version__ = "$Revision: 106061 $"
41
42
43# Standard Python imports.
44import os;
45import re;
46import shutil;
47import subprocess;
48
49# Validation Kit imports.
50from common import utils;
51
52def getRTLdrFltPath(asPaths):
53 """
54 Returns the path to the RTLdrFlt tool looking in the provided paths
55 or None if not found.
56 """
57
58 for sPath in asPaths:
59 for sDirPath, _, asFiles in os.walk(sPath):
60 if 'RTLdrFlt' in asFiles:
61 return os.path.join(sDirPath, 'RTLdrFlt');
62
63 return None;
64
65
66
67class BacktraceResolverOs(object):
68 """
69 Base class for all OS specific resolvers.
70 """
71
72 def __init__(self, sScratchPath, sBuildRoot, fnLog = None):
73 self.sScratchPath = sScratchPath;
74 self.sBuildRoot = sBuildRoot;
75 self.fnLog = fnLog;
76
77 def log(self, sText):
78 """
79 Internal logger callback.
80 """
81 if self.fnLog is not None:
82 self.fnLog(sText);
83
84
85
86class BacktraceResolverOsLinux(BacktraceResolverOs):
87 """
88 Linux specific backtrace resolver.
89 """
90
91 def __init__(self, sScratchPath, sBuildRoot, fnLog = None):
92 """
93 Constructs a Linux host specific backtrace resolver.
94 """
95 BacktraceResolverOs.__init__(self, sScratchPath, sBuildRoot, fnLog);
96
97 self.asDbgFiles = {};
98
99 def prepareEnv(self):
100 """
101 Prepares the environment for annotating Linux reports.
102 """
103 fRc = False;
104 try:
105 sDbgArchive = os.path.join(self.sBuildRoot, 'bin', 'VirtualBox-dbg.tar.bz2');
106
107 # Extract debug symbol archive if it was found.
108 if os.path.exists(sDbgArchive):
109 asMembers = utils.unpackFile(sDbgArchive, self.sScratchPath, self.fnLog,
110 self.fnLog);
111 if asMembers:
112 # Populate the list of debug files.
113 for sMember in asMembers:
114 if os.path.isfile(sMember):
115 self.asDbgFiles[os.path.basename(sMember)] = sMember;
116 fRc = True;
117 except:
118 self.log('Failed to setup debug symbols');
119
120 return fRc;
121
122 def cleanupEnv(self):
123 """
124 Cleans up the environment.
125 """
126 fRc = False;
127 try:
128 shutil.rmtree(self.sScratchPath, True);
129 fRc = True;
130 except:
131 pass;
132
133 return fRc;
134
135 def getDbgSymPathFromBinary(self, sBinary, sArch):
136 """
137 Returns the path to file containing the debug symbols for the specified binary.
138 """
139 _ = sArch;
140 sDbgFilePath = None;
141 try:
142 sDbgFilePath = self.asDbgFiles[sBinary];
143 except:
144 pass;
145
146 return sDbgFilePath;
147
148 def getBinaryListWithLoadAddrFromReport(self, asReport):
149 """
150 Parses the given VM state report and returns a list of binaries and their
151 load address.
152
153 Returns a list if tuples containing the binary and load addres or an empty
154 list on failure.
155 """
156 asListBinaries = [];
157
158 # Look for the line "Mapped address spaces:"
159 iLine = 0;
160 while iLine < len(asReport):
161 if asReport[iLine].startswith('Mapped address spaces:'):
162 break;
163 iLine += 1;
164
165 for sLine in asReport[iLine:]:
166 asCandidate = sLine.split();
167 if len(asCandidate) == 5 \
168 and asCandidate[0].startswith('0x') \
169 and asCandidate[1].startswith('0x') \
170 and asCandidate[2].startswith('0x') \
171 and (asCandidate[3] == '0x0' or asCandidate[3] == '0')\
172 and 'VirtualBox' in asCandidate[4]:
173 asListBinaries.append((asCandidate[0], os.path.basename(asCandidate[4])));
174
175 return asListBinaries;
176
177
178
179class BacktraceResolverOsDarwin(BacktraceResolverOs):
180 """
181 Darwin specific backtrace resolver.
182 """
183
184 def __init__(self, sScratchPath, sBuildRoot, fnLog = None):
185 """
186 Constructs a Linux host specific backtrace resolver.
187 """
188 BacktraceResolverOs.__init__(self, sScratchPath, sBuildRoot, fnLog);
189
190 self.asDbgFiles = {};
191
192 def prepareEnv(self):
193 """
194 Prepares the environment for annotating Darwin reports.
195 """
196 fRc = False;
197 try:
198 #
199 # Walk the scratch path directory and look for .dSYM directories, building a
200 # list of them.
201 #
202 asDSymPaths = [];
203
204 for sDirPath, asDirs, _ in os.walk(self.sBuildRoot):
205 for sDir in asDirs:
206 if sDir.endswith('.dSYM'):
207 asDSymPaths.append(os.path.join(sDirPath, sDir));
208
209 # Expand the dSYM paths to full DWARF debug files in the next step
210 # and add them to the debug files dictionary.
211 for sDSymPath in asDSymPaths:
212 sBinary = os.path.basename(sDSymPath).strip('.dSYM');
213 self.asDbgFiles[sBinary] = os.path.join(sDSymPath, 'Contents', 'Resources',
214 'DWARF', sBinary);
215
216 fRc = True;
217 except:
218 self.log('Failed to setup debug symbols');
219
220 return fRc;
221
222 def cleanupEnv(self):
223 """
224 Cleans up the environment.
225 """
226 fRc = False;
227 try:
228 shutil.rmtree(self.sScratchPath, True);
229 fRc = True;
230 except:
231 pass;
232
233 return fRc;
234
235 def getDbgSymPathFromBinary(self, sBinary, sArch):
236 """
237 Returns the path to file containing the debug symbols for the specified binary.
238 """
239 # Hack to exclude executables as RTLdrFlt has some problems with it currently.
240 _ = sArch;
241 sDbgSym = None;
242 try:
243 sDbgSym = self.asDbgFiles[sBinary];
244 except:
245 pass;
246
247 if sDbgSym is not None and sDbgSym.endswith('.dylib'):
248 return sDbgSym;
249
250 return None;
251
252 def _getReportVersion(self, asReport):
253 """
254 Returns the version of the darwin report.
255 """
256 # Find the line starting with "Report Version:"
257 iLine = 0;
258 iVersion = 0;
259 while iLine < len(asReport):
260 if asReport[iLine].startswith('Report Version:'):
261 break;
262 iLine += 1;
263
264 if iLine < len(asReport):
265 # Look for the start of the number
266 sVersion = asReport[iLine];
267 iStartVersion = len('Report Version:');
268 iEndVersion = len(sVersion);
269
270 while iStartVersion < len(sVersion) \
271 and not sVersion[iStartVersion:iStartVersion+1].isdigit():
272 iStartVersion += 1;
273
274 while iEndVersion > 0 \
275 and not sVersion[iEndVersion-1:iEndVersion].isdigit():
276 iEndVersion -= 1;
277
278 iVersion = int(sVersion[iStartVersion:iEndVersion]);
279 else:
280 self.log('Couldn\'t find the report version');
281
282 return iVersion;
283
284 def _getListOfBinariesFromReportPreSierra(self, asReport):
285 """
286 Returns a list of loaded binaries with their load address obtained from
287 a pre Sierra report.
288 """
289 asListBinaries = [];
290
291 # Find the line starting with "Binary Images:"
292 iLine = 0;
293 while iLine < len(asReport):
294 if asReport[iLine].startswith('Binary Images:'):
295 break;
296 iLine += 1;
297
298 if iLine < len(asReport):
299 # List starts after that
300 iLine += 1;
301
302 # A line for a loaded binary looks like the following:
303 # 0x100042000 - 0x100095fff +VBoxDDU.dylib (4.3.15) <EB19C44D-F882-0803-DBDD-9995723111B7> /Application...
304 # We need the start address and the library name.
305 # To distinguish between our own libraries and ones from Apple we check whether the path at the end starts with
306 # /Applications/VirtualBox.app/Contents/MacOS
307 oRegExpPath = re.compile(r'/VirtualBox.app/Contents/MacOS');
308 oRegExpAddr = re.compile(r'0x\w+');
309 oRegExpBinPath = re.compile(r'VirtualBox.app/Contents/MacOS/\S*');
310 while iLine < len(asReport):
311 asMatches = oRegExpPath.findall(asReport[iLine]);
312 if asMatches:
313 # Line contains the path, extract start address and path to binary
314 sAddr = oRegExpAddr.findall(asReport[iLine]);
315 sPath = oRegExpBinPath.findall(asReport[iLine]);
316
317 if sAddr and sPath:
318 # Construct the path in into the build cache containing the debug symbols
319 oRegExp = re.compile(r'\w+\.{0,1}\w*$');
320 sFilename = oRegExp.findall(sPath[0]);
321
322 asListBinaries.append((sAddr[0], sFilename[0]));
323 else:
324 break; # End of image list
325 iLine += 1;
326 else:
327 self.log('Couldn\'t find the list of loaded binaries in the given report');
328
329 return asListBinaries;
330
331 def _getListOfBinariesFromReportSierra(self, asReport):
332 """
333 Returns a list of loaded binaries with their load address obtained from
334 a Sierra+ report.
335 """
336 asListBinaries = [];
337
338 # A line for a loaded binary looks like the following:
339 # 4 VBoxXPCOMIPCC.dylib 0x00000001139f17ea 0x1139e4000 + 55274
340 # We need the start address and the library name.
341 # To distinguish between our own libraries and ones from Apple we check whether the library
342 # name contains VBox or VirtualBox
343 iLine = 0;
344 while iLine < len(asReport):
345 asStackTrace = asReport[iLine].split();
346
347 # Check whether the line is made up of 6 elements separated by whitespace
348 # and the first one is a number.
349 if len(asStackTrace) == 6 and asStackTrace[0].isdigit() \
350 and (asStackTrace[1].find('VBox') != -1 or asStackTrace[1].find('VirtualBox') != -1) \
351 and asStackTrace[3].startswith('0x'):
352
353 # Check whether the library is already in our list an only add new ones
354 fFound = False;
355 for _, sLibrary in asListBinaries:
356 if asStackTrace[1] == sLibrary:
357 fFound = True;
358 break;
359
360 if not fFound:
361 asListBinaries.append((asStackTrace[3], asStackTrace[1]));
362 iLine += 1;
363
364 return asListBinaries;
365
366 def getBinaryListWithLoadAddrFromReport(self, asReport):
367 """
368 Parses the given VM state report and returns a list of binaries and their
369 load address.
370
371 Returns a list if tuples containing the binary and load addres or an empty
372 list on failure.
373 """
374 asListBinaries = [];
375
376 iVersion = self._getReportVersion(asReport);
377 if iVersion > 0:
378 if iVersion <= 11:
379 self.log('Pre Sierra Report');
380 asListBinaries = self._getListOfBinariesFromReportPreSierra(asReport);
381 elif iVersion == 12:
382 self.log('Sierra report');
383 asListBinaries = self._getListOfBinariesFromReportSierra(asReport);
384 else:
385 self.log('Unsupported report version %s' % (iVersion, ));
386
387 return asListBinaries;
388
389
390
391class BacktraceResolverOsSolaris(BacktraceResolverOs):
392 """
393 Solaris specific backtrace resolver.
394 """
395
396 def __init__(self, sScratchPath, sBuildRoot, fnLog = None):
397 """
398 Constructs a Linux host specific backtrace resolver.
399 """
400 BacktraceResolverOs.__init__(self, sScratchPath, sBuildRoot, fnLog);
401
402 self.asDbgFiles = {};
403
404 def prepareEnv(self):
405 """
406 Prepares the environment for annotating Linux reports.
407 """
408 fRc = False;
409 try:
410 sDbgArchive = os.path.join(self.sBuildRoot, 'bin', 'VirtualBoxDebug.tar.bz2');
411
412 # Extract debug symbol archive if it was found.
413 if os.path.exists(sDbgArchive):
414 asMembers = utils.unpackFile(sDbgArchive, self.sScratchPath, self.fnLog,
415 self.fnLog);
416 if asMembers:
417 # Populate the list of debug files.
418 for sMember in asMembers:
419 if os.path.isfile(sMember):
420 sArch = '';
421 if 'amd64' in sMember:
422 sArch = 'amd64';
423 else:
424 sArch = 'x86';
425 self.asDbgFiles[os.path.basename(sMember) + '/' + sArch] = sMember;
426 fRc = True;
427 else:
428 self.log('Unpacking the debug archive failed');
429 except:
430 self.log('Failed to setup debug symbols');
431
432 return fRc;
433
434 def cleanupEnv(self):
435 """
436 Cleans up the environment.
437 """
438 fRc = False;
439 try:
440 shutil.rmtree(self.sScratchPath, True);
441 fRc = True;
442 except:
443 pass;
444
445 return fRc;
446
447 def getDbgSymPathFromBinary(self, sBinary, sArch):
448 """
449 Returns the path to file containing the debug symbols for the specified binary.
450 """
451 sDbgFilePath = None;
452 try:
453 sDbgFilePath = self.asDbgFiles[sBinary + '/' + sArch];
454 except:
455 pass;
456
457 return sDbgFilePath;
458
459 def getBinaryListWithLoadAddrFromReport(self, asReport):
460 """
461 Parses the given VM state report and returns a list of binaries and their
462 load address.
463
464 Returns a list if tuples containing the binary and load addres or an empty
465 list on failure.
466 """
467 asListBinaries = [];
468
469 # Look for the beginning of the process address space mappings"
470 for sLine in asReport:
471 asItems = sLine.split();
472 if len(asItems) == 4 \
473 and asItems[3].startswith('/opt/VirtualBox') \
474 and ( asItems[2] == 'r-x--' \
475 or asItems[2] == 'r-x----'):
476 fFound = False;
477 sBinaryFile = os.path.basename(asItems[3]);
478 for _, sBinary in asListBinaries:
479 if sBinary == sBinaryFile:
480 fFound = True;
481 break;
482 if not fFound:
483 asListBinaries.append(('0x' + asItems[0], sBinaryFile));
484
485 return asListBinaries;
486
487
488
489class BacktraceResolver(object):
490 """
491 A backtrace resolving class.
492 """
493
494 def __init__(self, sScratchPath, sBuildRoot, sTargetOs, sArch, sRTLdrFltPath = None, fnLog = None):
495 """
496 Constructs a backtrace resolver object for the given target OS,
497 architecture and path to the directory containing the debug symbols and tools
498 we need.
499 """
500 # Initialize all members first.
501 self.sScratchPath = sScratchPath;
502 self.sBuildRoot = sBuildRoot;
503 self.sTargetOs = sTargetOs;
504 self.sArch = sArch;
505 self.sRTLdrFltPath = sRTLdrFltPath;
506 self.fnLog = fnLog;
507 self.sDbgSymPath = None;
508 self.oResolverOs = None;
509 self.sScratchDbgPath = os.path.join(self.sScratchPath, 'dbgsymbols');
510
511 if self.fnLog is None:
512 self.fnLog = self.logStub;
513
514 if self.sRTLdrFltPath is None:
515 self.sRTLdrFltPath = getRTLdrFltPath([self.sScratchPath, self.sBuildRoot]);
516 if self.sRTLdrFltPath is not None:
517 self.log('Found RTLdrFlt in %s' % (self.sRTLdrFltPath,));
518 else:
519 self.log('Couldn\'t find RTLdrFlt in either %s or %s' % (self.sScratchPath, self.sBuildRoot));
520
521 def log(self, sText):
522 """
523 Internal logger callback.
524 """
525 if self.fnLog is not None:
526 self.fnLog(sText);
527
528 def logStub(self, sText):
529 """
530 Logging stub doing nothing.
531 """
532 _ = sText;
533
534 def prepareEnv(self):
535 """
536 Prepares the environment to annotate backtraces, finding the required tools
537 and retrieving the debug symbols depending on the host OS.
538
539 Returns True on success and False on error or if not supported.
540 """
541
542 # No access to the RTLdrFlt tool means no symbols so no point in trying
543 # to set something up.
544 if self.sRTLdrFltPath is None:
545 return False;
546
547 # Create a directory containing the scratch space for the OS resolver backends.
548 fRc = True;
549 if not os.path.exists(self.sScratchDbgPath):
550 try:
551 os.makedirs(self.sScratchDbgPath, 0o750);
552 except:
553 fRc = False;
554 self.log('Failed to create scratch directory for debug symbols');
555
556 if fRc:
557 if self.sTargetOs == 'linux':
558 self.oResolverOs = BacktraceResolverOsLinux(self.sScratchDbgPath, self.sScratchPath, self.fnLog);
559 elif self.sTargetOs == 'darwin':
560 self.oResolverOs = BacktraceResolverOsDarwin(self.sScratchDbgPath, self.sScratchPath, self.fnLog); # pylint: disable=redefined-variable-type
561 elif self.sTargetOs == 'solaris':
562 self.oResolverOs = BacktraceResolverOsSolaris(self.sScratchDbgPath, self.sScratchPath, self.fnLog); # pylint: disable=redefined-variable-type
563 else:
564 self.log('The backtrace resolver is not supported on %s' % (self.sTargetOs,));
565 fRc = False;
566
567 if fRc:
568 fRc = self.oResolverOs.prepareEnv();
569 if not fRc:
570 self.oResolverOs = None;
571
572 if not fRc:
573 shutil.rmtree(self.sScratchDbgPath, True)
574
575 return fRc;
576
577 def cleanupEnv(self):
578 """
579 Prepares the environment to annotate backtraces, finding the required tools
580 and retrieving the debug symbols depending on the host OS.
581
582 Returns True on success and False on error or if not supported.
583 """
584 fRc = False;
585 if self.oResolverOs is not None:
586 fRc = self.oResolverOs.cleanupEnv();
587
588 shutil.rmtree(self.sScratchDbgPath, True);
589 return fRc;
590
591 def annotateReport(self, sReport):
592 """
593 Annotates the given report with the previously prepared environment.
594
595 Returns the annotated report on success or None on failure.
596 """
597 sReportAn = None;
598
599 if self.oResolverOs is not None:
600 asListBinaries = self.oResolverOs.getBinaryListWithLoadAddrFromReport(sReport.split('\n'));
601
602 if asListBinaries:
603 asArgs = [self.sRTLdrFltPath, ];
604
605 for sLoadAddr, sBinary in asListBinaries:
606 sDbgSymPath = self.oResolverOs.getDbgSymPathFromBinary(sBinary, self.sArch);
607 if sDbgSymPath is not None:
608 asArgs.append(sDbgSymPath);
609 asArgs.append(sLoadAddr);
610
611 oRTLdrFltProc = subprocess.Popen(asArgs, stdin=subprocess.PIPE, # pylint: disable=consider-using-with
612 stdout=subprocess.PIPE, bufsize=0);
613 if oRTLdrFltProc is not None:
614 try:
615 sReportAn, _ = oRTLdrFltProc.communicate(sReport);
616 except:
617 self.log('Retrieving annotation report failed (broken pipe / no matching interpreter?)');
618 else:
619 self.log('Error spawning RTLdrFlt process');
620 else:
621 self.log('Getting list of loaded binaries failed');
622 else:
623 self.log('Backtrace resolver not fully initialized, not possible to annotate');
624
625 return sReportAn;
626
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette