diff --git a/README.md b/README.md index d77d0fef..5a47515a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/doc/demo_async_stepout.gif b/doc/demo_async_stepout.gif new file mode 100644 index 00000000..dad35155 Binary files /dev/null and b/doc/demo_async_stepout.gif differ diff --git a/src/debugger/async/mod.rs b/src/debugger/async/mod.rs index 9731a24d..f6ef87df 100644 --- a/src/debugger/async/mod.rs +++ b/src/debugger/async/mod.rs @@ -105,6 +105,11 @@ enum AsyncStepResult { ty: WatchpointHitType, quiet: bool, }, + #[allow(unused)] + Breakpoint { + pid: Pid, + addr: RelocatedAddress, + }, } impl AsyncStepResult { @@ -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) => { @@ -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)); + } _ => {} } @@ -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 { + 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, }) } } diff --git a/src/debugger/async/tokio/mod.rs b/src/debugger/async/tokio/mod.rs index e042a148..db5abc9b 100644 --- a/src/debugger/async/tokio/mod.rs +++ b/src/debugger/async/tokio/mod.rs @@ -1,6 +1,6 @@ pub mod park; pub mod task; -mod types; +pub mod types; pub mod worker; use crate::{version::Version, version_specialized}; diff --git a/src/debugger/async/tokio/types.rs b/src/debugger/async/tokio/types.rs index 2c2a0ea4..318ecb33 100644 --- a/src/debugger/async/tokio/types.rs +++ b/src/debugger/async/tokio/types.rs @@ -51,3 +51,10 @@ impl From for u64 { pub fn header_type_name() -> &'static str { "NonNull" } + +#[inline(always)] +pub fn complete_flag() -> usize { + // taken from tokio sources + const COMPLETE: usize = 0b0010; + COMPLETE +} diff --git a/src/ui/command/async.rs b/src/ui/command/async.rs index 060ef521..bc93d646 100644 --- a/src/ui/command/async.rs +++ b/src/ui/command/async.rs @@ -13,6 +13,7 @@ pub enum Command { pub enum AsyncCommandResult<'a> { StepOver, + StepOut, ShortBacktrace(AsyncBacktrace), FullBacktrace(AsyncBacktrace), CurrentTask(AsyncBacktrace, Option<&'a str>), @@ -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) } diff --git a/src/ui/console/editor.rs b/src/ui/console/editor.rs index c4aca394..a63edcce 100644 --- a/src/ui/console/editor.rs +++ b/src/ui/console/editor.rs @@ -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, @@ -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 { diff --git a/src/ui/console/help.rs b/src/ui/console/help.rs index 64cc816d..7669d7b5 100644 --- a/src/ui/console/help.rs +++ b/src/ui/console/help.rs @@ -286,6 +286,7 @@ async backtrace all - show state of async workers and blocking threads, show inf async task - 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 = "\ diff --git a/src/ui/console/mod.rs b/src/ui/console/mod.rs index eccfe7b1..c6cd007c 100644 --- a/src/ui/console/mod.rs +++ b/src/ui/console/mod.rs @@ -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) { diff --git a/tests/integration/test_async.py b/tests/integration/test_async.py index 0ca359ce..891f493e 100644 --- a/tests/integration/test_async.py +++ b/tests/integration/test_async.py @@ -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') +