Skip to content

Commit

Permalink
feat(async): introduce new async stepout command
Browse files Browse the repository at this point in the history
  • Loading branch information
godzie44 committed Jan 12, 2025
1 parent 7c30ff0 commit b418383
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 28 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,9 @@ An async step works like a usual step (step, stepover, stepout) but in the conte
[demo async stepover](https://github.com/godzie44/BugStalker/blob/master/doc/demo_async_bt.gif)
- `async stepover` - step a program, stepping over subroutine (function) calls, ends if task going into a completed state (alias: `async next`).

[demo async stepout](https://github.com/godzie44/BugStalker/blob/master/doc/demo_async_stepout.gif)
- `async stepout` - execute the program until the current task moves into the completed state (alias: `async finish`).


## Other commands

Expand Down
Binary file added doc/demo_async_stepout.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
171 changes: 146 additions & 25 deletions src/debugger/async/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ enum AsyncStepResult {
ty: WatchpointHitType,
quiet: bool,
},
#[allow(unused)]
Breakpoint {
pid: Pid,
addr: RelocatedAddress,
},
}

impl AsyncStepResult {
Expand Down Expand Up @@ -368,17 +373,23 @@ impl Debugger {
.map(|_| ())
})?;

macro_rules! clear {
() => {
to_delete.into_iter().try_for_each(|addr| {
self.remove_breakpoint(Address::Relocated(addr)).map(|_| ())
})?;
if let Some(wp) = waiter_wp {
self.remove_watchpoint_by_addr(wp.address)?;
}
};
}

loop {
let stop_reason = self.continue_execution()?;
// hooks already called at [`Self::continue_execution`], so use `quite` opt
match stop_reason {
super::StopReason::SignalStop(_, sign) => {
to_delete.into_iter().try_for_each(|addr| {
self.remove_breakpoint(Address::Relocated(addr)).map(|_| ())
})?;
if let Some(wp) = waiter_wp {
self.remove_watchpoint_by_addr(wp.address)?;
}
clear!();
return Ok(AsyncStepResult::signal_interrupt_quiet(sign));
}
super::StopReason::Watchpoint(pid, current_pc, ty) => {
Expand All @@ -392,27 +403,23 @@ impl Debugger {
};

if is_tmp_wp {
// taken from tokio sources
const COMPLETE: usize = 0b0010;
let (value, _) = task_header_state_value_and_ptr(self, task_ptr)?;

if value & COMPLETE == COMPLETE {
if value & tokio::types::complete_flag() == tokio::types::complete_flag() {
task_completed = true;
break;
} else {
continue;
}
} else {
to_delete.into_iter().try_for_each(|addr| {
self.remove_breakpoint(Address::Relocated(addr)).map(|_| ())
})?;
if let Some(wp) = waiter_wp {
self.remove_watchpoint_by_addr(wp.address)?;
}

clear!();
return Ok(AsyncStepResult::wp_interrupt_quite(pid, current_pc, ty));
}
}
super::StopReason::DebugeeExit(code) => {
clear!();
return Err(ProcessExit(code));
}
_ => {}
}

Expand Down Expand Up @@ -450,23 +457,137 @@ impl Debugger {
// wait until next break are hits
}

to_delete
.into_iter()
.try_for_each(|addr| self.remove_breakpoint(Address::Relocated(addr)).map(|_| ()))?;
clear!();
self.expl_ctx_update_location()?;
Ok(AsyncStepResult::Done {
task_id,
completed: task_completed,
})
}

/// Wait for current task ends.
pub fn async_step_out(&mut self) -> Result<(), Error> {
disable_when_not_stared!(self);
self.expl_ctx_restore_frame()?;

if let Some(wp) = waiter_wp {
self.remove_watchpoint_by_addr(wp.address)?;
match self.step_out_task()? {
AsyncStepResult::Done { task_id, completed } => {
self.execute_on_async_step_hook(task_id, completed)?
}
AsyncStepResult::SignalInterrupt { signal, quiet } if !quiet => {
self.hooks.on_signal(signal);
}
AsyncStepResult::WatchpointInterrupt {
pid,
addr,
ref ty,
quiet,
} if !quiet => self.execute_on_watchpoint_hook(pid, addr, ty)?,
_ => {}
};

Ok(())
}

/// Do step out from current task.
/// Returns [`StepResult::SignalInterrupt`] if step is interrupted by a signal,
/// [`StepResult::WatchpointInterrupt`] if step is interrupted by a watchpoint,
/// or [`StepResult::Done`] if step done or task completed.
///
/// **! change exploration context**
fn step_out_task(&mut self) -> Result<AsyncStepResult, Error> {
let async_bt = self.async_backtrace()?;
let current_task = async_bt
.current_task()
.ok_or(AsyncError::NoCurrentTaskFound)?;
let task_id = current_task.task_id;
let task_ptr = current_task.raw_ptr;

let (_, state_ptr) = task_header_state_value_and_ptr(self, task_ptr)?;
let state_addr = RelocatedAddress::from(state_ptr);
let wp = self
.set_watchpoint_on_memory(
state_addr,
BreakSize::Bytes8,
BreakCondition::DataWrites,
true,
)?
.to_owned();

// ignore all breakpoint until step ends
self.breakpoints
.active_breakpoints()
.iter()
.for_each(|brkpt| {
_ = brkpt.disable();
});

macro_rules! clear {
() => {
self.breakpoints
.active_breakpoints()
.iter()
.for_each(|brkpt| {
_ = brkpt.enable();
});
self.remove_watchpoint_by_addr(wp.address)?;
};
}

if self.debugee.is_exited() {
// todo add exit code here
return Err(ProcessExit(0));
loop {
let stop_reason = self.continue_execution()?;
// hooks already called at [`Self::continue_execution`], so use `quite` opt
match stop_reason {
super::StopReason::SignalStop(_, sign) => {
clear!();
return Ok(AsyncStepResult::signal_interrupt_quiet(sign));
}
super::StopReason::Watchpoint(pid, current_pc, ty) => {
let is_tmp_wp = if let WatchpointHitType::DebugRegister(ref reg) = ty {
self.watchpoints
.all()
.iter()
.any(|wp| wp.register() == Some(*reg) && wp.is_temporary())
} else {
false
};

if is_tmp_wp {
let (value, _) = task_header_state_value_and_ptr(self, task_ptr)?;

if value & tokio::types::complete_flag() == tokio::types::complete_flag() {
break;
} else {
continue;
}
} else {
self.remove_watchpoint_by_addr(wp.address)?;
return Ok(AsyncStepResult::wp_interrupt_quite(pid, current_pc, ty));
}
}
super::StopReason::DebugeeExit(code) => {
clear!();
return Err(ProcessExit(code));
}
super::StopReason::Breakpoint(_, _) => {
continue;
}
super::debugee::tracer::StopReason::NoSuchProcess(_) => {
clear!();
debug_assert!(false, "unreachable error `NoSuchProcess`");
return Err(ProcessExit(0));
}
super::debugee::tracer::StopReason::DebugeeStart => {
unreachable!()
}
}
}

clear!();
self.expl_ctx_update_location()?;
Ok(AsyncStepResult::Done {
task_id,
completed: task_completed,
completed: true,
})
}
}
2 changes: 1 addition & 1 deletion src/debugger/async/tokio/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pub mod park;
pub mod task;
mod types;
pub mod types;
pub mod worker;

use crate::{version::Version, version_specialized};
Expand Down
7 changes: 7 additions & 0 deletions src/debugger/async/tokio/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,10 @@ impl From<TaskIdValue> for u64 {
pub fn header_type_name() -> &'static str {
"NonNull<tokio::runtime::task::core::Header>"
}

#[inline(always)]
pub fn complete_flag() -> usize {
// taken from tokio sources
const COMPLETE: usize = 0b0010;
COMPLETE
}
6 changes: 5 additions & 1 deletion src/ui/command/async.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub enum Command {

pub enum AsyncCommandResult<'a> {
StepOver,
StepOut,
ShortBacktrace(AsyncBacktrace),
FullBacktrace(AsyncBacktrace),
CurrentTask(AsyncBacktrace, Option<&'a str>),
Expand Down Expand Up @@ -43,7 +44,10 @@ impl<'a> Handler<'a> {
self.dbg.async_step_over()?;
AsyncCommandResult::StepOver
}
Command::StepOut => todo!(),
Command::StepOut => {
self.dbg.async_step_out()?;
AsyncCommandResult::StepOut
}
};
Ok(result)
}
Expand Down
5 changes: 4 additions & 1 deletion src/ui/console/editor.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::ui::command::parser::{
ARG_ALL_KEY, ARG_COMMAND, ASYNC_COMMAND, ASYNC_COMMAND_BACKTRACE_SUBCOMMAND,
ASYNC_COMMAND_BACKTRACE_SUBCOMMAND_SHORT, ASYNC_COMMAND_STEP_OVER_SUBCOMMAND,
ASYNC_COMMAND_BACKTRACE_SUBCOMMAND_SHORT, ASYNC_COMMAND_STEP_OUT_SUBCOMMAND,
ASYNC_COMMAND_STEP_OUT_SUBCOMMAND_SHORT, ASYNC_COMMAND_STEP_OVER_SUBCOMMAND,
ASYNC_COMMAND_STEP_OVER_SUBCOMMAND_SHORT, ASYNC_COMMAND_TASK_SUBCOMMAND,
BACKTRACE_ALL_SUBCOMMAND, BACKTRACE_COMMAND, BACKTRACE_COMMAND_SHORT, BREAK_COMMAND,
BREAK_COMMAND_SHORT, CONTINUE_COMMAND, CONTINUE_COMMAND_SHORT, FRAME_COMMAND,
Expand Down Expand Up @@ -427,6 +428,8 @@ pub fn create_editor(
ASYNC_COMMAND_BACKTRACE_SUBCOMMAND_SHORT.to_string() + " all",
ASYNC_COMMAND_STEP_OVER_SUBCOMMAND.to_string(),
ASYNC_COMMAND_STEP_OVER_SUBCOMMAND_SHORT.to_string(),
ASYNC_COMMAND_STEP_OUT_SUBCOMMAND.to_string(),
ASYNC_COMMAND_STEP_OUT_SUBCOMMAND_SHORT.to_string(),
],
},
CommandHint {
Expand Down
1 change: 1 addition & 0 deletions src/ui/console/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ async backtrace all - show state of async workers and blocking threads, show inf
async task <async_fn_regex> - show active task (active task means a task that is running on the thread that is currently in focus) if `async_fn_regex` parameter is empty,
or show task list with async functions matched by regular expression
async next, async stepover - perform a stepover within the context of the current task. If the task moves into a completed state, the application will stop too
async finish, async stepout - execute the program until the current task moves into the completed state
";

pub const HELP_TUI: &str = "\
Expand Down
3 changes: 3 additions & 0 deletions src/ui/console/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,9 @@ impl AppLoop {
AsyncCommandResult::StepOver => {
_ = self.update_completer_variables();
}
AsyncCommandResult::StepOut => {
_ = self.update_completer_variables();
}
}
}
Command::Oracle(name, subcmd) => match self.debugger.get_oracle(&name) {
Expand Down
12 changes: 12 additions & 0 deletions tests/integration/test_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,15 @@ def test_step_over(self):
self.debugger.cmd_re('async next', r'Task id: \d', r'34 }')
self.debugger.cmd_re('async next', r'Task #\d completed, stopped')

def test_step_out(self):
"""Do async step out"""

for binary in tokio_binaries():
self.debugger = Debugger(path=f"./examples/tokio_vars/{binary}/target/debug/{binary}")
self.debugger.cmd('break main.rs:18')
self.debugger.cmd('break main.rs:28')
self.debugger.cmd('run', 'Hit breakpoint 1')
self.debugger.cmd_re('async stepout', r'Task #\d completed, stopped')
self.debugger.cmd('continue', 'Hit breakpoint 2')
self.debugger.cmd_re('async stepout', r'Task #\d completed, stopped')

0 comments on commit b418383

Please sign in to comment.