Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

libdrgn: add support for remote debugging via OpenOCD #338

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions docs/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,43 @@ explicitly::
int counter;
} atomic_t

Remote Debugging
^^^^^^^^^^^^^^^^

drgn supports remote kernel debugging using `OpenOCD
<https://www.openocd.org/>`_ as an interface to a JTAG/SWD debug adapter
connected to a remote target. To use the remote debugging feature,
you must have the following information about your target.

* A suitable OpenOCD configuration file for your target.
* The name of the Test Access Point (TAP) which may be used to access the
physical memory where the kernel is running. The TAP name will
be specified in the OpenOCD configuration file.
* The load address of the kernel in the target's physical memory.

You must also have debugging symbols for the kernel running on your target
(i.e. the ``vmlinux`` file).

To begin the debugging session, ensure that your target is running the
kernel and that OpenOCD is running and connected to your target. Then
issue the following command, making the necessary substitutions::

$ drgn --openocd /path/to/vmlinux --openocd-tap $TAP_NAME --openocd-addr $LOAD_ADDR

Debug information for the specified kernel will be loaded automatically,
but there is currently no support for automatically loading debug
information for loaded kernel modules. Debug information for modules
must be loaded manually using :meth:`drgn.Program.load_debug_info()`.

drgn may also be used to debug microcontroller firmware and other
bare-metal MMU-less programs. To do this, you must have debugging
symbols for your firmware. To begin debugging, ensure that your target
is running the firmware and that OpenOCD is running and connected to
your target. Then issue the following command, making the necessary
substitutions::

$ drgn --openocd /path/to/firmware.elf --openocd-tap $TAP_NAME --openocd-nommu

Next Steps
----------

Expand Down
30 changes: 30 additions & 0 deletions drgn/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ def _main() -> None:
program_group.add_argument(
"-k", "--kernel", action="store_true", help="debug the running kernel (default)"
)
program_group.add_argument(
"--openocd", metavar="PATH", type=str, help="debug the given kernel on a remote target using OpenOCD"
)
program_group.add_argument(
"-c", "--core", metavar="PATH", type=str, help="debug the given core dump"
)
Expand Down Expand Up @@ -137,6 +140,23 @@ def _main() -> None:
help="don't load any debugging symbols that were not explicitly added with -s",
)

openocd_group = parser.add_argument_group(title="OpenOCD options")
openocd_group.add_argument(
"--openocd-host", metavar="HOST", type=str, default="localhost", help="hostname of OpenOCD server"
)
openocd_group.add_argument(
"--openocd-port", metavar="PORT", type=str, default="6666", help="TCL port number of OpenOCD server"
)
openocd_group.add_argument(
"--openocd-addr", metavar="ADDR", type=lambda x: int(x,0), help="debug kernel at given physical address"
)
openocd_group.add_argument(
"--openocd-nommu", action="store_true", help="debug a program that does not use the MMU"
)
openocd_group.add_argument(
"--openocd-tap", metavar="TAP", type=str, help="debug kernel using given TAP"
)

parser.add_argument(
"-q",
"--quiet",
Expand Down Expand Up @@ -182,6 +202,16 @@ def _main() -> None:
prog.set_core_dump(args.core)
elif args.pid is not None:
prog.set_pid(args.pid or os.getpid())
elif args.openocd:
if args.openocd_tap is None:
print("error: --openocd-tap required when using OpenOCD", file=sys.stderr)
sys.exit(1)
Comment on lines +207 to +208
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny suggestion, Python allows you to shorten this:

              sys.exit("error: --openocd-tap required when using OpenOCD")

Non-int args are printed to stderr and an error code of 1 is returned.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, I'll take care of that when rebasing.

if args.openocd_addr is None and args.openocd_nommu is None:
print("error: --openocd-addr or --openocd-nommu required when using OpenOCD", file=sys.stderr)
sys.exit(1)
prog.set_openocd(vmlinux=args.openocd, host=args.openocd_host,
port=args.openocd_port, tap=args.openocd_tap,
mmu=not args.openocd_nommu, paddr=args.openocd_addr or 0)
else:
prog.set_kernel()
except PermissionError as e:
Expand Down
142 changes: 140 additions & 2 deletions libdrgn/arch_aarch64.c
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,8 @@ static void linux_kernel_pgtable_iterator_init_aarch64(struct drgn_program *prog
{
struct pgtable_iterator_aarch64 *it =
container_of(_it, struct pgtable_iterator_aarch64, it);
if (it->it.pgtable == prog->vmcoreinfo.swapper_pg_dir) {
if (it->it.pgtable == prog->vmcoreinfo.swapper_pg_dir &&
it->it.pgtable_phys == prog->vmcoreinfo.swapper_pg_dir_phys) {
it->va_range_min = UINT64_MAX << prog->vmcoreinfo.va_bits;
it->va_range_max = UINT64_MAX;
} else {
Expand Down Expand Up @@ -408,7 +409,7 @@ linux_kernel_pgtable_iterator_next_aarch64(struct drgn_program *prog,
int level;
uint16_t num_entries = it->last_level_num_entries;
uint64_t table = it->it.pgtable;
bool table_physical = false;
bool table_physical = it->it.pgtable_phys;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't delved deep into the pgtable code, so I was not aware that table_physical was here for pretty much all of them and that it would be that easy to properly support a physical page table base.

I had a hack where I fooled drgn into using a physical swapper_pg_dir address. But your way is the proper way, I like it a lot!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Longer term I was thinking that we might want to try removing the pgtable_phys flag and have all page table iterator accesses go via physical memory. That should simplify the code a bit. This seems like it should also work with core dumps because the physical address can be calculated from the program headers in the core dump.


if (virt_addr < it->va_range_min ||
virt_addr > it->va_range_max) {
Expand Down Expand Up @@ -466,6 +467,141 @@ linux_kernel_pgtable_iterator_next_aarch64(struct drgn_program *prog,
}
}

static struct drgn_error *
linux_kernel_init_vmcoreinfo_from_phys_aarch64(struct drgn_program *prog,
Elf *vmlinux, GElf_Ehdr *ehdr,
uint64_t text_pa)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having text_pa makes sense for OpenOCD targets. But for other "bare metal" cases (e.g. linux core dumps without proper virtual address ranges provided), it may be that you can compute text_pa by some other data in the vmcoreinfo note. I don't know if there's one function signature that could cover all the necessary data that an arch-specific hook would need...

We may find ourselves needing to take a page out of crash's book, which allows you to use -m key=value to specify arch-specific key-value data to help it make sense of the target. This would be require more work, but it would give architectures the flexibility to handle all sorts of interesting cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if there's one function signature that could cover all the necessary data that an arch-specific hook would need...

To me it seems that there should be different APIs for "starting from a vmcoreinfo note" and "starting from a physical address". In each case the information discovery mechanism is very different. For example, initialization from PA doesn't even require the kernel to be configured with CONFIG_CRASH_CORE.

We may find ourselves needing to take a page out of crash's book, which allows you to use -m key=value to specify arch-specific key-value data to help it make sense of the target. This would be require more work, but it would give architectures the flexibility to handle all sorts of interesting cases.

Possibly. I suppose that another option would be to get any information we need added to the vmlinux file in some form, and then we can read it from there. The page size in the arm64 kernel header is one example. You might also have noticed changes in the code to tolerate missing information (i.e. changes to linux_kernel.c to tolerate missing nr_section_roots).

{
Comment on lines +470 to +474
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a clever way of getting the important information into Drgn without needing to have a more architectural shift to understand "bare-metal" targets explicitly. But the downside is that faking vmcoreinfo data is a time-honored tradition in Linux core dump debugging which has been the source of several bugs (e.g. makedumpfile and azure-linux-utils both do it, badly). That said, I don't have a specific issue I'm worried about in this case -- especially given that this hook is only used by the aarch64 OpenOCD backend (for now).

I guess the thing that would make me more comfortable is not tying this directly to the "vmcoreinfo" concept, which is Linux-specific. If instead we could have a way of representing that this is a bare metal target (i.e. one where we need to handle all virtual address translations ourselves, and need to bootstrap those translations with physical address information), then we can separate this from the linux kernel / vmcoreinfo specific code.

That way, in the future we could still have a target which is both bare-metal and Linux kernel, where we can find the vmcoreinfo via the equivalent of prog["vmcoreinfo_data"] so we can get at the rest of that data without clobbering the page table data which is set here.

That said, I don't know if it's worth blocking on these concerns or if we should try to update the architecture as it becomes an issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it would be worth trying to remove dependencies on Linux-isms such as vmcoreinfo as drgn starts to become useful for debugging bare-metal programs other than the Linux kernel. I personally don't think it needs to be done immediately though.

struct drgn_error *err = NULL;
Elf_Data *symtab = NULL;
Elf_Data *head_text = NULL;
size_t shstrndx;
uint64_t strtab;
uint64_t numsyms;

uint64_t swapper_pg_dir_rva = 0;
uint64_t text_rva = 0;
uint64_t image_flags;
uint64_t kimage_vaddr_rva = 0;
uint64_t kimage_vaddr;
uint64_t vabits_actual_rva = 0;
uint64_t vabits_actual;

bool bswap;
err = drgn_program_bswap(prog, &bswap);
if (err)
return err;

if (elf_getshdrstrndx(vmlinux, &shstrndx))
return drgn_error_libelf();

for (int i = 0; i != ehdr->e_shnum; ++i) {
Elf_Scn *section = elf_getscn(vmlinux, i);
GElf_Shdr shdr_mem, *shdr;

if (!section)
continue;

shdr = gelf_getshdr(section, &shdr_mem);
if (!shdr)
continue;

if (shdr->sh_type == SHT_SYMTAB) {
symtab = elf_getdata(section, NULL);
strtab = shdr->sh_link;
numsyms = shdr->sh_size / shdr->sh_entsize;
continue;
}

const char *name = elf_strptr(vmlinux, shstrndx, shdr->sh_name);
if (strcmp(name, ".head.text") == 0) {
head_text = elf_getdata(section, NULL);
}
}

if (!symtab || !head_text)
return drgn_error_format(DRGN_ERROR_MISSING_DEBUG_INFO,
"could not find required section");

for (int i = 1; i != numsyms; ++i) {
GElf_Sym sym_mem, *sym;
const char *name;

sym = gelf_getsym(symtab, i, &sym_mem);
if (!sym)
continue;

name = elf_strptr(vmlinux, strtab, sym->st_name);

if (strcmp(name, "swapper_pg_dir") == 0)
swapper_pg_dir_rva = sym->st_value;
if (strcmp(name, "_text") == 0)
text_rva = sym->st_value;
if (strcmp(name, "kimage_vaddr") == 0)
kimage_vaddr_rva = sym->st_value;
if (strcmp(name, "vabits_actual") == 0)
vabits_actual_rva = sym->st_value;
}

if (!swapper_pg_dir_rva || !text_rva || !kimage_vaddr_rva)
return drgn_error_format(DRGN_ERROR_MISSING_DEBUG_INFO,
"could not find required symbol");


/* Read the flags field, from which we infer the page size. */
image_flags = *(uint64_t *)(((char *)head_text->d_buf) + 24);
if (!HOST_LITTLE_ENDIAN)
image_flags = bswap_64(image_flags);
if ((image_flags & 6) == 0)
return drgn_error_format(DRGN_ERROR_MISSING_DEBUG_INFO,
"unknown page size");
prog->vmcoreinfo.page_shift = (image_flags & 6) + 10;
prog->vmcoreinfo.page_size = 1 << prog->vmcoreinfo.page_shift;

if (vabits_actual_rva) {
err = drgn_program_read_memory(
prog, &prog->vmcoreinfo.va_bits,
text_pa + (vabits_actual_rva - text_rva), 8, true);
if (err)
return err;
if (bswap)
prog->vmcoreinfo.va_bits = bswap_64(prog->vmcoreinfo.va_bits);
} else {
/*
* Assumes that kernel RVAs start in the middle of the kernel
* address space.
*/
for (int bit = 63; bit != 0; --bit) {
if (!(text_rva & (1ULL << bit))) {
prog->vmcoreinfo.va_bits = bit + 2;
break;
}
}
if (!prog->vmcoreinfo.va_bits) {
return drgn_error_format(DRGN_ERROR_MISSING_DEBUG_INFO,
"could not infer VA_BITS");
}
}
/*
* This is correct for newer kernels which set TCR_EL1.TBID1, but it
* doesn't hurt to mask the top byte in older kernels as well.
*/
prog->aarch64_insn_pac_mask = ~((1ULL << prog->vmcoreinfo.va_bits) - 1);

err = drgn_program_read_memory(prog, &kimage_vaddr,
text_pa + (kimage_vaddr_rva - text_rva),
8, true);
if (err)
return err;
if (bswap)
kimage_vaddr = bswap_64(kimage_vaddr);
prog->vmcoreinfo.kaslr_offset = kimage_vaddr - text_rva;
prog->vmcoreinfo.swapper_pg_dir =
swapper_pg_dir_rva + text_pa - text_rva;
prog->vmcoreinfo.swapper_pg_dir_phys = true;
return NULL;
}

static uint64_t untagged_addr_aarch64(uint64_t addr)
{
/* Apply TBI by sign extending bit 55 into bits 56-63. */
Expand Down Expand Up @@ -494,5 +630,7 @@ const struct drgn_architecture_info arch_info_aarch64 = {
linux_kernel_pgtable_iterator_init_aarch64,
.linux_kernel_pgtable_iterator_next =
linux_kernel_pgtable_iterator_next_aarch64,
.linux_kernel_init_vmcoreinfo_from_phys =
linux_kernel_init_vmcoreinfo_from_phys_aarch64,
.untagged_addr = untagged_addr_aarch64,
};
20 changes: 20 additions & 0 deletions libdrgn/drgn.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,26 @@ struct drgn_error *drgn_program_set_core_dump(struct drgn_program *prog,
*/
struct drgn_error *drgn_program_set_kernel(struct drgn_program *prog);

/**
* Set a @ref drgn_program to a running operating system kernel on a remote
* target accessible via OpenOCD.
*
* @param[in] vmlinux The path to the kernel vmlinux binary.
* @param[in] tap The name of the Test Access Port (TAP) providing access to
* the physical memory where the kernel is running.
* @param[in] mmu Whether this program uses the MMU. True for the Linux
* kernel, but may be set to false to debug an MMU-less program such as a
* bootloader or microcontroller firmware.
* @param[in] text_pa The kernel load address in physical memory. Only used
* if @p mmu is true.
* @return @c NULL on success, non-@c NULL on error.
*/
struct drgn_error *drgn_program_set_openocd(struct drgn_program *prog,
const char *vmlinux,
const char *host, const char *port,
const char *tap, bool mmu,
uint64_t text_pa);

/**
* Set a @ref drgn_program to a running process.
*
Expand Down
1 change: 1 addition & 0 deletions libdrgn/drgn_program_parse_vmcoreinfo.inc.strswitch
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ struct drgn_error *drgn_program_parse_vmcoreinfo(struct drgn_program *prog,
&prog->vmcoreinfo.swapper_pg_dir);
if (err)
return err;
prog->vmcoreinfo.swapper_pg_dir_phys = false;
break;
@case "LENGTH(mem_section)"@
err = parse_vmcoreinfo_u64(value, newline, 0,
Expand Down
10 changes: 6 additions & 4 deletions libdrgn/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,18 @@ struct drgn_error *linux_helper_direct_mapping_offset(struct drgn_program *prog,
uint64_t *ret);

struct drgn_error *linux_helper_read_vm(struct drgn_program *prog,
uint64_t pgtable, uint64_t virt_addr,
void *buf, size_t count);
uint64_t pgtable, bool pgtable_phys,
uint64_t virt_addr, void *buf,
size_t count);

struct drgn_error *linux_helper_read_cstr(struct drgn_program *prog,
uint64_t pgtable, uint64_t virt_addr,
uint64_t pgtable, bool pgtable_phys,
uint64_t virt_addr,
struct string_builder *str,
bool *done, size_t count);

struct drgn_error *linux_helper_follow_phys(struct drgn_program *prog,
uint64_t pgtable,
uint64_t pgtable, bool pgtable_phys,
uint64_t virt_addr, uint64_t *ret);

struct drgn_error *linux_helper_per_cpu_ptr(struct drgn_object *res,
Expand Down
4 changes: 3 additions & 1 deletion libdrgn/linux_kernel.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ static struct drgn_error *read_memory_via_pgtable(void *buf, uint64_t address,
{
struct drgn_program *prog = arg;
return linux_helper_read_vm(prog, prog->vmcoreinfo.swapper_pg_dir,
prog->vmcoreinfo.swapper_pg_dir_phys,
address, buf, count);
}

Expand All @@ -48,6 +49,7 @@ static struct drgn_error *read_cstr_via_pgtable(struct string_builder *str,
{
struct drgn_program *prog = arg;
return linux_helper_read_cstr(prog, prog->vmcoreinfo.swapper_pg_dir,
prog->vmcoreinfo.swapper_pg_dir_phys,
address, str, done, limit);
}

Expand Down Expand Up @@ -328,7 +330,7 @@ linux_kernel_get_vmemmap_address(struct drgn_program *prog, uint64_t *ret)
}

// Find a valid section.
for (uint64_t i = 0; i < nr_section_roots; i++) {
for (uint64_t i = 0; !nr_section_roots || i < nr_section_roots; i++) {
err = drgn_object_subscript(&root, &mem_section, i);
if (err)
goto out;
Expand Down
Loading