File indexing completed on 2024-04-21 16:12:18

0001 /*
0002     SPDX-FileCopyrightText: 2009-2010 George Kiagiadakis <gkiagia@users.sourceforge.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 #include "backtraceparsergdb.h"
0007 #include "backtraceparser_p.h"
0008 #include "drkonqi_parser_debug.h"
0009 
0010 #include <QFileInfo>
0011 #include <QRegularExpression>
0012 
0013 // BEGIN BacktraceLineGdb
0014 
0015 const QLatin1String BacktraceParserGdb::KCRASH_INFO_MESSAGE("KCRASH_INFO_MESSAGE: ");
0016 
0017 BacktraceLineGdb::BacktraceLineGdb(const QString &lineStr)
0018     : BacktraceLine()
0019 {
0020     d->m_line = lineStr;
0021     d->m_functionName = QLatin1String("??");
0022     parse();
0023     if (d->m_type == StackFrame) {
0024         rate();
0025     }
0026 }
0027 
0028 void BacktraceLineGdb::parse()
0029 {
0030     if (d->m_line == QLatin1Char('\n')) {
0031         d->m_type = EmptyLine;
0032         return;
0033     } else if (d->m_line == QLatin1String("[KCrash Handler]\n")) {
0034         d->m_type = KCrash;
0035         return;
0036     } else if (d->m_line.contains(QLatin1String("<signal handler called>"))) {
0037         d->m_type = SignalHandlerStart;
0038         return;
0039     }
0040 
0041     static QRegularExpression regExp;
0042     // make dot "." char match new lines, to match e.g.:
0043     // "#5  0x00007f50e99f776f in QWidget::testAttribute_helper (this=0x6e6440,\n    attribute=Qt::WA_WState_Created) at kernel/qwidget.cpp:9081\n"
0044     // gdb breaks long stack frame lines into multiple ones for readability
0045     regExp.setPatternOptions(QRegularExpression::DotMatchesEverythingOption);
0046     regExp.setPattern(
0047         QRegularExpression::anchoredPattern(QStringLiteral("#([0-9]+)" // matches the stack frame number, ex. "#0"
0048                                                            "[\\s]+(?:0x[0-9a-f]+[\\s]+in[\\s]+)?" // matches " 0x0000dead in " (optionally)
0049                                                            "((?:\\(anonymous namespace\\)::)?[^\\(]+)?" // matches the function name
0050                                                            //(anything except left parenthesis, which is the start of the arguments section)
0051                                                            // and optionally the prefix "(anonymous namespace)::"
0052                                                            "(?:\\(.*\\))?" // matches the function arguments
0053                                                                            //(when the app doesn't have debugging symbols)
0054                                                            "[\\s]+(?:const[\\s]+)?" // matches a trailing const, if it exists
0055                                                            "\\(.*\\)" // matches the arguments of the function with their values
0056                                                                       //(when the app has debugging symbols)
0057                                                            "([\\s]+" // beginning of optional file information
0058                                                            "(from|at)[\\s]+" // matches "from " or "at "
0059                                                            "(.+)" // matches the filename (source file or shared library file)
0060                                                            ")?\n"))); // matches trailing newline.
0061     // the )? at the end closes the parenthesis before [\\s]+(from|at) and
0062     // notes that the whole expression from there is optional.
0063 
0064     QRegularExpressionMatch match = regExp.match(d->m_line);
0065     if (match.hasMatch()) {
0066         d->m_type = StackFrame;
0067         d->m_stackFrameNumber = match.captured(1).toInt();
0068         d->m_functionName = match.captured(2).trimmed();
0069 
0070         if (!match.captured(3).isEmpty()) { // we have file information (stuff after from|at)
0071             bool file = match.captured(4) == QLatin1String("at"); //'at' means we have a source file (likely)
0072             // Gdb isn't entirely consistent here, when it uses 'from' it always refers to a library, but
0073             // sometimes the stack can resolve to a library even when it uses the 'at' key word.
0074             // This specifically seems to happen when a frame has no function name.
0075             const QString path = match.captured(5);
0076             const auto completeSuffix = QFileInfo(path).completeSuffix();
0077             file = file && completeSuffix != QLatin1String("so") /* libf.so (so) */
0078                 && !completeSuffix.startsWith(QLatin1String("so.")) /* libf.so.1 (so.1) */
0079                 && !completeSuffix.contains(QLatin1String(".so") /* libf-1.0.so.1 (0.so.1)*/);
0080             if (file) {
0081                 d->m_file = match.captured(5);
0082             } else { //'from' means we have a library
0083                 d->m_library = match.captured(5);
0084             }
0085         }
0086 
0087         qCDebug(DRKONQI_PARSER_LOG) << d->m_stackFrameNumber << d->m_functionName << d->m_file << d->m_library;
0088         return;
0089     }
0090 
0091     if (d->m_line.contains(BacktraceParserGdb::KCRASH_INFO_MESSAGE)) {
0092         qCDebug(DRKONQI_PARSER_LOG) << "info:" << d->m_line;
0093         d->m_type = Info;
0094         return;
0095     }
0096 
0097     regExp.setPatternOptions(regExp.patternOptions() & ~QRegularExpression::DotMatchesEverythingOption);
0098 
0099     regExp.setPattern(
0100         QRegularExpression::anchoredPattern(QStringLiteral(".*\\(no debugging symbols found\\).*|"
0101                                                            ".*\\[Thread debugging using libthread_db enabled\\].*|"
0102                                                            ".*\\[New .*|"
0103                                                            "0x[0-9a-f]+.*|"
0104                                                            "Current language:.*")));
0105     if (regExp.match(d->m_line).hasMatch()) {
0106         qCDebug(DRKONQI_PARSER_LOG) << "garbage detected:" << d->m_line;
0107         d->m_type = Crap;
0108         return;
0109     }
0110 
0111     regExp.setPattern(QRegularExpression::anchoredPattern(QStringLiteral("Thread [0-9]+\\s+\\(Thread [0-9a-fx]+\\s+\\(.*\\)\\):\n")));
0112     if (regExp.match(d->m_line).hasMatch()) {
0113         qCDebug(DRKONQI_PARSER_LOG) << "thread start detected:" << d->m_line;
0114         d->m_type = ThreadStart;
0115         return;
0116     }
0117 
0118     regExp.setPattern(QRegularExpression::anchoredPattern(QStringLiteral("\\[Current thread is [0-9]+ \\(.*\\)\\]\n")));
0119     if (regExp.match(d->m_line).hasMatch()) {
0120         qCDebug(DRKONQI_PARSER_LOG) << "thread indicator detected:" << d->m_line;
0121         d->m_type = ThreadIndicator;
0122         return;
0123     }
0124 
0125     qCDebug(DRKONQI_PARSER_LOG) << "line" << d->m_line << "did not match";
0126 }
0127 
0128 void BacktraceLineGdb::rate()
0129 {
0130     LineRating r;
0131 
0132     // for explanations, see the LineRating enum definition
0133     if (!fileName().isEmpty()) {
0134         r = Good;
0135     } else if (!libraryName().isEmpty()) {
0136         if (functionName() == QLatin1String("??") || functionName().isEmpty()) {
0137             r = MissingFunction;
0138         } else {
0139             r = MissingSourceFile;
0140         }
0141     } else {
0142         if (functionName() == QLatin1String("??") || functionName().isEmpty()) {
0143             r = MissingEverything;
0144         } else {
0145             r = MissingLibrary;
0146         }
0147     }
0148 
0149     d->m_rating = r;
0150 }
0151 
0152 // END BacktraceLineGdb
0153 
0154 // BEGIN BacktraceParserGdb
0155 
0156 class BacktraceParserGdbPrivate : public BacktraceParserPrivate
0157 {
0158 public:
0159     using BacktraceParserPrivate::BacktraceParserPrivate;
0160 
0161     QString m_lineInputBuffer;
0162     int m_possibleKCrashStart = 0;
0163     int m_threadsCount = 0;
0164     bool m_isBelowSignalHandler = false;
0165     bool m_frameZeroAppeared = false;
0166 };
0167 
0168 BacktraceParserGdb::BacktraceParserGdb(QObject *parent)
0169     : BacktraceParser(parent)
0170 {
0171 }
0172 
0173 BacktraceParserPrivate *BacktraceParserGdb::constructPrivate() const
0174 {
0175     return new BacktraceParserGdbPrivate;
0176 }
0177 
0178 void BacktraceParserGdb::newLine(const QString &lineStr)
0179 {
0180     Q_D(BacktraceParserGdb);
0181 
0182     // when the line is too long, gdb splits it into two lines.
0183     // This breaks parsing and results in two Unknown lines instead of a StackFrame one.
0184     // Here we workaround this by joining the two lines when such a scenario is detected.
0185     if (d->m_lineInputBuffer.isEmpty()) {
0186         d->m_lineInputBuffer = lineStr;
0187     } else if (lineStr.startsWith(QLatin1Char(' ')) || lineStr.startsWith(QLatin1Char('\t'))) {
0188         // gdb always adds some whitespace at the beginning of the second line
0189         d->m_lineInputBuffer.append(lineStr);
0190     } else {
0191         parseLine(d->m_lineInputBuffer);
0192         d->m_lineInputBuffer = lineStr;
0193     }
0194 }
0195 
0196 void BacktraceParserGdb::parseLine(const QString &lineStr)
0197 {
0198     Q_D(BacktraceParserGdb);
0199 
0200     BacktraceLineGdb line(lineStr);
0201     switch (line.type()) {
0202     case BacktraceLine::Crap:
0203         break; // we don't want crap in the backtrace ;)
0204     case BacktraceLine::Info:
0205         d->m_infoLines << line.toString().mid(KCRASH_INFO_MESSAGE.size());
0206         break;
0207     case BacktraceLine::ThreadStart:
0208         d->m_linesList.append(line);
0209         d->m_possibleKCrashStart = d->m_linesList.size();
0210         d->m_threadsCount++;
0211         // reset the state of the flags that need to be per-thread
0212         d->m_isBelowSignalHandler = false;
0213         d->m_frameZeroAppeared = false; // gdb bug workaround flag, see below
0214         break;
0215     case BacktraceLine::SignalHandlerStart:
0216         if (!d->m_isBelowSignalHandler) {
0217             // replace the stack frames of KCrash with a nice message
0218             d->m_linesList.erase(d->m_linesList.begin() + d->m_possibleKCrashStart, d->m_linesList.end());
0219             d->m_linesList.insert(d->m_possibleKCrashStart, BacktraceLineGdb(QStringLiteral("[KCrash Handler]\n")));
0220             d->m_isBelowSignalHandler = true; // next line is the first below the signal handler
0221         } else {
0222             // this is not the first time we see a crash handler frame on the same thread,
0223             // so we just add it to the list
0224             d->m_linesList.append(line);
0225         }
0226         break;
0227     case BacktraceLine::StackFrame:
0228         // gdb workaround - (v6.8 at least) - 'thread apply all bt' writes
0229         // the #0 stack frame again at the end.
0230         // Here we ignore this frame by using a flag that tells us whether
0231         // this is the first or the second time that the #0 frame appears in this thread.
0232         // The flag is cleared on each thread start.
0233         if (line.frameNumber() == 0) {
0234             if (d->m_frameZeroAppeared) {
0235                 break; // break from the switch so that the frame is not added to the list.
0236             } else {
0237                 d->m_frameZeroAppeared = true;
0238             }
0239         }
0240 
0241         // rate the stack frame if we are below the signal handler
0242         if (d->m_isBelowSignalHandler) {
0243             d->m_linesToRate.append(line);
0244         }
0245         Q_FALLTHROUGH();
0246         // fall through and append the line to the list
0247     default:
0248         d->m_linesList.append(line);
0249         break;
0250     }
0251 }
0252 
0253 QString BacktraceParserGdb::parsedBacktrace() const
0254 {
0255     Q_D(const BacktraceParserGdb);
0256 
0257     QString result;
0258     if (d) {
0259         QList<BacktraceLine>::const_iterator i;
0260         for (i = d->m_linesList.constBegin(); i != d->m_linesList.constEnd(); ++i) {
0261             // if there is only one thread, we can omit the thread indicator,
0262             // the thread header and all the empty lines.
0263             if (d->m_threadsCount == 1
0264                 && ((*i).type() == BacktraceLine::ThreadIndicator || (*i).type() == BacktraceLine::ThreadStart || (*i).type() == BacktraceLine::EmptyLine)) {
0265                 continue;
0266             }
0267             result += i->toString();
0268         }
0269     }
0270     return result;
0271 }
0272 
0273 QList<BacktraceLine> BacktraceParserGdb::parsedBacktraceLines() const
0274 {
0275     Q_D(const BacktraceParserGdb);
0276 
0277     QList<BacktraceLine> result;
0278     if (d) {
0279         QList<BacktraceLine>::const_iterator i;
0280         for (i = d->m_linesList.constBegin(); i != d->m_linesList.constEnd(); ++i) {
0281             // if there is only one thread, we can omit the thread indicator,
0282             // the thread header and all the empty lines.
0283             if (d->m_threadsCount == 1
0284                 && ((*i).type() == BacktraceLine::ThreadIndicator || (*i).type() == BacktraceLine::ThreadStart || (*i).type() == BacktraceLine::EmptyLine)) {
0285                 continue;
0286             }
0287             result.append(*i);
0288         }
0289     }
0290     return result;
0291 }
0292 
0293 // END BacktraceParserGdb