@@ -20,10 +20,6 @@ struct ruby_procs_t {
2020// NOTE: the maximum size stack is FRAMES_PER_WALK_RUBY_STACK * calls to tail_call().
2121#define FRAMES_PER_WALK_RUBY_STACK 32
2222
23- // The maximum number of JIT frames to unwind via frame pointers.
24- // YJIT creates one native frame per JIT entry (not per Ruby method),
25- // so in practice there is typically only 1 (occasionally 2 for nested entries).
26- #define MAX_JIT_FP_FRAMES 4
2723// When resolving a CME, we need to traverse environment pointers until we
2824// find IMEMO_MENT. Since we can't do a while loop, we have to bound this
2925// the max encountered in experimentation on a production rails app is 6.
@@ -34,7 +30,7 @@ struct ruby_procs_t {
3430// This increases insn for the kernel verifier: all code in the ep check "loop"
3531// is M*N for instruction checks, so be extra sensitive about additions there.
3632// If we get ERR_RUBY_READ_CME_MAX_EP regularly, we may need to raise it.
37- #define MAX_EP_CHECKS 10
33+ #define MAX_EP_CHECKS 10
3834
3935// Constants related to reading a method entry
4036// https://github.com/ruby/ruby/blob/523857bfcb0f0cdfd1ed7faa09b9c59a0266e7e2/method.h#L118
@@ -456,55 +452,27 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
456452 record -> rubyUnwindState .cfunc_saved_frame = 0 ;
457453 }
458454
459- // If the CPU PC is in the JIT region, walk the native frame pointer chain through JIT frames .
460- // This follows the same pattern as the V8 unwinder (v8_tracer.ebpf.c): push each JIT frame,
461- // then use unwinder_unwind_frame_pointer() to advance PC/SP/ FP to the caller .
462- // YJIT creates one native FP frame per JIT entry, not per Ruby method, so there are
463- // typically only 1-2 frames to walk.
455+ // Detect if the CPU PC is in the JIT region.
456+ // When frame pointers are available, we keep the native unwind state in sync with
457+ // the Ruby VM stack by advancing the FP chain by one frame per loop iteration .
458+ // This handles both YJIT (1 JIT frame, exits after first iteration) and ZJIT
459+ // (1 JIT frame per iseq, 1:1 with CFPs, stays in sync throughout the walk) .
464460 //
465- // If frame_pointers_enabled is false (e.g. x86_64 without --yjit-perf), we push a single
466- // dummy JIT frame and skip FP walking -- the stack will be truncated at the Ruby VM frames
467- // but won't produce garbage from following an invalid FP chain.
468- if (
469- rubyinfo -> jit_start > 0 && record -> state .pc >= rubyinfo -> jit_start &&
470- record -> state .pc < rubyinfo -> jit_end ) {
471- if (rubyinfo -> frame_pointers_enabled ) {
472- // Walk the native FP chain through JIT frames, pushing each as a JIT frame
473- // so it can potentially be symbolized via perf maps later.
474- UNROLL for (int j = 0 ; j < MAX_JIT_FP_FRAMES ; j ++ )
475- {
476- ErrorCode jit_error =
477- push_ruby (& record -> state , trace , RUBY_FRAME_TYPE_JIT , (u64 )record -> state .pc , 0 , 0 );
478- if (jit_error ) {
479- return jit_error ;
480- }
461+ // When frame pointers are not available, we push a single dummy JIT frame and
462+ // set jit_detected to suppress native unwinding.
463+ bool in_jit = rubyinfo -> jit_start > 0 && record -> state .pc >= rubyinfo -> jit_start &&
464+ record -> state .pc < rubyinfo -> jit_end ;
481465
482- if (!unwinder_unwind_frame_pointer (& record -> state )) {
483- // FP chain broken, cannot continue
484- * next_unwinder = PROG_UNWIND_STOP ;
485- return ERR_OK ;
486- }
487-
488- // Check if we've left the JIT region
489- if (record -> state .pc < rubyinfo -> jit_start || record -> state .pc >= rubyinfo -> jit_end ) {
490- break ;
491- }
492- }
493- // After walking JIT frames, PC should be in rb_vm_exec or other native code.
494- // We must resolve the mapping for the new PC so that text_section_id/offset/bias
495- // are up to date. Without this, the native unwinder would try to use stale mapping
496- // info from the JIT region and fail with ERR_NATIVE_NO_PID_PAGE_MAPPING.
497- ErrorCode map_err = get_next_unwinder_after_native_frame (record , next_unwinder );
498- if (map_err ) {
499- return map_err ;
466+ if (in_jit ) {
467+ if (rubyinfo -> frame_pointers_enabled ) {
468+ // Push a leaf JIT frame with the raw machine PC for perf-map symbolization.
469+ ErrorCode jit_error =
470+ push_ruby (& record -> state , trace , RUBY_FRAME_TYPE_JIT , (u64 )record -> state .pc , 0 , 0 );
471+ if (jit_error ) {
472+ return jit_error ;
500473 }
501- // The resolved unwinder should be PROG_UNWIND_RUBY (since PC is in rb_vm_exec
502- // which is in interpreter_offsets) or PROG_UNWIND_NATIVE. Either way, we continue
503- // with the Ruby VM stack walk below and the mapping state is now correct for when
504- // we eventually hand off to the native unwinder.
505474 } else {
506475 // No frame pointers available: push a single dummy JIT frame.
507- // We cannot walk the FP chain so we will not be able to resume native unwinding.
508476 // Mark jit_detected so that cfuncs are pushed inline and end-of-stack uses
509477 // PROG_UNWIND_STOP instead of PROG_UNWIND_NATIVE.
510478 record -> rubyUnwindState .jit_detected = true;
@@ -513,18 +481,38 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
513481 if (jit_error ) {
514482 return jit_error ;
515483 }
484+ in_jit = false;
516485 }
517486 }
518487
519488 for (u32 i = 0 ; i < FRAMES_PER_WALK_RUBY_STACK ; ++ i ) {
489+ // Keep the native unwind state in sync: if the native PC is still in the JIT
490+ // region, advance it by one frame pointer to match the Ruby VM stack pop.
491+ // For YJIT this exits JIT on the first iteration. For ZJIT this pops one JIT
492+ // native frame per CFP, keeping the two stacks in lockstep.
493+ if (in_jit ) {
494+ if (!unwinder_unwind_frame_pointer (& record -> state )) {
495+ * next_unwinder = PROG_UNWIND_STOP ;
496+ return ERR_OK ;
497+ }
498+ if (record -> state .pc < rubyinfo -> jit_start || record -> state .pc >= rubyinfo -> jit_end ) {
499+ // Exited the JIT region. Resolve the mapping for the post-JIT PC so that
500+ // text_section_id/offset/bias are correct for native unwinding later.
501+ in_jit = false;
502+ ErrorCode map_err = get_next_unwinder_after_native_frame (record , next_unwinder );
503+ if (map_err ) {
504+ return map_err ;
505+ }
506+ }
507+ }
508+
520509 error = read_ruby_frame (record , rubyinfo , stack_ptr , next_unwinder );
521510 if (error != ERR_OK )
522511 return error ;
523512
524513 if (last_stack_frame <= stack_ptr ) {
525514 // We have processed all frames in the Ruby VM and can stop here.
526- // If we walked through JIT frames via FP, the state is clean and native unwinding
527- // can continue. If JIT was detected without FP, the PC is still in the JIT region
515+ // If JIT was detected without FP, the PC is still in the JIT region
528516 // and native unwinding would fail, so we stop.
529517 * next_unwinder = record -> rubyUnwindState .jit_detected ? PROG_UNWIND_STOP : PROG_UNWIND_NATIVE ;
530518 goto save_state ;
0 commit comments