Skip to content

Commit

Permalink
Support call stacks for MinGW-w64 builds (#15117)
Browse files Browse the repository at this point in the history
Introduces new methods for extracting COFF debug information from programs in the PE format, integrating them into Crystal's existing DWARF parsing functionality. Resolves part of #6170.

It is questionable whether reusing `src/exception/call_stack/elf.cr` for MinGW-w64 is appropriate, since nothing here is in the ELF format, but this PR tries to avoid moving existing code around, save for the old `Exception::CallStack.setup_crash_handler` as it remains the only common portion between MSVC and MinGW-w64.
  • Loading branch information
HertzDevil authored Oct 24, 2024
1 parent 94386b6 commit 454744a
Show file tree
Hide file tree
Showing 11 changed files with 379 additions and 109 deletions.
6 changes: 3 additions & 3 deletions spec/std/exception/call_stack_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ describe "Backtrace" do

_, output, _ = compile_and_run_file(source_file)

# resolved file:line:column (no column for windows PDB because of poor
# support in general)
{% if flag?(:win32) %}
# resolved file:line:column (no column for MSVC PDB because of poor support
# by external tooling in general)
{% if flag?(:msvc) %}
output.should match(/^#{Regex.escape(source_file)}:3 in 'callee1'/m)
output.should match(/^#{Regex.escape(source_file)}:13 in 'callee3'/m)
{% else %}
Expand Down
110 changes: 110 additions & 0 deletions src/crystal/pe.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
module Crystal
# :nodoc:
#
# Portable Executable reader.
#
# Documentation:
# - <https://learn.microsoft.com/en-us/windows/win32/debug/pe-format>
struct PE
class Error < Exception
end

record SectionHeader, name : String, virtual_offset : UInt32, offset : UInt32, size : UInt32

record COFFSymbol, offset : UInt32, name : String

# addresses in COFF debug info are relative to this image base; used by
# `Exception::CallStack.read_dwarf_sections` to calculate the real relocated
# addresses
getter original_image_base : UInt64

@section_headers : Slice(SectionHeader)
@string_table_base : UInt32

# mapping from zero-based section index to list of symbols sorted by
# offsets within that section
getter coff_symbols = Hash(Int32, Array(COFFSymbol)).new

def self.open(path : String | ::Path, &)
File.open(path, "r") do |file|
yield new(file)
end
end

def initialize(@io : IO::FileDescriptor)
dos_header = uninitialized LibC::IMAGE_DOS_HEADER
io.read_fully(pointerof(dos_header).to_slice(1).to_unsafe_bytes)
raise Error.new("Invalid DOS header") unless dos_header.e_magic == 0x5A4D # MZ

io.seek(dos_header.e_lfanew)
nt_header = uninitialized LibC::IMAGE_NT_HEADERS
io.read_fully(pointerof(nt_header).to_slice(1).to_unsafe_bytes)
raise Error.new("Invalid PE header") unless nt_header.signature == 0x00004550 # PE\0\0

@original_image_base = nt_header.optionalHeader.imageBase
@string_table_base = nt_header.fileHeader.pointerToSymbolTable + nt_header.fileHeader.numberOfSymbols * sizeof(LibC::IMAGE_SYMBOL)

section_count = nt_header.fileHeader.numberOfSections
nt_section_headers = Pointer(LibC::IMAGE_SECTION_HEADER).malloc(section_count).to_slice(section_count)
io.read_fully(nt_section_headers.to_unsafe_bytes)

@section_headers = nt_section_headers.map do |nt_header|
if nt_header.name[0] === '/'
# section name is longer than 8 bytes; look up the COFF string table
name_buf = nt_header.name.to_slice + 1
string_offset = String.new(name_buf.to_unsafe, name_buf.index(0) || name_buf.size).to_i
io.seek(@string_table_base + string_offset)
name = io.gets('\0', chomp: true).not_nil!
else
name = String.new(nt_header.name.to_unsafe, nt_header.name.index(0) || nt_header.name.size)
end

SectionHeader.new(name: name, virtual_offset: nt_header.virtualAddress, offset: nt_header.pointerToRawData, size: nt_header.virtualSize)
end

io.seek(nt_header.fileHeader.pointerToSymbolTable)
image_symbol_count = nt_header.fileHeader.numberOfSymbols
image_symbols = Pointer(LibC::IMAGE_SYMBOL).malloc(image_symbol_count).to_slice(image_symbol_count)
io.read_fully(image_symbols.to_unsafe_bytes)

aux_count = 0
image_symbols.each_with_index do |sym, i|
if aux_count == 0
aux_count = sym.numberOfAuxSymbols.to_i
else
aux_count &-= 1
end

next unless aux_count == 0
next unless sym.type.bits_set?(0x20) # COFF function
next unless sym.sectionNumber > 0 # one-based section index
next unless sym.storageClass.in?(LibC::IMAGE_SYM_CLASS_EXTERNAL, LibC::IMAGE_SYM_CLASS_STATIC)

if sym.n.name.short == 0
io.seek(@string_table_base + sym.n.name.long)
name = io.gets('\0', chomp: true).not_nil!
else
name = String.new(sym.n.shortName.to_slice).rstrip('\0')
end

# `@coff_symbols` uses zero-based indices
section_coff_symbols = @coff_symbols.put_if_absent(sym.sectionNumber.to_i &- 1) { [] of COFFSymbol }
section_coff_symbols << COFFSymbol.new(sym.value, name)
end

# add one sentinel symbol to ensure binary search on the offsets works
@coff_symbols.each_with_index do |(_, symbols), i|
symbols.sort_by!(&.offset)
symbols << COFFSymbol.new(@section_headers[i].size, "??")
end
end

def read_section?(name : String, &)
if sh = @section_headers.find(&.name.== name)
@io.seek(sh.offset) do
yield sh, @io
end
end
end
end
end
44 changes: 44 additions & 0 deletions src/crystal/system/win32/signal.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "c/signal"
require "c/malloc"

module Crystal::System::Signal
def self.trap(signal, handler) : Nil
Expand All @@ -16,4 +17,47 @@ module Crystal::System::Signal
def self.ignore(signal) : Nil
raise NotImplementedError.new("Crystal::System::Signal.ignore")
end

def self.setup_seh_handler
LibC.AddVectoredExceptionHandler(1, ->(exception_info) do
case exception_info.value.exceptionRecord.value.exceptionCode
when LibC::EXCEPTION_ACCESS_VIOLATION
addr = exception_info.value.exceptionRecord.value.exceptionInformation[1]
Crystal::System.print_error "Invalid memory access (C0000005) at address %p\n", Pointer(Void).new(addr)
{% if flag?(:gnu) %}
Exception::CallStack.print_backtrace
{% else %}
Exception::CallStack.print_backtrace(exception_info)
{% end %}
LibC._exit(1)
when LibC::EXCEPTION_STACK_OVERFLOW
LibC._resetstkoflw
Crystal::System.print_error "Stack overflow (e.g., infinite or very deep recursion)\n"
{% if flag?(:gnu) %}
Exception::CallStack.print_backtrace
{% else %}
Exception::CallStack.print_backtrace(exception_info)
{% end %}
LibC._exit(1)
else
LibC::EXCEPTION_CONTINUE_SEARCH
end
end)

# ensure that even in the case of stack overflow there is enough reserved
# stack space for recovery (for other threads this is done in
# `Crystal::System::Thread.thread_proc`)
stack_size = Crystal::System::Fiber::RESERVED_STACK_SIZE
LibC.SetThreadStackGuarantee(pointerof(stack_size))

# this catches invalid argument checks inside the C runtime library
LibC._set_invalid_parameter_handler(->(expression, _function, _file, _line, _pReserved) do
message = expression ? String.from_utf16(expression)[0] : "(no message)"
Crystal::System.print_error "CRT invalid parameter handler invoked: %s\n", message
caller.each do |frame|
Crystal::System.print_error " from %s\n", frame
end
LibC._exit(1)
end)
end
end
5 changes: 1 addition & 4 deletions src/exception/call_stack.cr
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
{% if flag?(:interpreted) %}
require "./call_stack/interpreter"
{% elsif flag?(:win32) %}
{% elsif flag?(:win32) && !flag?(:gnu) %}
require "./call_stack/stackwalk"
{% if flag?(:gnu) %}
require "./lib_unwind"
{% end %}
{% elsif flag?(:wasm32) %}
require "./call_stack/null"
{% else %}
Expand Down
4 changes: 4 additions & 0 deletions src/exception/call_stack/dwarf.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ struct Exception::CallStack
@@dwarf_line_numbers : Crystal::DWARF::LineNumbers?
@@dwarf_function_names : Array(Tuple(LibC::SizeT, LibC::SizeT, String))?

{% if flag?(:win32) %}
@@coff_symbols : Hash(Int32, Array(Crystal::PE::COFFSymbol))?
{% end %}

# :nodoc:
def self.load_debug_info : Nil
return if ENV["CRYSTAL_LOAD_DEBUG_INFO"]? == "0"
Expand Down
86 changes: 52 additions & 34 deletions src/exception/call_stack/elf.cr
Original file line number Diff line number Diff line change
@@ -1,65 +1,83 @@
require "crystal/elf"
{% unless flag?(:wasm32) %}
require "c/link"
{% if flag?(:win32) %}
require "crystal/pe"
{% else %}
require "crystal/elf"
{% unless flag?(:wasm32) %}
require "c/link"
{% end %}
{% end %}

struct Exception::CallStack
private struct DlPhdrData
getter program : String
property base_address : LibC::Elf_Addr = 0
{% unless flag?(:win32) %}
private struct DlPhdrData
getter program : String
property base_address : LibC::Elf_Addr = 0

def initialize(@program : String)
def initialize(@program : String)
end
end
end
{% end %}

protected def self.load_debug_info_impl : Nil
program = Process.executable_path
return unless program && File::Info.readable? program
data = DlPhdrData.new(program)

phdr_callback = LibC::DlPhdrCallback.new do |info, size, data|
# `dl_iterate_phdr` does not always visit the current program first; on
# Android the first object is `/system/bin/linker64`, the second is the
# full program path (not the empty string), so we check both here
name_c_str = info.value.name
if name_c_str && (name_c_str.value == 0 || LibC.strcmp(name_c_str, data.as(DlPhdrData*).value.program) == 0)
# The first entry is the header for the current program.
# Note that we avoid allocating here and just store the base address
# to be passed to self.read_dwarf_sections when dl_iterate_phdr returns.
# Calling self.read_dwarf_sections from this callback may lead to reallocations
# and deadlocks due to the internal lock held by dl_iterate_phdr (#10084).
data.as(DlPhdrData*).value.base_address = info.value.addr
1
else
0

{% if flag?(:win32) %}
if LibC.GetModuleHandleExW(LibC::GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, nil, out hmodule) != 0
self.read_dwarf_sections(program, hmodule.address)
end
end
{% else %}
data = DlPhdrData.new(program)

LibC.dl_iterate_phdr(phdr_callback, pointerof(data))
self.read_dwarf_sections(data.program, data.base_address)
phdr_callback = LibC::DlPhdrCallback.new do |info, size, data|
# `dl_iterate_phdr` does not always visit the current program first; on
# Android the first object is `/system/bin/linker64`, the second is the
# full program path (not the empty string), so we check both here
name_c_str = info.value.name
if name_c_str && (name_c_str.value == 0 || LibC.strcmp(name_c_str, data.as(DlPhdrData*).value.program) == 0)
# The first entry is the header for the current program.
# Note that we avoid allocating here and just store the base address
# to be passed to self.read_dwarf_sections when dl_iterate_phdr returns.
# Calling self.read_dwarf_sections from this callback may lead to reallocations
# and deadlocks due to the internal lock held by dl_iterate_phdr (#10084).
data.as(DlPhdrData*).value.base_address = info.value.addr
1
else
0
end
end

LibC.dl_iterate_phdr(phdr_callback, pointerof(data))
self.read_dwarf_sections(data.program, data.base_address)
{% end %}
end

protected def self.read_dwarf_sections(program, base_address = 0)
Crystal::ELF.open(program) do |elf|
line_strings = elf.read_section?(".debug_line_str") do |sh, io|
{{ flag?(:win32) ? Crystal::PE : Crystal::ELF }}.open(program) do |image|
{% if flag?(:win32) %}
base_address -= image.original_image_base
@@coff_symbols = image.coff_symbols
{% end %}

line_strings = image.read_section?(".debug_line_str") do |sh, io|
Crystal::DWARF::Strings.new(io, sh.offset, sh.size)
end

strings = elf.read_section?(".debug_str") do |sh, io|
strings = image.read_section?(".debug_str") do |sh, io|
Crystal::DWARF::Strings.new(io, sh.offset, sh.size)
end

elf.read_section?(".debug_line") do |sh, io|
image.read_section?(".debug_line") do |sh, io|
@@dwarf_line_numbers = Crystal::DWARF::LineNumbers.new(io, sh.size, base_address, strings, line_strings)
end

elf.read_section?(".debug_info") do |sh, io|
image.read_section?(".debug_info") do |sh, io|
names = [] of {LibC::SizeT, LibC::SizeT, String}

while (offset = io.pos - sh.offset) < sh.size
info = Crystal::DWARF::Info.new(io, offset)

elf.read_section?(".debug_abbrev") do |sh, io|
image.read_section?(".debug_abbrev") do |sh, io|
info.read_abbreviations(io)
end

Expand Down
Loading

0 comments on commit 454744a

Please sign in to comment.