mirror of
https://github.com/apple/swift.git
synced 2026-06-20 15:42:51 +02:00
29a63e572a
`DebugActiveProcess()` can fail under certain circumstances, which results in us not seeing the crashing thread, hence you don't get a backtrace (in fact, the backtracer used to crash, since we never expected to be in that situation). Instead, use `NtQuerySystemInformation()` to get the thread list, then `OpenProcess()` and `OpenThread()` to get process/thread handles. This should be more robust. rdar://171438432
528 lines
17 KiB
Swift
528 lines
17 KiB
Swift
//===--- TargetWindows.swift - Represents a process we are inspecting -----===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2022-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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
//
|
|
// Defines `Target`, which represents the process we are inspecting.
|
|
// This is the Windows version.
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#if os(Windows)
|
|
|
|
import WinSDK
|
|
import CRT
|
|
|
|
import Runtime
|
|
@_spi(Internal) import Runtime
|
|
@_spi(Contexts) import Runtime
|
|
@_spi(MemoryReaders) import Runtime
|
|
|
|
internal import BacktracingImpl.OS.Windows
|
|
internal import BacktracingImpl.OS.WinNTInternals
|
|
|
|
enum SomeBacktrace {
|
|
case raw(Backtrace)
|
|
case symbolicated(SymbolicatedBacktrace)
|
|
}
|
|
|
|
struct TargetThread {
|
|
typealias ThreadID = DWORD
|
|
|
|
var id: ThreadID
|
|
var context: HostContext?
|
|
var name: String
|
|
var backtrace: SomeBacktrace
|
|
}
|
|
|
|
fileprivate func GetProcessImageFileName(_ hProcess: HANDLE) -> String? {
|
|
return withUnsafeTemporaryAllocation(of: WCHAR.self, capacity: 2048) {
|
|
buffer in
|
|
|
|
let dwRet = K32GetProcessImageFileNameW(hProcess, buffer.baseAddress,
|
|
DWORD(buffer.count))
|
|
if dwRet == 0 {
|
|
return nil
|
|
}
|
|
|
|
let slice = buffer[0..<Int(dwRet)]
|
|
|
|
return String(decoding: slice, as: UTF16.self)
|
|
}
|
|
}
|
|
|
|
fileprivate func GetThreadDescription(_ hThread: HANDLE) -> String? {
|
|
var description: PWSTR? = nil
|
|
|
|
let result = GetThreadDescription(hThread, &description)
|
|
if result != 0 {
|
|
return nil
|
|
}
|
|
|
|
return String.decodeCString(description!, as: UTF16.self)?.result
|
|
}
|
|
|
|
fileprivate func getProcessName(_ hProcess: HANDLE) -> String? {
|
|
// According to Microsoft's documentation, using GetModuleBaseName()
|
|
// for this is unreliable, and we should use GetProcessImageFileName()
|
|
// instead, then process the returned path.
|
|
//
|
|
// See https://learn.microsoft.com/en-gb/windows/win32/api/psapi/nf-psapi-getmodulebasenamew
|
|
|
|
guard let filename = GetProcessImageFileName(hProcess) else {
|
|
return nil
|
|
}
|
|
|
|
if let ndx = filename.lastIndex(of: "\\"), ndx < filename.endIndex {
|
|
return String(filename.suffix(from: filename.index(after: ndx)))
|
|
} else {
|
|
return filename
|
|
}
|
|
}
|
|
|
|
let ntDll = GetModuleHandleA("ntdll.dll")
|
|
let ntQuerySystemInformation = unsafeBitCast(GetProcAddress(ntDll, "NtQuerySystemInformation")!, to: PNT_QUERY_SYSTEM_INFORMATION.self)
|
|
let ntSuspendProcess = unsafeBitCast(GetProcAddress(ntDll, "NtSuspendProcess")!, to: PNT_SUSPEND_PROCESS.self)
|
|
let ntResumeProcess = unsafeBitCast(GetProcAddress(ntDll, "NtResumeProcess")!, to: PNT_RESUME_PROCESS.self)
|
|
|
|
fileprivate func threadIdsForProcess(_ processId: DWORD) -> [DWORD]? {
|
|
var bufferSize: ULONG = 0
|
|
|
|
let status = ntQuerySystemInformation(SystemProcessInformation,
|
|
nil,
|
|
0,
|
|
&bufferSize)
|
|
if status != 0 && status != NTSTATUS(bitPattern: STATUS_INFO_LENGTH_MISMATCH) {
|
|
return nil
|
|
}
|
|
|
|
var threadIds: [DWORD]? = nil
|
|
while true {
|
|
let status =
|
|
withUnsafeTemporaryAllocation(
|
|
of: UInt8.self, capacity: Int(bufferSize)
|
|
) { buffer in
|
|
let status = ntQuerySystemInformation(SystemProcessInformation,
|
|
buffer.baseAddress,
|
|
ULONG(buffer.count),
|
|
&bufferSize)
|
|
|
|
if status != 0 {
|
|
return status
|
|
}
|
|
|
|
var offset = 0
|
|
let totalSize = Int(bufferSize)
|
|
let spiSize = MemoryLayout<SYSTEM_PROCESS_INFORMATION>.size
|
|
while threadIds == nil
|
|
&& offset < totalSize
|
|
&& totalSize - offset >= spiSize {
|
|
let slice = UnsafeBufferPointer(rebasing:
|
|
buffer[offset..<offset+spiSize])
|
|
slice.withMemoryRebound(to: SYSTEM_PROCESS_INFORMATION.self) {
|
|
info in
|
|
|
|
if DWORD(UInt(bitPattern: info[0].UniqueProcessId)) == processId {
|
|
// If this is the process we're interested in, find the thread
|
|
// information
|
|
let threadInfoOffset = offset + spiSize
|
|
let stiSize = MemoryLayout<SYSTEM_THREAD_INFORMATION>.stride
|
|
let stiTotal = stiSize * Int(info[0].NumberOfThreads)
|
|
|
|
if threadInfoOffset + stiTotal <= totalSize {
|
|
let slice = buffer[threadInfoOffset..<threadInfoOffset+stiTotal]
|
|
var result: [DWORD] = []
|
|
|
|
UnsafeBufferPointer(rebasing:slice).withMemoryRebound(
|
|
to: SYSTEM_THREAD_INFORMATION.self
|
|
) { threads in
|
|
for thread in threads {
|
|
result.append(DWORD(UInt(bitPattern:
|
|
thread.ClientId.UniqueThread)))
|
|
}
|
|
}
|
|
|
|
threadIds = result
|
|
}
|
|
}
|
|
|
|
let nextOffset = Int(info[0].NextEntryOffset)
|
|
if nextOffset == 0 {
|
|
offset = totalSize
|
|
} else {
|
|
offset += nextOffset
|
|
}
|
|
}
|
|
}
|
|
|
|
return status
|
|
}
|
|
|
|
if status == 0 {
|
|
return threadIds
|
|
} else if status == NTSTATUS(bitPattern: STATUS_INFO_LENGTH_MISMATCH) {
|
|
continue
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
class Target {
|
|
typealias Address = UInt64
|
|
|
|
var hProcess: HANDLE = INVALID_HANDLE_VALUE
|
|
var hThreads: [HANDLE] = []
|
|
|
|
var pid: DWORD
|
|
var name: String
|
|
var crashingThread: TargetThread.ThreadID
|
|
var exceptionCode: DWORD
|
|
var faultAddress: Address
|
|
var exceptionInfo: Address
|
|
|
|
var images: ImageMap
|
|
|
|
var threads: [TargetThread] = []
|
|
var crashingThreadNdx: Int = -1
|
|
|
|
var signalDescription: String {
|
|
switch exceptionCode {
|
|
case EXCEPTION_ACCESS_VIOLATION: return "Access violation"
|
|
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "Array bounds exceeded"
|
|
case EXCEPTION_BREAKPOINT: return "Breakpoint"
|
|
case EXCEPTION_DATATYPE_MISALIGNMENT: return "Data type misalignment"
|
|
case EXCEPTION_FLT_DENORMAL_OPERAND: return "Denormal floating point"
|
|
case EXCEPTION_FLT_INEXACT_RESULT: return "Inexact floating point result"
|
|
case EXCEPTION_FLT_INVALID_OPERATION:
|
|
return "Invalid floating point operation"
|
|
case EXCEPTION_FLT_OVERFLOW: return "Floating point overflow"
|
|
case EXCEPTION_FLT_STACK_CHECK: return "Floating point stack check failed"
|
|
case EXCEPTION_FLT_UNDERFLOW: return "Floating point underflow"
|
|
case EXCEPTION_GUARD_PAGE: return "Guard page"
|
|
case EXCEPTION_ILLEGAL_INSTRUCTION: return "Illegal instruction"
|
|
case EXCEPTION_IN_PAGE_ERROR: return "Page fault"
|
|
case EXCEPTION_INT_DIVIDE_BY_ZERO: return "Integer divide by zero"
|
|
case EXCEPTION_INT_OVERFLOW: return "Integer overflow"
|
|
case EXCEPTION_INVALID_DISPOSITION: return "Invalid disposition"
|
|
case EXCEPTION_INVALID_HANDLE: return "Invalid handle"
|
|
case EXCEPTION_NONCONTINUABLE_EXCEPTION: return "Noncontinuable exception"
|
|
case EXCEPTION_PRIV_INSTRUCTION: return "Privileged instruction"
|
|
case EXCEPTION_SINGLE_STEP: return "Single step"
|
|
case EXCEPTION_STACK_OVERFLOW: return "Stack overflow"
|
|
default:
|
|
return "Exception \(hex(exceptionCode))"
|
|
}
|
|
}
|
|
|
|
var reader: RemoteMemoryReader
|
|
|
|
init(pid: DWORD,
|
|
crashInfoAddr: UInt64, limit: Int?, top: Int, cache: Bool,
|
|
symbolicate: SwiftBacktrace.Symbolication) {
|
|
self.pid = pid
|
|
|
|
guard let hProcess = OpenProcess(
|
|
DWORD(
|
|
PROCESS_VM_READ
|
|
| PROCESS_QUERY_LIMITED_INFORMATION
|
|
| PROCESS_SUSPEND_RESUME
|
|
),
|
|
false,
|
|
pid
|
|
) else {
|
|
let error = GetLastError()
|
|
print("swift-backtrace: unable to open process: \(hex(error)).",
|
|
to: &standardError)
|
|
exit(1)
|
|
}
|
|
|
|
let suspendStatus = ntSuspendProcess(hProcess)
|
|
if suspendStatus != 0 {
|
|
print("swift-backtrace: unable to suspend process: \(hex(suspendStatus)).",
|
|
to: &standardError)
|
|
exit(1)
|
|
}
|
|
|
|
guard let threads = threadIdsForProcess(pid) else {
|
|
print("swift-backtrace: unable to get thread list.",
|
|
to: &standardError)
|
|
exit(1)
|
|
}
|
|
|
|
for thread in threads {
|
|
guard let hThread = OpenThread(
|
|
DWORD(
|
|
THREAD_GET_CONTEXT
|
|
| THREAD_QUERY_LIMITED_INFORMATION
|
|
| THREAD_SUSPEND_RESUME
|
|
),
|
|
false,
|
|
thread
|
|
) else {
|
|
let error = GetLastError()
|
|
print("swift-backtrace: unable to open thread \(thread): \(hex(error)).",
|
|
to: &standardError)
|
|
continue
|
|
}
|
|
|
|
hThreads.append(hThread)
|
|
SuspendThread(hThread)
|
|
}
|
|
|
|
let resumeStatus = ntResumeProcess(hProcess)
|
|
if resumeStatus != 0 {
|
|
print("swift-backtrace: unable to resume process: \(hex(resumeStatus)).",
|
|
to: &standardError)
|
|
}
|
|
|
|
if hThreads.count < 1 {
|
|
print("swift-backtrace: no threads.", to: &standardError)
|
|
exit(1)
|
|
}
|
|
|
|
reader = RemoteMemoryReader(hProcess: UInt(bitPattern: hProcess))
|
|
|
|
if let theName = getProcessName(hProcess) {
|
|
name = theName
|
|
} else {
|
|
print("swift-backtrace: unable to fetch process name.", to: &standardError)
|
|
exit(1)
|
|
}
|
|
|
|
let crashInfo: CrashInfo
|
|
do {
|
|
crashInfo = try reader.fetch(from: crashInfoAddr, as: CrashInfo.self)
|
|
} catch {
|
|
print("swift-backtrace: unable to fetch crash info.", to: &standardError)
|
|
exit(1)
|
|
}
|
|
|
|
crashingThread = DWORD(truncatingIfNeeded: crashInfo.crashing_thread)
|
|
exceptionCode = DWORD(crashInfo.signal)
|
|
faultAddress = Address(truncatingIfNeeded: crashInfo.fault_address)
|
|
exceptionInfo = Address(truncatingIfNeeded: crashInfo.exception_info)
|
|
|
|
images = ImageMap.capture(for: UInt(bitPattern: hProcess))
|
|
|
|
fetchThreads(limit: limit, top: top, cache: cache, symbolicate: symbolicate)
|
|
}
|
|
|
|
func fetchThreads(
|
|
limit: Int?, top: Int, cache: Bool,
|
|
symbolicate: SwiftBacktrace.Symbolication
|
|
) {
|
|
for hThread in hThreads {
|
|
let id = GetThreadId(hThread)
|
|
let name = GetThreadDescription(hThread) ?? ""
|
|
|
|
// Grab the NT context and convert to our type
|
|
var ctx = CONTEXT()
|
|
|
|
let context = withUnsafeMutablePointer(to: &ctx) {
|
|
(lpctx: UnsafeMutablePointer<CONTEXT>) -> HostContext? in
|
|
if id == crashingThread {
|
|
do {
|
|
let ppctx = exceptionInfo + Address(MemoryLayout<UInt>.size)
|
|
let pctx = try reader.fetch(from: ppctx, as: UInt.self)
|
|
lpctx.pointee = try reader.fetch(from: Address(pctx),
|
|
as: CONTEXT.self)
|
|
} catch {
|
|
return nil
|
|
}
|
|
} else if !GetThreadContext(hThread, lpctx) {
|
|
return nil
|
|
}
|
|
|
|
return HostContext(ntContext: UnsafeRawPointer(lpctx))
|
|
}
|
|
|
|
guard let context else {
|
|
let err = GetLastError()
|
|
print("swift-backtrace: failed to get thread context: \(hex(err))",
|
|
to: &standardError)
|
|
continue
|
|
}
|
|
|
|
guard let backtrace = try? Backtrace.capture(from: context,
|
|
using: reader,
|
|
images: images,
|
|
algorithm: .auto,
|
|
limit: limit,
|
|
offset: 0,
|
|
top: top) else {
|
|
print("swift-backtrace: unable to capture backtrace from context for thread \(id)",
|
|
to: &standardError)
|
|
exit(1)
|
|
}
|
|
|
|
let shouldSymbolicate: Bool
|
|
var options: Backtrace.SymbolicationOptions
|
|
switch symbolicate {
|
|
case .off:
|
|
shouldSymbolicate = false
|
|
options = []
|
|
case .fast:
|
|
shouldSymbolicate = true
|
|
options = [ .showSourceLocations ]
|
|
case .full:
|
|
shouldSymbolicate = true
|
|
options = [ .showInlineFrames, .showSourceLocations ]
|
|
}
|
|
|
|
if cache {
|
|
options.insert(.useSymbolCache)
|
|
}
|
|
|
|
if id == crashingThread {
|
|
crashingThreadNdx = threads.count
|
|
ResumeThread(hThread)
|
|
}
|
|
|
|
if shouldSymbolicate {
|
|
guard let symbolicated = backtrace.symbolicated(
|
|
with: images,
|
|
options: options
|
|
) else {
|
|
print("unable to symbolicate backtrace from context for thread \(id)",
|
|
to: &standardError)
|
|
exit(1)
|
|
}
|
|
|
|
threads.append(TargetThread(id: id,
|
|
context: context,
|
|
name: name,
|
|
backtrace: .symbolicated(symbolicated)))
|
|
} else {
|
|
threads.append(TargetThread(id: id,
|
|
context: context,
|
|
name: name,
|
|
backtrace: .raw(backtrace)))
|
|
}
|
|
}
|
|
}
|
|
|
|
func redoBacktraces(
|
|
limit: Int?, top: Int,
|
|
cache: Bool,
|
|
symbolicate: SwiftBacktrace.Symbolication
|
|
) {
|
|
for (ndx, thread) in threads.enumerated() {
|
|
guard let backtrace = try? Backtrace.capture(from: thread.context!,
|
|
using: reader,
|
|
images: images,
|
|
algorithm: .auto,
|
|
limit: limit,
|
|
offset: 0,
|
|
top: top) else {
|
|
print("swift-backtrace: unable to capture backtrace from context for thread \(thread.id)",
|
|
to: &standardError)
|
|
continue
|
|
}
|
|
|
|
let shouldSymbolicate: Bool
|
|
var options: Backtrace.SymbolicationOptions
|
|
switch symbolicate {
|
|
case .off:
|
|
shouldSymbolicate = false
|
|
options = []
|
|
case .fast:
|
|
shouldSymbolicate = true
|
|
options = [ .showSourceLocations ]
|
|
case .full:
|
|
shouldSymbolicate = true
|
|
options = [ .showInlineFrames, .showSourceLocations ]
|
|
}
|
|
|
|
if cache {
|
|
options.insert(.useSymbolCache)
|
|
}
|
|
|
|
if shouldSymbolicate {
|
|
guard let symbolicated = backtrace.symbolicated(
|
|
with: images,
|
|
options: options
|
|
) else {
|
|
print("swift-backtrace: unable to symbolicate backtrace from context for thread \(thread.id)",
|
|
to: &standardError)
|
|
continue
|
|
}
|
|
|
|
threads[ndx].backtrace = .symbolicated(symbolicated)
|
|
} else {
|
|
threads[ndx].backtrace = .raw(backtrace)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum DebuggerResult {
|
|
case abort
|
|
case handOffToDebugger
|
|
}
|
|
|
|
func withDebugger(_ body: () -> DebuggerResult) throws {
|
|
let cmdline = """
|
|
cmd.exe "/c echo Once LLDB has attached, \
|
|
return to the other window and press any key \
|
|
& echo. \
|
|
& lldb --attach-pid \(pid) -o c \
|
|
& pause "
|
|
"""
|
|
let title = "Debugging \(name) (\(pid))"
|
|
|
|
var startupInfo = STARTUPINFOW()
|
|
startupInfo.cb = DWORD(MemoryLayout<STARTUPINFOW>.size)
|
|
var processInfo = PROCESS_INFORMATION()
|
|
|
|
let ret =
|
|
title.withCString(encodedAs: UTF16.self) { pwszTitle in
|
|
// Not really mutating
|
|
startupInfo.lpTitle = UnsafeMutablePointer(mutating: pwszTitle)
|
|
|
|
return cmdline.withCString(encodedAs: UTF16.self) { pwszCmdline in
|
|
return CreateProcessW(nil,
|
|
// Not really mutating
|
|
UnsafeMutablePointer(mutating: pwszCmdline),
|
|
nil,
|
|
nil,
|
|
false,
|
|
DWORD(NORMAL_PRIORITY_CLASS
|
|
| CREATE_NEW_CONSOLE
|
|
| CREATE_NEW_PROCESS_GROUP),
|
|
nil,
|
|
nil,
|
|
&startupInfo,
|
|
&processInfo)
|
|
}
|
|
}
|
|
|
|
if !ret {
|
|
let err = GetLastError()
|
|
print("swift-backtrace: unable to spawn debugger: \(hex(err))")
|
|
} else {
|
|
CloseHandle(processInfo.hProcess)
|
|
CloseHandle(processInfo.hThread)
|
|
|
|
if body() == .handOffToDebugger {
|
|
for (ndx,hThread) in hThreads.enumerated() {
|
|
if ndx != crashingThreadNdx {
|
|
ResumeThread(hThread)
|
|
}
|
|
}
|
|
exit(0)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif // os(Windows)
|
|
|