diff --git a/librz/bin/meson.build b/librz/bin/meson.build index f9744a18f8..da40667708 100644 --- a/librz/bin/meson.build +++ b/librz/bin/meson.build @@ -103,6 +103,7 @@ rz_bin_sources = [ 'filter.c', 'golang.c', 'relocs_patch.c', + 'stabs.c', 'p/bin_any.c', 'p/bin_art.c', 'p/bin_avr.c', diff --git a/librz/bin/stabs.c b/librz/bin/stabs.c new file mode 100644 index 0000000000..08a60b02bb --- /dev/null +++ b/librz/bin/stabs.c @@ -0,0 +1,924 @@ +// SPDX-FileCopyrightText: 2026 RizinOrg +// SPDX-License-Identifier: LGPL-3.0-only + +#include +#include +#include +#include +#include + +/** + * \file stabs.c + * \brief Parser for the legacy STABS debug information format. + * + * The `.stab` section is an array of fixed-size 12-byte records. Each record is + * laid out (little- or big-endian, matching the host object) as: + * + * | n_strx (4) | n_type (1) | n_other (1) | n_desc (2) | n_value (4) | + * + * The strings referenced by `n_strx` live in the `.stabstr` section. STABS files + * can be the concatenation of several compilation units; the first record of + * each unit is an `N_UNDF` header whose `n_value` is the size of that unit's + * slice of the string table, so string offsets must be rebased per unit. + */ + +static bool stabs_big_endian(RzBinFile *bf) { + return bf->o && bf->o->info ? bf->o->info->big_endian : false; +} + +static RzBinSection *stabs_section(RzBinFile *bf, const char *name) { + rz_return_val_if_fail(bf && name, NULL); + RzBinObject *o = bf->o; + if (!o || !o->sections) { + return NULL; + } + void **it; + rz_pvector_foreach (o->sections, it) { + RzBinSection *s = *it; + if (s && RZ_STR_EQ(s->name, name)) { + return s; + } + } + // some toolchains prefix the section names, fall back to a suffix match + rz_pvector_foreach (o->sections, it) { + RzBinSection *s = *it; + if (s && RZ_STR_ISNOTEMPTY(s->name) && rz_str_endswith(s->name, name)) { + return s; + } + } + return NULL; +} + +static ut8 *stabs_read_section(RzBinFile *bf, RzBinSection *section, ut64 *out_len) { + if (!section || section->paddr >= bf->size) { + return NULL; + } + ut64 len = RZ_MIN(section->size, bf->size - section->paddr); + if (!len) { + return NULL; + } + // Allocate one extra byte and keep it zeroed so the buffer is always safe to + // treat as a NUL-terminated string (the `.stabstr` table is not guaranteed + // to end with a NUL on malformed input). + ut8 *buf = calloc(len + 1, 1); + if (!buf) { + return NULL; + } + if (rz_buf_read_at(bf->buf, section->paddr, buf, len) != (st64)len) { + free(buf); + return NULL; + } + *out_len = len; + return buf; +} + +/** + * \brief Parse the STABS information contained in a binary + * + * Reads the `.stab` and `.stabstr` sections, decodes every fixed-size record and + * resolves its string (rebasing the per-compilation-unit string offsets). + * + * \param bf The binary file to read the STABS sections from + * \return A newly allocated \ref RzBinStabs (owned by the caller), or NULL if the + * binary has no usable STABS data + */ +RZ_API RZ_OWN RzBinStabs *rz_bin_stabs_parse(RZ_NONNULL RzBinFile *bf) { + rz_return_val_if_fail(bf, NULL); + RzBinSection *stab_sec = stabs_section(bf, ".stab"); + RzBinSection *stabstr_sec = stabs_section(bf, ".stabstr"); + if (!stab_sec || !stabstr_sec) { + return NULL; + } + + ut64 stab_len = 0; + ut8 *stab_buf = stabs_read_section(bf, stab_sec, &stab_len); + if (!stab_buf) { + return NULL; + } + if (stab_len < RZ_BIN_STABS_RECORD_SIZE) { + free(stab_buf); + return NULL; + } + + RzBinStabs *stabs = RZ_NEW0(RzBinStabs); + if (!stabs) { + free(stab_buf); + return NULL; + } + stabs->big_endian = stabs_big_endian(bf); + stabs->str = (char *)stabs_read_section(bf, stabstr_sec, &stabs->str_size); + rz_vector_init(&stabs->entries, sizeof(RzBinStabsEntry), NULL, NULL); + + const bool be = stabs->big_endian; + // .stab must contain a whole number of fixed-size records; a trailing + // partial record (corrupted input) is ignored. + if (stab_len % RZ_BIN_STABS_RECORD_SIZE) { + RZ_LOG_DEBUG("stabs: .stab size %" PFMT64u " is not a multiple of %d, ignoring trailing bytes\n", + stab_len, RZ_BIN_STABS_RECORD_SIZE); + } + const size_t count = stab_len / RZ_BIN_STABS_RECORD_SIZE; + // stab_len is already bounded by the file size (see stabs_read_section), so + // only cap the up-front reservation to avoid a large allocation from a + // crafted section size; the vector still grows to hold every record. + rz_vector_reserve(&stabs->entries, RZ_MIN(count, 0x1000)); + + // Base offset into the string table for the current compilation unit. STABS + // strings are split into per-unit slices: each unit opens with an N_UNDF + // header record whose `value` field holds the byte size of that unit's + // slice, so the next unit's strings start right after it. + ut64 str_base = 0; + ut64 unit_str_size = 0; + for (size_t i = 0; i < count; i++) { + const ut8 *p = stab_buf + i * RZ_BIN_STABS_RECORD_SIZE; + RzBinStabsEntry e = { 0 }; + e.strx = rz_read_at_ble32(p, 0, be); + e.type = p[4]; + e.other = p[5]; + e.desc = rz_read_at_ble16(p, 6, be); + e.value = rz_read_at_ble32(p, 8, be); + if (e.type == RZ_BIN_STABS_N_UNDF) { + // new compilation unit: advance the base past the previous unit's + // slice and remember the size of this one (from its header value) + str_base += unit_str_size; + unit_str_size = e.value; + } + ut64 off = str_base + e.strx; + e.string = (e.strx && stabs->str && off < stabs->str_size) + ? stabs->str + off + : NULL; + rz_vector_push(&stabs->entries, &e); + } + free(stab_buf); + return stabs; +} + +/** + * \brief Free a \ref RzBinStabs and everything it owns + * + * \param stabs The object to free, may be NULL + */ +RZ_API void rz_bin_stabs_free(RZ_NULLABLE RzBinStabs *stabs) { + if (!stabs) { + return; + } + rz_vector_fini(&stabs->entries); + free(stabs->str); + free(stabs); +} + +/** + * \brief Build source line information from parsed STABS data + * + * Only the records relevant to line information are used: N_SO selects the + * current source file, N_FUN gives the base address of the current function and + * N_SLINE carries a line number together with an offset relative to that + * function. The result can be merged into RzBinObject.lines just like the + * DWARF line program output. + * + * \param stabs The parsed STABS data + * \return A newly allocated RzBinSourceLineInfo owned by the caller, or NULL on + * allocation failure + */ +RZ_API RZ_OWN RzBinSourceLineInfo *rz_bin_stabs_source_line_info(RZ_NONNULL const RzBinStabs *stabs) { + rz_return_val_if_fail(stabs, NULL); + RzBinSourceLineInfoBuilder builder; + rz_bin_source_line_info_builder_init(&builder); + + char *comp_dir = NULL; // directory carried by a trailing-'/' N_SO + char *cur_file = NULL; // full path of the current source file + ut64 func_base = 0; // address of the current N_FUN + bool have_func = false; + + RzBinStabsEntry *e; + rz_vector_foreach (&stabs->entries, e) { + switch (e->type) { + case RZ_BIN_STABS_N_SO: + if (RZ_STR_ISEMPTY(e->string)) { + // closing N_SO: the value is the first address no longer + // covered, emit a closing sample like DW_LNE_end_sequence + rz_bin_source_line_info_builder_push_sample(&builder, e->value, 0, 0, NULL); + RZ_FREE(cur_file); + RZ_FREE(comp_dir); + have_func = false; + func_base = 0; + } else if (rz_str_endswith(e->string, "/")) { + free(comp_dir); + comp_dir = rz_str_dup(e->string); + } else { + free(cur_file); + cur_file = comp_dir + ? rz_str_newf("%s%s", comp_dir, e->string) + : rz_str_dup(e->string); + } + break; + case RZ_BIN_STABS_N_FUN: + if (RZ_STR_ISNOTEMPTY(e->string)) { + // a function definition: subsequent N_SLINE offsets are + // relative to this address + func_base = e->value; + have_func = true; + } + break; + case RZ_BIN_STABS_N_SLINE: { + ut64 addr = have_func ? func_base + e->value : e->value; + rz_bin_source_line_info_builder_push_sample(&builder, addr, e->desc, 0, cur_file); + break; + } + default: + break; + } + } + + free(cur_file); + free(comp_dir); + return rz_bin_source_line_info_builder_build_and_fini(&builder); +} + +/* ------------------------------------------------------------------------- * + * Symbol, type and variable extraction * + * ------------------------------------------------------------------------- */ + +/* + * The higher level information lives in the record strings, encoded with the + * STABS type-descriptor grammar. It is a recursive (context-free) grammar: + * aggregates nest arbitrarily (a struct member may be a pointer to an array of + * another struct, ...), so it is handled with a recursive-descent parser, the + * same approach rizin already uses for its DWARF and C/C++ type parsers. A + * rough sketch of the grammar: + * + * type := typenum [ '=' typedef ] + * typenum := '(' int ',' int ')' | int + * typedef := range | pointer | array | struct | union | enum | xref | func | type + * range := 'r' typenum ';' int ';' int ';' + * pointer := '*' type + * array := 'ar' type ';' int ';' int ';' type + * struct := 's' size { member } ';' ( union := 'u' size { member } ';' ) + * member := name ':' type ',' bitoff ',' bitsize ';' + * enum := 'e' { name ':' int ',' } ';' + * xref := 'x' ('s'|'u'|'e') name ':' + * func := 'f' type + * + * See https://sourceware.org/gdb/onlinedocs/stabs.html for the full grammar. + */ + +/// Encode a STABS type number (file, number) into a single key. +#define STABS_TYPE_KEY(file, num) (((ut64)(ut32)(file) << 32) | (ut32)(num)) + +/// Upper bound on the type-descriptor recursion, to reject pathologically +/// nested types from a crafted binary without overflowing the stack. +#define STABS_MAX_TYPE_DEPTH 64 + +typedef struct { + RzTypeDB *typedb; + HtUP /**/ *types; ///< canonical RzType per STABS type id, owned by the table + ut32 anon_counter; ///< used to name untagged structs, unions and enums + ut32 depth; ///< current type-descriptor recursion depth +} StabsTypeParser; + +static RzType *stabs_type_parse(StabsTypeParser *tp, const char **pp); + +static RzType *stabs_ident(const char *name, RzTypeIdentifierKind kind) { + RzType *t = RZ_NEW0(RzType); + if (!t) { + return NULL; + } + t->kind = RZ_TYPE_KIND_IDENTIFIER; + t->identifier.kind = kind; + t->identifier.name = rz_str_dup(name); + t->identifier.is_const = false; + return t; +} + +/// Parse a type number "(file,num)" or a bare "num", advancing \p pp. +static bool stabs_typenum(const char **pp, ut64 *key) { + const char *p = *pp; + if (*p == '(') { + p++; + char *end = NULL; + long file = strtol(p, &end, 10); + if (end == p || *end != ',') { + return false; + } + p = end + 1; + long num = strtol(p, &end, 10); + if (end == p || *end != ')') { + return false; + } + *key = STABS_TYPE_KEY(file, num); + *pp = end + 1; + return true; + } + if (IS_DIGIT(*p)) { + char *end = NULL; + long num = strtol(p, &end, 10); + if (end == p) { + return false; + } + *key = STABS_TYPE_KEY(0, num); + *pp = end; + return true; + } + return false; +} + +/// Parse a range descriptor "r;lo;hi;" (already past 'r') and derive +/// the size, signedness and whether it is a floating point type. +static void stabs_parse_range(const char **pp, ut64 *size_bits, bool *is_signed, bool *is_float) { + const char *p = *pp; + ut64 dummy = 0; + stabs_typenum(&p, &dummy); + if (*p == ';') { + p++; + } + const char *lo = p; + bool lo_neg = (*p == '-'); + while (*p && *p != ';') { + p++; + } + size_t lo_len = p - lo; + if (*p == ';') { + p++; + } + const char *hi = p; + bool hi_neg = (*p == '-'); + while (*p && *p != ';') { + p++; + } + size_t hi_len = p - hi; + if (*p == ';') { + p++; + } + *pp = p; + + *is_float = false; + *is_signed = lo_neg; + // Floating point is encoded as "r(ref);bytes;0;". + if (hi_len == 1 && hi[0] == '0' && !lo_neg && lo_len > 0 && !(lo_len == 1 && lo[0] == '0')) { + *is_float = true; + *is_signed = true; + *size_bits = (ut64)strtoull(lo, NULL, 10) * 8; + return; + } + // "0;-1" is GCC's unsigned with no fixed upper bound; treat as 4 byte unsigned. + if (!lo_neg && lo_len == 1 && lo[0] == '0' && hi_neg) { + *is_signed = false; + *size_bits = 32; + return; + } + ut64 hival = hi_neg ? 0 : (ut64)strtoull(hi, NULL, 10); + ut64 bytes; + if (lo_neg) { + bytes = hival <= 0x7fULL ? 1 : hival <= 0x7fffULL ? 2 + : hival <= 0x7fffffffULL ? 4 + : 8; + *is_signed = true; + } else { + bytes = hival <= 0xffULL ? 1 : hival <= 0xffffULL ? 2 + : hival <= 0xffffffffULL ? 4 + : 8; + *is_signed = false; + } + *size_bits = bytes * 8; +} + +static const char *stabs_atomic_name(ut64 size_bits, bool is_signed, bool is_float) { + if (is_float) { + switch (size_bits) { + case 32: return "float"; + case 64: return "double"; + default: return "long double"; + } + } + if (is_signed) { + switch (size_bits) { + case 8: return "char"; + case 16: return "short"; + case 32: return "int"; + default: return "long"; + } + } + switch (size_bits) { + case 8: return "unsigned char"; + case 16: return "unsigned short"; + case 32: return "unsigned int"; + default: return "unsigned long"; + } +} + +/// Register an atomic base type (unless one with that name already exists) and +/// return a fresh identifier referencing it. +static RzType *stabs_atomic(StabsTypeParser *tp, const char *name, ut64 size_bits) { + if (!rz_type_db_get_base_type(tp->typedb, name)) { + RzBaseType *bt = rz_type_base_type_new(RZ_BASE_TYPE_KIND_ATOMIC); + if (bt) { + bt->name = rz_str_dup(name); + bt->size = size_bits; + bt->type = stabs_ident(name, RZ_TYPE_IDENTIFIER_KIND_UNSPECIFIED); + rz_type_db_save_base_type(tp->typedb, bt); + } + } + return stabs_ident(name, RZ_TYPE_IDENTIFIER_KIND_UNSPECIFIED); +} + +/// Register a typedef "name" aliasing \p target and return identifier(name). +static RzType *stabs_make_typedef(StabsTypeParser *tp, const char *name, const RzType *target) { + if (!rz_type_db_get_base_type(tp->typedb, name)) { + RzBaseType *bt = rz_type_base_type_new(RZ_BASE_TYPE_KIND_TYPEDEF); + if (bt) { + bt->name = rz_str_dup(name); + bt->type = target ? rz_type_clone(target) : stabs_ident("void", RZ_TYPE_IDENTIFIER_KIND_UNSPECIFIED); + rz_type_db_save_base_type(tp->typedb, bt); + } + } + return stabs_ident(name, RZ_TYPE_IDENTIFIER_KIND_UNSPECIFIED); +} + +/// Parse a struct/union body "...;", already past 's'/'u'. +static RzType *stabs_struct_union(StabsTypeParser *tp, const char **pp, const char *name, bool is_struct) { + const char *p = *pp; + char *end = NULL; + ut64 size_bytes = (ut64)strtoull(p, &end, 10); + p = end; + + char synth[64]; + if (!name) { + rz_strf(synth, "anonymous %s %u", is_struct ? "struct" : "union", tp->anon_counter++); + name = synth; + } + + RzTypeIdentifierKind ik = is_struct ? RZ_TYPE_IDENTIFIER_KIND_STRUCT : RZ_TYPE_IDENTIFIER_KIND_UNION; + if (name && rz_type_db_get_base_type(tp->typedb, name)) { + // already defined: skip the body and just reference it + while (*p && *p != ';') { + p++; + } + if (*p == ';') { + p++; + } + *pp = p; + return stabs_ident(name, ik); + } + + RzBaseType *bt = rz_type_base_type_new(is_struct ? RZ_BASE_TYPE_KIND_STRUCT : RZ_BASE_TYPE_KIND_UNION); + if (!bt) { + *pp = p; + return NULL; + } + bt->name = rz_str_dup(name); + bt->size = size_bytes * 8; + + while (*p && *p != ';') { + const char *ms = p; + while (*p && *p != ':') { + p++; + } + char *mname = rz_str_ndup(ms, p - ms); + if (*p == ':') { + p++; + } + RzType *mtype = stabs_type_parse(tp, &p); + ut64 bitoff = 0; + if (*p == ',') { + p++; + bitoff = (ut64)strtoull(p, &end, 10); + p = end; + } + if (*p == ',') { + p++; + (void)strtoull(p, &end, 10); // bit size, unused for non-bitfields + p = end; + } + if (*p == ';') { + p++; + } + if (is_struct) { + RzTypeStructMember member = { .name = mname, .type = mtype, .offset = bitoff / 8, .size = 0 }; + rz_vector_push(&bt->struct_data.members, &member); + } else { + RzTypeUnionMember member = { .name = mname, .type = mtype, .offset = bitoff / 8, .size = 0 }; + rz_vector_push(&bt->union_data.members, &member); + } + } + if (*p == ';') { + p++; + } + *pp = p; + + RzType *ident = stabs_ident(bt->name, ik); + rz_type_db_save_base_type(tp->typedb, bt); + return ident; +} + +/// Parse an enum body "name:val,...;", already past 'e'. +static RzType *stabs_enum(StabsTypeParser *tp, const char **pp, const char *name) { + const char *p = *pp; + char synth[64]; + if (!name) { + rz_strf(synth, "anonymous enum %u", tp->anon_counter++); + name = synth; + } + if (name && rz_type_db_get_base_type(tp->typedb, name)) { + while (*p && *p != ';') { + p++; + } + if (*p == ';') { + p++; + } + *pp = p; + return stabs_ident(name, RZ_TYPE_IDENTIFIER_KIND_ENUM); + } + + RzBaseType *bt = rz_type_base_type_new(RZ_BASE_TYPE_KIND_ENUM); + if (!bt) { + *pp = p; + return NULL; + } + bt->name = rz_str_dup(name); + bt->size = 32; + bt->type = stabs_ident("int", RZ_TYPE_IDENTIFIER_KIND_UNSPECIFIED); + + char *end = NULL; + while (*p && *p != ';') { + const char *cs = p; + while (*p && *p != ':') { + p++; + } + char *cname = rz_str_ndup(cs, p - cs); + if (*p == ':') { + p++; + } + st64 val = (st64)strtoll(p, &end, 10); + p = end; + if (*p == ',') { + p++; + } + RzTypeEnumCase ec = { .name = cname, .val = val }; + rz_vector_push(&bt->enum_data.cases, &ec); + } + if (*p == ';') { + p++; + } + *pp = p; + + RzType *ident = stabs_ident(bt->name, RZ_TYPE_IDENTIFIER_KIND_ENUM); + rz_type_db_save_base_type(tp->typedb, bt); + return ident; +} + +/// Parse a type definition that follows '=' and register the canonical type +/// for \p key. Returns an owned RzType. +static RzType *stabs_type_def(StabsTypeParser *tp, const char **pp, ut64 key, const char *name) { + const char *p = *pp; + RzType *t = NULL; + char d = *p; + + if (d == '(' || d == '-' || IS_DIGIT(d)) { + RzType *other = stabs_type_parse(tp, &p); + if (name) { + t = stabs_make_typedef(tp, name, other); + rz_type_free(other); + } else { + t = other; + } + } else if (!d) { + // The descriptor string ended prematurely (truncated or malformed + // input): treat the missing type as void, without advancing past the + // NUL terminator so the caller does not read out of bounds. + t = stabs_ident("void", RZ_TYPE_IDENTIFIER_KIND_UNSPECIFIED); + } else { + p++; // consume the descriptor character + switch (d) { + case 'r': { + ut64 sz = 32; + bool sg = true, fl = false; + stabs_parse_range(&p, &sz, &sg, &fl); + t = stabs_atomic(tp, name ? name : stabs_atomic_name(sz, sg, fl), sz); + break; + } + case '*': { + RzType *inner = stabs_type_parse(tp, &p); + t = RZ_NEW0(RzType); + if (t) { + t->kind = RZ_TYPE_KIND_POINTER; + t->pointer.type = inner; + t->pointer.is_const = false; + } else { + rz_type_free(inner); + } + break; + } + case 'a': { + if (*p == 'r') { + p++; // "ar" + } + RzType *idx = stabs_type_parse(tp, &p); + rz_type_free(idx); + char *end = NULL; + if (*p == ';') { + p++; + } + st64 lo = (st64)strtoll(p, &end, 10); + p = end; + if (*p == ';') { + p++; + } + st64 hi = (st64)strtoll(p, &end, 10); + p = end; + if (*p == ';') { + p++; + } + RzType *elem = stabs_type_parse(tp, &p); + t = RZ_NEW0(RzType); + if (t) { + t->kind = RZ_TYPE_KIND_ARRAY; + t->array.type = elem; + t->array.count = (hi >= lo) ? (ut64)(hi - lo + 1) : 0; + } else { + rz_type_free(elem); + } + break; + } + case 's': + case 'u': + t = stabs_struct_union(tp, &p, name, d == 's'); + break; + case 'e': + t = stabs_enum(tp, &p, name); + break; + case 'x': { + char tag = *p; + if (tag) { + p++; + } + const char *xs = p; + while (*p && *p != ':') { + p++; + } + char *xname = rz_str_ndup(xs, p - xs); + if (*p == ':') { + p++; + } + RzTypeIdentifierKind ik = tag == 's' ? RZ_TYPE_IDENTIFIER_KIND_STRUCT + : tag == 'u' ? RZ_TYPE_IDENTIFIER_KIND_UNION + : tag == 'e' ? RZ_TYPE_IDENTIFIER_KIND_ENUM + : RZ_TYPE_IDENTIFIER_KIND_UNSPECIFIED; + t = stabs_ident(xname, ik); + free(xname); + break; + } + case 'f': + t = stabs_type_parse(tp, &p); // function returning type, approximated as the return type + break; + default: + t = stabs_ident("void", RZ_TYPE_IDENTIFIER_KIND_UNSPECIFIED); + break; + } + } + + *pp = p; + if (t && key) { + ht_up_update(tp->types, key, rz_type_clone(t)); + } + return t; +} + +static RzType *stabs_type_parse(StabsTypeParser *tp, const char **pp) { + if (tp->depth >= STABS_MAX_TYPE_DEPTH) { + return stabs_ident("void", RZ_TYPE_IDENTIFIER_KIND_UNSPECIFIED); + } + tp->depth++; + const char *p = *pp; + ut64 key = 0; + RzType *t = NULL; + if (stabs_typenum(&p, &key)) { + if (*p == '=') { + p++; + t = stabs_type_def(tp, &p, key, NULL); + } else { + RzType *canon = ht_up_find(tp->types, key, NULL); + t = canon ? rz_type_clone(canon) : stabs_ident("void", RZ_TYPE_IDENTIFIER_KIND_UNSPECIFIED); + } + } else { + // no type number: a descriptor follows directly + t = stabs_type_def(tp, &p, 0, NULL); + } + *pp = p; + tp->depth--; + return t; +} + +static void stabs_symbol_free(RzBinStabsSymbol *sym) { + if (!sym) { + return; + } + free(sym->name); + rz_type_free(sym->type); + free(sym); +} + +static void stabs_add_symbol(RzBinStabsDebugInfo *di, const char *name, RzBinStabsSymbolKind kind, RZ_OWN RzType *type, ut64 value, ut64 function) { + RzBinStabsSymbol *sym = RZ_NEW0(RzBinStabsSymbol); + if (!sym) { + rz_type_free(type); + return; + } + sym->name = rz_str_dup(name); + sym->kind = kind; + sym->type = type; + sym->value = value; + sym->function = function; + rz_pvector_push(&di->symbols, sym); +} + +static void stabs_finalize_callable(RzTypeDB *typedb, RzCallable **callable) { + if (!*callable) { + return; + } + if (!rz_type_func_save(typedb, *callable)) { + rz_type_callable_free(*callable); + } + *callable = NULL; +} + +static void stabs_process_symbol(StabsTypeParser *tp, RzBinStabsDebugInfo *di, const RzBinStabsEntry *e, ut64 *cur_func, RzCallable **callable) { + const char *s = e->string; + const char *colon = strchr(s, ':'); + if (!colon) { + return; + } + char *name = (colon == s) ? NULL : rz_str_ndup(s, colon - s); + const char *p = colon + 1; + char desc = *p; + + // A missing descriptor letter (the type number starts right away) denotes a + // local variable. + if (desc == '(' || desc == '-' || IS_DIGIT(desc)) { + RzType *t = stabs_type_parse(tp, &p); + if (name) { + stabs_add_symbol(di, name, RZ_BIN_STABS_SYMBOL_LOCAL, t, e->value, *cur_func); + } else { + rz_type_free(t); + } + free(name); + return; + } + + p++; // consume the descriptor letter + switch (desc) { + case 't': { // typedef or named atomic type + ut64 key = 0; + if (stabs_typenum(&p, &key)) { + if (*p == '=') { + p++; + rz_type_free(stabs_type_def(tp, &p, key, name)); + } else { + RzType *target = ht_up_find(tp->types, key, NULL); + if (name && target) { + rz_type_free(stabs_make_typedef(tp, name, target)); + } + } + } + break; + } + case 'T': { // struct/union/enum tag + ut64 key = 0; + if (stabs_typenum(&p, &key) && *p == '=') { + p++; + rz_type_free(stabs_type_def(tp, &p, key, name)); + } + break; + } + case 'G': { // global variable (address comes from the symbol table) + RzType *t = stabs_type_parse(tp, &p); + if (name) { + stabs_add_symbol(di, name, RZ_BIN_STABS_SYMBOL_GLOBAL, t, 0, 0); + } else { + rz_type_free(t); + } + break; + } + case 'S': + case 'V': { // file or function scope static + RzType *t = stabs_type_parse(tp, &p); + if (name) { + stabs_add_symbol(di, name, RZ_BIN_STABS_SYMBOL_STATIC, t, e->value, 0); + } else { + rz_type_free(t); + } + break; + } + case 'F': + case 'f': { // function + stabs_finalize_callable(tp->typedb, callable); + RzType *ret = stabs_type_parse(tp, &p); + *cur_func = e->value; + if (name) { + stabs_add_symbol(di, name, RZ_BIN_STABS_SYMBOL_FUNCTION, rz_type_clone(ret), e->value, 0); + *callable = rz_type_callable_new(name); + if (*callable) { + (*callable)->ret = ret; + ret = NULL; + } + } + rz_type_free(ret); + break; + } + case 'p': + case 'P': { // parameter + RzType *t = stabs_type_parse(tp, &p); + if (name) { + stabs_add_symbol(di, name, RZ_BIN_STABS_SYMBOL_PARAMETER, rz_type_clone(t), e->value, *cur_func); + if (*callable) { + RzCallableArg *arg = rz_type_callable_arg_new(tp->typedb, name, t); + t = NULL; + if (arg) { + rz_type_callable_arg_add(*callable, arg); + } + } + } + rz_type_free(t); + break; + } + case 'r': { // register variable + RzType *t = stabs_type_parse(tp, &p); + if (name) { + stabs_add_symbol(di, name, RZ_BIN_STABS_SYMBOL_LOCAL, t, e->value, *cur_func); + } else { + rz_type_free(t); + } + break; + } + default: + break; + } + free(name); +} + +/** + * \brief Recover symbols, types and variables from parsed STABS data + * + * Walks the records and decodes the STABS type-descriptor grammar. Types and + * function prototypes are registered directly into \p typedb; the functions, + * global/static variables, parameters and local variables are returned so the + * caller can turn them into analysis objects. + * + * \param stabs The parsed STABS data + * \param typedb The type database the recovered types and prototypes are added to + * \return A newly allocated \ref RzBinStabsDebugInfo owned by the caller, or NULL + * on allocation failure + */ +RZ_API RZ_OWN RzBinStabsDebugInfo *rz_bin_stabs_debug_info(RZ_NONNULL const RzBinStabs *stabs, RZ_NONNULL RzTypeDB *typedb) { + rz_return_val_if_fail(stabs && typedb, NULL); + RzBinStabsDebugInfo *di = RZ_NEW0(RzBinStabsDebugInfo); + if (!di) { + return NULL; + } + rz_pvector_init(&di->symbols, (RzPVectorFree)stabs_symbol_free); + + StabsTypeParser tp = { + .typedb = typedb, + .types = ht_up_new(NULL, (HtUPFreeValue)rz_type_free) + }; + if (!tp.types) { + rz_bin_stabs_debug_info_free(di); + return NULL; + } + + ut64 cur_func = 0; + RzCallable *callable = NULL; + RzBinStabsEntry *e; + rz_vector_foreach (&stabs->entries, e) { + if (RZ_STR_ISEMPTY(e->string)) { + continue; + } + switch (e->type) { + case RZ_BIN_STABS_N_GSYM: + case RZ_BIN_STABS_N_STSYM: + case RZ_BIN_STABS_N_LCSYM: + case RZ_BIN_STABS_N_FUN: + case RZ_BIN_STABS_N_LSYM: + case RZ_BIN_STABS_N_PSYM: + case RZ_BIN_STABS_N_RSYM: + stabs_process_symbol(&tp, di, e, &cur_func, &callable); + break; + default: + break; + } + } + stabs_finalize_callable(typedb, &callable); + ht_up_free(tp.types); + return di; +} + +/** + * \brief Free a \ref RzBinStabsDebugInfo and everything it owns + * + * \param di The object to free, may be NULL + */ +RZ_API void rz_bin_stabs_debug_info_free(RZ_NULLABLE RzBinStabsDebugInfo *di) { + if (!di) { + return; + } + rz_pvector_fini(&di->symbols); + free(di); +} diff --git a/librz/core/cbin.c b/librz/core/cbin.c index 573b85542d..9f68c004f8 100644 --- a/librz/core/cbin.c +++ b/librz/core/cbin.c @@ -208,6 +208,7 @@ RZ_API bool rz_core_bin_apply_info(RzCore *r, RzBinFile *binfile, ut32 mask) { } if (mask & RZ_CORE_BIN_ACC_DWARF) { rz_core_bin_apply_dwarf(r, binfile); + rz_core_bin_apply_stabs(r, binfile); } if (mask & RZ_CORE_BIN_ACC_LUAC_DEBUG) { rz_core_bin_apply_luac_debug(r, binfile); @@ -697,6 +698,126 @@ RZ_API bool rz_core_bin_apply_dwarf(RzCore *core, RzBinFile *binfile) { return true; } +/** + * \brief Parse STABS debug information and apply its source line info + * + * STABS is the legacy debug format that predates DWARF and can still be found in + * old binaries (and in objects produced by GCC <= 12 with -gstabs). Only the + * source line information is consumed here; it is merged into the binary's line + * info so that commands such as \p ix and source-aware disassembly work the same + * way they do for DWARF. + * Look up the virtual address of a named symbol in the binary's symbol table. + * STABS global symbols carry no address of their own, so it is recovered here. + */ +static ut64 stabs_global_vaddr(RzBinFile *binfile, const char *name) { + if (!binfile->o || !binfile->o->symbols) { + return UT64_MAX; + } + void **it; + rz_pvector_foreach (binfile->o->symbols, it) { + RzBinSymbol *sym = *it; + if (RZ_STR_EQ(sym->name, name)) { + return sym->vaddr; + } + } + return UT64_MAX; +} + +/// Turn a single recovered STABS variable into an analysis global variable. +/// Returns true if a global variable was created. +static bool stabs_apply_global(RzCore *core, RzBinFile *binfile, const RzBinStabsSymbol *sym) { + if (!sym->type) { + return false; + } + ut64 addr; + if (sym->kind == RZ_BIN_STABS_SYMBOL_STATIC) { + addr = sym->value; + } else if (sym->kind == RZ_BIN_STABS_SYMBOL_GLOBAL) { + // a STABS global carries no address of its own, recover it from the symbol table + addr = stabs_global_vaddr(binfile, sym->name); + if (addr == UT64_MAX) { + return false; + } + } else { + return false; + } + return rz_analysis_var_global_create(core->analysis, sym->name, rz_type_clone(sym->type), addr); +} + +/// Merge the source line information recovered from STABS into the bin object. +static bool stabs_apply_line_info(RzBinFile *binfile, RzBinStabs *stabs) { + RzBinSourceLineInfo *li = rz_bin_stabs_source_line_info(stabs); + if (!li) { + return false; + } + bool applied = false; + if (li->samples_count) { + if (!binfile->o->lines) { + binfile->o->lines = RZ_NEW0(RzBinSourceLineInfo); + if (binfile->o->lines) { + rz_str_constpool_init(&binfile->o->lines->filename_pool); + } + } + if (binfile->o->lines) { + rz_bin_source_line_info_merge(binfile->o->lines, li); + applied = true; + } + } + rz_bin_source_line_info_free(li); + return applied; +} + +/// Recover types, function signatures and global variables from STABS. The types +/// and the function prototypes are registered into the type database by the +/// extractor; only the global and static variables are applied here. +static bool stabs_apply_debug_info(RzCore *core, RzBinFile *binfile, RzBinStabs *stabs) { + RzTypeDB *typedb = rz_analysis_get_type_db(core->analysis); + if (!typedb) { + return false; + } + RzBinStabsDebugInfo *di = rz_bin_stabs_debug_info(stabs, typedb); + if (!di) { + return false; + } + bool applied = false; + void **it; + rz_pvector_foreach (&di->symbols, it) { + if (stabs_apply_global(core, binfile, *it)) { + applied = true; + } + } + rz_bin_stabs_debug_info_free(di); + return applied; +} + +/** + * \brief Recover STABS debug information from a binary into the analysis + * + * Applies the source line information, registers the recovered types and + * function prototypes into the type database and creates analysis global + * variables for the recovered globals and statics. + * + * \param core The current core, whose analysis receives the recovered information + * \param binfile The binary file to read the STABS sections from + * \return true if any information was applied, false otherwise + */ +RZ_API bool rz_core_bin_apply_stabs(RzCore *core, RzBinFile *binfile) { + rz_return_val_if_fail(core && core->analysis && binfile, false); + if (!rz_config_get_bool(core->config, "bin.dbginfo") || !binfile->o) { + return false; + } + RzBinStabs *stabs = rz_bin_stabs_parse(binfile); + if (!stabs) { + return false; + } + bool applied = stabs_apply_line_info(binfile, stabs); + if (stabs_apply_debug_info(core, binfile, stabs)) { + applied = true; + } + rz_bin_stabs_free(stabs); + return applied; +} + static inline bool is_initfini(RzBinAddr *entry) { switch (entry->type) { case RZ_BIN_ENTRY_TYPE_INIT: diff --git a/librz/include/rz_bin.h b/librz/include/rz_bin.h index 1c3f22494b..ab8db43b72 100644 --- a/librz/include/rz_bin.h +++ b/librz/include/rz_bin.h @@ -18,6 +18,7 @@ typedef struct rz_bin_file_t RzBinFile; typedef struct rz_bin_reloc_storage_t RzBinRelocStorage; #include +#include #include #ifdef __cplusplus diff --git a/librz/include/rz_bin_stabs.h b/librz/include/rz_bin_stabs.h new file mode 100644 index 0000000000..8cdfbbb130 --- /dev/null +++ b/librz/include/rz_bin_stabs.h @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2026 RizinOrg +// SPDX-License-Identifier: LGPL-3.0-only + +#ifndef RZ_BIN_STABS_H +#define RZ_BIN_STABS_H + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * \file rz_bin_stabs.h + * \brief Parser for the STABS debugging information format. + * + * STABS (symbol table strings) is the legacy debug format emitted by old + * toolchains (and by GCC up to version 12 with -gstabs). The information is + * stored as an array of fixed 12-byte records in the `.stab` section, with the + * strings kept in the separate `.stabstr` section. + * + * See https://sourceware.org/gdb/onlinedocs/stabs.html for the format details. + */ + +/** + * \brief STABS symbol descriptor types (the on-disk `n_type` field). + * + * The values are fixed by the format. + * See https://sourceware.org/gdb/onlinedocs/stabs.html for details. + */ +typedef enum { + RZ_BIN_STABS_N_UNDF = 0x00, ///< Header record (and undefined entries) + RZ_BIN_STABS_N_GSYM = 0x20, ///< Global symbol + RZ_BIN_STABS_N_FNAME = 0x22, ///< Function name (BSD Fortran) + RZ_BIN_STABS_N_FUN = 0x24, ///< Function + RZ_BIN_STABS_N_STSYM = 0x26, ///< Static symbol (data segment) + RZ_BIN_STABS_N_LCSYM = 0x28, ///< Static symbol (bss segment) + RZ_BIN_STABS_N_MAIN = 0x2a, ///< Name of main routine + RZ_BIN_STABS_N_OPT = 0x3c, ///< Compiler options marker + RZ_BIN_STABS_N_RSYM = 0x40, ///< Register variable + RZ_BIN_STABS_N_SLINE = 0x44, ///< Source line number in the text segment + RZ_BIN_STABS_N_DSLINE = 0x46, ///< Source line number in the data segment + RZ_BIN_STABS_N_BSLINE = 0x48, ///< Source line number in the bss segment + RZ_BIN_STABS_N_SO = 0x64, ///< Source file name + RZ_BIN_STABS_N_LSYM = 0x80, ///< Local symbol or type definition + RZ_BIN_STABS_N_BINCL = 0x82, ///< Beginning of an include file + RZ_BIN_STABS_N_SOL = 0x84, ///< Name of an included source file + RZ_BIN_STABS_N_PSYM = 0xa0, ///< Function parameter + RZ_BIN_STABS_N_EINCL = 0xa2, ///< End of an include file + RZ_BIN_STABS_N_LBRAC = 0xc0, ///< Beginning of a lexical block (scope) + RZ_BIN_STABS_N_EXCL = 0xc2, ///< Placeholder for a deleted include file + RZ_BIN_STABS_N_RBRAC = 0xe0, ///< End of a lexical block (scope) +} RzBinStabsType; + +/** Size in bytes of a single on-disk STABS record. */ +#define RZ_BIN_STABS_RECORD_SIZE 12 + +/** + * \brief A single decoded STABS record. + * + * On disk every record is a fixed 12-byte little/big-endian structure laid out + * as `strx` (4 bytes), `type` (1 byte), `other` (1 byte), `desc` (2 bytes) and + * `value` (4 bytes). The fields are independent; the values below are the + * already decoded fields. + */ +typedef struct rz_bin_stabs_entry_t { + ut32 strx; ///< Offset of the associated string inside the current unit's string table slice + RzBinStabsType type; ///< Symbol descriptor type + ut8 other; ///< Reserved field, usually zero + ut16 desc; ///< Description field, e.g. the line number for N_SLINE records + ut64 value; ///< An address, a function-relative offset or, for an N_UNDF header, the byte size of the unit's string table slice + RZ_NULLABLE const char *string; ///< The resolved string, borrowed from RzBinStabs.str (may be NULL) +} RzBinStabsEntry; + +/** + * \brief All STABS information parsed out of a binary. + */ +typedef struct rz_bin_stabs_t { + RzVector /**/ entries; ///< The decoded records, in file order + char *str; ///< Copy of the `.stabstr` string table + ut64 str_size; ///< Size of the string table in bytes + bool big_endian; ///< Endianness used to decode the records +} RzBinStabs; + +/** + * \brief The kind of program object a STABS symbol describes. + */ +typedef enum { + RZ_BIN_STABS_SYMBOL_FUNCTION, ///< A function (descriptor 'F' or 'f') + RZ_BIN_STABS_SYMBOL_GLOBAL, ///< A global variable (descriptor 'G') + RZ_BIN_STABS_SYMBOL_STATIC, ///< A static variable (descriptor 'S'/'V', or N_LCSYM) + RZ_BIN_STABS_SYMBOL_PARAMETER, ///< A function parameter (descriptor 'p'/'P') + RZ_BIN_STABS_SYMBOL_LOCAL, ///< A function local variable +} RzBinStabsSymbolKind; + +/** + * \brief A symbol (function, variable or parameter) recovered from STABS. + */ +typedef struct rz_bin_stabs_symbol_t { + char *name; ///< Symbol name + RzBinStabsSymbolKind kind; ///< What the symbol describes + RZ_NULLABLE RzType *type; ///< Resolved type, owned by this symbol (may be NULL) + ut64 value; ///< Absolute address for functions/globals/statics, frame offset for parameters/locals + ut64 function; ///< For parameters and locals: address of the enclosing function +} RzBinStabsSymbol; + +/** + * \brief All higher level debug information recovered from STABS. + * + * The types and the function signatures are registered directly into the + * RzTypeDB passed to rz_bin_stabs_debug_info(). The remaining symbols + * (functions, globals, statics, parameters and locals) are returned here so the + * caller can turn them into analysis objects. + */ +typedef struct rz_bin_stabs_debug_info_t { + RzPVector /**/ symbols; ///< Recovered symbols +} RzBinStabsDebugInfo; + +struct rz_bin_file_t; + +RZ_API RZ_OWN RzBinStabs *rz_bin_stabs_parse(RZ_NONNULL struct rz_bin_file_t *bf); +RZ_API void rz_bin_stabs_free(RZ_NULLABLE RzBinStabs *stabs); +RZ_API RZ_OWN RzBinSourceLineInfo *rz_bin_stabs_source_line_info(RZ_NONNULL const RzBinStabs *stabs); +RZ_API RZ_OWN RzBinStabsDebugInfo *rz_bin_stabs_debug_info(RZ_NONNULL const RzBinStabs *stabs, RZ_NONNULL RzTypeDB *typedb); +RZ_API void rz_bin_stabs_debug_info_free(RZ_NULLABLE RzBinStabsDebugInfo *di); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/librz/include/rz_core.h b/librz/include/rz_core.h index edcfa9ae22..00598b0464 100644 --- a/librz/include/rz_core.h +++ b/librz/include/rz_core.h @@ -1015,6 +1015,7 @@ RZ_API bool rz_core_bin_apply_config(RzCore *r, RzBinFile *binfile); RZ_API bool rz_core_bin_apply_maps(RzCore *core, RzBinFile *binfile, bool va); RZ_API bool rz_core_bin_apply_main(RzCore *r, RzBinFile *binfile, bool va); RZ_API bool rz_core_bin_apply_dwarf(RzCore *core, RzBinFile *binfile); +RZ_API bool rz_core_bin_apply_stabs(RzCore *core, RzBinFile *binfile); RZ_API bool rz_core_bin_apply_entry(RzCore *core, RzBinFile *binfile, bool va); RZ_API bool rz_core_bin_apply_sections(RzCore *core, RzBinFile *binfile, bool va); RZ_API bool rz_core_bin_apply_relocs(RzCore *core, RzBinFile *binfile, bool va); diff --git a/test/db/cmd/stabs b/test/db/cmd/stabs new file mode 100644 index 0000000000..7d84dc24f1 --- /dev/null +++ b/test/db/cmd/stabs @@ -0,0 +1,54 @@ +NAME=stabs ELF source line info +FILE=bins/elf/stabs/stabs_hello +CMDS=< +// SPDX-License-Identifier: LGPL-3.0-only + +#include +#include +#include +#include +#include "../unit/minunit.h" + +static RzBinStabsEntry *entry_at(RzBinStabs *s, size_t i) { + return (RzBinStabsEntry *)rz_vector_index_ptr(&s->entries, i); +} + +static RzBinStabsSymbol *find_sym(RzBinStabsDebugInfo *di, const char *name) { + void **it; + rz_pvector_foreach (&di->symbols, it) { + RzBinStabsSymbol *s = *it; + if (RZ_STR_EQ(s->name, name)) { + return s; + } + } + return NULL; +} + +bool test_stabs(void) { + RzBin *bin = rz_bin_new(); + RzIO *io = rz_io_new(); + rz_io_bind(io, &bin->iob); + RzBinOptions opt = { 0 }; + rz_bin_options_init(&opt, 0, 0, 0, false); + RzBinFile *bf = rz_bin_open(bin, "bins/elf/stabs/stabs_hello", &opt); + mu_assert_notnull(bf, "couldn't open file"); + + RzBinStabs *stabs = rz_bin_stabs_parse(bf); + mu_assert_notnull(stabs, "stabs should be parsed"); + + // record parsing + mu_assert_eq(rz_vector_len(&stabs->entries), 24, "stab entry count"); + + // the first record is the compilation unit header + RzBinStabsEntry *hdr = entry_at(stabs, 0); + mu_assert_eq(hdr->type, RZ_BIN_STABS_N_UNDF, "header type"); + + // source file + RzBinStabsEntry *so = entry_at(stabs, 1); + mu_assert_eq(so->type, RZ_BIN_STABS_N_SO, "N_SO type"); + mu_assert_streq(so->string, "stabs_hello.c", "N_SO string"); + mu_assert_eq(so->value, 0x401136, "N_SO value"); + + // function "add" + RzBinStabsEntry *fun_add = entry_at(stabs, 3); + mu_assert_eq(fun_add->type, RZ_BIN_STABS_N_FUN, "N_FUN add type"); + mu_assert_true(rz_str_startswith(fun_add->string, "add:"), "N_FUN add name"); + mu_assert_eq(fun_add->value, 0x401136, "N_FUN add value"); + + // function "main" + RzBinStabsEntry *fun_main = entry_at(stabs, 14); + mu_assert_eq(fun_main->type, RZ_BIN_STABS_N_FUN, "N_FUN main type"); + mu_assert_true(rz_str_startswith(fun_main->string, "main:"), "N_FUN main name"); + mu_assert_eq(fun_main->value, 0x401154, "N_FUN main value"); + + // nine source line records overall + size_t sline = 0; + RzBinStabsEntry *e; + rz_vector_foreach (&stabs->entries, e) { + if (e->type == RZ_BIN_STABS_N_SLINE) { + sline++; + } + } + mu_assert_eq(sline, 9, "N_SLINE count"); + + // line info: N_SLINE offsets are relative to the enclosing function, the + // builder must resolve them to absolute addresses and keep them sorted + RzBinSourceLineInfo *li = rz_bin_stabs_source_line_info(stabs); + mu_assert_notnull(li, "line info should be built"); + mu_assert_eq(li->samples_count, 10, "line samples count"); + + const ut64 addrs[] = { 0x401136, 0x401144, 0x40114f, 0x401152, 0x401154, + 0x401160, 0x401172, 0x40118b, 0x401190, 0x401192 }; + const ut32 lines[] = { 6, 7, 8, 9, 11, 12, 13, 14, 15, 0 }; + for (size_t i = 0; i < 10; i++) { + mu_assert_eq(li->samples[i].address, addrs[i], "sample address"); + mu_assert_eq(li->samples[i].line, lines[i], "sample line"); + } + // every covered sample points at the source file + mu_assert_streq(li->samples[0].file, "stabs_hello.c", "sample file"); + // the trailing sample closes the range, like DW_LNE_end_sequence + mu_assert_true(rz_bin_source_line_sample_is_closing(&li->samples[9]), "closing sample"); + + rz_bin_source_line_info_free(li); + rz_bin_stabs_free(stabs); + rz_io_free(io); + rz_bin_free(bin); + mu_end; +} + +bool test_stabs_extraction(void) { + RzBin *bin = rz_bin_new(); + RzIO *io = rz_io_new(); + rz_io_bind(io, &bin->iob); + RzBinOptions opt = { 0 }; + rz_bin_options_init(&opt, 0, 0, 0, false); + RzBinFile *bf = rz_bin_open(bin, "bins/elf/stabs/stabs_syms", &opt); + mu_assert_notnull(bf, "couldn't open file"); + + RzBinStabs *stabs = rz_bin_stabs_parse(bf); + mu_assert_notnull(stabs, "stabs should be parsed"); + + RzTypeDB *typedb = rz_type_db_new(); + mu_assert_notnull(typedb, "typedb should be created"); + + RzBinStabsDebugInfo *di = rz_bin_stabs_debug_info(stabs, typedb); + mu_assert_notnull(di, "debug info should be extracted"); + + // 3 globals/statics + 3 functions + 4 parameters + 4 locals + mu_assert_eq(rz_pvector_len(&di->symbols), 14, "recovered symbol count"); + + // global and static variables, with their resolved types + RzBinStabsSymbol *g_counter = find_sym(di, "g_counter"); + mu_assert_notnull(g_counter, "g_counter recovered"); + mu_assert_eq(g_counter->kind, RZ_BIN_STABS_SYMBOL_GLOBAL, "g_counter is a global"); + mu_assert_eq(g_counter->type->kind, RZ_TYPE_KIND_IDENTIFIER, "g_counter type is an identifier"); + mu_assert_streq(g_counter->type->identifier.name, "int", "g_counter is an int"); + + RzBinStabsSymbol *s_buffer = find_sym(di, "s_buffer"); + mu_assert_notnull(s_buffer, "s_buffer recovered"); + mu_assert_eq(s_buffer->kind, RZ_BIN_STABS_SYMBOL_STATIC, "s_buffer is a static"); + mu_assert_eq(s_buffer->value, 0x404040, "s_buffer has its own address"); + mu_assert_eq(s_buffer->type->kind, RZ_TYPE_KIND_ARRAY, "s_buffer is an array"); + mu_assert_eq(s_buffer->type->array.count, 16, "s_buffer holds 16 elements"); + + RzBinStabsSymbol *g_origin = find_sym(di, "g_origin"); + mu_assert_notnull(g_origin, "g_origin recovered"); + mu_assert_eq(g_origin->kind, RZ_BIN_STABS_SYMBOL_GLOBAL, "g_origin is a global"); + mu_assert_streq(g_origin->type->identifier.name, "point", "g_origin is a struct point"); + mu_assert_eq(g_origin->type->identifier.kind, RZ_TYPE_IDENTIFIER_KIND_STRUCT, "g_origin identifier is a struct"); + + // functions, with return types and addresses + RzBinStabsSymbol *add = find_sym(di, "add"); + mu_assert_notnull(add, "add recovered"); + mu_assert_eq(add->kind, RZ_BIN_STABS_SYMBOL_FUNCTION, "add is a function"); + mu_assert_eq(add->value, 0x401136, "add address"); + mu_assert_streq(add->type->identifier.name, "int", "add returns an int"); + + RzBinStabsSymbol *scale = find_sym(di, "scale"); + mu_assert_notnull(scale, "scale recovered"); + mu_assert_eq(scale->value, 0x401154, "scale address"); + mu_assert_streq(scale->type->identifier.name, "long", "scale returns a long"); + + // parameters, linked back to their enclosing function + RzBinStabsSymbol *a = find_sym(di, "a"); + mu_assert_notnull(a, "parameter a recovered"); + mu_assert_eq(a->kind, RZ_BIN_STABS_SYMBOL_PARAMETER, "a is a parameter"); + mu_assert_eq(a->function, 0x401136, "a belongs to add"); + mu_assert_streq(a->type->identifier.name, "int", "a is an int"); + + RzBinStabsSymbol *p = find_sym(di, "p"); + mu_assert_notnull(p, "parameter p recovered"); + mu_assert_eq(p->kind, RZ_BIN_STABS_SYMBOL_PARAMETER, "p is a parameter"); + mu_assert_eq(p->function, 0x401154, "p belongs to scale"); + mu_assert_eq(p->type->kind, RZ_TYPE_KIND_POINTER, "p is a pointer"); + mu_assert_streq(p->type->pointer.type->identifier.name, "point", "p points to point"); + + // local variables, linked back to their enclosing function + RzBinStabsSymbol *sum = find_sym(di, "sum"); + mu_assert_notnull(sum, "local sum recovered"); + mu_assert_eq(sum->kind, RZ_BIN_STABS_SYMBOL_LOCAL, "sum is a local"); + mu_assert_eq(sum->function, 0x401136, "sum belongs to add"); + + RzBinStabsSymbol *c = find_sym(di, "c"); + mu_assert_notnull(c, "local c recovered"); + mu_assert_eq(c->kind, RZ_BIN_STABS_SYMBOL_LOCAL, "c is a local"); + mu_assert_eq(c->function, 0x40118a, "c belongs to main"); + mu_assert_streq(c->type->identifier.name, "color", "c is an enum color"); + + // types registered into the type database: struct, enum and typedef + RzBaseType *point = rz_type_db_get_base_type(typedb, "point"); + mu_assert_notnull(point, "struct point registered"); + mu_assert_eq(point->kind, RZ_BASE_TYPE_KIND_STRUCT, "point is a struct"); + mu_assert_eq(rz_vector_len(&point->struct_data.members), 2, "point has two members"); + RzTypeStructMember *m0 = rz_vector_index_ptr(&point->struct_data.members, 0); + mu_assert_streq(m0->name, "x", "first member is x"); + mu_assert_eq(m0->offset, 0, "x is at offset 0"); + RzTypeStructMember *m1 = rz_vector_index_ptr(&point->struct_data.members, 1); + mu_assert_streq(m1->name, "y", "second member is y"); + mu_assert_eq(m1->offset, 4, "y is at offset 4"); + + RzBaseType *color = rz_type_db_get_base_type(typedb, "color"); + mu_assert_notnull(color, "enum color registered"); + mu_assert_eq(color->kind, RZ_BASE_TYPE_KIND_ENUM, "color is an enum"); + mu_assert_eq(rz_vector_len(&color->enum_data.cases), 3, "color has three cases"); + RzTypeEnumCase *case0 = rz_vector_index_ptr(&color->enum_data.cases, 0); + mu_assert_streq(case0->name, "RED", "first case is RED"); + mu_assert_eq(case0->val, 0, "RED is 0"); + RzTypeEnumCase *case2 = rz_vector_index_ptr(&color->enum_data.cases, 2); + mu_assert_streq(case2->name, "BLUE", "third case is BLUE"); + mu_assert_eq(case2->val, 2, "BLUE is 2"); + + RzBaseType *uint_t = rz_type_db_get_base_type(typedb, "uint_t"); + mu_assert_notnull(uint_t, "typedef uint_t registered"); + mu_assert_eq(uint_t->kind, RZ_BASE_TYPE_KIND_TYPEDEF, "uint_t is a typedef"); + + rz_bin_stabs_debug_info_free(di); + rz_type_db_free(typedb); + rz_bin_stabs_free(stabs); + rz_io_free(io); + rz_bin_free(bin); + mu_end; +} + +bool test_stabs_truncated_descriptor(void) { + // Regression: a struct member whose type descriptor is empty at the end of + // the string used to advance the cursor past the NUL terminator, causing a + // heap buffer over-read. The truncated type must instead resolve to void. + RzBinStabs stabs = { 0 }; + rz_vector_init(&stabs.entries, sizeof(RzBinStabsEntry), NULL, NULL); + RzBinStabsEntry e = { 0 }; + e.type = RZ_BIN_STABS_N_LSYM; + e.string = "trunc:T(1,2)=s4a:"; // member "a" with its type missing at end-of-string + e.strx = 1; + rz_vector_push(&stabs.entries, &e); + + RzTypeDB *typedb = rz_type_db_new(); + RzBinStabsDebugInfo *di = rz_bin_stabs_debug_info(&stabs, typedb); + mu_assert_notnull(di, "debug info built for a truncated descriptor"); + + RzBaseType *bt = rz_type_db_get_base_type(typedb, "trunc"); + mu_assert_notnull(bt, "truncated struct still registered"); + mu_assert_eq(bt->kind, RZ_BASE_TYPE_KIND_STRUCT, "trunc is a struct"); + mu_assert_eq(rz_vector_len(&bt->struct_data.members), 1, "the single member is parsed"); + RzTypeStructMember *m = rz_vector_index_ptr(&bt->struct_data.members, 0); + mu_assert_streq(m->name, "a", "member name is recovered"); + + rz_bin_stabs_debug_info_free(di); + rz_type_db_free(typedb); + rz_vector_fini(&stabs.entries); + + // A global whose type definition is missing right after '=' must likewise + // resolve to void without reading past the end of the string. + RzBinStabs s2 = { 0 }; + rz_vector_init(&s2.entries, sizeof(RzBinStabsEntry), NULL, NULL); + RzBinStabsEntry e2 = { 0 }; + e2.type = RZ_BIN_STABS_N_GSYM; + e2.string = "g:G(1,2)="; // nothing follows the '=' + e2.strx = 1; + rz_vector_push(&s2.entries, &e2); + + RzTypeDB *db2 = rz_type_db_new(); + RzBinStabsDebugInfo *di2 = rz_bin_stabs_debug_info(&s2, db2); + mu_assert_notnull(di2, "debug info built for an empty descriptor"); + RzBinStabsSymbol *g = find_sym(di2, "g"); + mu_assert_notnull(g, "global with an empty descriptor is recovered"); + mu_assert_notnull(g->type, "global has a type"); + mu_assert_eq(g->type->kind, RZ_TYPE_KIND_IDENTIFIER, "empty descriptor resolves to an identifier"); + + rz_bin_stabs_debug_info_free(di2); + rz_type_db_free(db2); + rz_vector_fini(&s2.entries); + + mu_end; +} + +bool all_tests() { + mu_run_test(test_stabs); + mu_run_test(test_stabs_extraction); + mu_run_test(test_stabs_truncated_descriptor); + return tests_passed != tests_run; +} + +mu_main(all_tests)