Skip to content

Commit

Permalink
[Backtracing][Linux] Add Linux support to swift-backtrace.
Browse files Browse the repository at this point in the history
We need a Linux specific `Target` implementation, and a couple of minor
tweaks to make things build everywhere.

rdar://110262673
  • Loading branch information
al45tair committed Jun 5, 2023
1 parent c8377ae commit 76e4076
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 15 deletions.
3 changes: 2 additions & 1 deletion stdlib/public/libexec/swift-backtrace/CMakeLists.txt
Expand Up @@ -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

Expand All @@ -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)

232 changes: 232 additions & 0 deletions 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)
6 changes: 3 additions & 3 deletions stdlib/public/libexec/swift-backtrace/Utils.swift
Expand Up @@ -51,7 +51,7 @@ internal func parseUInt64<S: StringProtocol>(_ s: S) -> UInt64? {
}
}

#if os(macOS)
#if os(macOS) || os(Linux)

struct PosixError: Error {
var errno: Int32
Expand Down Expand Up @@ -139,6 +139,8 @@ internal func spawn(_ path: String, args: [String]) throws {
}
}

#endif // os(macOS)

struct CFileStream: TextOutputStream {
var fp: UnsafeMutablePointer<FILE>

Expand All @@ -153,5 +155,3 @@ struct CFileStream: TextOutputStream {

var standardOutput = CFileStream(fp: stdout)
var standardError = CFileStream(fp: stderr)

#endif // os(macOS)
40 changes: 29 additions & 11 deletions stdlib/public/libexec/swift-backtrace/main.swift
Expand Up @@ -10,7 +10,7 @@
//
//===----------------------------------------------------------------------===//

#if os(macOS)
#if (os(macOS) || os(Linux)) && (arch(x86_64) || arch(arm64))

#if canImport(Darwin)
import Darwin.C
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -607,15 +613,14 @@ Generate a backtrace for the parent process.

writeln("")
writeln(theme.crashReason(description))
writeln("")

var mentionedImages = Set<Int>()
let formatter = backtraceFormatter()

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 {
Expand Down Expand Up @@ -660,6 +665,7 @@ Generate a backtrace for the parent process.
}
}

let addressWidthInChars = (crashingThread.backtrace.addressWidth + 3) / 4
switch args.showImages! {
case .none:
break
Expand All @@ -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))
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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":
Expand Down

0 comments on commit 76e4076

Please sign in to comment.