add support for CFA in the eval system

Canonical Frame Address is a DWARF-only concept encoded in the unwind info.
Debugger must store this data for each call frame and provide it to
the eval system based on selected stack frame.

To abstract over format details, we introduce a "Frame Unwind Context".
For DWARF, this context maintains the intermediate unwind state and exposes
the CFA, whereas for PE, it remains empty.
This commit is contained in:
Nikita Smith
2025-11-26 15:42:40 -08:00
parent 2a6d0d737c
commit f9561cb618
9 changed files with 225 additions and 100 deletions
+140 -60
View File
@@ -1838,7 +1838,7 @@ DW_MEM_READ(ctrl_unwind_mem_read_dwarf_x64)
B32 is_read = ctrl_process_memory_read(ctx->process_handle, r1u64(addr, addr + size), &is_stale, buffer, ctx->endt_us);
DW_UnwindStatus status = DW_UnwindStatus_Fail;
if(is_stale && is_read)
if(is_stale)
{
status = DW_UnwindStatus_Maybe;
}
@@ -1903,10 +1903,33 @@ DW_REG_WRITE(ctrl_unwind_reg_write_dwarf_x64)
}
internal CTRL_UnwindStepResult
ctrl_unwind_step__dwarf(CTRL_Handle process_handle, CTRL_Handle module_handle, Arch arch, void *regs, U64 endt_us)
ctrl_unwind_step_result_from_dwarf_status(DW_UnwindStatus s)
{
Temp scratch = scratch_begin(0, 0);
CTRL_UnwindStepResult result = {0};
switch(s)
{
case DW_UnwindStatus_Ok:
{
result.flags &= ~(CTRL_UnwindFlag_Error|CTRL_UnwindFlag_Stale);
}break;
case DW_UnwindStatus_Fail:
{
result.flags |= CTRL_UnwindFlag_Error;
}break;
case DW_UnwindStatus_Maybe:
{
result.flags &= ~CTRL_UnwindFlag_Error;
result.flags |= CTRL_UnwindFlag_Stale;
}break;
default: { InvalidPath; } break;
}
return result;
}
internal CTRL_UnwindStepResult
ctrl_establish_frame_unwind_context__dwarf(Arena *arena, CTRL_Handle process_handle, CTRL_Handle module_handle, Arch arch, void *regs, U64 endt_us, CTRL_FrameUnwindContext *ctx_out)
{
Temp scratch = scratch_begin(&arena, 1);
CTRL_UnwindStepResult result = { .flags = CTRL_UnwindFlag_Error };
// gather context for virtual stack unwinder
@@ -2051,9 +2074,67 @@ ctrl_unwind_step__dwarf(CTRL_Handle process_handle, CTRL_Handle module_handle, A
}
// find register rules for IP
DW_CFI_Row *cfi_row = dw_cfi_row_from_pc(scratch.arena, arch, &cie, &fde, decode_ptr_func, decode_ptr_ctx, ip);
DW_CFI_Row *cfi_row = dw_cfi_row_from_pc(arena, arch, &cie, &fde, decode_ptr_func, decode_ptr_ctx, ip);
if(cfi_row)
{
// setup machine ops
void *mem_read_ctx = 0;
void *reg_read_ctx = 0;
DW_MemRead *mem_read_func = 0;
DW_RegRead *reg_read_func = 0;
switch(arch)
{
case Arch_Null: break;
case Arch_x64:
{
CTRL_MemoryReadContextDwarfX64 *mem_read_ctx_x64 = push_array(scratch.arena, CTRL_MemoryReadContextDwarfX64, 1);
mem_read_ctx_x64->process_handle = process_handle;
mem_read_ctx_x64->endt_us = endt_us;
mem_read_ctx = mem_read_ctx_x64;
reg_read_ctx = regs;
mem_read_func = ctrl_unwind_mem_read_dwarf_x64;
reg_read_func = ctrl_unwind_reg_read_dwarf_x64;
}break;
case Arch_x86:
case Arch_arm64:
case Arch_arm32:
{
NotImplemented;
}break;
default: { InvalidPath; }break;
}
// compute CFA for the row
U64 cfa = 0;
DW_UnwindStatus unwind_status = dw_compute_cfa(arch, cfi_row, mem_read_func, mem_read_ctx, reg_read_func, reg_read_ctx, &cfa);
// on success fill out output
if(unwind_status == DW_UnwindStatus_Ok)
{
ctx_out->cfa = cfa;
ctx_out->cfi_row = cfi_row;
ctx_out->ret_addr_reg = cie.ret_addr_reg;
}
// translate unwind status code
result = ctrl_unwind_step_result_from_dwarf_status(unwind_status);
}
}
}
scratch_end(scratch);
return result;
}
internal CTRL_UnwindStepResult
ctrl_unwind_step__dwarf(CTRL_Handle process_handle, CTRL_Handle module_handle, Arch arch, void *regs, CTRL_FrameUnwindContext *frame_ctx, U64 endt_us)
{
Temp scratch = scratch_begin(0, 0);
CTRL_UnwindStepResult result = { .flags = CTRL_UnwindFlag_Error };
// setup machine ops
void *mem_read_ctx = 0;
void *reg_read_ctx = 0;
@@ -2088,9 +2169,9 @@ ctrl_unwind_step__dwarf(CTRL_Handle process_handle, CTRL_Handle module_handle, A
}
// apply register rules to the context
DW_UnwindStatus cfi_uw_status = dw_cfi_apply_register_rules(arch,
&cie,
cfi_row,
DW_UnwindStatus unwind_status = dw_cfi_apply_register_rules(arch,
frame_ctx->cfa,
frame_ctx->cfi_row,
mem_read_func,
mem_read_ctx,
reg_read_func,
@@ -2099,36 +2180,13 @@ ctrl_unwind_step__dwarf(CTRL_Handle process_handle, CTRL_Handle module_handle, A
reg_write_ctx);
// last frame typically has undefined rule for IP
if(cfi_row->regs[cie.ret_addr_reg].rule == DW_CFI_RegisterRule_Undefined)
if(frame_ctx->cfi_row->regs[frame_ctx->ret_addr_reg].rule == DW_CFI_RegisterRule_Undefined)
{
regs_arch_block_write_rip(arch, regs, 0);
}
// translate unwind status code to control layer's result flags
switch(cfi_uw_status)
{
case DW_UnwindStatus_Ok:
{
result.flags &= ~(CTRL_UnwindFlag_Error|CTRL_UnwindFlag_Stale);
}break;
case DW_UnwindStatus_Fail:
{
result.flags |= CTRL_UnwindFlag_Error;
}break;
case DW_UnwindStatus_Maybe:
{
result.flags &= ~CTRL_UnwindFlag_Error;
result.flags |= CTRL_UnwindFlag_Stale;
}break;
default: { InvalidPath; } break;
}
}
}
}
else
{
// TODO: if IP does not have FDE, does this mean function is a leaf?
}
// translate unwind status code
result = ctrl_unwind_step_result_from_dwarf_status(unwind_status);
scratch_end(scratch);
return result;
@@ -2977,23 +3035,41 @@ ctrl_unwind_step__pe_x64(CTRL_Handle process_handle, CTRL_Handle module_handle,
//- rjf: abstracted unwind step
internal CTRL_UnwindStepResult
ctrl_unwind_step(CTRL_Handle process, CTRL_Handle module, U64 module_base_vaddr, Arch arch, void *reg_block, U64 endt_us)
ctrl_establish_frame_unwind_context(Arena *arena, CTRL_Handle process_handle, CTRL_Handle module_handle, Arch arch, void *regs, U64 endt_us, CTRL_FrameUnwindContext *ctx_out)
{
CTRL_UnwindStepResult result = {0};
#if OS_LINUX
result = ctrl_establish_frame_unwind_context__dwarf(arena, process_handle, module_handle, arch, regs, endt_us, ctx_out);
#elif OS_WINDOWS
// windows does not have a concept of frame context
#else
# error "unwinder is not defined for current OS"
#endif
return result;
}
result = ctrl_unwind_step__dwarf(process, module, arch, reg_block, endt_us);
if(result.flags == CTRL_UnwindFlag_Error && ~result.flags & CTRL_UnwindFlag_Stale)
internal CTRL_UnwindStepResult
ctrl_unwind_step(CTRL_Handle process, CTRL_Handle module, U64 module_base_vaddr, Arch arch, void *reg_block, CTRL_FrameUnwindContext *frame_ctx, U64 endt_us)
{
CTRL_UnwindStepResult result = {0};
#if OS_LINUX
result = ctrl_unwind_step__dwarf(process, module, arch, reg_block, frame_ctx, endt_us);
#elif OS_WINDOWS
switch(arch)
{
default:{}break;
case Arch_x64:
case Arch_Null:{}break;
case Arch_x64:{result = ctrl_unwind_step__pe_x64(process, module, module_base_vaddr, reg_block, endt_us);}break;
case Arch_x86:
case Arch_arm32:
case Arch_arm64:
{
result = ctrl_unwind_step__pe_x64(process, module, module_base_vaddr, (REGS_RegBlockX64 *)reg_block, endt_us);
NotImplemented;
}break;
default:{InvalidPath;}break;
}
}
#else
# error "unwinder is not defined for current OS"
#endif
return result;
}
@@ -3004,8 +3080,7 @@ ctrl_unwind_from_thread(Arena *arena, CTRL_EntityCtx *ctx, CTRL_Handle thread, U
{
ProfBeginFunction();
Temp scratch = scratch_begin(&arena, 1);
CTRL_Unwind unwind = {0};
unwind.flags |= CTRL_UnwindFlag_Error;
CTRL_Unwind unwind = { .flags = CTRL_UnwindFlag_Error };
//- rjf: unpack args
CTRL_Entity *thread_entity = ctrl_entity_from_handle(ctx, thread);
@@ -3023,24 +3098,25 @@ ctrl_unwind_from_thread(Arena *arena, CTRL_EntityCtx *ctx, CTRL_Handle thread, U
U64 frame_node_count = 0;
if(regs_block_good)
{
unwind.flags = 0;
for(;;)
for(unwind.flags = 0;;)
{
// rjf: regs -> rip*module
U64 rip = regs_rip_from_arch_block(arch, regs_block);
U64 rsp = regs_rsp_from_arch_block(arch, regs_block);
CTRL_Entity *module = &ctrl_entity_nil;
for(CTRL_Entity *m = process_entity->first; m != &ctrl_entity_nil; m = m->next)
// rjf: cancel on 0 rip
if(rip == 0)
{
if(m->kind == CTRL_EntityKind_Module && contains_1u64(m->vaddr_range, rip))
{
module = m;
break;
}
}
// rjf: cancel on 0 rip/rsp
if(rsp == 0 && rip == 0)
// rip -> module
CTRL_Entity *module = ctrl_module_from_process_vaddr(process_entity, rip);
// establish frame context
CTRL_FrameUnwindContext frame_ctx = {0};
CTRL_UnwindStepResult frame_ctx_result = ctrl_establish_frame_unwind_context(scratch.arena, process_entity->handle, module->handle, arch, regs_block, endt_us, &frame_ctx);
unwind.flags |= frame_ctx_result.flags;
if(unwind.flags & (CTRL_UnwindFlag_Stale|CTRL_UnwindFlag_Error))
{
break;
}
@@ -3048,19 +3124,19 @@ ctrl_unwind_from_thread(Arena *arena, CTRL_EntityCtx *ctx, CTRL_Handle thread, U
// rjf: valid step -> push frame
CTRL_UnwindFrameNode *frame_node = push_array(scratch.arena, CTRL_UnwindFrameNode, 1);
CTRL_UnwindFrame *frame = &frame_node->v;
frame->cfa = frame_ctx.cfa;
frame->regs = push_array_no_zero(arena, U8, arch_reg_block_size);
MemoryCopy(frame->regs, regs_block, arch_reg_block_size);
DLLPushBack(first_frame_node, last_frame_node, frame_node);
frame_node_count += 1;
// rjf: unwind one step
CTRL_UnwindStepResult step = ctrl_unwind_step(process_entity->handle, module->handle, module->vaddr_range.min, arch, regs_block, endt_us);
CTRL_UnwindStepResult step = ctrl_unwind_step(process_entity->handle, module->handle, module->vaddr_range.min, arch, regs_block, &frame_ctx, endt_us);
unwind.flags |= step.flags;
if(step.flags & CTRL_UnwindFlag_Error ||
regs_rsp_from_arch_block(arch, regs_block) == 0 ||
regs_rip_from_arch_block(arch, regs_block) == 0 ||
(regs_rsp_from_arch_block(arch, regs_block) == rsp &&
regs_rip_from_arch_block(arch, regs_block) == rip))
// stop unwinding on errors or stale data
if(unwind.flags & (CTRL_UnwindFlag_Stale|CTRL_UnwindFlag_Error) ||
(regs_rsp_from_arch_block(arch, regs_block) == rsp && regs_rip_from_arch_block(arch, regs_block) == rip))
{
break;
}
@@ -3133,6 +3209,7 @@ ctrl_call_stack_from_unwind(Arena *arena, CTRL_Entity *process, CTRL_Unwind *bas
SLLQueuePush(first_frame, last_frame, dst_inline);
dst_inline->v.unwind_count = base_frame_idx;
dst_inline->v.regs = src->regs;
dst_inline->v.cfa = src->cfa;
frame_count += 1;
inline_frame_count += 1;
}
@@ -3142,6 +3219,7 @@ ctrl_call_stack_from_unwind(Arena *arena, CTRL_Entity *process, CTRL_Unwind *bas
SLLQueuePush(first_frame, last_frame, dst_base);
dst_base->v.unwind_count = base_frame_idx;
dst_base->v.regs = src->regs;
dst_base->v.cfa = src->cfa;
frame_count += 1;
// rjf: hook up inline frames to point to concrete frame, and to account for inline depth
@@ -4898,6 +4976,8 @@ ctrl_thread__eval_scope_begin(Arena *arena, CTRL_UserBreakpointList *user_bps, C
ctx->frame_base = push_array(arena, U64, 1);
// TODO(rjf): need to compute this out here somehow... ctx->frame_base[0] = ;
ctx->tls_base = push_array(arena, U64, 1);
// TODO: compute CFA
ctx->cfa = 0;
}
e_select_interpret_ctx(&scope->interpret_ctx, eval_dbg_infos_primary->rdi, thread_rip_voff);
+14 -2
View File
@@ -248,6 +248,7 @@ typedef struct CTRL_UnwindFrame CTRL_UnwindFrame;
struct CTRL_UnwindFrame
{
void *regs;
U64 cfa;
};
typedef struct CTRL_UnwindFrameNode CTRL_UnwindFrameNode;
@@ -272,6 +273,15 @@ struct CTRL_Unwind
CTRL_UnwindFlags flags;
};
typedef struct CTRL_FrameUnwindContext CTRL_FrameUnwindContext;
struct CTRL_FrameUnwindContext
{
// DWARF
U64 cfa;
DW_CFI_Row *cfi_row;
U64 ret_addr_reg;
};
////////////////////////////////
//~ rjf: Call Stack Types
@@ -281,6 +291,7 @@ struct CTRL_CallStackFrame
U64 unwind_count;
U64 inline_depth;
void *regs;
U64 cfa;
};
typedef struct CTRL_CallStack CTRL_CallStack;
@@ -948,14 +959,15 @@ internal Arch ctrl_arch_from_process_handle(CTRL_Handle process_handle);
internal CTRL_Unwind ctrl_unwind_deep_copy(Arena *arena, Arch arch, CTRL_Unwind *src);
//- DWARF
internal CTRL_UnwindStepResult ctrl_unwind_step__dwarf(CTRL_Handle process_handle, CTRL_Handle module_handle, Arch arch, void *regs, U64 endt_us);
internal CTRL_UnwindStepResult ctrl_establish_frame_unwind_context__dwarf(Arena *arena, CTRL_Handle process_handle, CTRL_Handle module_handle, Arch arch, void *regs, U64 endt_us, CTRL_FrameUnwindContext *ctx_out);
internal CTRL_UnwindStepResult ctrl_unwind_step__dwarf(CTRL_Handle process_handle, CTRL_Handle module_handle, Arch arch, void *regs, CTRL_FrameUnwindContext *frame_ctx, U64 endt_us);
//- rjf: [x64]
internal REGS_Reg64 *ctrl_unwind_reg_from_pe_gpr_reg__pe_x64(REGS_RegBlockX64 *regs, PE_UnwindGprRegX64 gpr_reg);
internal CTRL_UnwindStepResult ctrl_unwind_step__pe_x64(CTRL_Handle process_handle, CTRL_Handle module_handle, U64 module_base_vaddr, REGS_RegBlockX64 *regs, U64 endt_us);
//- rjf: abstracted unwind step
internal CTRL_UnwindStepResult ctrl_unwind_step(CTRL_Handle process, CTRL_Handle module, U64 module_base_vaddr, Arch arch, void *reg_block, U64 endt_us);
internal CTRL_UnwindStepResult ctrl_unwind_step(CTRL_Handle process, CTRL_Handle module, U64 module_base_vaddr, Arch arch, void *reg_block, CTRL_FrameUnwindContext *frame_ctx, U64 endt_us);
//- rjf: abstracted full unwind
internal CTRL_Unwind ctrl_unwind_from_thread(Arena *arena, CTRL_EntityCtx *ctx, CTRL_Handle thread, U64 endt_us);
+14
View File
@@ -1138,6 +1138,20 @@ d_query_cached_rip_from_thread_unwind(CTRL_Entity *thread, U64 unwind_count)
return result;
}
internal U64
d_query_cached_cfa_from_thread_unwind(CTRL_Entity *thread, U64 unwind_count)
{
U64 cfa = 0;
Access *access = access_open();
CTRL_CallStack callstack = ctrl_call_stack_from_thread(access, thread->handle, 1, 0);
if(callstack.concrete_frames_count != 0)
{
cfa = callstack.concrete_frames[unwind_count%callstack.concrete_frames_count]->cfa;
}
access_close(access);
return cfa;
}
internal U64
d_query_cached_tls_base_vaddr_from_process_root_rip(CTRL_Entity *process, U64 root_vaddr, U64 rip_vaddr)
{
+1
View File
@@ -418,6 +418,7 @@ internal DI_KeyList d_push_active_dbgi_key_list(Arena *arena);
//- rjf: per-run caches
internal U64 d_query_cached_rip_from_thread(CTRL_Entity *thread);
internal U64 d_query_cached_rip_from_thread_unwind(CTRL_Entity *thread, U64 unwind_count);
internal U64 d_query_cached_cfa_from_thread_unwind(CTRL_Entity *thread, U64 unwind_count);
internal U64 d_query_cached_tls_base_vaddr_from_process_root_rip(CTRL_Entity *process, U64 root_vaddr, U64 rip_vaddr);
internal E_String2NumMap *d_query_cached_locals_map_from_dbgi_key_voff(DI_Key dbgi_key, U64 voff);
internal E_String2NumMap *d_query_cached_member_map_from_dbgi_key_voff(DI_Key dbgi_key, U64 voff);
+20 -9
View File
@@ -237,18 +237,14 @@ dw_cfi_row_from_pc(Arena *arena, Arch arch, DW_CIE *cie, DW_FDE *fde, DW_DecodeP
}
internal DW_UnwindStatus
dw_cfi_apply_register_rules(Arch arch,
DW_CIE *cie,
dw_compute_cfa(Arch arch,
DW_CFI_Row *row,
DW_MemRead *mem_read_func, void *mem_read_ud,
DW_RegRead *reg_read_func, void *reg_read_ud,
DW_RegWrite *reg_write_func, void *reg_write_ud)
U64 *cfa_out)
{
Temp scratch = scratch_begin(0,0);
DW_UnwindStatus unwind_status = DW_UnwindStatus_Ok;
DW_UnwindStatus unwind_status = DW_UnwindStatus_Fail;
// establish CFA
U64 cfa = 0;
switch (row->cfa.rule) {
case DW_CFA_Rule_Null: break;
case DW_CFA_Rule_RegOff: {
@@ -257,14 +253,29 @@ dw_cfi_apply_register_rules(Arch arch,
U64 reg_size = dw_reg_size_from_code(arch, row->cfa.reg);
AssertAlways(reg_size <= sizeof(cfa_reg_value));
unwind_status = reg_read_func(row->cfa.reg, &cfa_reg_value, reg_size, reg_read_ud);
if (unwind_status != DW_UnwindStatus_Ok) { goto exit; }
cfa = cfa_reg_value + row->cfa.off;
if (unwind_status != DW_UnwindStatus_Ok) { break; }
*cfa_out = cfa_reg_value + row->cfa.off;
} break;
case DW_CFA_Rule_Expression: {
// TODO: evaluate expression
NotImplemented;
} break;
}
return unwind_status;
}
internal DW_UnwindStatus
dw_cfi_apply_register_rules(Arch arch,
U64 cfa,
DW_CFI_Row *row,
DW_MemRead *mem_read_func, void *mem_read_ud,
DW_RegRead *reg_read_func, void *reg_read_ud,
DW_RegWrite *reg_write_func, void *reg_write_ud)
{
Temp scratch = scratch_begin(0,0);
DW_UnwindStatus unwind_status = DW_UnwindStatus_Ok;
U64 max_reg_size = dw_reg_max_size_from_arch(arch);
void *reg_buffer = push_array(scratch.arena, U8, max_reg_size);
+1 -1
View File
@@ -89,7 +89,7 @@ internal DW_CFI_Unwind * dw_cfi_unwind_init(Arena *arena, Arch arch, DW_CIE *cie
internal B32 dw_cfi_next_row(Arena *arena, DW_CFI_Unwind *uw);
internal DW_CFI_Row * dw_cfi_row_from_pc(Arena *arena, Arch arch, struct DW_CIE *cie, struct DW_FDE *fde, DW_DecodePtr *decode_ptr_func, void *decode_ptr_ctx, U64 pc);
internal DW_UnwindStatus dw_cfi_apply_register_rules(Arch arch, DW_CIE *cie, DW_CFI_Row *row, DW_MemRead *mem_read_func, void *mem_read_ud, DW_RegRead *reg_read_func, void *reg_read_ud, DW_RegWrite *reg_write_func, void *reg_write_ud);
internal DW_UnwindStatus dw_cfi_apply_register_rules(Arch arch, U64 cfa, DW_CFI_Row *row, DW_MemRead *mem_read_func, void *mem_read_ud, DW_RegRead *reg_read_func, void *reg_read_ud, DW_RegWrite *reg_write_func, void *reg_write_ud);
#endif // DWARF_UNWIND_H
+7 -2
View File
@@ -26,10 +26,10 @@ e_select_interpret_ctx(E_InterpretCtx *ctx, RDI_Parsed *primary_rdi, U64 ip_voff
RDI_LocationKind loc_kind = *(RDI_LocationKind *)(all_location_data + block->location_data_off);
if(loc_kind == RDI_LocationKind_ValBytecodeStream || loc_kind == RDI_LocationKind_AddrBytecodeStream)
{
U8 *bytecode_ptr = all_location_data + block->location_data_off + sizeof(RDI_LocationKind);
U8 *bytecode_ptr = all_location_data + block->location_data_off;
U8 *bytecode_opl = all_location_data + all_location_data_size;
U64 bytecode_size = rdi_size_from_bytecode_stream(bytecode_ptr, bytecode_opl);
String8 bytecode = str8(bytecode_ptr, bytecode_size);
String8 bytecode = str8(bytecode_ptr + sizeof(RDI_LocationKind), bytecode_size);
frame_base = e_interpret(bytecode);
}
else if(loc_kind != RDI_LocationKind_NULL)
@@ -960,6 +960,11 @@ e_interpret(String8 bytecode)
// TODO: add support for pushing multiple values onto the stack
NotImplemented;
}break;
case RDI_EvalOp_PushCfa:
{
nval.u64 = e_interpret_ctx->cfa;
}break;
}
// rjf: push
+1
View File
@@ -17,6 +17,7 @@ struct E_InterpretCtx
U64 *module_base;
U64 *frame_base;
U64 *tls_base;
U64 cfa;
};
////////////////////////////////
+1
View File
@@ -11912,6 +11912,7 @@ rd_frame(void)
ctx->frame_base = push_array(scratch.arena, U64, 1);
ctx->tls_base = push_array(scratch.arena, U64, 1);
ctx->tls_base[0] = d_query_cached_tls_base_vaddr_from_process_root_rip(process, tls_root_vaddr, rip_vaddr);
ctx->cfa = d_query_cached_cfa_from_thread_unwind(thread, unwind_count);
}
e_select_interpret_ctx(interpret_ctx, eval_dbg_infos_primary->rdi, rip_voff);