Files
swift-mirror/stdlib/public/Backtracing/Backtrace.swift
Alastair Houghton 293842a9d0 [Backtracing] Update module maps.
Now that Darwin is provided by the system, we no longer need to disable the
system module maps in favour of our own, which also resolves a few other
problems that doing that was creating.

rdar://137201928
2024-12-12 15:58:40 +00:00

686 lines
23 KiB
Swift

//===--- Backtrace.swift --------------------------------------*- 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
//
//===----------------------------------------------------------------------===//
//
// Defines the `Backtrace` struct that represents a captured backtrace.
//
//===----------------------------------------------------------------------===//
import Swift
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
internal import Darwin
#elseif os(Windows)
internal import ucrt
#elseif canImport(Glibc)
internal import Glibc
#elseif canImport(Musl)
internal import Musl
#endif
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
internal import BacktracingImpl.OS.Darwin
#endif
#if os(Linux)
internal import BacktracingImpl.ImageFormats.Elf
#endif
/// Holds a backtrace.
public struct Backtrace: CustomStringConvertible, Sendable {
/// The type of an address.
///
/// This is intentionally _not_ a pointer, because you shouldn't be
/// dereferencing them; they may refer to some other process, for
/// example.
public typealias Address = UInt64
/// The unwind algorithm to use.
public enum UnwindAlgorithm {
/// Choose the most appropriate for the platform.
case auto
/// Use the fastest viable method.
///
/// Typically this means walking the frame pointers.
case fast
/// Use the most precise available method.
///
/// On Darwin and on ELF platforms, this will use EH unwind
/// information. On Windows, it will use Win32 API functions.
case precise
}
/// Represents an individual frame in a backtrace.
public enum Frame: CustomStringConvertible, Sendable {
/// A program counter value.
///
/// This might come from a signal handler, or an exception or some
/// other situation in which we have captured the actual program counter.
///
/// These can be directly symbolicated, as-is, with no adjustment.
case programCounter(Address)
/// A return address.
///
/// Corresponds to a normal function call.
///
/// Requires adjustment when symbolicating for a backtrace, because it
/// points at the address after the one that triggered the child frame.
case returnAddress(Address)
/// An async resume point.
///
/// Corresponds to an `await` in an async task.
///
/// Can be directly symbolicated, as-is.
case asyncResumePoint(Address)
/// Indicates a discontinuity in the backtrace.
///
/// This occurs when you set a limit and a minimum number of frames at
/// the top. For example, if you set a limit of 10 frames and a minimum
/// of 4 top frames, but the backtrace generated 100 frames, you will see
///
/// 0: frame 100 <----- bottom of call stack
/// 1: frame 99
/// 2: frame 98
/// 3: frame 97
/// 4: frame 96
/// 5: ... <----- omittedFrames(92)
/// 6: frame 3
/// 7: frame 2
/// 8: frame 1
/// 9: frame 0 <----- top of call stack
///
/// Note that the limit *includes* the discontinuity.
///
/// This is good for handling cases involving deep recursion.
case omittedFrames(Int)
/// Indicates a discontinuity of unknown length.
case truncated
/// The program counter, without any adjustment.
public var originalProgramCounter: Address {
switch self {
case let .returnAddress(addr):
return addr
case let .programCounter(addr):
return addr
case let .asyncResumePoint(addr):
return addr
case .omittedFrames(_), .truncated:
return 0
}
}
/// The adjusted program counter to use for symbolication.
public var adjustedProgramCounter: Address {
switch self {
case let .returnAddress(addr):
return addr - 1
case let .programCounter(addr):
return addr
case let .asyncResumePoint(addr):
return addr
case .omittedFrames(_), .truncated:
return 0
}
}
/// A textual description of this frame.
public func description(width: Int) -> String {
switch self {
case let .programCounter(addr):
return "\(hex(addr, width: width))"
case let .returnAddress(addr):
return "\(hex(addr, width: width)) [ra]"
case let .asyncResumePoint(addr):
return "\(hex(addr, width: width)) [async]"
case .omittedFrames(_), .truncated:
return "..."
}
}
/// A textual description of this frame.
public var description: String {
return description(width: MemoryLayout<Address>.size * 2)
}
}
/// Represents an image loaded in the process's address space
public struct Image: CustomStringConvertible, Sendable {
/// The name of the image (e.g. libswiftCore.dylib).
public var name: String
/// The full path to the image (e.g. /usr/lib/swift/libswiftCore.dylib).
public var path: String
/// The build ID of the image, as a byte array (note that the exact number
/// of bytes may vary, and that some images may not have a build ID).
public var buildID: [UInt8]?
/// The base address of the image.
public var baseAddress: Backtrace.Address
/// The end of the text segment in this image.
public var endOfText: Backtrace.Address
/// Provide a textual description of an Image.
public func description(width: Int) -> String {
if let buildID = self.buildID {
return "\(hex(baseAddress, width: width))-\(hex(endOfText, width: width)) \(hex(buildID)) \(name) \(path)"
} else {
return "\(hex(baseAddress, width: width))-\(hex(endOfText, width: width)) <no build ID> \(name) \(path)"
}
}
/// A textual description of an Image.
public var description: String {
return description(width: MemoryLayout<Address>.size * 2)
}
}
/// The architecture of the system that captured this backtrace.
public var architecture: String
/// The width of an address in this backtrace, in bits.
public var addressWidth: Int
/// A list of captured frame information.
public var frames: [Frame]
/// A list of captured images.
///
/// Some backtracing algorithms may require this information, in which case
/// it will be filled in by the `capture()` method. Other algorithms may
/// not, in which case it will be `nil` and you can capture an image list
/// separately yourself using `captureImages()`.
public var images: [Image]?
/// Holds information about the shared cache.
public struct SharedCacheInfo: Sendable {
/// The UUID from the shared cache.
public var uuid: [UInt8]
/// The base address of the shared cache.
public var baseAddress: Backtrace.Address
/// Says whether there is in fact a shared cache.
public var noCache: Bool
}
/// Information about the shared cache.
///
/// Holds information about the shared cache. On Darwin only, this is
/// required for symbolication. On non-Darwin platforms it will always
/// be `nil`.
public var sharedCacheInfo: SharedCacheInfo?
/// Format an address according to the addressWidth.
///
/// @param address The address to format.
/// @param prefix Whether to include a "0x" prefix.
///
/// @returns A String containing the formatted Address.
public func formatAddress(_ address: Address,
prefix: Bool = true) -> String {
return hex(address, prefix: prefix, width: (addressWidth + 3) / 4)
}
/// Capture a backtrace from the current program location.
///
/// The `capture()` method itself will not be included in the backtrace;
/// i.e. the first frame will be the one in which `capture()` was called,
/// and its programCounter value will be the return address for the
/// `capture()` method call.
///
/// @param algorithm Specifies which unwind mechanism to use. If this
/// is set to `.auto`, we will use the platform default.
/// @param limit The backtrace will include at most this number of
/// frames; you can set this to `nil` to remove the
/// limit completely if required.
/// @param offset Says how many frames to skip; this makes it easy to
/// wrap this API without having to inline things and
/// without including unnecessary frames in the backtrace.
/// @param top Sets the minimum number of frames to capture at the
/// top of the stack.
///
/// @returns A new `Backtrace` struct.
@inline(never)
@_semantics("use_frame_pointer")
public static func capture(algorithm: UnwindAlgorithm = .auto,
limit: Int? = 64,
offset: Int = 0,
top: Int = 16) throws -> Backtrace {
#if os(Linux)
let images = captureImages()
#else
let images: [Image]? = nil
#endif
// N.B. We use offset+1 here to skip this frame, rather than inlining
// this code into the client.
return try HostContext.withCurrentContext { ctx in
try capture(from: ctx,
using: UnsafeLocalMemoryReader(),
images: images,
algorithm: algorithm,
limit: limit,
offset: offset + 1,
top: top)
}
}
@_spi(Internal)
public static func capture<Ctx: Context, Rdr: MemoryReader>(
from context: Ctx,
using memoryReader: Rdr,
images: [Image]?,
algorithm: UnwindAlgorithm = .auto,
limit: Int? = 64,
offset: Int = 0,
top: Int = 16
) throws -> Backtrace {
let addressWidth = 8 * MemoryLayout<Ctx.Address>.size
switch algorithm {
// All of them, for now, use the frame pointer unwinder. In the long
// run, we should be using DWARF EH frame data for .precise.
case .auto, .fast, .precise:
let unwinder =
FramePointerUnwinder(context: context,
images: images,
memoryReader: memoryReader)
.dropFirst(offset)
if let limit = limit {
if limit <= 0 {
return Backtrace(architecture: context.architecture,
addressWidth: addressWidth,
frames: [.truncated])
}
let realTop = top < limit ? top : limit - 1
var iterator = unwinder.makeIterator()
var frames: [Frame] = []
// Capture frames normally until we hit limit
while let frame = iterator.next() {
if frames.count < limit {
frames.append(frame)
if frames.count == limit {
break
}
}
}
if realTop == 0 {
if let _ = iterator.next() {
// More frames than we were asked for; replace the last
// one with a discontinuity
frames[limit - 1] = .truncated
}
return Backtrace(architecture: context.architecture,
addressWidth: addressWidth,
frames: frames)
} else {
// If we still have frames at this point, start tracking the
// last `realTop` frames in a circular buffer.
if let frame = iterator.next() {
let topSection = limit - realTop
var topFrames: [Frame] = []
var topNdx = 0
var omittedFrames = 0
topFrames.reserveCapacity(realTop)
topFrames.insert(contentsOf: frames.suffix(realTop - 1), at: 0)
topFrames.append(frame)
while let frame = iterator.next() {
topFrames[topNdx] = frame
topNdx += 1
omittedFrames += 1
if topNdx >= realTop {
topNdx = 0
}
}
// Fix the backtrace to include a discontinuity followed by
// the contents of the circular buffer.
let firstPart = realTop - topNdx
let secondPart = topNdx
frames[topSection - 1] = .omittedFrames(omittedFrames)
frames.replaceSubrange(topSection..<(topSection+firstPart),
with: topFrames.suffix(firstPart))
frames.replaceSubrange((topSection+firstPart)..<limit,
with: topFrames.prefix(secondPart))
}
return Backtrace(architecture: context.architecture,
addressWidth: addressWidth,
frames: frames,
images: images)
}
} else {
return Backtrace(architecture: context.architecture,
addressWidth: addressWidth,
frames: Array(unwinder),
images: images)
}
}
}
/// Capture a list of the images currently mapped into the calling
/// process.
///
/// @returns A list of `Image`s.
public static func captureImages() -> [Image] {
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
return captureImages(for: mach_task_self())
#else
return captureImages(using: UnsafeLocalMemoryReader())
#endif
}
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
private static func withDyldProcessInfo<T>(for task: task_t,
fn: (OpaquePointer?) throws -> T)
rethrows -> T {
var kret = kern_return_t(KERN_SUCCESS)
let dyldInfo = _dyld_process_info_create(task, 0, &kret)
if kret != KERN_SUCCESS {
fatalError("error: cannot create dyld process info")
}
defer {
_dyld_process_info_release(dyldInfo)
}
return try fn(dyldInfo)
}
#endif
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
@_spi(Internal)
public static func captureImages(for process: Any) -> [Image] {
var images: [Image] = []
let task = process as! task_t
withDyldProcessInfo(for: task) { dyldInfo in
_dyld_process_info_for_each_image(dyldInfo) {
(machHeaderAddress, uuid, path) in
if let path = path, let uuid = uuid {
let pathString = String(cString: path)
let theUUID = Array(UnsafeBufferPointer(start: uuid,
count: MemoryLayout<uuid_t>.size))
let name: String
if let slashIndex = pathString.lastIndex(of: "/") {
name = String(pathString.suffix(from:
pathString.index(after:slashIndex)))
} else {
name = pathString
}
// Find the end of the __TEXT segment
var endOfText = machHeaderAddress + 4096
_dyld_process_info_for_each_segment(dyldInfo, machHeaderAddress) {
address, size, name in
if let name = String(validatingCString: name!), name == "__TEXT" {
endOfText = address + size
}
}
images.append(Image(name: name,
path: pathString,
buildID: theUUID,
baseAddress: Address(machHeaderAddress),
endOfText: Address(endOfText)))
}
}
}
return images.sorted(by: { $0.baseAddress < $1.baseAddress })
}
#else // !(os(macOS) || os(iOS) || os(tvOS) || os(watchOS))
private struct AddressRange {
var low: Address = 0
var high: Address = 0
}
@_spi(Internal)
public static func captureImages<M: MemoryReader>(using reader: M,
forProcess pid: Int? = nil) -> [Image] {
var images: [Image] = []
#if os(Linux)
let path: String
if let pid = pid {
path = "/proc/\(pid)/maps"
} else {
path = "/proc/self/maps"
}
guard let procMaps = readString(from: path) else {
return []
}
// Find all the mapped files and get high/low ranges
var mappedFiles: [Substring:AddressRange] = [:]
for match in ProcMapsScanner(procMaps) {
let path = stripWhitespace(match.pathname)
if match.inode == "0" || path == "" {
continue
}
guard let start = Address(match.start, radix: 16),
let end = Address(match.end, radix: 16) else {
continue
}
if let range = mappedFiles[path] {
mappedFiles[path] = AddressRange(low: min(start, range.low),
high: max(end, range.high))
} else {
mappedFiles[path] = AddressRange(low: start,
high: end)
}
}
// Look for ELF headers in the process' memory
typealias Source = MemoryImageSource<M>
let source = Source(with: reader)
for match in ProcMapsScanner(procMaps) {
let path = stripWhitespace(match.pathname)
if match.inode == "0" || path == "" {
continue
}
guard let start = Address(match.start, radix: 16),
let end = Address(match.end, radix: 16),
let offset = Address(match.offset, radix: 16) else {
continue
}
if offset != 0 || end - start < EI_NIDENT {
continue
}
// Extract the filename from path
let name: Substring
if let slashIndex = path.lastIndex(of: "/") {
name = path.suffix(from: path.index(after: slashIndex))
} else {
name = path
}
// Inspect the image and extract the UUID and end of text
let range = mappedFiles[path]!
let subSource = SubImageSource(parent: source,
baseAddress: Source.Address(range.low),
length: Source.Size(range.high
- range.low))
var theUUID: [UInt8]? = nil
var endOfText: Address = range.low
if let image = try? Elf32Image(source: subSource) {
theUUID = image.uuid
for hdr in image.programHeaders {
if hdr.p_type == .PT_LOAD && (hdr.p_flags & PF_X) != 0 {
endOfText = max(endOfText, range.low + Address(hdr.p_vaddr
+ hdr.p_memsz))
}
}
} else if let image = try? Elf64Image(source: subSource) {
theUUID = image.uuid
for hdr in image.programHeaders {
if hdr.p_type == .PT_LOAD && (hdr.p_flags & PF_X) != 0 {
endOfText = max(endOfText, range.low + Address(hdr.p_vaddr
+ hdr.p_memsz))
}
}
} else {
// Not a valid ELF image
continue
}
let image = Image(name: String(name),
path: String(path),
buildID: theUUID,
baseAddress: range.low,
endOfText: endOfText)
images.append(image)
}
#endif
return images.sorted(by: { $0.baseAddress < $1.baseAddress })
}
#endif
/// Capture shared cache information.
///
/// @returns A `SharedCacheInfo`.
public static func captureSharedCacheInfo() -> SharedCacheInfo? {
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
return captureSharedCacheInfo(for: mach_task_self())
#else
return nil
#endif
}
@_spi(Internal)
public static func captureSharedCacheInfo(for t: Any) -> SharedCacheInfo? {
#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS)
let task = t as! task_t
return withDyldProcessInfo(for: task) { dyldInfo in
var cacheInfo = dyld_process_cache_info()
_dyld_process_info_get_cache(dyldInfo, &cacheInfo)
let theUUID = withUnsafePointer(to: cacheInfo.cacheUUID) {
Array(UnsafeRawBufferPointer(start: $0,
count: MemoryLayout<uuid_t>.size))
}
return SharedCacheInfo(uuid: theUUID,
baseAddress: Address(cacheInfo.cacheBaseAddress),
noCache: cacheInfo.noCache)
}
#else // !os(Darwin)
return nil
#endif
}
/// Return a symbolicated version of the backtrace.
///
/// @param images Specifies the set of images to use for symbolication.
/// If `nil`, the function will look to see if the `Backtrace`
/// has already captured images. If it has, those will be
/// used; otherwise we will capture images at this point.
///
/// @param sharedCacheInfo Provides information about the location and
/// identity of the shared cache, if applicable.
///
/// @param showInlineFrames If `true` and we know how on the platform we're
/// running on, add virtual frames to show inline
/// function calls.
///
/// @param showSourceLocation If `true`, look up the source location for
/// each address.
///
/// @param useSymbolCache If the system we are on has a symbol cache,
/// says whether or not to use it.
///
/// @returns A new `SymbolicatedBacktrace`.
public func symbolicated(with images: [Image]? = nil,
sharedCacheInfo: SharedCacheInfo? = nil,
showInlineFrames: Bool = true,
showSourceLocations: Bool = true,
useSymbolCache: Bool = true)
-> SymbolicatedBacktrace? {
return SymbolicatedBacktrace.symbolicate(
backtrace: self,
images: images,
sharedCacheInfo: sharedCacheInfo,
showInlineFrames: showInlineFrames,
showSourceLocations: showSourceLocations,
useSymbolCache: useSymbolCache
)
}
/// Provide a textual version of the backtrace.
public var description: String {
var lines: [String] = []
let addressChars = (addressWidth + 3) / 4
var n = 0
for frame in frames {
lines.append("\(n)\t\(frame.description(width: addressChars))")
switch frame {
case let .omittedFrames(count):
n += count
default:
n += 1
}
}
if let images = images {
lines.append("")
lines.append("Images:")
lines.append("")
for (n, image) in images.enumerated() {
lines.append("\(n)\t\(image.description(width: addressChars))")
}
}
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
if let sharedCacheInfo = sharedCacheInfo {
lines.append("")
lines.append("Shared Cache:")
lines.append("")
lines.append(" UUID: \(hex(sharedCacheInfo.uuid))")
lines.append(" Base: \(hex(sharedCacheInfo.baseAddress, width: addressChars))")
lines.append(" Active: \(!sharedCacheInfo.noCache)")
}
#endif
return lines.joined(separator: "\n")
}
}