Commit 88a82120 authored by Andrii Nakryiko's avatar Andrii Nakryiko Committed by Alexei Starovoitov

libbpf: Factor out common ELF operations and improve logging

Factor out common ELF operations done throughout the libbpf. This simplifies
usage across multiple places in libbpf, as well as hide error reporting from
higher-level functions and make error logging more consistent.
Signed-off-by: default avatarAndrii Nakryiko <andriin@fb.com>
Signed-off-by: default avatarAlexei Starovoitov <ast@kernel.org>
Link: https://lore.kernel.org/bpf/20200820231250.1293069-3-andriin@fb.com
parent 3ac2e20f
......@@ -398,6 +398,7 @@ struct bpf_object {
Elf_Data *rodata;
Elf_Data *bss;
Elf_Data *st_ops_data;
size_t shstrndx; /* section index for section name strings */
size_t strtabidx;
struct {
GElf_Shdr shdr;
......@@ -435,6 +436,14 @@ struct bpf_object {
};
#define obj_elf_valid(o) ((o)->efile.elf)
static const char *elf_sym_str(const struct bpf_object *obj, size_t off);
static const char *elf_sec_str(const struct bpf_object *obj, size_t off);
static Elf_Scn *elf_sec_by_idx(const struct bpf_object *obj, size_t idx);
static Elf_Scn *elf_sec_by_name(const struct bpf_object *obj, const char *name);
static int elf_sec_hdr(const struct bpf_object *obj, Elf_Scn *scn, GElf_Shdr *hdr);
static const char *elf_sec_name(const struct bpf_object *obj, Elf_Scn *scn);
static Elf_Data *elf_sec_data(const struct bpf_object *obj, Elf_Scn *scn);
void bpf_program__unload(struct bpf_program *prog)
{
int i;
......@@ -496,7 +505,7 @@ static char *__bpf_program__pin_name(struct bpf_program *prog)
}
static int
bpf_program__init(void *data, size_t size, char *section_name, int idx,
bpf_program__init(void *data, size_t size, const char *section_name, int idx,
struct bpf_program *prog)
{
const size_t bpf_insn_sz = sizeof(struct bpf_insn);
......@@ -545,7 +554,7 @@ bpf_program__init(void *data, size_t size, char *section_name, int idx,
static int
bpf_object__add_program(struct bpf_object *obj, void *data, size_t size,
char *section_name, int idx)
const char *section_name, int idx)
{
struct bpf_program prog, *progs;
int nr_progs, err;
......@@ -570,7 +579,7 @@ bpf_object__add_program(struct bpf_object *obj, void *data, size_t size,
return -ENOMEM;
}
pr_debug("found program %s\n", prog.section_name);
pr_debug("elf: found program '%s'\n", prog.section_name);
obj->programs = progs;
obj->nr_programs = nr_progs + 1;
prog.obj = obj;
......@@ -590,8 +599,7 @@ bpf_object__init_prog_names(struct bpf_object *obj)
prog = &obj->programs[pi];
for (si = 0; si < symbols->d_size / sizeof(GElf_Sym) && !name;
si++) {
for (si = 0; si < symbols->d_size / sizeof(GElf_Sym) && !name; si++) {
GElf_Sym sym;
if (!gelf_getsym(symbols, si, &sym))
......@@ -601,11 +609,9 @@ bpf_object__init_prog_names(struct bpf_object *obj)
if (GELF_ST_BIND(sym.st_info) != STB_GLOBAL)
continue;
name = elf_strptr(obj->efile.elf,
obj->efile.strtabidx,
sym.st_name);
name = elf_sym_str(obj, sym.st_name);
if (!name) {
pr_warn("failed to get sym name string for prog %s\n",
pr_warn("prog '%s': failed to get symbol name\n",
prog->section_name);
return -LIBBPF_ERRNO__LIBELF;
}
......@@ -615,17 +621,14 @@ bpf_object__init_prog_names(struct bpf_object *obj)
name = ".text";
if (!name) {
pr_warn("failed to find sym for prog %s\n",
pr_warn("prog '%s': failed to find program symbol\n",
prog->section_name);
return -EINVAL;
}
prog->name = strdup(name);
if (!prog->name) {
pr_warn("failed to allocate memory for prog sym %s\n",
name);
if (!prog->name)
return -ENOMEM;
}
}
return 0;
......@@ -1069,7 +1072,7 @@ static int bpf_object__elf_init(struct bpf_object *obj)
GElf_Ehdr *ep;
if (obj_elf_valid(obj)) {
pr_warn("elf init: internal error\n");
pr_warn("elf: init internal error\n");
return -LIBBPF_ERRNO__LIBELF;
}
......@@ -1087,7 +1090,7 @@ static int bpf_object__elf_init(struct bpf_object *obj)
err = -errno;
cp = libbpf_strerror_r(err, errmsg, sizeof(errmsg));
pr_warn("failed to open %s: %s\n", obj->path, cp);
pr_warn("elf: failed to open %s: %s\n", obj->path, cp);
return err;
}
......@@ -1095,22 +1098,36 @@ static int bpf_object__elf_init(struct bpf_object *obj)
}
if (!obj->efile.elf) {
pr_warn("failed to open %s as ELF file\n", obj->path);
pr_warn("elf: failed to open %s as ELF file: %s\n", obj->path, elf_errmsg(-1));
err = -LIBBPF_ERRNO__LIBELF;
goto errout;
}
if (!gelf_getehdr(obj->efile.elf, &obj->efile.ehdr)) {
pr_warn("failed to get EHDR from %s\n", obj->path);
pr_warn("elf: failed to get ELF header from %s: %s\n", obj->path, elf_errmsg(-1));
err = -LIBBPF_ERRNO__FORMAT;
goto errout;
}
ep = &obj->efile.ehdr;
if (elf_getshdrstrndx(obj->efile.elf, &obj->efile.shstrndx)) {
pr_warn("elf: failed to get section names section index for %s: %s\n",
obj->path, elf_errmsg(-1));
err = -LIBBPF_ERRNO__FORMAT;
goto errout;
}
/* Elf is corrupted/truncated, avoid calling elf_strptr. */
if (!elf_rawdata(elf_getscn(obj->efile.elf, obj->efile.shstrndx), NULL)) {
pr_warn("elf: failed to get section names strings from %s: %s\n",
obj->path, elf_errmsg(-1));
return -LIBBPF_ERRNO__FORMAT;
}
/* Old LLVM set e_machine to EM_NONE */
if (ep->e_type != ET_REL ||
(ep->e_machine && ep->e_machine != EM_BPF)) {
pr_warn("%s is not an eBPF object file\n", obj->path);
pr_warn("elf: %s is not a valid eBPF object file\n", obj->path);
err = -LIBBPF_ERRNO__FORMAT;
goto errout;
}
......@@ -1132,7 +1149,7 @@ static int bpf_object__check_endianness(struct bpf_object *obj)
#else
# error "Unrecognized __BYTE_ORDER__"
#endif
pr_warn("endianness mismatch.\n");
pr_warn("elf: endianness mismatch in %s.\n", obj->path);
return -LIBBPF_ERRNO__ENDIAN;
}
......@@ -1167,55 +1184,10 @@ static bool bpf_map_type__is_map_in_map(enum bpf_map_type type)
return false;
}
static int bpf_object_search_section_size(const struct bpf_object *obj,
const char *name, size_t *d_size)
{
const GElf_Ehdr *ep = &obj->efile.ehdr;
Elf *elf = obj->efile.elf;
Elf_Scn *scn = NULL;
int idx = 0;
while ((scn = elf_nextscn(elf, scn)) != NULL) {
const char *sec_name;
Elf_Data *data;
GElf_Shdr sh;
idx++;
if (gelf_getshdr(scn, &sh) != &sh) {
pr_warn("failed to get section(%d) header from %s\n",
idx, obj->path);
return -EIO;
}
sec_name = elf_strptr(elf, ep->e_shstrndx, sh.sh_name);
if (!sec_name) {
pr_warn("failed to get section(%d) name from %s\n",
idx, obj->path);
return -EIO;
}
if (strcmp(name, sec_name))
continue;
data = elf_getdata(scn, 0);
if (!data) {
pr_warn("failed to get section(%d) data from %s(%s)\n",
idx, name, obj->path);
return -EIO;
}
*d_size = data->d_size;
return 0;
}
return -ENOENT;
}
int bpf_object__section_size(const struct bpf_object *obj, const char *name,
__u32 *size)
{
int ret = -ENOENT;
size_t d_size;
*size = 0;
if (!name) {
......@@ -1233,9 +1205,13 @@ int bpf_object__section_size(const struct bpf_object *obj, const char *name,
if (obj->efile.st_ops_data)
*size = obj->efile.st_ops_data->d_size;
} else {
ret = bpf_object_search_section_size(obj, name, &d_size);
if (!ret)
*size = d_size;
Elf_Scn *scn = elf_sec_by_name(obj, name);
Elf_Data *data = elf_sec_data(obj, scn);
if (data) {
ret = 0; /* found it */
*size = data->d_size;
}
}
return *size ? 0 : ret;
......@@ -1260,8 +1236,7 @@ int bpf_object__variable_offset(const struct bpf_object *obj, const char *name,
GELF_ST_TYPE(sym.st_info) != STT_OBJECT)
continue;
sname = elf_strptr(obj->efile.elf, obj->efile.strtabidx,
sym.st_name);
sname = elf_sym_str(obj, sym.st_name);
if (!sname) {
pr_warn("failed to get sym name string for var %s\n",
name);
......@@ -1738,12 +1713,12 @@ static int bpf_object__init_user_maps(struct bpf_object *obj, bool strict)
if (!symbols)
return -EINVAL;
scn = elf_getscn(obj->efile.elf, obj->efile.maps_shndx);
if (scn)
data = elf_getdata(scn, NULL);
scn = elf_sec_by_idx(obj, obj->efile.maps_shndx);
data = elf_sec_data(obj, scn);
if (!scn || !data) {
pr_warn("failed to get Elf_Data from map section %d\n",
obj->efile.maps_shndx);
pr_warn("elf: failed to get legacy map definitions for %s\n",
obj->path);
return -EINVAL;
}
......@@ -1765,12 +1740,12 @@ static int bpf_object__init_user_maps(struct bpf_object *obj, bool strict)
nr_maps++;
}
/* Assume equally sized map definitions */
pr_debug("maps in %s: %d maps in %zd bytes\n",
obj->path, nr_maps, data->d_size);
pr_debug("elf: found %d legacy map definitions (%zd bytes) in %s\n",
nr_maps, data->d_size, obj->path);
if (!data->d_size || nr_maps == 0 || (data->d_size % nr_maps) != 0) {
pr_warn("unable to determine map definition size section %s, %d maps in %zd bytes\n",
obj->path, nr_maps, data->d_size);
pr_warn("elf: unable to determine legacy map definition size in %s\n",
obj->path);
return -EINVAL;
}
map_def_sz = data->d_size / nr_maps;
......@@ -1791,8 +1766,7 @@ static int bpf_object__init_user_maps(struct bpf_object *obj, bool strict)
if (IS_ERR(map))
return PTR_ERR(map);
map_name = elf_strptr(obj->efile.elf, obj->efile.strtabidx,
sym.st_name);
map_name = elf_sym_str(obj, sym.st_name);
if (!map_name) {
pr_warn("failed to get map #%d name sym string for obj %s\n",
i, obj->path);
......@@ -2274,12 +2248,11 @@ static int bpf_object__init_user_btf_maps(struct bpf_object *obj, bool strict,
if (obj->efile.btf_maps_shndx < 0)
return 0;
scn = elf_getscn(obj->efile.elf, obj->efile.btf_maps_shndx);
if (scn)
data = elf_getdata(scn, NULL);
scn = elf_sec_by_idx(obj, obj->efile.btf_maps_shndx);
data = elf_sec_data(obj, scn);
if (!scn || !data) {
pr_warn("failed to get Elf_Data from map section %d (%s)\n",
obj->efile.maps_shndx, MAPS_ELF_SEC);
pr_warn("elf: failed to get %s map definitions for %s\n",
MAPS_ELF_SEC, obj->path);
return -EINVAL;
}
......@@ -2337,20 +2310,12 @@ static int bpf_object__init_maps(struct bpf_object *obj,
static bool section_have_execinstr(struct bpf_object *obj, int idx)
{
Elf_Scn *scn;
GElf_Shdr sh;
scn = elf_getscn(obj->efile.elf, idx);
if (!scn)
return false;
if (gelf_getshdr(scn, &sh) != &sh)
if (elf_sec_hdr(obj, elf_sec_by_idx(obj, idx), &sh))
return false;
if (sh.sh_flags & SHF_EXECINSTR)
return true;
return false;
return sh.sh_flags & SHF_EXECINSTR;
}
static bool btf_needs_sanitization(struct bpf_object *obj)
......@@ -2594,61 +2559,156 @@ static int bpf_object__sanitize_and_load_btf(struct bpf_object *obj)
return err;
}
static const char *elf_sym_str(const struct bpf_object *obj, size_t off)
{
const char *name;
name = elf_strptr(obj->efile.elf, obj->efile.strtabidx, off);
if (!name) {
pr_warn("elf: failed to get section name string at offset %zu from %s: %s\n",
off, obj->path, elf_errmsg(-1));
return NULL;
}
return name;
}
static const char *elf_sec_str(const struct bpf_object *obj, size_t off)
{
const char *name;
name = elf_strptr(obj->efile.elf, obj->efile.shstrndx, off);
if (!name) {
pr_warn("elf: failed to get section name string at offset %zu from %s: %s\n",
off, obj->path, elf_errmsg(-1));
return NULL;
}
return name;
}
static Elf_Scn *elf_sec_by_idx(const struct bpf_object *obj, size_t idx)
{
Elf_Scn *scn;
scn = elf_getscn(obj->efile.elf, idx);
if (!scn) {
pr_warn("elf: failed to get section(%zu) from %s: %s\n",
idx, obj->path, elf_errmsg(-1));
return NULL;
}
return scn;
}
static Elf_Scn *elf_sec_by_name(const struct bpf_object *obj, const char *name)
{
Elf_Scn *scn = NULL;
Elf *elf = obj->efile.elf;
const char *sec_name;
while ((scn = elf_nextscn(elf, scn)) != NULL) {
sec_name = elf_sec_name(obj, scn);
if (!sec_name)
return NULL;
if (strcmp(sec_name, name) != 0)
continue;
return scn;
}
return NULL;
}
static int elf_sec_hdr(const struct bpf_object *obj, Elf_Scn *scn, GElf_Shdr *hdr)
{
if (!scn)
return -EINVAL;
if (gelf_getshdr(scn, hdr) != hdr) {
pr_warn("elf: failed to get section(%zu) header from %s: %s\n",
elf_ndxscn(scn), obj->path, elf_errmsg(-1));
return -EINVAL;
}
return 0;
}
static const char *elf_sec_name(const struct bpf_object *obj, Elf_Scn *scn)
{
const char *name;
GElf_Shdr sh;
if (!scn)
return NULL;
if (elf_sec_hdr(obj, scn, &sh))
return NULL;
name = elf_sec_str(obj, sh.sh_name);
if (!name) {
pr_warn("elf: failed to get section(%zu) name from %s: %s\n",
elf_ndxscn(scn), obj->path, elf_errmsg(-1));
return NULL;
}
return name;
}
static Elf_Data *elf_sec_data(const struct bpf_object *obj, Elf_Scn *scn)
{
Elf_Data *data;
if (!scn)
return NULL;
data = elf_getdata(scn, 0);
if (!data) {
pr_warn("elf: failed to get section(%zu) %s data from %s: %s\n",
elf_ndxscn(scn), elf_sec_name(obj, scn) ?: "<?>",
obj->path, elf_errmsg(-1));
return NULL;
}
return data;
}
static int bpf_object__elf_collect(struct bpf_object *obj)
{
Elf *elf = obj->efile.elf;
GElf_Ehdr *ep = &obj->efile.ehdr;
Elf_Data *btf_ext_data = NULL;
Elf_Data *btf_data = NULL;
Elf_Scn *scn = NULL;
int idx = 0, err = 0;
/* Elf is corrupted/truncated, avoid calling elf_strptr. */
if (!elf_rawdata(elf_getscn(elf, ep->e_shstrndx), NULL)) {
pr_warn("failed to get e_shstrndx from %s\n", obj->path);
return -LIBBPF_ERRNO__FORMAT;
}
while ((scn = elf_nextscn(elf, scn)) != NULL) {
char *name;
const char *name;
GElf_Shdr sh;
Elf_Data *data;
idx++;
if (gelf_getshdr(scn, &sh) != &sh) {
pr_warn("failed to get section(%d) header from %s\n",
idx, obj->path);
if (elf_sec_hdr(obj, scn, &sh))
return -LIBBPF_ERRNO__FORMAT;
}
name = elf_strptr(elf, ep->e_shstrndx, sh.sh_name);
if (!name) {
pr_warn("failed to get section(%d) name from %s\n",
idx, obj->path);
name = elf_sec_str(obj, sh.sh_name);
if (!name)
return -LIBBPF_ERRNO__FORMAT;
}
data = elf_getdata(scn, 0);
if (!data) {
pr_warn("failed to get section(%d) data from %s(%s)\n",
idx, name, obj->path);
data = elf_sec_data(obj, scn);
if (!data)
return -LIBBPF_ERRNO__FORMAT;
}
pr_debug("section(%d) %s, size %ld, link %d, flags %lx, type=%d\n",
pr_debug("elf: section(%d) %s, size %ld, link %d, flags %lx, type=%d\n",
idx, name, (unsigned long)data->d_size,
(int)sh.sh_link, (unsigned long)sh.sh_flags,
(int)sh.sh_type);
if (strcmp(name, "license") == 0) {
err = bpf_object__init_license(obj,
data->d_buf,
data->d_size);
err = bpf_object__init_license(obj, data->d_buf, data->d_size);
if (err)
return err;
} else if (strcmp(name, "version") == 0) {
err = bpf_object__init_kversion(obj,
data->d_buf,
data->d_size);
err = bpf_object__init_kversion(obj, data->d_buf, data->d_size);
if (err)
return err;
} else if (strcmp(name, "maps") == 0) {
......@@ -2661,8 +2721,7 @@ static int bpf_object__elf_collect(struct bpf_object *obj)
btf_ext_data = data;
} else if (sh.sh_type == SHT_SYMTAB) {
if (obj->efile.symbols) {
pr_warn("bpf: multiple SYMTAB in %s\n",
obj->path);
pr_warn("elf: multiple symbol tables in %s\n", obj->path);
return -LIBBPF_ERRNO__FORMAT;
}
obj->efile.symbols = data;
......@@ -2675,16 +2734,8 @@ static int bpf_object__elf_collect(struct bpf_object *obj)
err = bpf_object__add_program(obj, data->d_buf,
data->d_size,
name, idx);
if (err) {
char errmsg[STRERR_BUFSIZE];
char *cp;
cp = libbpf_strerror_r(-err, errmsg,
sizeof(errmsg));
pr_warn("failed to alloc program %s (%s): %s",
name, obj->path, cp);
if (err)
return err;
}
} else if (strcmp(name, DATA_SEC) == 0) {
obj->efile.data = data;
obj->efile.data_shndx = idx;
......@@ -2695,7 +2746,8 @@ static int bpf_object__elf_collect(struct bpf_object *obj)
obj->efile.st_ops_data = data;
obj->efile.st_ops_shndx = idx;
} else {
pr_debug("skip section(%d) %s\n", idx, name);
pr_debug("elf: skipping unrecognized data section(%d) %s\n",
idx, name);
}
} else if (sh.sh_type == SHT_REL) {
int nr_sects = obj->efile.nr_reloc_sects;
......@@ -2706,34 +2758,32 @@ static int bpf_object__elf_collect(struct bpf_object *obj)
if (!section_have_execinstr(obj, sec) &&
strcmp(name, ".rel" STRUCT_OPS_SEC) &&
strcmp(name, ".rel" MAPS_ELF_SEC)) {
pr_debug("skip relo %s(%d) for section(%d)\n",
name, idx, sec);
pr_debug("elf: skipping relo section(%d) %s for section(%d) %s\n",
idx, name, sec,
elf_sec_name(obj, elf_sec_by_idx(obj, sec)) ?: "<?>");
continue;
}
sects = libbpf_reallocarray(sects, nr_sects + 1,
sizeof(*obj->efile.reloc_sects));
if (!sects) {
pr_warn("reloc_sects realloc failed\n");
if (!sects)
return -ENOMEM;
}
obj->efile.reloc_sects = sects;
obj->efile.nr_reloc_sects++;
obj->efile.reloc_sects[nr_sects].shdr = sh;
obj->efile.reloc_sects[nr_sects].data = data;
} else if (sh.sh_type == SHT_NOBITS &&
strcmp(name, BSS_SEC) == 0) {
} else if (sh.sh_type == SHT_NOBITS && strcmp(name, BSS_SEC) == 0) {
obj->efile.bss = data;
obj->efile.bss_shndx = idx;
} else {
pr_debug("skip section(%d) %s\n", idx, name);
pr_debug("elf: skipping section(%d) %s\n", idx, name);
}
}
if (!obj->efile.strtabidx || obj->efile.strtabidx > idx) {
pr_warn("Corrupted ELF file: index of strtab invalid\n");
pr_warn("elf: symbol strings section missing or invalid in %s\n", obj->path);
return -LIBBPF_ERRNO__FORMAT;
}
return bpf_object__init_btf(obj, btf_data, btf_ext_data);
......@@ -2894,14 +2944,13 @@ static int bpf_object__collect_externs(struct bpf_object *obj)
if (!obj->efile.symbols)
return 0;
scn = elf_getscn(obj->efile.elf, obj->efile.symbols_shndx);
if (!scn)
return -LIBBPF_ERRNO__FORMAT;
if (gelf_getshdr(scn, &sh) != &sh)
scn = elf_sec_by_idx(obj, obj->efile.symbols_shndx);
if (elf_sec_hdr(obj, scn, &sh))
return -LIBBPF_ERRNO__FORMAT;
n = sh.sh_size / sh.sh_entsize;
n = sh.sh_size / sh.sh_entsize;
pr_debug("looking for externs among %d symbols...\n", n);
for (i = 0; i < n; i++) {
GElf_Sym sym;
......@@ -2909,8 +2958,7 @@ static int bpf_object__collect_externs(struct bpf_object *obj)
return -LIBBPF_ERRNO__FORMAT;
if (!sym_is_extern(&sym))
continue;
ext_name = elf_strptr(obj->efile.elf, obj->efile.strtabidx,
sym.st_name);
ext_name = elf_sym_str(obj, sym.st_name);
if (!ext_name || !ext_name[0])
continue;
......@@ -3289,16 +3337,15 @@ bpf_program__collect_reloc(struct bpf_program *prog, GElf_Shdr *shdr,
return -LIBBPF_ERRNO__FORMAT;
}
if (!gelf_getsym(symbols, GELF_R_SYM(rel.r_info), &sym)) {
pr_warn("relocation: symbol %"PRIx64" not found\n",
GELF_R_SYM(rel.r_info));
pr_warn("relocation: symbol %zx not found\n",
(size_t)GELF_R_SYM(rel.r_info));
return -LIBBPF_ERRNO__FORMAT;
}
if (rel.r_offset % sizeof(struct bpf_insn))
return -LIBBPF_ERRNO__FORMAT;
insn_idx = rel.r_offset / sizeof(struct bpf_insn);
name = elf_strptr(obj->efile.elf, obj->efile.strtabidx,
sym.st_name) ? : "<?>";
name = elf_sym_str(obj, sym.st_name) ?: "<?>";
pr_debug("relo for shdr %u, symb %zu, value %zu, type %d, bind %d, name %d (\'%s\'), insn %u\n",
(__u32)sym.st_shndx, (size_t)GELF_R_SYM(rel.r_info),
......@@ -5720,8 +5767,7 @@ static int bpf_object__collect_map_relos(struct bpf_object *obj,
i, (size_t)GELF_R_SYM(rel.r_info));
return -LIBBPF_ERRNO__FORMAT;
}
name = elf_strptr(obj->efile.elf, obj->efile.strtabidx,
sym.st_name) ? : "<?>";
name = elf_sym_str(obj, sym.st_name) ?: "<?>";
if (sym.st_shndx != obj->efile.btf_maps_shndx) {
pr_warn(".maps relo #%d: '%s' isn't a BTF-defined map\n",
i, name);
......@@ -7663,8 +7709,7 @@ static int bpf_object__collect_st_ops_relos(struct bpf_object *obj,
return -LIBBPF_ERRNO__FORMAT;
}
name = elf_strptr(obj->efile.elf, obj->efile.strtabidx,
sym.st_name) ? : "<?>";
name = elf_sym_str(obj, sym.st_name) ?: "<?>";
map = find_struct_ops_map_by_offset(obj, rel.r_offset);
if (!map) {
pr_warn("struct_ops reloc: cannot find map at rel.r_offset %zu\n",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment