diff --git a/.vscode/launch.json b/.vscode/launch.json index 1f75677f26..ee01283389 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -100,6 +100,27 @@ "help", ], "cwd": "${workspaceFolder}/debugger" + }, + { + "type": "lldb", + "request": "launch", + "name": "display debug subcommand help text", + "cargo": { + "args": [ + "build", + "--bin=probe-rs-debugger", + "--package=probe-rs-debugger" + ], + "filter": { + "name": "probe-rs-debugger", + "kind": "bin" + } + }, + "args": [ + "help", + "debug", + ], + "cwd": "${workspaceFolder}/debugger" } ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3289bcbe87..10678ff4dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Debugger: Add support for DAP Requests (ReadMemory, WriteMemory, Evaluate & SetVariable) (#1035) - Debugger: Add support for DAP Requests (Disassemble & SetInstructionBreakpoints) (#1049) - Debugger: Add support for stepping at 'statement' level, plus 'step in', 'step out' (#1056) +- Debugger: Add support for navigating and monitoring SVD Peripheral Registers. (#1072) - Added GD32F3x0 series support (#1079) ### Changed @@ -44,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated STM32H7 series yaml to support newly released chips. (#1011) - Debugger: Removed the CLI mode, in favour of `probe-rs-cli` which has richer functionality. (#1041) - Renamed `Probe::speed` to `Probe::speed_khz`. +- Debugger: Changes to DAP Client `launch.json` to prepare for WIP multi-core support. (#1072) - `ram_download` example now uses clap syntax. ### Fixed diff --git a/cli/src/debugger.rs b/cli/src/debugger.rs index aac461e43f..99a689e84c 100644 --- a/cli/src/debugger.rs +++ b/cli/src/debugger.rs @@ -392,7 +392,7 @@ impl DebugCli { println!( "{}: {} = {}", child.name, - child.type_name.display(), + child.type_name, child.get_value(local_variable_cache) ); } diff --git a/debugger/Cargo.toml b/debugger/Cargo.toml index d540012821..600e338d19 100644 --- a/debugger/Cargo.toml +++ b/debugger/Cargo.toml @@ -39,6 +39,8 @@ schemafy = "^0.6" chrono = { version = "0.4", features = ["serde"] } goblin = "0.5.1" base64 = "0.13" +svd-parser = "0.13.1" +svd-rs = { version = "0.13.1", features = ["derive-from"] } [dev-dependencies] insta = "1.8.0" diff --git a/debugger/src/debug_adapter.rs b/debugger/src/debug_adapter/dap_adapter.rs similarity index 81% rename from debugger/src/debug_adapter.rs rename to debugger/src/debug_adapter/dap_adapter.rs index 23e9852409..122347d9d6 100644 --- a/debugger/src/debug_adapter.rs +++ b/debugger/src/debug_adapter/dap_adapter.rs @@ -1,24 +1,22 @@ -use crate::dap_types; -use crate::debugger::BreakpointType; -use crate::debugger::ConsoleLog; -use crate::debugger::CoreData; -use crate::DebuggerError; +use crate::{ + debug_adapter::{dap_types, protocol::ProtocolAdapter}, + debugger::{configuration::ConsoleLog, core_data::CoreHandle, session_data::BreakpointType}, + DebuggerError, +}; use anyhow::{anyhow, Result}; use dap_types::*; use num_traits::Zero; use parse_int::parse; -use probe_rs::debug::Registers; -use probe_rs::debug::SourceLocation; -use probe_rs::debug::SteppingMode; -use probe_rs::debug::VariableLocation; -use probe_rs::debug::{VariableCache, VariableName}; -use probe_rs::{debug::ColumnType, CoreStatus, HaltReason, MemoryInterface}; +use probe_rs::{ + debug::{ + ColumnType, Registers, SourceLocation, SteppingMode, VariableCache, VariableName, + VariableNodeType, + }, + CoreStatus, HaltReason, MemoryInterface, +}; use probe_rs_cli_util::rtt; use serde::{de::DeserializeOwned, Serialize}; -use std::string::ToString; -use std::{convert::TryInto, path::Path, str, thread, time::Duration}; - -use crate::protocol::ProtocolAdapter; +use std::{convert::TryInto, path::Path, str, string::ToString, thread, time::Duration}; /// Progress ID used for progress reporting when the debug adapter protocol is used. type ProgressId = i64; @@ -55,8 +53,8 @@ impl DebugAdapter

{ } } - pub(crate) fn status(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { - let status = match core_data.target_core.status() { + pub(crate) fn status(&mut self, target_core: &mut CoreHandle, request: Request) -> Result<()> { + let status = match target_core.core.status() { Ok(status) => { self.last_known_status = status; status @@ -72,9 +70,9 @@ impl DebugAdapter

{ } }; if status.is_halted() { - let pc = core_data - .target_core - .read_core_reg(core_data.target_core.registers().program_counter()); + let pc = target_core + .core + .read_core_reg(target_core.core.registers().program_counter()); match pc { Ok(pc) => self.send_response( request, @@ -93,12 +91,12 @@ impl DebugAdapter

{ } } - pub(crate) fn pause(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { + pub(crate) fn pause(&mut self, target_core: &mut CoreHandle, request: Request) -> Result<()> { // let args: PauseArguments = get_arguments(&request)?; - match core_data.target_core.halt(Duration::from_millis(500)) { + match target_core.core.halt(Duration::from_millis(500)) { Ok(cpu_info) => { - let new_status = match core_data.target_core.status() { + let new_status = match target_core.core.status() { Ok(new_status) => new_status, Err(error) => { self.send_response::<()>(request, Err(DebuggerError::ProbeRs(error)))?; @@ -109,7 +107,7 @@ impl DebugAdapter

{ let event_body = Some(StoppedEventBody { reason: "pause".to_owned(), description: Some(self.last_known_status.short_long_status().1.to_owned()), - thread_id: Some(core_data.target_core.id() as i64), + thread_id: Some(target_core.core.id() as i64), preserve_focus_hint: Some(false), text: None, all_threads_stopped: Some(false), // TODO: Implement multi-core logic here @@ -133,7 +131,11 @@ impl DebugAdapter

{ } } - pub(crate) fn read_memory(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { + pub(crate) fn read_memory( + &mut self, + target_core: &mut CoreHandle, + request: Request, + ) -> Result<()> { let arguments: ReadMemoryArguments = match get_arguments(&request) { Ok(arguments) => arguments, Err(error) => return self.send_response::<()>(request, Err(error)), @@ -164,7 +166,7 @@ impl DebugAdapter

{ let mut num_bytes_unread = arguments.count as usize; let mut buff = vec![]; while num_bytes_unread > 0 { - if let Ok(good_byte) = core_data.target_core.read_word_8(address) { + if let Ok(good_byte) = target_core.core.read_word_8(address) { buff.push(good_byte); address += 1; num_bytes_unread -= 1; @@ -195,7 +197,7 @@ impl DebugAdapter

{ pub(crate) fn write_memory( &mut self, - core_data: &mut CoreData, + target_core: &mut CoreHandle, request: Request, ) -> Result<()> { let arguments: WriteMemoryArguments = match get_arguments(&request) { @@ -236,8 +238,8 @@ impl DebugAdapter

{ ); } }; - match core_data - .target_core + match target_core + .core .write_8(address, &data_bytes) .map_err(DebuggerError::ProbeRs) { @@ -265,7 +267,11 @@ impl DebugAdapter

{ /// Evaluates the given expression in the context of the top most stack frame. /// The expression has access to any variables and arguments that are in scope. - pub(crate) fn evaluate(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { + pub(crate) fn evaluate( + &mut self, + target_core: &mut CoreHandle, + request: Request, + ) -> Result<()> { // TODO: When variables appear in the `watch` context, they will not resolve correctly after a 'step' function. Consider doing the lazy load for 'either/or' of Variables vs. Evaluate let arguments: EvaluateArguments = match get_arguments(&request) { @@ -284,18 +290,19 @@ impl DebugAdapter

{ variables_reference: 0_i64, }; - // The Variables request always returns a 'evaluate_name' = 'name', this means that the expression will always be the variable name we are looking for. + // The Variables request sometimes returns the variable name, and other times the variable id, so this expression will be tested to determine if it is an id or not. let expression = arguments.expression.clone(); // Make sure we have a valid StackFrame if let Some(stack_frame) = match arguments.frame_id { - Some(frame_id) => core_data + Some(frame_id) => target_core + .core_data .stack_frames .iter_mut() .find(|stack_frame| stack_frame.id == frame_id), None => { // Use the current frame_id - core_data.stack_frames.first_mut() + target_core.core_data.stack_frames.first_mut() } } { // Always search the registers first, because we don't have a VariableCache for them. @@ -319,14 +326,28 @@ impl DebugAdapter

{ let mut variable_cache: Option<&mut VariableCache> = None; // Search through available caches and stop as soon as the variable is found #[allow(clippy::manual_flatten)] - for stack_frame_variable_cache in [ + for variable_cache_entry in [ stack_frame.local_variables.as_mut(), stack_frame.static_variables.as_mut(), + target_core + .core_data + .core_peripherals + .as_mut() + .map(|core_peripherals| &mut core_peripherals.svd_variable_cache), ] { - if let Some(search_cache) = stack_frame_variable_cache { - variable = search_cache - .get_variable_by_name(&VariableName::Named(expression.clone())); - if variable.is_some() { + if let Some(search_cache) = variable_cache_entry { + if let Ok(expression_as_key) = expression.parse::() { + variable = search_cache.get_variable_by_key(expression_as_key); + } else { + variable = search_cache + .get_variable_by_name(&VariableName::Named(expression.clone())); + } + if let Some(variable) = &mut variable { + if variable.variable_node_type == VariableNodeType::SvdRegister + || variable.variable_node_type == VariableNodeType::SvdField + { + variable.extract_value(&mut target_core.core, search_cache) + } variable_cache = Some(search_cache); break; } @@ -340,10 +361,7 @@ impl DebugAdapter

{ indexed_child_variables_cnt, ) = self.get_variable_reference(&variable, variable_cache); response_body.indexed_variables = Some(indexed_child_variables_cnt); - - if let VariableLocation::Address(address) = variable.memory_location { - response_body.memory_reference = Some(format!("{:#010x}", address)); - } + response_body.memory_reference = Some(format!("{}", variable.memory_location)); response_body.named_variables = Some(named_child_variables_cnt); response_body.result = variable.get_value(variable_cache); response_body.type_ = Some(format!("{:?}", variable.type_name)); @@ -360,7 +378,7 @@ impl DebugAdapter

{ /// Set the variable with the given name in the variable container to a new value. pub(crate) fn set_variable( &mut self, - core_data: &mut CoreData, + target_core: &mut CoreHandle, request: Request, ) -> Result<()> { let arguments: SetVariableArguments = match get_arguments(&request) { @@ -383,7 +401,10 @@ impl DebugAdapter

{ let parent_key = arguments.variables_reference; let new_value = arguments.value.clone(); - match core_data + //TODO: Check for, and prevent SVD Peripheral/Register/Field values from being updated, until such time as we can do it safely. + + match target_core + .core_data .stack_frames .iter_mut() .find(|stack_frame| stack_frame.id == parent_key) @@ -417,7 +438,7 @@ impl DebugAdapter

{ // The parent_key refers to a local or static variable in one of the in-scope StackFrames. let mut cache_variable: Option = None; let mut variable_cache: Option<&mut VariableCache> = None; - for search_frame in core_data.stack_frames.iter_mut() { + for search_frame in target_core.core_data.stack_frames.iter_mut() { if let Some(search_cache) = &mut search_frame.local_variables { if let Some(search_variable) = search_cache .get_variable_by_name_and_parent(&variable_name, Some(parent_key)) @@ -443,7 +464,7 @@ impl DebugAdapter

{ { // We have found the variable that needs to be updated. match cache_variable.update_value( - &mut core_data.target_core, + &mut target_core.core, variable_cache, new_value.clone(), ) { @@ -492,10 +513,10 @@ impl DebugAdapter

{ pub(crate) fn restart( &mut self, - core_data: &mut CoreData, + target_core: &mut CoreHandle, request: Option, ) -> Result<()> { - match core_data.target_core.halt(Duration::from_millis(500)) { + match target_core.core.halt(Duration::from_millis(500)) { Ok(_) => {} Err(error) => { if let Some(request) = request { @@ -511,12 +532,12 @@ impl DebugAdapter

{ // Different code paths if we invoke this from a request, versus an internal function. if let Some(request) = request { - match core_data.target_core.reset() { + match target_core.core.reset() { Ok(_) => { self.last_known_status = CoreStatus::Running; let event_body = Some(ContinuedEventBody { all_threads_continued: Some(false), // TODO: Implement multi-core logic here - thread_id: core_data.target_core.id() as i64, + thread_id: target_core.core.id() as i64, }); self.send_event("continued", event_body) @@ -531,10 +552,7 @@ impl DebugAdapter

{ } else { // The DAP Client will always do a `reset_and_halt`, and then will consider `halt_after_reset` value after the `configuration_done` request. // Otherwise the probe will run past the `main()` before the DAP Client has had a chance to set breakpoints in `main()`. - match core_data - .target_core - .reset_and_halt(Duration::from_millis(500)) - { + match target_core.core.reset_and_halt(Duration::from_millis(500)) { Ok(_) => { if let Some(request) = request { return self.send_response::<()>(request, Ok(None)); @@ -549,7 +567,7 @@ impl DebugAdapter

{ .1 .to_string(), ), - thread_id: Some(core_data.target_core.id() as i64), + thread_id: Some(target_core.core.id() as i64), preserve_focus_hint: None, text: None, all_threads_stopped: Some(false), // TODO: Implement multi-core logic here @@ -588,7 +606,7 @@ impl DebugAdapter

{ /// - Any other status will send and error. pub(crate) fn configuration_done( &mut self, - _core_data: &mut CoreData, + _core_data: &mut CoreHandle, request: Request, ) -> Result<()> { self.send_response::<()>(request, Ok(None)) @@ -596,7 +614,7 @@ impl DebugAdapter

{ pub(crate) fn set_breakpoints( &mut self, - core_data: &mut CoreData, + target_core: &mut CoreHandle, request: Request, ) -> Result<()> { let args: SetBreakpointsArguments = match get_arguments(&request) { @@ -617,7 +635,7 @@ impl DebugAdapter

{ let source_path = args.source.path.as_ref().map(Path::new); // Always clear existing breakpoints before setting new ones. The DAP Specification doesn't make allowances for deleting and setting individual breakpoints. - match core_data.clear_breakpoints(BreakpointType::SourceBreakpoint) { + match target_core.clear_breakpoints(BreakpointType::SourceBreakpoint) { Ok(_) => {} Err(error) => { return self.send_response::<()>( @@ -651,7 +669,7 @@ impl DebugAdapter

{ }; let (verified_breakpoint, reason_msg) = if let Some(source_path) = source_path { - match core_data.debug_info.get_breakpoint_location( + match target_core.core_data.debug_info.get_breakpoint_location( source_path, requested_breakpoint_line, requested_breakpoint_column, @@ -660,7 +678,7 @@ impl DebugAdapter

{ if let Some(breakpoint_address) = valid_breakpoint_location.first_halt_address { - match core_data.set_breakpoint( + match target_core.set_breakpoint( breakpoint_address as u32, BreakpointType::SourceBreakpoint, ) { @@ -738,7 +756,7 @@ impl DebugAdapter

{ pub(crate) fn set_instruction_breakpoints( &mut self, - core_data: &mut CoreData, + target_core: &mut CoreHandle, request: Request, ) -> Result<()> { let arguments: SetInstructionBreakpointsArguments = match get_arguments(&request) { @@ -758,7 +776,7 @@ impl DebugAdapter

{ let mut created_breakpoints: Vec = Vec::new(); // Always clear existing breakpoints before setting new ones. - match core_data.clear_breakpoints(BreakpointType::InstructionBreakpoint) { + match target_core.clear_breakpoints(BreakpointType::InstructionBreakpoint) { Ok(_) => {} Err(error) => log::warn!("Failed to clear instruction breakpoints. {}", error), } @@ -787,7 +805,7 @@ impl DebugAdapter

{ requested_breakpoint.instruction_reference.parse() } { - match core_data + match target_core .set_breakpoint(memory_reference, BreakpointType::InstructionBreakpoint) { Ok(_) => { @@ -795,7 +813,8 @@ impl DebugAdapter

{ breakpoint_response.instruction_reference = Some(format!("{:#010x}", memory_reference)); // Try to resolve the source location for this breakpoint. - match core_data + match target_core + .core_data .debug_info .get_source_location(memory_reference as u64) { @@ -845,13 +864,13 @@ impl DebugAdapter

{ self.send_response(request, Ok(Some(instruction_breakpoint_body))) } - pub(crate) fn threads(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { + pub(crate) fn threads(&mut self, target_core: &mut CoreHandle, request: Request) -> Result<()> { // TODO: Implement actual thread resolution. For now, we just use the core id as the thread id. let mut threads: Vec = vec![]; match self.last_known_status { CoreStatus::Unknown => { // We are probably here because the `configuration_done` request just happened, so we can make sure the client and debugger are in synch. - match core_data.target_core.status() { + match target_core.core.status() { Ok(core_status) => { self.last_known_status = core_status; // Make sure the DAP Client and the DAP Server are in sync with the status of the core. @@ -864,7 +883,7 @@ impl DebugAdapter

{ description: Some( core_status.short_long_status().1.to_string(), ), - thread_id: Some(core_data.target_core.id() as i64), + thread_id: Some(target_core.core.id() as i64), preserve_focus_hint: None, text: None, all_threads_stopped: Some(false), // TODO: Implement multi-core logic here @@ -873,15 +892,15 @@ impl DebugAdapter

{ self.send_event("stopped", event_body)?; } else { let single_thread = Thread { - id: core_data.target_core.id() as i64, - name: core_data.target_name.clone(), + id: target_core.core.id() as i64, + name: target_core.core_data.target_name.clone(), }; threads.push(single_thread); self.send_response( request.clone(), Ok(Some(ThreadsResponseBody { threads })), )?; - return self.r#continue(core_data, request); + return self.r#continue(target_core, request); } } } @@ -898,14 +917,14 @@ impl DebugAdapter

{ } CoreStatus::Halted(_) => { let single_thread = Thread { - id: core_data.target_core.id() as i64, - name: core_data.target_name.clone(), + id: target_core.core.id() as i64, + name: target_core.core_data.target_name.clone(), }; threads.push(single_thread); // We do the actual stack trace here, because VSCode sometimes sends multiple StackTrace requests, which lead to unnecessary unwind processing. // By doing it here, we do it once, and serve up the results when we get the StackTrace requests. - let regs = core_data.target_core.registers(); - let pc = match core_data.target_core.read_core_reg(regs.program_counter()) { + let regs = target_core.core.registers(); + let pc = match target_core.core.read_core_reg(regs.program_counter()) { Ok(pc) => pc, Err(error) => { return self @@ -914,11 +933,13 @@ impl DebugAdapter

{ }; log::debug!( "Updating the stack frame data for core #{}", - core_data.target_core.id() + target_core.core.id() ); - *core_data.stack_frames = core_data + + target_core.core_data.stack_frames = target_core + .core_data .debug_info - .unwind(&mut core_data.target_core, u64::from(pc))?; + .unwind(&mut target_core.core, u64::from(pc))?; } CoreStatus::Running | CoreStatus::LockedUp | CoreStatus::Sleeping => { return self.send_response::<()>( @@ -933,8 +954,12 @@ impl DebugAdapter

{ self.send_response(request, Ok(Some(ThreadsResponseBody { threads }))) } - pub(crate) fn stack_trace(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { - let _status = match core_data.target_core.status() { + pub(crate) fn stack_trace( + &mut self, + target_core: &mut CoreHandle, + request: Request, + ) -> Result<()> { + let _status = match target_core.core.status() { Ok(status) => { if !status.is_halted() { return self.send_response::<()>( @@ -966,9 +991,9 @@ impl DebugAdapter

{ if let Some(levels) = arguments.levels { if let Some(start_frame) = arguments.start_frame { // Determine the correct 'slice' of available [StackFrame]s to serve up ... - let total_frames = core_data.stack_frames.len() as i64; + let total_frames = target_core.core_data.stack_frames.len() as i64; - // We need to copy some parts of StackFrame so that we can re-use it later without references to core_data. + // We need to copy some parts of StackFrame so that we can re-use it later without references to target_core. struct PartialStackFrameData { id: i64, function_name: String, @@ -979,13 +1004,22 @@ impl DebugAdapter

{ let frame_set = if levels == 1 && start_frame == 0 { // Just the first frame - use the LHS of the split at `levels` - core_data.stack_frames.split_at(levels as usize).0 + target_core + .core_data + .stack_frames + .split_at(levels as usize) + .0 } else if total_frames <= 20 && start_frame >= 0 && start_frame <= total_frames { // When we have less than 20 frames - use the RHS of of the split at `start_frame` - core_data.stack_frames.split_at(start_frame as usize).1 + target_core + .core_data + .stack_frames + .split_at(start_frame as usize) + .1 } else if total_frames > 20 && start_frame + levels <= total_frames { - // When we have more than 20 frames - we can safely split twice. - core_data + // When we have more than 20 frames - we can safely split twice + target_core + .core_data .stack_frames .split_at(start_frame as usize) .1 @@ -993,7 +1027,11 @@ impl DebugAdapter

{ .0 } else if total_frames > 20 && start_frame + levels > total_frames { // The MS DAP spec may also ask for more frames than what we reported. - core_data.stack_frames.split_at(start_frame as usize).1 + target_core + .core_data + .stack_frames + .split_at(start_frame as usize) + .1 } else { return self.send_response::<()>( request, @@ -1092,7 +1130,7 @@ impl DebugAdapter

{ /// - static scope : Variables with `static` modifier /// - registers : The [probe_rs::Core::registers] for the target [probe_rs::CoreType] /// - local scope : Variables defined between start of current frame, and the current pc (program counter) - pub(crate) fn scopes(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { + pub(crate) fn scopes(&mut self, target_core: &mut CoreHandle, request: Request) -> Result<()> { let arguments: ScopesArguments = match get_arguments(&request) { Ok(arguments) => arguments, Err(error) => return self.send_response::<()>(request, Err(error)), @@ -1100,17 +1138,10 @@ impl DebugAdapter

{ let mut dap_scopes: Vec = vec![]; - log::trace!("Getting scopes for frame {}", arguments.frame_id,); - - if let Some(stack_frame) = core_data.get_stackframe(arguments.frame_id) { - if let Some(static_root_variable) = - stack_frame - .static_variables - .as_ref() - .and_then(|stack_frame| { - stack_frame - .get_variable_by_name_and_parent(&VariableName::StaticScopeRoot, None) - }) + if let Some(core_peripherals) = &mut target_core.core_data.core_peripherals { + if let Some(peripherals_root_variable) = core_peripherals + .svd_variable_cache + .get_variable_by_name_and_parent(&VariableName::PeripheralScopeRoot, None) { dap_scopes.push(Scope { line: None, @@ -1119,14 +1150,18 @@ impl DebugAdapter

{ end_line: None, expensive: true, // VSCode won't open this tree by default. indexed_variables: None, - name: "Static".to_string(), - presentation_hint: Some("statics".to_string()), + name: "Peripherals".to_string(), + presentation_hint: Some("registers".to_string()), named_variables: None, source: None, - variables_reference: static_root_variable.variable_key, + variables_reference: peripherals_root_variable.variable_key, }); - }; + } + }; + + log::trace!("Getting scopes for frame {}", arguments.frame_id,); + if let Some(stack_frame) = target_core.get_stackframe(arguments.frame_id) { dap_scopes.push(Scope { line: None, column: None, @@ -1142,6 +1177,30 @@ impl DebugAdapter

{ variables_reference: stack_frame.id, }); + if let Some(static_root_variable) = + stack_frame + .static_variables + .as_ref() + .and_then(|stack_frame| { + stack_frame + .get_variable_by_name_and_parent(&VariableName::StaticScopeRoot, None) + }) + { + dap_scopes.push(Scope { + line: None, + column: None, + end_column: None, + end_line: None, + expensive: true, // VSCode won't open this tree by default. + indexed_variables: None, + name: "Static".to_string(), + presentation_hint: Some("statics".to_string()), + named_variables: None, + source: None, + variables_reference: static_root_variable.variable_key, + }); + }; + if let Some(locals_root_variable) = stack_frame .local_variables @@ -1174,14 +1233,13 @@ impl DebugAdapter

{ }); }; } - self.send_response(request, Ok(Some(ScopesResponseBody { scopes: dap_scopes }))) } /// Attempt to extract disassembled source code to supply the instruction_count required. pub(crate) fn get_disassembled_source( &mut self, - core_data: &mut CoreData, + target_core: &mut CoreHandle, // The program_counter where our desired instruction range is based. memory_reference: i64, // The number of bytes offset from the memory reference. Can be zero. @@ -1191,8 +1249,10 @@ impl DebugAdapter

{ // The EXACT number of instructions to return in the result. instruction_count: i64, ) -> Result, DebuggerError> { - let instruction_offset_as_bytes = - (instruction_offset * core_data.debug_info.get_instruction_size() as i64) / 4 * 5; + let instruction_offset_as_bytes = (instruction_offset + * target_core.core_data.debug_info.get_instruction_size() as i64) + / 4 + * 5; // The vector we will use to return results. let mut assembly_lines: Vec = vec![]; @@ -1203,13 +1263,25 @@ impl DebugAdapter

{ // Control whether we need to read target memory in order to disassemble the next instruction. let mut read_more_bytes = true; - // The memory address for the next read from target memory. We have to manually adjust it to be word aligned. - let mut read_pointer = - (memory_reference + byte_offset + instruction_offset_as_bytes) as u32; - read_pointer = read_pointer - read_pointer % 4; + // The memory address for the next read from target memory. We have to manually adjust it to be word aligned, and make sure it doesn't underflow/overflow. + let mut read_pointer = if byte_offset.is_negative() { + Some(memory_reference.saturating_sub(byte_offset.abs()) as u32) + } else { + Some(memory_reference.saturating_add(byte_offset) as u32) + }; + read_pointer = if instruction_offset_as_bytes.is_negative() { + read_pointer.map(|rp| rp.saturating_sub(instruction_offset_as_bytes.abs() as u32)) + } else { + read_pointer.map(|rp| rp.saturating_add(instruction_offset_as_bytes as u32)) + }; // The memory address for the next instruction to be disassembled - let mut instruction_pointer = read_pointer as u64; + let mut instruction_pointer = if let Some(read_pointer) = read_pointer { + read_pointer as u64 + } else { + let error_message = format!("Unable to calculate starting address for disassembly request with memory reference:{:#010X}, byte offset:{:#010X}, and instruction offset:{:#010X}.", memory_reference, byte_offset, instruction_offset); + return Err(DebuggerError::Other(anyhow!(error_message))); + }; // We will only include source location data in a resulting instruction, if it is different from the previous one. let mut stored_source_location = None; @@ -1217,35 +1289,45 @@ impl DebugAdapter

{ // The MS DAP spec requires that we always have to return a fixed number of instructions. while assembly_lines.len() < instruction_count as usize { if read_more_bytes { - match core_data.target_core.read_word_32(read_pointer) { - Ok(new_word) => { - // Advance the read pointer for next time we need it. - read_pointer += 4; - // Update the code buffer. - for new_byte in new_word.to_le_bytes() { - code_buffer.push(new_byte); + if let Some(current_read_pointer) = read_pointer { + match target_core.core.read_word_32(current_read_pointer) { + Ok(new_word) => { + // Advance the read pointer for next time we need it. + read_pointer = if let Some(valid_read_pointer) = + current_read_pointer.checked_add(4) + { + Some(valid_read_pointer) + } else { + // If this happens, the next loop will generate "invalid instruction" records. + read_pointer = None; + continue; + }; + // Update the code buffer. + for new_byte in new_word.to_le_bytes() { + code_buffer.push(new_byte); + } + } + Err(_) => { + // If we can't read data at a given address, then create a "invalid instruction" record, and keep trying. + assembly_lines.push(dap_types::DisassembledInstruction { + address: format!("{:#010X}", current_read_pointer), + column: None, + end_column: None, + end_line: None, + instruction: "".to_string(), + instruction_bytes: None, + line: None, + location: None, + symbol: None, + }); + read_pointer = Some(current_read_pointer.saturating_add(4)); + continue; } - } - Err(_) => { - // If we can't read data at a given address, then create a "invalid instruction" record, and keep trying. - read_pointer += 4; - assembly_lines.push(dap_types::DisassembledInstruction { - address: format!("{:#010X}", read_pointer), - column: None, - end_column: None, - end_line: None, - instruction: "".to_string(), - instruction_bytes: None, - line: None, - location: None, - symbol: None, - }); - continue; } } } - match core_data + match target_core .capstone .disasm_count(&code_buffer, instruction_pointer as u64, 1) { @@ -1272,7 +1354,8 @@ impl DebugAdapter

{ let mut location = None; let mut line = None; let mut column = None; - if let Some(current_source_location) = core_data + if let Some(current_source_location) = target_core + .core_data .debug_info .get_source_location(instruction.address()) { if let Some(previous_source_location) = stored_source_location.clone() { @@ -1345,7 +1428,11 @@ impl DebugAdapter

{ /// - Reached the required number of instructions. /// - We encounter 'unreadable' memory on the target. /// - In this case, pad the results with, as the api requires, "implementation defined invalid instructions" - pub(crate) fn disassemble(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { + pub(crate) fn disassemble( + &mut self, + target_core: &mut CoreHandle, + request: Request, + ) -> Result<()> { let arguments: DisassembleArguments = match get_arguments(&request) { Ok(arguments) => arguments, Err(error) => return self.send_response::<()>(request, Err(error)), @@ -1358,7 +1445,7 @@ impl DebugAdapter

{ arguments.memory_reference.parse() } { match self.get_disassembled_source( - core_data, + target_core, memory_reference as i64, arguments.offset.unwrap_or(0_i64), arguments.instruction_offset.unwrap_or(0_i64), @@ -1385,18 +1472,87 @@ impl DebugAdapter

{ } } - pub(crate) fn variables(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { + /// The MS DAP Specification only gives us the unique reference of the variable, and does not tell us which StackFrame it belongs to, nor does it specify if this variable is in the local, register or static scope. Unfortunately this means we have to search through all the available [VariableCache]'s until we find it. To minimize the impact of this, we will search in the most 'likely' places first (first stack frame's locals, then statics, then registers, then move to next stack frame, and so on ...) + pub(crate) fn variables( + &mut self, + target_core: &mut CoreHandle, + request: Request, + ) -> Result<()> { let arguments: VariablesArguments = match get_arguments(&request) { Ok(arguments) => arguments, Err(error) => return self.send_response::<()>(request, Err(error)), }; + if let Some(core_peripherals) = &mut target_core.core_data.core_peripherals { + // First we check the SVD VariableCache, we do this first because it is the lowest computational overhead. + if let Some(search_variable) = core_peripherals + .svd_variable_cache + .get_variable_by_key(arguments.variables_reference) + { + let dap_variables: Vec = core_peripherals + .svd_variable_cache + .get_children(Some(search_variable.variable_key))? + .iter_mut() + // Convert the `probe_rs::debug::Variable` to `probe_rs_debugger::dap_types::Variable` + .map(|variable| { + let ( + variables_reference, + named_child_variables_cnt, + indexed_child_variables_cnt, + ) = self.get_variable_reference( + variable, + &mut core_peripherals.svd_variable_cache, + ); + Variable { + name: if let VariableName::Named(variable_name) = &variable.name { + if let Some(last_part) = variable_name.split_terminator('.').last() + { + last_part.to_string() + } else { + variable_name.to_string() + } + } else { + variable.name.to_string() + }, + // We use fully qualified Peripheral.Register.Field form to ensure the `evaluate` request can find the right registers and fields by name. + evaluate_name: Some(variable.name.to_string()), + memory_reference: variable + .memory_location + .memory_address() + .map_or_else( + |_| None, + |address| Some(format!("{:#010x}", address)), + ), + indexed_variables: Some(indexed_child_variables_cnt), + named_variables: Some(named_child_variables_cnt), + presentation_hint: None, + type_: Some(variable.type_name.to_string()), + value: { + // The SVD cache is not automatically refreshed on every stack trace, and we only need to refresh the field values. + variable.extract_value( + &mut target_core.core, + &core_peripherals.svd_variable_cache, + ); + variable.get_value(&core_peripherals.svd_variable_cache) + }, + variables_reference, + } + }) + .collect(); + return self.send_response( + request, + Ok(Some(VariablesResponseBody { + variables: dap_variables, + })), + ); + } + } + let response = { - // The MS DAP Specification only gives us the unique reference of the variable, and does not tell us which StackFrame it belongs to, nor does it specify if this variable is in the local, register or static scope. Unfortunately this means we have to search through all the available [VariableCache]'s until we find it. To minimize the impact of this, we will search in the most 'likely' places first (first stack frame's locals, then statics, then registers, then move to next stack frame, and so on ...) let mut parent_variable: Option = None; let mut variable_cache: Option<&mut VariableCache> = None; let mut stack_frame_registers: Option<&Registers> = None; - for stack_frame in core_data.stack_frames.iter_mut() { + for stack_frame in target_core.core_data.stack_frames.iter_mut() { if let Some(search_cache) = &mut stack_frame.local_variables { if let Some(search_variable) = search_cache.get_variable_by_key(arguments.variables_reference) @@ -1467,9 +1623,9 @@ impl DebugAdapter

{ && !variable_cache.has_children(parent_variable)? { if let Some(stack_frame_registers) = stack_frame_registers { - core_data.debug_info.cache_deferred_variables( + target_core.core_data.debug_info.cache_deferred_variables( variable_cache, - &mut core_data.target_core, + &mut target_core.core, parent_variable, stack_frame_registers, )?; @@ -1502,18 +1658,13 @@ impl DebugAdapter

{ named_child_variables_cnt, indexed_child_variables_cnt, ) = self.get_variable_reference(variable, variable_cache); - - let memory_reference = - if let VariableLocation::Address(address) = &variable.memory_location { - Some(format!("{:#x}", address)) - } else { - None - }; - Variable { name: variable.name.to_string(), - evaluate_name: Some(variable.name.to_string()), - memory_reference, + // evaluate_name: Some(variable.name.to_string()), + // Do NOT use evaluate_name. It is impossible to distinguish between duplicate variable + // TODO: Implement qualified names. + evaluate_name: None, + memory_reference: Some(variable.memory_location.to_string()), indexed_variables: Some(indexed_child_variables_cnt), named_variables: Some(named_child_variables_cnt), presentation_hint: None, @@ -1537,13 +1688,14 @@ impl DebugAdapter

{ self.send_response(request, response) } - pub(crate) fn r#continue(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { - match core_data.target_core.run() { + pub(crate) fn r#continue( + &mut self, + target_core: &mut CoreHandle, + request: Request, + ) -> Result<()> { + match target_core.core.run() { Ok(_) => { - self.last_known_status = core_data - .target_core - .status() - .unwrap_or(CoreStatus::Unknown); + self.last_known_status = target_core.core.status().unwrap_or(CoreStatus::Unknown); if request.command.as_str() == "continue" { // If this continue was initiated as part of some other request, then do not respond. self.send_response( @@ -1557,13 +1709,13 @@ impl DebugAdapter

{ // but "immediately" afterwards, the MCU hits a breakpoint or exception. // So we have to check the status again to be sure. thread::sleep(Duration::from_millis(100)); // Small delay to make sure the MCU hits user breakpoints early in `main()`. - let core_status = match core_data.target_core.status() { + let core_status = match target_core.core.status() { Ok(new_status) => match new_status { CoreStatus::Halted(_) => { let event_body = Some(StoppedEventBody { reason: new_status.short_long_status().0.to_owned(), description: Some(new_status.short_long_status().1.to_string()), - thread_id: Some(core_data.target_core.id() as i64), + thread_id: Some(target_core.core.id() as i64), preserve_focus_hint: None, text: None, all_threads_stopped: Some(false), // TODO: Implement multi-core logic here @@ -1590,7 +1742,7 @@ impl DebugAdapter

{ /// Steps through the code at the requested granularity. /// - [SteppingMode::StepInstruction]: If MS DAP [SteppingGranularity::Instruction] (usually sent from the disassembly view) /// - [SteppingMode::OverStatement]: In all other cases. - pub(crate) fn next(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { + pub(crate) fn next(&mut self, target_core: &mut CoreHandle, request: Request) -> Result<()> { let arguments: NextArguments = get_arguments(&request)?; let stepping_granularity = match arguments.granularity { @@ -1598,26 +1750,30 @@ impl DebugAdapter

{ _ => SteppingMode::OverStatement, }; - self.debug_step(stepping_granularity, core_data, request) + self.debug_step(stepping_granularity, target_core, request) } /// Steps through the code at the requested granularity. /// - [SteppingMode::StepInstruction]: If MS DAP [SteppingGranularity::Instruction] (usually sent from the disassembly view) /// - [SteppingMode::IntoStatement]: In all other cases. - pub(crate) fn step_in(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { + pub(crate) fn step_in(&mut self, target_core: &mut CoreHandle, request: Request) -> Result<()> { let arguments: StepInArguments = get_arguments(&request)?; let stepping_granularity = match arguments.granularity { Some(SteppingGranularity::Instruction) => SteppingMode::StepInstruction, _ => SteppingMode::IntoStatement, }; - self.debug_step(stepping_granularity, core_data, request) + self.debug_step(stepping_granularity, target_core, request) } /// Steps through the code at the requested granularity. /// - [SteppingMode::StepInstruction]: If MS DAP [SteppingGranularity::Instruction] (usually sent from the disassembly view) /// - [SteppingMode::OutOfStatement]: In all other cases. - pub(crate) fn step_out(&mut self, core_data: &mut CoreData, request: Request) -> Result<()> { + pub(crate) fn step_out( + &mut self, + target_core: &mut CoreHandle, + request: Request, + ) -> Result<()> { let arguments: StepOutArguments = get_arguments(&request)?; let stepping_granularity = match arguments.granularity { @@ -1625,36 +1781,37 @@ impl DebugAdapter

{ _ => SteppingMode::OutOfStatement, }; - self.debug_step(stepping_granularity, core_data, request) + self.debug_step(stepping_granularity, target_core, request) } /// Common code for the `next`, `step_in`, and `step_out` methods. fn debug_step( &mut self, stepping_granularity: SteppingMode, - core_data: &mut CoreData, + target_core: &mut CoreHandle, request: Request, ) -> Result<(), anyhow::Error> { - let (new_status, program_counter) = - match stepping_granularity.step(&mut core_data.target_core, core_data.debug_info) { - Ok((new_status, program_counter)) => (new_status, program_counter), - Err(error) => match &error { - probe_rs::debug::DebugError::NoValidHaltLocation { - message, - pc_at_error, - } => { - self.show_message( - MessageSeverity::Information, - format!("Step error @{:#010X}: {}", pc_at_error, message), - ); - (core_data.target_core.status()?, *pc_at_error as u32) - } - other_error => { - core_data.target_core.halt(Duration::from_millis(100)).ok(); - return Err(anyhow!("Unexpected error during stepping :{}", other_error)); - } - }, - }; + let (new_status, program_counter) = match stepping_granularity + .step(&mut target_core.core, &target_core.core_data.debug_info) + { + Ok((new_status, program_counter)) => (new_status, program_counter), + Err(error) => match &error { + probe_rs::debug::DebugError::NoValidHaltLocation { + message, + pc_at_error, + } => { + self.show_message( + MessageSeverity::Information, + format!("Step error @{:#010X}: {}", pc_at_error, message), + ); + (target_core.core.status()?, *pc_at_error as u32) + } + other_error => { + target_core.core.halt(Duration::from_millis(100)).ok(); + return Err(anyhow!("Unexpected error during stepping :{}", other_error)); + } + }, + }; self.last_known_status = new_status; self.send_response::<()>(request, Ok(None))?; @@ -1666,7 +1823,7 @@ impl DebugAdapter

{ new_status.short_long_status().1, program_counter )), - thread_id: Some(core_data.target_core.id() as i64), + thread_id: Some(target_core.core.id() as i64), preserve_focus_hint: None, text: None, all_threads_stopped: Some(false), // TODO: Implement multi-core logic here @@ -1738,9 +1895,29 @@ impl DebugAdapter

{ } pub fn send_error_response(&mut self, response: &DebuggerError) -> Result<()> { + let expanded_error = { + let mut response_message = response.to_string(); + let mut offset_iterations = 0; + let mut child_error: Option<&dyn std::error::Error> = + std::error::Error::source(&response); + while let Some(source_error) = child_error { + offset_iterations += 1; + response_message = format!("{}\n", response_message,); + for _offset_counter in 0..offset_iterations { + response_message = format!("{}\t", response_message); + } + response_message = format!( + "{}{}", + response_message, + ::to_string(source_error) + ); + child_error = std::error::Error::source(source_error); + } + response_message + }; if self .adapter - .show_message(MessageSeverity::Error, response.to_string()) + .show_message(MessageSeverity::Error, expanded_error) { Ok(()) } else { @@ -1852,7 +2029,7 @@ impl DebugAdapter

{ /// The progress has the range [0..1]. pub fn update_progress( &mut self, - progress: f64, + progress: Option, message: Option>, progress_id: i64, ) -> Result { @@ -1865,7 +2042,7 @@ impl DebugAdapter

{ "progressUpdate", Some(ProgressUpdateEventBody { message: message.map(|v| v.into()), - percentage: Some(progress * 100.0), + percentage: progress.map(|progress| progress * 100.0), progress_id: progress_id.to_string(), }), )?; diff --git a/debugger/src/dap_types.rs b/debugger/src/debug_adapter/dap_types.rs similarity index 97% rename from debugger/src/dap_types.rs rename to debugger/src/debug_adapter/dap_types.rs index f81081eaa6..f6aa2ca4f0 100644 --- a/debugger/src/dap_types.rs +++ b/debugger/src/debug_adapter/dap_types.rs @@ -7,7 +7,8 @@ use schemafy::schemafy; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; -schemafy!(root: debugserver_types "src/debugProtocol.json"); +// Convert the MSDAP `debugAdaptor.json` file, renamed here as `schema.json` into Rust types. +schemafy!(root: debugserver_types "src/debug_adapter/schema.json"); /// Custom 'quit' request, so that VSCode can tell the `probe-rs-debugger` to terminate its own process. #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] diff --git a/debugger/src/debug_adapter/mod.rs b/debugger/src/debug_adapter/mod.rs new file mode 100644 index 0000000000..e12a0f7766 --- /dev/null +++ b/debugger/src/debug_adapter/mod.rs @@ -0,0 +1,6 @@ +/// Implements the logic for each of the MS DAP types (events, requests, etc.) +pub(crate) mod dap_adapter; +/// The MS DAP api, and extensions, for communicating with the MS DAP client. +pub(crate) mod dap_types; +/// Communication interfaces to connect the DAP client and probe-rs-debugger. +pub(crate) mod protocol; diff --git a/debugger/src/protocol.rs b/debugger/src/debug_adapter/protocol.rs similarity index 97% rename from debugger/src/protocol.rs rename to debugger/src/debug_adapter/protocol.rs index 945839976e..c97cf44519 100644 --- a/debugger/src/protocol.rs +++ b/debugger/src/debug_adapter/protocol.rs @@ -1,20 +1,20 @@ -use crate::debugger::ConsoleLog; -use crate::DebuggerError; +use crate::{ + debug_adapter::dap_types::{ + Event, MessageSeverity, OutputEventBody, ProtocolMessage, Request, Response, + ShowMessageEventBody, + }, + debugger::configuration::ConsoleLog, + DebuggerError, +}; +use anyhow::anyhow; use serde::Serialize; -use std::collections::HashMap; -use std::string::ToString; use std::{ + collections::HashMap, io::{BufRead, BufReader, Read, Write}, str, + string::ToString, }; -use crate::dap_types::{ - Event, MessageSeverity, OutputEventBody, ProtocolMessage, Request, Response, - ShowMessageEventBody, -}; - -use anyhow::anyhow; - pub trait ProtocolAdapter { /// Listen for a request. This call should be non-blocking, and if not request is available, it should /// return None. @@ -404,7 +404,7 @@ fn get_content_len(header: &str) -> Option { mod test { use std::io::{self, ErrorKind, Read}; - use crate::protocol::{get_content_len, ProtocolAdapter}; + use crate::debug_adapter::protocol::{get_content_len, ProtocolAdapter}; use super::DapAdapter; diff --git a/debugger/src/debugProtocol.json b/debugger/src/debug_adapter/schema.json similarity index 98% rename from debugger/src/debugProtocol.json rename to debugger/src/debug_adapter/schema.json index e331ac604a..aa70609e47 100644 --- a/debugger/src/debugProtocol.json +++ b/debugger/src/debug_adapter/schema.json @@ -1035,7 +1035,7 @@ "DisconnectRequest": { "allOf": [ { "$ref": "#/definitions/Request" }, { "type": "object", - "description": "The 'disconnect' request is sent from the client to the debug adapter in order to stop debugging.\nIt asks the debug adapter to disconnect from the debuggee and to terminate the debug adapter.\nIf the debuggee has been started with the 'launch' request, the 'disconnect' request terminates the debuggee.\nIf the 'attach' request was used to connect to the debuggee, 'disconnect' does not terminate the debuggee.\nThis behavior can be controlled with the 'terminateDebuggee' argument (if supported by the debug adapter).", + "description": "The 'disconnect' request asks the debug adapter to disconnect from the debuggee (thus ending the debug session) and then to shut down itself (the debug adapter).\nIn addition, the debug adapter must terminate the debuggee if it was started with the 'launch' request. If an 'attach' request was used to connect to the debuggee, then the debug adapter must not terminate the debuggee.\nThis implicit behavior of when to terminate the debuggee can be overridden with the optional argument 'terminateDebuggee' (which is only supported by a debug adapter if the corresponding capability 'supportTerminateDebuggee' is true).", "properties": { "command": { "type": "string", @@ -1076,7 +1076,7 @@ "TerminateRequest": { "allOf": [ { "$ref": "#/definitions/Request" }, { "type": "object", - "description": "The 'terminate' request is sent from the client to the debug adapter in order to give the debuggee a chance for terminating itself.\nClients should only call this request if the capability 'supportsTerminateRequest' is true.", + "description": "The 'terminate' request is sent from the client to the debug adapter in order to shut down the debuggee gracefully. Clients should only call this request if the capability 'supportsTerminateRequest' is true.\nTypically a debug adapter implements 'terminate' by sending a software signal which the debuggee intercepts in order to clean things up properly before terminating itself.\nPlease note that this request does not directly affect the state of the debug session: if the debuggee decides to veto the graceful shutdown for any reason by not terminating itself, then the debug session will just continue.\nClients can surface the 'terminate' request as an explicit command or they can integrate it into a two stage Stop command that first sends 'terminate' to request a graceful shutdown, and if that fails uses 'disconnect' for a forceful shutdown.", "properties": { "command": { "type": "string", @@ -1195,7 +1195,7 @@ "properties": { "source": { "$ref": "#/definitions/Source", - "description": "The source location of the breakpoints; either 'source.path' or 'source.reference' must be specified." + "description": "The source location of the breakpoints; either 'source.path' or 'source.sourceReference' must be specified." }, "breakpoints": { "type": "array", @@ -2417,18 +2417,19 @@ }, "context": { "type": "string", - "_enum": [ "watch", "repl", "hover", "clipboard" ], + "_enum": [ "variables", "watch", "repl", "hover", "clipboard" ], "enumDescriptions": [ - "evaluate is run in a watch.", - "evaluate is run from REPL console.", - "evaluate is run from a data hover.", - "evaluate is run to generate the value that will be stored in the clipboard.\nThe attribute is only honored by a debug adapter if the capability 'supportsClipboardContext' is true." + "evaluate is called from a variables view context.", + "evaluate is called from a watch view context.", + "evaluate is called from a REPL context.", + "evaluate is called to generate the debug hover contents.\nThis value should only be used if the capability 'supportsEvaluateForHovers' is true.", + "evaluate is called to generate clipboard contents.\nThis value should only be used if the capability 'supportsClipboardContext' is true." ], - "description": "The context in which the evaluate request is run." + "description": "The context in which the evaluate request is used." }, "format": { "$ref": "#/definitions/ValueFormat", - "description": "Specifies details on how to format the Evaluate result.\nThe attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true." + "description": "Specifies details on how to format the result.\nThe attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true." } }, "required": [ "expression" ] @@ -3940,7 +3941,7 @@ "ExceptionFilterOptions": { "type": "object", - "description": "An ExceptionFilterOptions is used to specify an exception filter together with a condition for the setExceptionsFilter request.", + "description": "An ExceptionFilterOptions is used to specify an exception filter together with a condition for the 'setExceptionBreakpoints' request.", "properties": { "filterId": { "type": "string", diff --git a/debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_request_with_invalid_json.snap b/debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_request_with_invalid_json.snap similarity index 92% rename from debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_request_with_invalid_json.snap rename to debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_request_with_invalid_json.snap index a535aa10c4..bc50f9e283 100644 --- a/debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_request_with_invalid_json.snap +++ b/debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_request_with_invalid_json.snap @@ -1,7 +1,6 @@ --- -source: debugger/src/protocol.rs +source: debugger/src/debug_adapter/protocol.rs expression: output_str - --- Content-Length: 156 diff --git a/debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_request_with_wrong_content_length.snap b/debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_request_with_wrong_content_length.snap similarity index 92% rename from debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_request_with_wrong_content_length.snap rename to debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_request_with_wrong_content_length.snap index 2e64ae4726..ff28dd8bf3 100644 --- a/debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_request_with_wrong_content_length.snap +++ b/debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_request_with_wrong_content_length.snap @@ -1,7 +1,6 @@ --- -source: debugger/src/protocol.rs +source: debugger/src/debug_adapter/protocol.rs expression: output_str - --- Content-Length: 165 diff --git a/debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_request_would_block.snap b/debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_request_would_block.snap new file mode 100644 index 0000000000..c1ec595c3e --- /dev/null +++ b/debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_request_would_block.snap @@ -0,0 +1,5 @@ +--- +source: debugger/src/debug_adapter/protocol.rs +expression: output_str +--- + diff --git a/debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_valid_request.snap b/debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_valid_request.snap similarity index 81% rename from debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_valid_request.snap rename to debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_valid_request.snap index 74e7b7d8a9..bcec575276 100644 --- a/debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_valid_request.snap +++ b/debugger/src/debug_adapter/snapshots/probe_rs_debugger__debug_adapter__protocol__test__receive_valid_request.snap @@ -1,7 +1,6 @@ --- -source: debugger/src/debug_adapter.rs +source: debugger/src/debug_adapter/protocol.rs expression: output_str - --- Content-Length: 151 diff --git a/debugger/src/debugger/configuration.rs b/debugger/src/debugger/configuration.rs new file mode 100644 index 0000000000..2227242ca0 --- /dev/null +++ b/debugger/src/debugger/configuration.rs @@ -0,0 +1,226 @@ +use crate::DebuggerError; +use anyhow::{anyhow, Result}; +use probe_rs::{DebugProbeSelector, WireProtocol}; +use probe_rs_cli_util::rtt; +use serde::Deserialize; +use std::{env::current_dir, path::PathBuf}; + +/// Shared options for all session level configuration. +#[derive(Clone, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct SessionConfig { + /// IP port number to listen for incoming DAP connections, e.g. "50000" + pub(crate) port: Option, + + /// Level of information to be logged to the debugger console (Error, Info or Debug ) + #[serde(default = "default_console_log")] + pub(crate) console_log_level: Option, + + /// Path to the requested working directory for the debugger + pub(crate) cwd: Option, + + /// The number associated with the debug probe to use. Use 'list' command to see available probes + #[serde(alias = "probe")] + pub(crate) probe_selector: Option, + + /// The target to be selected. + pub(crate) chip: Option, + + /// Assert target's reset during connect + #[serde(default)] + pub(crate) connect_under_reset: bool, + + /// Protocol speed in kHz + pub(crate) speed: Option, + + /// Protocol to use for target connection + #[serde(rename = "wire_protocol")] + pub(crate) protocol: Option, + + ///Allow the session to erase all memory of the chip or reset it to factory default. + #[serde(default)] + pub(crate) allow_erase_all: bool, + + /// Flashing configuration + pub(crate) flashing_config: FlashingConfig, + + /// Every core on the target has certain configuration. + /// + /// NOTE: Although we allow specifying multiple core configurations, this is a work in progress, and probe-rs-debugger currently only supports debugging a single core. + pub(crate) core_configs: Vec, +} + +impl SessionConfig { + /// Ensure all file names are correctly specified and that the files they point to are accessible. + pub(crate) fn validate_config_files(&mut self) -> Result<(), DebuggerError> { + // Update the `cwd`. + self.cwd = self.resolve_cwd()?; + + for target_core_config in &mut self.core_configs { + // Update the `program_binary` and validate that the file exists. + target_core_config.program_binary = match get_absolute_path( + self.cwd.clone(), + target_core_config.program_binary.as_ref(), + ) { + Ok(program_binary) => { + if !program_binary.is_file() { + return Err(DebuggerError::Other(anyhow!( + "Invalid program binary file specified '{:?}'", + program_binary + ))); + } + Some(program_binary) + } + Err(error) => { + return Err(DebuggerError::Other(anyhow!( + "Please use the `program-binary` option to specify an executable for this target core. {:?}", error + ))); + } + }; + // Update the `svd_file` and validate that the file exists. + // If there is a problem with this file, warn the user and continue with the session. + target_core_config.svd_file = + match get_absolute_path(self.cwd.clone(), target_core_config.svd_file.as_ref()) { + Ok(svd_file) => { + if !svd_file.is_file() { + log::error!("SVD file {:?} not found.", svd_file); + None + } else { + Some(svd_file) + } + } + Err(error) => { + // SVD file is not mandatory. + log::debug!("SVD file not specified: {:?}", &error); + None + } + }; + } + + Ok(()) + } + + /// Validate the new given cwd for this process exists, or else update the cwd setting to use the running process' current working directory. + pub(crate) fn resolve_cwd(&self) -> Result, DebuggerError> { + Ok(match &self.cwd { + Some(temp_path) => { + if temp_path.is_dir() { + Some(temp_path.to_path_buf()) + } else if let Ok(current_dir) = current_dir() { + Some(current_dir) + } else { + log::error!("Cannot use current working directory. Please check existence and permissions."); + None + } + } + None => { + if let Ok(current_dir) = current_dir() { + Some(current_dir) + } else { + log::error!("Cannot use current working directory. Please check existence and permissions."); + None + } + } + }) + } +} + +/// If the path to the program to be debugged is relative, we join if with the cwd. +fn get_absolute_path( + configured_cwd: Option, + os_file_to_validate: Option<&PathBuf>, +) -> Result { + match os_file_to_validate { + Some(temp_path) => { + let mut new_path = PathBuf::new(); + if temp_path.is_relative() { + if let Some(cwd_path) = configured_cwd.clone() { + new_path.push(cwd_path); + } else { + return Err(DebuggerError::Other(anyhow!( + "Invalid value {:?} for `cwd`", + configured_cwd + ))); + } + } + new_path.push(temp_path); + Ok(new_path) + } + None => Err(DebuggerError::Other(anyhow!("Missing value for file."))), + } +} + +/// Configuration options to control flashing. +#[derive(Clone, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct FlashingConfig { + /// Flash the target before debugging + #[serde(default)] + pub(crate) flashing_enabled: bool, + + /// Reset the target after flashing + #[serde(default)] + pub(crate) reset_after_flashing: bool, + + /// Halt the target after reset + #[serde(default)] + pub(crate) halt_after_reset: bool, + + /// Do a full chip erase, versus page-by-page erase + #[serde(default)] + pub(crate) full_chip_erase: bool, + + /// Restore erased bytes that will not be rewritten from ELF + #[serde(default)] + pub(crate) restore_unwritten_bytes: bool, +} + +/// Configuration options for all core level configuration. +#[derive(Clone, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +pub struct CoreConfig { + /// The MCU Core to debug. Default is 0 + #[serde(default)] + pub(crate) core_index: usize, + + /// Binary to debug as a path. Relative to `cwd`, or fully qualified. + pub(crate) program_binary: Option, + + /// CMSIS-SVD file for the target. Relative to `cwd`, or fully qualified. + pub(crate) svd_file: Option, + + #[serde(flatten)] + pub(crate) rtt_config: rtt::RttConfig, +} + +fn default_console_log() -> Option { + Some(ConsoleLog::Error) +} + +/// The level of information to be logged to the debugger console. The DAP Client will set appropriate RUST_LOG env for 'launch' configurations, and will pass the rust log output to the client debug console. +#[derive(Copy, Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)] +pub enum ConsoleLog { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl std::str::FromStr for ConsoleLog { + type Err = String; + + fn from_str(s: &str) -> Result { + match &s.to_ascii_lowercase()[..] { + "error" => Ok(ConsoleLog::Error), + "warn" => Ok(ConsoleLog::Error), + "info" => Ok(ConsoleLog::Info), + "debug" => Ok(ConsoleLog::Debug), + "trace" => Ok(ConsoleLog::Trace), + _ => Err(format!( + "'{}' is not a valid console log level. Choose from [error, warn, info, debug, or trace].", + s + )), + } + } +} diff --git a/debugger/src/debugger/core_data.rs b/debugger/src/debugger/core_data.rs new file mode 100644 index 0000000000..bdb2a9bbca --- /dev/null +++ b/debugger/src/debugger/core_data.rs @@ -0,0 +1,139 @@ +use super::session_data; +use crate::{ + debug_adapter::{dap_adapter::DebugAdapter, protocol::ProtocolAdapter}, + debugger::debug_rtt, + peripherals::svd_variables::SvdCache, + DebuggerError, +}; +use anyhow::Result; +use capstone::Capstone; +use probe_rs::{debug::DebugInfo, Core}; +use probe_rs_cli_util::rtt; + +/// [CoreData] is used to cache data needed by the debugger, on a per-core basis. +pub struct CoreData { + pub(crate) core_index: usize, + pub(crate) target_name: String, + pub(crate) debug_info: DebugInfo, + pub(crate) core_peripherals: Option, + pub(crate) stack_frames: Vec, + pub(crate) breakpoints: Vec, + pub(crate) rtt_connection: Option, +} + +/// [CoreHandle] provides handles to various data structures required to debug a single instance of a core. The actual state is stored in [SessionData]. +/// +/// Usage: To get access to this structure please use the [SessionData::attach_core] method. Please keep access/locks to this to a minumum duration. +pub struct CoreHandle<'p> { + pub(crate) core: Core<'p>, + pub(crate) capstone: &'p Capstone, + pub(crate) core_data: &'p mut CoreData, +} + +impl<'p> CoreHandle<'p> { + /// Search available [StackFrame]'s for the given `id` + pub(crate) fn get_stackframe(&'p self, id: i64) -> Option<&'p probe_rs::debug::StackFrame> { + self.core_data + .stack_frames + .iter() + .find(|stack_frame| stack_frame.id == id) + } + + /// Confirm RTT initialization on the target, and use the RTT channel configurations to initialize the output windows on the DAP Client. + pub fn attach_to_rtt( + &mut self, + debug_adapter: &mut DebugAdapter

, + target_memory_map: &[probe_rs::config::MemoryRegion], + program_binary: &std::path::Path, + rtt_config: &rtt::RttConfig, + ) -> Result<()> { + let mut debugger_rtt_channels: Vec = vec![]; + match rtt::attach_to_rtt( + &mut self.core, + target_memory_map, + program_binary, + rtt_config, + ) { + Ok(target_rtt) => { + for any_channel in target_rtt.active_channels.iter() { + if let Some(up_channel) = &any_channel.up_channel { + debugger_rtt_channels.push(debug_rtt::DebuggerRttChannel { + channel_number: up_channel.number(), + // This value will eventually be set to true by a VSCode client request "rttWindowOpened" + has_client_window: false, + }); + debug_adapter.rtt_window( + up_channel.number(), + any_channel.channel_name.clone(), + any_channel.data_format, + ); + } + } + self.core_data.rtt_connection = Some(debug_rtt::RttConnection { + target_rtt, + debugger_rtt_channels, + }); + } + Err(_error) => { + log::warn!("Failed to initalize RTT. Will try again on the next request... "); + } + }; + Ok(()) + } + + /// Set a single breakpoint in target configuration as well as [`CoreHandle::breakpoints`] + pub(crate) fn set_breakpoint( + &mut self, + address: u32, + breakpoint_type: session_data::BreakpointType, + ) -> Result<(), DebuggerError> { + self.core + .set_hw_breakpoint(address) + .map_err(DebuggerError::ProbeRs)?; + self.core_data + .breakpoints + .push(session_data::ActiveBreakpoint { + breakpoint_type, + breakpoint_address: address, + }); + Ok(()) + } + + /// Clear a single breakpoint from target configuration as well as [`CoreHandle::breakpoints`] + pub(crate) fn clear_breakpoint(&mut self, address: u32) -> Result<()> { + self.core + .clear_hw_breakpoint(address) + .map_err(DebuggerError::ProbeRs)?; + let mut breakpoint_position: Option = None; + for (position, active_breakpoint) in self.core_data.breakpoints.iter().enumerate() { + if active_breakpoint.breakpoint_address == address { + breakpoint_position = Some(position); + break; + } + } + if let Some(breakpoint_position) = breakpoint_position { + self.core_data + .breakpoints + .remove(breakpoint_position as usize); + } + Ok(()) + } + + /// Clear all breakpoints of a specified [`BreakpointType`]. Affects target configuration as well as [`CoreHandle::breakpoints`] + pub(crate) fn clear_breakpoints( + &mut self, + breakpoint_type: session_data::BreakpointType, + ) -> Result<()> { + let target_breakpoints = self + .core_data + .breakpoints + .iter() + .filter(|breakpoint| breakpoint.breakpoint_type == breakpoint_type) + .map(|breakpoint| breakpoint.breakpoint_address) + .collect::>(); + for breakpoint in target_breakpoints { + self.clear_breakpoint(breakpoint).ok(); + } + Ok(()) + } +} diff --git a/debugger/src/debugger.rs b/debugger/src/debugger/debug_entry.rs similarity index 52% rename from debugger/src/debugger.rs rename to debugger/src/debugger/debug_entry.rs index 0da8eed804..82872651cd 100644 --- a/debugger/src/debugger.rs +++ b/debugger/src/debugger/debug_entry.rs @@ -1,79 +1,29 @@ -use crate::dap_types::*; -use crate::debug_adapter::*; -use crate::protocol::{DapAdapter, ProtocolAdapter}; - -use crate::DebuggerError; -use anyhow::{anyhow, Context, Result}; -use capstone::{ - arch::arm::ArchMode as armArchMode, arch::riscv::ArchMode as riscvArchMode, prelude::*, - Capstone, Endian, +use super::session_data; +use crate::{ + debug_adapter::{ + dap_adapter::*, + dap_types::*, + protocol::{DapAdapter, ProtocolAdapter}, + }, + debugger::configuration::{self, ConsoleLog}, + peripherals::svd_variables::SvdCache, + DebuggerError, }; -use probe_rs::config::TargetSelector; -use probe_rs::debug::DebugInfo; - -use probe_rs::flashing::download_file_with_options; -use probe_rs::flashing::DownloadOptions; -use probe_rs::flashing::FlashProgress; -use probe_rs::flashing::Format; -use probe_rs::ProbeCreationError; +use anyhow::{anyhow, Context, Result}; use probe_rs::{ - Core, CoreStatus, DebugProbeError, DebugProbeSelector, Permissions, Probe, Session, - WireProtocol, + flashing::{download_file_with_options, DownloadOptions, FlashProgress, Format}, + CoreStatus, Probe, }; -use probe_rs_cli_util::rtt; use serde::Deserialize; -use std::cell::RefCell; -use std::net::Ipv4Addr; -use std::net::TcpListener; -use std::ops::Mul; -use std::rc::Rc; use std::{ - env::{current_dir, set_current_dir}, - path::PathBuf, - str::FromStr, + cell::RefCell, + net::{Ipv4Addr, TcpListener}, + ops::Mul, + rc::Rc, thread, time::Duration, }; -fn default_console_log() -> Option { - Some(ConsoleLog::Error) -} - -fn parse_probe_selector(src: &str) -> Result { - match DebugProbeSelector::from_str(src) { - Ok(probe_selector) => Ok(probe_selector), - Err(error) => Err(error.to_string()), - } -} - -/// The level of information to be logged to the debugger console. The DAP Client will set appropriate RUST_LOG env for 'launch' configurations, and will pass the rust log output to the client debug console. -#[derive(Copy, Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)] -pub enum ConsoleLog { - Error, - Warn, - Info, - Debug, - Trace, -} - -impl std::str::FromStr for ConsoleLog { - type Err = String; - - fn from_str(s: &str) -> Result { - match &s.to_ascii_lowercase()[..] { - "error" => Ok(ConsoleLog::Error), - "warn" => Ok(ConsoleLog::Error), - "info" => Ok(ConsoleLog::Info), - "debug" => Ok(ConsoleLog::Debug), - "trace" => Ok(ConsoleLog::Trace), - _ => Err(format!( - "'{}' is not a valid console log level. Choose from [error, warn, info, debug, or trace].", - s - )), - } - } -} - #[derive(clap::Parser, Copy, Clone, Debug, Deserialize)] pub(crate) enum TargetSessionType { AttachRequest, @@ -95,547 +45,6 @@ impl std::str::FromStr for TargetSessionType { } } -/// Shared options for all commands which use a specific probe -#[derive(clap::Parser, Clone, Deserialize, Debug, Default)] -#[serde(rename_all = "camelCase")] -pub struct DebuggerOptions { - /// Path to the requested working directory for the debugger - #[clap(long, parse(from_os_str))] - pub(crate) cwd: Option, - - /// Binary to debug as a path. Relative to `cwd`, or fully qualified. - #[clap(long, parse(from_os_str))] - pub(crate) program_binary: Option, - - /// The number associated with the debug probe to use. Use 'list' command to see available probes - #[clap( - long = "probe", - parse(try_from_str = parse_probe_selector), - help = "Use this flag to select a specific probe in the list.\n\ - Use '--probe VID:PID' or '--probe VID:PID:Serial' if you have more than one probe with the same VID:PID." - )] - #[serde(alias = "probe")] - pub(crate) probe_selector: Option, - - /// The MCU Core to debug. Default is 0 - #[clap(long = "core-index", default_value_t)] - #[serde(default)] - pub(crate) core_index: usize, - - /// The target to be selected. - #[clap(short, long)] - pub(crate) chip: Option, - - /// Protocol to use for target connection - #[clap(short, long)] - #[serde(rename = "wire_protocol")] - pub(crate) protocol: Option, - - /// Protocol speed in kHz - #[clap(short, long)] - pub(crate) speed: Option, - - /// Assert target's reset during connect - #[clap(long)] - #[serde(default)] - pub(crate) connect_under_reset: bool, - - /// Allow the chip to be fully erased - #[structopt(long)] - #[serde(default)] - pub(crate) allow_erase_all: bool, - - /// IP port number to listen for incoming DAP connections, e.g. "50000" - #[clap(long)] - pub(crate) port: Option, - - /// Flash the target before debugging - #[clap(long)] - #[serde(default)] - pub(crate) flashing_enabled: bool, - - /// Reset the target after flashing - #[clap(long, required_if_eq("flashing-enabled", "true"))] - #[serde(default)] - pub(crate) reset_after_flashing: bool, - - /// Halt the target after reset - #[clap(long)] - #[serde(default)] - pub(crate) halt_after_reset: bool, - - /// Do a full chip erase, versus page-by-page erase - #[clap(long, required_if_eq("flashing-enabled", "true"))] - #[serde(default)] - pub(crate) full_chip_erase: bool, - - /// Restore erased bytes that will not be rewritten from ELF - #[clap(long, required_if_eq("flashing-enabled", "true"))] - #[serde(default)] - pub(crate) restore_unwritten_bytes: bool, - - /// Level of information to be logged to the debugger console (Error, Info or Debug ) - #[clap(long)] - #[serde(default = "default_console_log")] - pub(crate) console_log_level: Option, - - #[clap(flatten)] - #[serde(flatten)] - pub(crate) rtt: rtt::RttConfig, -} - -impl DebuggerOptions { - /// Validate the new cwd, or else set it from the environment. - pub(crate) fn validate_and_update_cwd(&mut self, new_cwd: Option) { - self.cwd = match new_cwd { - Some(temp_path) => { - if temp_path.is_dir() { - Some(temp_path) - } else if let Ok(current_dir) = current_dir() { - Some(current_dir) - } else { - log::error!("Cannot use current working directory. Please check existence and permissions."); - None - } - } - None => { - if let Ok(current_dir) = current_dir() { - Some(current_dir) - } else { - log::error!("Cannot use current working directory. Please check existence and permissions."); - None - } - } - }; - } - - /// If the path to the programm to be debugged is relative, we join if with the cwd. - pub(crate) fn qualify_and_update_program_binary( - &mut self, - new_program_binary: Option, - ) -> Result<(), DebuggerError> { - self.program_binary = match new_program_binary { - Some(temp_path) => { - let mut new_path = PathBuf::new(); - if temp_path.is_relative() { - if let Some(cwd_path) = self.cwd.clone() { - new_path.push(cwd_path); - } else { - return Err(DebuggerError::Other(anyhow!( - "Invalid value {:?} for `cwd`", - self.cwd - ))); - } - } - new_path.push(temp_path); - Some(new_path) - } - None => None, - }; - Ok(()) - } -} - -/// The supported breakpoint types -#[derive(Debug, PartialEq)] -pub enum BreakpointType { - InstructionBreakpoint, - SourceBreakpoint, -} - -/// Provide the storage and methods to handle various [`BreakPointType`] -#[derive(Debug)] -pub struct ActiveBreakpoint { - breakpoint_type: BreakpointType, - breakpoint_address: u32, -} - -/// DebugSession is designed to be similar to [probe_rs::Session], in as much that it provides handles to the [CoreData] instances for each of the available [probe_rs::Core] involved in the debug session. -/// To get access to the [CoreData] for a specific [Core], the -/// TODO: Adjust [DebuggerOptions] to allow multiple cores (and if appropriate, their binaries) to be specified. -pub struct DebugSession { - pub(crate) session: Session, - /// Provides ability to disassemble binary code. - pub(crate) capstone: Capstone, - /// [DebugSession] will manage one [DebugInfo] per [DebuggerOptions::program_binary] - pub(crate) debug_infos: Vec, - /// [DebugSession] will manage a `Vec` per [Core]. Each core's collection of StackFrames will be recreated whenever a stacktrace is performed, using the results of [DebugInfo::unwind] - pub(crate) stack_frames: Vec>, - /// [DebugSession] will manage a `Vec` per [Core]. Each core's collection of ActiveBreakpoint's will be managed on demand. - pub(crate) breakpoints: Vec>, - /// The control structures for handling RTT in this Core of the DebugSession. - pub(crate) rtt_connection: Option, -} - -impl DebugSession { - pub(crate) fn new(debugger_options: &DebuggerOptions) -> Result { - let mut target_probe = match debugger_options.probe_selector.clone() { - Some(selector) => Probe::open(selector.clone()).map_err(|e| match e { - DebugProbeError::ProbeCouldNotBeCreated(ProbeCreationError::NotFound) => { - DebuggerError::Other(anyhow!( - "Could not find the probe_selector specified as {:04x}:{:04x}:{:?}", - selector.vendor_id, - selector.product_id, - selector.serial_number - )) - } - other_error => DebuggerError::DebugProbe(other_error), - }), - None => { - // Only automatically select a probe if there is only a single probe detected. - let list = Probe::list_all(); - if list.len() > 1 { - return Err(DebuggerError::Other(anyhow!( - "Found multiple ({}) probes", - list.len() - ))); - } - - if let Some(info) = list.first() { - Probe::open(info).map_err(DebuggerError::DebugProbe) - } else { - return Err(DebuggerError::Other(anyhow!( - "No probes found. Please check your USB connections." - ))); - } - } - }?; - - let target_selector = match &debugger_options.chip { - Some(identifier) => identifier.into(), - None => TargetSelector::Auto, - }; - - // Set the protocol, if the user explicitly selected a protocol. Otherwise, use the default protocol of the probe. - if let Some(protocol) = debugger_options.protocol { - target_probe.select_protocol(protocol)?; - } - - // Set the speed. - if let Some(speed) = debugger_options.speed { - let actual_speed = target_probe.set_speed(speed)?; - if actual_speed != speed { - log::warn!( - "Protocol speed {} kHz not supported, actual speed is {} kHz", - speed, - actual_speed - ); - } - } - - let mut permissions = Permissions::new(); - if debugger_options.allow_erase_all { - permissions = permissions.allow_erase_all(); - } - - // Attach to the probe. - let target_session = if debugger_options.connect_under_reset { - target_probe.attach_under_reset(target_selector, permissions)? - } else { - target_probe - .attach(target_selector, permissions) - .map_err(|err| { - anyhow!( - "Error attaching to the probe: {:?}.\nTry the --connect-under-reset option", - err - ) - })? - }; - - // Create an instance of the [`capstone::Capstone`] for disassembly capabilities. - let capstone = match target_session.architecture() { - probe_rs::Architecture::Arm => Capstone::new() - .arm() - .mode(armArchMode::Thumb) - .endian(Endian::Little) - .build() - .map_err(|err| anyhow!("Error creating Capstone disassembler: {:?}", err))?, - probe_rs::Architecture::Riscv => Capstone::new() - .riscv() - .mode(riscvArchMode::RiscV32) - .endian(Endian::Little) - .build() - .map_err(|err| anyhow!("Error creating Capstone disassembler: {:?}", err))?, - }; - - // Change the current working directory if `debugger_options.cwd` is `Some(T)`. - if let Some(new_cwd) = debugger_options.cwd.clone() { - set_current_dir(new_cwd.as_path()).map_err(|err| { - anyhow!( - "Failed to set current working directory to: {:?}, {:?}", - new_cwd, - err - ) - })?; - }; - - // TODO: We currently only allow a single core & binary to be specified in [DebuggerOptions]. When this is extended to support multicore, the following should initialize [DebugInfo] and [VariableCache] for each available core. - // Configure the [DebugInfo]. - let debug_infos = vec![ - if let Some(binary_path) = &debugger_options.program_binary { - DebugInfo::from_file(binary_path) - .map_err(|error| DebuggerError::Other(anyhow!(error)))? - } else { - return Err(anyhow!( - "Please provide a valid `program_binary` for this debug session" - ) - .into()); - }, - ]; - - // Configure the [VariableCache]. - let stack_frames = target_session.list_cores() - .iter() - .map(|(core_id, core_type)| { - log::debug!( - "Preparing stack frame variable cache for DebugSession and CoreData for core #{} of type: {:?}", - core_id, - core_type - ); - Vec::::new() - }).collect(); - - // Prepare the breakpoint cache - let breakpoints = target_session.list_cores() - .iter() - .map(|(core_id, core_type)| { - log::debug!( - "Preparing breakpoint cache for DebugSession and CoreData for core #{} of type: {:?}", - core_id, - core_type - ); - Vec::::new() - }).collect(); - - Ok(DebugSession { - session: target_session, - capstone, - debug_infos, - stack_frames, - breakpoints, - rtt_connection: None, - }) - } - - pub fn attach_core(&mut self, core_index: usize) -> Result { - let target_name = self.session.target().name.clone(); - // Do a 'light weight'(just get references to existing data structures) attach to the core and return relevant debug data. - match self.session.core(core_index) { - Ok(target_core) => Ok(CoreData { - target_core, - target_name: format!("{}-{}", core_index, target_name), - debug_info: self.debug_infos.get(core_index).ok_or_else(|| { - DebuggerError::Other(anyhow!( - "No available `DebugInfo` for core # {}", - core_index - )) - })?, - stack_frames: self.stack_frames.get_mut(core_index).ok_or_else(|| { - DebuggerError::Other(anyhow!( - "StackFrame cache was not correctly configured for core # {}", - core_index - )) - })?, - capstone: &self.capstone, - breakpoints: self.breakpoints.get_mut(core_index).ok_or_else(|| { - DebuggerError::Other(anyhow!( - "ActiveBreakpoint cache was not correctly configured for core # {}", - core_index - )) - })?, - rtt_connection: &mut self.rtt_connection, - }), - Err(_) => Err(DebuggerError::UnableToOpenProbe(Some( - "No core at the specified index.", - ))), - } - } -} - -/// Manage the active RTT target for a specific DebugSession, as well as provide methods to reliably move RTT from target, through the debug_adapter, to the client. -pub(crate) struct RttConnection { - /// The connection to RTT on the target - target_rtt: rtt::RttActiveTarget, - /// Some status fields and methods to ensure continuity in flow of data from target to debugger to client. - debugger_rtt_channels: Vec, -} - -impl RttConnection { - /// Polls all the available channels for data and transmits data to the client. - /// If at least one channel had data, then return a `true` status. - pub fn process_rtt_data( - &mut self, - debug_adapter: &mut DebugAdapter

, - target_core: &mut Core, - ) -> bool { - let mut at_least_one_channel_had_data = false; - for debugger_rtt_channel in self.debugger_rtt_channels.iter_mut() { - if debugger_rtt_channel.poll_rtt_data(target_core, debug_adapter, &mut self.target_rtt) - { - at_least_one_channel_had_data = true; - } - } - at_least_one_channel_had_data - } -} - -pub(crate) struct DebuggerRttChannel { - pub(crate) channel_number: usize, - // We will not poll target RTT channels until we have confirmation from the client that the output window has been opened. - pub(crate) has_client_window: bool, -} -impl DebuggerRttChannel { - /// Poll and retrieve data from the target, and send it to the client, depending on the state of `hasClientWindow`. - /// Doing this selectively ensures that we don't pull data from target buffers until we have an output window, and also helps us drain buffers after the target has entered a `is_halted` state. - /// Errors will be reported back to the `debug_adapter`, and the return `bool` value indicates whether there was available data that was processed. - pub(crate) fn poll_rtt_data( - &mut self, - core: &mut Core, - debug_adapter: &mut DebugAdapter

, - rtt_target: &mut rtt::RttActiveTarget, - ) -> bool { - if self.has_client_window { - rtt_target - .active_channels - .iter_mut() - .find(|active_channel| { - if let Some(channel_number) = active_channel.number() { - channel_number == self.channel_number - } else { - false - } - }) - .and_then(|rtt_channel| { - rtt_channel.get_rtt_data(core, rtt_target.defmt_state.as_ref()) - }) - .and_then(|(channel_number, channel_data)| { - if debug_adapter - .rtt_output(channel_number.parse::().unwrap_or(0), channel_data) - { - Some(true) - } else { - None - } - }) - .is_some() - } else { - false - } - } -} - -/// [CoreData] provides handles to various data structures required to debug a single instance of a core. The actual state is stored in [SessionData]. -/// -/// Usage: To get access to this structure please use the [DebugSession::attach_core] method. Please keep access/locks to this to a minumum duration. -pub struct CoreData<'p> { - pub(crate) target_core: Core<'p>, - pub(crate) target_name: String, - pub(crate) debug_info: &'p DebugInfo, - pub(crate) stack_frames: &'p mut Vec, - pub(crate) capstone: &'p Capstone, - pub(crate) breakpoints: &'p mut Vec, - pub(crate) rtt_connection: &'p mut Option, -} - -impl<'p> CoreData<'p> { - /// Search available [StackFrame]'s for the given `id` - pub(crate) fn get_stackframe(&'p self, id: i64) -> Option<&'p probe_rs::debug::StackFrame> { - self.stack_frames - .iter() - .find(|stack_frame| stack_frame.id == id) - } - - /// Confirm RTT initialization on the target, and use the RTT channel configurations to initialize the output windows on the DAP Client. - pub fn attach_to_rtt( - &mut self, - debug_adapter: &mut DebugAdapter

, - target_memory_map: &[probe_rs::config::MemoryRegion], - program_binary: &std::path::Path, - rtt_config: &rtt::RttConfig, - ) -> Result<()> { - let mut debugger_rtt_channels: Vec = vec![]; - match rtt::attach_to_rtt( - &mut self.target_core, - target_memory_map, - program_binary, - rtt_config, - ) { - Ok(target_rtt) => { - for any_channel in target_rtt.active_channels.iter() { - if let Some(up_channel) = &any_channel.up_channel { - debugger_rtt_channels.push(DebuggerRttChannel { - channel_number: up_channel.number(), - // This value will eventually be set to true by a VSCode client request "rttWindowOpened" - has_client_window: false, - }); - debug_adapter.rtt_window( - up_channel.number(), - any_channel.channel_name.clone(), - any_channel.data_format, - ); - } - } - *self.rtt_connection = Some(RttConnection { - target_rtt, - debugger_rtt_channels, - }); - } - Err(_error) => { - log::warn!("Failed to initalize RTT. Will try again on the next request... "); - } - }; - Ok(()) - } - - /// Set a single breakpoint in target configuration as well as [`CoreData::breakpoints`] - pub(crate) fn set_breakpoint( - &mut self, - address: u32, - breakpoint_type: BreakpointType, - ) -> Result<(), DebuggerError> { - self.target_core - .set_hw_breakpoint(address) - .map_err(DebuggerError::ProbeRs)?; - self.breakpoints.push(ActiveBreakpoint { - breakpoint_type, - breakpoint_address: address, - }); - Ok(()) - } - - /// Clear a single breakpoint from target configuration as well as [`CoreData::breakpoints`] - pub(crate) fn clear_breakpoint(&mut self, address: u32) -> Result<()> { - self.target_core - .clear_hw_breakpoint(address) - .map_err(DebuggerError::ProbeRs)?; - let mut breakpoint_position: Option = None; - for (position, active_breakpoint) in self.breakpoints.iter().enumerate() { - if active_breakpoint.breakpoint_address == address { - breakpoint_position = Some(position); - break; - } - } - if let Some(breakpoint_position) = breakpoint_position { - self.breakpoints.remove(breakpoint_position as usize); - } - Ok(()) - } - - /// Clear all breakpoints of a specified [`BreakpointType`]. Affects target configuration as well as [`CoreData::breakpoints`] - pub(crate) fn clear_breakpoints(&mut self, breakpoint_type: BreakpointType) -> Result<()> { - let target_breakpoints = self - .breakpoints - .iter() - .filter(|breakpoint| breakpoint.breakpoint_type == breakpoint_type) - .map(|breakpoint| breakpoint.breakpoint_address) - .collect::>(); - for breakpoint in target_breakpoints { - self.clear_breakpoint(breakpoint).ok(); - } - Ok(()) - } -} - #[derive(Debug)] /// The `DebuggerStatus` is used to control how the Debugger::debug_session() decides if it should respond to DAP Client requests such as `Terminate`, `Disconnect`, and `Reset`, as well as how to repond to unrecoverable errors during a debug session interacting with a target session. pub(crate) enum DebuggerStatus { @@ -649,78 +58,70 @@ pub(crate) enum DebuggerStatus { /// - In this case, the management (start and stop) of the server process is the responsibility of the user. e.g. /// - `probe-rs-debug --debug --port ` : Uses TCP Sockets to the defined IP port number to service DAP requests. pub struct Debugger { - debugger_options: DebuggerOptions, + config: configuration::SessionConfig, } impl Debugger { - pub fn new(debugger_options: DebuggerOptions) -> Self { - // Define all the commands supported by the debugger. - // TODO: There is a lot of repetitive code here, and a great opportunity for macros. - Self { debugger_options } + pub fn new(port: Option) -> Self { + Self { + config: configuration::SessionConfig { + port, + ..Default::default() + }, + } } + /// The logic of this function is as follows: + /// - While we are waiting for DAP-Client (TCP or STDIO), we have to continuously check in on the status of the probe. + /// - Initally, while `LAST_KNOWN_STATUS` probe-rs::CoreStatus::Unknown, we do nothing. Wait until latter part of `debug_session` sets it to something known. + /// - If the `LAST_KNOWN_STATUS` is `Halted`, then we stop polling the Probe until the next DAP-Client request attempts an action + /// - If the `new_status` is an Err, then the probe is no longer available, and we end the debugging session + /// - If the `new_status` is different from the `LAST_KNOWN_STATUS`, then we have to tell the DAP-Client by way of an `Event` + /// - If the `new_status` is `Running`, then we have to poll on a regular basis, until the Probe stops for good reasons like breakpoints, or bad reasons like panics. Then tell the DAP-Client. pub(crate) fn process_next_request( &mut self, - session_data: &mut DebugSession, + session_data: &mut session_data::SessionData, debug_adapter: &mut DebugAdapter

, ) -> Result { let request = debug_adapter.listen_for_request()?; match request { None => { - /* - The logic of this command is as follows: - - While we are waiting for DAP-Client (TCP or STDIO), we have to continuously check in on the status of the probe. - - Initally, while `LAST_KNOWN_STATUS` probe-rs::CoreStatus::Unknown, we do nothing. Wait until latter part of `debug_session` sets it to something known. - - If the `LAST_KNOWN_STATUS` is `Halted`, then we stop polling the Probe until the next DAP-Client request attempts an action - - If the `new_status` is an Err, then the probe is no longer available, and we end the debugging session - - If the `new_status` is different from the `LAST_KNOWN_STATUS`, then we have to tell the DAP-Client by way of an `Event` - - If the `new_status` is `Running`, then we have to poll on a regular basis, until the Probe stops for good reasons like breakpoints, or bad reasons like panics. Then tell the DAP-Client. - */ + // If there are no requests, we poll target cores for status, which includes handling RTT data. match debug_adapter.last_known_status { - CoreStatus::Unknown => Ok(DebuggerStatus::ContinueSession), // Don't do anything until we know VSCode's startup sequence is complete, and changes this to either Halted or Running. + CoreStatus::Unknown => { + // Don't do anything until we know VSCode's startup sequence is complete, and changes this to either Halted or Running. + Ok(DebuggerStatus::ContinueSession) + } CoreStatus::Halted(_) => { - // Make sure the RTT buffers are drained. - match session_data.attach_core(self.debugger_options.core_index) { - Ok(mut core_data) => { - if let Some(rtt_active_target) = &mut core_data.rtt_connection { - rtt_active_target.process_rtt_data( - debug_adapter, - &mut core_data.target_core, - ); - }; - } - Err(error) => { - let _ = debug_adapter.send_error_response(&error)?; - return Err(error); - } - }; - + session_data.poll_rtt(&self.config, debug_adapter); // No need to poll the target status if we know it is halted and waiting for us to do something. thread::sleep(Duration::from_millis(50)); // Small delay to reduce fast looping costs on the client Ok(DebuggerStatus::ContinueSession) } _other => { - let mut received_rtt_data = false; - let mut core_data = - match session_data.attach_core(self.debugger_options.core_index) { - Ok(mut core_data) => { - // Use every opportunity to poll the RTT channels for data - if let Some(rtt_active_target) = &mut core_data.rtt_connection { - received_rtt_data = rtt_active_target.process_rtt_data( - debug_adapter, - &mut core_data.target_core, - ); - } - core_data - } - Err(error) => { - let _ = debug_adapter.send_error_response(&error)?; - return Err(error); - } - }; + let received_rtt_data = session_data.poll_rtt(&self.config, debug_adapter); // Check and update the core status. - let new_status = match core_data.target_core.status() { + // TODO: This only works for a single core, so until it can be redesigned, will use the first one configured. + let mut target_core = if let Some(target_core_config) = + self.config.core_configs.first_mut() + { + if let Ok(core_handle) = + session_data.attach_core(target_core_config.core_index) + { + core_handle + } else { + return Err(DebuggerError::Other(anyhow!( + "Unable to connect to target core" + ))); + } + } else { + return Err(DebuggerError::Other(anyhow!( + "Cannot continue unless one target core configuration is defined." + ))); + }; + + let new_status = match target_core.core.status() { Ok(new_status) => new_status, Err(error) => { let error = DebuggerError::ProbeRs(error); @@ -745,7 +146,7 @@ impl Debugger { CoreStatus::Running | CoreStatus::Sleeping => { let event_body = Some(ContinuedEventBody { all_threads_continued: Some(true), - thread_id: core_data.target_core.id() as i64, + thread_id: target_core.core.id() as i64, }); debug_adapter.send_event("continued", event_body)?; } @@ -753,7 +154,7 @@ impl Debugger { let event_body = Some(StoppedEventBody { reason: new_status.short_long_status().0.to_owned(), description: Some(new_status.short_long_status().1.to_owned()), - thread_id: Some(core_data.target_core.id() as i64), + thread_id: Some(target_core.core.id() as i64), preserve_focus_hint: Some(false), text: None, all_threads_stopped: Some(true), @@ -787,17 +188,22 @@ impl Debugger { } Some(request) => { // First, attach to the core. - let mut core_data = match session_data.attach_core(self.debugger_options.core_index) + // TODO: This only works for a single core, so until it can be redesigned, will use the first one configured. + let mut target_core = if let Some(target_core_config) = + self.config.core_configs.first_mut() { - Ok(core_data) => core_data, - Err(error) => { - let failed_command = request.command.clone(); - debug_adapter.send_response::<()>(request, Err(error))?; + if let Ok(core_handle) = session_data.attach_core(target_core_config.core_index) + { + core_handle + } else { return Err(DebuggerError::Other(anyhow!( - "Error while attaching to core. Could not complete command {}", - failed_command + "Unable to connect to target core" ))); } + } else { + return Err(DebuggerError::Other(anyhow!( + "Cannot continue unless one target core configuration is defined." + ))); }; // For some operations, we need to make sure the core isn't sleeping, by calling `Core::halt()`. @@ -817,10 +223,10 @@ impl Debugger { | "readMemory" | "writeMemory" | "disassemble" => { - match core_data.target_core.status() { + match target_core.core.status() { Ok(current_status) => { if current_status == CoreStatus::Sleeping { - match core_data.target_core.halt(Duration::from_millis(100)) { + match target_core.core.halt(Duration::from_millis(100)) { Ok(_) => { debug_adapter.last_known_status = CoreStatus::Halted(probe_rs::HaltReason::Request); @@ -855,7 +261,9 @@ impl Debugger { // Now we are ready to execute supported commands, or return an error if it isn't supported. match match request.command.clone().as_ref() { "rttWindowOpened" => { - if let Some(debugger_rtt_target) = core_data.rtt_connection { + if let Some(debugger_rtt_target) = + target_core.core_data.rtt_connection.as_mut() + { match get_arguments::(&request) { Ok(arguments) => { debugger_rtt_target @@ -889,68 +297,68 @@ impl Debugger { .send_response::<()>(request, Ok(None)) .and(Ok(DebuggerStatus::TerminateSession)), "terminate" => debug_adapter - .pause(&mut core_data, request) + .pause(&mut target_core, request) .and(Ok(DebuggerStatus::TerminateSession)), "status" => debug_adapter - .status(&mut core_data, request) + .status(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "next" => debug_adapter - .next(&mut core_data, request) + .next(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "stepIn" => debug_adapter - .step_in(&mut core_data, request) + .step_in(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "stepOut" => debug_adapter - .step_out(&mut core_data, request) + .step_out(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "pause" => debug_adapter - .pause(&mut core_data, request) + .pause(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "readMemory" => debug_adapter - .read_memory(&mut core_data, request) + .read_memory(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "writeMemory" => debug_adapter - .write_memory(&mut core_data, request) + .write_memory(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "setVariable" => debug_adapter - .set_variable(&mut core_data, request) + .set_variable(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "configurationDone" => debug_adapter - .configuration_done(&mut core_data, request) + .configuration_done(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "threads" => debug_adapter - .threads(&mut core_data, request) + .threads(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "restart" => { // Reset RTT so that the link can be re-established - *core_data.rtt_connection = None; + target_core.core_data.rtt_connection = None; debug_adapter - .restart(&mut core_data, Some(request)) + .restart(&mut target_core, Some(request)) .and(Ok(DebuggerStatus::ContinueSession)) } "setBreakpoints" => debug_adapter - .set_breakpoints(&mut core_data, request) + .set_breakpoints(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "setInstructionBreakpoints" => debug_adapter - .set_instruction_breakpoints(&mut core_data, request) + .set_instruction_breakpoints(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "stackTrace" => debug_adapter - .stack_trace(&mut core_data, request) + .stack_trace(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "scopes" => debug_adapter - .scopes(&mut core_data, request) + .scopes(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "disassemble" => debug_adapter - .disassemble(&mut core_data, request) + .disassemble(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "variables" => debug_adapter - .variables(&mut core_data, request) + .variables(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "continue" => debug_adapter - .r#continue(&mut core_data, request) + .r#continue(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), "evaluate" => debug_adapter - .evaluate(&mut core_data, request) + .evaluate(&mut target_core, request) .and(Ok(DebuggerStatus::ContinueSession)), other_command => { // Unimplemented command. @@ -962,7 +370,7 @@ impl Debugger { } { Ok(debugger_status) => { if unhalt_me { - match core_data.target_core.run() { + match target_core.core.run() { Ok(_) => debug_adapter.last_known_status = CoreStatus::Running, Err(error) => { debug_adapter.send_error_response(&DebuggerError::Other( @@ -1077,7 +485,7 @@ impl Debugger { // Process either the Launch or Attach request. let requested_target_session_type: Option; - let la_request = loop { + let launch_attach_request = loop { let current_request = if let Some(request) = debug_adapter.listen_for_request()? { request } else { @@ -1108,23 +516,24 @@ impl Debugger { }; }; - match get_arguments(&la_request) { + // TODO: Multi-core: This currently only supports the first `SessionConfig::core_configs` + match get_arguments(&launch_attach_request) { Ok(arguments) => { if requested_target_session_type.is_some() { - self.debugger_options = DebuggerOptions { ..arguments }; + self.config = configuration::SessionConfig { ..arguments }; if matches!( requested_target_session_type, Some(TargetSessionType::AttachRequest) ) { // Since VSCode doesn't do field validation checks for relationships in launch.json request types, check it here. - if self.debugger_options.flashing_enabled - || self.debugger_options.reset_after_flashing - || self.debugger_options.halt_after_reset - || self.debugger_options.full_chip_erase - || self.debugger_options.restore_unwritten_bytes + if self.config.flashing_config.flashing_enabled + || self.config.flashing_config.reset_after_flashing + || self.config.flashing_config.halt_after_reset + || self.config.flashing_config.full_chip_erase + || self.config.flashing_config.restore_unwritten_bytes { debug_adapter.send_response::<()>( - la_request, + launch_attach_request, Err(DebuggerError::Other(anyhow!( "Please do not use any of the `flashing_enabled`, `reset_after_flashing`, halt_after_reset`, `full_chip_erase`, or `restore_unwritten_bytes` options when using `attach` request type."))), )?; @@ -1135,90 +544,56 @@ impl Debugger { } } debug_adapter.set_console_log_level( - self.debugger_options - .console_log_level - .unwrap_or(ConsoleLog::Error), + self.config.console_log_level.unwrap_or(ConsoleLog::Error), ); - // Update the `cwd` and `program_binary`. - self.debugger_options - .validate_and_update_cwd(self.debugger_options.cwd.clone()); - match self - .debugger_options - .qualify_and_update_program_binary(self.debugger_options.program_binary.clone()) - { - Ok(_) => {} - Err(error) => { - let err = DebuggerError::Other(anyhow!( - "Unable to validate the program_binary path '{:?}'", - error - )); - - debug_adapter.send_error_response(&err)?; - return Err(err); - } - } - match self.debugger_options.program_binary.clone() { - Some(program_binary) => { - if !program_binary.is_file() { - debug_adapter.send_response::<()>( - la_request, - Err(DebuggerError::Other(anyhow!( - "Invalid program binary file specified '{:?}'", - program_binary - ))), - )?; - return Err(DebuggerError::Other(anyhow!( - "Invalid program binary file specified '{:?}'", - program_binary - ))); - } - } - None => { - debug_adapter.send_response::<()>( - la_request, - Err(DebuggerError::Other(anyhow!( - "Please use the --program-binary option to specify an executable" - ))), - )?; - - return Err(DebuggerError::Other(anyhow!( - "Please use the --program-binary option to specify an executable" - ))); - } - } - debug_adapter.send_response::<()>(la_request, Ok(None))?; } Err(error) => { - let err_1 = anyhow!( - - "Could not derive DebuggerOptions from request '{}', with arguments {:?}\n{:?} ", la_request.command, la_request.arguments, error - - ); - let err_2 =anyhow!( - - "Could not derive DebuggerOptions from request '{}', with arguments {:?}\n{:?} ", la_request.command, la_request.arguments, error - - ); + let error_message = format!( + "Could not derive SessionConfig from request '{}', with arguments {:?}\n{:?} ", + launch_attach_request.command, launch_attach_request.arguments, error + ); + debug_adapter.send_response::<()>( + launch_attach_request, + Err(DebuggerError::Other(anyhow!(error_message.clone()))), + )?; - debug_adapter.send_response::<()>(la_request, Err(DebuggerError::Other(err_1)))?; + return Err(DebuggerError::Other(anyhow!(error_message))); + } + }; - return Err(DebuggerError::Other(err_2)); + // Validate file specifications in the config. + match self.config.validate_config_files() { + Ok(_) => {} + Err(error) => { + return Err(error); } }; - let mut session_data = match DebugSession::new(&self.debugger_options) { + let mut session_data = match session_data::SessionData::new(&self.config) { Ok(session_data) => session_data, Err(error) => { debug_adapter.send_error_response(&error)?; return Err(error); } }; - debug_adapter.halt_after_reset = self.debugger_options.halt_after_reset; + // TODO: Currently the logic of processing MS DAP requests and executing them, is based on having a single core. It needs to be re-thought for multiple cores. Not all DAP requests require access to the core. One possible is to do the core attach inside each of the request implementations for those that need it, because the applicable core_index can be read from the request arguments. + // TODO: Until we refactor this, we only support a single core (always the first one specified in `SessionConfig::core_configs`) + let target_core_config = + if let Some(target_core_config) = self.config.core_configs.first_mut() { + target_core_config + } else { + return Err(DebuggerError::Other(anyhow!( + "Cannot continue unless one target core configuration is defined." + ))); + }; + + debug_adapter.halt_after_reset = self.config.flashing_config.halt_after_reset; // Do the flashing. + // TODO: Multi-core ... needs to flash multiple binaries { - if self.debugger_options.flashing_enabled { - let path_to_elf = match self.debugger_options.program_binary.clone() { + if self.config.flashing_config.flashing_enabled { + let path_to_elf = match &target_core_config.program_binary { Some(program_binary) => program_binary, None => { let err = DebuggerError::Other(anyhow!( @@ -1233,12 +608,14 @@ impl Debugger { &path_to_elf )); - let progress_id = debug_adapter.start_progress("Flashing device", None).ok(); + let progress_id = debug_adapter + .start_progress("Flashing device", Some(launch_attach_request.seq)) + .ok(); let mut download_options = DownloadOptions::default(); download_options.keep_unwritten_bytes = - self.debugger_options.restore_unwritten_bytes; - download_options.do_chip_erase = self.debugger_options.full_chip_erase; + self.config.flashing_config.restore_unwritten_bytes; + download_options.do_chip_erase = self.config.flashing_config.full_chip_erase; let flash_result = { let rc_debug_adapter = Rc::new(RefCell::new(debug_adapter)); let rc_debug_adapter_clone = rc_debug_adapter.clone(); @@ -1289,7 +666,11 @@ impl Debugger { } probe_rs::flashing::ProgressEvent::StartedFilling => { debug_adapter - .update_progress(0.0, Some("Reading Old Pages ..."), id) + .update_progress( + Some(0.0), + Some("Reading Old Pages ..."), + id, + ) .ok(); } probe_rs::flashing::ProgressEvent::PageFilled { @@ -1300,7 +681,7 @@ impl Debugger { / flash_progress.total_fill_size as f64; debug_adapter .update_progress( - progress, + Some(progress), Some(format!("Reading Old Pages ({})", progress)), id, ) @@ -1309,7 +690,7 @@ impl Debugger { probe_rs::flashing::ProgressEvent::FailedFilling => { debug_adapter .update_progress( - 1.0, + Some(1.0), Some("Reading Old Pages Failed!"), id, ) @@ -1318,7 +699,7 @@ impl Debugger { probe_rs::flashing::ProgressEvent::FinishedFilling => { debug_adapter .update_progress( - 1.0, + Some(1.0), Some("Reading Old Pages Complete!"), id, ) @@ -1326,7 +707,11 @@ impl Debugger { } probe_rs::flashing::ProgressEvent::StartedErasing => { debug_adapter - .update_progress(0.0, Some("Erasing Sectors ..."), id) + .update_progress( + Some(0.0), + Some("Erasing Sectors ..."), + id, + ) .ok(); } probe_rs::flashing::ProgressEvent::SectorErased { @@ -1338,7 +723,7 @@ impl Debugger { / flash_progress.total_sector_size as f64; debug_adapter .update_progress( - progress, + Some(progress), Some(format!("Erasing Sectors ({})", progress)), id, ) @@ -1347,7 +732,7 @@ impl Debugger { probe_rs::flashing::ProgressEvent::FailedErasing => { debug_adapter .update_progress( - 1.0, + Some(1.0), Some("Erasing Sectors Failed!"), id, ) @@ -1356,7 +741,7 @@ impl Debugger { probe_rs::flashing::ProgressEvent::FinishedErasing => { debug_adapter .update_progress( - 1.0, + Some(1.0), Some("Erasing Sectors Complete!"), id, ) @@ -1364,7 +749,11 @@ impl Debugger { } probe_rs::flashing::ProgressEvent::StartedProgramming => { debug_adapter - .update_progress(0.0, Some("Programming Pages ..."), id) + .update_progress( + Some(0.0), + Some("Programming Pages ..."), + id, + ) .ok(); } probe_rs::flashing::ProgressEvent::PageProgrammed { @@ -1376,7 +765,7 @@ impl Debugger { / flash_progress.total_page_size as f64; debug_adapter .update_progress( - progress, + Some(progress), Some(format!( "Programming Pages ({:02.0}%)", progress.mul(100_f64) @@ -1388,7 +777,7 @@ impl Debugger { probe_rs::flashing::ProgressEvent::FailedProgramming => { debug_adapter .update_progress( - 1.0, + Some(1.0), Some("Flashing Pages Failed!"), id, ) @@ -1397,7 +786,7 @@ impl Debugger { probe_rs::flashing::ProgressEvent::FinishedProgramming => { debug_adapter .update_progress( - 1.0, + Some(1.0), Some("Flashing Pages Complete!"), id, ) @@ -1449,18 +838,34 @@ impl Debugger { // This is the first attach to the requested core. If this one works, all subsequent ones will be no-op requests for a Core reference. Do NOT hold onto this reference for the duration of the session ... that is why this code is in a block of its own. { // First, attach to the core - let mut core_data = match session_data.attach_core(self.debugger_options.core_index) { - Ok(mut core_data) => { + let mut target_core = match session_data.attach_core(target_core_config.core_index) { + Ok(mut target_core) => { // Immediately after attaching, halt the core, so that we can finish initalization without bumping into user code. - // Depending on supplied `debugger_options`, the core will be restarted at the end of initialization in the `configuration_done` request. - match halt_core(&mut core_data.target_core) { + // Depending on supplied `config`, the core will be restarted at the end of initialization in the `configuration_done` request. + match halt_core(&mut target_core.core) { Ok(_) => {} Err(error) => { debug_adapter.send_error_response(&error)?; return Err(error); } } - core_data + // Before we complete, load the (optional) CMSIS-SVD file and its variable cache. + // Configure the [CorePeripherals]. + if let Some(svd_file) = &target_core_config.svd_file { + target_core.core_data.core_peripherals = match SvdCache::new( + svd_file, + &mut target_core.core, + &mut debug_adapter, + launch_attach_request.seq, + ) { + Ok(core_peripherals) => Some(core_peripherals), + Err(error) => { + log::error!("{:?}", error); + None + } + }; + } + target_core } Err(error) => { debug_adapter.send_error_response(&error)?; @@ -1468,16 +873,18 @@ impl Debugger { } }; - if self.debugger_options.flashing_enabled && self.debugger_options.reset_after_flashing + if self.config.flashing_config.flashing_enabled + && self.config.flashing_config.reset_after_flashing { debug_adapter - .restart(&mut core_data, None) + .restart(&mut target_core, None) .context("Failed to restart core")?; } } // After flashing and forced setup, we can signal the client that are ready to receive incoming requests. // Send the `initalized` event to client. + debug_adapter.send_response::<()>(launch_attach_request, Ok(None))?; if debug_adapter .send_event::("initialized", None) .is_err() @@ -1494,35 +901,7 @@ impl Debugger { loop { match self.process_next_request(&mut session_data, &mut debug_adapter) { Ok(DebuggerStatus::ContinueSession) => { - // Validate and if necessary, initialize the RTT structure. - if self.debugger_options.rtt.enabled - && session_data.rtt_connection.is_none() - && !(debug_adapter.last_known_status == CoreStatus::Unknown - || debug_adapter.last_known_status.is_halted()) - // Do not attempt this until we have processed the MSDAP request for "configurationDone" ... - { - let target_memory_map = session_data.session.target().memory_map.clone(); - let mut core_data = - match session_data.attach_core(self.debugger_options.core_index) { - Ok(core_data) => core_data, - Err(error) => { - debug_adapter.send_error_response(&error)?; - return Err(error); - } - }; - log::info!("Attempting to initialize the RTT."); - // RTT can only be initialized if the target application has been allowed to run to the point where it does the RTT initialization. - // If the target halts before it processes this code, then this RTT intialization silently fails, and will try again later ... - // See `probe-rs-rtt::Rtt` for more information. - // We can safely unwrap() program_binary here, because it is validated to exist at startup of the debugger - #[allow(clippy::unwrap_used)] - core_data.attach_to_rtt( - &mut debug_adapter, - &target_memory_map, - self.debugger_options.program_binary.as_ref().unwrap(), - &self.debugger_options.rtt, - )?; - } + // All is good. We can process the next request. } Ok(DebuggerStatus::TerminateSession) => { return Ok(DebuggerStatus::TerminateSession); @@ -1579,16 +958,16 @@ pub fn list_supported_chips() -> Result<()> { Ok(()) } -pub fn debug(debugger_options: DebuggerOptions, vscode: bool) -> Result<()> { +pub fn debug(port: Option, vscode: bool) -> Result<()> { let program_name = clap::crate_name!(); - let mut debugger = Debugger::new(debugger_options); + let mut debugger = Debugger::new(port); println!( "{} CONSOLE: Starting as a DAP Protocol server", &program_name ); - match &debugger.debugger_options.port.clone() { + match &debugger.config.port.clone() { Some(port) => { let addr = std::net::SocketAddr::new( std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), @@ -1628,7 +1007,14 @@ pub fn debug(debugger_options: DebuggerOptions, vscode: bool) -> Result<()> { let debug_adapter = DebugAdapter::new(dap_adapter); match debugger.debug_session(debug_adapter) { - Err(_) | Ok(DebuggerStatus::TerminateSession) => { + Err(error) => { + log::error!("probe-rs-debugger session ended: {}", &error); + println!( + "{} CONSOLE: ....Closing session from :{}, due to error: {}", + &program_name, addr, error + ); + } + Ok(DebuggerStatus::TerminateSession) => { println!( "{} CONSOLE: ....Closing session from :{}", &program_name, addr diff --git a/debugger/src/debugger/debug_rtt.rs b/debugger/src/debugger/debug_rtt.rs new file mode 100644 index 0000000000..f9455f5c44 --- /dev/null +++ b/debugger/src/debugger/debug_rtt.rs @@ -0,0 +1,74 @@ +use crate::debug_adapter::{dap_adapter::*, protocol::ProtocolAdapter}; +use probe_rs::Core; +use probe_rs_cli_util::rtt; + +/// Manage the active RTT target for a specific SessionData, as well as provide methods to reliably move RTT from target, through the debug_adapter, to the client. +pub(crate) struct RttConnection { + /// The connection to RTT on the target + pub(crate) target_rtt: rtt::RttActiveTarget, + /// Some status fields and methods to ensure continuity in flow of data from target to debugger to client. + pub(crate) debugger_rtt_channels: Vec, +} + +impl RttConnection { + /// Polls all the available channels for data and transmits data to the client. + /// If at least one channel had data, then return a `true` status. + pub fn process_rtt_data( + &mut self, + debug_adapter: &mut DebugAdapter

, + target_core: &mut Core, + ) -> bool { + let mut at_least_one_channel_had_data = false; + for debugger_rtt_channel in self.debugger_rtt_channels.iter_mut() { + at_least_one_channel_had_data |= + debugger_rtt_channel.poll_rtt_data(target_core, debug_adapter, &mut self.target_rtt) + } + at_least_one_channel_had_data + } +} + +pub(crate) struct DebuggerRttChannel { + pub(crate) channel_number: usize, + // We will not poll target RTT channels until we have confirmation from the client that the output window has been opened. + pub(crate) has_client_window: bool, +} + +impl DebuggerRttChannel { + /// Poll and retrieve data from the target, and send it to the client, depending on the state of `hasClientWindow`. + /// Doing this selectively ensures that we don't pull data from target buffers until we have an output window, and also helps us drain buffers after the target has entered a `is_halted` state. + /// Errors will be reported back to the `debug_adapter`, and the return `bool` value indicates whether there was available data that was processed. + pub(crate) fn poll_rtt_data( + &mut self, + core: &mut Core, + debug_adapter: &mut DebugAdapter

, + rtt_target: &mut rtt::RttActiveTarget, + ) -> bool { + if self.has_client_window { + rtt_target + .active_channels + .iter_mut() + .find(|active_channel| { + if let Some(channel_number) = active_channel.number() { + channel_number == self.channel_number + } else { + false + } + }) + .and_then(|rtt_channel| { + rtt_channel.get_rtt_data(core, rtt_target.defmt_state.as_ref()) + }) + .and_then(|(channel_number, channel_data)| { + if debug_adapter + .rtt_output(channel_number.parse::().unwrap_or(0), channel_data) + { + Some(true) + } else { + None + } + }) + .is_some() + } else { + false + } + } +} diff --git a/debugger/src/debugger/mod.rs b/debugger/src/debugger/mod.rs new file mode 100644 index 0000000000..b0bd1d6c9d --- /dev/null +++ b/debugger/src/debugger/mod.rs @@ -0,0 +1,10 @@ +/// All the shared options that control the behaviour of the debugger. +pub(crate) mod configuration; +/// The data structures borrowed from the [`SessionData`], that applies to a specific core. +pub(crate) mod core_data; +/// This is where the primary processing for the debugger is driven from. +pub(crate) mod debug_entry; +/// The debugger support for rtt. +pub(crate) mod debug_rtt; +/// The data structures needed to keep track of a [`SessionData`]. +pub(crate) mod session_data; diff --git a/debugger/src/debugger/session_data.rs b/debugger/src/debugger/session_data.rs new file mode 100644 index 0000000000..6176735a83 --- /dev/null +++ b/debugger/src/debugger/session_data.rs @@ -0,0 +1,282 @@ +use super::{ + configuration::{self, CoreConfig, SessionConfig}, + core_data::{CoreData, CoreHandle}, +}; +use crate::{ + debug_adapter::{dap_adapter::DebugAdapter, protocol::ProtocolAdapter}, + DebuggerError, +}; +use anyhow::{anyhow, Result}; +use capstone::{ + arch::arm::ArchMode as armArchMode, arch::riscv::ArchMode as riscvArchMode, prelude::*, + Capstone, Endian, +}; +use probe_rs::{ + config::TargetSelector, debug::DebugInfo, CoreStatus, DebugProbeError, Permissions, Probe, + ProbeCreationError, Session, +}; +use std::env::set_current_dir; + +/// The supported breakpoint types +#[derive(Debug, PartialEq)] +pub enum BreakpointType { + InstructionBreakpoint, + SourceBreakpoint, +} + +/// Provide the storage and methods to handle various [`BreakPointType`] +#[derive(Debug)] +pub struct ActiveBreakpoint { + pub(crate) breakpoint_type: BreakpointType, + pub(crate) breakpoint_address: u32, +} + +/// SessionData is designed to be similar to [probe_rs::Session], in as much that it provides handles to the [CoreHandle] instances for each of the available [probe_rs::Core] involved in the debug session. +/// To get access to the [CoreHandle] for a specific [Core], the +/// TODO: Adjust [SessionConfig] to allow multiple cores (and if appropriate, their binaries) to be specified. +pub struct SessionData { + pub(crate) session: Session, + /// Provides ability to disassemble binary code. + pub(crate) capstone: Capstone, + /// [SessionData] will manage one [CoreData] per target core, that is also present in [SessionConfig::core_configs] + pub(crate) core_data: Vec, +} + +impl SessionData { + pub(crate) fn new(config: &configuration::SessionConfig) -> Result { + // `SessionConfig` Probe/Session level configurations initialization. + let mut target_probe = match config.probe_selector.clone() { + Some(selector) => Probe::open(selector.clone()).map_err(|e| match e { + DebugProbeError::ProbeCouldNotBeCreated(ProbeCreationError::NotFound) => { + DebuggerError::Other(anyhow!( + "Could not find the probe_selector specified as {:04x}:{:04x}:{:?}", + selector.vendor_id, + selector.product_id, + selector.serial_number + )) + } + other_error => DebuggerError::DebugProbe(other_error), + }), + None => { + // Only automatically select a probe if there is only a single probe detected. + let list = Probe::list_all(); + if list.len() > 1 { + return Err(DebuggerError::Other(anyhow!( + "Found multiple ({}) probes", + list.len() + ))); + } + + if let Some(info) = list.first() { + Probe::open(info).map_err(DebuggerError::DebugProbe) + } else { + return Err(DebuggerError::Other(anyhow!( + "No probes found. Please check your USB connections." + ))); + } + } + }?; + + let target_selector = match &config.chip { + Some(identifier) => identifier.into(), + None => TargetSelector::Auto, + }; + + // Set the protocol, if the user explicitly selected a protocol. Otherwise, use the default protocol of the probe. + if let Some(protocol) = config.protocol { + target_probe.select_protocol(protocol)?; + } + + // Set the speed. + if let Some(speed) = config.speed { + let actual_speed = target_probe.set_speed(speed)?; + if actual_speed != speed { + log::warn!( + "Protocol speed {} kHz not supported, actual speed is {} kHz", + speed, + actual_speed + ); + } + } + + let mut permissions = Permissions::new(); + if config.allow_erase_all { + permissions = permissions.allow_erase_all(); + } + + // Attach to the probe. + let target_session = if config.connect_under_reset { + target_probe.attach_under_reset(target_selector, permissions)? + } else { + target_probe + .attach(target_selector, permissions) + .map_err(|err| { + anyhow!( + "Error attaching to the probe: {:?}.\nTry the --connect-under-reset option", + err + ) + })? + }; + + // Create an instance of the [`capstone::Capstone`] for disassembly capabilities. + // TODO: I believe it is safe to share this between multiple cores, but needs to be tested. + let capstone = match target_session.architecture() { + probe_rs::Architecture::Arm => Capstone::new() + .arm() + .mode(armArchMode::Thumb) + .endian(Endian::Little) + .build() + .map_err(|err| anyhow!("Error creating Capstone disassembler: {:?}", err))?, + probe_rs::Architecture::Riscv => Capstone::new() + .riscv() + .mode(riscvArchMode::RiscV32) + .endian(Endian::Little) + .build() + .map_err(|err| anyhow!("Error creating Capstone disassembler: {:?}", err))?, + }; + + // Change the current working directory if `config.cwd` is `Some(T)`. + if let Some(new_cwd) = config.cwd.clone() { + set_current_dir(new_cwd.as_path()).map_err(|err| { + anyhow!( + "Failed to set current working directory to: {:?}, {:?}", + new_cwd, + err + ) + })?; + }; + + // `FlashingConfig` probe level initialization. + + // `CoreConfig` probe level initialization. + if config.core_configs.len() != 1 { + // TODO: For multi-core, allow > 1. + return Err(DebuggerError::Other(anyhow!("probe-rs-debugger requires that one, and only one, core be configured for debugging."))); + } + + // Filter `CoreConfig` entries based on those that match an actual core on the target probe. + let valid_core_configs = config + .core_configs + .iter() + .filter(|&core_config| { + matches!( + target_session + .list_cores() + .iter() + .find(|(target_core_index, _)| *target_core_index + == core_config.core_index), + Some(_) + ) + }) + .cloned() + .collect::>(); + + let mut core_data_vec = vec![]; + + for core_configuration in &valid_core_configs { + // Configure the [DebugInfo]. + let debug_info = if let Some(binary_path) = &core_configuration.program_binary { + DebugInfo::from_file(binary_path) + .map_err(|error| DebuggerError::Other(anyhow!(error)))? + } else { + return Err(anyhow!( + "Please provide a valid `program_binary` for debug core: {:?}", + core_configuration.core_index + ) + .into()); + }; + + core_data_vec.push(CoreData { + core_index: core_configuration.core_index, + target_name: format!( + "{}-{}", + core_configuration.core_index, + target_session.target().name + ), + debug_info, + core_peripherals: None, + stack_frames: Vec::::new(), + breakpoints: Vec::::new(), + rtt_connection: None, + }) + } + + Ok(SessionData { + session: target_session, + capstone, + core_data: core_data_vec, + }) + } + + /// Do a 'light weight'(just get references to existing data structures) attach to the core and return relevant debug data. + pub(crate) fn attach_core(&mut self, core_index: usize) -> Result { + if let (Ok(target_core), Some(core_data)) = ( + self.session.core(core_index), + self.core_data + .iter_mut() + .find(|core_data| core_data.core_index == core_index), + ) { + Ok(CoreHandle { + core: target_core, + capstone: &self.capstone, + core_data, + }) + } else { + Err(DebuggerError::UnableToOpenProbe(Some( + "No core at the specified index.", + ))) + } + } + + /// Check all target cores to ensure they have a configured and initialized RTT connections and if they do, process the RTT data. + /// Return true if at least one channel on one core had data in the buffer. + pub(crate) fn poll_rtt( + &mut self, + session_config: &SessionConfig, + debug_adapter: &mut DebugAdapter

, + ) -> bool { + let mut at_least_one_channel_had_data = false; + for core_config in session_config.core_configs.iter() { + if core_config.rtt_config.enabled { + let target_memory_map = self.session.target().memory_map.clone(); + if let Ok(mut target_core) = self.attach_core(core_config.core_index) { + if let Some(core_rtt) = &mut target_core.core_data.rtt_connection { + // We should poll the target for rtt data. + at_least_one_channel_had_data |= + core_rtt.process_rtt_data(debug_adapter, &mut target_core.core); + } else { + // We have not yet reached the point in the target application where the RTT buffers are initialized, so let's check again. + if debug_adapter.last_known_status != CoreStatus::Unknown + // Do not attempt this until we have processed the MSDAP request for "configurationDone" ... + { + #[allow(clippy::unwrap_used)] + match target_core.attach_to_rtt( + debug_adapter, + &target_memory_map, + core_config.program_binary.as_ref().unwrap(), + &core_config.rtt_config, + ) { + Ok(_) => { + // Nothing else to do. + } + Err(error) => { + debug_adapter + .send_error_response(&DebuggerError::Other(error)) + .ok(); + } + } + } + } + } else { + log::debug!( + "Failed to attach to target core #{}. Cannot poll for RTT data.", + core_config.core_index + ); + } + } else { + // No RTT configured. + } + } + at_least_one_channel_had_data + } +} diff --git a/debugger/src/main.rs b/debugger/src/main.rs index 62850dcced..8f2b90d32a 100644 --- a/debugger/src/main.rs +++ b/debugger/src/main.rs @@ -1,17 +1,16 @@ // Bad things happen to the VSCode debug extenison and debug_adapter if we panic at the wrong time. #![warn(clippy::unwrap_used, clippy::panic, clippy::expect_used)] // Uses Schemafy to generate DAP types from Json -mod dap_types; mod debug_adapter; mod debugger; -mod protocol; +mod peripherals; use anyhow::Result; use clap::{crate_authors, crate_description, crate_name, crate_version, Parser}; -use debugger::{debug, list_connected_devices, list_supported_chips, DebuggerOptions}; -use probe_rs::architecture::arm::ap::AccessPortError; -use probe_rs::flashing::FileDownloadError; -use probe_rs::{DebugProbeError, Error}; +use debugger::debug_entry::{debug, list_connected_devices, list_supported_chips}; +use probe_rs::{ + architecture::arm::ap::AccessPortError, flashing::FileDownloadError, DebugProbeError, Error, +}; #[derive(Debug, thiserror::Error)] pub enum DebuggerError { @@ -62,6 +61,8 @@ pub enum DebuggerError { author = crate_authors!(), version = crate_version!() )] + +/// There are only 3 command line options for the debugger. enum CliCommands { /// List all connected debug probes List {}, @@ -71,8 +72,9 @@ enum CliCommands { /// Open target in debug mode and accept debug commands. /// This only works as a [protocol::DapAdapter] and uses DAP Protocol debug commands (enables connections from clients such as Microsoft Visual Studio Code). Debug { - #[clap(flatten)] - debugger_options: DebuggerOptions, + /// IP port number to listen for incoming DAP connections, e.g. "50000" + #[clap(long)] + port: Option, /// The debug adapter processed was launched by VSCode, and should terminate itself at the end of every debug session (when receiving `Disconnect` or `Terminate` Request from VSCode). The "false"(default) state of this option implies that the process was launched (and will be managed) by the user. #[clap(long, hide = true)] @@ -90,10 +92,7 @@ fn main() -> Result<()> { match matches { CliCommands::List {} => list_connected_devices()?, CliCommands::ListChips {} => list_supported_chips()?, - CliCommands::Debug { - debugger_options, - vscode, - } => debug(debugger_options, vscode)?, + CliCommands::Debug { port, vscode } => debug(port, vscode)?, } Ok(()) } diff --git a/debugger/src/peripherals/mod.rs b/debugger/src/peripherals/mod.rs new file mode 100644 index 0000000000..7630c702c3 --- /dev/null +++ b/debugger/src/peripherals/mod.rs @@ -0,0 +1,9 @@ +/// Notes about SVD: +/// Peripherals: +/// - Are 'grouped', but many only belong to a single group. +/// - The 'derived from' properties need to be read first, then overlay the specified properties +/// Start off with everything being read-only. +/// We only have to build the structure once down to 'fields' level. +/// Fields need to be read every stacktrace, because they will change value. +/// Once an SVD file has been parsed, it's structure is loaded as a hierarchical set of variables. +pub(crate) mod svd_variables; diff --git a/debugger/src/peripherals/svd_variables.rs b/debugger/src/peripherals/svd_variables.rs new file mode 100644 index 0000000000..981af92ec1 --- /dev/null +++ b/debugger/src/peripherals/svd_variables.rs @@ -0,0 +1,347 @@ +use crate::{ + debug_adapter::{dap_adapter::DebugAdapter, protocol::ProtocolAdapter}, + DebuggerError, +}; +use probe_rs::{ + debug::{ + Variable, VariableCache, VariableLocation, VariableName, VariableNodeType, VariableType, + }, + Core, +}; +use std::{fmt::Debug, fs::File, io::Read, path::Path}; +use svd_parser::{self as svd}; +use svd_rs::{Access, DeriveFrom, Device, FieldInfo, PeripheralInfo, RegisterInfo}; + +/// The SVD file contents and related data +#[derive(Debug)] +pub(crate) struct SvdCache { + /// The SVD contents and structure will be stored as variables, down to the Field level. + /// Unlike other VariableCache instances, it will only be built once per DebugSession. + /// After that, only the SVD fields values change values, and the data for these will be re-read everytime they are queried by the debugger. + pub(crate) svd_variable_cache: VariableCache, +} + +impl SvdCache { + /// Create the SVD cache for a specific core. This function loads the file, parses it, and then builds the VariableCache. + pub(crate) fn new( + svd_file: &Path, + core: &mut Core, + debug_adapter: &mut DebugAdapter

, + dap_request_id: i64, + ) -> Result { + let svd_xml = &mut String::new(); + match File::open(svd_file) { + Ok(mut svd_opened_file) => { + let progress_id = debug_adapter.start_progress( + format!("Loading SVD file : {:?}", &svd_file).as_str(), + Some(dap_request_id), + )?; + let _ = svd_opened_file.read_to_string(svd_xml); + let svd_cache = match svd::parse(svd_xml) { + Ok(peripheral_device) => { + debug_adapter + .update_progress( + None, + Some(format!("Done loading SVD file :{:?}", &svd_file)), + progress_id, + ) + .ok(); + + Ok(SvdCache { + svd_variable_cache: variable_cache_from_svd( + peripheral_device, + core, + debug_adapter, + progress_id, + )?, + }) + } + Err(error) => Err(DebuggerError::Other(anyhow::anyhow!( + "Unable to parse CMSIS-SVD file: {:?}. {:?}", + svd_file, + error, + ))), + }; + let _ = debug_adapter.end_progress(progress_id)?; + svd_cache + } + Err(error) => Err(DebuggerError::Other(anyhow::anyhow!("{}", error))), + } + } +} + +/// Create a [`probe_rs::debug::VariableCache`] from a Device that was parsed from a CMSIS-SVD file. +pub(crate) fn variable_cache_from_svd( + peripheral_device: Device, + core: &mut Core, + debug_adapter: &mut DebugAdapter

, + progress_id: i64, +) -> Result { + let mut svd_cache = probe_rs::debug::VariableCache::new(); + let mut device_root_variable = Variable::new(None, None); + device_root_variable.variable_node_type = VariableNodeType::DoNotRecurse; + device_root_variable.name = VariableName::PeripheralScopeRoot; + device_root_variable = svd_cache.cache_variable(None, device_root_variable, core)?; + // Adding the Peripheral Group Name as an additional level in the structure helps to keep the 'variable tree' more compact, but more importantly, it helps to avoid having duplicate variable names that conflict with hal crates. + let mut peripheral_group_variable = Variable::new(None, None); + peripheral_group_variable.name = VariableName::Named(peripheral_device.name.clone()); + let mut peripheral_parent_key = device_root_variable.variable_key; + for peripheral in &resolve_peripherals(&peripheral_device)? { + if let (Some(peripheral_group_name), VariableName::Named(variable_group_name)) = + (&peripheral.group_name, &peripheral_group_variable.name) + { + if variable_group_name != peripheral_group_name { + peripheral_group_variable = Variable::new(None, None); + peripheral_group_variable.name = VariableName::Named(peripheral_group_name.clone()); + peripheral_group_variable.type_name = + VariableType::Other("Peripheral Group".to_string()); + peripheral_group_variable.variable_node_type = VariableNodeType::SvdPeripheral; + peripheral_group_variable.set_value(probe_rs::debug::VariableValue::Valid( + peripheral + .description + .clone() + .unwrap_or_else(|| peripheral.name.clone()), + )); + peripheral_group_variable = svd_cache.cache_variable( + Some(device_root_variable.variable_key), + peripheral_group_variable, + core, + )?; + peripheral_parent_key = peripheral_group_variable.variable_key; + debug_adapter + .update_progress( + None, + Some(format!( + "SVD loading peripheral group:{}", + &peripheral_group_name + )), + progress_id, + ) + .ok(); + } + } + + let mut peripheral_variable = Variable::new(None, None); + peripheral_variable.name = VariableName::Named(format!( + "{}.{}", + peripheral_group_variable.name.clone(), + peripheral.name.clone() + )); + peripheral_variable.type_name = VariableType::Other("Peripheral".to_string()); + peripheral_variable.variable_node_type = VariableNodeType::SvdPeripheral; + peripheral_variable.memory_location = + VariableLocation::Address(peripheral.base_address as u32); + peripheral_variable.set_value(probe_rs::debug::VariableValue::Valid( + peripheral + .description + .clone() + .unwrap_or_else(|| format!("{}", peripheral_variable.name)), + )); + peripheral_variable = + svd_cache.cache_variable(Some(peripheral_parent_key), peripheral_variable, core)?; + for register in &resolve_registers(peripheral)? { + let mut register_variable = Variable::new(None, None); + register_variable.name = VariableName::Named(format!( + "{}.{}", + &peripheral_variable.name, + register.name.clone() + )); + register_variable.type_name = VariableType::Other( + register + .description + .clone() + .unwrap_or_else(|| "Peripheral Register".to_string()), + ); + register_variable.variable_node_type = VariableNodeType::SvdRegister; + register_variable.memory_location = + VariableLocation::Address(peripheral.base_address as u32 + register.address_offset); + let mut register_has_restricted_read = false; + if register.read_action.is_some() + || (if let Some(register_access) = register.properties.access { + register_access == Access::ReadWriteOnce || register_access == Access::WriteOnly + } else { + false + }) + { + register_variable.set_value(probe_rs::debug::VariableValue::Error( + "Register access doesn't allow reading, or will have side effects.".to_string(), + )); + register_has_restricted_read = true; + } + register_variable = svd_cache.cache_variable( + Some(peripheral_variable.variable_key), + register_variable, + core, + )?; + for field in &resolve_fields(register)? { + let mut field_variable = Variable::new(None, None); + field_variable.name = VariableName::Named(format!( + "{}.{}", + ®ister_variable.name, + field.name.clone() + )); + field_variable.type_name = VariableType::Other( + field + .description + .clone() + .unwrap_or_else(|| "Register Field".to_string()), + ); + field_variable.variable_node_type = VariableNodeType::SvdField; + field_variable.memory_location = register_variable.memory_location.clone(); + // For SVD fields, we overload the range_lower_bound and range_upper_bound as the bit range LSB and MSB. + field_variable.range_lower_bound = field.bit_offset() as i64; + field_variable.range_upper_bound = (field.bit_offset() + field.bit_width()) as i64; + if register_has_restricted_read { + register_variable.set_value(probe_rs::debug::VariableValue::Error( + "Register access doesn't allow reading, or will have side effects." + .to_string(), + )); + } else if field.read_action.is_some() + || (if let Some(field_access) = field.access { + field_access == Access::ReadWriteOnce || field_access == Access::WriteOnly + } else { + false + }) + { + field_variable.set_value(probe_rs::debug::VariableValue::Error( + "Field access doesn't allow reading, or will have side effects." + .to_string(), + )); + // If we can't read any of the bits, then don't read the register either. + register_variable.set_value(probe_rs::debug::VariableValue::Error( + "Some fields' access doesn't allow reading, or will have side effects." + .to_string(), + )); + register_has_restricted_read = true; + register_variable = svd_cache.cache_variable( + Some(peripheral_variable.variable_key), + register_variable, + core, + )?; + } + // TODO: Extend the Variable definition, so that we can resolve the EnumeratedValues for fields. + svd_cache.cache_variable( + Some(register_variable.variable_key), + field_variable, + core, + )?; + } + } + } + + Ok(svd_cache) +} + +/// Resolve all the peripherals through their (optional) `derived_from` peripheral. +pub(crate) fn resolve_peripherals( + peripheral_device: &Device, +) -> Result, DebuggerError> { + let mut resolved_peripherals = vec![]; + for device_peripheral in &peripheral_device.peripherals { + if let Some(derived_from) = &device_peripheral.derived_from { + if let Some(derived_result) = peripheral_device.get_peripheral(derived_from) { + match device_peripheral.derive_from(derived_result) { + svd_rs::MaybeArray::Single(derived_peripheral) => { + resolved_peripherals.push(derived_peripheral); + } + svd_rs::MaybeArray::Array(peripheral_array, _) => { + log::warn!("Unsupported Array in SVD for Peripheral:{}. Only the first instance will be visible.", peripheral_array.name); + resolved_peripherals.push(peripheral_array); + } + } + } else { + return Err(DebuggerError::Other(anyhow::anyhow!( + "Unable to retrieve 'derived_from' SVD peripheral: {:?}", + derived_from + ))); + }; + } else { + match device_peripheral { + svd_rs::MaybeArray::Single(original_peripheral) => { + resolved_peripherals.push(original_peripheral.clone()) + } + svd_rs::MaybeArray::Array(peripheral_array, _) => { + log::warn!("Unsupported Array in SVD for Peripheral:{}. Only the first instance will be visible.", peripheral_array.name); + resolved_peripherals.push(peripheral_array.clone()); + } + } + } + } + Ok(resolved_peripherals) +} + +/// Resolve all the registers of a peripheral through their (optional) `derived_from` register. +pub(crate) fn resolve_registers( + peripheral: &PeripheralInfo, +) -> Result, DebuggerError> { + // TODO: Need to code for the impact of register clusters. + let mut resolved_registers = vec![]; + for peripheral_register in peripheral.registers() { + if let Some(derived_from) = &peripheral_register.derived_from { + if let Some(derived_result) = peripheral.get_register(derived_from) { + match peripheral_register.derive_from(derived_result) { + svd_rs::MaybeArray::Single(derived_register) => { + resolved_registers.push(derived_register) + } + svd_rs::MaybeArray::Array(register_array, _) => { + log::warn!("Unsupported Array in SVD for Register:{}. Only the first instance will be visible.", register_array.name); + resolved_registers.push(register_array); + } + } + } else { + return Err(DebuggerError::Other(anyhow::anyhow!( + "Unable to retrieve 'derived_from' SVD register: {:?}", + derived_from + ))); + }; + } else { + match peripheral_register { + svd_rs::MaybeArray::Single(original_register) => { + resolved_registers.push(original_register.clone()) + } + svd_rs::MaybeArray::Array(register_array, _) => { + log::warn!("Unsupported Array in SVD for Register:{}. Only the first instance will be visible.", register_array.name); + resolved_registers.push(register_array.clone()); + } + } + } + } + Ok(resolved_registers) +} + +/// Resolve all the fields of a register through their (optional) `derived_from` field. +pub(crate) fn resolve_fields(register: &RegisterInfo) -> Result, DebuggerError> { + // TODO: Need to code for the impact of field clusters. + let mut resolved_fields = vec![]; + for register_field in register.fields() { + if let Some(derived_from) = ®ister_field.derived_from { + if let Some(derived_result) = register.get_field(derived_from) { + match register_field.derive_from(derived_result) { + svd_rs::MaybeArray::Single(derived_field) => { + resolved_fields.push(derived_field) + } + svd_rs::MaybeArray::Array(field_array, _) => { + log::warn!("Unsupported Array in SVD for Field:{}. Only the first instance will be visible.", field_array.name); + resolved_fields.push(field_array); + } + } + } else { + return Err(DebuggerError::Other(anyhow::anyhow!( + "Unable to retrieve 'derived_from' SVD field: {:?}", + derived_from + ))); + }; + } else { + match register_field { + svd_rs::MaybeArray::Single(original_field) => { + resolved_fields.push(original_field.clone()) + } + svd_rs::MaybeArray::Array(field_array, _) => { + log::warn!("Unsupported Array in SVD for Field:{}. Only the first instance will be visible.", field_array.name); + resolved_fields.push(field_array.clone()); + } + } + } + } + Ok(resolved_fields) +} diff --git a/debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_request_with_invalid_json.snap b/debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_request_with_invalid_json.snap deleted file mode 100644 index f1f2cdcd65..0000000000 --- a/debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_request_with_invalid_json.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: debugger/src/debug_adapter.rs -expression: output_str - ---- -Content-Length: 136 - -{"command":"error","message":"EOF while parsing a string at line 1 column 49","request_seq":1,"seq":1,"success":false,"type":"response"}Content-Length: 156 - -{"body":{"category":"console","group":"probe-rs-debug","output":"EOF while parsing a string at line 1 column 49\n"},"event":"output","seq":2,"type":"event"}Content-Length: 145 - -{"body":{"message":"EOF while parsing a string at line 1 column 49\n","severity":"error"},"event":"probe-rs-show-message","seq":3,"type":"event"}Content-Length: 154 - -{"body":{"category":"console","group":"probe-rs-debug","output":"\nTriggered DAP Event: probe-rs-show-message\n"},"event":"output","seq":4,"type":"event"} diff --git a/debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_request_with_wrong_content_length.snap b/debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_request_with_wrong_content_length.snap deleted file mode 100644 index d0d6ccb9d6..0000000000 --- a/debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_request_with_wrong_content_length.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: debugger/src/debug_adapter.rs -expression: output_str - ---- -Content-Length: 145 - -{"command":"error","message":"Failed to read the expected 60 bytes from incoming data","request_seq":1,"seq":1,"success":false,"type":"response"}Content-Length: 165 - -{"body":{"category":"console","group":"probe-rs-debug","output":"Failed to read the expected 60 bytes from incoming data\n"},"event":"output","seq":2,"type":"event"}Content-Length: 154 - -{"body":{"message":"Failed to read the expected 60 bytes from incoming data\n","severity":"error"},"event":"probe-rs-show-message","seq":3,"type":"event"}Content-Length: 154 - -{"body":{"category":"console","group":"probe-rs-debug","output":"\nTriggered DAP Event: probe-rs-show-message\n"},"event":"output","seq":4,"type":"event"} diff --git a/debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_request_would_block.snap b/debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_request_would_block.snap deleted file mode 100644 index d4ef3f4364..0000000000 --- a/debugger/src/snapshots/probe_rs_debugger__debug_adapter__test__receive_request_would_block.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: debugger/src/debug_adapter.rs -expression: output_str - ---- - diff --git a/debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_request_would_block.snap b/debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_request_would_block.snap deleted file mode 100644 index be05c98255..0000000000 --- a/debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_request_would_block.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: debugger/src/protocol.rs -expression: output_str - ---- - diff --git a/debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_valid_request.snap b/debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_valid_request.snap deleted file mode 100644 index ad3060ae2a..0000000000 --- a/debugger/src/snapshots/probe_rs_debugger__protocol__test__receive_valid_request.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: debugger/src/protocol.rs -expression: output_str - ---- -Content-Length: 151 - -{"body":{"category":"console","group":"probe-rs-debug","output":"\nReceived DAP Request sequence #3 : test\n"},"event":"output","seq":1,"type":"event"} diff --git a/probe-rs-cli-util/src/rtt.rs b/probe-rs-cli-util/src/rtt.rs index 3631f7ef1a..e57c031e87 100644 --- a/probe-rs-cli-util/src/rtt.rs +++ b/probe-rs-cli-util/src/rtt.rs @@ -48,6 +48,12 @@ fn default_channel_formats() -> Vec { vec![] } +/// Used by serde to provide defaults for `RttChannelConfig::show_location` +fn default_include_location() -> bool { + // Setting this to true to allow compatibility with behaviour prior to when this option was introduced. + true +} + #[derive(Debug, Copy, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum DataFormat { String, @@ -96,7 +102,12 @@ pub struct RttChannelConfig { pub data_format: DataFormat, #[structopt(skip)] #[serde(default)] + // Control the inclusion of timestamps for DataFormat::String. pub show_timestamps: bool, + #[structopt(skip)] + #[serde(default = "default_include_location")] + // Control the inclusion of source location information for DataFormat::Defmt. + pub show_location: bool, } /// This is the primary interface through which RTT channel data is read and written. Every actual RTT channel has a configuration and buffer that is used for this purpose. @@ -110,6 +121,7 @@ pub struct RttActiveChannel { _input_data: String, rtt_buffer: RttBuffer, show_timestamps: bool, + show_location: bool, } /// A fully configured RttActiveChannel. The configuration will always try to 'default' based on information read from the RTT control block in the binary. Where insufficient information is available, it will use the supplied configuration, with final hardcoded defaults where no other information was available. @@ -119,8 +131,8 @@ impl RttActiveChannel { down_channel: Option, channel_config: Option, ) -> Self { - let full_config = match channel_config { - Some(channel_config) => channel_config, + let full_config = match &channel_config { + Some(channel_config) => channel_config.clone(), None => RttChannelConfig { ..Default::default() // Will set intelligent defaults below ... }, @@ -149,10 +161,15 @@ impl RttActiveChannel { .map(|down| down.name() == Some("defmt")) }) .unwrap_or(false); // If no explicit config is requested, assign a default - let data_format: DataFormat = if defmt_enabled { - DataFormat::Defmt + let (data_format, show_location) = if defmt_enabled { + let show_location = if let Some(channel_config) = channel_config { + channel_config.show_location + } else { + true + }; + (DataFormat::Defmt, show_location) } else { - full_config.data_format + (full_config.data_format, false) }; Self { up_channel, @@ -162,6 +179,7 @@ impl RttActiveChannel { _input_data: String::new(), rtt_buffer: RttBuffer::new(buffer_size), show_timestamps: full_config.show_timestamps, + show_location, } } @@ -240,20 +258,22 @@ impl RttActiveChannel { // verified to exist in the `locs` map. let loc = locs.as_ref().map(|locs| &locs[&frame.index()]); writeln!(formatted_data, "{}", frame.display(false)).map_or_else(|err| log::error!("Failed to format RTT data - {:?}", err), |r|r); - if let Some(loc) = loc { - let relpath = if let Ok(relpath) = - loc.file.strip_prefix(&std::env::current_dir().unwrap()) - { - relpath - } else { - // not relative; use full path - &loc.file - }; - writeln!(formatted_data, - "└─ {}:{}", - relpath.display(), - loc.line - ).map_or_else(|err| log::error!("Failed to format RTT data - {:?}", err), |r|r); + if self.show_location { + if let Some(loc) = loc { + let relpath = if let Ok(relpath) = + loc.file.strip_prefix(&std::env::current_dir().unwrap()) + { + relpath + } else { + // not relative; use full path + &loc.file + }; + writeln!(formatted_data, + "└─ {}:{}", + relpath.display(), + loc.line + ).map_or_else(|err| log::error!("Failed to format RTT data - {:?}", err), |r|r); + } } } } diff --git a/probe-rs/src/debug/mod.rs b/probe-rs/src/debug/mod.rs index fef0a41c47..f251284f06 100644 --- a/probe-rs/src/debug/mod.rs +++ b/probe-rs/src/debug/mod.rs @@ -12,13 +12,14 @@ mod variable; use crate::{ core::{Core, RegisterFile}, - debug::variable::VariableType, CoreStatus, MemoryInterface, }; +use gimli::{ + DebuggingInformationEntry, FileEntry, LineProgramHeader, Location, UnitOffset, UnwindContext, +}; use num_traits::Zero; +use object::read::{Object, ObjectSection}; use probe_rs_target::Architecture; -pub use variable::{Variable, VariableCache, VariableLocation, VariableName, VariantRole}; - use std::{ borrow, collections::HashMap, @@ -30,13 +31,10 @@ use std::{ sync::atomic::{AtomicI64, Ordering}, vec, }; - -use gimli::{ - DebuggingInformationEntry, FileEntry, LineProgramHeader, Location, UnitOffset, UnwindContext, +pub use variable::{ + Variable, VariableCache, VariableLocation, VariableName, VariableNodeType, VariableType, + VariableValue, VariantRole, }; -use object::read::{Object, ObjectSection}; - -use self::variable::{VariableNodeType, VariableValue}; /// An error occurred while debugging the target. #[derive(Debug, thiserror::Error)] @@ -896,8 +894,8 @@ impl DebugInfo { variable::VariableLocation::Address(memory_location) } Err(error) => { - log::error!("Failed to read referenced variable address from memory location {:#010x?} : {}.", parent_variable.memory_location , error); - variable::VariableLocation::Error(format!("Failed to read referenced variable address from memory location {:#010x?} : {}.", parent_variable.memory_location, error)) + log::error!("Failed to read referenced variable address from memory location {} : {}.", parent_variable.memory_location , error); + variable::VariableLocation::Error(format!("Failed to read referenced variable address from memory location {} : {}.", parent_variable.memory_location, error)) } }; } @@ -1013,7 +1011,7 @@ impl DebugInfo { } } } - VariableNodeType::DoNotRecurse | VariableNodeType::RecurseToBaseType => { + _ => { // Do nothing. These have already been recursed to their maximum. } } @@ -1505,7 +1503,8 @@ impl DebugInfo { ) { Ok(frame_descriptor_entry) => frame_descriptor_entry, Err(error) => { - log::error!( + // This is not an error, it just means we cannot unwind any deeper. + log::debug!( "UNWIND: Error reading previous FrameDescriptorEntry at PC={:#010x} : {}", previous_frame_pc, error @@ -3044,8 +3043,7 @@ impl<'debuginfo> UnitInfo<'debuginfo> { }; child_variable.set_value(VariableValue::Valid(format!( "{}::{}", - child_variable.type_name.display(), - enumumerator_value + child_variable.type_name, enumumerator_value ))); // We don't need to keep these children. cache.remove_cache_entry_children(child_variable.variable_key)?; diff --git a/probe-rs/src/debug/variable.rs b/probe-rs/src/debug/variable.rs index d08d18ad50..32f25be1b4 100644 --- a/probe-rs/src/debug/variable.rs +++ b/probe-rs/src/debug/variable.rs @@ -99,7 +99,13 @@ impl VariableCache { // As the final act, we need to update the variable with an appropriate value. // This requires distinct steps to ensure we don't get `borrow` conflicts on the variable cache. if let Some(mut stored_variable) = self.get_variable_by_key(stored_key) { - stored_variable.extract_value(core, self); + if !(stored_variable.variable_node_type == VariableNodeType::SvdPeripheral + || stored_variable.variable_node_type == VariableNodeType::SvdRegister + || stored_variable.variable_node_type == VariableNodeType::SvdField) + { + // Only do this for non-SVD variables. Those will extract their value everytime they are read from the client. + stored_variable.extract_value(core, self); + } if self .variable_hash_map .insert(stored_variable.variable_key, stored_variable.clone()) @@ -320,6 +326,8 @@ pub enum VariableName { RegistersRoot, /// Top-level variable for local scoped variables, child of a stack frame variable. LocalScopeRoot, + /// Top-level variable for CMSIS-SVD file Device peripherals/registers/fields. + PeripheralScopeRoot, /// Artificial variable, without a name (e.g. enum discriminant) Artifical, /// Anonymous namespace @@ -344,6 +352,7 @@ impl std::fmt::Display for VariableName { VariableName::StaticScopeRoot => write!(f, "Static Variable"), VariableName::RegistersRoot => write!(f, "Platform Register"), VariableName::LocalScopeRoot => write!(f, "Function Variable"), + VariableName::PeripheralScopeRoot => write!(f, "Peripheral Variable"), VariableName::Artifical => write!(f, ""), VariableName::AnonymousNamespace => write!(f, ""), VariableName::Namespace(name) => name.fmt(f), @@ -380,9 +389,16 @@ pub enum VariableNodeType { /// - Rule: For now, Array types WILL ALWAYS BE recursed. TODO: Evaluate if it is beneficial to defer these. /// - Rule: For now, Union types WILL ALWAYS BE recursed. TODO: Evaluate if it is beneficial to defer these. RecurseToBaseType, + /// SVD Device Peripherals + SvdPeripheral, + /// SVD Peripheral Registers + SvdRegister, + /// SVD Register Fields + SvdField, } impl VariableNodeType { + /// Will return true if any of the `variable_node_type` value implies that the variable will be 'lazy' resolved. pub fn is_deferred(&self) -> bool { match self { VariableNodeType::ReferenceOffset(_) @@ -399,20 +415,30 @@ impl Default for VariableNodeType { } } +/// The variants of VariableType allows us to streamline the conditional logic that requires specific handling depending on the nature of the variable. #[derive(Debug, Clone, PartialEq)] pub enum VariableType { + /// A variable with a Rust base datatype. Base(String), + /// A Rust struct. Struct(String), + /// A Rust enum. Enum(String), + /// Namespace refers to the path that qualifies a variable. e.g. "std::string" is the namespace for the strucct "String" Namespace, + /// A Pointer is a variable that contains a reference to another variable Pointer(Option), + /// A Rust array. Array { // TODO: Use a proper type here, not variable name + /// The name of the variable. entry_type: VariableName, + /// The number of entries in the array. count: usize, }, - Unnamed, + /// When we are unable to determine the name of a variable. Unknown, + /// For infrequently used categories of variables that does not fall into any of the other VriableType variants. Other(String), } @@ -423,6 +449,7 @@ impl Default for VariableType { } impl VariableType { + /// A Rust PhantomData type used as a marker for to "act like" they own a specific type. pub fn is_phantom_data(&self) -> bool { match self { VariableType::Struct(name) => name.starts_with("PhantomData"), @@ -430,6 +457,7 @@ impl VariableType { } } + /// This variable is a reference to another variable. pub fn is_reference(&self) -> bool { match self { VariableType::Pointer(Some(name)) => name.starts_with('&'), @@ -437,23 +465,26 @@ impl VariableType { } } + /// This variable is an array, and requires special processing during pub fn is_array(&self) -> bool { matches!(self, VariableType::Array { .. }) } +} - pub fn display(&self) -> String { +impl std::fmt::Display for VariableType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - VariableType::Base(base) => base.clone(), - VariableType::Struct(struct_name) => struct_name.clone(), - VariableType::Enum(enum_name) => enum_name.clone(), - VariableType::Namespace => "".to_string(), + VariableType::Base(base) => base.fmt(f), + VariableType::Struct(struct_name) => struct_name.fmt(f), + VariableType::Enum(enum_name) => enum_name.fmt(f), + VariableType::Namespace => "".fmt(f), VariableType::Pointer(pointer_name) => pointer_name .clone() - .unwrap_or_else(|| "".to_string()), - VariableType::Array { entry_type, count } => format!("[{}; {}]", entry_type, count), - VariableType::Unnamed => "".to_string(), - VariableType::Unknown => "".to_string(), - VariableType::Other(other) => other.clone(), + .unwrap_or_else(|| "".to_string()) + .fmt(f), + VariableType::Array { entry_type, count } => write!(f, "[{}; {}]", entry_type, count), + VariableType::Unknown => "".fmt(f), + VariableType::Other(other) => other.fmt(f), } } } @@ -501,6 +532,20 @@ impl VariableLocation { } } +impl std::fmt::Display for VariableLocation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + VariableLocation::Unknown => "".fmt(f), + VariableLocation::Unavailable => "".fmt(f), + VariableLocation::Address(address) => write!(f, "{:#010X}", address), + VariableLocation::Register(register) => write!(f, "r{}", register), + VariableLocation::Value => "".fmt(f), + VariableLocation::Error(error) => error.fmt(f), + VariableLocation::Unsupported(reason) => reason.fmt(f), + } + } +} + impl Default for VariableLocation { fn default() -> Self { VariableLocation::Unknown @@ -551,7 +596,7 @@ pub struct Variable { impl Variable { /// In most cases, Variables will be initialized with their ELF references so that we resolve their data types and values on demand. - pub(crate) fn new( + pub fn new( header_offset: Option, entries_offset: Option, ) -> Variable { @@ -564,7 +609,7 @@ impl Variable { /// Implementing set_value(), because the library passes errors into the value of the variable. /// This ensures debug front ends can see the errors, but doesn't fail because of a single variable not being able to decode correctly. - pub(crate) fn set_value(&mut self, new_value: VariableValue) { + pub fn set_value(&mut self, new_value: VariableValue) { // Allow some block when logic requires it. #[allow(clippy::if_same_then_else)] if new_value.is_valid() { @@ -664,7 +709,45 @@ impl Variable { pub fn get_value(&self, variable_cache: &VariableCache) -> String { // Allow for chained `if let` without complaining #[allow(clippy::if_same_then_else)] - if !self.value.is_empty() { + if VariableNodeType::SvdRegister == self.variable_node_type { + if let VariableValue::Valid(register_value) = &self.value { + if let Ok(register_u32_value) = register_value.parse::() { + format!( + "{:032b} @ {:#010X}", + register_u32_value, + self.memory_location.memory_address().unwrap_or(u32::MAX) // We should never encounter a memory location that is invalid if we already used it to read the register value. + ) + } else { + format!("Invalid register value {}", register_value) + } + } else { + format!("{}", self.value) + } + } else if VariableNodeType::SvdField == self.variable_node_type { + // In this special case, we extract just the bits we need from the stored value of the register. + if let VariableValue::Valid(register_value) = &self.value { + if let Ok(register_u32_value) = register_value.parse::() { + let mut bit_value: u32 = register_u32_value; + bit_value <<= 32 - self.range_upper_bound; + bit_value >>= 32 - (self.range_upper_bound - self.range_lower_bound); + format!( + "{:0width$b} @ {:#010X}:{}..{}", + bit_value, + self.memory_location.memory_address().unwrap_or(u32::MAX), + self.range_lower_bound, + self.range_upper_bound, + width = (self.range_upper_bound - self.range_lower_bound) as usize + ) + } else { + format!( + "Invalid bit range {}..{} from value {}", + self.range_lower_bound, self.range_upper_bound, register_value + ) + } + } else { + format!("{}", self.value) + } + } else if !self.value.is_empty() { // The `value` for this `Variable` is non empty because ... // - It is base data type for which a value was determined based on the core runtime, or ... // - We encountered an error somewhere, so report it to the user @@ -710,25 +793,39 @@ impl Variable { } /// Evaluate the variable's result if possible and set self.value, or else set self.value as the error String. - fn extract_value(&mut self, core: &mut Core<'_>, variable_cache: &VariableCache) { - // Quick exit if we don't really need to do much more. - if !self.value.is_empty() - // The value was set explicitly, so just leave it as is., or it was an error, so don't attempt anything else - || self.memory_location == variable::VariableLocation::Value + pub fn extract_value(&mut self, core: &mut Core<'_>, variable_cache: &VariableCache) { + if let VariableValue::Error(_) = self.value { + // Nothing more to do ... + return; + } else if self.variable_node_type == VariableNodeType::SvdRegister + || self.variable_node_type == VariableNodeType::SvdField + { + // Special handling for SVD registers. + // Because we cache the SVD structure once per sesion, we have to re-read the actual register values whenever queried. + match core.read_word_32(self.memory_location.memory_address().unwrap_or(u32::MAX)) { + Ok(u32_value) => self.value = VariableValue::Valid(u32_value.to_le().to_string()), + Err(error) => { + self.value = VariableValue::Error(format!( + "Unable to read peripheral register value @ {:#010X} : {:?}", + self.memory_location.memory_address().unwrap_or(u32::MAX), + error + )) + } + } + return; + } else if !self.value.is_empty() + // The value was set explicitly, so just leave it as is, or it was an error, so don't attempt anything else || !self.memory_location.valid() - // Early on in the process of `Variable` evaluation + // This may just be that we are early on in the process of `Variable` evaluation || self.type_name == VariableType::Unknown + // This may just be that we are early on in the process of `Variable` evaluation { + // Quick exit if we don't really need to do much more. return; } else if self.variable_node_type.is_deferred() { // And we have not previously assigned the value, then assign the type and address as the value - let location = match &self.memory_location { - VariableLocation::Address(address) => format!("{:#010X}", address), - other => format!("{:?}", other), - }; - self.value = - VariableValue::Valid(format!("{} @ {}", self.type_name.display(), location)); + VariableValue::Valid(format!("{} @ {}", self.type_name, self.memory_location)); return; } @@ -863,11 +960,7 @@ impl Variable { // Use the supplied value or error message. format!( "{}{:\t