Skip to content

Commit 4fdb8cb

Browse files
authored
Make stop-hooks fire when lldb first gains control of a process. (#137410)
stop-hooks are supposed to trigger every time the process stops, but as initially implemented they would only fire when control was returned to the user. So for instance when a process was launched the stop hook would only trigger when the process hit a breakpoint or crashed. However, it would be really useful to be able to trigger a stop hook when lldb first gains control over the process. One way to do that would be to implement general "target lifecycle events" and then send process created events that users could bind actions to. OTOH, extending the stop hooks to fire when lldb first gains control over the process is a pretty natural extension to the notion of a stop hook. So this patch takes the shorter route to that ability by making stop-hooks fire when lldb first gains control over the process. I also added the ability to specify whether to trigger the stop hook "on gaining control". I'm on the fence about whether to set the default to be "trigger on gaining control" or "don't trigger on gaining control". Since I think it's a generally useful feature, I've set the default to "trigger on gaining control".
1 parent 42f5d71 commit 4fdb8cb

File tree

12 files changed

+1221
-10
lines changed

12 files changed

+1221
-10
lines changed

lldb/include/lldb/Target/Target.h

+10-1
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,12 @@ class Target : public std::enable_shared_from_this<Target>,
13701370

13711371
bool GetAutoContinue() const { return m_auto_continue; }
13721372

1373+
void SetRunAtInitialStop(bool at_initial_stop) {
1374+
m_at_initial_stop = at_initial_stop;
1375+
}
1376+
1377+
bool GetRunAtInitialStop() const { return m_at_initial_stop; }
1378+
13731379
void GetDescription(Stream &s, lldb::DescriptionLevel level) const;
13741380
virtual void GetSubclassDescription(Stream &s,
13751381
lldb::DescriptionLevel level) const = 0;
@@ -1380,6 +1386,7 @@ class Target : public std::enable_shared_from_this<Target>,
13801386
std::unique_ptr<ThreadSpec> m_thread_spec_up;
13811387
bool m_active = true;
13821388
bool m_auto_continue = false;
1389+
bool m_at_initial_stop = true;
13831390

13841391
StopHook(lldb::TargetSP target_sp, lldb::user_id_t uid);
13851392
};
@@ -1446,7 +1453,9 @@ class Target : public std::enable_shared_from_this<Target>,
14461453

14471454
// Runs the stop hooks that have been registered for this target.
14481455
// Returns true if the stop hooks cause the target to resume.
1449-
bool RunStopHooks();
1456+
// Pass at_initial_stop if this is the stop where lldb gains
1457+
// control over the process for the first time.
1458+
bool RunStopHooks(bool at_initial_stop = false);
14501459

14511460
size_t GetStopHookSize();
14521461

lldb/source/Commands/CommandObjectTarget.cpp

+16
Original file line numberDiff line numberDiff line change
@@ -4794,6 +4794,17 @@ class CommandObjectTargetStopHookAdd : public CommandObjectParsed,
47944794
m_one_liner.push_back(std::string(option_arg));
47954795
break;
47964796

4797+
case 'I': {
4798+
bool value, success;
4799+
value = OptionArgParser::ToBoolean(option_arg, false, &success);
4800+
if (success)
4801+
m_at_initial_stop = value;
4802+
else
4803+
error = Status::FromErrorStringWithFormat(
4804+
"invalid boolean value '%s' passed for -F option",
4805+
option_arg.str().c_str());
4806+
} break;
4807+
47974808
default:
47984809
llvm_unreachable("Unimplemented option");
47994810
}
@@ -4820,6 +4831,7 @@ class CommandObjectTargetStopHookAdd : public CommandObjectParsed,
48204831
m_use_one_liner = false;
48214832
m_one_liner.clear();
48224833
m_auto_continue = false;
4834+
m_at_initial_stop = true;
48234835
}
48244836

48254837
std::string m_class_name;
@@ -4840,6 +4852,7 @@ class CommandObjectTargetStopHookAdd : public CommandObjectParsed,
48404852
// Instance variables to hold the values for one_liner options.
48414853
bool m_use_one_liner = false;
48424854
std::vector<std::string> m_one_liner;
4855+
bool m_at_initial_stop;
48434856

48444857
bool m_auto_continue = false;
48454858
};
@@ -5005,6 +5018,9 @@ Filter Options:
50055018
if (specifier_up)
50065019
new_hook_sp->SetSpecifier(specifier_up.release());
50075020

5021+
// Should we run at the initial stop:
5022+
new_hook_sp->SetRunAtInitialStop(m_options.m_at_initial_stop);
5023+
50085024
// Next see if any of the thread options have been entered:
50095025

50105026
if (m_options.m_thread_specified) {

lldb/source/Commands/Options.td

+7-1
Original file line numberDiff line numberDiff line change
@@ -1052,8 +1052,14 @@ let Command = "target stop hook add" in {
10521052
Arg<"FunctionName">, Desc<"Set the function name within which the stop hook"
10531053
" will be run.">, Completion<"Symbol">;
10541054
def target_stop_hook_add_auto_continue : Option<"auto-continue", "G">,
1055-
Arg<"Boolean">, Desc<"The breakpoint will auto-continue after running its"
1055+
Arg<"Boolean">, Desc<"The stop-hook will auto-continue after running its"
10561056
" commands.">;
1057+
def target_stop_hook_add_at_initial_stop : Option<"at-initial-stop", "I">,
1058+
Arg<"Boolean">, Desc<"Whether the stop-hook will trigger when lldb "
1059+
"initially gains control of the process. For a process launch, this "
1060+
"initial stop may happen very early on - before the loader has run. You "
1061+
"can use this option if you do not want some stop-hooks to run then. "
1062+
"Defaults to true.">;
10571063
}
10581064

10591065
let Command = "thread backtrace" in {

lldb/source/Target/Process.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -2841,6 +2841,9 @@ Status Process::LoadCore() {
28412841
"Did not get stopped event after loading the core file.");
28422842
}
28432843
RestoreProcessEvents();
2844+
// Since we hijacked the event stream, we will have we won't have run the
2845+
// stop hooks. Make sure we do that here:
2846+
GetTarget().RunStopHooks(/* at_initial_stop= */ true);
28442847
}
28452848
return error;
28462849
}
@@ -3211,6 +3214,9 @@ void Process::CompleteAttach() {
32113214
: "<none>");
32123215
}
32133216
}
3217+
// Since we hijacked the event stream, we will have we won't have run the
3218+
// stop hooks. Make sure we do that here:
3219+
GetTarget().RunStopHooks(/* at_initial_stop= */ true);
32143220
}
32153221

32163222
Status Process::ConnectRemote(llvm::StringRef remote_url) {

lldb/source/Target/Target.cpp

+33-5
Original file line numberDiff line numberDiff line change
@@ -3038,7 +3038,7 @@ void Target::SetAllStopHooksActiveState(bool active_state) {
30383038
}
30393039
}
30403040

3041-
bool Target::RunStopHooks() {
3041+
bool Target::RunStopHooks(bool at_initial_stop) {
30423042
if (m_suppress_stop_hooks)
30433043
return false;
30443044

@@ -3047,14 +3047,19 @@ bool Target::RunStopHooks() {
30473047

30483048
// Somebody might have restarted the process:
30493049
// Still return false, the return value is about US restarting the target.
3050-
if (m_process_sp->GetState() != eStateStopped)
3050+
lldb::StateType state = m_process_sp->GetState();
3051+
if (!(state == eStateStopped || state == eStateAttaching))
30513052
return false;
30523053

30533054
if (m_stop_hooks.empty())
30543055
return false;
30553056

30563057
bool no_active_hooks =
3057-
llvm::none_of(m_stop_hooks, [](auto &p) { return p.second->IsActive(); });
3058+
llvm::none_of(m_stop_hooks, [at_initial_stop](auto &p) {
3059+
bool should_run_now =
3060+
!at_initial_stop || p.second->GetRunAtInitialStop();
3061+
return p.second->IsActive() && should_run_now;
3062+
});
30583063
if (no_active_hooks)
30593064
return false;
30603065

@@ -3084,9 +3089,22 @@ bool Target::RunStopHooks() {
30843089
}
30853090

30863091
// If no threads stopped for a reason, don't run the stop-hooks.
3092+
// However, if this is the FIRST stop for this process, then we are in the
3093+
// state where an attach or a core file load was completed without designating
3094+
// a particular thread as responsible for the stop. In that case, we do
3095+
// want to run the stop hooks, but do so just on one thread.
30873096
size_t num_exe_ctx = exc_ctx_with_reasons.size();
3088-
if (num_exe_ctx == 0)
3089-
return false;
3097+
if (num_exe_ctx == 0) {
3098+
if (at_initial_stop && num_threads > 0) {
3099+
lldb::ThreadSP thread_to_use_sp = cur_threadlist.GetThreadAtIndex(0);
3100+
exc_ctx_with_reasons.emplace_back(
3101+
m_process_sp.get(), thread_to_use_sp.get(),
3102+
thread_to_use_sp->GetStackFrameAtIndex(0).get());
3103+
num_exe_ctx = 1;
3104+
} else {
3105+
return false;
3106+
}
3107+
}
30903108

30913109
StreamSP output_sp = m_debugger.GetAsyncOutputStream();
30923110
auto on_exit = llvm::make_scope_exit([output_sp] { output_sp->Flush(); });
@@ -3100,6 +3118,8 @@ bool Target::RunStopHooks() {
31003118
StopHookSP cur_hook_sp = stop_entry.second;
31013119
if (!cur_hook_sp->IsActive())
31023120
continue;
3121+
if (at_initial_stop && !cur_hook_sp->GetRunAtInitialStop())
3122+
continue;
31033123

31043124
bool any_thread_matched = false;
31053125
for (auto exc_ctx : exc_ctx_with_reasons) {
@@ -3426,10 +3446,14 @@ Status Target::Launch(ProcessLaunchInfo &launch_info, Stream *stream) {
34263446
m_process_sp->RestoreProcessEvents();
34273447

34283448
if (rebroadcast_first_stop) {
3449+
// We don't need to run the stop hooks by hand here, they will get
3450+
// triggered when this rebroadcast event gets fetched.
34293451
assert(first_stop_event_sp);
34303452
m_process_sp->BroadcastEvent(first_stop_event_sp);
34313453
return error;
34323454
}
3455+
// Run the stop hooks that want to run at entry.
3456+
RunStopHooks(true /* at entry point */);
34333457

34343458
switch (state) {
34353459
case eStateStopped: {
@@ -3582,6 +3606,10 @@ Status Target::Attach(ProcessAttachInfo &attach_info, Stream *stream) {
35823606
true, SelectMostRelevantFrame);
35833607
process_sp->RestoreProcessEvents();
35843608

3609+
// Run the stop hooks here. Since we were hijacking the events, they
3610+
// wouldn't have gotten run as part of event delivery.
3611+
RunStopHooks(/* at_initial_stop= */ true);
3612+
35853613
if (state != eStateStopped) {
35863614
const char *exit_desc = process_sp->GetExitDescription();
35873615
if (exit_desc)

lldb/test/API/commands/target/stop-hooks/TestStopHookScripted.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ def test_bad_handler(self):
5050

5151
def test_stop_hooks_scripted(self):
5252
"""Test that a scripted stop hook works with no specifiers"""
53-
self.stop_hooks_scripted(5)
53+
self.stop_hooks_scripted(5, "-I false")
54+
55+
def test_stop_hooks_scripted_no_entry(self):
56+
"""Test that a scripted stop hook works with no specifiers"""
57+
self.stop_hooks_scripted(10)
5458

5559
def test_stop_hooks_scripted_right_func(self):
5660
"""Test that a scripted stop hook fires when there is a function match"""

lldb/test/API/commands/target/stop-hooks/TestStopHooks.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def step_out_test(self):
5555
def after_expr_test(self):
5656
interp = self.dbg.GetCommandInterpreter()
5757
result = lldb.SBCommandReturnObject()
58-
interp.HandleCommand("target stop-hook add -o 'expr g_var++'", result)
58+
interp.HandleCommand("target stop-hook add -o 'expr g_var++' -I false", result)
5959
self.assertTrue(result.Succeeded(), "Set the target stop hook")
6060

6161
(target, process, thread, first_bkpt) = lldbutil.run_to_source_breakpoint(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
Test that stop hooks fire on core load (first stop)
3+
"""
4+
5+
6+
import lldb
7+
import os
8+
from lldbsuite.test.decorators import *
9+
from lldbsuite.test.lldbtest import *
10+
from lldbsuite.test import lldbutil
11+
12+
13+
class TestStopOnCoreLoad(TestBase):
14+
NO_DEBUG_INFO_TESTCASE = True
15+
16+
# This was originally marked as expected failure on Windows, but it has
17+
# started timing out instead, so the expectedFailure attribute no longer
18+
# correctly tracks it: llvm.org/pr37371
19+
@skipIfWindows
20+
def test_hook_runs_no_threads(self):
21+
# Create core form YAML.
22+
core_path = self.getBuildArtifact("test.core")
23+
self.yaml2obj("test.core.yaml", core_path)
24+
25+
# Since mach core files don't have stop reasons, we should choose
26+
# the first thread:
27+
self.do_test(core_path, 1)
28+
29+
def test_hook_one_thread(self):
30+
core_path = os.path.join(self.getSourceDir(), "linux-x86_64.core")
31+
self.do_test(core_path, 3)
32+
33+
def do_test(self, core_path, stop_thread):
34+
# Set debugger into synchronous mode
35+
self.dbg.SetAsync(False)
36+
37+
# Create a target by the debugger.
38+
target = self.dbg.CreateTarget("")
39+
40+
# load the stop hook module and add the stop hook:
41+
stop_hook_path = os.path.join(self.getSourceDir(), "stop_hook.py")
42+
self.runCmd(f"command script import {stop_hook_path}")
43+
self.runCmd("target stop-hook add -P stop_hook.stop_handler")
44+
45+
# Load core.
46+
process = target.LoadCore(core_path)
47+
self.assertTrue(process, PROCESS_IS_VALID)
48+
# Now run our report command and make sure we get the right answer.
49+
50+
result = lldb.SBCommandReturnObject()
51+
self.dbg.GetCommandInterpreter().HandleCommand("report_command", result)
52+
print(f"Command Output: '{result.GetOutput}'")
53+
self.assertIn(
54+
f"Stop Threads: {stop_thread}", result.GetOutput(), "Ran the stop hook"
55+
)
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import lldb
2+
3+
4+
def report_command(debugger, command, exe_ctx, result, internal_dict):
5+
global stop_thread
6+
print(f"About to report out stop_thread: {stop_thread}")
7+
mssg = f"Stop Threads: {stop_thread}"
8+
result.AppendMessage(mssg)
9+
10+
result.SetStatus(lldb.eReturnStatusSuccessFinishResult)
11+
12+
13+
class stop_handler:
14+
def __init__(self, target, extra_args, dict):
15+
global stop_thread
16+
stop_thead = 0
17+
self.target = target
18+
19+
def handle_stop(self, exe_ctx, stream):
20+
global stop_thread
21+
thread = exe_ctx.thread
22+
stop_thread = thread.idx
23+
24+
25+
def __lldb_init_module(debugger, internal_dict):
26+
global stop_thread
27+
stop_thread = 0
28+
debugger.HandleCommand(
29+
f"command script add -o -f '{__name__}.report_command' report_command"
30+
)

0 commit comments

Comments
 (0)