|
1 | 1 | # Copyright (c) Meta Platforms, Inc. and affiliates.
|
2 | 2 | # SPDX-License-Identifier: LGPL-2.1-or-later
|
3 | 3 |
|
| 4 | +import logging |
4 | 5 | import os
|
| 6 | +import re |
5 | 7 | import unittest
|
6 | 8 |
|
7 | 9 | from _drgn_util.platform import NORMALIZED_MACHINE_NAME
|
8 |
| -from drgn import Object, Program, reinterpret |
9 |
| -from tests import assertReprPrettyEqualsStr, modifyenv |
| 10 | +from drgn import MissingDebugInfoError, Object, Program, TypeMember, reinterpret |
| 11 | +from drgn.helpers.linux import load_module_kallsyms, load_vmlinux_kallsyms |
| 12 | +from tests import assertReprPrettyEqualsStr, drgn_log_level, modifyenv |
10 | 13 | from tests.linux_kernel import (
|
11 | 14 | LinuxKernelTestCase,
|
12 | 15 | fork_and_stop,
|
@@ -59,6 +62,60 @@ def test_by_pid_dwarf(self):
|
59 | 62 | def test_by_pid_orc(self):
|
60 | 63 | self._test_by_pid(True)
|
61 | 64 |
|
| 65 | + def _check_logged_orc_message(self, captured_logs, module): |
| 66 | + # To be sure that we actually used ORC to unwind through the drgn_test |
| 67 | + # stack frames, search for the log output. We don't know which ORC |
| 68 | + # version is used, so just ensure that we have a log line that mentions |
| 69 | + # loading ORC. |
| 70 | + expr = re.compile( |
| 71 | + r"DEBUG:drgn:Loaded built-in ORC \(v\d+\) for module " + module |
| 72 | + ) |
| 73 | + for line in captured_logs.output: |
| 74 | + if expr.fullmatch(line): |
| 75 | + break |
| 76 | + else: |
| 77 | + self.fail(f"Did not load built-in ORC for {module}") |
| 78 | + |
| 79 | + @unittest.skipUnless( |
| 80 | + NORMALIZED_MACHINE_NAME == "x86_64", |
| 81 | + f"{NORMALIZED_MACHINE_NAME} does not use ORC", |
| 82 | + ) |
| 83 | + @skip_unless_have_test_kmod |
| 84 | + def test_by_pid_builtin_orc(self): |
| 85 | + # ORC was introduced in kernel 4.14. Detect the presence of ORC or skip |
| 86 | + # the test. |
| 87 | + try: |
| 88 | + self.prog.symbol("__start_orc_unwind") |
| 89 | + except LookupError: |
| 90 | + ver = self.prog["UTS_RELEASE"].string_().decode() |
| 91 | + self.skipTest(f"ORC is not available for {ver}") |
| 92 | + |
| 93 | + with drgn_log_level(logging.DEBUG): |
| 94 | + # Create a program with the core kernel debuginfo loaded, |
| 95 | + # but without module debuginfo. Load a symbol finder using |
| 96 | + # kallsyms so that the module's stack traces can still have |
| 97 | + # usable frame names. |
| 98 | + prog = Program() |
| 99 | + prog.set_kernel() |
| 100 | + try: |
| 101 | + prog.load_default_debug_info() |
| 102 | + except MissingDebugInfoError: |
| 103 | + pass |
| 104 | + kallsyms = load_module_kallsyms(prog) |
| 105 | + prog.register_symbol_finder("module_kallsyms", kallsyms, enable_index=1) |
| 106 | + for thread in prog.threads(): |
| 107 | + if b"drgn_test_kthread".startswith(thread.object.comm.string_()): |
| 108 | + pid = thread.tid |
| 109 | + break |
| 110 | + else: |
| 111 | + self.fail("couldn't find drgn_test_kthread") |
| 112 | + # We must set drgn's log level manually, beacuse it won't log messages |
| 113 | + # to the logger if it isn't enabled for them. |
| 114 | + with self.assertLogs("drgn", logging.DEBUG) as log: |
| 115 | + self._test_drgn_test_kthread_trace(prog.stack_trace(pid)) |
| 116 | + |
| 117 | + self._check_logged_orc_message(log, "drgn_test") |
| 118 | + |
62 | 119 | @skip_unless_have_test_kmod
|
63 | 120 | def test_by_pt_regs(self):
|
64 | 121 | pt_regs = self.prog["drgn_test_kthread_pt_regs"]
|
@@ -104,6 +161,84 @@ def test_locals(self):
|
104 | 161 | else:
|
105 | 162 | self.fail("Couldn't find drgn_test_kthread_fn3 frame")
|
106 | 163 |
|
| 164 | + @unittest.skipUnless( |
| 165 | + NORMALIZED_MACHINE_NAME == "x86_64", |
| 166 | + f"{NORMALIZED_MACHINE_NAME} does not use ORC", |
| 167 | + ) |
| 168 | + def test_vmlinux_builtin_orc(self): |
| 169 | + # ORC was introduced in kernel 4.14. Detect the presence of ORC or skip |
| 170 | + # the test. |
| 171 | + try: |
| 172 | + self.prog.symbol("__start_orc_unwind") |
| 173 | + except LookupError: |
| 174 | + ver = self.prog["UTS_RELEASE"].string_().decode() |
| 175 | + self.skipTest(f"ORC is not available for {ver}") |
| 176 | + |
| 177 | + with drgn_log_level(logging.DEBUG): |
| 178 | + # It is difficult to test stack unwinding in a program without also |
| 179 | + # loading types, which necessarily will also make DWARF CFI and ORC |
| 180 | + # available in the debug file. The way we get around this is by creating |
| 181 | + # a new program with no debuginfo, getting a pt_regs from the program |
| 182 | + # that has debuginfo, and then using that to unwind the kernel. We still |
| 183 | + # need a symbol finder, and we'll need the Module API to recognize the |
| 184 | + # kernel address range correctly. |
| 185 | + prog = Program() |
| 186 | + prog.set_kernel() |
| 187 | + prog.register_symbol_finder( |
| 188 | + "vmlinux_kallsyms", load_vmlinux_kallsyms(prog), enable_index=0 |
| 189 | + ) |
| 190 | + main, _ = prog.main_module(name="kernel", create=True) |
| 191 | + main.address_range = self.prog.main_module().address_range |
| 192 | + |
| 193 | + # Luckily, all drgn cares about for x86_64 pt_regs is that it is a |
| 194 | + # structure. Rather than creating a matching struct pt_regs definition, |
| 195 | + # we can just create a dummy one of the correct size: |
| 196 | + # struct pt_regs { unsigned char[size]; }; |
| 197 | + # Drgn will happily use that and reinterpret the bytes correctly. |
| 198 | + real_pt_regs_type = self.prog.type("struct pt_regs") |
| 199 | + fake_pt_regs_type = prog.struct_type( |
| 200 | + tag="pt_regs", |
| 201 | + size=real_pt_regs_type.size, |
| 202 | + members=[ |
| 203 | + TypeMember( |
| 204 | + prog.array_type( |
| 205 | + prog.int_type("unsigned char", 1, False), |
| 206 | + real_pt_regs_type.size, |
| 207 | + ), |
| 208 | + "data", |
| 209 | + ), |
| 210 | + ], |
| 211 | + ) |
| 212 | + |
| 213 | + with fork_and_stop() as pid: |
| 214 | + trace = self.prog.stack_trace(pid) |
| 215 | + regs_dict = trace[0].registers() |
| 216 | + pt_regs_obj = Object( |
| 217 | + self.prog, |
| 218 | + real_pt_regs_type, |
| 219 | + { |
| 220 | + "bp": regs_dict["rbp"], |
| 221 | + "sp": regs_dict["rsp"], |
| 222 | + "ip": regs_dict["rip"], |
| 223 | + "r15": regs_dict["r15"], |
| 224 | + }, |
| 225 | + ) |
| 226 | + fake_pt_regs_obj = Object.from_bytes_( |
| 227 | + prog, fake_pt_regs_type, pt_regs_obj.to_bytes_() |
| 228 | + ) |
| 229 | + # We must set drgn's log level manually, beacuse it won't log messages |
| 230 | + # to the logger if it isn't enabled for them. |
| 231 | + with self.assertLogs("drgn", logging.DEBUG) as log: |
| 232 | + no_debuginfo_trace = prog.stack_trace(fake_pt_regs_obj) |
| 233 | + |
| 234 | + dwarf_pcs = [] |
| 235 | + for frame in trace: |
| 236 | + if not dwarf_pcs or dwarf_pcs[-1] != frame.pc: |
| 237 | + dwarf_pcs.append(frame.pc) |
| 238 | + orc_pcs = [frame.pc for frame in no_debuginfo_trace] |
| 239 | + self.assertEqual(dwarf_pcs, orc_pcs) |
| 240 | + self._check_logged_orc_message(log, "kernel") |
| 241 | + |
107 | 242 | def test_registers(self):
|
108 | 243 | # Smoke test that we get at least one register and that
|
109 | 244 | # StackFrame.registers() agrees with StackFrame.register().
|
|
0 commit comments