From 395290243ec5b06ecfbe4ac041deea3477444455 Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Sun, 26 Jul 2026 17:07:49 +0200 Subject: [PATCH] port my third-party `back` package to replace `core:debug/trace` --- core/debug/trace/allocator.odin | 255 ++++++++++++++++++++ core/debug/trace/doc.odin | 65 +++-- core/debug/trace/trace.odin | 215 +++++++++++++---- core/debug/trace/trace_cpp.odin | 194 --------------- core/debug/trace/trace_darwin.odin | 157 ++++++++++++ core/debug/trace/trace_instrumentation.odin | 98 ++++++++ core/debug/trace/trace_linux.odin | 210 ++++++++++++++++ core/debug/trace/trace_nil.odin | 22 -- core/debug/trace/trace_other.odin | 28 +++ core/debug/trace/trace_windows.odin | 137 +++++++---- core/sys/windows/dbghelp.odin | 3 +- 11 files changed, 1036 insertions(+), 348 deletions(-) create mode 100644 core/debug/trace/allocator.odin delete mode 100644 core/debug/trace/trace_cpp.odin create mode 100644 core/debug/trace/trace_darwin.odin create mode 100644 core/debug/trace/trace_instrumentation.odin create mode 100644 core/debug/trace/trace_linux.odin delete mode 100644 core/debug/trace/trace_nil.odin create mode 100644 core/debug/trace/trace_other.odin diff --git a/core/debug/trace/allocator.odin b/core/debug/trace/allocator.odin new file mode 100644 index 000000000..95c61cabb --- /dev/null +++ b/core/debug/trace/allocator.odin @@ -0,0 +1,255 @@ +#+vet explicit-allocators +package debug_trace + +import "base:runtime" + +import "core:fmt" +import "core:mem" +import "core:sync" + +// TODO: should this just be added to `core:mem.Tracking_Allocator`? + +/* +The backtrace tracking allocator is a similar allocator as the `core:mem` tracking allocator but keeps +backtraces for each allocation. + +Print results at the end using `tracking_allocator_print_results`. + +Example: + package main + + import "core:debug/trace" + + main :: proc() { + track: trace.Tracking_Allocator + trace.tracking_allocator_init(&track, context.allocator) + defer trace.tracking_allocator_destroy(&track) + + context.allocator = trace.tracking_allocator(&track) + defer trace.tracking_allocator_print_results(&track) + + _main() + } + + _main :: proc() { + for _ in 0..<5 { + _ = new(int) + free(rawptr(uintptr(100))) + } + } +*/ +Tracking_Allocator :: struct { + backing: mem.Allocator, + internals_allocator: mem.Allocator, + allocation_map: map[rawptr]Tracking_Allocator_Entry, + bad_free_array: [dynamic]Tracking_Allocator_Bad_Free_Entry, + mutex: sync.Mutex, + clear_on_free_all: bool, +} + +Tracking_Allocator_Entry :: struct { + memory: rawptr, + size: int, + alignment: int, + mode: mem.Allocator_Mode, + err: mem.Allocator_Error, + location: runtime.Source_Code_Location, + backtrace: Capture_Const, +} + +Tracking_Allocator_Bad_Free_Entry :: struct { + memory: rawptr, + location: runtime.Source_Code_Location, + backtrace: Capture_Const, +} + +tracking_allocator_init :: proc( + t: ^Tracking_Allocator, + backing_allocator: mem.Allocator, + internals_allocator := context.allocator, +) { + t.backing = backing_allocator + t.internals_allocator = internals_allocator + t.allocation_map.allocator = internals_allocator + t.bad_free_array.allocator = internals_allocator + + if .Free_All in mem.query_features(t.backing) { + t.clear_on_free_all = true + } +} + +tracking_allocator_destroy :: proc(t: ^Tracking_Allocator) { + delete(t.allocation_map) + delete(t.bad_free_array) +} + +tracking_allocator_clear :: proc(t: ^Tracking_Allocator) { + sync.guard(&t.mutex) + + clear(&t.allocation_map) + clear(&t.bad_free_array) +} + +@(require_results) +tracking_allocator :: proc(data: ^Tracking_Allocator) -> mem.Allocator { + return mem.Allocator{data = data, procedure = tracking_allocator_proc} +} + +tracking_allocator_proc :: proc( + allocator_data: rawptr, + mode: mem.Allocator_Mode, + size, alignment: int, + old_memory: rawptr, + old_size: int, + loc := #caller_location, +) -> ( + result: []byte, + err: mem.Allocator_Error, +) { + data := (^Tracking_Allocator)(allocator_data) + + sync.mutex_guard(&data.mutex) + + if mode == .Query_Info { + info := (^mem.Allocator_Query_Info)(old_memory) + if info != nil && info.pointer != nil { + if entry, ok := data.allocation_map[info.pointer]; ok { + info.size = entry.size + info.alignment = entry.alignment + } + info.pointer = nil + } + + return + } + + if mode == .Free && old_memory != nil && old_memory not_in data.allocation_map { + append( + &data.bad_free_array, + Tracking_Allocator_Bad_Free_Entry{ + memory = old_memory, + location = loc, + backtrace = capture(skip=1), + }, + ) + } else { + result = data.backing.procedure( + data.backing.data, + mode, + size, + alignment, + old_memory, + old_size, + loc, + ) or_return + } + result_ptr := raw_data(result) + + if data.allocation_map.allocator.procedure == nil { + data.allocation_map.allocator = context.allocator + } + + switch mode { + case .Alloc, .Alloc_Non_Zeroed: + data.allocation_map[result_ptr] = Tracking_Allocator_Entry { + memory = result_ptr, + size = size, + mode = mode, + alignment = alignment, + err = err, + location = loc, + backtrace = capture(skip=1), + } + case .Free: + delete_key(&data.allocation_map, old_memory) + case .Free_All: + if data.clear_on_free_all { + clear_map(&data.allocation_map) + } + case .Resize, .Resize_Non_Zeroed: + if old_memory != result_ptr { + delete_key(&data.allocation_map, old_memory) + } + data.allocation_map[result_ptr] = Tracking_Allocator_Entry { + memory = result_ptr, + size = size, + mode = mode, + alignment = alignment, + err = err, + location = loc, + backtrace = capture(skip=1), + } + + case .Query_Features: + set := (^mem.Allocator_Mode_Set)(old_memory) + if set != nil { + set^ = { + .Alloc, + .Alloc_Non_Zeroed, + .Free, + .Free_All, + .Resize, + .Query_Features, + .Query_Info, + } + } + return nil, nil + + case .Query_Info: + unreachable() + } + + return +} + +tracking_allocator_print_results :: proc(t: ^Tracking_Allocator, temp_allocator := context.temp_allocator) { + i: int + ALLOCATOR_MAX_BACKTRACES :: 16 + + for _, leak in t.allocation_map { + fmt.eprintfln("\x1b[31m%v leaked %m\x1b[0m", leak.location, leak.size) + + defer i += 1 + if i > ALLOCATOR_MAX_BACKTRACES { + continue + } + + trace, err := resolve(leak.backtrace, temp_allocator, temp_allocator) + defer locations_destroy(trace, temp_allocator) + + fmt.eprintln("[back trace]") + + if err != nil { + fmt.eprintfln("backtrace error: %v", err) + continue + } + + print(trace) + fmt.eprintln() + } + + for bad_free, _ in t.bad_free_array { + fmt.eprintfln( + "\x1b[31m%v allocation %p was freed badly\x1b[0m", + bad_free.location, + bad_free.memory, + ) + + defer i += 1 + if i > ALLOCATOR_MAX_BACKTRACES { + continue + } + + trace, err := resolve(bad_free.backtrace, temp_allocator, temp_allocator) + defer locations_destroy(trace, temp_allocator) + + fmt.eprintln("[back trace]") + + if err != nil { + fmt.eprintf("backtrace error: %v\n", err) + continue + } + + print(trace) + } +} diff --git a/core/debug/trace/doc.odin b/core/debug/trace/doc.odin index f9bb82fb5..a2409caa4 100644 --- a/core/debug/trace/doc.odin +++ b/core/debug/trace/doc.odin @@ -1,51 +1,40 @@ /* -Stack trace library. Only works when debug symbols are enabled using `-debug`. +Captures and resolves stack traces for debugging purpose. Example: - import "base:runtime" - import "core:debug/trace" + package main + import "core:debug/trace" import "core:fmt" - global_trace_ctx: trace.Context - - debug_trace_assertion_failure_proc :: proc(prefix, message: string, loc := #caller_location) -> ! { - runtime.print_caller_location(loc) - runtime.print_string(" ") - runtime.print_string(prefix) - if len(message) > 0 { - runtime.print_string(": ") - runtime.print_string(message) - } - runtime.print_byte('\n') - - ctx := &global_trace_ctx - if !trace.in_resolve(ctx) { - buf: [64]trace.Frame - runtime.print_string("Debug Trace:\n") - frames := trace.frames(ctx, 1, buf[:]) - for f, i in frames { - fl := trace.resolve(ctx, f, context.temp_allocator) - if fl.loc.file_path == "" && fl.loc.line == 0 { - continue - } - runtime.print_caller_location(fl.loc) - runtime.print_string(" - frame ") - runtime.print_int(i) - runtime.print_byte('\n') - } - } - runtime.trap() - } - main :: proc() { - trace.init(&global_trace_ctx) - defer trace.destroy(&global_trace_ctx) + track: trace.Tracking_Allocator + trace.tracking_allocator_init(&track, context.allocator) + defer trace.tracking_allocator_destroy(&track) - context.assertion_failure_proc = debug_trace_assertion_failure_proc + context.allocator = trace.tracking_allocator(&track) + defer trace.tracking_allocator_print_results(&track) - ... + context.assertion_failure_proc = trace.assertion_failure_proc + + _main() } + _main :: proc() { + // Uncomment to try tracking allocator. + // for _ in 0..<5 { + // _ = new(int) + // free(rawptr(uintptr(100))) + // } + + // Uncomment to try assertion failure handling. + // assert(false) + + capture := trace.capture() + locations, err := trace.resolve(capture) + if err != nil { fmt.eprintfln("trace error: %v", err) } + defer trace.locations_destroy(locations) + trace.print(locations) + } */ package debug_trace diff --git a/core/debug/trace/trace.odin b/core/debug/trace/trace.odin index 6a9ba9dab..9257d93bf 100644 --- a/core/debug/trace/trace.odin +++ b/core/debug/trace/trace.odin @@ -1,77 +1,198 @@ +#+vet explicit-allocators package debug_trace -import "base:intrinsics" import "base:runtime" -Frame :: distinct uintptr +import "core:io" +import "core:os" +import "core:sync" -Frame_Location :: struct { - using loc: runtime.Source_Code_Location, - allocator: runtime.Allocator, +// Size of a constant backtrace, as used by the tracking allocator for example. +BACKTRACE_SIZE :: #config(ODIN_TRACE_SIZE, 16) + +/* +Use the instrumentation based trace mode, instead of debug info based. +This mode has a little bit of runtime performance impact, but is supported on all targets. +*/ +INSTRUMENTATION_MODE :: #config(ODIN_TRACE_INSTRUMENTATION_MODE, false) + +/* +When using the instrumentation mode, but you also want to use Odin's instrumentation features +for something else. You can define this, and call this package's instrumentation procedures: +`instrumentation_enter`, and `instrumentation_exit`, in your own instrumentation enter/exit procedures. +*/ +CUSTOM_INSTRUMENTATION :: #config(ODIN_TRACE_CUSTOM_INSTRUMENTATION, false) + +/* +The path/command to invoke for symbolization. +Linux only. +*/ +SYMBOLIZER_PROGRAM :: #config(ODIN_TRACE_SYMBOLIZER_PROGRAM, "addr2line") + +// The string that is used when allocation failed for a symbol/file path. +OOM_MARKER :: "??OOM" + +Capture :: []Capture_Entry + +Capture_Const :: struct { + trace: [BACKTRACE_SIZE]Capture_Entry, + len: int, } -delete_frame_location :: proc(fl: Frame_Location) -> runtime.Allocator_Error { - allocator := fl.allocator - delete(fl.loc.procedure, allocator) or_return - delete(fl.loc.file_path, allocator) or_return - return nil +// Platform specific. +Capture_Entry :: distinct _Capture_Entry + +/* +Capture a constantly sized trace (defined by `-define:ODIN_TRACE_SIZE`). + +The trace starts at the stack frame that called this procedure, +you can start higher up the stack using the `skip` argument. +*/ +capture :: #force_no_inline proc(skip := 0) -> (bt: Capture_Const) { + bt.len = _capture(bt.trace[:], skip) + return } -Context :: struct { - in_resolve: bool, // atomic - impl: _Context, +/* +Capture must be deleted by the caller. +*/ + +/* +Capture a trace of the given size. + +The trace starts at the stack frame that called this procedure, +you can start higher up the stack using the `skip` argument. + +The capture must be deleted by the caller. +*/ +capture_n :: #force_no_inline proc(max_len: i32, skip := 0, allocator := context.allocator) -> Capture { + bt := make([]Capture_Entry, max_len, allocator) + n := _capture(bt[:], skip) + return bt[:n] } -init :: proc(ctx: ^Context) -> bool { - return _init(ctx) +/* +Capture a trace into the given preallocated capture buffer (owned by the caller). + +The trace starts at the stack frame that called this procedure, +you can start higher up the stack using the `skip` argument. +*/ +capture_fill :: #force_no_inline proc(buf: Capture, skip := 0) -> int { + return _capture(buf, skip) } -destroy :: proc(ctx: ^Context) -> bool { - return _destroy(ctx) +Resolve_Error :: enum { + None, + Allocator_Error, + Parse_Address_Failed, + Resolve_Aborted, + Resolve_Failed, } -@(require_results) -frames :: proc(ctx: ^Context, skip: uint, frames_buffer: []Frame) -> []Frame { - return _frames(ctx, skip, frames_buffer) +Location :: runtime.Source_Code_Location + +/* +Resolve the back trace into source code locations, if possible. + +Compile with `-debug` (or use `-define:ODIN_TRACE_INSTRUMENTATION_MODE`) for the most useful information. + +The result must be destroyed using `locations_destroy`. +*/ +resolve :: proc { + resolve_n, + resolve_const, } -@(require_results) -resolve :: proc(ctx: ^Context, frame: Frame, allocator: runtime.Allocator) -> (result: Frame_Location) { - return _resolve(ctx, frame, allocator) +/* +See the procedure group `resolve`. +*/ +resolve_n :: proc(bt: Capture, allocator := context.allocator, temp_allocator := context.temp_allocator) -> (out: []Location, err: Resolve_Error) { + return _resolve(bt, allocator, temp_allocator) } - -@(require_results) -in_resolve :: proc "contextless" (ctx: ^Context) -> bool { - return intrinsics.atomic_load(&ctx.in_resolve) +/* +See the procedure group `resolve`. +*/ +resolve_const :: proc(bt: Capture_Const, allocator := context.allocator, temp_allocator := context.temp_allocator) -> (out: []Location, err: Resolve_Error) { + bt := bt + return _resolve(bt.trace[:bt.len], allocator, temp_allocator) } -_format_hex :: proc(buf: []byte, val: uintptr, allocator: runtime.Allocator) -> int { - _digits := "0123456789abcdef" +locations_destroy :: proc(locations: []Location, allocator := context.allocator) { + _locations_destroy(locations, allocator) +} - shift := (size_of(uintptr) * 8) - 4 - offs := 0 +/* +An assertion failure procedure that prints a back trace. - for shift >= 0 { - d := (val >> uint(shift)) & 0xf - buf[offs] = _digits[d] - shift -= 4 - offs += 1 +Example: + context.assertion_failure_proc = trace.assertion_failure_proc + assert(false) +*/ +assertion_failure_proc :: proc(prefix, message: string, loc: runtime.Source_Code_Location) -> ! { + { + runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD() + + lines, err := resolve(capture(skip=1), context.temp_allocator, context.temp_allocator) + if err != nil { + os.write_string(os.stderr, "could not get backtrace for assertion failure\n") + } else { + os.write_string(os.stderr, "[back trace]\n") + print(lines) + locations_destroy(lines, context.temp_allocator) + } } - return offs + runtime.default_assertion_failure_proc(prefix, message, loc) } -_format_missing_proc :: proc(addr: uintptr, allocator: runtime.Allocator) -> string { - PREFIX :: "proc:0x" - buf, buf_err := make([]byte, len(PREFIX) + 16, allocator) - copy(buf, PREFIX) +/* +Print/writes locations. - if buf_err != nil { - return "OUT_OF_MEMORY" +Inputs: +- locations: the result of a `resolve` call. +- padding: padding to print before each line, defaults to a tab. +- w: the writer to print to, defaults to stderr. +*/ +print :: proc(locations: []Location, padding := "\t", w: Maybe(io.Writer) = nil) { + w := w.? or_else os.to_writer(os.stderr) + + for location, i in locations { + io.write_string(w, padding) + io.write_byte(w, '#') + io.write_int(w, i) + io.write_string(w, " ") + io.write_string(w, location.procedure) + io.write_string(w, " at ") + io.write_string(w, location.file_path) + + if location.line > 0 { + when ODIN_ERROR_POS_STYLE == .Default { + io.write_byte(w, '(') + io.write_int(w, int(location.line)) + if location.column > 0 { + io.write_byte(w, ':') + io.write_int(w, int(location.column)) + } + io.write_byte(w, ')') + } else when ODIN_ERROR_POS_STYLE == .Unix { + io.write_byte(w, ':') + io.write_int(w, int(location.line)) + if location.column > 0 { + io.write_byte(w, ':') + io.write_int(w, int(location.column)) + } + } else { + #panic("unhandled ODIN_ERROR_POS_STYLE") + } + } + + io.write_byte(w, '\n') } - - offs := len(PREFIX) - offs += _format_hex(buf[offs:], uintptr(addr), allocator) - return string(buf[:offs]) } + +/* +The dbghelp library of win32 is not thread safe, this library uses this mutex to get exclusive access. +It is provided in case you want to use the dbghelp library, and want to coordinate access with this package. +*/ +_win32_dbghelp_mutex: sync.Mutex diff --git a/core/debug/trace/trace_cpp.odin b/core/debug/trace/trace_cpp.odin deleted file mode 100644 index 4828b207e..000000000 --- a/core/debug/trace/trace_cpp.odin +++ /dev/null @@ -1,194 +0,0 @@ -#+private file -#+build linux, darwin -package debug_trace - -import "base:intrinsics" -import "base:runtime" -import "core:strings" -import "core:c" - -// NOTE: Relies on C++23 which adds and becomes ABI and that can be used -foreign import stdcpplibbacktrace "system:stdc++exp" - -foreign import libdl "system:dl" - -backtrace_state :: struct {} -backtrace_error_callback :: proc "c" (data: rawptr, msg: cstring, errnum: c.int) -backtrace_simple_callback :: proc "c" (data: rawptr, pc: uintptr) -> c.int -backtrace_full_callback :: proc "c" (data: rawptr, pc: uintptr, filename: cstring, lineno: c.int, function: cstring) -> c.int -backtrace_syminfo_callback :: proc "c" (data: rawptr, pc: uintptr, symname: cstring, symval: uintptr, symsize: uintptr) - -@(default_calling_convention="c", link_prefix="__glibcxx_") -foreign stdcpplibbacktrace { - backtrace_create_state :: proc( - filename: cstring, - threaded: c.int, - error_callback: backtrace_error_callback, - data: rawptr, - ) -> ^backtrace_state --- - backtrace_simple :: proc( - state: ^backtrace_state, - skip: c.int, - callback: backtrace_simple_callback, - error_callback: backtrace_error_callback, - data: rawptr, - ) -> c.int --- - backtrace_pcinfo :: proc( - state: ^backtrace_state, - pc: uintptr, - callback: backtrace_full_callback, - error_callback: backtrace_error_callback, - data: rawptr, - ) -> c.int --- - backtrace_syminfo :: proc( - state: ^backtrace_state, - addr: uintptr, - callback: backtrace_syminfo_callback, - error_callback: backtrace_error_callback, - data: rawptr, - ) -> c.int --- - - // NOTE(bill): this is technically an internal procedure, but it is exposed - backtrace_free :: proc( - state: ^backtrace_state, - p: rawptr, - size: c.size_t, // unused - error_callback: backtrace_error_callback, // unused - data: rawptr, // unused - ) --- -} - -Dl_info :: struct { - dli_fname: cstring, - dli_fbase: rawptr, - dli_sname: cstring, - dli_saddr: rawptr, -} - -@(default_calling_convention="c") -foreign libdl { - dladdr :: proc(addr: rawptr, info: ^Dl_info) -> c.int --- -} - -@(private="package") -_Context :: struct { - state: ^backtrace_state, -} - -@(private="package") -_init :: proc(ctx: ^Context) -> (ok: bool) { - defer if !ok { destroy(ctx) } - - ctx.impl.state = backtrace_create_state("odin-debug-trace", 1, nil, ctx) - return ctx.impl.state != nil -} - -@(private="package") -_destroy :: proc(ctx: ^Context) -> bool { - if ctx != nil { - backtrace_free(ctx.impl.state, nil, 0, nil, nil) - } - return true -} - -@(private="package") -_frames :: proc "contextless" (ctx: ^Context, skip: uint, frames_buffer: []Frame) -> (frames: []Frame) { - Backtrace_Context :: struct { - ctx: ^Context, - frames: []Frame, - frame_count: int, - } - - btc := &Backtrace_Context{ - ctx = ctx, - frames = frames_buffer, - } - backtrace_simple( - ctx.impl.state, - c.int(skip + 2), - proc "c" (user: rawptr, address: uintptr) -> c.int { - btc := (^Backtrace_Context)(user) - address := Frame(address) - if address == 0 { - return 1 - } - if btc.frame_count == len(btc.frames) { - return 1 - } - btc.frames[btc.frame_count] = address - btc.frame_count += 1 - return 0 - }, - nil, - btc, - ) - - if btc.frame_count > 0 { - frames = btc.frames[:btc.frame_count] - } - return -} - -@(private="package") -_resolve :: proc(ctx: ^Context, frame: Frame, allocator: runtime.Allocator) -> Frame_Location { - intrinsics.atomic_store(&ctx.in_resolve, true) - defer intrinsics.atomic_store(&ctx.in_resolve, false) - - Backtrace_Context :: struct { - rt_ctx: runtime.Context, - allocator: runtime.Allocator, - frame: Frame_Location, - } - - btc := &Backtrace_Context{ - rt_ctx = context, - allocator = allocator, - } - done := backtrace_pcinfo( - ctx.impl.state, - uintptr(frame), - proc "c" (data: rawptr, address: uintptr, file: cstring, line: c.int, symbol: cstring) -> c.int { - btc := (^Backtrace_Context)(data) - context = btc.rt_ctx - - frame := &btc.frame - - if file != nil { - frame.file_path = strings.clone_from_cstring(file, btc.allocator) - } else if info: Dl_info; dladdr(rawptr(address), &info) != 0 && info.dli_fname != "" { - frame.file_path = strings.clone_from_cstring(info.dli_fname, btc.allocator) - } - if symbol != nil { - frame.procedure = strings.clone_from_cstring(symbol, btc.allocator) - } else if info: Dl_info; dladdr(rawptr(address), &info) != 0 && info.dli_sname != "" { - frame.procedure = strings.clone_from_cstring(info.dli_sname, btc.allocator) - } else { - frame.procedure = _format_missing_proc(address, btc.allocator) - } - frame.line = i32(line) - return 0 - }, - nil, - btc, - ) - if done != 0 { - return btc.frame - } - - // NOTE(bill): pcinfo cannot resolve, but it might be possible to get the procedure name at least - backtrace_syminfo( - ctx.impl.state, - uintptr(frame), - proc "c" (data: rawptr, address: uintptr, symbol: cstring, _ignore0, _ignore1: uintptr) { - if symbol != nil { - btc := (^Backtrace_Context)(data) - context = btc.rt_ctx - btc.frame.procedure = strings.clone_from_cstring(symbol, btc.allocator) - } - }, - nil, - btc, - ) - - return btc.frame -} diff --git a/core/debug/trace/trace_darwin.odin b/core/debug/trace/trace_darwin.odin new file mode 100644 index 000000000..89ed1dda9 --- /dev/null +++ b/core/debug/trace/trace_darwin.odin @@ -0,0 +1,157 @@ +#+vet explicit-allocators +#+private file +package debug_trace + +@require import "base:runtime" + +@require import "core:strings" +@require import "core:sys/posix" + +when !INSTRUMENTATION_MODE { + +foreign import system "system:System.framework" + +// NOTE: CoreSymbolication is a private framework, Apple is allowed to break it and doesn't provide +// headers, although the API has as of my knowledge been the same in the past 10 years at least. +@(extra_linker_flags="-iframework /System/Library/PrivateFrameworks") +foreign import symbolication "system:CoreSymbolication.framework" + +@(private="package") +_Capture_Entry :: rawptr + +@(private="package") +_capture :: #force_no_inline proc(buf: Capture, skip: int) -> (n: int) { + ctx: unw_context_t + cursor: unw_cursor_t + + ret: i32 + if ret = unw_getcontext(&ctx); ret != 0 { return } + if ret = unw_init_local(&cursor, &ctx); ret != 0 { return } + + // Skip this function's frame and the caller, in addition to `skip`. + for _ in 0.. 0 && n < len(buf); n += 1 { + if ret = unw_get_reg(&cursor, .IP, &pc); ret != 0 { return } + buf[n] = Capture_Entry(pc) + } + + return +} + +@(private="package") +_locations_destroy :: proc(locations: []Location, allocator: runtime.Allocator) { + for location in locations { + delete(location.file_path, allocator) + delete(location.procedure, allocator) + } + delete(locations, allocator) +} + +@(private="package") +_resolve :: proc(bt: Capture, allocator, _: runtime.Allocator) -> (out: []Location, err: Resolve_Error) { + out = make([]Location, len(bt), allocator) + defer if err != nil { _locations_destroy(out, allocator) } + + symbolicator := CSSymbolicatorCreateWithPid(posix.getpid()) + defer CSRelease(symbolicator) + + for &location, i in out { + symbol := CSSymbolicatorGetSymbolWithAddressAtTime(symbolicator, uintptr(bt[i]), CSNow) + info := CSSymbolicatorGetSourceInfoWithAddressAtTime(symbolicator, uintptr(bt[i]), CSNow) + + location.procedure = clone_or_oom_marker(CSSymbolGetName(symbol), allocator) + + // No debug info. + if CSIsNull(info) { + owner := CSSymbolGetSymbolOwner(symbol) + location.file_path = clone_or_oom_marker(CSSymbolOwnerGetPath(owner), allocator) + } else { + location.file_path = clone_or_oom_marker(CSSourceInfoGetPath(info), allocator) + location.line = CSSourceInfoGetLineNumber(info) + } + } + + return + + clone_or_oom_marker :: proc(s: cstring, allocator: runtime.Allocator) -> string { + clone, err := strings.clone_from(s, allocator) + if err != nil { + return OOM_MARKER + } + + return clone + } + +} + +CSTypeRef :: struct { + csCppData: rawptr, + csCppObj: rawptr, +} + +CSSymbolicatorRef :: distinct CSTypeRef +CSSymbolRef :: distinct CSTypeRef +CSSourceInfoRef :: distinct CSTypeRef +CSSymbolOwnerRef :: distinct CSTypeRef + +CSNow :: 0x80000000 + +foreign symbolication { + @(link_name="CSIsNull") + _CSIsNull :: proc(ref: CSTypeRef) -> bool --- + @(link_name="CSRelease") + _CSRelease :: proc(ref: CSTypeRef) --- + + CSSymbolicatorCreateWithPid :: proc(pid: posix.pid_t) -> CSSymbolicatorRef --- + + CSSymbolicatorGetSymbolWithAddressAtTime :: proc(symbolicator: CSSymbolicatorRef, addr: uintptr, time: u64) -> CSSymbolRef --- + CSSymbolicatorGetSourceInfoWithAddressAtTime :: proc(symbolicator: CSSymbolicatorRef, adrr: uintptr, time: u64) -> CSSourceInfoRef --- + + CSSymbolGetName :: proc(symbol: CSSymbolRef) -> cstring --- + CSSymbolGetSymbolOwner :: proc(symbol: CSSymbolRef) -> CSSymbolOwnerRef --- + + CSSourceInfoGetPath :: proc(info: CSSourceInfoRef) -> cstring --- + CSSourceInfoGetLineNumber :: proc(info: CSSourceInfoRef) -> i32 --- + CSSourceInfoGetSymbol :: proc(info: CSSourceInfoRef) -> CSSymbolRef --- + + CSSymbolOwnerGetPath :: proc(owner: CSSymbolOwnerRef) -> cstring --- +} + +CSRelease :: #force_inline proc(ref: $T) { + _CSRelease(CSTypeRef(ref)) +} + +CSIsNull :: #force_inline proc(ref: $T) -> bool { + return _CSIsNull(CSTypeRef(ref)) +} + +// These could actually be smaller, but then we would have to define and check the size on each +// architecture, the sizes here are the largest they can be. +_LIBUNWIND_CONTEXT_SIZE :: 167 +_LIBUNWIND_CURSOR_SIZE :: 204 + +unw_context_t :: struct { + data: [_LIBUNWIND_CONTEXT_SIZE]u64, +} + +unw_cursor_t :: struct { + data: [_LIBUNWIND_CURSOR_SIZE]u64, +} + +// Cross-platform registers, each architecture has additional registers but these are enough for us. +Register :: enum i32 { + SP = -2, + IP = -1, +} + +foreign system { + unw_getcontext :: proc(ctx: ^unw_context_t) -> i32 --- + unw_init_local :: proc(cursor: ^unw_cursor_t, ctx: ^unw_context_t) -> i32 --- + unw_get_reg :: proc(cursor: ^unw_cursor_t, name: Register, reg: ^uintptr) -> i32 --- + unw_step :: proc(cursor: ^unw_cursor_t) -> i32 --- +} +} // INSTRUMENTATION_MODE diff --git a/core/debug/trace/trace_instrumentation.odin b/core/debug/trace/trace_instrumentation.odin new file mode 100644 index 000000000..fcc49ef77 --- /dev/null +++ b/core/debug/trace/trace_instrumentation.odin @@ -0,0 +1,98 @@ +#+vet explicit-allocators +#+private file +package debug_trace + +@require import "base:runtime" + +@require import "core:slice" + +when INSTRUMENTATION_MODE { + +when ODIN_OPTIMIZATION_MODE == .None { + #panic("the `trace` package's instrumentation mode requires at least `-o:minimal` to work (it requires `#force_inline` to actually be applied)") +} + +when ODIN_USE_SEPARATE_MODULES { + #panic("the `trace` package's instrumentation mode requires `-use-single-module` to work (there are subtle instrumentation bugs to hunt down)") +} + +@(private="package", no_instrumentation) +instrumentation_enter :: #force_inline proc "contextless" (a, b: rawptr, loc: runtime.Source_Code_Location) { + _instrumentation_enter(a, b, loc) +} + +@(private="package", no_instrumentation) +instrumentation_exit :: #force_inline proc "contextless" (a, b: rawptr, loc: runtime.Source_Code_Location) { + _instrumentation_exit(a, b, loc) +} + +@(private="package") +_Capture_Entry :: Location + +@(private="package") +_capture :: #force_no_inline proc(buf: Capture, skip: int) -> (n: int) { + lframe := frame + + // Omit `skip+2` frames. 2 being the current and caller. + for _ in 0.. (out: []Location, err: Resolve_Error) { + clone, mem_err := slice.clone(bt, allocator) + if mem_err != nil { return nil, .Allocator_Error } + return transmute([]Location)clone, nil +} + +Frame :: struct { + prev: ^Frame, + loc: runtime.Source_Code_Location, +} + +@(thread_local) +frame: ^Frame + +when CUSTOM_INSTRUMENTATION { + @(no_instrumentation) + _instrumentation_enter :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = &Frame{ + prev = frame, + loc = loc, + } + } + + @(no_instrumentation) + _instrumentation_exit :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = frame.prev + } +} else { + @(instrumentation_enter) + _instrumentation_enter :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = &Frame{ + prev = frame, + loc = loc, + } + } + + @(instrumentation_exit) + _instrumentation_exit :: #force_inline proc "contextless" (_, _: rawptr, loc: runtime.Source_Code_Location) { + frame = frame.prev + } +} +} // INSTRUMENTATION_MODE diff --git a/core/debug/trace/trace_linux.odin b/core/debug/trace/trace_linux.odin new file mode 100644 index 000000000..b9d68c0c2 --- /dev/null +++ b/core/debug/trace/trace_linux.odin @@ -0,0 +1,210 @@ +#+vet explicit-allocators +#+private file +package debug_trace + +@require import "base:intrinsics" +@require import "base:runtime" + +@require import "core:c" +@require import "core:os" +@require import "core:strconv" +@require import "core:strings" + +when !INSTRUMENTATION_MODE { + +foreign import lib "system:c" + +@(private="package") +_Capture_Entry :: rawptr + +@(private="package") +_capture :: #force_no_inline proc(buf: Capture, skip: int) -> (n: int) { + foreign lib { + backtrace :: proc(buffer: [^]rawptr, size: c.int) -> c.int --- + } + + skip := skip + skip += 2 // This function + caller. + + #assert(intrinsics.type_base_type(intrinsics.type_elem_type(Capture)) == rawptr) + // In order to omit `skip` frames, we alloca a temp buffer with `skip` extra slots. + bigger_buf := ([^]Capture_Entry)(intrinsics.alloca((skip + len(buf)) * size_of(Capture_Entry), align_of(Capture_Entry)))[:len(buf)+2] + _n := int(backtrace(([^]rawptr)(raw_data(bigger_buf)), i32(len(bigger_buf)))) + if _n > skip { + copy(buf, bigger_buf[skip:]) + return _n-skip + } + + return 0 +} + +@(private="package") +_locations_destroy :: proc(locations: []Location, allocator: runtime.Allocator) { + for location in locations { + if location.file_path != OOM_MARKER { delete(location.file_path, allocator) } + if location.procedure != OOM_MARKER { delete(location.procedure, allocator) } + } + delete(locations, allocator) +} + +@(private="package") +_resolve :: proc(bt: Capture, allocator, temp_allocator: runtime.Allocator) -> (locations: []Location, err: Resolve_Error) { + foreign lib { + backtrace_symbols :: proc(buffer: [^]rawptr, size: c.int) -> [^]cstring --- + @(link_name="free") + backtrace_free :: proc(ptr: rawptr) --- + } + + #assert(intrinsics.type_base_type(intrinsics.type_elem_type(Capture)) == rawptr) + msgs := backtrace_symbols(([^]rawptr)(raw_data(bt)), i32(len(bt)))[:len(bt)] + defer backtrace_free(raw_data(msgs)) + + { + mem_err: runtime.Allocator_Error + locations, mem_err = make([]Location, len(bt), allocator) + if mem_err != nil { + return nil, .Allocator_Error + } + } + + defer if err != nil { + _locations_destroy(locations, allocator) + } + + // Debug info is needed. + when !ODIN_DEBUG { + return fallback_to_unresolved(locations, msgs, allocator) + } + + i := 0 + + command := make([dynamic]string, temp_allocator) + defer delete(command) + + if _, err := append(&command, SYMBOLIZER_PROGRAM, "--functions", "--exe", ""); err != nil { + return fallback_to_unresolved(locations, msgs, allocator) + } + + COMMAND_EXE_POS :: 3 + COMMAND_START_LEN :: 4 + + for msg in msgs { + exe, addr := parse_address(msg) or_return + if command[COMMAND_EXE_POS] == "" { + command[COMMAND_EXE_POS] = exe + } else if command[COMMAND_EXE_POS] != exe { + i += exec_and_fill(command[:], locations[i:], msgs[i:], allocator, temp_allocator) or_return + + command[COMMAND_EXE_POS] = exe + resize(&command, COMMAND_START_LEN) + } + + if _, err := append(&command, addr); err != nil { + return fallback_to_unresolved(locations, msgs, allocator) + } + } + + if len(command) > COMMAND_START_LEN { + i += exec_and_fill(command[:], locations[i:], msgs[i:], allocator, temp_allocator) or_return + } + + return + + clone_or_oom_marker :: proc(s: string, allocator: runtime.Allocator) -> string { + clone, err := strings.clone(s, allocator) + if err != nil { + return OOM_MARKER + } + + return clone + } + + fallback_to_unresolved :: proc(locations: []Location, msgs: []cstring, allocator: runtime.Allocator) -> ([]Location, Resolve_Error) { + for msg, i in msgs { + exe, address, parse_err := parse_address(msg) + if parse_err != nil { + locations[i] = Location { + procedure = clone_or_oom_marker(string(msg), allocator), + } + continue + } + + locations[i] = Location { + file_path = clone_or_oom_marker(exe, allocator), + procedure = clone_or_oom_marker(address, allocator), + } + } + + return locations, nil + } + + // Parses the exe and address out of a backtrace line. + // Example: .../main(+0x20) [0x100000] -> .../main, +0x20, nil + parse_address :: proc(cmsg: cstring) -> (string, string, Resolve_Error) { + msg := string(cmsg) + close_idx := strings.last_index_byte(msg, ')') + if close_idx < 1 { return "", "", .Parse_Address_Failed } + + open_idx := strings.last_index_byte(msg[:close_idx], '(') + if open_idx < 0 { return "", "", .Parse_Address_Failed } + + return msg[:open_idx], msg[open_idx+1:close_idx], nil + } + + exec_and_fill :: proc(command: []string, locations: []Location, msgs: []cstring, allocator, temp_allocator: runtime.Allocator) -> (filled: int, err: Resolve_Error) { + state, stdout, stderr, perr := os.process_exec({command = command}, temp_allocator) + defer delete(stdout, temp_allocator) + defer delete(stderr, temp_allocator) + + if perr != nil || !state.success { return 0, .Resolve_Failed } + + count := len(command)-COMMAND_START_LEN + + sstdout := string(stdout) + for i in 0.. (file_path: string, line: i32) { + file_path = location + + colon_idx := strings.last_index_byte(location, ':') + if colon_idx > 0 { + line_str := location[colon_idx+1:] + if line_int, ok := strconv.parse_i64_of_base(line_str, 10); ok { + file_path = location[:colon_idx] + line = i32(line_int) + } + } + + return + } + + process_line :: proc(line: string, ok: bool) -> (string, Resolve_Error) { + if !ok { return "", .Resolve_Aborted } + if line == "" { return "", .Resolve_Failed } + return strings.trim_right_space(line), nil + } +} +} // INSTRUMENTATION_MODE diff --git a/core/debug/trace/trace_nil.odin b/core/debug/trace/trace_nil.odin deleted file mode 100644 index 8ee96720e..000000000 --- a/core/debug/trace/trace_nil.odin +++ /dev/null @@ -1,22 +0,0 @@ -#+build !windows -#+build !linux -#+build !darwin -package debug_trace - -import "base:runtime" - -_Context :: struct { -} - -_init :: proc(ctx: ^Context) -> (ok: bool) { - return true -} -_destroy :: proc(ctx: ^Context) -> bool { - return true -} -_frames :: proc(ctx: ^Context, skip: uint, frames_buffer: []Frame) -> []Frame { - return nil -} -_resolve :: proc(ctx: ^Context, frame: Frame, allocator: runtime.Allocator) -> (result: Frame_Location) { - return -} diff --git a/core/debug/trace/trace_other.odin b/core/debug/trace/trace_other.odin new file mode 100644 index 000000000..443ad3c24 --- /dev/null +++ b/core/debug/trace/trace_other.odin @@ -0,0 +1,28 @@ +#+build !linux +#+build !darwin +#+build !windows +#+private file +#+vet explicit-allocators +package debug_trace + +@(require) import "base:runtime" + +when !INSTRUMENTATION_MODE { + +@(private="package") +_Capture_Entry :: struct {} + +@(private="package") +_capture :: #force_no_inline proc(buf: Capture, skip: int) -> (n: int) { + return +} + +@(private="package") +_locations_destroy :: proc(locations: []Location, allocator: runtime.Allocator) {} + +@(private="package") +_resolve :: proc(bt: Capture, allocator, temp_allocator: runtime.Allocator) -> (locations: []Location, err: Resolve_Error) { + return +} + +} // INSTRUMENTATION_MODE diff --git a/core/debug/trace/trace_windows.odin b/core/debug/trace/trace_windows.odin index b27b0ee29..f421f7364 100644 --- a/core/debug/trace/trace_windows.odin +++ b/core/debug/trace/trace_windows.odin @@ -1,69 +1,114 @@ -#+private -#+build windows +#+vet explicit-allocators +#+private file package debug_trace -import "base:intrinsics" -import "base:runtime" +@require import "base:runtime" -import win32 "core:sys/windows" +@require import "core:strings" +@require import "core:sync" +@require import win "core:sys/windows" -_Context :: struct { - hProcess: win32.HANDLE, - lock: win32.SRWLOCK, -} +when !INSTRUMENTATION_MODE { -_init :: proc "contextless" (ctx: ^Context) -> (ok: bool) { - defer if !ok { _destroy(ctx) } - ctx.impl.hProcess = win32.GetCurrentProcess() - win32.SymInitialize(ctx.impl.hProcess, nil, true) or_return - win32.SymSetOptions(win32.SYMOPT_LOAD_LINES) - return true -} +@(private="package") +_Capture_Entry :: uintptr -_destroy :: proc "contextless" (ctx: ^Context) -> bool { - if ctx != nil { - win32.SymCleanup(ctx.impl.hProcess) - } - return true -} +@(private="package") +_capture :: #force_no_inline proc(buf: Capture, skip: int) -> (n: int) { + frame_count := win.RtlCaptureStackBackTrace(u32(skip)+2, u32(len(buf)), ([^]rawptr)(raw_data(buf)), nil) -_frames :: proc "contextless" (ctx: ^Context, skip: uint, frames_buffer: []Frame) -> []Frame { - frame_count := win32.RtlCaptureStackBackTrace(u32(skip) + 2, u32(len(frames_buffer)), ([^]rawptr)(&frames_buffer[0]), nil) - for i in 0.. (fl: Frame_Location) { - intrinsics.atomic_store(&ctx.in_resolve, true) - defer intrinsics.atomic_store(&ctx.in_resolve, false) +@(private="package") +_resolve :: proc(bt: Capture, allocator, temp_allocator: runtime.Allocator) -> (out: []Location, err: Resolve_Error) { + mem_err: runtime.Allocator_Error + out, mem_err = make([]Location, len(bt), allocator) + if mem_err != nil { return nil, .Allocator_Error } - // NOTE(bill): Dbghelp is not thread-safe - win32.AcquireSRWLockExclusive(&ctx.impl.lock) - defer win32.ReleaseSRWLockExclusive(&ctx.impl.lock) + defer if err != nil { _locations_destroy(out, allocator) } - data: [size_of(win32.SYMBOL_INFOW) + size_of([256]win32.WCHAR)]byte - symbol := (^win32.SYMBOL_INFOW)(&data[0]) + // Debug info is needed, if we call with out-of-date debug symbols it will return out-of-date info, so better to short-circuit right away. + when !ODIN_DEBUG { + for &location, i in locations { + location.file_path = "??" + + builder := strings.builder_make(allocator) + strings.write_string(&builder, "0x") + strings.write_i64 (&builder, i64(bt[i]), 16) + location.procedure = strings.to_string(builder) + } + return + } + + process := win.GetCurrentProcess() + + sync.guard(&_win32_dbghelp_mutex) + + if !win.SymInitialize(process, nil, true) { + err = .Resolve_Aborted + return + } + defer win.SymCleanup(process) + + win.SymSetOptions(win.SYMOPT_LOAD_LINES|win.SYMOPT_DEFERRED_LOADS) + + data: [size_of(win.SYMBOL_INFOW) + size_of([256]win.WCHAR)]byte + symbol := (^win.SYMBOL_INFOW)(&data[0]) // The value of SizeOfStruct must be the size of the whole struct, // not just the size of the pointer symbol.SizeOfStruct = size_of(symbol^) symbol.MaxNameLen = 255 - if win32.SymFromAddrW(ctx.impl.hProcess, win32.DWORD64(frame), &{}, symbol) { - fl.procedure, _ = win32.wstring_to_utf8(cstring16(&symbol.Name[0]), -1, allocator) - } else { - fl.procedure = _format_missing_proc(uintptr(frame), allocator) - } - line: win32.IMAGEHLP_LINE64 - line.SizeOfStruct = size_of(line) - if win32.SymGetLineFromAddrW64(ctx.impl.hProcess, win32.DWORD64(frame), &{}, &line) { - fl.file_path, _ = win32.wstring_to_utf8(line.FileName, -1, allocator) - fl.line = i32(line.LineNumber) + for &line, i in out { + if win.SymFromAddrW(process, win.DWORD64(bt[i]), nil, symbol) { + symbol, mem_err := win.wstring_to_utf8(cstring16(&symbol.Name[0]), int(symbol.NameLen), allocator) + if mem_err != nil { + line.procedure = OOM_MARKER + } else if symbol == "??" { + delete(symbol, allocator) + line.procedure = "??" + } else { + line.procedure = symbol + } + } else { + line.procedure = "??" + } + + lineInfo: win.IMAGEHLP_LINE64 + lineInfo.SizeOfStruct = size_of(lineInfo) + if win.SymGetLineFromAddrW64(process, win.DWORD64(bt[i]), &{}, &lineInfo) { + line.file_path, mem_err = win.wstring_to_utf8(lineInfo.FileName, len(lineInfo.FileName), allocator) + if mem_err != nil { + line.file_path = OOM_MARKER + } + line.line = i32(lineInfo.LineNumber) + } else { + location := strings.builder_make(allocator) + strings.write_string(&location, "0x") + strings.write_i64 (&location, i64(bt[i]), 16) + line.file_path = strings.to_string(location) + } } return -} \ No newline at end of file +} + +} // INSTRUMENTATION_MODE diff --git a/core/sys/windows/dbghelp.odin b/core/sys/windows/dbghelp.odin index 4904c5fb4..dc37f169f 100644 --- a/core/sys/windows/dbghelp.odin +++ b/core/sys/windows/dbghelp.odin @@ -289,7 +289,8 @@ IMAGEHLP_LINE64 :: struct { PSYMBOL_INFOW :: ^SYMBOL_INFOW PIMAGEHLP_LINEW64 :: ^IMAGEHLP_LINE64 -SYMOPT_LOAD_LINES :: 0x00000010 +SYMOPT_LOAD_LINES :: 0x00000010 +SYMOPT_DEFERRED_LOADS :: 0x00000004 @(default_calling_convention = "system") foreign Dbghelp {