Files
swift-mirror/stdlib/public/libexec/swift-backtrace/TargetWindows.swift
Alastair Houghton 7f8398e60b [Backtracing] Improve debug option on Windows.
Made the debug option on Windows work better.

rdar://101623384
2026-02-03 18:23:30 +00:00

473 lines
15 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
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
}
}
fileprivate func duplicateHandle(_ handle: HANDLE) -> HANDLE? {
var hResult: HANDLE? = nil
if DuplicateHandle(GetCurrentProcess(),
handle,
GetCurrentProcess(),
&hResult,
0,
false,
DWORD(DUPLICATE_SAME_ACCESS)) {
return hResult
}
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
if !DebugActiveProcess(pid) {
let error = GetLastError()
print("swift-backtrace: unable to debug process: \(hex(error)).",
to: &standardError)
exit(1)
}
// Make sure that if we crash, the original process continues
DebugSetProcessKillOnExit(false)
var event = DEBUG_EVENT()
// Process the debug events that tell us about the process
while WaitForDebugEvent(&event, 0) {
switch Int32(event.dwDebugEventCode) {
case CREATE_PROCESS_DEBUG_EVENT:
hProcess = event.u.CreateProcessInfo.hProcess
hThreads.append(event.u.CreateProcessInfo.hThread)
case CREATE_THREAD_DEBUG_EVENT:
hThreads.append(event.u.CreateThread.hThread)
default:
break
}
}
if hProcess == INVALID_HANDLE_VALUE {
print("swift-backtrace: didn't get a process handle.", to: &standardError)
exit(1)
}
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
}
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 {
// Suspend all the threads and duplicate the thread handles, since
// DebugActiveProcessStop() will close them, and we want to call
// ResumeThread() on them after LLDB attaches.
var hDuplicateThreadHandles: [HANDLE] = []
for hThread in hThreads {
if let hDuplicate = duplicateHandle(hThread) {
SuspendThread(hThread)
hDuplicateThreadHandles.append(hDuplicate)
}
}
// Stop debugging the process
DebugActiveProcessStop(pid)
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 hThread in hDuplicateThreadHandles {
ResumeThread(hThread)
CloseHandle(hThread)
}
exit(0)
}
}
// Resume debugging the process
while !DebugActiveProcess(pid) {
let err = GetLastError()
print("swift-backtrace: unable to debug process: \(hex(err)); retrying in 5s")
Sleep(5000)
}
// Resume the threads we suspended and close the copied handles
for hThread in hDuplicateThreadHandles {
ResumeThread(hThread)
CloseHandle(hThread)
}
// Update our handles
var event = DEBUG_EVENT()
hThreads = []
while WaitForDebugEvent(&event, 0) {
switch Int32(event.dwDebugEventCode) {
case CREATE_PROCESS_DEBUG_EVENT:
hProcess = event.u.CreateProcessInfo.hProcess
hThreads.append(event.u.CreateProcessInfo.hThread)
case CREATE_THREAD_DEBUG_EVENT:
hThreads.append(event.u.CreateThread.hThread)
default:
break
}
}
}
}
#endif // os(Windows)