xref: /linux/scripts/gdb/linux/bpf.py (revision 509d3f45847627f4c5cdce004c3ec79262b5239c)
1# SPDX-License-Identifier: GPL-2.0
2
3import json
4import subprocess
5import tempfile
6
7import gdb
8
9from linux import constants, lists, radixtree, utils
10
11
12if constants.LX_CONFIG_BPF and constants.LX_CONFIG_BPF_JIT:
13    bpf_ksym_type = utils.CachedType("struct bpf_ksym")
14if constants.LX_CONFIG_BPF_SYSCALL:
15    bpf_prog_type = utils.CachedType("struct bpf_prog")
16
17
18def get_ksym_name(ksym):
19    name = ksym["name"].bytes
20    end = name.find(b"\x00")
21    if end != -1:
22        name = name[:end]
23    return name.decode()
24
25
26def list_ksyms():
27    if not (constants.LX_CONFIG_BPF and constants.LX_CONFIG_BPF_JIT):
28        return []
29    bpf_kallsyms = gdb.parse_and_eval("&bpf_kallsyms")
30    bpf_ksym_ptr_type = bpf_ksym_type.get_type().pointer()
31    return list(lists.list_for_each_entry(bpf_kallsyms,
32                                          bpf_ksym_ptr_type,
33                                          "lnode"))
34
35
36class KsymAddBreakpoint(gdb.Breakpoint):
37    def __init__(self, monitor):
38        super(KsymAddBreakpoint, self).__init__("bpf_ksym_add", internal=True)
39        self.silent = True
40        self.monitor = monitor
41
42    def stop(self):
43        self.monitor.add(gdb.parse_and_eval("ksym"))
44        return False
45
46
47class KsymRemoveBreakpoint(gdb.Breakpoint):
48    def __init__(self, monitor):
49        super(KsymRemoveBreakpoint, self).__init__("bpf_ksym_del",
50                                                   internal=True)
51        self.silent = True
52        self.monitor = monitor
53
54    def stop(self):
55        self.monitor.remove(gdb.parse_and_eval("ksym"))
56        return False
57
58
59class KsymMonitor:
60    def __init__(self, add, remove):
61        self.add = add
62        self.remove = remove
63
64        self.add_bp = KsymAddBreakpoint(self)
65        self.remove_bp = KsymRemoveBreakpoint(self)
66
67        self.notify_initial()
68
69    def notify_initial(self):
70        for ksym in list_ksyms():
71            self.add(ksym)
72
73    def delete(self):
74        self.add_bp.delete()
75        self.remove_bp.delete()
76
77
78def list_progs():
79    if not constants.LX_CONFIG_BPF_SYSCALL:
80        return []
81    idr_rt = gdb.parse_and_eval("&prog_idr.idr_rt")
82    bpf_prog_ptr_type = bpf_prog_type.get_type().pointer()
83    progs = []
84    for _, slot in radixtree.for_each_slot(idr_rt):
85        prog = slot.dereference().cast(bpf_prog_ptr_type)
86        progs.append(prog)
87        # Subprogs are not registered in prog_idr, fetch them manually.
88        # func[0] is the current prog.
89        aux = prog["aux"]
90        func = aux["func"]
91        real_func_cnt = int(aux["real_func_cnt"])
92        for i in range(1, real_func_cnt):
93            progs.append(func[i])
94    return progs
95
96
97class ProgAddBreakpoint(gdb.Breakpoint):
98    def __init__(self, monitor):
99        super(ProgAddBreakpoint, self).__init__("bpf_prog_kallsyms_add",
100                                                internal=True)
101        self.silent = True
102        self.monitor = monitor
103
104    def stop(self):
105        self.monitor.add(gdb.parse_and_eval("fp"))
106        return False
107
108
109class ProgRemoveBreakpoint(gdb.Breakpoint):
110    def __init__(self, monitor):
111        super(ProgRemoveBreakpoint, self).__init__("bpf_prog_free_id",
112                                                   internal=True)
113        self.silent = True
114        self.monitor = monitor
115
116    def stop(self):
117        self.monitor.remove(gdb.parse_and_eval("prog"))
118        return False
119
120
121class ProgMonitor:
122    def __init__(self, add, remove):
123        self.add = add
124        self.remove = remove
125
126        self.add_bp = ProgAddBreakpoint(self)
127        self.remove_bp = ProgRemoveBreakpoint(self)
128
129        self.notify_initial()
130
131    def notify_initial(self):
132        for prog in list_progs():
133            self.add(prog)
134
135    def delete(self):
136        self.add_bp.delete()
137        self.remove_bp.delete()
138
139
140def btf_str_by_offset(btf, offset):
141    while offset < btf["start_str_off"]:
142        btf = btf["base_btf"]
143
144    offset -= btf["start_str_off"]
145    if offset < btf["hdr"]["str_len"]:
146        return (btf["strings"] + offset).string()
147
148    return None
149
150
151def bpf_line_info_line_num(line_col):
152    return line_col >> 10
153
154
155def bpf_line_info_line_col(line_col):
156    return line_col & 0x3ff
157
158
159class LInfoIter:
160    def __init__(self, prog):
161        # See bpf_prog_get_file_line() for details.
162        self.pos = 0
163        self.nr_linfo = 0
164
165        if prog is None:
166            return
167
168        self.bpf_func = int(prog["bpf_func"])
169        aux = prog["aux"]
170        self.btf = aux["btf"]
171        linfo_idx = aux["linfo_idx"]
172        self.nr_linfo = int(aux["nr_linfo"]) - linfo_idx
173        if self.nr_linfo == 0:
174            return
175
176        linfo_ptr = aux["linfo"]
177        tpe = linfo_ptr.type.target().array(self.nr_linfo).pointer()
178        self.linfo = (linfo_ptr + linfo_idx).cast(tpe).dereference()
179        jited_linfo_ptr = aux["jited_linfo"]
180        tpe = jited_linfo_ptr.type.target().array(self.nr_linfo).pointer()
181        self.jited_linfo = (jited_linfo_ptr + linfo_idx).cast(tpe).dereference()
182
183        self.filenos = {}
184
185    def get_code_off(self):
186        if self.pos >= self.nr_linfo:
187            return -1
188        return self.jited_linfo[self.pos] - self.bpf_func
189
190    def advance(self):
191        self.pos += 1
192
193    def get_fileno(self):
194        file_name_off = int(self.linfo[self.pos]["file_name_off"])
195        fileno = self.filenos.get(file_name_off)
196        if fileno is not None:
197            return fileno, None
198        file_name = btf_str_by_offset(self.btf, file_name_off)
199        fileno = len(self.filenos) + 1
200        self.filenos[file_name_off] = fileno
201        return fileno, file_name
202
203    def get_line_col(self):
204        line_col = int(self.linfo[self.pos]["line_col"])
205        return bpf_line_info_line_num(line_col), \
206               bpf_line_info_line_col(line_col)
207
208
209def generate_debug_obj(ksym, prog):
210    name = get_ksym_name(ksym)
211    # Avoid read_memory(); it throws bogus gdb.MemoryError in some contexts.
212    start = ksym["start"]
213    code = start.cast(gdb.lookup_type("unsigned char")
214                      .array(int(ksym["end"]) - int(start))
215                      .pointer()).dereference().bytes
216    linfo_iter = LInfoIter(prog)
217
218    result = tempfile.NamedTemporaryFile(suffix=".o", mode="wb")
219    try:
220        with tempfile.NamedTemporaryFile(suffix=".s", mode="w") as src:
221            # ".loc" does not apply to ".byte"s, only to ".insn"s, but since
222            # this needs to work for all architectures, the latter are not an
223            # option. Ask the assembler to apply ".loc"s to labels as well,
224            # and generate dummy labels after each ".loc".
225            src.write(".loc_mark_labels 1\n")
226
227            src.write(".globl {}\n".format(name))
228            src.write(".type {},@function\n".format(name))
229            src.write("{}:\n".format(name))
230            for code_off, code_byte in enumerate(code):
231                if linfo_iter.get_code_off() == code_off:
232                    fileno, file_name = linfo_iter.get_fileno()
233                    if file_name is not None:
234                        src.write(".file {} {}\n".format(
235                            fileno, json.dumps(file_name)))
236                    line, col = linfo_iter.get_line_col()
237                    src.write(".loc {} {} {}\n".format(fileno, line, col))
238                    src.write("0:\n")
239                    linfo_iter.advance()
240                src.write(".byte {}\n".format(code_byte))
241            src.write(".size {},{}\n".format(name, len(code)))
242            src.flush()
243
244            try:
245                subprocess.check_call(["as", "-c", src.name, "-o", result.name])
246            except FileNotFoundError:
247                # "as" is not installed.
248                result.close()
249                return None
250        return result
251    except:
252        result.close()
253        raise
254