mirror of
https://github.com/apple/swift.git
synced 2025-12-14 20:36:38 +01:00
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
686 lines
23 KiB
Swift
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")
|
|
}
|
|
}
|