From be5f004df095eac17a004fa86aa67e50d5745783 Mon Sep 17 00:00:00 2001 From: zy Date: Sun, 26 Nov 2023 22:09:56 -0500 Subject: [PATCH] ucli could debug --- source/ucli/Makefile | 16 ++ source/ucli/accessors.cc | 485 +++++++++++++++++++++++++++++++++++++++ source/ucli/misc.cc | 40 ++++ source/ucli/symbol.cc | 104 ++++++++- source/ucli/ucli | Bin 0 -> 1257064 bytes source/ucli/ucli-lib.cc | 154 ++++++++++--- source/ucli/ucli.h | 100 +++++++- source/ucli/unwind.cc | 133 +++++++++++ source/ucli/unwind.h | 109 +++++++++ 9 files changed, 1101 insertions(+), 40 deletions(-) create mode 100644 source/ucli/Makefile create mode 100644 source/ucli/accessors.cc create mode 100644 source/ucli/misc.cc create mode 100755 source/ucli/ucli create mode 100644 source/ucli/unwind.cc create mode 100644 source/ucli/unwind.h diff --git a/source/ucli/Makefile b/source/ucli/Makefile new file mode 100644 index 0000000..fa87363 --- /dev/null +++ b/source/ucli/Makefile @@ -0,0 +1,16 @@ +TARGET_EXE=ucli +SOURCES=ucli.cc ucli-lib.cc unwind.cc symbol.cc accessors.cc elf.cc +OBJECTS=$(SOURCES:.cc=.o) + +CFLAGS=-g -O0 +INCLUDES=-I/usr/include/elf +LIBS=-lunwind-x86_64 -lunwind -lelf + +%.o: %.cc + $(CXX) $(CFLAGS) $(INCLUDES) -c $< -o $@ + +$(TARGET_EXE): $(OBJECTS) + $(CXX) $^ $(LIBS) -o $@ + +clean: + $(RM) $(TARGET_EXE) $(OBJECTS) \ No newline at end of file diff --git a/source/ucli/accessors.cc b/source/ucli/accessors.cc new file mode 100644 index 0000000..621ad95 --- /dev/null +++ b/source/ucli/accessors.cc @@ -0,0 +1,485 @@ +#include +#include // for GElf_Ehdr | Elf +#include +#include +#include +#include + +#include "unwind.h" + +extern "C" { +int UNW_OBJ(dwarf_search_unwind_table)(unw_addr_space_t as, unw_word_t ip, + unw_dyn_info_t *di, unw_proc_info_t *pi, + int need_unwind_info, void *arg); +} + +#define dwarf_search_unwind_table UNW_OBJ(dwarf_search_unwind_table) + +#define DW_EH_PE_FORMAT_MASK 0x0f /* format of the encoded value */ +#define DW_EH_PE_APPL_MASK 0x70 /* how the value is to be applied */ + +/* Pointer-encoding formats: */ +#define DW_EH_PE_omit 0xff +#define DW_EH_PE_ptr 0x00 /* pointer-sized unsigned value */ +#define DW_EH_PE_udata4 0x03 /* unsigned 32-bit value */ +#define DW_EH_PE_udata8 0x04 /* unsigned 64-bit value */ +#define DW_EH_PE_sdata4 0x0b /* signed 32-bit value */ +#define DW_EH_PE_sdata8 0x0c /* signed 64-bit value */ + +/* Pointer-encoding application: */ +#define DW_EH_PE_absptr 0x00 /* absolute value */ +#define DW_EH_PE_pcrel 0x10 /* rel. to addr. of encoded value */ + +static vma *find_map(unw_word_t ip, struct unwind_info *ui) { + return ui->sp->find_vma(ui->pid, ip); +} + +static Elf_Scn *elf_section_by_name(Elf *elf, GElf_Ehdr *ep, GElf_Shdr *shp, + const char *name) { + Elf_Scn *sec = NULL; + + while ((sec = elf_nextscn(elf, sec)) != NULL) { + char *str; + + gelf_getshdr(sec, shp); + str = elf_strptr(elf, ep->e_shstrndx, shp->sh_name); + if (!strcmp(name, str)) break; + } + + return sec; +} + +static u64 elf_section_offset(int fd, const char *name) { + Elf *elf; + GElf_Ehdr ehdr; + GElf_Shdr shdr; + u64 offset = 0; + + elf = elf_begin(fd, ELF_C_READ_MMAP, NULL); + if (elf == NULL) return 0; + + do { + if (gelf_getehdr(elf, &ehdr) == NULL) break; + + if (!elf_section_by_name(elf, &ehdr, &shdr, name)) break; + + offset = shdr.sh_offset; + } while (0); + + elf_end(elf); + return offset; +} + +struct eh_frame_hdr { + unsigned char version; + unsigned char eh_frame_ptr_enc; + unsigned char fde_count_enc; + unsigned char table_enc; + + /* + * The rest of the header is variable-length and consists of the + * following members: + * + * encoded_t eh_frame_ptr; + * encoded_t fde_count; + */ + + /* A single encoded pointer should not be more than 8 bytes. */ + u64 enc[2]; + + /* + * struct { + * encoded_t start_ip; + * encoded_t fde_addr; + * } binary_search_table[fde_count]; + */ + char data[0]; +} __attribute__((__packed__)); + +int dso_data_fd(vma *dso) { return open(dso->name.c_str(), O_RDONLY); } + +ssize_t dso_read(vma *dso, u64 offset, u8 *data, ssize_t size) { + ssize_t ret = -1; + int fd; + + fd = dso_data_fd(dso); + if (fd < 0) return -1; + + do { + if (-1 == lseek(fd, offset, SEEK_SET)) break; + + ret = read(fd, data, size); + if (ret <= 0) break; + } while (0); + + close(fd); + return ret; +} + +ssize_t dso__data_read_offset(vma *dso, u64 offset, u8 *data, ssize_t size) { + ssize_t r = 0; + u8 *p = data; + + do { + ssize_t ret; + ret = dso_read(dso, offset, p, size); + if (ret <= 0) { + return -1; + } + if (ret > size) { + return -1; + } + r += ret; + p += ret; + offset += ret; + size -= ret; + } while (size); + return r; +} + +#define dw_read(ptr, type, end) \ + ({ \ + type *__p = (type *)ptr; \ + type __v; \ + if ((__p + 1) > (type *)end) return -EINVAL; \ + __v = *__p++; \ + ptr = (typeof(ptr))__p; \ + __v; \ + }) + +static int __dw_read_encoded_value(u8 **p, u8 *end, u64 *val, u8 encoding) { + u8 *cur = *p; + *val = 0; + + switch (encoding) { + case DW_EH_PE_omit: + *val = 0; + goto out; + case DW_EH_PE_ptr: + *val = dw_read(cur, unsigned long, end); + goto out; + default: + break; + } + + switch (encoding & DW_EH_PE_APPL_MASK) { + case DW_EH_PE_absptr: + break; + case DW_EH_PE_pcrel: + *val = (unsigned long)cur; + break; + default: + return -EINVAL; + } + + if ((encoding & 0x07) == 0x00) encoding |= DW_EH_PE_udata4; + + switch (encoding & DW_EH_PE_FORMAT_MASK) { + case DW_EH_PE_sdata4: + *val += dw_read(cur, s32, end); + break; + case DW_EH_PE_udata4: + *val += dw_read(cur, u32, end); + break; + case DW_EH_PE_sdata8: + *val += dw_read(cur, s64, end); + break; + case DW_EH_PE_udata8: + *val += dw_read(cur, u64, end); + break; + default: + return -EINVAL; + } + +out: + *p = cur; + return 0; +} + +#define dw_read_encoded_value(ptr, end, enc) \ + ({ \ + u64 __v; \ + if (__dw_read_encoded_value(&ptr, end, &__v, enc)) { \ + return -EINVAL; \ + } \ + __v; \ + }) + +static int unwind_spec_ehframe(vma *dso, u64 offset, u64 *table_data, + u64 *segbase, u64 *fde_count) { + struct eh_frame_hdr hdr; + u8 *enc = (u8 *)&hdr.enc; + u8 *end = (u8 *)&hdr.data; + ssize_t r; + + r = dso__data_read_offset(dso, offset, (u8 *)&hdr, sizeof(hdr)); + if (r != sizeof(hdr)) { + return -EINVAL; + } + + /* We dont need eh_frame_ptr, just skip it. */ + dw_read_encoded_value(enc, end, hdr.eh_frame_ptr_enc); + + *fde_count = dw_read_encoded_value(enc, end, hdr.fde_count_enc); + *segbase = offset; + *table_data = (enc - (u8 *)&hdr) + offset; + + return 0; +} + +static int read_unwind_spec(vma *dso, u64 *table_data, u64 *segbase, + u64 *fde_count) { + int ret = -EINVAL, fd; + + if (dso->eh_frame_hdr_offset == 0 && dso->elf_read_error == 0) { + fd = dso_data_fd(dso); + if (fd < 0) return -EINVAL; + + dso->eh_frame_hdr_offset = elf_section_offset(fd, ".eh_frame_hdr"); + close(fd); + ret = unwind_spec_ehframe(dso, dso->eh_frame_hdr_offset, &dso->table_data, + &dso->eh_frame_hdr_offset, &dso->fde_count); + if (ret != 0) { + dso->eh_frame_hdr_offset = 0; + dso->elf_read_error = 1; + return -EINVAL; + } + } + + *table_data = dso->table_data; + *segbase = dso->eh_frame_hdr_offset; + *fde_count = dso->fde_count; + + /* TODO .debug_frame check if eh_frame_hdr fails */ + return 0; +} + +struct table_entry { + u32 start_ip_offset; + u32 fde_offset; +}; + +static int find_proc_info(unw_addr_space_t as, unw_word_t ip, + unw_proc_info_t *pi, int need_unwind_info, + void *arg) { + struct unwind_info *ui = (struct unwind_info *)arg; + unw_dyn_info_t di; + u64 table_data, segbase, fde_count; + + vma *map; + map = find_map(ip, ui); + if (!map) { + return -EINVAL; + } + + if (!read_unwind_spec(map, &table_data, &segbase, &fde_count)) { + memset(&di, 0, sizeof(di)); + di.format = UNW_INFO_FORMAT_REMOTE_TABLE; + di.start_ip = map->start; + di.end_ip = map->end; + di.u.rti.segbase = map->start + segbase; + di.u.rti.table_data = map->start + table_data; + di.u.rti.table_len = + fde_count * sizeof(struct table_entry) / sizeof(unw_word_t); + return dwarf_search_unwind_table(as, ip, &di, pi, need_unwind_info, arg); + } + // return -EINVAL; + return -UNW_ENOINFO; +} + +static void put_unwind_info(unw_addr_space_t as, unw_proc_info_t *pi, + void *arg) { + // pr_debug("unwind: put_unwind_info called\n"); +} + +static int get_dyn_info_list_addr(unw_addr_space_t as, unw_word_t *dil_addr, + void *arg) { + return -UNW_ENOINFO; +} + +ssize_t dso__data_read_addr(vma *map, u64 addr, u8 *data, ssize_t size) { + u64 offset; + + if (map->name.size() > 0 && map->name[0] != '/') return 0; + + offset = addr - map->start + map->offset; + return dso__data_read_offset(map, offset, data, size); +} + +struct map *last_map = NULL; +static int access_dso_mem(struct unwind_info *ui, unw_word_t addr, + unw_word_t *data) { + ssize_t size; + + // ip in the first page is invalid + if (addr == 0 || addr == (unsigned long)(-1) || (long)addr < 4096) { + return -UNW_ENOINFO; + } + + vma *map; + map = find_map(addr, ui); + if (!map) { + return -UNW_ENOINFO; + } + + if (map->type != NATIVE_TYPE) { + return -UNW_ENOINFO; + } + + size = dso__data_read_addr(map, addr, (u8 *)data, sizeof(*data)); + + return !(size == sizeof(*data)); +} + +/* + * Optimization point. + */ +static int reg_value(unw_word_t *valp, struct regs_dump *regs, int id) { + /* we only support 3 registers. RIP, RSP and RBP */ + if (id < 0 || id > 2) return -EINVAL; + + *valp = regs->regs[id]; + return 0; +} + +unw_word_t last_addr = 0; +unw_word_t last_val = 0; +int stack_offset = 0; + +static int access_mem(unw_addr_space_t as, unw_word_t addr, unw_word_t *valp, + int __write, void *arg) { + struct unwind_info *ui = (struct unwind_info *)arg; + struct stack_dump *stack = &ui->sample->user_stack; + unw_word_t start, end; + int offset; + int ret; + + if (addr == last_addr) { + (*valp) = last_val; + return 0; + } + + last_addr = addr; + + /* Don't support write, probably not needed. */ + if (__write || !stack || !ui->sample->user_regs.regs) { + *valp = 0; + // fprintf(stderr, "access_mem: __write memory\n"); + last_val = *valp; + return 0; + } + + /* start is the SP */ + ret = reg_value(&start, &ui->sample->user_regs, PERF_REG_SP); + if (ret) { + // fprintf(stderr, "access_mem: reg_value error (ret: %d)\n", ret); + return ret; + } + + end = start + stack->size; + + /* Check overflow. */ + if (addr + sizeof(unw_word_t) < addr) { + // fprintf(stderr, "access_mem: Check overflow.\n"); + return -EINVAL; + } + + if (addr < start || addr + sizeof(unw_word_t) >= end) { + ret = access_dso_mem(ui, addr, valp); + if (ret) { + // pr_debug("unwind: access_mem %p not inside range %p-%p\n", + // (void *)addr, (void *)start, (void *)end); + *valp = 0; + last_val = 0; + return ret; + } + last_val = *valp; + return 0; + } + + offset = addr - start; + *valp = *(unw_word_t *)&stack->data[offset]; + last_val = *valp; + stack_offset = offset; + + // pr_debug("unwind: access_mem addr %p, val %lx, offset %d\n", + // (void *)addr, (unsigned long)*valp, offset); + return 0; +} + +int unwind__arch_reg_id(int regnum) { + int id; + + switch (regnum) { + case UNW_X86_64_RBP: + id = PERF_REG_BP; + break; + case UNW_X86_64_RSP: + id = PERF_REG_SP; + break; + case UNW_X86_64_RIP: + id = PERF_REG_IP; + break; + default: + return -EINVAL; + } + + return id; +} + +static int access_reg(unw_addr_space_t as, unw_regnum_t regnum, + unw_word_t *valp, int __write, void *arg) { + struct unwind_info *ui = (struct unwind_info *)arg; + int id, ret; + + /* Don't support write, I suspect we don't need it. */ + if (__write) { + // pr_err("unwind: access_reg w %d\n", regnum); + return 0; + } + + if (!ui->sample->user_regs.regs) { + *valp = 0; + return 0; + } + + id = unwind__arch_reg_id(regnum); + if (id < 0) { + // fprintf(stderr, "Cannot get reg: %d\n", regnum); + return -EINVAL; + } + + ret = reg_value(valp, &ui->sample->user_regs, id); + if (ret) { + // pr_err("unwind: can't read reg %d\n", regnum); + return ret; + } + + // pr_debug("unwind: reg %d, val %lx\n", regnum, (unsigned long)*valp); + return 0; +} + +static int access_fpreg(unw_addr_space_t as, unw_regnum_t num, unw_fpreg_t *val, + int __write, void *arg) { + return -UNW_EINVAL; +} + +static int resume(unw_addr_space_t as, unw_cursor_t *cu, void *arg) { + return -UNW_EINVAL; +} + +static int get_proc_name(unw_addr_space_t as, unw_word_t addr, char *bufp, + size_t buf_len, unw_word_t *offp, void *arg) { + return -UNW_EINVAL; +} + +unw_accessors_t accessors = { + .find_proc_info = find_proc_info, + .put_unwind_info = put_unwind_info, + .get_dyn_info_list_addr = get_dyn_info_list_addr, + .access_mem = access_mem, + .access_reg = access_reg, + .access_fpreg = access_fpreg, + .resume = resume, + .get_proc_name = get_proc_name, +}; \ No newline at end of file diff --git a/source/ucli/misc.cc b/source/ucli/misc.cc new file mode 100644 index 0000000..1c31b9f --- /dev/null +++ b/source/ucli/misc.cc @@ -0,0 +1,40 @@ +#include +#include +#include + +class pid_cmdline pid_cmdline; + +class pid_cmdline { + private: + std::map cmdlines; + + public: + void clear(void); + std::string& get_pid_cmdline(int pid); +}; + +static string unknow_symbol("UNKNOWN"); + +void pid_cmdline::clear(void) { cmdlines.clear(); } + +std::string& pid_cmdline::get_pid_cmdline(int pid) { + if (cmdlines.count(pid) == 0) { + int i; + char buf[255]; + char file[255]; + std::fstream ifs; + + snprintf(file, sizeof(file), "/proc/%d/cmdline", pid); + ifs.open(file, ios::binary | ios::in); + ifs.getline(buf, 255); + for (i = 0; i < ifs.gcount() && i < 255; i++) { + if (buf[i] < ' ') { + buf[i] = ' '; + } + } + + cmdlines[pid] = buf; + } + + return cmdlines[pid]; +} \ No newline at end of file diff --git a/source/ucli/symbol.cc b/source/ucli/symbol.cc index 30e2256..c07cfe2 100644 --- a/source/ucli/symbol.cc +++ b/source/ucli/symbol.cc @@ -1,6 +1,11 @@ +#include "symbol.h" + #include #include +#include +#include + #include "elf.h" symbol_parser g_symbol_parser; @@ -213,16 +218,95 @@ bool symbol_parser::find_elf_symbol(symbol &sym, const elf_file &file, int pid, return true; } -bool symbol_parser::find_kernel_symbol(symbol &sym) -{ - load_kernel(); - sym.end = sym.start = 0; - std::set::iterator it = kernel_symbols.find(sym); - if (it != kernel_symbols.end()) { - sym.end = it->end; - sym.start = it->start; - sym.name = it->name; - return true; +static bool load_kernel_symbol_list(std::vector &sym_list) { + FILE *fp = fopen("/proc/kallsyms", "r"); + if (!fp) { + return -1; + } + + char buf[256]; + char type; + int len; + while (fgets(buf, sizeof(buf), fp) != NULL) { + sscanf(buf, "%*p %c %*s\n", &type); + if ((type | 0x20) != 't') { + continue; } + len = strlen(buf); + if (buf[len - 1] == '\n') { + buf[len - 1] = ' '; + } + sym_list.push_back(buf); + } + fclose(fp); + + std::sort(sym_list.begin(), sym_list.end()); + return true; +} + +bool is_space(int ch) { return std::isspace(ch); } + +static inline void rtrim(std::string &s) { + s.erase(std::find_if(s.rbegin(), s.rend(), is_space).base(), s.end()); +} + +static bool get_next_kernel_symbol(std::set &syms, + std::vector &sym_list, + std::vector::iterator cursor) { + if (cursor == sym_list.end()) { return false; + } + symbol sym; + size_t start, end; + sscanf(cursor->c_str(), "%p %*c %*s\n", (void **)&start); + sym.name = cursor->c_str() + 19; + rtrim(sym.name); +#if 0 + if (sym.name[sym.name.size()-1] == '\n') { + sym.name[sym.name.size()-1] = '\0'; + } +#endif + cursor++; + if (cursor != sym_list.end()) { + sscanf(cursor->c_str(), "%p %*c %*s\n", (void **)&end); + } else { + end = INVALID_ADDR; + } + sym.start = start; + sym.end = end; + sym.ip = start; + + syms.insert(sym); + return true; +} + +bool symbol_parser::load_kernel() { + if (kernel_symbols.size() != 0) { + return true; + } + + std::vector sym_list; + if (!load_kernel_symbol_list(sym_list)) { + exit(0); + return false; + } + + std::vector::iterator cursor = sym_list.begin(); + while (get_next_kernel_symbol(kernel_symbols, sym_list, cursor)) { + cursor++; + } + return true; +} + +bool symbol_parser::find_kernel_symbol(symbol &sym) { + load_kernel(); + sym.end = sym.start = 0; + std::set::iterator it = kernel_symbols.find(sym); + if (it != kernel_symbols.end()) { + sym.end = it->end; + sym.start = it->start; + sym.name = it->name; + return true; + } + return false; } \ No newline at end of file diff --git a/source/ucli/ucli b/source/ucli/ucli new file mode 100755 index 0000000000000000000000000000000000000000..03b867de89fba171123214975daf7875f2276db4 GIT binary patch literal 1257064 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