librz/bin: add STABS format parser (#6511)

Co-authored-by: Anton Kochkov <anton.kochkov@gmail.com>
This commit is contained in:
NOT XVilka 2026-06-17 04:13:23 +08:00 committed by GitHub
parent c0945b0231
commit 62bcb8f6fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1507 additions and 0 deletions

View file

@ -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',

924
librz/bin/stabs.c Normal file
View file

@ -0,0 +1,924 @@
// SPDX-FileCopyrightText: 2026 RizinOrg <info@rizin.re>
// SPDX-License-Identifier: LGPL-3.0-only
#include <rz_bin.h>
#include <rz_bin_stabs.h>
#include <rz_endian.h>
#include <rz_type.h>
#include <rz_util.h>
/**
* \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 /*<ut64, RzType *>*/ *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<typeref>;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 "<size><member>...;", 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);
}

View file

@ -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:

View file

@ -18,6 +18,7 @@ typedef struct rz_bin_file_t RzBinFile;
typedef struct rz_bin_reloc_storage_t RzBinRelocStorage;
#include <rz_bin_dwarf.h>
#include <rz_bin_stabs.h>
#include <rz_pdb.h>
#ifdef __cplusplus

View file

@ -0,0 +1,134 @@
// SPDX-FileCopyrightText: 2026 RizinOrg <info@rizin.re>
// SPDX-License-Identifier: LGPL-3.0-only
#ifndef RZ_BIN_STABS_H
#define RZ_BIN_STABS_H
#include <rz_types.h>
#include <rz_vector.h>
#include <rz_type.h>
#include <rz_bin_source_line.h>
#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 /*<RzBinStabsEntry>*/ 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 /*<RzBinStabsSymbol *>*/ 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

View file

@ -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);

54
test/db/cmd/stabs Normal file
View file

@ -0,0 +1,54 @@
NAME=stabs ELF source line info
FILE=bins/elf/stabs/stabs_hello
CMDS=<<EOF
ix
ixj
ixf
s 0x401172
ix.
EOF
EXPECT=<<EOF
0x00401136 stabs_hello.c 6 0
0x00401144 stabs_hello.c 7 0
0x0040114f stabs_hello.c 8 0
0x00401152 stabs_hello.c 9 0
0x00401154 stabs_hello.c 11 0
0x00401160 stabs_hello.c 12 0
0x00401172 stabs_hello.c 13 0
0x0040118b stabs_hello.c 14 0
0x00401190 stabs_hello.c 15 0
0x00401192 - 0 0
[{"file":"stabs_hello.c","line":6,"addr":4198710},{"file":"stabs_hello.c","line":7,"addr":4198724},{"file":"stabs_hello.c","line":8,"addr":4198735},{"file":"stabs_hello.c","line":9,"addr":4198738},{"file":"stabs_hello.c","line":11,"addr":4198740},{"file":"stabs_hello.c","line":12,"addr":4198752},{"file":"stabs_hello.c","line":13,"addr":4198770},{"file":"stabs_hello.c","line":14,"addr":4198795},{"file":"stabs_hello.c","line":15,"addr":4198800},{"line":0,"addr":4198802}]
[Source file]
stabs_hello.c
0x00401172 stabs_hello.c 13 0
EOF
RUN
NAME=stabs ELF symbol type and variable extraction
FILE=bins/elf/stabs/stabs_syms
CMDS=<<EOF
t~point,color,uint_t
ts point
te color
tf add
tf scale
avgl
avglj
EOF
EXPECT=<<EOF
color
point
uint_t
pf "d4d4 x y"
RED = 0x0
GREEN = 0x1
BLUE = 0x2
int add(int a, int b);
long scale(struct point *p, unsigned int factor);
global int g_counter @ 0x404018
global struct point g_origin @ 0x404030
global unsigned char [16] s_buffer @ 0x404040
[{"name":"g_counter","type":"int","size":4,"addr":"0x404018"},{"name":"g_origin","type":"struct point","size":8,"addr":"0x404030"},{"name":"s_buffer","type":"unsigned char [16]","size":16,"addr":"0x404040"}]
EOF
RUN

View file

@ -27,6 +27,7 @@ if get_option('enable_tests') and cli_enabled
'pdb',
'project_migrate',
'rzpipe',
'stabs',
'str_search',
]

View file

@ -0,0 +1,270 @@
// SPDX-FileCopyrightText: 2026 RizinOrg <info@rizin.re>
// SPDX-License-Identifier: LGPL-3.0-only
#include <rz_util.h>
#include <rz_bin.h>
#include <rz_bin_stabs.h>
#include <rz_type.h>
#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)