mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
It's useful to capture the platform and platform version with the image map. Also, display both the platform and architecture information when generating a crash log. rdar://124913332
1378 lines
43 KiB
Swift
1378 lines
43 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2023 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
|
|
}
|
|
|
|
enum Symbolication {
|
|
case off
|
|
case fast
|
|
case full
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
static var args = Arguments()
|
|
static var formattingOptions = BacktraceFormattingOptions()
|
|
|
|
static var target: Target? = nil
|
|
static var currentThread: Int = 0
|
|
|
|
static var theme: any Theme {
|
|
if args.color {
|
|
return Themes.color
|
|
} else {
|
|
return Themes.plain
|
|
}
|
|
}
|
|
|
|
static var outputStream: CFileStream {
|
|
switch args.outputTo {
|
|
case .stdout: return standardOutput
|
|
case .stderr: return standardError
|
|
}
|
|
}
|
|
|
|
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 <algorithm>] [--demangle [<bool>]] [--interactive [<bool>]] [--color [<bool>]] [--timeout <seconds>] [--preset <preset>] [--threads [<bool>]] [--registers <registers>] [--images <images>] [--cache [<bool>]] [--output-to <stream>] --crashinfo <addr>
|
|
|
|
Generate a backtrace for the parent process.
|
|
|
|
--unwind <algorithm>
|
|
-u <algorithm> Set the unwind algorithm to use. Supported algorithms
|
|
are "fast" and "precise".
|
|
|
|
--demangle [<bool>]
|
|
-d [<bool>] Set whether or not to demangle identifiers.
|
|
|
|
--interactive [<bool>]
|
|
-i [<bool>] Set whether to be interactive.
|
|
|
|
--color [<bool>]
|
|
-c [<bool>] Set whether to use ANSI color in the output.
|
|
|
|
--timeout <seconds>
|
|
-t <seconds> Set how long to wait for interaction.
|
|
|
|
--preset <preset>
|
|
-p <preset> Set the backtrace format (by preset). Options are
|
|
"friendly", "medium" and "full".
|
|
|
|
--threads [<bool>]
|
|
-h [<bool>] Set whether or not to show all threads.
|
|
|
|
--registers <registers>
|
|
-r <registers> Set which registers dumps to show. Options are "none",
|
|
"all" and "crashed".
|
|
|
|
--images <images>
|
|
-m <images> Set which images to list. Options are "none", "all"
|
|
and "mentioned".
|
|
|
|
--limit <count>
|
|
-l <count> Set the limit on the number of frames to capture.
|
|
Can be set to "none" to disable the limit.
|
|
|
|
--top <count>
|
|
-T <count> 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 [<bool>]
|
|
-s [<bool>] Set whether or not to sanitize paths.
|
|
|
|
--cache [<bool>] Set whether or not to use the symbol cache, if any.
|
|
|
|
--output-to <stream> Set which output stream to use. Options are "stdout"
|
|
-o <stream> and "stderr". The default is "stdout".
|
|
|
|
--crashinfo <addr>
|
|
-a <addr> Provide a pointer to a platform specific CrashInfo
|
|
structure. <addr> should be in hexadecimal.
|
|
|
|
--symbolicate [<mode>] 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:
|
|
print("swift-backtrace: unknown output-to setting '\(v)'",
|
|
to: &standardError)
|
|
usage()
|
|
exit(1)
|
|
}
|
|
} 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
|
|
}
|
|
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.
|
|
let duration = measureDuration {
|
|
target = Target(crashInfoAddr: crashInfoAddr,
|
|
limit: args.limit, top: args.top,
|
|
cache: args.cache,
|
|
symbolicate: args.symbolicate)
|
|
|
|
currentThread = target!.crashingThreadNdx
|
|
}
|
|
|
|
printCrashLog()
|
|
|
|
writeln("")
|
|
|
|
let formattedDuration = format(duration: duration)
|
|
|
|
writeln("Backtrace took \(formattedDuration)s")
|
|
writeln("")
|
|
|
|
#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<Int32>.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))"
|
|
}
|
|
|
|
// Clear (or complete) the message written by the crash handler
|
|
if args.color {
|
|
write("\r\u{1b}[0K")
|
|
} else {
|
|
write(" done ***\n\n")
|
|
}
|
|
|
|
writeln(theme.crashReason(description))
|
|
|
|
var mentionedImages = Set<Int>()
|
|
let formatter = backtraceFormatter()
|
|
|
|
let platform = target.images.platform
|
|
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 <start-address> [<end-address>|+<byte-count>]")
|
|
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<UInt8>) -> 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..<ndx+todo].map{
|
|
hex($0, withPrefix: false)
|
|
}.joined(separator: " "))
|
|
let printedBytes = printableBytes(from: bytes[ndx..<ndx+todo])
|
|
let padding = String(repeating: " ",
|
|
count: (lineChunk - todo) * 3)
|
|
|
|
let hexAddr = theme.address(hex(addr, withPrefix: false))
|
|
writeln("\(hexAddr) \(formattedBytes)\(padding) \(printedBytes)")
|
|
|
|
ndx += todo
|
|
}
|
|
}
|
|
|
|
static func showRegister<T: FixedWidthInteger>(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) {
|
|
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<C: Context>(name: String, context: C, register: C.Register) {
|
|
// Get the register contents
|
|
let value = context.getRegister(register)!
|
|
|
|
showRegister(name: name, value: value)
|
|
}
|
|
|
|
static func showGPRs<C: Context, Rs: Sequence>(_ context: C, range: Rs) where Rs.Element == C.Register {
|
|
for reg in range {
|
|
showGPR(name: "\(reg)", context: context, register: reg)
|
|
}
|
|
}
|
|
|
|
static func x86StatusFlags<T: FixedWidthInteger>(_ 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)
|