From 3b43427f10aaee5cb3244a16b1db83d6078965c6 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Sat, 13 Nov 2021 21:56:17 +0800 Subject: [PATCH 1/7] emit CodeView debug info for msvc debug builds --- src/compiler/crystal/codegen/debug.cr | 13 ++++++++++--- src/compiler/crystal/compiler.cr | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/compiler/crystal/codegen/debug.cr b/src/compiler/crystal/codegen/debug.cr index feb58a32bd13..478017f25b2d 100644 --- a/src/compiler/crystal/codegen/debug.cr +++ b/src/compiler/crystal/codegen/debug.cr @@ -33,9 +33,16 @@ module Crystal def push_debug_info_metadata(mod) di_builder(mod).end - # DebugInfo generation in LLVM by default uses a higher version of dwarf - # than OS X currently understands. Android has the same problem. - if @program.has_flag?("osx") || @program.has_flag?("android") + if @program.has_flag?("windows") + # Windows uses CodeView instead of DWARF + mod.add_flag( + LLVM::ModuleFlag::Warning, + "CodeView", + mod.context.int32.const_int(1) + ) + elsif @program.has_flag?("osx") || @program.has_flag?("android") + # DebugInfo generation in LLVM by default uses a higher version of dwarf + # than OS X currently understands. Android has the same problem. mod.add_flag( LLVM::ModuleFlag::Warning, "Dwarf Version", diff --git a/src/compiler/crystal/compiler.cr b/src/compiler/crystal/compiler.cr index c657811dd067..8ebf609e79d0 100644 --- a/src/compiler/crystal/compiler.cr +++ b/src/compiler/crystal/compiler.cr @@ -346,7 +346,7 @@ module Crystal object_arg = Process.quote_windows(object_names) output_arg = Process.quote_windows("/Fe#{output_filename}") - args = %(/nologo #{object_arg} #{output_arg} /link #{lib_flags} #{@link_flags}).gsub("\n", " ") + args = %(/nologo #{object_arg} #{output_arg} /link #{lib_flags} #{@link_flags} #{"/DEBUG:FULL" unless debug.none?}).gsub("\n", " ") cmd = "#{CL} #{args}" if cmd.to_utf16.size > 32000 From 0908acf5ee19a6e92084aebfce55c2432554f81a Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Sat, 13 Nov 2021 23:23:14 +0800 Subject: [PATCH 2/7] implement call stack api on windows --- src/exception.cr | 6 +- src/exception/call_stack.cr | 11 +- src/exception/call_stack/stackwalk.cr | 179 ++++++++++++++++++ src/kernel.cr | 7 +- src/lib_c/x86_64-windows-msvc/c/dbghelp.cr | 137 ++++++++++++++ src/lib_c/x86_64-windows-msvc/c/int_safe.cr | 1 + .../c/processthreadsapi.cr | 1 + src/lib_c/x86_64-windows-msvc/c/winnt.cr | 75 ++++++++ src/raise.cr | 1 + src/windows_stubs.cr | 6 - 10 files changed, 407 insertions(+), 17 deletions(-) create mode 100644 src/exception/call_stack/stackwalk.cr create mode 100644 src/lib_c/x86_64-windows-msvc/c/dbghelp.cr diff --git a/src/exception.cr b/src/exception.cr index 9987f886a3f2..6938bb92888f 100644 --- a/src/exception.cr +++ b/src/exception.cr @@ -35,11 +35,7 @@ class Exception # The backtrace is an array of strings, each containing # “0xAddress: Function at File Line Column”. def backtrace? - {% if flag?(:win32) %} - Array(String).new - {% else %} - @callstack.try &.printable_backtrace - {% end %} + @callstack.try &.printable_backtrace end def to_s(io : IO) : Nil diff --git a/src/exception/call_stack.cr b/src/exception/call_stack.cr index 498791d97a2b..46cf985a9fa5 100644 --- a/src/exception/call_stack.cr +++ b/src/exception/call_stack.cr @@ -1,6 +1,8 @@ -{% skip_file if flag?(:win32) %} - -require "./call_stack/libunwind" +{% if flag?(:win32) %} + require "./call_stack/stackwalk" +{% else %} + require "./call_stack/libunwind" +{% end %} # Returns the current execution stack as an array containing strings # usually in the form file:line:column or file:line:column in 'method'. @@ -14,8 +16,7 @@ struct Exception::CallStack # are always shown relative to the *starting* working directory. CURRENT_DIR = begin if dir = Process::INITIAL_PWD - dir += File::SEPARATOR unless dir.ends_with?(File::SEPARATOR) - dir + Path[dir] end end diff --git a/src/exception/call_stack/stackwalk.cr b/src/exception/call_stack/stackwalk.cr new file mode 100644 index 000000000000..7e3347a117ea --- /dev/null +++ b/src/exception/call_stack/stackwalk.cr @@ -0,0 +1,179 @@ +require "c/dbghelp" + +# :nodoc: +struct Exception::CallStack + skip(__FILE__) + + @@sym_loaded = false + + # :nodoc: + def self.load_debug_info + return if ENV["CRYSTAL_LOAD_DEBUG_INFO"]? == "0" + + unless @@sym_loaded + @@sym_loaded = true + begin + load_debug_info_impl + rescue ex + Crystal::System.print_exception "Unable to load debug information", ex + end + end + end + + private def self.load_debug_info_impl + if LibC.SymInitializeW(LibC.GetCurrentProcess, nil, 1) == 0 + raise RuntimeError.from_errno("SymInitializeW") + end + LibC.SymSetOptions(LibC.SymGetOptions | LibC::SYMOPT_LOAD_LINES | LibC::SYMOPT_FAIL_CRITICAL_ERRORS | LibC::SYMOPT_NO_PROMPTS) + end + + def self.unwind + load_debug_info + + # TODO: use stack if possible (must be 16-byte aligned) + context = Pointer(LibC::CONTEXT).malloc(1) + context.value.contextFlags = LibC::CONTEXT_FULL + LibC.RtlCaptureContext(context) + + stack_frame = LibC::STACKFRAME64.new + stack_frame.addrPC.mode = LibC::ADDRESS_MODE::AddrModeFlat + stack_frame.addrFrame.mode = LibC::ADDRESS_MODE::AddrModeFlat + stack_frame.addrStack.mode = LibC::ADDRESS_MODE::AddrModeFlat + + machine_type = LibC::IMAGE_FILE_MACHINE_AMD64 + {% if flag?(:x86_64) %} + stack_frame.addrPC.offset = context.value.rip + stack_frame.addrFrame.offset = context.value.rbp + stack_frame.addrStack.offset = context.value.rsp + {% elsif flag?(:i386) %} + machine_type = LibC::IMAGE_FILE_MACHINE_I386 + stack_frame.addrPC.offset = context.value.eip + stack_frame.addrFrame.offset = context.value.ebp + stack_frame.addrStack.offset = context.value.esp + {% else %} + {% raise "architecture not supported" %} + {% end %} + + stack = [] of Void* + + while true + ret = LibC.StackWalk64( + machine_type, + LibC.GetCurrentProcess, + LibC.GetCurrentThread, + pointerof(stack_frame), + context, + nil, + nil, # ->LibC.SymFunctionTableAccess64, + nil, # ->LibC.SymGetModuleBase64, + nil + ) + break if ret == 0 + stack << Pointer(Void).new(stack_frame.addrPC.offset) + end + + stack + end + + private def decode_backtrace + show_full_info = ENV["CRYSTAL_CALLSTACK_FULL_INFO"]? == "1" + + @callstack.compact_map do |ip| + pc = decode_address(ip) + + file, line_number, _ = CallStack.decode_line_number(pc) + + if file && file != "??" + next if @@skip.includes?(file) + + # Turn to relative to the current dir, if possible + if current_dir = CURRENT_DIR + if rel = Path[file].relative_to?(current_dir) + rel = rel.to_s + file = rel unless rel.starts_with?("..") + end + end + + file_line = "#{file}:#{line_number}" unless line_number == "??" + end + + if name = CallStack.decode_function_name(pc) + function = name + elsif frame = CallStack.decode_frame(ip) + _, function, file = frame + # Crystal methods (their mangled name) start with `*`, so + # we remove that to have less clutter in the output. + function = function.lchop('*') + else + function = "??" + end + + if file_line + line = "#{file_line} in '#{function}'" + else + if file == "??" && function == "??" + line = "???" + else + line = "#{file} in '#{function}'" + end + end + + if show_full_info + line = "#{line} at 0x#{ip.address.to_s(16)}" + end + + line + end + end + + protected def self.decode_line_number(pc) + load_debug_info + + line_info = uninitialized LibC::IMAGEHLP_LINEW64 + line_info.sizeOfStruct = sizeof(LibC::IMAGEHLP_LINEW64) + + displacement = uninitialized LibC::DWORD + if LibC.SymGetLineFromAddrW64(LibC.GetCurrentProcess, pc, pointerof(displacement), pointerof(line_info)) != 0 + file_name = String.from_utf16(line_info.fileName)[0] + line_number = line_info.lineNumber + else + line_number = 0 + end + + unless file_name + module_info = Pointer(LibC::IMAGEHLP_MODULEW64).malloc(1) + module_info.value.sizeOfStruct = sizeof(LibC::IMAGEHLP_MODULEW64) + + if LibC.SymGetModuleInfoW64(LibC.GetCurrentProcess, pc, module_info) != 0 + mod_displacement = pc - LibC.SymGetModuleBase64(LibC.GetCurrentProcess, pc) + file_name = "#{String.from_utf16(module_info.value.loadedImageName.to_unsafe)[0]}+0x#{mod_displacement.to_s(16)}" + else + file_name = "??" + end + end + + {file_name, line_number, 0} + end + + protected def self.decode_function_name(pc) + load_debug_info + + symbol_size = sizeof(LibC::SYMBOL_INFOW) + (LibC::MAX_SYM_NAME - 1) * sizeof(LibC::WCHAR) + symbol = Pointer(UInt8).malloc(symbol_size).as(LibC::SYMBOL_INFOW*) + symbol.value.sizeOfStruct = sizeof(LibC::SYMBOL_INFOW) + symbol.value.maxNameLen = LibC::MAX_SYM_NAME + + sym_displacement = LibC::DWORD64.zero + if LibC.SymFromAddrW(LibC.GetCurrentProcess, pc, pointerof(sym_displacement), symbol) != 0 + String.from_utf16(symbol.value.name.to_unsafe)[0] + # UnDecorateSymbolNameW + end + end + + protected def self.decode_frame(pc) + end + + private def decode_address(ip) + ip.address + end +end diff --git a/src/kernel.cr b/src/kernel.cr index 27df6cbe5798..39140ec59c98 100644 --- a/src/kernel.cr +++ b/src/kernel.cr @@ -536,7 +536,12 @@ end Signal.setup_segfault_handler {% end %} -{% if !flag?(:win32) %} +{% if flag?(:win32) %} + Exception::CallStack.load_debug_info if ENV["CRYSTAL_LOAD_DEBUG_INFO"]? == "1" + # TODO: figure out when to call SymCleanup (it cannot be done in `at_exit` + # because unhandled exceptions in `main_user_code` are printed after those + # handlers) +{% else %} # load dwarf on start up of the program is executed with CRYSTAL_LOAD_DWARF=1 # this will make dwarf available on print_frame that is used by Crystal's segfault handler # diff --git a/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr b/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr new file mode 100644 index 000000000000..9a9e2fd15ec9 --- /dev/null +++ b/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr @@ -0,0 +1,137 @@ +@[Link("DbgHelp")] +lib LibC + MAX_SYM_NAME = 2000 + + SYMOPT_LOAD_LINES = 0x00000010 + SYMOPT_FAIL_CRITICAL_ERRORS = 0x00000200 + SYMOPT_NO_PROMPTS = 0x00080000 + + struct SYMBOL_INFOW + sizeOfStruct : DWORD + typeIndex : DWORD + reserved : DWORD64[2] + index : DWORD + size : DWORD + modBase : DWORD64 + flags : DWORD + value : DWORD64 + address : DWORD64 + register : DWORD + scope : DWORD + tag : DWORD + nameLen : DWORD + maxNameLen : DWORD + name : WCHAR[1] # VLA + end + + struct IMAGEHLP_LINEW64 + sizeOfStruct : DWORD + key : Void* + lineNumber : DWORD + fileName : LPWSTR + address : DWORD64 + end + + alias SYM_TYPE = DWORD + + struct IMAGEHLP_MODULEW64 + sizeOfStruct : DWORD + baseOfImage : DWORD64 + imageSize : DWORD + timeDateStamp : DWORD + checkSum : DWORD + numSyms : DWORD + symType : SYM_TYPE + moduleName : WCHAR[32] + imageName : WCHAR[256] + loadedImageName : WCHAR[256] + loadedPdbName : WCHAR[256] + cVSig : DWORD + cVData : WCHAR[780] # MAX_PATH * 3 + pdbSig : DWORD + pdbSig70 : GUID + pdbAge : DWORD + pdbUnmatched : BOOL + dbgUnmatched : BOOL + lineNumbers : BOOL + globalSymbols : BOOL + typeInfo : BOOL + sourceIndexed : BOOL + publics : BOOL + machineType : DWORD + reserved : DWORD + end + + fun SymInitializeW(hProcess : HANDLE, userSearchPath : CHAR*, fInvadeProcess : BOOL) : BOOL + fun SymCleanup(hProcess : HANDLE) : BOOL + fun SymGetOptions : DWORD + fun SymSetOptions(symOptions : DWORD) : DWORD + fun SymFromAddrW(hProcess : HANDLE, address : DWORD64, displacement : DWORD64*, symbol : SYMBOL_INFOW*) : BOOL + fun SymGetLineFromAddrW64(hProcess : HANDLE, dwAddr : DWORD64, pdwDisplacement : DWORD*, line : IMAGEHLP_LINEW64*) : BOOL + fun SymGetModuleInfoW64(hProcess : HANDLE, qwAddr : DWORD64, moduleInfo : IMAGEHLP_MODULEW64*) : BOOL + + # fun SymFunctionTableAccess64(hProcess : HANDLE, addrBase : DWORD64) : Void* + fun SymGetModuleBase64(hProcess : HANDLE, qwAddr : DWORD64) : DWORD64 + + enum ADDRESS_MODE + AddrMode1616 + AddrMode1632 + AddrModeReal + AddrModeFlat + end + + struct ADDRESS64 + offset : DWORD64 + segment : WORD + mode : ADDRESS_MODE + end + + struct KDHELP64 + thread : DWORD64 + thCallbackStack : DWORD + thCallbackBStore : DWORD + nextCallback : DWORD + framePointer : DWORD + kiCallUserMode : DWORD64 + keUserCallbackDispatcher : DWORD64 + systemRangeStart : DWORD64 + kiUserExceptionDispatcher : DWORD64 + stackBase : DWORD64 + stackLimit : DWORD64 + buildVersion : DWORD + retpolineStubFunctionTableSize : DWORD + retpolineStubFunctionTable : DWORD64 + retpolineStubOffset : DWORD + retpolineStubSize : DWORD + reserved0 : DWORD64[2] + end + + struct STACKFRAME64 + addrPC : ADDRESS64 + addrReturn : ADDRESS64 + addrFrame : ADDRESS64 + addrStack : ADDRESS64 + addrBStore : ADDRESS64 + funcTableEntry : Void* + params : DWORD64[4] + far : BOOL + virtual : BOOL + reserved : DWORD64[3] + kdHelp : KDHELP64 + end + + IMAGE_FILE_MACHINE_I386 = DWORD.new!(0x014C) + IMAGE_FILE_MACHINE_IA64 = DWORD.new!(0x0200) + IMAGE_FILE_MACHINE_AMD64 = DWORD.new!(0x8664) + + alias PREAD_PROCESS_MEMORY_ROUTINE64 = HANDLE, DWORD64, Void*, DWORD, DWORD* -> BOOL + alias PFUNCTION_TABLE_ACCESS_ROUTINE64 = HANDLE, DWORD64 -> Void* + alias PGET_MODULE_BASE_ROUTINE64 = HANDLE, DWORD64 -> DWORD64 + alias PTRANSLATE_ADDRESS_ROUTINE64 = HANDLE, HANDLE, ADDRESS64* -> DWORD64 + + fun StackWalk64( + machineType : DWORD, hProcess : HANDLE, hThread : HANDLE, stackFrame : STACKFRAME64*, contextRecord : Void*, + readMemoryRoutine : PREAD_PROCESS_MEMORY_ROUTINE64, functionTableAccessRoutine : PFUNCTION_TABLE_ACCESS_ROUTINE64, + getModuleBaseRoutine : PGET_MODULE_BASE_ROUTINE64, translateAddress : PTRANSLATE_ADDRESS_ROUTINE64 + ) : BOOL +end diff --git a/src/lib_c/x86_64-windows-msvc/c/int_safe.cr b/src/lib_c/x86_64-windows-msvc/c/int_safe.cr index c415f981e943..0b9ea19eacd9 100644 --- a/src/lib_c/x86_64-windows-msvc/c/int_safe.cr +++ b/src/lib_c/x86_64-windows-msvc/c/int_safe.cr @@ -1,3 +1,4 @@ lib LibC alias DWORD = UInt32 + alias DWORD64 = UInt64 end diff --git a/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr b/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr index 5da638141581..ca76d7ca36cf 100644 --- a/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr +++ b/src/lib_c/x86_64-windows-msvc/c/processthreadsapi.cr @@ -32,6 +32,7 @@ lib LibC hStdError : HANDLE end + fun GetCurrentThread : HANDLE fun GetCurrentThreadStackLimits(lowLimit : ULONG_PTR*, highLimit : ULONG_PTR*) : Void fun GetCurrentProcess : HANDLE fun GetCurrentProcessId : DWORD diff --git a/src/lib_c/x86_64-windows-msvc/c/winnt.cr b/src/lib_c/x86_64-windows-msvc/c/winnt.cr index c20d77aba883..e910a217e1bd 100644 --- a/src/lib_c/x86_64-windows-msvc/c/winnt.cr +++ b/src/lib_c/x86_64-windows-msvc/c/winnt.cr @@ -78,4 +78,79 @@ lib LibC # (STANDARD_RIGHTS_WRITE | KEY_SET_VALUE | KEY_CREATE_SUB_KEY) & ~SYNCHRONIZE WRITE = 0x20006 end + + struct CONTEXT + p1Home : DWORD64 + p2Home : DWORD64 + p3Home : DWORD64 + p4Home : DWORD64 + p5Home : DWORD64 + p6Home : DWORD64 + contextFlags : DWORD + mxCsr : DWORD + segCs : WORD + segDs : WORD + segEs : WORD + segFs : WORD + segGs : WORD + segSs : WORD + eFlags : DWORD + dr0 : DWORD64 + dr1 : DWORD64 + dr2 : DWORD64 + dr3 : DWORD64 + dr6 : DWORD64 + dr7 : DWORD64 + rax : DWORD64 + rcx : DWORD64 + rdx : DWORD64 + rbx : DWORD64 + rsp : DWORD64 + rbp : DWORD64 + rsi : DWORD64 + rdi : DWORD64 + r8 : DWORD64 + r9 : DWORD64 + r10 : DWORD64 + r11 : DWORD64 + r12 : DWORD64 + r13 : DWORD64 + r14 : DWORD64 + r15 : DWORD64 + rip : DWORD64 + fltSave : UInt8[512] # DUMMYUNIONNAME + vectorRegister : UInt8[16][26] # M128A[26] + vectorControl : DWORD64 + debugControl : DWORD64 + lastBranchToRip : DWORD64 + lastBranchFromRip : DWORD64 + lastExceptionToRip : DWORD64 + lastExceptionFromRip : DWORD64 + end + + {% if flag?(:x86_64) %} + CONTEXT_AMD64 = DWORD.new!(0x00100000) + + CONTEXT_CONTROL = CONTEXT_AMD64 | 0x00000001 + CONTEXT_INTEGER = CONTEXT_AMD64 | 0x00000002 + CONTEXT_SEGMENTS = CONTEXT_AMD64 | 0x00000004 + CONTEXT_FLOATING_POINT = CONTEXT_AMD64 | 0x00000008 + CONTEXT_DEBUG_REGISTERS = CONTEXT_AMD64 | 0x00000010 + + CONTEXT_FULL = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_FLOATING_POINT + {% elsif flag?(:i386) %} + CONTEXT_i386 = DWORD.new!(0x00010000i64) + CONTEXT_i486 = DWORD.new!(0x00010000i64) + + CONTEXT_CONTROL = CONTEXT_i386 | 0x00000001 + CONTEXT_INTEGER = CONTEXT_i386 | 0x00000002 + CONTEXT_SEGMENTS = CONTEXT_i386 | 0x00000004 + CONTEXT_FLOATING_POINT = CONTEXT_i386 | 0x00000008 + CONTEXT_DEBUG_REGISTERS = CONTEXT_i386 | 0x00000010 + CONTEXT_EXTENDED_REGISTERS = CONTEXT_i386 | 0x00000020 + + CONTEXT_FULL = CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS + {% end %} + + fun RtlCaptureContext(contextRecord : CONTEXT*) end diff --git a/src/raise.cr b/src/raise.cr index b017e6a0b981..f5a23b23e8aa 100644 --- a/src/raise.cr +++ b/src/raise.cr @@ -111,6 +111,7 @@ end exception.inspect_with_backtrace(STDERR) {% end %} + exception.callstack ||= Exception::CallStack.new LibC._CxxThrowException(pointerof(exception).as(Void*), throw_info) end diff --git a/src/windows_stubs.cr b/src/windows_stubs.cr index 01783ab9e71b..47e465726f1a 100644 --- a/src/windows_stubs.cr +++ b/src/windows_stubs.cr @@ -1,11 +1,5 @@ require "c/synchapi" -struct Exception::CallStack - def self.skip(*args) - # do nothing - end -end - abstract class IO private class Encoder def initialize(@encoding_options : EncodingOptions) From 449ec9cced5020cb3d80dc10fdc6f03a1d539d69 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Sat, 13 Nov 2021 23:39:09 +0800 Subject: [PATCH 3/7] refactor and unify win32/unix call stack code --- src/exception/call_stack.cr | 66 +++++++++++++++++++++++++-- src/exception/call_stack/dwarf.cr | 11 +++-- src/exception/call_stack/elf.cr | 2 +- src/exception/call_stack/libunwind.cr | 53 --------------------- src/exception/call_stack/mach_o.cr | 2 +- src/exception/call_stack/stackwalk.cr | 57 ++--------------------- src/kernel.cr | 21 +++------ 7 files changed, 80 insertions(+), 132 deletions(-) diff --git a/src/exception/call_stack.cr b/src/exception/call_stack.cr index 46cf985a9fa5..035151ef8f07 100644 --- a/src/exception/call_stack.cr +++ b/src/exception/call_stack.cr @@ -14,11 +14,7 @@ end struct Exception::CallStack # Compute current directory at the beginning so filenames # are always shown relative to the *starting* working directory. - CURRENT_DIR = begin - if dir = Process::INITIAL_PWD - Path[dir] - end - end + private CURRENT_DIR = Process::INITIAL_PWD.try { |dir| Path[dir] } @@skip = [] of String @@ -38,4 +34,64 @@ struct Exception::CallStack def printable_backtrace : Array(String) @backtrace ||= decode_backtrace end + + private def decode_backtrace + show_full_info = ENV["CRYSTAL_CALLSTACK_FULL_INFO"]? == "1" + + @callstack.compact_map do |ip| + pc = CallStack.decode_address(ip) + + file, line_number, column_number = CallStack.decode_line_number(pc) + + if file && file != "??" + next if @@skip.includes?(file) + + # Turn to relative to the current dir, if possible + if current_dir = CURRENT_DIR + if rel = Path[file].relative_to?(current_dir) + rel = rel.to_s + file = rel unless rel.starts_with?("..") + end + end + + file_line_column = file + unless line_number == 0 + file_line_column = "#{file_line_column}:#{line_number}" + file_line_column = "#{file_line_column}:#{column_number}" unless column_number == 0 + end + end + + if name = CallStack.decode_function_name(pc) + function = name + elsif frame = CallStack.decode_frame(ip) + _, function, file = frame + # Crystal methods (their mangled name) start with `*`, so + # we remove that to have less clutter in the output. + function = function.lchop('*') + else + function = "??" + end + + if file_line_column + if show_full_info && (frame = CallStack.decode_frame(ip)) + _, sname, _ = frame + line = "#{file_line_column} in '#{sname}'" + else + line = "#{file_line_column} in '#{function}'" + end + else + if file == "??" && function == "??" + line = "???" + else + line = "#{file} in '#{function}'" + end + end + + if show_full_info + line = "#{line} at 0x#{ip.address.to_s(16)}" + end + + line + end + end end diff --git a/src/exception/call_stack/dwarf.cr b/src/exception/call_stack/dwarf.cr index bb5f124c0256..bf5a91c687e8 100644 --- a/src/exception/call_stack/dwarf.cr +++ b/src/exception/call_stack/dwarf.cr @@ -11,12 +11,13 @@ struct Exception::CallStack @@dwarf_function_names : Array(Tuple(LibC::SizeT, LibC::SizeT, String))? # :nodoc: - def self.load_dwarf + def self.load_debug_info + return if ENV["CRYSTAL_LOAD_DEBUG_INFO"]? == "0" + unless @@dwarf_loaded @@dwarf_loaded = true begin - return if ENV["CRYSTAL_LOAD_DWARF"]? == "0" - load_dwarf_impl + load_debug_info_impl rescue ex @@dwarf_line_numbers = nil @@dwarf_function_names = nil @@ -26,7 +27,7 @@ struct Exception::CallStack end protected def self.decode_line_number(pc) - load_dwarf + load_debug_info if ln = @@dwarf_line_numbers if row = ln.find(pc) return {row.path, row.line, row.column} @@ -36,7 +37,7 @@ struct Exception::CallStack end protected def self.decode_function_name(pc) - load_dwarf + load_debug_info if fn = @@dwarf_function_names fn.each do |(low_pc, high_pc, function_name)| return function_name if low_pc <= pc <= high_pc diff --git a/src/exception/call_stack/elf.cr b/src/exception/call_stack/elf.cr index 755a6a4ba93a..8c7bb45b3219 100644 --- a/src/exception/call_stack/elf.cr +++ b/src/exception/call_stack/elf.cr @@ -2,7 +2,7 @@ require "crystal/elf" require "c/link" struct Exception::CallStack - protected def self.load_dwarf_impl + protected def self.load_debug_info_impl phdr_callback = LibC::DlPhdrCallback.new do |info, size, data| # The first entry is the header for the current program read_dwarf_sections(info.value.addr) diff --git a/src/exception/call_stack/libunwind.cr b/src/exception/call_stack/libunwind.cr index 96369dfbe665..b05ddafbcf6b 100644 --- a/src/exception/call_stack/libunwind.cr +++ b/src/exception/call_stack/libunwind.cr @@ -129,59 +129,6 @@ struct Exception::CallStack end end - private def decode_backtrace - show_full_info = ENV["CRYSTAL_CALLSTACK_FULL_INFO"]? == "1" - - @callstack.compact_map do |ip| - pc = CallStack.decode_address(ip) - - file, line, column = CallStack.decode_line_number(pc) - - if file && file != "??" - next if @@skip.includes?(file) - - # Turn to relative to the current dir, if possible - if current_dir = CURRENT_DIR - file = file.lchop(current_dir) - end - - file_line_column = "#{file}:#{line}:#{column}" - end - - if name = CallStack.decode_function_name(pc) - function = name - elsif frame = CallStack.decode_frame(ip) - _, function, file = frame - # Crystal methods (their mangled name) start with `*`, so - # we remove that to have less clutter in the output. - function = function.lchop('*') - else - function = "??" - end - - if file_line_column - if show_full_info && (frame = CallStack.decode_frame(ip)) - _, sname, _ = frame - line = "#{file_line_column} in '#{sname}'" - else - line = "#{file_line_column} in '#{function}'" - end - else - if file == "??" && function == "??" - line = "???" - else - line = "#{file} in '#{function}'" - end - end - - if show_full_info - line = "#{line} at 0x#{ip.address.to_s(16)}" - end - - line - end - end - protected def self.decode_frame(ip, original_ip = ip) if LibC.dladdr(ip, out info) != 0 offset = original_ip - info.dli_saddr diff --git a/src/exception/call_stack/mach_o.cr b/src/exception/call_stack/mach_o.cr index a4cd742d74af..7f6bbc0d3756 100644 --- a/src/exception/call_stack/mach_o.cr +++ b/src/exception/call_stack/mach_o.cr @@ -9,7 +9,7 @@ end struct Exception::CallStack @@image_slide : LibC::Long? - protected def self.load_dwarf_impl + protected def self.load_debug_info_impl read_dwarf_sections end diff --git a/src/exception/call_stack/stackwalk.cr b/src/exception/call_stack/stackwalk.cr index 7e3347a117ea..2a7445cd079d 100644 --- a/src/exception/call_stack/stackwalk.cr +++ b/src/exception/call_stack/stackwalk.cr @@ -6,7 +6,6 @@ struct Exception::CallStack @@sym_loaded = false - # :nodoc: def self.load_debug_info return if ENV["CRYSTAL_LOAD_DEBUG_INFO"]? == "0" @@ -21,6 +20,9 @@ struct Exception::CallStack end private def self.load_debug_info_impl + # TODO: figure out if and when to call SymCleanup (it cannot be done in + # `at_exit` because unhandled exceptions in `main_user_code` are printed + # after those handlers) if LibC.SymInitializeW(LibC.GetCurrentProcess, nil, 1) == 0 raise RuntimeError.from_errno("SymInitializeW") end @@ -75,57 +77,6 @@ struct Exception::CallStack stack end - private def decode_backtrace - show_full_info = ENV["CRYSTAL_CALLSTACK_FULL_INFO"]? == "1" - - @callstack.compact_map do |ip| - pc = decode_address(ip) - - file, line_number, _ = CallStack.decode_line_number(pc) - - if file && file != "??" - next if @@skip.includes?(file) - - # Turn to relative to the current dir, if possible - if current_dir = CURRENT_DIR - if rel = Path[file].relative_to?(current_dir) - rel = rel.to_s - file = rel unless rel.starts_with?("..") - end - end - - file_line = "#{file}:#{line_number}" unless line_number == "??" - end - - if name = CallStack.decode_function_name(pc) - function = name - elsif frame = CallStack.decode_frame(ip) - _, function, file = frame - # Crystal methods (their mangled name) start with `*`, so - # we remove that to have less clutter in the output. - function = function.lchop('*') - else - function = "??" - end - - if file_line - line = "#{file_line} in '#{function}'" - else - if file == "??" && function == "??" - line = "???" - else - line = "#{file} in '#{function}'" - end - end - - if show_full_info - line = "#{line} at 0x#{ip.address.to_s(16)}" - end - - line - end - end - protected def self.decode_line_number(pc) load_debug_info @@ -173,7 +124,7 @@ struct Exception::CallStack protected def self.decode_frame(pc) end - private def decode_address(ip) + protected def self.decode_address(ip) ip.address end end diff --git a/src/kernel.cr b/src/kernel.cr index 39140ec59c98..c698fd6d4dff 100644 --- a/src/kernel.cr +++ b/src/kernel.cr @@ -536,20 +536,13 @@ end Signal.setup_segfault_handler {% end %} -{% if flag?(:win32) %} - Exception::CallStack.load_debug_info if ENV["CRYSTAL_LOAD_DEBUG_INFO"]? == "1" - # TODO: figure out when to call SymCleanup (it cannot be done in `at_exit` - # because unhandled exceptions in `main_user_code` are printed after those - # handlers) -{% else %} - # load dwarf on start up of the program is executed with CRYSTAL_LOAD_DWARF=1 - # this will make dwarf available on print_frame that is used by Crystal's segfault handler - # - # - CRYSTAL_LOAD_DWARF=0 will never use dwarf information (See Exception::CallStack.load_dwarf) - # - CRYSTAL_LOAD_DWARF=1 will load dwarf on startup - # - Other values will load dwarf on demand: when the backtrace of the first exception is generated - Exception::CallStack.load_dwarf if ENV["CRYSTAL_LOAD_DWARF"]? == "1" -{% end %} +# load debug info on start up of the program is executed with CRYSTAL_LOAD_DEBUG_INFO=1 +# this will make debug info available on print_frame that is used by Crystal's segfault handler +# +# - CRYSTAL_LOAD_DEBUG_INFO=0 will never use debug info (See Exception::CallStack.load_debug_info) +# - CRYSTAL_LOAD_DEBUG_INFO=1 will load debug info on startup +# - Other values will load debug info on demand: when the backtrace of the first exception is generated +Exception::CallStack.load_debug_info if ENV["CRYSTAL_LOAD_DEBUG_INFO"]? == "1" {% if flag?(:preview_mt) %} Crystal::Scheduler.init_workers From ed4f51ea5d430153f68abcd20db8eef7edaf0170 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 17 Nov 2021 04:40:13 +0800 Subject: [PATCH 4/7] enable specs --- .github/workflows/win.yml | 2 +- spec/std/exception/call_stack_spec.cr | 15 ++++++++++----- spec/std/raise_spec.cr | 4 ++-- src/compiler/crystal/codegen/codegen.cr | 2 ++ 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 69dfb1545d46..11aec109258e 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -172,7 +172,7 @@ jobs: cl /MT /c src\llvm\ext\llvm_ext.cc -I llvm\include /Fosrc\llvm\ext\llvm_ext.obj - name: Link Crystal executable run: | - Invoke-Expression "cl crystal.obj /Fecrystal-cross src\llvm\ext\llvm_ext.obj $(llvm\bin\llvm-config.exe --libs) libs\pcre.lib libs\gc.lib WS2_32.lib advapi32.lib libcmt.lib legacy_stdio_definitions.lib /F10000000" + Invoke-Expression "cl crystal.obj /Fecrystal-cross src\llvm\ext\llvm_ext.obj $(llvm\bin\llvm-config.exe --libs) libs\pcre.lib libs\gc.lib WS2_32.lib advapi32.lib libcmt.lib dbghelp.lib legacy_stdio_definitions.lib /F10000000" - name: Re-build Crystal run: | diff --git a/spec/std/exception/call_stack_spec.cr b/spec/std/exception/call_stack_spec.cr index bb9318d442ae..a414129288a7 100644 --- a/spec/std/exception/call_stack_spec.cr +++ b/spec/std/exception/call_stack_spec.cr @@ -1,7 +1,7 @@ require "../spec_helper" describe "Backtrace" do - pending_win32 "prints file line:column" do + it "prints file line:column" do source_file = datapath("backtrace_sample") # CallStack tries to make files relative to the current dir, @@ -13,8 +13,13 @@ describe "Backtrace" do _, output, _ = compile_and_run_file(source_file) # resolved file line:column - output.should match(/^#{source_file}:3:10 in 'callee1'/m) - output.should match(/^#{source_file}:13:5 in 'callee3'/m) + {% if flag?(:win32) %} + output.should match(/^#{Regex.escape(source_file)}:3 in 'callee1'/m) + output.should match(/^#{Regex.escape(source_file)}:13 in 'callee3'/m) + {% else %} + output.should match(/^#{Regex.escape(source_file)}:3:10 in 'callee1'/m) + output.should match(/^#{Regex.escape(source_file)}:13:5 in 'callee3'/m) + {% end %} # skipped internal details output.should_not contain("src/callstack.cr") @@ -22,7 +27,7 @@ describe "Backtrace" do output.should_not contain("src/raise.cr") end - pending_win32 "doesn't relativize paths outside of current dir (#10169)" do + it "doesn't relativize paths outside of current dir (#10169)" do with_tempfile("source_file") do |source_file| source_path = Path.new(source_file) source_path.absolute?.should be_true @@ -36,7 +41,7 @@ describe "Backtrace" do EOF _, output, _ = compile_and_run_file(source_file) - output.should match /\A(#{source_path}):/ + output.should match /\A(#{Regex.escape(source_path.to_s)}):/ end end diff --git a/spec/std/raise_spec.cr b/spec/std/raise_spec.cr index 0d80aca5a1df..026741802df9 100644 --- a/spec/std/raise_spec.cr +++ b/spec/std/raise_spec.cr @@ -3,14 +3,14 @@ require "./spec_helper" describe "raise" do callstack_on_rescue = nil - pending_win32 "should set exception's callstack" do + it "should set exception's callstack" do exception = expect_raises Exception, "without callstack" do raise "without callstack" end exception.callstack.should_not be_nil end - pending_win32 "shouldn't overwrite the callstack on re-raise" do + it "shouldn't overwrite the callstack on re-raise" do exception_after_reraise = expect_raises Exception, "exception to be rescued" do begin raise "exception to be rescued" diff --git a/src/compiler/crystal/codegen/codegen.cr b/src/compiler/crystal/codegen/codegen.cr index 1efa7d170f5e..c4b0d6959bf5 100644 --- a/src/compiler/crystal/codegen/codegen.cr +++ b/src/compiler/crystal/codegen/codegen.cr @@ -28,6 +28,8 @@ module Crystal def evaluate(node, debug = Debug::Default) llvm_mod = codegen(node, single_module: true, debug: debug)[""].mod + llvm_mod.target = target_machine.triple + main = llvm_mod.functions[MAIN_NAME] main_return_type = main.return_type From 5433b7dc7abf118ff4e24176188f6f47240bb623 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 17 Nov 2021 06:46:12 +0800 Subject: [PATCH 5/7] fixup --- spec/std/exception/call_stack_spec.cr | 3 ++- src/lib_c/x86_64-windows-msvc/c/dbghelp.cr | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/std/exception/call_stack_spec.cr b/spec/std/exception/call_stack_spec.cr index a414129288a7..b0a33e5a467c 100644 --- a/spec/std/exception/call_stack_spec.cr +++ b/spec/std/exception/call_stack_spec.cr @@ -12,7 +12,8 @@ describe "Backtrace" do _, output, _ = compile_and_run_file(source_file) - # resolved file line:column + # resolved file:line:column (no column for windows PDB because of poor + # support in general) {% if flag?(:win32) %} output.should match(/^#{Regex.escape(source_file)}:3 in 'callee1'/m) output.should match(/^#{Regex.escape(source_file)}:13 in 'callee3'/m) diff --git a/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr b/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr index 9a9e2fd15ec9..7a491aa10777 100644 --- a/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr +++ b/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr @@ -62,7 +62,7 @@ lib LibC reserved : DWORD end - fun SymInitializeW(hProcess : HANDLE, userSearchPath : CHAR*, fInvadeProcess : BOOL) : BOOL + fun SymInitializeW(hProcess : HANDLE, userSearchPath : LPWSTR, fInvadeProcess : BOOL) : BOOL fun SymCleanup(hProcess : HANDLE) : BOOL fun SymGetOptions : DWORD fun SymSetOptions(symOptions : DWORD) : DWORD From f49a320a25fb9524a0028e19b0c52a2e948c5758 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 17 Nov 2021 22:05:16 +0800 Subject: [PATCH 6/7] fixup --- src/exception/call_stack/stackwalk.cr | 28 +++++++++++----------- src/lib_c/x86_64-windows-msvc/c/dbghelp.cr | 2 -- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/exception/call_stack/stackwalk.cr b/src/exception/call_stack/stackwalk.cr index 2a7445cd079d..0dfb8f237aa9 100644 --- a/src/exception/call_stack/stackwalk.cr +++ b/src/exception/call_stack/stackwalk.cr @@ -32,6 +32,15 @@ struct Exception::CallStack def self.unwind load_debug_info + machine_type = {% if flag?(:x86_64) %} + LibC::IMAGE_FILE_MACHINE_AMD64 + {% elsif flag?(:i386) %} + # TODO: use WOW64_CONTEXT in place of CONTEXT + {% raise "x86 not supported" %} + {% else %} + {% raise "architecture not supported" %} + {% end %} + # TODO: use stack if possible (must be 16-byte aligned) context = Pointer(LibC::CONTEXT).malloc(1) context.value.contextFlags = LibC::CONTEXT_FULL @@ -42,19 +51,9 @@ struct Exception::CallStack stack_frame.addrFrame.mode = LibC::ADDRESS_MODE::AddrModeFlat stack_frame.addrStack.mode = LibC::ADDRESS_MODE::AddrModeFlat - machine_type = LibC::IMAGE_FILE_MACHINE_AMD64 - {% if flag?(:x86_64) %} - stack_frame.addrPC.offset = context.value.rip - stack_frame.addrFrame.offset = context.value.rbp - stack_frame.addrStack.offset = context.value.rsp - {% elsif flag?(:i386) %} - machine_type = LibC::IMAGE_FILE_MACHINE_I386 - stack_frame.addrPC.offset = context.value.eip - stack_frame.addrFrame.offset = context.value.ebp - stack_frame.addrStack.offset = context.value.esp - {% else %} - {% raise "architecture not supported" %} - {% end %} + stack_frame.addrPC.offset = context.value.rip + stack_frame.addrFrame.offset = context.value.rbp + stack_frame.addrStack.offset = context.value.rsp stack = [] of Void* @@ -97,7 +96,8 @@ struct Exception::CallStack if LibC.SymGetModuleInfoW64(LibC.GetCurrentProcess, pc, module_info) != 0 mod_displacement = pc - LibC.SymGetModuleBase64(LibC.GetCurrentProcess, pc) - file_name = "#{String.from_utf16(module_info.value.loadedImageName.to_unsafe)[0]}+0x#{mod_displacement.to_s(16)}" + image_name = String.from_utf16(module_info.value.loadedImageName.to_unsafe)[0] + file_name = "#{image_name} +#{mod_displacement}" else file_name = "??" end diff --git a/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr b/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr index 7a491aa10777..cea35511eaea 100644 --- a/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr +++ b/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr @@ -120,8 +120,6 @@ lib LibC kdHelp : KDHELP64 end - IMAGE_FILE_MACHINE_I386 = DWORD.new!(0x014C) - IMAGE_FILE_MACHINE_IA64 = DWORD.new!(0x0200) IMAGE_FILE_MACHINE_AMD64 = DWORD.new!(0x8664) alias PREAD_PROCESS_MEMORY_ROUTINE64 = HANDLE, DWORD64, Void*, DWORD, DWORD* -> BOOL From bf1c95603797d35a356dd2cc117882a48ca6ae2c Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Mon, 22 Nov 2021 06:01:50 +0800 Subject: [PATCH 7/7] fixup --- src/compiler/crystal/compiler.cr | 2 +- src/exception/call_stack/stackwalk.cr | 10 ++++------ src/lib_c/x86_64-windows-msvc/c/dbghelp.cr | 1 + 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/compiler/crystal/compiler.cr b/src/compiler/crystal/compiler.cr index 8ebf609e79d0..3cd21fae2a84 100644 --- a/src/compiler/crystal/compiler.cr +++ b/src/compiler/crystal/compiler.cr @@ -346,7 +346,7 @@ module Crystal object_arg = Process.quote_windows(object_names) output_arg = Process.quote_windows("/Fe#{output_filename}") - args = %(/nologo #{object_arg} #{output_arg} /link #{lib_flags} #{@link_flags} #{"/DEBUG:FULL" unless debug.none?}).gsub("\n", " ") + args = %(/nologo #{object_arg} #{output_arg} /link#{" /DEBUG:FULL" unless debug.none?} #{lib_flags} #{@link_flags}).gsub("\n", " ") cmd = "#{CL} #{args}" if cmd.to_utf16.size > 32000 diff --git a/src/exception/call_stack/stackwalk.cr b/src/exception/call_stack/stackwalk.cr index 0dfb8f237aa9..fb3ae4744233 100644 --- a/src/exception/call_stack/stackwalk.cr +++ b/src/exception/call_stack/stackwalk.cr @@ -24,9 +24,9 @@ struct Exception::CallStack # `at_exit` because unhandled exceptions in `main_user_code` are printed # after those handlers) if LibC.SymInitializeW(LibC.GetCurrentProcess, nil, 1) == 0 - raise RuntimeError.from_errno("SymInitializeW") + raise RuntimeError.from_winerror("SymInitializeW") end - LibC.SymSetOptions(LibC.SymGetOptions | LibC::SYMOPT_LOAD_LINES | LibC::SYMOPT_FAIL_CRITICAL_ERRORS | LibC::SYMOPT_NO_PROMPTS) + LibC.SymSetOptions(LibC.SymGetOptions | LibC::SYMOPT_UNDNAME | LibC::SYMOPT_LOAD_LINES | LibC::SYMOPT_FAIL_CRITICAL_ERRORS | LibC::SYMOPT_NO_PROMPTS) end def self.unwind @@ -82,8 +82,7 @@ struct Exception::CallStack line_info = uninitialized LibC::IMAGEHLP_LINEW64 line_info.sizeOfStruct = sizeof(LibC::IMAGEHLP_LINEW64) - displacement = uninitialized LibC::DWORD - if LibC.SymGetLineFromAddrW64(LibC.GetCurrentProcess, pc, pointerof(displacement), pointerof(line_info)) != 0 + if LibC.SymGetLineFromAddrW64(LibC.GetCurrentProcess, pc, out displacement, pointerof(line_info)) != 0 file_name = String.from_utf16(line_info.fileName)[0] line_number = line_info.lineNumber else @@ -116,8 +115,7 @@ struct Exception::CallStack sym_displacement = LibC::DWORD64.zero if LibC.SymFromAddrW(LibC.GetCurrentProcess, pc, pointerof(sym_displacement), symbol) != 0 - String.from_utf16(symbol.value.name.to_unsafe)[0] - # UnDecorateSymbolNameW + String.from_utf16(symbol.value.name.to_unsafe.to_slice(symbol.value.nameLen)) end end diff --git a/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr b/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr index cea35511eaea..af37cb0c7f0c 100644 --- a/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr +++ b/src/lib_c/x86_64-windows-msvc/c/dbghelp.cr @@ -2,6 +2,7 @@ lib LibC MAX_SYM_NAME = 2000 + SYMOPT_UNDNAME = 0x00000002 SYMOPT_LOAD_LINES = 0x00000010 SYMOPT_FAIL_CRITICAL_ERRORS = 0x00000200 SYMOPT_NO_PROMPTS = 0x00080000