Skip to content

Commit

Permalink
Support call stacks for MinGW-w64 builds
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil committed Oct 22, 2024
1 parent 5f72133 commit 16a3985
Show file tree
Hide file tree
Showing 11 changed files with 371 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
102 changes: 102 additions & 0 deletions src/crystal/pe.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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

getter original_image_base : UInt64
@section_headers : Slice(SectionHeader)
@string_table_base : UInt32
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|
name_buf = nt_header.name.to_slice
while name_buf.last?.try(&.zero?)
name_buf = name_buf[0, name_buf.size - 1]
end
name = String.new(name_buf)

if name.starts_with?('/')
io.seek(@string_table_base + name[1..].to_i)
name = io.gets('\0', chomp: true).not_nil!
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 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

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

@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 16a3985

Please sign in to comment.