Commit | Line | Data |
---|---|---|
40b2a4a7 | 1 | #!/usr/bin/env python3 |
591ee332 | 2 | # |
9d16b343 | 3 | # Copyright (C) 2017 Francis Deslauriers <francis.deslauriers@efficios.com> |
591ee332 | 4 | # |
9d16b343 | 5 | # SPDX-License-Identifier: LGPL-2.1-only |
591ee332 | 6 | # |
591ee332 FD |
7 | |
8 | import sys | |
9 | import bisect | |
10 | import subprocess | |
11 | import re | |
12 | ||
6a871bbe | 13 | |
591ee332 FD |
14 | def addr2line(executable, addr): |
15 | """ | |
6a871bbe | 16 | Uses binutils' addr2line to get function containing a given address |
591ee332 | 17 | """ |
6a871bbe | 18 | cmd = ["addr2line"] |
591ee332 | 19 | |
6a871bbe | 20 | cmd += ["-e", executable] |
591ee332 FD |
21 | |
22 | # Print function names | |
6a871bbe | 23 | cmd += ["--functions"] |
591ee332 FD |
24 | |
25 | # Expand inlined functions | |
6a871bbe | 26 | cmd += ["--addresses", addr] |
591ee332 | 27 | |
40b2a4a7 OD |
28 | status = subprocess.run(cmd, stdout=subprocess.PIPE, check=True) |
29 | ||
9613cdd0 JG |
30 | addr2line_output = status.stdout.decode("utf-8").splitlines() |
31 | # addr2line's output is made of 3-tuples: | |
32 | # - address | |
33 | # - function name | |
34 | # - source location | |
35 | if len(addr2line_output) % 3 != 0: | |
6a871bbe KS |
36 | raise Exception( |
37 | "Unexpected addr2line output:\n\t{}".format("\n\t".join(addr2line_output)) | |
38 | ) | |
9613cdd0 JG |
39 | |
40 | function_names = [] | |
41 | for address_line_number in range(0, len(addr2line_output), 3): | |
42 | function_name = addr2line_output[address_line_number + 1] | |
43 | ||
44 | # Filter-out unresolved functions | |
45 | if "??" not in function_name: | |
46 | function_names.append(addr2line_output[address_line_number + 1]) | |
47 | ||
48 | return function_names | |
591ee332 | 49 | |
6a871bbe | 50 | |
591ee332 FD |
51 | def extract_user_func_names(executable, raw_callstack): |
52 | """ | |
6a871bbe KS |
53 | Given a callstack from the Babeltrace CLI output, returns a set |
54 | containing the name of the functions. This assumes that the binary have | |
55 | not changed since the execution. | |
591ee332 FD |
56 | """ |
57 | recorded_callstack = set() | |
58 | ||
59 | # Remove commas and split on spaces | |
6a871bbe | 60 | for index, addr in enumerate(raw_callstack.replace(",", "").split(" ")): |
591ee332 FD |
61 | # Consider only the elements starting with '0x' which are the |
62 | # addresses recorded in the callstack | |
6a871bbe | 63 | if "0x" in addr[:2]: |
591ee332 FD |
64 | funcs = addr2line(executable, addr) |
65 | recorded_callstack.update(funcs) | |
66 | ||
67 | return recorded_callstack | |
68 | ||
6a871bbe | 69 | |
591ee332 FD |
70 | def extract_kernel_func_names(raw_callstack): |
71 | """ | |
6a871bbe KS |
72 | Given a callstack from the Babeltrace CLI output, returns a set |
73 | containing the name of the functions. | |
74 | Uses the /proc/kallsyms procfile to find the symbol associated with an | |
75 | address. This function should only be used if the user is root or has | |
76 | access to /proc/kallsyms. | |
591ee332 FD |
77 | """ |
78 | recorded_callstack = set() | |
6a871bbe KS |
79 | syms = [] |
80 | addresses = [] | |
591ee332 | 81 | # We read kallsyms file and save the output |
6a871bbe | 82 | with open("/proc/kallsyms") as kallsyms_f: |
591ee332 FD |
83 | for line in kallsyms_f: |
84 | line_tokens = line.split() | |
85 | addr = line_tokens[0] | |
86 | symbol = line_tokens[2] | |
87 | addresses.append(int(addr, 16)) | |
6a871bbe | 88 | syms.append({"addr": int(addr, 16), "symbol": symbol}) |
591ee332 FD |
89 | |
90 | # Save the address and symbol in a sorted list of tupple | |
6a871bbe | 91 | syms = sorted(syms, key=lambda k: k["addr"]) |
591ee332 FD |
92 | # We save the list of addresses in a seperate sorted list to easily bisect |
93 | # the closer address of a symbol. | |
94 | addresses = sorted(addresses) | |
95 | ||
96 | # Remove commas and split on spaces | |
6a871bbe KS |
97 | for addr in raw_callstack.replace(",", "").split(" "): |
98 | if "0x" in addr[:2]: | |
591ee332 FD |
99 | # Search the location of the address in the addresses list and |
100 | # deference this location in the syms list to add the associated | |
101 | # symbol. | |
102 | loc = bisect.bisect(addresses, int(addr, 16)) | |
6a871bbe | 103 | recorded_callstack.add(syms[loc - 1]["symbol"]) |
591ee332 FD |
104 | |
105 | return recorded_callstack | |
106 | ||
6a871bbe | 107 | |
591ee332 | 108 | # Regex capturing the callstack_user and callstack_kernel context |
6a871bbe KS |
109 | user_cs_rexp = ".*callstack_user\ \=\ \[(.*)\]\ .*\}, \{.*\}" |
110 | kernel_cs_rexp = ".*callstack_kernel\ \=\ \[(.*)\]\ .*\}, \{.*\}" | |
111 | ||
591ee332 FD |
112 | |
113 | def main(): | |
114 | """ | |
6a871bbe KS |
115 | Reads a line from stdin and expect it to be a wellformed Babeltrace CLI |
116 | output containing containing a callstack context of the domain passed | |
117 | as argument. | |
591ee332 FD |
118 | """ |
119 | expected_callstack = set() | |
120 | recorded_callstack = set() | |
6a871bbe | 121 | cs_type = None |
591ee332 FD |
122 | |
123 | if len(sys.argv) <= 2: | |
124 | print(sys.argv) | |
6a871bbe KS |
125 | raise ValueError( |
126 | "USAGE: ./{} (--kernel|--user EXE) FUNC-NAMES".format(sys.argv[0]) | |
127 | ) | |
591ee332 FD |
128 | |
129 | # If the `--user` option is passed, save the next argument as the path | |
130 | # to the executable | |
6a871bbe KS |
131 | argc = 1 |
132 | executable = None | |
133 | if sys.argv[argc] in "--kernel": | |
591ee332 | 134 | rexp = kernel_cs_rexp |
6a871bbe KS |
135 | cs_type = "kernel" |
136 | elif sys.argv[argc] in "--user": | |
591ee332 | 137 | rexp = user_cs_rexp |
6a871bbe KS |
138 | cs_type = "user" |
139 | argc += 1 | |
591ee332 FD |
140 | executable = sys.argv[argc] |
141 | else: | |
6a871bbe | 142 | raise Exception("Unknown domain") |
591ee332 | 143 | |
6a871bbe | 144 | argc += 1 |
591ee332 FD |
145 | |
146 | # Extract the function names that are expected to be found call stack of | |
147 | # the current events | |
148 | for func in sys.argv[argc:]: | |
149 | expected_callstack.add(func) | |
150 | ||
151 | # Read the tested line for STDIN | |
152 | event_line = None | |
153 | for line in sys.stdin: | |
154 | event_line = line | |
155 | break | |
156 | ||
157 | # Extract the userspace callstack context of the event | |
158 | m = re.match(rexp, event_line) | |
159 | ||
160 | # If there is no match, exit with error | |
161 | if m is None: | |
6a871bbe | 162 | raise re.error("Callstack not found in event line") |
591ee332 FD |
163 | else: |
164 | raw_callstack = str(m.group(1)) | |
6a871bbe KS |
165 | if cs_type in "user": |
166 | recorded_callstack = extract_user_func_names(executable, raw_callstack) | |
167 | elif cs_type in "kernel": | |
168 | recorded_callstack = extract_kernel_func_names(raw_callstack) | |
591ee332 | 169 | else: |
6a871bbe | 170 | raise Exception("Unknown domain") |
591ee332 FD |
171 | |
172 | # Verify that all expected function are present in the callstack | |
173 | for e in expected_callstack: | |
174 | if e not in recorded_callstack: | |
6a871bbe | 175 | raise Exception("Expected function name not found in recorded callstack") |
591ee332 FD |
176 | |
177 | sys.exit(0) | |
178 | ||
6a871bbe KS |
179 | |
180 | if __name__ == "__main__": | |
591ee332 | 181 | main() |