Skip to content

Commit f4797b0

Browse files
committed
trace: STRCMP helper function
`trace` filters and print expressions can now use the magic STRCMP helper function to compare strings. The first string must be a compile-time constant literal string, such as "test", and the second string can be determined at runtime (e.g., from a function argument). The codegen for STRCMP is on a case-by-case basis for each literal string, and it generates an inline function with a constant-length loop that compares the string's characters. This is a decent workaround until we get something more reasonable from the kernel side, such as a `bpf_strcmp` helper. Usage example: ``` trace 'p:c:open (STRCMP("test.txt", arg1)) "%s", arg1' ``
1 parent 56ddca0 commit f4797b0

3 files changed

Lines changed: 49 additions & 8 deletions

File tree

man/man8/trace.8

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ Note that only arg1-arg6 are supported, and only if the function is using the
9494
standard x86_64 convention where the first six arguments are in the RDI, RSI,
9595
RDX, RCX, R8, R9 registers. If no predicate is specified, all function
9696
invocations are traced.
97+
98+
The predicate expression may also use the STRCMP pseudo-function to compare
99+
a predefined string to a string argument. For example: STRCMP("test", arg1).
100+
The order of arguments is important: the first argument MUST be a quoted
101+
literal string, and the second argument can be a runtime string, most typically
102+
an argument.
97103
.TP
98104
.B ["format string"[, arguments]]
99105
A printf-style format string that will be used for the trace message. You can

tools/trace.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def monotonic_time():
4646

4747
class Probe(object):
4848
probe_count = 0
49+
streq_index = 0
4950
max_events = None
5051
event_count = 0
5152
first_ts = 0
@@ -61,6 +62,7 @@ def configure(cls, args):
6162

6263
def __init__(self, probe, string_size, kernel_stack, user_stack):
6364
self.usdt = None
65+
self.streq_functions = ""
6466
self.raw_probe = probe
6567
self.string_size = string_size
6668
self.kernel_stack = kernel_stack
@@ -159,7 +161,7 @@ def _find_usdt_probe(self):
159161
self._bail("unrecognized USDT probe %s" % self.usdt_name)
160162

161163
def _parse_filter(self, filt):
162-
self.filter = self._replace_args(filt)
164+
self.filter = self._rewrite_expr(filt)
163165

164166
def _parse_types(self, fmt):
165167
for match in re.finditer(
@@ -178,14 +180,14 @@ def _parse_action(self, action):
178180
return
179181

180182
action = action.strip()
181-
match = re.search(r'(\".*\"),?(.*)', action)
183+
match = re.search(r'(\".*?\"),?(.*)', action)
182184
if match is None:
183185
self._bail("expected format string in \"s")
184186

185187
self.raw_format = match.group(1)
186188
self._parse_types(self.raw_format)
187-
for part in match.group(2).split(','):
188-
part = self._replace_args(part)
189+
for part in re.split('(?<!"),', match.group(2)):
190+
part = self._rewrite_expr(part)
189191
if len(part) > 0:
190192
self.values.append(part)
191193

@@ -204,14 +206,37 @@ def _parse_action(self, action):
204206
"$cpu": "bpf_get_smp_processor_id()"
205207
}
206208

207-
def _replace_args(self, expr):
209+
def _generate_streq_function(self, string):
210+
fname = "streq_%d" % Probe.streq_index
211+
Probe.streq_index += 1
212+
self.streq_functions += """
213+
static inline bool %s(char const *ignored, unsigned long str) {
214+
char needle[] = %s;
215+
char haystack[sizeof(needle)];
216+
bpf_probe_read(&haystack, sizeof(haystack), (void *)str);
217+
for (int i = 0; i < sizeof(needle); ++i) {
218+
if (needle[i] != haystack[i]) {
219+
return false;
220+
}
221+
}
222+
return true;
223+
}
224+
""" % (fname, string)
225+
return fname
226+
227+
def _rewrite_expr(self, expr):
208228
for alias, replacement in Probe.aliases.items():
209229
# For USDT probes, we replace argN values with the
210230
# actual arguments for that probe obtained using
211231
# bpf_readarg_N macros emitted at BPF construction.
212232
if alias.startswith("arg") and self.probe_type == "u":
213233
continue
214234
expr = expr.replace(alias, replacement)
235+
matches = re.finditer('STRCMP\\(("[^"]+\\")', expr)
236+
for match in matches:
237+
string = match.group(1)
238+
fname = self._generate_streq_function(string)
239+
expr = expr.replace("STRCMP", fname, 1)
215240
return expr
216241

217242
p_type = {"u": ct.c_uint, "d": ct.c_int,
@@ -405,7 +430,7 @@ def generate_program(self, include_self):
405430
self.struct_name, data_fields,
406431
stack_trace, self.events_name, ctx_name)
407432

408-
return data_decl + "\n" + text
433+
return self.streq_functions + data_decl + "\n" + text
409434

410435
@classmethod
411436
def _time_off_str(cls, timestamp_ns):
@@ -526,7 +551,7 @@ class Tool(object):
526551
Trace the write() call from libc to monitor writes to STDOUT
527552
trace 'r::__kmalloc (retval == 0) "kmalloc failed!"
528553
Trace returns from __kmalloc which returned a null pointer
529-
trace 'r:c:malloc (retval) "allocated = %p", retval
554+
trace 'r:c:malloc (retval) "allocated = %x", retval
530555
Trace returns from malloc and print non-NULL allocated buffers
531556
trace 't:block:block_rq_complete "sectors=%d", args->nr_sector'
532557
Trace the block_rq_complete kernel tracepoint and print # of tx sectors

tools/trace_example.txt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,16 @@ In the previous invocation, arg1 and arg2 are the class name and method name
136136
for the Ruby method being invoked.
137137

138138

139+
Occasionally, it can be useful to filter specific strings. For example, you
140+
might be interested in open() calls that open a specific file:
141+
142+
# trace 'p:c:open (STRCMP("test.txt", arg1)) "opening %s", arg1'
143+
TIME PID COMM FUNC -
144+
01:43:15 10938 cat open opening test.txt
145+
01:43:20 10939 cat open opening test.txt
146+
^C
147+
148+
139149
As a final example, let's trace open syscalls for a specific process. By
140150
default, tracing is system-wide, but the -p switch overrides this:
141151

@@ -202,7 +212,7 @@ trace 'p:c:write (arg1 == 1) "writing %d bytes to STDOUT", arg3'
202212
Trace the write() call from libc to monitor writes to STDOUT
203213
trace 'r::__kmalloc (retval == 0) "kmalloc failed!"
204214
Trace returns from __kmalloc which returned a null pointer
205-
trace 'r:c:malloc (retval) "allocated = %p", retval
215+
trace 'r:c:malloc (retval) "allocated = %x", retval
206216
Trace returns from malloc and print non-NULL allocated buffers
207217
trace 't:block:block_rq_complete "sectors=%d", args->nr_sector'
208218
Trace the block_rq_complete kernel tracepoint and print # of tx sectors

0 commit comments

Comments
 (0)