/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ // Copyright (c) 2020 Wenbo Zhang // // Based on ksyms improvements from Andrii Nakryiko, add more helpers. // 28-Feb-2020 Wenbo Zhang Created this. #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "trace_helpers.h" #include "uprobe_helpers.h" #define min(x, y) ({ \ typeof(x) _min1 = (x); \ typeof(y) _min2 = (y); \ (void) (&_min1 == &_min2); \ _min1 < _min2 ? _min1 : _min2; }) #define DISK_NAME_LEN 32 #define MINORBITS 20 #define MINORMASK ((1U << MINORBITS) - 1) #define MKDEV(ma, mi) (((ma) << MINORBITS) | (mi)) struct ksyms { struct ksym *syms; int syms_sz; int syms_cap; char *strs; int strs_sz; int strs_cap; }; static int ksyms__add_symbol(struct ksyms *ksyms, const char *name, unsigned long addr) { size_t new_cap, name_len = strlen(name) + 1; struct ksym *ksym; void *tmp; if (ksyms->strs_sz + name_len > ksyms->strs_cap) { new_cap = ksyms->strs_cap * 4 / 3; if (new_cap < ksyms->strs_sz + name_len) new_cap = ksyms->strs_sz + name_len; if (new_cap < 1024) new_cap = 1024; tmp = realloc(ksyms->strs, new_cap); if (!tmp) return -1; ksyms->strs = tmp; ksyms->strs_cap = new_cap; } if (ksyms->syms_sz + 1 > ksyms->syms_cap) { new_cap = ksyms->syms_cap * 4 / 3; if (new_cap < 1024) new_cap = 1024; tmp = realloc(ksyms->syms, sizeof(*ksyms->syms) * new_cap); if (!tmp) return -1; ksyms->syms = tmp; ksyms->syms_cap = new_cap; } ksym = &ksyms->syms[ksyms->syms_sz]; /* while constructing, re-use pointer as just a plain offset */ ksym->name = (void *)(unsigned long)ksyms->strs_sz; ksym->addr = addr; memcpy(ksyms->strs + ksyms->strs_sz, name, name_len); ksyms->strs_sz += name_len; ksyms->syms_sz++; return 0; } static int ksym_cmp(const void *p1, const void *p2) { const struct ksym *s1 = p1, *s2 = p2; if (s1->addr == s2->addr) return strcmp(s1->name, s2->name); return s1->addr < s2->addr ? -1 : 1; } struct ksyms *ksyms__load(void) { char sym_type, sym_name[256]; struct ksyms *ksyms; unsigned long sym_addr; int i, ret; FILE *f; f = fopen("/proc/kallsyms", "r"); if (!f) return NULL; ksyms = calloc(1, sizeof(*ksyms)); if (!ksyms) goto err_out; while (true) { ret = fscanf(f, "%lx %c %s%*[^\n]\n", &sym_addr, &sym_type, sym_name); if (ret == EOF && feof(f)) break; if (ret != 3) goto err_out; if (ksyms__add_symbol(ksyms, sym_name, sym_addr)) goto err_out; } /* now when strings are finalized, adjust pointers properly */ for (i = 0; i < ksyms->syms_sz; i++) ksyms->syms[i].name += (unsigned long)ksyms->strs; qsort(ksyms->syms, ksyms->syms_sz, sizeof(*ksyms->syms), ksym_cmp); fclose(f); return ksyms; err_out: ksyms__free(ksyms); fclose(f); return NULL; } void ksyms__free(struct ksyms *ksyms) { if (!ksyms) return; free(ksyms->syms); free(ksyms->strs); free(ksyms); } const struct ksym *ksyms__map_addr(const struct ksyms *ksyms, unsigned long addr) { int start = 0, end = ksyms->syms_sz - 1, mid; unsigned long sym_addr; /* find largest sym_addr <= addr using binary search */ while (start < end) { mid = start + (end - start + 1) / 2; sym_addr = ksyms->syms[mid].addr; if (sym_addr <= addr) start = mid; else end = mid - 1; } if (start == end && ksyms->syms[start].addr <= addr) return &ksyms->syms[start]; return NULL; } const struct ksym *ksyms__get_symbol(const struct ksyms *ksyms, const char *name) { int i; for (i = 0; i < ksyms->syms_sz; i++) { if (strcmp(ksyms->syms[i].name, name) == 0) return &ksyms->syms[i]; } return NULL; } struct load_range { uint64_t start; uint64_t end; uint64_t file_off; }; enum elf_type { EXEC, DYN, PERF_MAP, VDSO, UNKNOWN, }; struct dso { char *name; struct load_range *ranges; int range_sz; /* Dyn's first text section virtual addr at execution */ uint64_t sh_addr; /* Dyn's first text section file offset */ uint64_t sh_offset; enum elf_type type; struct sym *syms; int syms_sz; int syms_cap; /* * libbpf's struct btf is actually a pretty efficient * "set of strings" data structure, so we create an * empty one and use it to store symbol names. */ struct btf *btf; }; struct map { uint64_t start_addr; uint64_t end_addr; uint64_t file_off; uint64_t dev_major; uint64_t dev_minor; uint64_t inode; }; struct syms { struct dso *dsos; int dso_sz; }; static bool is_file_backed(const char *mapname) { #define STARTS_WITH(mapname, prefix) \ (!strncmp(mapname, prefix, sizeof(prefix) - 1)) return mapname[0] && !( STARTS_WITH(mapname, "//anon") || STARTS_WITH(mapname, "/dev/zero") || STARTS_WITH(mapname, "/anon_hugepage") || STARTS_WITH(mapname, "[stack") || STARTS_WITH(mapname, "/SYSV") || STARTS_WITH(mapname, "[heap]") || STARTS_WITH(mapname, "[vsyscall]")); } static bool is_perf_map(const char *path) { return false; } static bool is_vdso(const char *path) { return !strcmp(path, "[vdso]"); } static int get_elf_type(const char *path) { GElf_Ehdr hdr; void *res; Elf *e; int fd; if (is_vdso(path)) return -1; e = open_elf(path, &fd); if (!e) return -1; res = gelf_getehdr(e, &hdr); close_elf(e, fd); if (!res) return -1; return hdr.e_type; } static int get_elf_text_scn_info(const char *path, uint64_t *addr, uint64_t *offset) { Elf_Scn *section = NULL; int fd = -1, err = -1; GElf_Shdr header; size_t stridx; Elf *e = NULL; char *name; e = open_elf(path, &fd); if (!e) goto err_out; err = elf_getshdrstrndx(e, &stridx); if (err < 0) goto err_out; err = -1; while ((section = elf_nextscn(e, section)) != 0) { if (!gelf_getshdr(section, &header)) continue; name = elf_strptr(e, stridx, header.sh_name); if (name && !strcmp(name, ".text")) { *addr = (uint64_t)header.sh_addr; *offset = (uint64_t)header.sh_offset; err = 0; break; } } err_out: close_elf(e, fd); return err; } static int syms__add_dso(struct syms *syms, struct map *map, const char *name) { struct dso *dso = NULL; int i, type; void *tmp; for (i = 0; i < syms->dso_sz; i++) { if (!strcmp(syms->dsos[i].name, name)) { dso = &syms->dsos[i]; break; } } if (!dso) { tmp = realloc(syms->dsos, (syms->dso_sz + 1) * sizeof(*syms->dsos)); if (!tmp) return -1; syms->dsos = tmp; dso = &syms->dsos[syms->dso_sz++]; memset(dso, 0, sizeof(*dso)); dso->name = strdup(name); dso->btf = btf__new_empty(); } tmp = realloc(dso->ranges, (dso->range_sz + 1) * sizeof(*dso->ranges)); if (!tmp) return -1; dso->ranges = tmp; dso->ranges[dso->range_sz].start = map->start_addr; dso->ranges[dso->range_sz].end = map->end_addr; dso->ranges[dso->range_sz].file_off = map->file_off; dso->range_sz++; type = get_elf_type(name); if (type == ET_EXEC) { dso->type = EXEC; } else if (type == ET_DYN) { dso->type = DYN; if (get_elf_text_scn_info(name, &dso->sh_addr, &dso->sh_offset) < 0) return -1; } else if (is_perf_map(name)) { dso->type = PERF_MAP; } else if (is_vdso(name)) { dso->type = VDSO; } else { dso->type = UNKNOWN; } return 0; } static struct dso *syms__find_dso(const struct syms *syms, unsigned long addr, uint64_t *offset) { struct load_range *range; struct dso *dso; int i, j; for (i = 0; i < syms->dso_sz; i++) { dso = &syms->dsos[i]; for (j = 0; j < dso->range_sz; j++) { range = &dso->ranges[j]; if (addr <= range->start || addr >= range->end) continue; if (dso->type == DYN || dso->type == VDSO) { /* Offset within the mmap */ *offset = addr - range->start + range->file_off; /* Offset within the ELF for dyn symbol lookup */ *offset += dso->sh_addr - dso->sh_offset; } else { *offset = addr; } return dso; } } return NULL; } static int dso__load_sym_table_from_perf_map(struct dso *dso) { return -1; } static int dso__add_sym(struct dso *dso, const char *name, uint64_t start, uint64_t size) { struct sym *sym; size_t new_cap; void *tmp; int off; off = btf__add_str(dso->btf, name); if (off < 0) return off; if (dso->syms_sz + 1 > dso->syms_cap) { new_cap = dso->syms_cap * 4 / 3; if (new_cap < 1024) new_cap = 1024; tmp = realloc(dso->syms, sizeof(*dso->syms) * new_cap); if (!tmp) return -1; dso->syms = tmp; dso->syms_cap = new_cap; } sym = &dso->syms[dso->syms_sz++]; /* while constructing, re-use pointer as just a plain offset */ sym->name = (void*)(unsigned long)off; sym->start = start; sym->size = size; return 0; } static int sym_cmp(const void *p1, const void *p2) { const struct sym *s1 = p1, *s2 = p2; if (s1->start == s2->start) return strcmp(s1->name, s2->name); return s1->start < s2->start ? -1 : 1; } static int dso__add_syms(struct dso *dso, Elf *e, Elf_Scn *section, size_t stridx, size_t symsize) { Elf_Data *data = NULL; while ((data = elf_getdata(section, data)) != 0) { size_t i, symcount = data->d_size / symsize; if (data->d_size % symsize) return -1; for (i = 0; i < symcount; ++i) { const char *name; GElf_Sym sym; if (!gelf_getsym(data, (int)i, &sym)) continue; if (!(name = elf_strptr(e, stridx, sym.st_name))) continue; if (name[0] == '\0') continue; if (sym.st_value == 0) continue; if (dso__add_sym(dso, name, sym.st_value, sym.st_size)) goto err_out; } } return 0; err_out: return -1; } static void dso__free_fields(struct dso *dso) { if (!dso) return; free(dso->name); free(dso->ranges); free(dso->syms); btf__free(dso->btf); } static int dso__load_sym_table_from_elf(struct dso *dso, int fd) { Elf_Scn *section = NULL; Elf *e; int i; e = fd > 0 ? open_elf_by_fd(fd) : open_elf(dso->name, &fd); if (!e) return -1; while ((section = elf_nextscn(e, section)) != 0) { GElf_Shdr header; if (!gelf_getshdr(section, &header)) continue; if (header.sh_type != SHT_SYMTAB && header.sh_type != SHT_DYNSYM) continue; if (dso__add_syms(dso, e, section, header.sh_link, header.sh_entsize)) goto err_out; } /* now when strings are finalized, adjust pointers properly */ for (i = 0; i < dso->syms_sz; i++) dso->syms[i].name = btf__name_by_offset(dso->btf, (unsigned long)dso->syms[i].name); qsort(dso->syms, dso->syms_sz, sizeof(*dso->syms), sym_cmp); close_elf(e, fd); return 0; err_out: dso__free_fields(dso); close_elf(e, fd); return -1; } static int create_tmp_vdso_image(struct dso *dso) { uint64_t start_addr, end_addr; long pid = getpid(); char buf[PATH_MAX]; void *image = NULL; char tmpfile[128]; int ret, fd = -1; uint64_t sz; char *name; FILE *f; snprintf(tmpfile, sizeof(tmpfile), "/proc/%ld/maps", pid); f = fopen(tmpfile, "r"); if (!f) return -1; while (true) { ret = fscanf(f, "%lx-%lx %*s %*x %*x:%*x %*u%[^\n]", &start_addr, &end_addr, buf); if (ret == EOF && feof(f)) break; if (ret != 3) goto err_out; name = buf; while (isspace(*name)) name++; if (!is_file_backed(name)) continue; if (is_vdso(name)) break; } sz = end_addr - start_addr; image = malloc(sz); if (!image) goto err_out; memcpy(image, (void *)start_addr, sz); snprintf(tmpfile, sizeof(tmpfile), "/tmp/libbpf_%ld_vdso_image_XXXXXX", pid); fd = mkostemp(tmpfile, O_CLOEXEC); if (fd < 0) { fprintf(stderr, "failed to create temp file: %s\n", strerror(errno)); goto err_out; } /* Unlink the file to avoid leaking */ if (unlink(tmpfile) == -1) fprintf(stderr, "failed to unlink %s: %s\n", tmpfile, strerror(errno)); if (write(fd, image, sz) == -1) { fprintf(stderr, "failed to write to vDSO image: %s\n", strerror(errno)); close(fd); fd = -1; goto err_out; } err_out: fclose(f); free(image); return fd; } static int dso__load_sym_table_from_vdso_image(struct dso *dso) { int fd = create_tmp_vdso_image(dso); if (fd < 0) return -1; return dso__load_sym_table_from_elf(dso, fd); } static int dso__load_sym_table(struct dso *dso) { if (dso->type == UNKNOWN) return -1; if (dso->type == PERF_MAP) return dso__load_sym_table_from_perf_map(dso); if (dso->type == EXEC || dso->type == DYN) return dso__load_sym_table_from_elf(dso, 0); if (dso->type == VDSO) return dso__load_sym_table_from_vdso_image(dso); return -1; } static struct sym *dso__find_sym(struct dso *dso, uint64_t offset) { unsigned long sym_addr; int start, end, mid; if (!dso->syms && dso__load_sym_table(dso)) return NULL; start = 0; end = dso->syms_sz - 1; /* find largest sym_addr <= addr using binary search */ while (start < end) { mid = start + (end - start + 1) / 2; sym_addr = dso->syms[mid].start; if (sym_addr <= offset) start = mid; else end = mid - 1; } if (start == end && dso->syms[start].start <= offset) return &dso->syms[start]; return NULL; } struct syms *syms__load_file(const char *fname) { char buf[PATH_MAX], perm[5]; struct syms *syms; struct map map; char *name; FILE *f; int ret; f = fopen(fname, "r"); if (!f) return NULL; syms = calloc(1, sizeof(*syms)); if (!syms) goto err_out; while (true) { ret = fscanf(f, "%lx-%lx %4s %lx %lx:%lx %lu%[^\n]", &map.start_addr, &map.end_addr, perm, &map.file_off, &map.dev_major, &map.dev_minor, &map.inode, buf); if (ret == EOF && feof(f)) break; if (ret != 8) /* perf-.map */ goto err_out; if (perm[2] != 'x') continue; name = buf; while (isspace(*name)) name++; if (!is_file_backed(name)) continue; if (syms__add_dso(syms, &map, name)) goto err_out; } fclose(f); return syms; err_out: syms__free(syms); fclose(f); return NULL; } struct syms *syms__load_pid(pid_t tgid) { char fname[128]; snprintf(fname, sizeof(fname), "/proc/%ld/maps", (long)tgid); return syms__load_file(fname); } void syms__free(struct syms *syms) { int i; if (!syms) return; for (i = 0; i < syms->dso_sz; i++) dso__free_fields(&syms->dsos[i]); free(syms->dsos); free(syms); } const struct sym *syms__map_addr(const struct syms *syms, unsigned long addr) { struct dso *dso; uint64_t offset; dso = syms__find_dso(syms, addr, &offset); if (!dso) return NULL; return dso__find_sym(dso, offset); } struct syms_cache { struct { struct syms *syms; int tgid; } *data; int nr; }; struct syms_cache *syms_cache__new(int nr) { struct syms_cache *syms_cache; syms_cache = calloc(1, sizeof(*syms_cache)); if (!syms_cache) return NULL; if (nr > 0) syms_cache->data = calloc(nr, sizeof(*syms_cache->data)); return syms_cache; } void syms_cache__free(struct syms_cache *syms_cache) { int i; if (!syms_cache) return; for (i = 0; i < syms_cache->nr; i++) syms__free(syms_cache->data[i].syms); free(syms_cache->data); free(syms_cache); } struct syms *syms_cache__get_syms(struct syms_cache *syms_cache, int tgid) { void *tmp; int i; for (i = 0; i < syms_cache->nr; i++) { if (syms_cache->data[i].tgid == tgid) return syms_cache->data[i].syms; } tmp = realloc(syms_cache->data, (syms_cache->nr + 1) * sizeof(*syms_cache->data)); if (!tmp) return NULL; syms_cache->data = tmp; syms_cache->data[syms_cache->nr].syms = syms__load_pid(tgid); syms_cache->data[syms_cache->nr].tgid = tgid; return syms_cache->data[syms_cache->nr++].syms; } struct partitions { struct partition *items; int sz; }; static int partitions__add_partition(struct partitions *partitions, const char *name, unsigned int dev) { struct partition *partition; void *tmp; tmp = realloc(partitions->items, (partitions->sz + 1) * sizeof(*partitions->items)); if (!tmp) return -1; partitions->items = tmp; partition = &partitions->items[partitions->sz]; partition->name = strdup(name); partition->dev = dev; partitions->sz++; return 0; } struct partitions *partitions__load(void) { char part_name[DISK_NAME_LEN]; unsigned int devmaj, devmin; unsigned long long nop; struct partitions *partitions; char buf[64]; FILE *f; f = fopen("/proc/partitions", "r"); if (!f) return NULL; partitions = calloc(1, sizeof(*partitions)); if (!partitions) goto err_out; while (fgets(buf, sizeof(buf), f) != NULL) { /* skip heading */ if (buf[0] != ' ' || buf[0] == '\n') continue; if (sscanf(buf, "%u %u %llu %s", &devmaj, &devmin, &nop, part_name) != 4) goto err_out; if (partitions__add_partition(partitions, part_name, MKDEV(devmaj, devmin))) goto err_out; } fclose(f); return partitions; err_out: partitions__free(partitions); fclose(f); return NULL; } void partitions__free(struct partitions *partitions) { int i; if (!partitions) return; for (i = 0; i < partitions->sz; i++) free(partitions->items[i].name); free(partitions->items); free(partitions); } const struct partition * partitions__get_by_dev(const struct partitions *partitions, unsigned int dev) { int i; for (i = 0; i < partitions->sz; i++) { if (partitions->items[i].dev == dev) return &partitions->items[i]; } return NULL; } const struct partition * partitions__get_by_name(const struct partitions *partitions, const char *name) { int i; for (i = 0; i < partitions->sz; i++) { if (strcmp(partitions->items[i].name, name) == 0) return &partitions->items[i]; } return NULL; } static void print_stars(unsigned int val, unsigned int val_max, int width) { int num_stars, num_spaces, i; bool need_plus; num_stars = min(val, val_max) * width / val_max; num_spaces = width - num_stars; need_plus = val > val_max; for (i = 0; i < num_stars; i++) printf("*"); for (i = 0; i < num_spaces; i++) printf(" "); if (need_plus) printf("+"); } void print_log2_hist(unsigned int *vals, int vals_size, const char *val_type) { int stars_max = 40, idx_max = -1; unsigned int val, val_max = 0; unsigned long long low, high; int stars, width, i; for (i = 0; i < vals_size; i++) { val = vals[i]; if (val > 0) idx_max = i; if (val > val_max) val_max = val; } if (idx_max < 0) return; printf("%*s%-*s : count distribution\n", idx_max <= 32 ? 5 : 15, "", idx_max <= 32 ? 19 : 29, val_type); if (idx_max <= 32) stars = stars_max; else stars = stars_max / 2; for (i = 0; i <= idx_max; i++) { low = (1ULL << (i + 1)) >> 1; high = (1ULL << (i + 1)) - 1; if (low == high) low -= 1; val = vals[i]; width = idx_max <= 32 ? 10 : 20; printf("%*lld -> %-*lld : %-8d |", width, low, width, high, val); print_stars(val, val_max, stars); printf("|\n"); } } void print_linear_hist(unsigned int *vals, int vals_size, unsigned int base, unsigned int step, const char *val_type) { int i, stars_max = 40, idx_min = -1, idx_max = -1; unsigned int val, val_max = 0; for (i = 0; i < vals_size; i++) { val = vals[i]; if (val > 0) { idx_max = i; if (idx_min < 0) idx_min = i; } if (val > val_max) val_max = val; } if (idx_max < 0) return; printf(" %-13s : count distribution\n", val_type); for (i = idx_min; i <= idx_max; i++) { val = vals[i]; printf(" %-10d : %-8d |", base + i * step, val); print_stars(val, val_max, stars_max); printf("|\n"); } } unsigned long long get_ktime_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * NSEC_PER_SEC + ts.tv_nsec; } int bump_memlock_rlimit(void) { struct rlimit rlim_new = { .rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY, }; return setrlimit(RLIMIT_MEMLOCK, &rlim_new); } bool is_kernel_module(const char *name) { bool found = false; char buf[64]; FILE *f; f = fopen("/proc/modules", "r"); if (!f) return false; while (fgets(buf, sizeof(buf), f) != NULL) { if (sscanf(buf, "%s %*s\n", buf) != 1) break; if (!strcmp(buf, name)) { found = true; break; } } fclose(f); return found; } bool fentry_exists(const char *name, const char *mod) { const char sysfs_vmlinux[] = "/sys/kernel/btf/vmlinux"; struct btf *base, *btf = NULL; const struct btf_type *type; const struct btf_enum *e; char sysfs_mod[80]; int id = -1, i; base = btf__parse(sysfs_vmlinux, NULL); if (libbpf_get_error(base)) { fprintf(stderr, "failed to parse vmlinux BTF at '%s': %s\n", sysfs_vmlinux, strerror(-libbpf_get_error(base))); goto err_out; } if (mod && module_btf_exists(mod)) { snprintf(sysfs_mod, sizeof(sysfs_mod), "/sys/kernel/btf/%s", mod); btf = btf__parse_split(sysfs_mod, base); if (libbpf_get_error(btf)) { fprintf(stderr, "failed to load BTF from %s: %s\n", sysfs_mod, strerror(-libbpf_get_error(btf))); btf = base; base = NULL; } } else { btf = base; base = NULL; } id = btf__find_by_name_kind(btf, "bpf_attach_type", BTF_KIND_ENUM); if (id < 0) goto err_out; type = btf__type_by_id(btf, id); /* * As kernel BTF is exposed starting from 5.4 kernel, but fentry/fexit * is actually supported starting from 5.5, so that's check this gap * first, then check if target func has btf type. */ for (id = -1, i = 0, e = btf_enum(type); i < btf_vlen(type); i++, e++) { if (!strcmp(btf__name_by_offset(btf, e->name_off), "BPF_TRACE_FENTRY")) { id = btf__find_by_name_kind(btf, name, BTF_KIND_FUNC); break; } } err_out: btf__free(btf); btf__free(base); return id > 0; } bool kprobe_exists(const char *name) { char sym_name[256]; FILE *f; int ret; f = fopen("/sys/kernel/debug/tracing/available_filter_functions", "r"); if (!f) goto slow_path; while (true) { ret = fscanf(f, "%s%*[^\n]\n", sym_name); if (ret == EOF && feof(f)) break; if (ret != 1) { fprintf(stderr, "failed to read symbol from available_filter_functions\n"); break; } if (!strcmp(name, sym_name)) { fclose(f); return true; } } fclose(f); return false; slow_path: f = fopen("/proc/kallsyms", "r"); if (!f) return false; while (true) { ret = fscanf(f, "%*x %*c %s%*[^\n]\n", sym_name); if (ret == EOF && feof(f)) break; if (ret != 1) { fprintf(stderr, "failed to read symbol from kallsyms\n"); break; } if (!strcmp(name, sym_name)) { fclose(f); return true; } } fclose(f); return false; } bool vmlinux_btf_exists(void) { if (!access("/sys/kernel/btf/vmlinux", R_OK)) return true; return false; } bool module_btf_exists(const char *mod) { char sysfs_mod[80]; if (mod) { snprintf(sysfs_mod, sizeof(sysfs_mod), "/sys/kernel/btf/%s", mod); if (!access(sysfs_mod, R_OK)) return true; } return false; }