From 7cc2446960bc87286c7f4a3ca57be2b2a3a5d44f Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Mon, 5 Jun 2023 16:16:24 +0100 Subject: [PATCH 1/3] [Backtracing][Linux] Add Linux support to swift-backtrace. We need a Linux specific `Target` implementation, and a couple of minor tweaks to make things build everywhere. rdar://110262673 --- .../libexec/swift-backtrace/CMakeLists.txt | 3 +- .../libexec/swift-backtrace/TargetLinux.swift | 232 ++++++++++++++++++ .../libexec/swift-backtrace/Utils.swift | 6 +- .../public/libexec/swift-backtrace/main.swift | 40 ++- 4 files changed, 266 insertions(+), 15 deletions(-) create mode 100644 stdlib/public/libexec/swift-backtrace/TargetLinux.swift diff --git a/stdlib/public/libexec/swift-backtrace/CMakeLists.txt b/stdlib/public/libexec/swift-backtrace/CMakeLists.txt index 50db65252bc84..10c8d6a9d3d70 100644 --- a/stdlib/public/libexec/swift-backtrace/CMakeLists.txt +++ b/stdlib/public/libexec/swift-backtrace/CMakeLists.txt @@ -30,6 +30,7 @@ add_swift_target_executable(swift-backtrace BUILD_WITH_LIBEXEC main.swift AnsiColor.swift TargetMacOS.swift + TargetLinux.swift Themes.swift Utils.swift @@ -44,5 +45,5 @@ add_swift_target_executable(swift-backtrace BUILD_WITH_LIBEXEC ${BACKTRACING_COMPILE_FLAGS} -parse-as-library - TARGET_SDKS OSX) + TARGET_SDKS OSX LINUX) diff --git a/stdlib/public/libexec/swift-backtrace/TargetLinux.swift b/stdlib/public/libexec/swift-backtrace/TargetLinux.swift new file mode 100644 index 0000000000000..c66435a864e22 --- /dev/null +++ b/stdlib/public/libexec/swift-backtrace/TargetLinux.swift @@ -0,0 +1,232 @@ +//===--- TargetLinux.swift - Represents a process we are inspecting -------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +// +// Defines `Target`, which represents the process we are inspecting. +// This is the Linux version. +// +//===----------------------------------------------------------------------===// + +#if os(Linux) + +import Glibc + +import _Backtracing +@_spi(Internal) import _Backtracing +@_spi(Contexts) import _Backtracing +@_spi(MemoryReaders) import _Backtracing +@_spi(Utils) import _Backtracing + +@_implementationOnly import Runtime + +struct TargetThread { + typealias ThreadID = pid_t + + var id: ThreadID + var context: HostContext? + var name: String + var backtrace: SymbolicatedBacktrace +} + +class Target { + typealias Address = UInt64 + + var pid: pid_t + var name: String + var signal: UInt64 + var faultAddress: Address + var crashingThread: TargetThread.ThreadID + + var images: [Backtrace.Image] = [] + + var threads: [TargetThread] = [] + var crashingThreadNdx: Int = -1 + + var signalName: String { + switch signal { + case UInt64(SIGQUIT): return "SIGQUIT" + case UInt64(SIGABRT): return "SIGABRT" + case UInt64(SIGBUS): return "SIGBUS" + case UInt64(SIGFPE): return "SIGFPE" + case UInt64(SIGILL): return "SIGILL" + case UInt64(SIGSEGV): return "SIGSEGV" + case UInt64(SIGTRAP): return "SIGTRAP" + default: return "\(signal)" + } + } + + var signalDescription: String { + switch signal { + case UInt64(SIGQUIT): return "Terminated" + case UInt64(SIGABRT): return "Aborted" + case UInt64(SIGBUS): return "Bus error" + case UInt64(SIGFPE): return "Floating point exception" + case UInt64(SIGILL): return "Illegal instruction" + case UInt64(SIGSEGV): return "Bad pointer dereference" + case UInt64(SIGTRAP): return "System trap" + default: + return "Signal \(signal)" + } + } + + var reader: MemserverMemoryReader + + // Get the name of a process + private static func getProcessName(pid: pid_t) -> String { + let path = "/proc/\(pid)/comm" + guard let name = readString(from: path) else { + return "" + } + return String(stripWhitespace(name)) + } + + /// Get the name of a thread + private func getThreadName(tid: Int64) -> String { + let path = "/proc/\(pid)/task/\(tid)/comm" + guard let name = readString(from: path) else { + return "" + } + let trimmed = String(stripWhitespace(name)) + + // Allow the main thread to use the process' name, but other + // threads will have an empty name unless they've set the name + // explicitly + if trimmed == self.name && pid != tid { + return "" + } + return trimmed + } + + init(crashInfoAddr: UInt64, limit: Int?, top: Int, cache: Bool) { + // fd #4 is reserved for the memory server + let memserverFd: CInt = 4 + + pid = getppid() + reader = MemserverMemoryReader(fd: memserverFd) + name = Self.getProcessName(pid: pid) + + let crashInfo: CrashInfo + do { + crashInfo = try reader.fetch(from: crashInfoAddr, as: CrashInfo.self) + } catch { + print("swift-backtrace: unable to fetch crash info.") + exit(1) + } + + crashingThread = TargetThread.ThreadID(crashInfo.crashing_thread) + signal = crashInfo.signal + faultAddress = crashInfo.fault_address + + images = Backtrace.captureImages(using: reader, + forProcess: Int(pid)) + + do { + try fetchThreads(threadListHead: Address(crashInfo.thread_list), + limit: limit, top: top, cache: cache) + } catch { + print("swift-backtrace: failed to fetch thread information: \(error)") + exit(1) + } + } + + /// Fetch information about all of the process's threads; the crash_info + /// structure contains a linked list of thread ucontexts, which may not + /// include every thread. In particular, if a thread was stuck in an + /// uninterruptible wait, we won't have a ucontext for it. + func fetchThreads( + threadListHead: Address, + limit: Int?, top: Int, cache: Bool + ) throws { + var next = threadListHead + + while next != 0 { + let t = try reader.fetch(from: next, as: thread.self) + + next = t.next + + guard let ucontext + = try? reader.fetch(from: t.uctx, as: ucontext_t.self) else { + // This can happen if a thread is in an uninterruptible wait + continue + } + + let context = HostContext.fromHostMContext(ucontext.uc_mcontext) + let backtrace = try Backtrace.capture(from: context, + using: reader, + images: images, + limit: limit, + top: top) + guard let symbolicated + = backtrace.symbolicated(with: images, + sharedCacheInfo: nil, + useSymbolCache: cache) else { + print("unable to symbolicate backtrace for thread \(t.tid)") + exit(1) + } + + threads.append(TargetThread(id: TargetThread.ThreadID(t.tid), + context: context, + name: getThreadName(tid: t.tid), + backtrace: symbolicated)) + } + + // Sort the threads by thread ID; the main thread always sorts + // lower than any other. + threads.sort { + return $0.id == pid || ($1.id != pid && $0.id < $1.id) + } + + // Find the crashing thread index + if let ndx = threads.firstIndex(where: { $0.id == crashingThread }) { + crashingThreadNdx = ndx + } else { + print("unable to find the crashing thread") + exit(1) + } + } + + public func redoBacktraces(limit: Int?, top: Int, cache: Bool) { + for (ndx, thread) in threads.enumerated() { + guard let context = thread.context else { + continue + } + + guard let backtrace = try? Backtrace.capture(from: context, + using: reader, + images: images, + limit: limit, + top: top) else { + print("unable to capture backtrace from context for thread \(ndx)") + continue + } + + guard let symbolicated = backtrace.symbolicated(with: images, + sharedCacheInfo: nil, + useSymbolCache: cache) else { + print("unable to symbolicate backtrace from context for thread \(ndx)") + continue + } + + threads[ndx].backtrace = symbolicated + } + } + + public func withDebugger(_ body: () -> ()) throws { + print(""" + From another shell, please run + + lldb --attach-pid \(pid) -o c + """) + body() + } +} + +#endif // os(Linux) diff --git a/stdlib/public/libexec/swift-backtrace/Utils.swift b/stdlib/public/libexec/swift-backtrace/Utils.swift index a17b765df16a5..9d104a857f174 100644 --- a/stdlib/public/libexec/swift-backtrace/Utils.swift +++ b/stdlib/public/libexec/swift-backtrace/Utils.swift @@ -51,7 +51,7 @@ internal func parseUInt64(_ s: S) -> UInt64? { } } -#if os(macOS) +#if os(macOS) || os(Linux) struct PosixError: Error { var errno: Int32 @@ -139,6 +139,8 @@ internal func spawn(_ path: String, args: [String]) throws { } } +#endif // os(macOS) + struct CFileStream: TextOutputStream { var fp: UnsafeMutablePointer @@ -153,5 +155,3 @@ struct CFileStream: TextOutputStream { var standardOutput = CFileStream(fp: stdout) var standardError = CFileStream(fp: stderr) - -#endif // os(macOS) diff --git a/stdlib/public/libexec/swift-backtrace/main.swift b/stdlib/public/libexec/swift-backtrace/main.swift index f31576ab42f86..038c44552e72c 100644 --- a/stdlib/public/libexec/swift-backtrace/main.swift +++ b/stdlib/public/libexec/swift-backtrace/main.swift @@ -10,7 +10,7 @@ // //===----------------------------------------------------------------------===// -#if os(macOS) +#if (os(macOS) || os(Linux)) && (arch(x86_64) || arch(arm64)) #if canImport(Darwin) import Darwin.C @@ -524,7 +524,13 @@ Generate a backtrace for the parent process. tcgetattr(0, &oldAttrs) var newAttrs = oldAttrs - newAttrs.c_lflag &= ~(UInt(ICANON) | UInt(ECHO)) + + #if os(Linux) + newAttrs.c_lflag &= ~UInt32(ICANON | ECHO) + #else + newAttrs.c_lflag &= ~UInt(ICANON | ECHO) + #endif + tcsetattr(0, TCSANOW, &newAttrs) return oldAttrs @@ -581,7 +587,7 @@ Generate a backtrace for the parent process. static func backtraceFormatter() -> BacktraceFormatter { var terminalSize = winsize(ws_row: 24, ws_col: 80, ws_xpixel: 1024, ws_ypixel: 768) - _ = ioctl(0, TIOCGWINSZ, &terminalSize) + _ = ioctl(0, CUnsignedLong(TIOCGWINSZ), &terminalSize) return BacktraceFormatter(formattingOptions .theme(theme) @@ -607,7 +613,6 @@ Generate a backtrace for the parent process. writeln("") writeln(theme.crashReason(description)) - writeln("") var mentionedImages = Set() let formatter = backtraceFormatter() @@ -615,7 +620,7 @@ Generate a backtrace for the parent process. func dump(ndx: Int, thread: TargetThread) { let crashed = thread.id == target.crashingThread ? " crashed" : "" let name = !thread.name.isEmpty ? " \"\(thread.name)\"" : "" - writeln("Thread \(ndx)\(name)\(crashed):\n") + writeln("\nThread \(ndx)\(name)\(crashed):\n") if args.registers! == .all { if let context = thread.context { @@ -660,6 +665,7 @@ Generate a backtrace for the parent process. } } + let addressWidthInChars = (crashingThread.backtrace.addressWidth + 3) / 4 switch args.showImages! { case .none: break @@ -671,10 +677,12 @@ Generate a backtrace for the parent process. } else { writeln("\n\nImages:\n") } - writeln(formatter.format(images: images)) + writeln(formatter.format(images: images, + addressWidth: addressWidthInChars)) case .all: writeln("\n\nImages:\n") - writeln(formatter.format(images: target.images)) + writeln(formatter.format(images: target.images, + addressWidth: addressWidthInChars)) } } @@ -750,11 +758,14 @@ Generate a backtrace for the parent process. let name = thread.name.isEmpty ? "" : " \(thread.name)" writeln("Thread \(currentThread) id=\(thread.id)\(name)\(crashed)\n") + let addressWidthInChars = (backtrace.addressWidth + 3) / 4 + if let frame = backtrace.frames.drop(while: { $0.isSwiftRuntimeFailure }).first { let formatter = backtraceFormatter() - let formatted = formatter.format(frame: frame) + let formatted = formatter.format(frame: frame, + addressWidth: addressWidthInChars) writeln("\(formatted)") } break @@ -809,6 +820,7 @@ Generate a backtrace for the parent process. var rows: [BacktraceFormatter.TableRow] = [] for (n, thread) in target.threads.enumerated() { let backtrace = thread.backtrace + let addressWidthInChars = (backtrace.addressWidth + 3) / 4 let crashed: String if n == target.crashingThreadNdx { @@ -827,7 +839,10 @@ Generate a backtrace for the parent process. $0.isSwiftRuntimeFailure }).first { - rows += formatter.formatRows(frame: frame).map{ row in + rows += formatter.formatRows( + frame: frame, + addressWidth: addressWidthInChars).map{ row in + switch row { case let .columns(columns): return .columns([ "", "" ] + columns) @@ -846,8 +861,11 @@ Generate a backtrace for the parent process. writeln(output) case "images": let formatter = backtraceFormatter() - let images = target.threads[currentThread].backtrace.images - let output = formatter.format(images: images) + let backtrace = target.threads[currentThread].backtrace + let images = backtrace.images + let addressWidthInChars = (backtrace.addressWidth + 3) / 4 + let output = formatter.format(images: images, + addressWidth: addressWidthInChars) writeln(output) case "set": From 937947747cb79138ea33bc51904014d8d89a1e66 Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Mon, 5 Jun 2023 22:37:35 +0100 Subject: [PATCH 2/3] Revert "[Backtracing][Linux] Temporarily add a default addressWidth." This reverts commit 0d7c45d9dac68ca820baec816b8857070168136a. --- .../Backtracing/BacktraceFormatter.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/stdlib/public/Backtracing/BacktraceFormatter.swift b/stdlib/public/Backtracing/BacktraceFormatter.swift index f475e0c500830..7f0abf15f637d 100644 --- a/stdlib/public/Backtracing/BacktraceFormatter.swift +++ b/stdlib/public/Backtracing/BacktraceFormatter.swift @@ -523,7 +523,7 @@ public struct BacktraceFormatter { /// /// @result An array of strings, one per column. public func formatColumns(frame: Backtrace.Frame, - addressWidth: Int = 64, + addressWidth: Int, index: Int? = nil) -> [String] { let pc: String var attrs: [String] = [] @@ -563,7 +563,7 @@ public struct BacktraceFormatter { /// /// @result An array of table rows. public func formatRows(frame: Backtrace.Frame, - addressWidth: Int = 64, + addressWidth: Int, index: Int? = nil) -> [TableRow] { return [.columns(formatColumns(frame: frame, addressWidth: addressWidth, @@ -578,7 +578,7 @@ public struct BacktraceFormatter { /// /// @result A `String` containing the formatted data. public func format(frame: Backtrace.Frame, - addressWidth: Int = 64, + addressWidth: Int, index: Int? = nil) -> String { let rows = formatRows(frame: frame, addressWidth: addressWidth, @@ -593,7 +593,7 @@ public struct BacktraceFormatter { /// /// @result A `String` containing the formatted data. public func format(frames: some Sequence, - addressWidth: Int = 64) -> String { + addressWidth: Int) -> String { var rows: [TableRow] = [] var n = 0 @@ -734,7 +734,7 @@ public struct BacktraceFormatter { /// /// @result An array of strings, one per column. public func formatColumns(frame: SymbolicatedBacktrace.Frame, - addressWidth: Int = 64, + addressWidth: Int, index: Int? = nil) -> [String] { let pc: String var attrs: [String] = [] @@ -851,7 +851,7 @@ public struct BacktraceFormatter { /// /// @result An array of table rows. public func formatRows(frame: SymbolicatedBacktrace.Frame, - addressWidth: Int = 64, + addressWidth: Int, index: Int? = nil, showSource: Bool = true) -> [TableRow] { let columns = formatColumns(frame: frame, @@ -880,7 +880,7 @@ public struct BacktraceFormatter { /// /// @result A `String` containing the formatted data. public func format(frame: SymbolicatedBacktrace.Frame, - addressWidth: Int = 64, + addressWidth: Int, index: Int? = nil, showSource: Bool = true) -> String { let rows = formatRows(frame: frame, addressWidth: addressWidth, @@ -902,7 +902,7 @@ public struct BacktraceFormatter { /// /// @result A `String` containing the formatted data. public func format(frames: some Sequence, - addressWidth: Int = 64) -> String { + addressWidth: Int) -> String { var rows: [TableRow] = [] var sourceLocationsShown = Set() @@ -982,7 +982,7 @@ public struct BacktraceFormatter { /// /// @result An array of strings, one per column. public func formatColumns(image: Backtrace.Image, - addressWidth: Int = 64) -> [String] { + addressWidth: Int) -> [String] { let addressRange = "\(hex(image.baseAddress, width: addressWidth))–\(hex(image.endOfText, width: addressWidth))" let buildID: String if let bytes = image.buildID { @@ -1011,7 +1011,7 @@ public struct BacktraceFormatter { /// /// @result A string containing the formatted data. public func format(images: some Sequence, - addressWidth: Int = 64) -> String { + addressWidth: Int) -> String { let rows = images.map{ TableRow.columns( formatColumns(image: $0, From f61e72cceea8bb80ad977d7202f7584277559404 Mon Sep 17 00:00:00 2001 From: Alastair Houghton Date: Tue, 6 Jun 2023 17:43:19 +0100 Subject: [PATCH 3/3] [Backtracing][Linux] Remove some stray "public" annotations. We don't need these here. They were a leftover from previous code. rdar://110262673 --- stdlib/public/libexec/swift-backtrace/TargetLinux.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stdlib/public/libexec/swift-backtrace/TargetLinux.swift b/stdlib/public/libexec/swift-backtrace/TargetLinux.swift index c66435a864e22..f6dda1b37d895 100644 --- a/stdlib/public/libexec/swift-backtrace/TargetLinux.swift +++ b/stdlib/public/libexec/swift-backtrace/TargetLinux.swift @@ -193,7 +193,7 @@ class Target { } } - public func redoBacktraces(limit: Int?, top: Int, cache: Bool) { + func redoBacktraces(limit: Int?, top: Int, cache: Bool) { for (ndx, thread) in threads.enumerated() { guard let context = thread.context else { continue @@ -219,7 +219,7 @@ class Target { } } - public func withDebugger(_ body: () -> ()) throws { + func withDebugger(_ body: () -> ()) throws { print(""" From another shell, please run