port my third-party back package to replace core:debug/trace

This commit is contained in:
Laytan Laats
2026-07-26 17:07:49 +02:00
parent e1691f9fac
commit 395290243e
11 changed files with 1036 additions and 348 deletions

View File

@@ -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)
}
}

View File

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

View File

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

View File

@@ -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 <stacktrace> 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
}

View File

@@ -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..<skip+1 {
if unw_step(&cursor) <= 0 { return }
}
pc: uintptr
for ; unw_step(&cursor) > 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

View File

@@ -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..<skip+2 {
if lframe != nil { lframe = lframe.prev }
}
for lframe != nil && n < len(buf) {
buf[n] = Capture_Entry(lframe.loc)
n += 1
lframe = lframe.prev
}
return
}
@(private="package")
_locations_destroy :: proc(locations: []Location, allocator: runtime.Allocator) {
delete(locations, allocator)
}
@(private="package")
_resolve :: proc(bt: Capture, allocator, temp_allocator: runtime.Allocator) -> (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

View File

@@ -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..<count {
exe, address, parse_err := parse_address(msgs[i])
assert(parse_err == nil)
procedure := process_line(strings.split_lines_iterator(&sstdout)) or_return
if procedure == "" || procedure == "??" {
procedure = address
}
location := process_line(strings.split_lines_iterator(&sstdout)) or_return
file_path, line := split_location(location)
if file_path == "" || strings.starts_with(file_path, "??") {
file_path = exe
}
locations[i] = {
procedure = clone_or_oom_marker(procedure, allocator),
file_path = clone_or_oom_marker(file_path, allocator),
line = line,
}
}
return count, nil
}
split_location :: proc(location: string) -> (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

View File

@@ -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
}

View File

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

View File

@@ -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..<frame_count {
for &frame in buf[:frame_count] {
// NOTE: Return address is one after the call instruction so subtract a byte to
// end up back inside the call instruction which is needed for SymFromAddr.
frames_buffer[i] -= 1
frame -= 1
}
return frames_buffer[:frame_count]
return int(frame_count)
}
@(private="package")
_locations_destroy :: proc(locations: []Location, allocator: runtime.Allocator) {
for line in locations {
delete(line.file_path, allocator)
if line.procedure != "??" && line.procedure != OOM_MARKER {
delete(line.procedure, allocator)
}
}
delete(locations, allocator)
}
_resolve :: proc(ctx: ^Context, frame: Frame, allocator: runtime.Allocator) -> (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
}
}
} // INSTRUMENTATION_MODE

View File

@@ -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 {