Skip to content

Commit 78423e1

Browse files
authored
Add support for executing a program and tracing it (#5362)
For short-lived processes, specify '-p pid' is difficult to trace. Add support for executing a program and tracing it. When the newly child process exits, tool itself exists subsequently. Added to syscount/opensnoop tools this time, other tools as needed. For example: ./syscount.py -i 1 --exec ls -l /tmp
1 parent 21143df commit 78423e1

3 files changed

Lines changed: 112 additions & 0 deletions

File tree

src/python/bcc/exec.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright 2025 Rocky Xing
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import signal
17+
import sys
18+
19+
child_exit = 0
20+
21+
pipe_read, pipe_write = os.pipe()
22+
23+
def _child_signal_handler(signum, frame):
24+
while True:
25+
try:
26+
pid, status = os.waitpid(-1, os.WNOHANG)
27+
if pid == 0:
28+
break
29+
except OSError:
30+
break
31+
32+
global child_exit
33+
child_exit = 1
34+
35+
def run_cmd(args) -> int:
36+
pid = os.fork()
37+
if pid < 0:
38+
print("failed to fork", file=sys.stderr)
39+
sys.exit(1)
40+
elif pid == 0:
41+
try:
42+
os.close(pipe_write)
43+
os.read(pipe_read, 1)
44+
os.execvp(args[0], args)
45+
except OSError as e:
46+
print("failed to exec command: %s: %s" % (' '.join(args), e), file=sys.stderr)
47+
sys.exit(1)
48+
finally:
49+
os.close(pipe_read)
50+
sys.exit(0)
51+
else:
52+
signal.signal(signal.SIGCHLD, _child_signal_handler)
53+
return pid
54+
55+
def cmd_ready():
56+
try:
57+
os.close(pipe_read)
58+
os.write(pipe_write, b'x')
59+
except OSError as e:
60+
sys.exit(1)
61+
finally:
62+
os.close(pipe_write)
63+
64+
def cmd_exited():
65+
return child_exit
66+

tools/opensnoop.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@
1919
# 06-Jan-2019 Takuma Kume Support filtering by UID
2020
# 21-Aug-2022 Rocky Xing Support showing full path for an open file.
2121
# 06-Sep-2022 Rocky Xing Support setting size of the perf ring buffer.
22+
# 13-Jul-2025 Rocky Xing Execute a program and trace it's open() syscalls.
2223

2324
from __future__ import print_function
2425
from bcc import ArgString, BPF
2526
from bcc.containers import filter_by_containers
27+
from bcc.exec import run_cmd, cmd_ready, cmd_exited
2628
from bcc.utils import printb
2729
import argparse
2830
from collections import defaultdict
@@ -82,8 +84,20 @@
8284
parser.add_argument("-b", "--buffer-pages", type=int, default=64,
8385
help="size of the perf ring buffer "
8486
"(must be a power of two number of pages and defaults to 64)")
87+
parser.add_argument('--exec', nargs=argparse.REMAINDER,
88+
help="execute command (as the last parameter, "
89+
"supports multiple parameters, for example: --exec ls -l /tmp")
8590
args = parser.parse_args()
8691
debug = 0
92+
93+
if args.pid and args.exec:
94+
print("ERROR: can only use -p or --exec. Exiting.")
95+
exit()
96+
97+
if args.exec is not None and len(args.exec) == 0:
98+
print("ERROR: --exec without command. Exiting.")
99+
exit()
100+
87101
if args.duration:
88102
args.duration = timedelta(seconds=int(args.duration))
89103
flag_filter_mask = 0
@@ -399,6 +413,12 @@
399413
'if (pid != %s) { return 0; }' % args.pid)
400414
bpf_text = bpf_text.replace('KFUNC_PID_TID_FILTER',
401415
'if (pid != %s) { events.ringbuf_discard(data, 0); return 0; }' % args.pid)
416+
elif args.exec:
417+
child_pid = run_cmd(args.exec)
418+
bpf_text = bpf_text.replace('KPROBE_PID_TID_FILTER',
419+
'if (pid != %s) { return 0; }' % child_pid)
420+
bpf_text = bpf_text.replace('KFUNC_PID_TID_FILTER',
421+
'if (pid != %s) { events.ringbuf_discard(data, 0); return 0; }' % child_pid)
402422
else:
403423
bpf_text = bpf_text.replace('KPROBE_PID_TID_FILTER', '')
404424
bpf_text = bpf_text.replace('KFUNC_PID_TID_FILTER', '')
@@ -514,6 +534,9 @@
514534
b.attach_kprobe(event=fnname_openat2, fn_name="syscall__trace_entry_openat2")
515535
b.attach_kretprobe(event=fnname_openat2, fn_name="trace_return")
516536

537+
if args.exec:
538+
cmd_ready()
539+
517540
initial_ts = 0
518541

519542
# header
@@ -604,3 +627,5 @@ def print_event(cpu, data, size):
604627
b.ring_buffer_poll()
605628
except KeyboardInterrupt:
606629
exit()
630+
if args.exec and cmd_exited():
631+
exit()

tools/syscount.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111
# 15-Feb-2017 Sasha Goldshtein Created this.
1212
# 16-May-2022 Rocky Xing Added TID filter support.
1313
# 26-Jul-2022 Rocky Xing Added syscall filter support.
14+
# 12-Jul-2025 Rocky Xing Execute a program and trace it's syscalls.
1415

1516
from time import sleep, strftime
1617
import argparse
1718
import errno
1819
import itertools
20+
import os
1921
import sys
2022
import signal
2123
from bcc import BPF
24+
from bcc.exec import run_cmd, cmd_ready, cmd_exited
2225
from bcc.utils import printb
2326
from bcc.syscall import syscall_name, syscalls
2427

@@ -73,12 +76,23 @@ def handle_errno(errstr):
7376
help="trace this syscall only (use option -l to get all recognized syscalls)")
7477
parser.add_argument("--ebpf", action="store_true",
7578
help=argparse.SUPPRESS)
79+
parser.add_argument('--exec', nargs=argparse.REMAINDER,
80+
help="execute command (as the last parameter, "
81+
"supports multiple parameters, for example: --exec ls -l /tmp")
7682
args = parser.parse_args()
7783
if args.duration and not args.interval:
7884
args.interval = args.duration
7985
if not args.interval:
8086
args.interval = 99999999
8187

88+
if args.pid and args.exec:
89+
print("ERROR: can only use -p or --exec. Exiting.")
90+
exit()
91+
92+
if args.exec is not None and len(args.exec) == 0:
93+
print("ERROR: --exec without command. Exiting.")
94+
exit()
95+
8296
syscall_nr = -1
8397
if args.syscall is not None:
8498
syscall = bytes(args.syscall, 'utf-8')
@@ -211,6 +225,8 @@ def handle_errno(errstr):
211225

212226
if args.pid:
213227
text = ("#define FILTER_PID %d\n" % args.pid) + text
228+
elif args.exec:
229+
text = ("#define FILTER_PID %d\n" % run_cmd(args.exec)) + text
214230
elif args.tid:
215231
text = ("#define FILTER_TID %d\n" % args.tid) + text
216232
elif args.ppid:
@@ -231,6 +247,9 @@ def handle_errno(errstr):
231247

232248
bpf = BPF(text=text)
233249

250+
if args.exec:
251+
cmd_ready()
252+
234253
def print_stats():
235254
if args.latency:
236255
print_latency_stats()
@@ -303,6 +322,8 @@ def print_latency_stats():
303322
signal.signal(signal.SIGINT, signal_ignore)
304323
if args.duration and seconds >= args.duration:
305324
exiting = 1
325+
if args.exec and cmd_exited():
326+
exiting = 1
306327

307328
print_stats()
308329

0 commit comments

Comments
 (0)