Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support call stacks on Windows #11461

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
18 changes: 12 additions & 6 deletions spec/std/exception/call_stack_spec.cr
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -12,17 +12,23 @@ 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)
# 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)
{% 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")
output.should_not contain("src/exception.cr")
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
Expand All @@ -36,7 +42,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

Expand Down
4 changes: 2 additions & 2 deletions spec/std/raise_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/crystal/codegen/codegen.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions src/compiler/crystal/codegen/debug.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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#{" /DEBUG:FULL" unless debug.none?} #{lib_flags} #{@link_flags}).gsub("\n", " ")
cmd = "#{CL} #{args}"

if cmd.to_utf16.size > 32000
Expand Down
6 changes: 1 addition & 5 deletions src/exception.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 66 additions & 9 deletions src/exception/call_stack.cr
Original file line number Diff line number Diff line change
@@ -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'.
Expand All @@ -12,12 +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
dir += File::SEPARATOR unless dir.ends_with?(File::SEPARATOR)
dir
end
end
private CURRENT_DIR = Process::INITIAL_PWD.try { |dir| Path[dir] }

@@skip = [] of String

Expand All @@ -37,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
11 changes: 6 additions & 5 deletions src/exception/call_stack/dwarf.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/exception/call_stack/elf.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
53 changes: 0 additions & 53 deletions src/exception/call_stack/libunwind.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/exception/call_stack/mach_o.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading