/* coremerge.c -- merge AMDGPU corefiles. Copyright (C) 2022-2024 Free Software Foundation, Inc. Copyright (C) 2022-2024 Advanced Micro Devices, Inc. All rights reserved. This file is part of GNU Binutils. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA. */ /* coremerge overview. coremerge is used to merge a AMDGPU core file (generated by the ROCm runtime) and the CPU core file generated for the same process and merge them into an unified core file GDB can load. */ #include "sysdep.h" #include "libiberty.h" #include "getopt.h" #include "bfd.h" #include "elf-bfd.h" #include "bucomm.h" #include #include #include #include #include "elf/external.h" #include #include #define verbose_printf(fmt, ...) \ do { \ if (verbose) \ printf (fmt, ##__VA_ARGS__); \ } while (0) \ #define is_power_of_two(x) ((x) && ((x) & ((x) - 1)) == 0) /* When generating sparse cores, we skip writing blocks comprised of all zeros if the block is of size of SPARSE_BLOCK_SIZE and aligned on a SPARSE_BLOCK_SIZE boundary in the underlying output file. */ #define SPARSE_BLOCK_SIZE 0x1000 /* Largest amount of data (in bytes) we copy from an input to the produced combined core file. */ #define COPY_SIZE (256 * SPARSE_BLOCK_SIZE) /* Set by command line flags. */ static int show_version = 0; static int verbose = 0; static int force = 0; /* Print help message to STREAM and exit the process with exit code STATUS. */ static void usage (FILE *, int) ATTRIBUTE_NORETURN; static void usage (FILE *stream, int status) { fprintf (stream, _("Usage: %s combined-corefile " "host-corefile gpu-corefile\n"), program_name); fprintf (stream, _(" Merge the two core dump files host-corefile and gpu-corefile " "and produce combined-corefile.\n")); fprintf (stream, _("\ -f, --force Overwrite the output file if it already exists\n")); fprintf (stream, _("\ @ Read options from \n")); fprintf (stream, _("\ -h, --help Display this information\n")); fprintf (stream, _("\ -v, --version Display this program's version number\n")); fprintf (stream, _("\ -V, --verbose Display detailed operation\n")); if (REPORT_BUGS_TO[0] && status == 0) fprintf (stream, _("Report bugs to %s.\n"), REPORT_BUGS_TO); exit (status); } static struct option long_options[]= { {"force", no_argument, &force, 1}, {"verbose", no_argument, &verbose, 1}, {"version", no_argument, &show_version, 1}, {"help", no_argument, NULL, 'h'}, {NULL, no_argument, NULL, 0} }; /* Read notes from the segment referenced by PHDR in IBFD and append their content to NOTES_DATA which will be the content of the note segment added to OBFD. NOTES_DATA_SIZE gives the size of the buffer pointed by NOTES_DATA. Return 0 on success, non-0 if an error occurred. This is based on bfd/elf.c:elf_parse_notes. */ static int gather_notes (bfd *obfd, bfd *ibfd, Elf_Internal_Phdr *phdr, char **notes_data, int *notes_data_size) { void *note_data, *note_end, *curr; Elf_External_Note *nh; Elf_Internal_Note in; int align = phdr->p_align; assert (notes_data != NULL); assert (notes_data_size != NULL); assert (phdr != NULL && phdr->p_type == PT_NOTE); if (align < 4) align = 4; if (align != 4 && align != 8) return -1; note_data = xmalloc (phdr->p_filesz); if (bfd_seek (ibfd, phdr->p_offset, SEEK_SET) != 0) { free (note_data); return -1; } if (bfd_read (note_data, phdr->p_filesz, ibfd) != phdr->p_filesz) { free (note_data); return -1; } note_end = ((char *) note_data) + phdr->p_filesz; curr = note_data; while (curr < note_end) { nh = (Elf_External_Note *) curr; in.type = H_GET_32 (ibfd, nh->type); in.namesz = H_GET_32 (ibfd, nh->namesz); in.namedata = nh->name; if (in.namedata + in.namesz > (char *) note_end) { non_fatal (_("Error decoding notes from %s."), bfd_get_filename (ibfd)); free (note_data); return -1; } in.descsz = H_GET_32 (ibfd, nh->descsz); in.descdata = ((char *) curr) + ELF_NOTE_DESC_OFFSET (in.namesz, align); if (in.descsz != 0 && (in.descdata > (char *) note_end || in.descdata + in.descsz > (char *) note_end)) { non_fatal (_("Error decoding notes from %s."), bfd_get_filename (ibfd)); free (note_data); return -1; } verbose_printf (" Found note %s[type=0x%lx]\n", in.namedata, in.type); *notes_data = elfcore_write_note (obfd, *notes_data, notes_data_size, in.namedata, in.type, in.descdata, in.descsz); curr = (((char *) curr) + ELF_NOTE_NEXT_OFFSET (in.namesz, in.descsz, align)); } free (note_data); return 0; } /* Describe load sections which should be created in the output BFD and keep track of where the actual data should be loaded from. */ struct out_section { /* The BFD the data comes from. */ bfd *ibfd; /* The BFD the data will be copied to. */ bfd *obfd; /* The Phdr describing where the data lives in IBFD. */ Elf_Internal_Phdr *iphdr; /* The section in OBFD where the data will be copied. Can be NULL if no data needs to be copied to the OBFD (content-less section). */ asection *osection; /* Next section to be processed. */ struct out_section *next; }; /* Check if the address ADDR is contained in the segment PHDR. Helper function for gather_load_segments. */ static inline int phdr_contains (Elf_Internal_Phdr *phdr, bfd_vma addr) { return phdr->p_vaddr <= addr && addr < phdr->p_vaddr + phdr->p_memsz; } /* Record the load segment described by PHDR in the list of segments to create in the output core file. It happens that the same memory region (i.e. segment with same load address and memory size) exists in both the host and gpu core. This is because the kernel creates empty (i.e. with a size of 0 on disk, so 0-initialized when loaded by the debugger) segments for the regions where the GPU memory is mapped onto the host process. For such segments, make sure that we only keep the segment which has actual data. Return 0 on success, non-0 otherwise. */ static int gather_load_segments (bfd *ibfd, Elf_Internal_Phdr *phdr, struct out_section **descr) { struct out_section *osec_dsc; assert (phdr != NULL && phdr->p_type == PT_LOAD); assert (descr != NULL); /* Check if we already have an entry for the same load address but no actual data. If so, replace it with the current segment. */ for (struct out_section *head = *descr; head != NULL; head = head->next) { if (head->iphdr->p_vaddr == phdr->p_vaddr && head->iphdr->p_filesz == 0 && head->iphdr->p_memsz == phdr->p_memsz) { verbose_printf (" Overriding segment loaded at %#lx with one from " "%s\n", head->iphdr->p_vaddr, bfd_get_filename (ibfd)); head->ibfd = ibfd; head->iphdr = phdr; return 0; } if (phdr_contains (head->iphdr, phdr->p_vaddr) || phdr_contains (head->iphdr, phdr->p_vaddr + phdr->p_memsz - 1) || phdr_contains (phdr, head->iphdr->p_vaddr) || phdr_contains (phdr, head->iphdr->p_vaddr + head->iphdr->p_memsz - 1)) non_fatal (_("[warning] Found overapping segments.")); } /* Record the current section to be loaded. */ verbose_printf (" Using segment loaded at %#lx from %s\n", phdr->p_vaddr, bfd_get_filename (ibfd)); osec_dsc = XCNEW (struct out_section); osec_dsc->ibfd = ibfd; osec_dsc->iphdr = phdr; /* Link the new descriptor in the list. */ osec_dsc->next = *descr; *descr = osec_dsc; return 0; } /* Returns 1 if all SIZE first bytes from the buffer referenced by BUF are all NUL bytes, 0 otherwise. SIZE must be at most SPARSE_BLOCK_SIZE. */ static int is_all_zeros (const bfd_byte *buf, size_t size) { assert (size <= SPARSE_BLOCK_SIZE); static const bfd_byte zeros[SPARSE_BLOCK_SIZE] = {0}; return memcmp (buf, zeros, size) == 0; } /* Wrapper around bfd_set_section_contents that skips writing blocks of SPARSE_BLOCK_SIZE size if such block only contains 0s. The caller ensures that blocks should be written on a SPARSE_BLOCK_SIZE boundary on the underlying file for this to effectively produce sparse core dumps. */ static int sparse_bfd_set_section_contents (bfd *abfd, asection *section, const void *data, file_ptr offset, bfd_size_type count) { for (size_t i = 0; i < count; i += SPARSE_BLOCK_SIZE) { size_t block_size = count - i; if (block_size > SPARSE_BLOCK_SIZE) block_size = SPARSE_BLOCK_SIZE; /* Ignore writing blocks of all-0s. */ if (!is_all_zeros (data + i, block_size)) { if (!bfd_set_section_contents (abfd, section, data + i, offset + i, block_size)) return 0; } } return 1; } /* Helper function which writes a single 0 at the last byte of the last non-empty section in the file. Since that section is at the very end of the output core dump, this ensures that the overall file size is valid, even if we skip writing blocks of 0s to obtain a sparse output. This last byte is expected to be overridden later if it should be non-0. If something wrong happens before the end of the copy, we do expect to have 0-initialized memory in the missing sections anyway, so this byte is valid. */ static void ensure_last_section_size (struct out_section *descr) { struct out_section *last_section = NULL; /* Output must have began so we are sure BFD has laid out all sections in the output file. */ assert (descr->obfd->output_has_begun); for (; descr != NULL; descr = descr->next) { if (descr->osection != NULL) { assert (descr->osection->size > 0); if (last_section == NULL || descr->osection->filepos > last_section->osection->filepos) last_section = descr; } } if (last_section != NULL) { assert (last_section->osection != NULL && last_section->osection->size > 0); const char byte = 0; bfd_set_section_contents (last_section->obfd, last_section->osection, &byte, last_section->osection->size - 1, 1); } } /* Copy data from source file to destination file according to DESCR. Return 0 on success, non-0 otherwise. */ static int do_copy_load_segments (struct out_section *descr) { char *buf = xmalloc (COPY_SIZE); ensure_last_section_size (descr); for (; descr != NULL; descr = descr->next) { /* No data to copy for this section. */ if (descr->osection == NULL) continue; size_t to_copy = descr->iphdr->p_filesz; verbose_printf (_(" Copying segment from %s offset=0x%lx," " size=0x%lx, vma=0x%lx\n"), bfd_get_filename (descr->ibfd), descr->iphdr->p_offset, descr->iphdr->p_filesz, descr->iphdr->p_vaddr); /* The first chunk should get us up to the next SPARSE_BLOCK_SIZE alignment on the underlying file. */ int sparse_align = 1; /* Copy data from the input file. */ while (to_copy > 0) { size_t to_read = to_copy < COPY_SIZE ? to_copy : COPY_SIZE; if (sparse_align) { size_t first_chunk_size = (SPARSE_BLOCK_SIZE - (descr->osection->filepos % SPARSE_BLOCK_SIZE)); if (first_chunk_size != SPARSE_BLOCK_SIZE) to_read = first_chunk_size; sparse_align = 0; } if (bfd_seek (descr->ibfd, descr->iphdr->p_offset + (descr->iphdr->p_filesz - to_copy), SEEK_SET) != 0 || bfd_read (buf, to_read, descr->ibfd) != to_read) { non_fatal (_("warning: Failed to read 0x%lx bytes from %s at " "offset 0x%lx: %s"), to_read, bfd_get_filename (descr->ibfd), (unsigned long) (descr->iphdr->p_offset + (descr->iphdr->p_filesz - to_copy)), bfd_errmsg (bfd_get_error ())); break; } if (!sparse_bfd_set_section_contents (descr->obfd, descr->osection, buf, descr->iphdr->p_filesz - to_copy, to_read)) { non_fatal (_("Failed to write 0x%zx bytes in %s at offset: %s"), to_read, bfd_get_filename (descr->ibfd), bfd_errmsg (bfd_get_error ())); free (buf); return -1; } to_copy -= to_read; } } free (buf); return 0; } /* Analyze input file IBFD. DESCR is populated with the required data which will be used later to copy data from IBFD to OBFD. NOTES_DATA and NOTES_DATA_SIZE gather note contents found while analysing IBFD. This procedure uses ELF backend data to analyse IBFD. This is so we list segments as seen by the loader, not sections as shown by BFD. BFD would otherwise create pseudo sections for each note it knows about, which we are not interested in as notes are processed by gather_notes. */ static int analyze_ibfd (bfd *obfd, bfd *ibfd, struct out_section **descr, int *notes_data_size, char **notes_data) { int ret; struct elf_obj_tdata *tdata;; size_t phindex; assert (descr != NULL); assert (notes_data_size != NULL); assert (notes_data != NULL); verbose_printf (_(" Analysing %s\n"), bfd_get_filename (ibfd)); tdata = ibfd->tdata.elf_obj_data; for (phindex = 0; phindex < tdata->elf_header->e_phnum; ++phindex) { if (tdata->phdr[phindex].p_type == PT_LOAD) { if ((ret = gather_load_segments (ibfd, &tdata->phdr[phindex], descr)) != 0) return ret; } else if (tdata->phdr[phindex].p_type == PT_NOTE) { if ((ret = gather_notes (obfd, ibfd, &tdata->phdr[phindex], notes_data, notes_data_size)) != 0) return ret; } else verbose_printf (_(" Ignoring segment at %lx of type %ld\n"), tdata->phdr[phindex].p_offset, tdata->phdr[phindex].p_type); } return 0; } /* Declare a load section of size SIZE to be loaded at address VMA in OBFD. Attributes are set according to FLAGS. The section alignment is specified by ALIGN. If OSECT is not NULL, it will be updated to point to the newly created asection. Return 0 on success, -1 on error. */ static int declare_load_sect (bfd *obfd, flagword flags, bfd_size_type size, bfd_vma vma, bfd_vma align, asection **osect) { asection *osection; if ((osection = bfd_make_section_anyway_with_flags (obfd, "load", flags)) == NULL || !bfd_set_section_size (osection, size) || !bfd_set_section_vma (osection, vma) || !bfd_set_section_lma (osection, 0)) { non_fatal (_("Failed to create output section: %s"), bfd_errmsg (bfd_get_error ())); return -1; } if (align >= 1) { /* Check that phdr->p_align is a power of 2. */ if (!is_power_of_two (align)) { non_fatal (_("Unsupported alignment %#lx."), align); return -1; } if (!bfd_set_section_alignment (osection, log2 (align))) { non_fatal (_("Failed to create output section.")); return -1; } } /* Init the program header for the newly created section. */ flags = PF_R; if ((bfd_section_flags (osection) & SEC_READONLY) == 0) flags |= PF_W; if ((bfd_section_flags (osection) & SEC_CODE) != 0) flags |= PF_X; if (!bfd_record_phdr (obfd, PT_LOAD, true, flags, false, 0, false, false, 1, &osection)) { non_fatal (_("Failed to create program header.")); return -1; } if (osect != NULL) *osect = osection; return 0; } static int prepare_obfd (bfd *obfd, struct out_section *descr, int notes_data_size, asection **note_sec) { verbose_printf (_("Creating output file structure.\n")); /* Create the NOTE segment. */ *note_sec = bfd_make_section_anyway_with_flags (obfd, "note", SEC_HAS_CONTENTS | SEC_READONLY | SEC_ALLOC); if (*note_sec == NULL) { non_fatal (_("Failed to create the output note section: %s"), bfd_errmsg (bfd_get_error ())); return -1; } bfd_set_section_vma (*note_sec, 0); bfd_set_section_alignment (*note_sec, 0); bfd_set_section_size (*note_sec, notes_data_size); if (!bfd_record_phdr (obfd, PT_NOTE, true, PF_R, false, 0, false, false, 1, note_sec)) { non_fatal (_("Failed to create program headers in %s."), bfd_get_filename (obfd)); return -1; } /* Create the LOAD sections. */ for (; descr != NULL; descr = descr->next) { descr->obfd = obfd; flagword flags = SEC_ALLOC; if ((descr->iphdr->p_flags & PF_W) == 0) flags |= SEC_READONLY; if ((descr->iphdr->p_flags & PF_X) != 0) flags |= SEC_CODE; else flags |= SEC_DATA; /* For the current segment, create a section with content (if p_filesz > 0), a section without content (if p_filesz < p_memsz), maybe both. Sections without content need no further processing, we only need to track sections for which we will need to copy data. */ if (descr->iphdr->p_filesz > 0) { if (declare_load_sect (obfd, flags | SEC_LOAD | SEC_HAS_CONTENTS, descr->iphdr->p_filesz, descr->iphdr->p_vaddr, descr->iphdr->p_align, &descr->osection) == -1) return -1; } if (descr->iphdr->p_filesz < descr->iphdr->p_memsz) { if (declare_load_sect (obfd, flags, (descr->iphdr->p_memsz - descr->iphdr->p_filesz), (descr->iphdr->p_vaddr + descr->iphdr->p_filesz), descr->iphdr->p_align, NULL) == -1) return -1; } } return 0; } /* Worker function merging contents of a host core dump HBFD and a gpu core dump GBFD into OBFD. This function will close OBFD, HBFD and GBFD before returning. This is because it needs to free some data which needs to be accessed when closing OBFD. Return 0 on success, non-0 otherwise. */ static int do_merge_cores (bfd *obfd, bfd *hbfd, bfd *gbfd) { int ret; char *notes_buf = NULL; int notes_buf_size = 0; asection *note_sec = NULL; struct out_section *descr = NULL; /* Parse both host core and GPU core. Create a section for each LOAD segment found and gather notes. */ if ((ret = analyze_ibfd (obfd, hbfd, &descr, ¬es_buf_size, ¬es_buf)) != 0 || (ret = analyze_ibfd (obfd, gbfd, &descr, ¬es_buf_size, ¬es_buf)) != 0) goto out; if (prepare_obfd (obfd, descr, notes_buf_size, ¬e_sec)) goto out; /* Start the output by writing the note section out. */ if (!bfd_set_section_contents (obfd, note_sec, notes_buf, 0, notes_buf_size)) { non_fatal (_("Failed to write notes in %s: %s"), bfd_get_filename (obfd), bfd_errmsg (bfd_get_error ())); ret = -1; goto out; } if ((ret = do_copy_load_segments (descr)) != 0) goto out; ret = 0; out: while (descr != NULL) { struct out_section *tmp = descr; descr = descr->next; free (tmp); } free (notes_buf); return ret; } /* Open PATH with bfd and ensure that it is an Elf64 based core file. Return NULL if the file cannot be opened, or if the format is not as expected. */ static bfd * open_core (const char *path) { bfd *ret; ret = bfd_openr (path, NULL); if (ret == NULL) { non_fatal (_("Failed to open %s: %s"), path, bfd_errmsg (bfd_get_error ())); return NULL; } if (!bfd_check_format (ret, bfd_core)) { non_fatal (_("%s is not a core file: %s"), bfd_get_filename (ret), bfd_errmsg (bfd_get_error ())); bfd_close (ret); return NULL; } /* This tool only handles ELF64 files. */ if (bfd_get_flavour (ret) != bfd_target_elf_flavour || get_elf_backend_data (ret)->s->elfclass != ELFCLASS64) { non_fatal (_("%s is not an Elf64 based core file."), bfd_get_filename (ret)); bfd_close (ret); return NULL; } return ret; } /* Merge GPU_CORE and HOST_CORE into OUT. */ static int merge_core (const char *out, const char *host_core, const char *gpu_core) { bfd *obfd; bfd *hbfd; bfd *gbfd; int ret; if (access (out, F_OK) == 0 && !force) { non_fatal (_("File %s already exist. Use '-f' to override."), out); return -1; } /* First open the host core file as we will copy fields from it. */ hbfd = open_core (host_core); if (hbfd == NULL) return -1; gbfd = open_core (gpu_core); if (gbfd == NULL) { bfd_close (hbfd); return -1; } verbose_printf (_("Creating %s\n"), out); if ((obfd = bfd_openw (out, bfd_get_target (hbfd))) == NULL || !bfd_set_format (obfd, bfd_core) || !bfd_set_arch_mach (obfd, bfd_get_arch (hbfd), bfd_get_mach (hbfd)) ) { non_fatal (_("Failed to create %s: %s"), out, bfd_errmsg (bfd_get_error ())); bfd_close (hbfd); bfd_close (gbfd); return -1; } ret = do_merge_cores (obfd, hbfd, gbfd); bfd_close (obfd); bfd_close (hbfd); bfd_close (gbfd); return ret; } int main (int argc, char **argv) { int c; int exit_status; #ifdef HAVE_LC_MESSAGES setlocale (LC_MESSAGES, ""); #endif setlocale (LC_CTYPE, ""); bindtextdomain (PACKAGE, LOCALEDIR); textdomain (PACKAGE); program_name = *argv; xmalloc_set_program_name (program_name); bfd_set_error_program_name (program_name); expandargv (&argc, &argv); if (bfd_init () != BFD_INIT_MAGIC) fatal (_("fatal error: libbfd ABI mismatch")); set_default_bfd_target (); while ((c = getopt_long (argc, argv, "vhVf", long_options, (int *) NULL)) != -1) { switch (c) { case 0: break; /* We've been given a long option. */ case 'v': show_version = true; break; case 'V': verbose = true; break; case 'h': usage (stdout, 0); break; case 'f': force = true; break; default: usage (stdout, 1); break; } } if (show_version) print_version (program_name); /* We must have 3 positional arguments provided: - the output file - 2 files to merge. */ if (optind + 3 > argc) { non_fatal (_("Missing positional arguments.\n")); usage (stderr, 2); } exit_status = merge_core (argv[optind], argv[optind + 1], argv[optind + 2]); return exit_status; }