mirror of
https://github.com/odin-lang/Odin.git
synced 2026-07-31 19:28:55 +00:00
Merge pull request #7141 from laytan/port-back-to-core-debug-trace
port my third-party `back` package to replace `core:debug/trace`
This commit is contained in:
253
core/debug/trace/allocator.odin
Normal file
253
core/debug/trace/allocator.odin
Normal file
@@ -0,0 +1,253 @@
|
||||
#+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("%v leaked %m", leak.location, leak.size)
|
||||
|
||||
defer i += 1
|
||||
if i > ALLOCATOR_MAX_BACKTRACES {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.eprintln("[back trace]")
|
||||
|
||||
trace, err := resolve(leak.backtrace, temp_allocator, temp_allocator)
|
||||
if err != nil {
|
||||
fmt.eprintfln("\tbacktrace error: %v", err)
|
||||
continue
|
||||
}
|
||||
defer locations_destroy(trace, temp_allocator)
|
||||
|
||||
print(trace)
|
||||
fmt.eprintln()
|
||||
}
|
||||
|
||||
for bad_free, _ in t.bad_free_array {
|
||||
fmt.eprintfln(
|
||||
"%v allocation %p was freed badly",
|
||||
bad_free.location,
|
||||
bad_free.memory,
|
||||
)
|
||||
|
||||
defer i += 1
|
||||
if i > ALLOCATOR_MAX_BACKTRACES {
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.eprintln("[back trace]")
|
||||
|
||||
trace, err := resolve(bad_free.backtrace, temp_allocator, temp_allocator)
|
||||
if err != nil {
|
||||
fmt.eprintfln("\tbacktrace error: %v", err)
|
||||
continue
|
||||
}
|
||||
defer locations_destroy(trace, temp_allocator)
|
||||
|
||||
print(trace)
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,49 @@
|
||||
/*
|
||||
Stack trace library. Only works when debug symbols are enabled using `-debug`.
|
||||
Captures and resolves stack traces for debugging purpose.
|
||||
|
||||
Debug info based support is implemented for Windows, MacOS, and Linux.
|
||||
|
||||
Other targets can use the instrumentation based mode, which uses Odin's instrumentation
|
||||
features to trace, with some runtime overhead.
|
||||
Opt in with `-define:ODIN_TRACE_INSTRUMENTATION_MODE=true`.
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
defer trace.locations_destroy(locations)
|
||||
trace.print(locations)
|
||||
}
|
||||
*/
|
||||
package debug_trace
|
||||
|
||||
@@ -1,77 +1,183 @@
|
||||
#+vet explicit-allocators
|
||||
package debug_trace
|
||||
|
||||
import "base:intrinsics"
|
||||
import "base:runtime"
|
||||
|
||||
Frame :: distinct uintptr
|
||||
import "core:fmt"
|
||||
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,
|
||||
}
|
||||
|
||||
@(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 {
|
||||
fmt.eprintfln("could not get backtrace for assertion failure: %v", err)
|
||||
} else {
|
||||
fmt.eprintfln("[back trace]")
|
||||
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 locations to stderr.
|
||||
|
||||
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.
|
||||
*/
|
||||
print :: proc(locations: []Location, padding := "\t") {
|
||||
for location, i in locations {
|
||||
fmt.eprintf("%s#%v %v at %v", padding, i, location.procedure, location.file_path)
|
||||
if location.line > 0 {
|
||||
when ODIN_ERROR_POS_STYLE == .Default {
|
||||
fmt.eprintf("(%v", location.line)
|
||||
if location.column > 0 {
|
||||
fmt.eprintf(":%v)", location.column)
|
||||
} else {
|
||||
fmt.eprint(")")
|
||||
}
|
||||
} else when ODIN_ERROR_POS_STYLE == .Unix {
|
||||
fmt.eprintf(":%v", location.line)
|
||||
if location.column > 0 {
|
||||
fmt.eprintf(":%v", location.column)
|
||||
}
|
||||
} else {
|
||||
#panic("unhandled ODIN_ERROR_POS_STYLE")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.eprintln()
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
157
core/debug/trace/trace_darwin.odin
Normal file
157
core/debug/trace/trace_darwin.odin
Normal 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
|
||||
98
core/debug/trace/trace_instrumentation.odin
Normal file
98
core/debug/trace/trace_instrumentation.odin
Normal 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
|
||||
218
core/debug/trace/trace_linux.odin
Normal file
218
core/debug/trace/trace_linux.odin
Normal file
@@ -0,0 +1,218 @@
|
||||
#+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 {
|
||||
fill_unresolved(locations, msgs, allocator)
|
||||
return
|
||||
}
|
||||
|
||||
i := 0
|
||||
|
||||
command := make([dynamic]string, temp_allocator)
|
||||
defer delete(command)
|
||||
|
||||
if _, err := append(&command, SYMBOLIZER_PROGRAM, "--functions", "--exe", ""); err != nil {
|
||||
fill_unresolved(locations, msgs, allocator)
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
fill_unresolved(locations, msgs, allocator)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fill_unresolved :: proc(locations: []Location, msgs: []cstring, allocator: runtime.Allocator) {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
count := len(command)-COMMAND_START_LEN
|
||||
|
||||
// `SYMBOLIZER_PROGRAM` does not exist, lets fall back to unresolved info.
|
||||
if perr == .Not_Exist {
|
||||
fill_unresolved(locations, msgs, allocator)
|
||||
return count, nil
|
||||
}
|
||||
|
||||
if perr != nil || !state.success {
|
||||
return 0, .Resolve_Aborted
|
||||
}
|
||||
|
||||
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 || line == "" { return "", .Resolve_Aborted }
|
||||
return strings.trim_right_space(line), nil
|
||||
}
|
||||
}
|
||||
} // INSTRUMENTATION_MODE
|
||||
@@ -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
|
||||
}
|
||||
28
core/debug/trace/trace_other.odin
Normal file
28
core/debug/trace/trace_other.odin
Normal 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
|
||||
@@ -1,69 +1,123 @@
|
||||
#+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 {
|
||||
if line.file_path != "??" && line.file_path != OOM_MARKER {
|
||||
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 out {
|
||||
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) {
|
||||
file_name, mem_err := win.wstring_to_utf8(lineInfo.FileName, len(lineInfo.FileName), allocator)
|
||||
if mem_err != nil {
|
||||
line.file_path = OOM_MARKER
|
||||
} else if file_name == "??" {
|
||||
delete(file_name, allocator)
|
||||
line.file_path = "??"
|
||||
} else {
|
||||
line.file_path = file_name
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user