150
|
1 //===-- ThreadPlanStepInRange.cpp -----------------------------------------===//
|
|
2 //
|
|
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
4 // See https://llvm.org/LICENSE.txt for license information.
|
|
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
6 //
|
|
7 //===----------------------------------------------------------------------===//
|
|
8
|
|
9 #include "lldb/Target/ThreadPlanStepInRange.h"
|
|
10 #include "lldb/Core/Architecture.h"
|
|
11 #include "lldb/Core/Module.h"
|
|
12 #include "lldb/Symbol/Function.h"
|
|
13 #include "lldb/Symbol/Symbol.h"
|
|
14 #include "lldb/Target/Process.h"
|
|
15 #include "lldb/Target/RegisterContext.h"
|
|
16 #include "lldb/Target/SectionLoadList.h"
|
|
17 #include "lldb/Target/Target.h"
|
|
18 #include "lldb/Target/Thread.h"
|
|
19 #include "lldb/Target/ThreadPlanStepOut.h"
|
|
20 #include "lldb/Target/ThreadPlanStepThrough.h"
|
|
21 #include "lldb/Utility/Log.h"
|
|
22 #include "lldb/Utility/RegularExpression.h"
|
|
23 #include "lldb/Utility/Stream.h"
|
|
24
|
|
25 using namespace lldb;
|
|
26 using namespace lldb_private;
|
|
27
|
|
28 uint32_t ThreadPlanStepInRange::s_default_flag_values =
|
|
29 ThreadPlanShouldStopHere::eStepInAvoidNoDebug;
|
|
30
|
|
31 // ThreadPlanStepInRange: Step through a stack range, either stepping over or
|
|
32 // into based on the value of \a type.
|
|
33
|
|
34 ThreadPlanStepInRange::ThreadPlanStepInRange(
|
|
35 Thread &thread, const AddressRange &range,
|
|
36 const SymbolContext &addr_context, const char *step_into_target,
|
|
37 lldb::RunMode stop_others, LazyBool step_in_avoids_code_without_debug_info,
|
|
38 LazyBool step_out_avoids_code_without_debug_info)
|
|
39 : ThreadPlanStepRange(ThreadPlan::eKindStepInRange,
|
|
40 "Step Range stepping in", thread, range, addr_context,
|
|
41 stop_others),
|
|
42 ThreadPlanShouldStopHere(this), m_step_past_prologue(true),
|
|
43 m_virtual_step(false), m_step_into_target(step_into_target) {
|
|
44 SetCallbacks();
|
|
45 SetFlagsToDefault();
|
|
46 SetupAvoidNoDebug(step_in_avoids_code_without_debug_info,
|
|
47 step_out_avoids_code_without_debug_info);
|
|
48 }
|
|
49
|
|
50 ThreadPlanStepInRange::~ThreadPlanStepInRange() = default;
|
|
51
|
|
52 void ThreadPlanStepInRange::SetupAvoidNoDebug(
|
|
53 LazyBool step_in_avoids_code_without_debug_info,
|
|
54 LazyBool step_out_avoids_code_without_debug_info) {
|
|
55 bool avoid_nodebug = true;
|
173
|
56 Thread &thread = GetThread();
|
150
|
57 switch (step_in_avoids_code_without_debug_info) {
|
|
58 case eLazyBoolYes:
|
|
59 avoid_nodebug = true;
|
|
60 break;
|
|
61 case eLazyBoolNo:
|
|
62 avoid_nodebug = false;
|
|
63 break;
|
|
64 case eLazyBoolCalculate:
|
173
|
65 avoid_nodebug = thread.GetStepInAvoidsNoDebug();
|
150
|
66 break;
|
|
67 }
|
|
68 if (avoid_nodebug)
|
|
69 GetFlags().Set(ThreadPlanShouldStopHere::eStepInAvoidNoDebug);
|
|
70 else
|
|
71 GetFlags().Clear(ThreadPlanShouldStopHere::eStepInAvoidNoDebug);
|
|
72
|
|
73 switch (step_out_avoids_code_without_debug_info) {
|
|
74 case eLazyBoolYes:
|
|
75 avoid_nodebug = true;
|
|
76 break;
|
|
77 case eLazyBoolNo:
|
|
78 avoid_nodebug = false;
|
|
79 break;
|
|
80 case eLazyBoolCalculate:
|
173
|
81 avoid_nodebug = thread.GetStepOutAvoidsNoDebug();
|
150
|
82 break;
|
|
83 }
|
|
84 if (avoid_nodebug)
|
|
85 GetFlags().Set(ThreadPlanShouldStopHere::eStepOutAvoidNoDebug);
|
|
86 else
|
|
87 GetFlags().Clear(ThreadPlanShouldStopHere::eStepOutAvoidNoDebug);
|
|
88 }
|
|
89
|
|
90 void ThreadPlanStepInRange::GetDescription(Stream *s,
|
|
91 lldb::DescriptionLevel level) {
|
|
92
|
|
93 auto PrintFailureIfAny = [&]() {
|
|
94 if (m_status.Success())
|
|
95 return;
|
|
96 s->Printf(" failed (%s)", m_status.AsCString());
|
|
97 };
|
|
98
|
|
99 if (level == lldb::eDescriptionLevelBrief) {
|
|
100 s->Printf("step in");
|
|
101 PrintFailureIfAny();
|
|
102 return;
|
|
103 }
|
|
104
|
|
105 s->Printf("Stepping in");
|
|
106 bool printed_line_info = false;
|
|
107 if (m_addr_context.line_entry.IsValid()) {
|
|
108 s->Printf(" through line ");
|
|
109 m_addr_context.line_entry.DumpStopContext(s, false);
|
|
110 printed_line_info = true;
|
|
111 }
|
|
112
|
|
113 const char *step_into_target = m_step_into_target.AsCString();
|
|
114 if (step_into_target && step_into_target[0] != '\0')
|
|
115 s->Printf(" targeting %s", m_step_into_target.AsCString());
|
|
116
|
|
117 if (!printed_line_info || level == eDescriptionLevelVerbose) {
|
|
118 s->Printf(" using ranges:");
|
|
119 DumpRanges(s);
|
|
120 }
|
|
121
|
|
122 PrintFailureIfAny();
|
|
123
|
|
124 s->PutChar('.');
|
|
125 }
|
|
126
|
|
127 bool ThreadPlanStepInRange::ShouldStop(Event *event_ptr) {
|
|
128 Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_STEP));
|
|
129
|
|
130 if (log) {
|
|
131 StreamString s;
|
173
|
132 DumpAddress(s.AsRawOstream(), GetThread().GetRegisterContext()->GetPC(),
|
|
133 GetTarget().GetArchitecture().GetAddressByteSize());
|
150
|
134 LLDB_LOGF(log, "ThreadPlanStepInRange reached %s.", s.GetData());
|
|
135 }
|
|
136
|
|
137 if (IsPlanComplete())
|
|
138 return true;
|
|
139
|
|
140 m_no_more_plans = false;
|
|
141 if (m_sub_plan_sp && m_sub_plan_sp->IsPlanComplete()) {
|
|
142 if (!m_sub_plan_sp->PlanSucceeded()) {
|
|
143 SetPlanComplete();
|
|
144 m_no_more_plans = true;
|
|
145 return true;
|
|
146 } else
|
|
147 m_sub_plan_sp.reset();
|
|
148 }
|
|
149
|
|
150 if (m_virtual_step) {
|
|
151 // If we've just completed a virtual step, all we need to do is check for a
|
|
152 // ShouldStopHere plan, and otherwise we're done.
|
|
153 // FIXME - This can be both a step in and a step out. Probably should
|
|
154 // record which in the m_virtual_step.
|
|
155 m_sub_plan_sp =
|
|
156 CheckShouldStopHereAndQueueStepOut(eFrameCompareYounger, m_status);
|
|
157 } else {
|
|
158 // Stepping through should be done running other threads in general, since
|
|
159 // we're setting a breakpoint and continuing. So only stop others if we
|
|
160 // are explicitly told to do so.
|
|
161
|
|
162 bool stop_others = (m_stop_others == lldb::eOnlyThisThread);
|
|
163
|
|
164 FrameComparison frame_order = CompareCurrentFrameToStartFrame();
|
|
165
|
173
|
166 Thread &thread = GetThread();
|
150
|
167 if (frame_order == eFrameCompareOlder ||
|
|
168 frame_order == eFrameCompareSameParent) {
|
|
169 // If we're in an older frame then we should stop.
|
|
170 //
|
|
171 // A caveat to this is if we think the frame is older but we're actually
|
|
172 // in a trampoline.
|
|
173 // I'm going to make the assumption that you wouldn't RETURN to a
|
|
174 // trampoline. So if we are in a trampoline we think the frame is older
|
|
175 // because the trampoline confused the backtracer.
|
173
|
176 m_sub_plan_sp = thread.QueueThreadPlanForStepThrough(
|
150
|
177 m_stack_id, false, stop_others, m_status);
|
|
178 if (!m_sub_plan_sp) {
|
|
179 // Otherwise check the ShouldStopHere for step out:
|
|
180 m_sub_plan_sp =
|
|
181 CheckShouldStopHereAndQueueStepOut(frame_order, m_status);
|
|
182 if (log) {
|
|
183 if (m_sub_plan_sp)
|
|
184 LLDB_LOGF(log,
|
|
185 "ShouldStopHere found plan to step out of this frame.");
|
|
186 else
|
|
187 LLDB_LOGF(log, "ShouldStopHere no plan to step out of this frame.");
|
|
188 }
|
|
189 } else if (log) {
|
|
190 LLDB_LOGF(
|
|
191 log, "Thought I stepped out, but in fact arrived at a trampoline.");
|
|
192 }
|
|
193 } else if (frame_order == eFrameCompareEqual && InSymbol()) {
|
|
194 // If we are not in a place we should step through, we're done. One
|
|
195 // tricky bit here is that some stubs don't push a frame, so we have to
|
|
196 // check both the case of a frame that is younger, or the same as this
|
|
197 // frame. However, if the frame is the same, and we are still in the
|
|
198 // symbol we started in, the we don't need to do this. This first check
|
|
199 // isn't strictly necessary, but it is more efficient.
|
|
200
|
|
201 // If we're still in the range, keep going, either by running to the next
|
|
202 // branch breakpoint, or by stepping.
|
|
203 if (InRange()) {
|
|
204 SetNextBranchBreakpoint();
|
|
205 return false;
|
|
206 }
|
|
207
|
|
208 SetPlanComplete();
|
|
209 m_no_more_plans = true;
|
|
210 return true;
|
|
211 }
|
|
212
|
|
213 // If we get to this point, we're not going to use a previously set "next
|
|
214 // branch" breakpoint, so delete it:
|
|
215 ClearNextBranchBreakpoint();
|
|
216
|
|
217 // We may have set the plan up above in the FrameIsOlder section:
|
|
218
|
|
219 if (!m_sub_plan_sp)
|
173
|
220 m_sub_plan_sp = thread.QueueThreadPlanForStepThrough(
|
150
|
221 m_stack_id, false, stop_others, m_status);
|
|
222
|
|
223 if (log) {
|
|
224 if (m_sub_plan_sp)
|
|
225 LLDB_LOGF(log, "Found a step through plan: %s",
|
|
226 m_sub_plan_sp->GetName());
|
|
227 else
|
|
228 LLDB_LOGF(log, "No step through plan found.");
|
|
229 }
|
|
230
|
|
231 // If not, give the "should_stop" callback a chance to push a plan to get
|
|
232 // us out of here. But only do that if we actually have stepped in.
|
|
233 if (!m_sub_plan_sp && frame_order == eFrameCompareYounger)
|
|
234 m_sub_plan_sp = CheckShouldStopHereAndQueueStepOut(frame_order, m_status);
|
|
235
|
|
236 // If we've stepped in and we are going to stop here, check to see if we
|
|
237 // were asked to run past the prologue, and if so do that.
|
|
238
|
|
239 if (!m_sub_plan_sp && frame_order == eFrameCompareYounger &&
|
|
240 m_step_past_prologue) {
|
173
|
241 lldb::StackFrameSP curr_frame = thread.GetStackFrameAtIndex(0);
|
150
|
242 if (curr_frame) {
|
|
243 size_t bytes_to_skip = 0;
|
173
|
244 lldb::addr_t curr_addr = thread.GetRegisterContext()->GetPC();
|
150
|
245 Address func_start_address;
|
|
246
|
|
247 SymbolContext sc = curr_frame->GetSymbolContext(eSymbolContextFunction |
|
|
248 eSymbolContextSymbol);
|
|
249
|
|
250 if (sc.function) {
|
|
251 func_start_address = sc.function->GetAddressRange().GetBaseAddress();
|
173
|
252 if (curr_addr == func_start_address.GetLoadAddress(&GetTarget()))
|
150
|
253 bytes_to_skip = sc.function->GetPrologueByteSize();
|
|
254 } else if (sc.symbol) {
|
|
255 func_start_address = sc.symbol->GetAddress();
|
173
|
256 if (curr_addr == func_start_address.GetLoadAddress(&GetTarget()))
|
150
|
257 bytes_to_skip = sc.symbol->GetPrologueByteSize();
|
|
258 }
|
|
259
|
|
260 if (bytes_to_skip == 0 && sc.symbol) {
|
173
|
261 const Architecture *arch = GetTarget().GetArchitecturePlugin();
|
150
|
262 if (arch) {
|
|
263 Address curr_sec_addr;
|
173
|
264 GetTarget().GetSectionLoadList().ResolveLoadAddress(curr_addr,
|
|
265 curr_sec_addr);
|
150
|
266 bytes_to_skip = arch->GetBytesToSkip(*sc.symbol, curr_sec_addr);
|
|
267 }
|
|
268 }
|
|
269
|
|
270 if (bytes_to_skip != 0) {
|
|
271 func_start_address.Slide(bytes_to_skip);
|
|
272 log = lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_STEP);
|
|
273 LLDB_LOGF(log, "Pushing past prologue ");
|
|
274
|
173
|
275 m_sub_plan_sp = thread.QueueThreadPlanForRunToAddress(
|
150
|
276 false, func_start_address, true, m_status);
|
|
277 }
|
|
278 }
|
|
279 }
|
|
280 }
|
|
281
|
|
282 if (!m_sub_plan_sp) {
|
|
283 m_no_more_plans = true;
|
|
284 SetPlanComplete();
|
|
285 return true;
|
|
286 } else {
|
|
287 m_no_more_plans = false;
|
|
288 m_sub_plan_sp->SetPrivate(true);
|
|
289 return false;
|
|
290 }
|
|
291 }
|
|
292
|
|
293 void ThreadPlanStepInRange::SetAvoidRegexp(const char *name) {
|
|
294 if (m_avoid_regexp_up)
|
207
|
295 *m_avoid_regexp_up = RegularExpression(name);
|
150
|
296 else
|
207
|
297 m_avoid_regexp_up = std::make_unique<RegularExpression>(name);
|
150
|
298 }
|
|
299
|
|
300 void ThreadPlanStepInRange::SetDefaultFlagValue(uint32_t new_value) {
|
|
301 // TODO: Should we test this for sanity?
|
|
302 ThreadPlanStepInRange::s_default_flag_values = new_value;
|
|
303 }
|
|
304
|
|
305 bool ThreadPlanStepInRange::FrameMatchesAvoidCriteria() {
|
|
306 StackFrame *frame = GetThread().GetStackFrameAtIndex(0).get();
|
|
307
|
|
308 // Check the library list first, as that's cheapest:
|
|
309 bool libraries_say_avoid = false;
|
|
310
|
|
311 FileSpecList libraries_to_avoid(GetThread().GetLibrariesToAvoid());
|
|
312 size_t num_libraries = libraries_to_avoid.GetSize();
|
|
313 if (num_libraries > 0) {
|
|
314 SymbolContext sc(frame->GetSymbolContext(eSymbolContextModule));
|
|
315 FileSpec frame_library(sc.module_sp->GetFileSpec());
|
|
316
|
|
317 if (frame_library) {
|
|
318 for (size_t i = 0; i < num_libraries; i++) {
|
|
319 const FileSpec &file_spec(libraries_to_avoid.GetFileSpecAtIndex(i));
|
|
320 if (FileSpec::Match(file_spec, frame_library)) {
|
|
321 libraries_say_avoid = true;
|
|
322 break;
|
|
323 }
|
|
324 }
|
|
325 }
|
|
326 }
|
|
327 if (libraries_say_avoid)
|
|
328 return true;
|
|
329
|
|
330 const RegularExpression *avoid_regexp_to_use = m_avoid_regexp_up.get();
|
|
331 if (avoid_regexp_to_use == nullptr)
|
|
332 avoid_regexp_to_use = GetThread().GetSymbolsToAvoidRegexp();
|
|
333
|
|
334 if (avoid_regexp_to_use != nullptr) {
|
|
335 SymbolContext sc = frame->GetSymbolContext(
|
|
336 eSymbolContextFunction | eSymbolContextBlock | eSymbolContextSymbol);
|
|
337 if (sc.symbol != nullptr) {
|
|
338 const char *frame_function_name =
|
|
339 sc.GetFunctionName(Mangled::ePreferDemangledWithoutArguments)
|
|
340 .GetCString();
|
|
341 if (frame_function_name) {
|
|
342 llvm::SmallVector<llvm::StringRef, 2> matches;
|
|
343 bool return_value =
|
|
344 avoid_regexp_to_use->Execute(frame_function_name, &matches);
|
|
345 if (return_value && matches.size() > 1) {
|
|
346 std::string match = matches[1].str();
|
|
347 LLDB_LOGF(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_STEP),
|
|
348 "Stepping out of function \"%s\" because it matches "
|
|
349 "the avoid regexp \"%s\" - match substring: \"%s\".",
|
|
350 frame_function_name,
|
|
351 avoid_regexp_to_use->GetText().str().c_str(),
|
|
352 match.c_str());
|
|
353 }
|
|
354 return return_value;
|
|
355 }
|
|
356 }
|
|
357 }
|
|
358 return false;
|
|
359 }
|
|
360
|
|
361 bool ThreadPlanStepInRange::DefaultShouldStopHereCallback(
|
|
362 ThreadPlan *current_plan, Flags &flags, FrameComparison operation,
|
|
363 Status &status, void *baton) {
|
|
364 bool should_stop_here = true;
|
|
365 StackFrame *frame = current_plan->GetThread().GetStackFrameAtIndex(0).get();
|
|
366 Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_STEP));
|
|
367
|
|
368 // First see if the ThreadPlanShouldStopHere default implementation thinks we
|
|
369 // should get out of here:
|
|
370 should_stop_here = ThreadPlanShouldStopHere::DefaultShouldStopHereCallback(
|
|
371 current_plan, flags, operation, status, baton);
|
|
372 if (!should_stop_here)
|
|
373 return false;
|
|
374
|
|
375 if (should_stop_here && current_plan->GetKind() == eKindStepInRange &&
|
|
376 operation == eFrameCompareYounger) {
|
|
377 ThreadPlanStepInRange *step_in_range_plan =
|
|
378 static_cast<ThreadPlanStepInRange *>(current_plan);
|
|
379 if (step_in_range_plan->m_step_into_target) {
|
|
380 SymbolContext sc = frame->GetSymbolContext(
|
|
381 eSymbolContextFunction | eSymbolContextBlock | eSymbolContextSymbol);
|
|
382 if (sc.symbol != nullptr) {
|
|
383 // First try an exact match, since that's cheap with ConstStrings.
|
|
384 // Then do a strstr compare.
|
|
385 if (step_in_range_plan->m_step_into_target == sc.GetFunctionName()) {
|
|
386 should_stop_here = true;
|
|
387 } else {
|
|
388 const char *target_name =
|
|
389 step_in_range_plan->m_step_into_target.AsCString();
|
|
390 const char *function_name = sc.GetFunctionName().AsCString();
|
|
391
|
|
392 if (function_name == nullptr)
|
|
393 should_stop_here = false;
|
|
394 else if (strstr(function_name, target_name) == nullptr)
|
|
395 should_stop_here = false;
|
|
396 }
|
|
397 if (log && !should_stop_here)
|
|
398 LLDB_LOGF(log,
|
|
399 "Stepping out of frame %s which did not match step into "
|
|
400 "target %s.",
|
|
401 sc.GetFunctionName().AsCString(),
|
|
402 step_in_range_plan->m_step_into_target.AsCString());
|
|
403 }
|
|
404 }
|
|
405
|
|
406 if (should_stop_here) {
|
|
407 ThreadPlanStepInRange *step_in_range_plan =
|
|
408 static_cast<ThreadPlanStepInRange *>(current_plan);
|
|
409 // Don't log the should_step_out here, it's easier to do it in
|
|
410 // FrameMatchesAvoidCriteria.
|
|
411 should_stop_here = !step_in_range_plan->FrameMatchesAvoidCriteria();
|
|
412 }
|
|
413 }
|
|
414
|
|
415 return should_stop_here;
|
|
416 }
|
|
417
|
|
418 bool ThreadPlanStepInRange::DoPlanExplainsStop(Event *event_ptr) {
|
|
419 // We always explain a stop. Either we've just done a single step, in which
|
|
420 // case we'll do our ordinary processing, or we stopped for some reason that
|
|
421 // isn't handled by our sub-plans, in which case we want to just stop right
|
|
422 // away. In general, we don't want to mark the plan as complete for
|
|
423 // unexplained stops. For instance, if you step in to some code with no debug
|
|
424 // info, so you step out and in the course of that hit a breakpoint, then you
|
|
425 // want to stop & show the user the breakpoint, but not unship the step in
|
|
426 // plan, since you still may want to complete that plan when you continue.
|
|
427 // This is particularly true when doing "step in to target function."
|
|
428 // stepping.
|
|
429 //
|
|
430 // The only variation is that if we are doing "step by running to next
|
|
431 // branch" in which case if we hit our branch breakpoint we don't set the
|
|
432 // plan to complete.
|
|
433
|
|
434 bool return_value = false;
|
|
435
|
|
436 if (m_virtual_step) {
|
|
437 return_value = true;
|
|
438 } else {
|
|
439 StopInfoSP stop_info_sp = GetPrivateStopInfo();
|
|
440 if (stop_info_sp) {
|
|
441 StopReason reason = stop_info_sp->GetStopReason();
|
|
442
|
|
443 if (reason == eStopReasonBreakpoint) {
|
|
444 if (NextRangeBreakpointExplainsStop(stop_info_sp)) {
|
|
445 return_value = true;
|
|
446 }
|
|
447 } else if (IsUsuallyUnexplainedStopReason(reason)) {
|
|
448 Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_STEP));
|
|
449 if (log)
|
|
450 log->PutCString("ThreadPlanStepInRange got asked if it explains the "
|
|
451 "stop for some reason other than step.");
|
|
452 return_value = false;
|
|
453 } else {
|
|
454 return_value = true;
|
|
455 }
|
|
456 } else
|
|
457 return_value = true;
|
|
458 }
|
|
459
|
|
460 return return_value;
|
|
461 }
|
|
462
|
|
463 bool ThreadPlanStepInRange::DoWillResume(lldb::StateType resume_state,
|
|
464 bool current_plan) {
|
|
465 m_virtual_step = false;
|
|
466 if (resume_state == eStateStepping && current_plan) {
|
173
|
467 Thread &thread = GetThread();
|
150
|
468 // See if we are about to step over a virtual inlined call.
|
173
|
469 bool step_without_resume = thread.DecrementCurrentInlinedDepth();
|
150
|
470 if (step_without_resume) {
|
|
471 Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_STEP));
|
|
472 LLDB_LOGF(log,
|
|
473 "ThreadPlanStepInRange::DoWillResume: returning false, "
|
|
474 "inline_depth: %d",
|
173
|
475 thread.GetCurrentInlinedDepth());
|
|
476 SetStopInfo(StopInfo::CreateStopReasonToTrace(thread));
|
150
|
477
|
|
478 // FIXME: Maybe it would be better to create a InlineStep stop reason, but
|
|
479 // then
|
|
480 // the whole rest of the world would have to handle that stop reason.
|
|
481 m_virtual_step = true;
|
|
482 }
|
|
483 return !step_without_resume;
|
|
484 }
|
|
485 return true;
|
|
486 }
|
|
487
|
|
488 bool ThreadPlanStepInRange::IsVirtualStep() { return m_virtual_step; }
|