//===----------------------------------------------------------------------===// // // This source file is part of the Swift.org open source project // // Copyright (c) 2023-2025 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 // //===----------------------------------------------------------------------===// #if (os(macOS) || os(Linux)) && (arch(x86_64) || arch(arm64)) #if canImport(Darwin) import Darwin #elseif canImport(Glibc) import Glibc #elseif canImport(Musl) import Musl #elseif canImport(CRT) import CRT #endif @_spi(Formatting) import Runtime @_spi(Contexts) import Runtime @_spi(Registers) import Runtime @_spi(MemoryReaders) import Runtime @main internal struct SwiftBacktrace { enum UnwindAlgorithm { case fast case precise } enum Preset { case none case friendly case medium case full } enum ImagesToShow { case none case all case mentioned } enum RegistersToShow { case none case all case crashedOnly } enum OutputTo { case stdout case stderr case file } enum Symbolication { case off case fast case full } enum OutputFormat { case text case json } struct Arguments { var unwindAlgorithm: UnwindAlgorithm = .precise var demangle = false var interactive = false var color = false var timeout = 30 var preset: Preset = .none var threads: Bool? = nil var registers: RegistersToShow? = nil var crashInfo: UInt64? = nil var showImages: ImagesToShow? = nil var limit: Int? = 64 var top = 16 var sanitize: Bool? = nil var cache = true var outputTo: OutputTo = .stdout var symbolicate: Symbolication = .full var format: OutputFormat = .text var outputPath: String = "/tmp" } static var args = Arguments() static var formattingOptions = BacktraceFormattingOptions() static var target: Target? = nil static var currentThread: Int = 0 static var now = timespec(tv_sec: 0, tv_nsec: 0) static var backtraceDuration = timespec(tv_sec: 0, tv_nsec: 0) static var theme: any Theme { if args.color { return Themes.color } else { return Themes.plain } } static var outputStream: CFileStream? = nil static func write(_ string: String, flush: Bool = false) { var stream = outputStream! print(string, terminator: "", to: &stream) if flush { stream.flush() } } static func writeln(_ string: String, flush: Bool = false) { var stream = outputStream! print(string, to: &stream) if flush { stream.flush() } } static func subtract(timespec ts: timespec, from: timespec) -> timespec { var sec = from.tv_sec - ts.tv_sec var nsec = from.tv_nsec - ts.tv_nsec if nsec < 0 { sec -= 1 nsec += 1000000000 } return timespec(tv_sec: sec, tv_nsec: nsec) } // We can't use Foundation here, so there's no String(format:, ...) static func format(duration: timespec) -> String { let centisRounded = (duration.tv_nsec + 5000000) / 10000000 let centis = centisRounded % 100 let secs = duration.tv_sec + (centisRounded / 100) let d1 = centis / 10 let d2 = centis % 10 return "\(secs).\(d1)\(d2)" } static func measureDuration(_ body: () -> ()) -> timespec { var startTime = timespec(tv_sec: 0, tv_nsec: 0) var endTime = timespec(tv_sec: 0, tv_nsec: 0) clock_gettime(CLOCK_MONOTONIC, &startTime) body() clock_gettime(CLOCK_MONOTONIC, &endTime) return subtract(timespec: startTime, from: endTime) } static func usage() { print(""" usage: swift-backtrace [--unwind ] [--demangle []] [--interactive []] [--color []] [--timeout ] [--preset ] [--threads []] [--registers ] [--images ] [--cache []] [--output-to ] --crashinfo Generate a backtrace for the parent process. --unwind -u Set the unwind algorithm to use. Supported algorithms are "fast" and "precise". --demangle [] -d [] Set whether or not to demangle identifiers. --interactive [] -i [] Set whether to be interactive. --color [] -c [] Set whether to use ANSI color in the output. --timeout -t Set how long to wait for interaction. --preset -p Set the backtrace format (by preset). Options are "friendly", "medium" and "full". --threads [] -h [] Set whether or not to show all threads. --registers -r Set which registers dumps to show. Options are "none", "all" and "crashed". --images -m Set which images to list. Options are "none", "all" and "mentioned". --limit -l Set the limit on the number of frames to capture. Can be set to "none" to disable the limit. --top -T Set the minimum number of frames to capture at the top of the stack. This is used with limit to ensure that you capture sufficient frames to understand deep traces. --sanitize [] -s [] Set whether or not to sanitize paths. --cache [] Set whether or not to use the symbol cache, if any. --output-to Set which output stream to use. Options are "stdout" -o and "stderr". The default is "stdout". Alternatively, you may specify a file path here. If the path points to a directory, a unique filename will be generated automatically. --format Set the output format. Options are "text" and "json"; the default is "text". --crashinfo -a Provide a pointer to a platform specific CrashInfo structure. should be in hexadecimal. --symbolicate [] Set to "full" to fully symbolicate, including inline frames and source locations; "fast" to just do symbol lookup, of "off" to disable. The default is "full". """, to: &standardError) } static func parseBool(_ s: some StringProtocol) -> Bool { let lowered = s.lowercased() return lowered == "yes" || lowered == "y" || lowered == "true" || lowered == "t" || lowered == "on" } static func handleArgument(_ arg: String, value: String?) { switch arg { case "-?", "--help": usage() exit(0) case "-u", "--unwind": if let v = value { switch v.lowercased() { case "fast": args.unwindAlgorithm = .fast case "precise": args.unwindAlgorithm = .precise default: print("swift-backtrace: unknown unwind algorithm '\(v)'", to: &standardError) usage() exit(1) } } else { print("swift-backtrace: missing unwind algorithm", to: &standardError) usage() exit(1) } case "-d", "--demangle": if let v = value { args.demangle = parseBool(v) } else { args.demangle = true } case "-i", "--interactive": if let v = value { args.interactive = parseBool(v) } else { args.interactive = true } case "-c", "--color": if let v = value { args.color = parseBool(v) } else { args.color = true } case "-t", "--timeout": if let v = value { if let secs = Int(v), secs >= 0 { args.timeout = secs } else { print("swift-backtrace: bad timeout '\(v)'", to: &standardError) } } else { print("swift-backtrace: missing timeout value", to: &standardError) usage() exit(1) } case "-p", "--preset": if let v = value { switch v.lowercased() { case "friendly": args.preset = .friendly case "medium": args.preset = .medium case "full": args.preset = .full default: print("swift-backtrace: unknown preset '\(v)'", to: &standardError) usage() exit(1) } } else { print("swift-backtrace: missing preset name", to: &standardError) usage() exit(1) } case "-h", "--threads": if let v = value { if v.lowercased() != "preset" { args.threads = parseBool(v) } } else { args.threads = true } case "-r", "--registers": if let v = value { switch v.lowercased() { case "preset": break case "none": args.registers = RegistersToShow.none case "all": args.registers = .all case "crashed": args.registers = .crashedOnly default: print("swift-backtrace: unknown registers setting '\(v)'", to: &standardError) usage() exit(1) } } else { print("swift-backtrace: missing registers setting", to: &standardError) usage() exit(1) } case "-m", "--images": if let v = value { switch v.lowercased() { case "preset": break case "none": args.showImages = ImagesToShow.none case "all": args.showImages = .all case "mentioned": args.showImages = .mentioned default: print("swift-backtrace: unknown images setting '\(v)'", to: &standardError) usage() exit(1) } } else { print("swift-backtrace: missing images setting", to: &standardError) usage() exit(1) } case "-l", "--limit": if let v = value { if v.lowercased() == "none" { args.limit = nil } else if let limit = Int(v), limit > 0 { args.limit = limit } else { print("swift-backtrace: bad limit value \(v)", to: &standardError) } } else { print("swift-backtrace: missing limit value", to: &standardError) usage() exit(1) } case "-T", "--top": if let v = value { if let top = Int(v), top >= 0 { args.top = top } else { print("swift-backtrace: bad top value \(v)", to: &standardError) } } else { print("swift-backtrace: missing top value", to: &standardError) usage() exit(1) } case "-s", "--sanitize": if let v = value { args.sanitize = parseBool(v) } else { args.sanitize = true } case "--cache": if let v = value { args.cache = parseBool(v) } else { args.cache = true } case "-o", "--output-to": if let v = value { switch v.lowercased() { case "stdout": args.outputTo = .stdout case "stderr": args.outputTo = .stderr default: args.outputTo = .file args.outputPath = v } } else { print("swift-backtrace: missing output-to value", to: &standardError) usage() exit(1) } case "-a", "--crashinfo": if let v = value { if let a = UInt64(v, radix: 16) { args.crashInfo = a } else { print("swift-backtrace: bad pointer '\(v)'", to: &standardError) usage() exit(1) } } else { print("swift-backtrace: missing pointer value", to: &standardError) usage() exit(1) } case "--symbolicate": if let v = value { switch v.lowercased() { case "off": args.symbolicate = .off case "fast": args.symbolicate = .fast case "full": args.symbolicate = .full default: print("swift-backtrace: unknown symbolicate setting '\(v)'", to: &standardError) } } else { args.symbolicate = .full } case "--format": if let v = value { switch v.lowercased() { case "text": args.format = .text case "json": args.format = .json default: print("swift-backtrace: unknown output format '\(v)'", to: &standardError) } } else { print("swift-backtrace: missing format value", to: &standardError) usage() exit(1) } default: print("swift-backtrace: unknown argument '\(arg)'", to: &standardError) usage() exit(1) } } static func unblockSignals() { var mask = sigset_t() sigfillset(&mask) sigprocmask(SIG_UNBLOCK, &mask, nil) } static func main() { unblockSignals() parseArguments() guard let crashInfoAddr = args.crashInfo else { print("swift-backtrace: --crashinfo is not optional", to: &standardError) usage() exit(1) } // Set-up the backtrace formatting options switch args.preset { case .friendly, .none: formattingOptions = .skipRuntimeFailures(true) .showAddresses(false) .showSourceCode(true) .showFrameAttributes(false) .sanitizePaths(args.sanitize ?? false) if args.threads == nil { args.threads = false } if args.registers == nil { args.registers = RegistersToShow.none } if args.showImages == nil { args.showImages = ImagesToShow.none } case .medium: formattingOptions = .skipRuntimeFailures(true) .showSourceCode(true) .showFrameAttributes(true) .sanitizePaths(args.sanitize ?? true) if args.threads == nil { args.threads = false } if args.registers == nil { args.registers = .crashedOnly } if args.showImages == nil { args.showImages = .mentioned } case .full: formattingOptions = .skipRuntimeFailures(false) .skipThunkFunctions(false) .skipSystemFrames(false) .sanitizePaths(args.sanitize ?? true) if args.threads == nil { args.threads = true } if args.registers == nil { args.registers = .crashedOnly } if args.showImages == nil { args.showImages = .mentioned } } formattingOptions = formattingOptions.demangle(args.demangle) // We never use the showImages option; if we're going to show images, we // want to do it *once* for all the backtraces we showed. formattingOptions = formattingOptions.showImages(.none) // Target's initializer fetches and symbolicates backtraces, so // we want to time that part here. backtraceDuration = measureDuration { target = Target(crashInfoAddr: crashInfoAddr, limit: args.limit, top: args.top, cache: args.cache, symbolicate: args.symbolicate) currentThread = target!.crashingThreadNdx } // Grab the current wall clock time; if clock_gettime() fails, get a // lower resolution version instead. if clock_gettime(CLOCK_REALTIME, &now) != 0 { now.tv_sec = time(nil) now.tv_nsec = 0 } // Set up the output stream var didOpenOutput = false switch args.outputTo { case .stdout: outputStream = standardOutput case .stderr: outputStream = standardError case .file: if isDir(args.outputPath) { // If the output path is a directory, generate a filename let name = target!.name let pid = target!.pid var now = timespec(tv_sec: 0, tv_nsec: 0) let ext: String switch args.format { case .text: ext = "log" case .json: ext = "json" } var filename = "\(args.outputPath)/\(name)-\(pid)-\(now.tv_sec).\(now.tv_nsec).\(ext)" var fd = open(filename, O_RDWR|O_CREAT|O_EXCL, 0o644) var ndx = 1 while fd < 0 && (errno == EEXIST || errno == EINTR) { if errno != EINTR { ndx += 1 filename = "\(args.outputPath)/\(name)-\(pid)-\(now.tv_sec).\(now.tv_nsec)-\(ndx).\(ext)" } fd = open(filename, O_RDWR|O_CREAT|O_EXCL, 0o644) } if fd < 0 { print("swift-backtrace: unable to create \(filename) for writing", to: &standardError) outputStream = standardError } if let cFile = fdopen(fd, "wt") { didOpenOutput = true outputStream = CFileStream(fp: cFile) } else { close(fd) unlink(filename) print("swift-backtrace: unable to fdopen \(filename) for writing", to: &standardError) outputStream = standardError } } else if let cFile = fopen(args.outputPath, "wt") { didOpenOutput = true outputStream = CFileStream(fp: cFile) } else { print("swift-backtrace: unable to open \(args.outputPath) for writing", to: &standardError) outputStream = standardError } } defer { if didOpenOutput { outputStream!.close() } } // Clear (or complete) the message written by the crash handler; this // is always on stdout or stderr, even if you specify a file for output. var handlerOut: CFileStream if args.outputTo == .stdout { handlerOut = standardOutput } else { handlerOut = standardError } if args.color { print("\r\u{1b}[0K", terminator: "", to: &handlerOut) } else { print(" done ***\n\n", terminator: "", to: &handlerOut) } switch args.format { case .text: printCrashLog() writeln("") let formattedDuration = format(duration: backtraceDuration) writeln("Backtrace took \(formattedDuration)s") writeln("") case .json: outputJSONCrashLog() } #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) // On Darwin, if Developer Mode is turned off, or we can't tell if it's // on or not, disable interactivity var developerMode: Int32 = 0 var developerModeSize: Int = MemoryLayout.size if args.interactive && (sysctlbyname("security.mac.amfi.developer_mode_status", &developerMode, &developerModeSize, nil, 0) == -1 || developerMode == 0) { args.interactive = false } #endif if args.interactive { // Make sure we're line buffered setvbuf(stdout, nil, _IOLBF, 0) while let ch = waitForKey("Press space to interact, D to debug, or any other key to quit", timeout: args.timeout) { switch UInt8(ch) { case UInt8(ascii: " "): interactWithUser() exit(0) case UInt8(ascii: "D"), UInt8(ascii: "d"): startDebugger() default: exit(0) } } } } // Parse the command line arguments; we can't use swift-argument-parser // from here because that would create a dependency problem, so we do // it manually. static func parseArguments() { var currentArg: String? = nil for arg in CommandLine.arguments[1...] { if arg.hasPrefix("-") { if let key = currentArg { handleArgument(key, value: nil) } currentArg = arg } else { if let key = currentArg { handleArgument(key, value: arg) currentArg = nil } else if arg != "" { print("swift-backtrace: unexpected argument '\(arg)'", to: &standardError) usage() exit(1) } } } if let key = currentArg { handleArgument(key, value: nil) } } #if os(Linux) || os(macOS) || os(iOS) || os(watchOS) || os(tvOS) static func setRawMode() -> termios { var oldAttrs = termios() tcgetattr(0, &oldAttrs) var newAttrs = oldAttrs #if os(Linux) newAttrs.c_lflag &= ~UInt32(ICANON | ECHO) #else newAttrs.c_lflag &= ~UInt(ICANON | ECHO) #endif tcsetattr(0, TCSANOW, &newAttrs) return oldAttrs } static func resetInputMode(mode: termios) { var theMode = mode tcsetattr(0, TCSANOW, &theMode) } static func waitForKey(_ message: String, timeout: Int?) -> Int32? { let oldMode = setRawMode() defer { write("\r\u{1b}[0K", flush: true) resetInputMode(mode: oldMode) } if let timeout = timeout { var remaining = timeout while true { write("\r\(message) (\(remaining)s) ", flush: true) var pfd = pollfd(fd: 0, events: Int16(POLLIN), revents: 0) let ret = poll(&pfd, 1, 1000) if ret == 0 { remaining -= 1 if remaining == 0 { break } continue } else if ret < 0 { break } return getchar() } } else { write("\r\(message)", flush: true) return getchar() } return nil } #elseif os(Windows) static func waitForKey(_ message: String, timeout: Int?) -> Int32? { // ###TODO return nil } #endif static func backtraceFormatter() -> BacktraceFormatter { var terminalSize = winsize(ws_row: 24, ws_col: 80, ws_xpixel: 1024, ws_ypixel: 768) _ = ioctl(0, CUnsignedLong(TIOCGWINSZ), &terminalSize) return BacktraceFormatter(formattingOptions .theme(theme) .width(Int(terminalSize.ws_col))) } static func printCrashLog() { guard let target = target else { print("swift-backtrace: unable to get target", to: &standardError) return } let crashingThread = target.threads[target.crashingThreadNdx] let description: String if case let .symbolicated(symbolicated) = crashingThread.backtrace, let failure = symbolicated.swiftRuntimeFailure { description = failure } else { description = "Program crashed: \(target.signalDescription) at \(hex(target.faultAddress))" } writeln(theme.crashReason(description)) var mentionedImages = Set() let formatter = backtraceFormatter() let architecture: String switch crashingThread.backtrace { case let .raw(backtrace): architecture = backtrace.architecture case let .symbolicated(backtrace): architecture = backtrace.architecture } writeln("\nPlatform: \(theme.architecture(architecture)) \(theme.platform(target.images.platform))") func dump(ndx: Int, thread: TargetThread) { let crashed = thread.id == target.crashingThread ? " crashed" : "" let name = !thread.name.isEmpty ? " \"\(thread.name)\"" : "" writeln("\nThread \(ndx)\(name)\(crashed):\n") if args.registers! == .all { if let context = thread.context { showRegisters(context) } else { writeln(" " + theme.info("no context for thread \(ndx)")) } writeln("") } let formatted: String switch thread.backtrace { case let .raw(backtrace): formatted = formatter.format(backtrace: backtrace) case let .symbolicated(backtrace): formatted = formatter.format(backtrace: backtrace) } writeln(formatted) if args.showImages! == .mentioned { switch thread.backtrace { case let .raw(backtrace): for frame in backtrace.frames { let address = frame.adjustedProgramCounter if let imageNdx = target.images.firstIndex( where: { address >= $0.baseAddress && address < $0.endOfText } ) { mentionedImages.insert(imageNdx) } } case let .symbolicated(backtrace): for frame in backtrace.frames { if formatter.shouldSkip(frame) { continue } if let symbol = frame.symbol, symbol.imageIndex >= 0 { mentionedImages.insert(symbol.imageIndex) } } } } } if args.threads! { for (ndx, thread) in target.threads.enumerated() { dump(ndx: ndx, thread: thread) } } else { dump(ndx: target.crashingThreadNdx, thread: crashingThread) } if args.registers! == .crashedOnly { writeln("\n\nRegisters:\n") if let context = target.threads[target.crashingThreadNdx].context { showRegisters(context) } else { writeln(theme.info("no context for thread \(target.crashingThreadNdx)")) } } switch args.showImages! { case .none: break case .mentioned: let images = mentionedImages.sorted().map{ target.images[$0] } let omitted = target.images.count - images.count if omitted > 0 { writeln("\n\nImages (\(omitted) omitted):\n") } else { writeln("\n\nImages:\n") } writeln(formatter.format(images: images)) case .all: writeln("\n\nImages:\n") writeln(formatter.format(images: target.images)) } } static func startDebugger() { guard let target = target else { return } do { try target.withDebugger { if let ch = waitForKey("Press any key once LLDB is attached, or A to abort", timeout: nil), ch != UInt8(ascii: "A") && ch != UInt8(ascii: "a") { exit(0) } } } catch { writeln(theme.error("unable to spawn debugger")) } } static func interactWithUser() { guard let target = target else { return } while true { outputStream!.flush() write(theme.prompt(">>> "), flush: true) guard let input = readLine() else { print("") break } let cmd = input.split(whereSeparator: { $0.isWhitespace }) if cmd.count < 1 { continue } // ###TODO: We should really replace this with something a little neater switch cmd[0].lowercased() { case "exit", "quit": return case "debug": startDebugger() case "bt", "backtrace": let formatter = backtraceFormatter() let formatted: String switch target.threads[currentThread].backtrace { case let .raw(backtrace): formatted = formatter.format(backtrace: backtrace) case let .symbolicated(backtrace): formatted = formatter.format(backtrace: backtrace) } writeln(formatted) case "thread": if cmd.count >= 2 { if let newThreadNdx = Int(cmd[1]), newThreadNdx >= 0 && newThreadNdx < target.threads.count { currentThread = newThreadNdx } else { writeln(theme.error("Bad thread index '\(cmd[1])'")) break } } let crashed: String if currentThread == target.crashingThreadNdx { crashed = " (crashed)" } else { crashed = "" } let thread = target.threads[currentThread] let name = thread.name.isEmpty ? "" : " \(thread.name)" writeln("Thread \(currentThread) id=\(thread.id)\(name)\(crashed)\n") let formatter = backtraceFormatter() switch thread.backtrace { case let .raw(backtrace): if let frame = backtrace.frames.unsafeFirst { let formatted = formatter.format(frame: frame) writeln("\(formatted)") } case let .symbolicated(backtrace): if let frame = backtrace.frames.drop(while: { $0.isSwiftRuntimeFailure }).unsafeFirst { let formatted = formatter.format(frame: frame) writeln("\(formatted)") } } break case "reg", "registers": if let context = target.threads[currentThread].context { showRegisters(context) } else { writeln(theme.info("no context for thread \(currentThread)")) } break case "mem", "memory": if cmd.count != 2 && cmd.count != 3 { writeln("memory [|+]") break } guard let startAddress = parseUInt64(cmd[1]) else { writeln(theme.error("bad start address \(cmd[1])")) break } let count: UInt64 if cmd.count == 3 { if cmd[2].hasPrefix("+") { guard let theCount = parseUInt64(cmd[2].dropFirst()) else { writeln(theme.error("bad byte count \(cmd[2])")) break } count = theCount } else { guard let addr = parseUInt64(cmd[2]) else { writeln(theme.error("bad end address \(cmd[2])")) break } if addr < startAddress { writeln("End address must be after start address") break } count = addr - startAddress } } else { count = 256 } dumpMemory(at: startAddress, count: count) break case "process", "threads": writeln("Process \(target.pid) \"\(target.name)\" has \(target.threads.count) thread(s):\n") let formatter = backtraceFormatter() var rows: [BacktraceFormatter.TableRow] = [] for (n, thread) in target.threads.enumerated() { let crashed: String if n == target.crashingThreadNdx { crashed = " (crashed)" } else { crashed = "" } let selected = currentThread == n ? "▶︎" : " " let name = thread.name.isEmpty ? "" : " \(thread.name)" rows.append(.columns([ selected, "\(n)", "id=\(thread.id)\(name)\(crashed)" ])) switch thread.backtrace { case let .raw(backtrace): if let frame = backtrace.frames.unsafeFirst { rows += formatter.formatRows( frame: frame ).map{ row in switch row { case let .columns(columns): return .columns([ "", "" ] + columns) default: return row } } } case let .symbolicated(backtrace): if let frame = backtrace.frames.drop(while: { $0.isSwiftRuntimeFailure }).unsafeFirst { rows += formatter.formatRows( frame: frame ).map{ row in switch row { case let .columns(columns): return .columns([ "", "" ] + columns) default: return row } } } } } let output = BacktraceFormatter.formatTable(rows, alignments: [ .left, .right ]) writeln(output) case "images": let formatter = backtraceFormatter() let images = target.images let output = formatter.format(images: images) writeln(output) case "set": if cmd.count == 1 { let limit: String if let lim = args.limit { limit = "\(lim)" } else { limit = "none" } let top = "\(args.top)" writeln(""" addresses = \(formattingOptions.shouldShowAddresses) demangle = \(formattingOptions.shouldDemangle) frame-attrs = \(formattingOptions.shouldShowFrameAttributes) image-names = \(formattingOptions.shouldShowImageNames) limit = \(limit) sanitize = \(formattingOptions.shouldSanitizePaths) source = \(formattingOptions.shouldShowSourceCode) source-context = \(formattingOptions.sourceContextLines) system-frames = \(!formattingOptions.shouldSkipSystemFrames) thunks = \(!formattingOptions.shouldSkipThunkFunctions) top = \(top) symbolicate = \(args.symbolicate) """) } else { for optval in cmd[1...] { let parts = optval.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) if parts.count == 1 { let option = parts[0] switch option { case "addresses": writeln("addresses = \(formattingOptions.shouldShowAddresses)") case "demangle": writeln("demangle = \(formattingOptions.shouldDemangle)") case "frame-attrs": writeln("frame-attrs = \(formattingOptions.shouldShowFrameAttributes)") case "image-names": writeln("image-names = \(formattingOptions.shouldShowImageNames)") case "limit": if let limit = args.limit { writeln("limit = \(limit)") } else { writeln("limit = none") } case "sanitize": writeln("sanitize = \(formattingOptions.shouldSanitizePaths)") case "source": writeln("source = \(formattingOptions.shouldShowSourceCode)") case "source-context": writeln("source-context = \(formattingOptions.sourceContextLines)") case "system-frames": writeln("system-frames = \(!formattingOptions.shouldSkipSystemFrames)") case "thunks": writeln("thunks = \(!formattingOptions.shouldSkipThunkFunctions)") case "top": writeln("top = \(args.top)") case "symbolicate": writeln("symbolicate = \(args.symbolicate)") default: writeln(theme.error("unknown option '\(option)'")) } } else { let option = parts[0] let value = parts[1] var changedBacktrace = false switch option { case "limit": if value == "none" { args.limit = nil changedBacktrace = true } else if let limit = Int(value), limit > 0 { args.limit = limit changedBacktrace = true } else { writeln(theme.error("bad limit value '\(value)'")) } case "top": if let top = Int(value), top >= 0 { args.top = top changedBacktrace = true } else { writeln(theme.error("bad top value '\(value)'")) } case "source": formattingOptions = formattingOptions.showSourceCode(parseBool(value), contextLines: formattingOptions.sourceContextLines) case "source-context": if let lines = Int(value), lines >= 0 { formattingOptions = formattingOptions.showSourceCode(formattingOptions.shouldShowSourceCode, contextLines: lines) } else { writeln(theme.error("bad source-context value '\(value)'")) } case "thunks": formattingOptions = formattingOptions.skipThunkFunctions(!parseBool(value)) case "system-frames": formattingOptions = formattingOptions.skipSystemFrames(!parseBool(value)) case "frame-attrs": formattingOptions = formattingOptions.showFrameAttributes(parseBool(value)) case "addresses": formattingOptions = formattingOptions.showAddresses(parseBool(value)) case "sanitize": formattingOptions = formattingOptions.sanitizePaths(parseBool(value)) case "demangle": formattingOptions = formattingOptions.demangle(parseBool(value)) case "image-names": formattingOptions = formattingOptions.showImageNames(parseBool(value)) case "symbolicate": let oldSymbolicate = args.symbolicate switch value.lowercased() { case "off": args.symbolicate = .off case "fast": args.symbolicate = .fast case "full": args.symbolicate = .full default: writeln(theme.error("bad symbolicate value '\(value)'")) } if args.symbolicate != oldSymbolicate { changedBacktrace = true } default: writeln(theme.error("unknown option '\(option)'")) } if changedBacktrace { target.redoBacktraces(limit: args.limit, top: args.top, cache: args.cache, symbolicate: args.symbolicate) } } } } case "help": writeln(""" Available commands: backtrace Display a backtrace. bt Synonym for backtrace. debug Attach the debugger. exit Exit interaction, allowing program to crash normally. help Display help. images List images loaded by the program. mem Synonym for memory. memory Inspect memory. process Show information about the process. quit Synonym for exit. reg Synonym for registers. registers Display the registers. set Set or show options. thread Show or set the current thread. threads Synonym for process. """) default: writeln(theme.error("unknown command '\(cmd[0])'")) } writeln("") } } static func printableBytes(from bytes: some Sequence) -> String { // It would be nice to join these with ZWNJs to prevent ligature processing, // but sadly Terminal displays ZWNJ as a space character. return bytes.map{ byte in switch byte { case 0..<32, 127, 0x80..<0xa0: return theme.nonPrintable("·") default: return theme.printable(String(Unicode.Scalar(byte))) } }.joined(separator:"") } static func dumpMemory(at address: UInt64, count: UInt64) { guard let bytes = try? target!.reader.fetch( from: RemoteMemoryReader.Address(address), count: Int(count), as: UInt8.self) else { writeln("Unable to read memory") return } let startAddress = HostContext.stripPtrAuth(address: address) var ndx = 0 while ndx < bytes.count { let addr = startAddress + UInt64(ndx) let remaining = bytes.count - ndx let lineChunk = 16 let todo = min(remaining, lineChunk) let formattedBytes = theme.data(bytes[ndx..(name: String, value: T) { let hexValue = theme.hexValue(hex(value)) // Pad the register name let regPad = String(repeating: " ", count: max(3 - name.count, 0)) let reg = theme.register(regPad + name) // Grab 16 bytes at each address if possible if let bytes = try? target!.reader.fetch( from: RemoteMemoryReader.Address(value), count: 16, as: UInt8.self) { if args.sanitize ?? false { writeln("\(reg) \(hexValue) ") } else { let formattedBytes = theme.data(bytes.map{ hex($0, withPrefix: false) }.joined(separator: " ")) let printedBytes = printableBytes(from: bytes) writeln("\(reg) \(hexValue) \(formattedBytes) \(printedBytes)") } } else { let decValue = theme.decimalValue("\(value)") writeln("\(reg) \(hexValue) \(decValue)") } } static func showGPR(name: String, context: C, register: C.Register) { // Get the register contents let value = context.getRegister(register)! showRegister(name: name, value: value) } static func showGPRs(_ context: C, range: Rs) where Rs.Element == C.Register { for reg in range { showGPR(name: "\(reg)", context: context, register: reg) } } static func x86StatusFlags(_ flags: T) -> String { var status: [String] = [] if (flags & 0x400) != 0 { status.append("OF") } if (flags & 0x80) != 0 { status.append("SF") } if (flags & 0x40) != 0 { status.append("ZF") } if (flags & 0x10) != 0 { status.append("AF") } if (flags & 0x4) != 0 { status.append("PF") } if (flags & 0x1) != 0 { status.append("CF") } return status.joined(separator: " ") } static func showRegisters(_ context: X86_64Context) { showGPRs(context, range: .rax ... .r15) showRegister(name: "rip", value: context.programCounter) let rflags = context.getRegister(.rflags)! let cs = theme.hexValue(hex(UInt16(context.getRegister(.cs)!))) let fs = theme.hexValue(hex(UInt16(context.getRegister(.fs)!))) let gs = theme.hexValue(hex(UInt16(context.getRegister(.gs)!))) let hexFlags = theme.hexValue(hex(rflags)) let status = theme.flags(x86StatusFlags(rflags)) writeln("") writeln("\(theme.register("rflags")) \(hexFlags) \(status)") writeln("") writeln("\(theme.register("cs")) \(cs) \(theme.register("fs")) \(fs) \(theme.register("gs")) \(gs)") } static func showRegisters(_ context: I386Context) { showGPRs(context, range: .eax ... .edi) showRegister(name: "eip", value: context.programCounter) let eflags = UInt32(context.getRegister(.eflags)!) let es = theme.hexValue(hex(UInt16(context.getRegister(.es)!))) let cs = theme.hexValue(hex(UInt16(context.getRegister(.cs)!))) let ss = theme.hexValue(hex(UInt16(context.getRegister(.ss)!))) let ds = theme.hexValue(hex(UInt16(context.getRegister(.ds)!))) let fs = theme.hexValue(hex(UInt16(context.getRegister(.fs)!))) let gs = theme.hexValue(hex(UInt16(context.getRegister(.gs)!))) let hexFlags = theme.hexValue(hex(eflags)) let status = theme.flags(x86StatusFlags(eflags)) writeln("") writeln("\(theme.register("eflags")) \(hexFlags) \(status)") writeln("") writeln("\(theme.register("es")): \(es) \(theme.register("cs")): \(cs) \(theme.register("ss")): \(ss) \(theme.register("ds")): \(ds) \(theme.register("fs")): \(fs)) \(theme.register("gs")): \(gs)") } static func showRegisters(_ context: ARM64Context) { showGPRs(context, range: .x0 ..< .x29) showGPR(name: "fp", context: context, register: .x29) showGPR(name: "lr", context: context, register: .x30) showGPR(name: "sp", context: context, register: .sp) showGPR(name: "pc", context: context, register: .pc) } static func showRegisters(_ context: ARMContext) { showGPRs(context, range: .r0 ... .r10) showGPR(name: "fp", context: context, register: .r11) showGPR(name: "ip", context: context, register: .r12) showGPR(name: "sp", context: context, register: .r13) showGPR(name: "lr", context: context, register: .r14) showGPR(name: "pc", context: context, register: .r15) } } #else @main internal struct SwiftBacktrace { static public func main() { print("swift-backtrace: not supported on this platform.") } } #endif // os(macOS)