python/xrelfo: the ELF xref extractor

This creates JSON dumps of all the xref structs littered around FRR.

Signed-off-by: David Lamparter <equinox@diac24.net>
This commit is contained in:
David Lamparter 2020-04-30 21:33:58 +02:00
parent 5609b3af49
commit 36a8fdfd74
9 changed files with 1516 additions and 0 deletions

View file

@ -187,8 +187,16 @@ EXTRA_DIST += \
\ \
python/clidef.py \ python/clidef.py \
python/clippy/__init__.py \ python/clippy/__init__.py \
python/clippy/elf.py \
python/clippy/uidhash.py \
python/makevars.py \ python/makevars.py \
python/makefile.py \ python/makefile.py \
python/tiabwarfo.py \
python/xrelfo.py \
python/test_xrelfo.py \
python/runtests.py \
\
python/xrefstructs.json \
\ \
redhat/frr.logrotate \ redhat/frr.logrotate \
redhat/frr.pam \ redhat/frr.pam \

View file

@ -21,6 +21,8 @@ import _clippy
from _clippy import parse, Graph, GraphNode from _clippy import parse, Graph, GraphNode
frr_top_src = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def graph_iterate(graph): def graph_iterate(graph):
"""iterator yielding all nodes of a graph """iterator yielding all nodes of a graph

574
python/clippy/elf.py Normal file
View file

@ -0,0 +1,574 @@
# FRR libelf wrapper
#
# Copyright (C) 2020 David Lamparter for NetDEF, Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; see the file COPYING; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
'''
Wrapping layer and additional utility around _clippy.ELFFile.
Essentially, the C bits have the low-level ELF access bits that should be
fast while this has the bits that string everything together (and would've
been a PITA to do in C.)
Surprisingly - or maybe through proper engineering - this actually works
across architecture, word size and even endianness boundaries. Both the C
module (through GElf_*) and this code (cf. struct.unpack format mangling
in ELFDissectStruct) will take appropriate measures to flip and resize
fields as needed.
'''
import struct
from collections import OrderedDict
from weakref import WeakValueDictionary
from _clippy import ELFFile, ELFAccessError
#
# data access
#
class ELFNull(object):
'''
NULL pointer, returned instead of ELFData
'''
def __init__(self):
self.symname = None
self._dstsect = None
def __repr__(self):
return '<ptr: NULL>'
def __hash__(self):
return hash(None)
def get_string(self):
return None
class ELFUnresolved(object):
'''
Reference to an unresolved external symbol, returned instead of ELFData
:param symname: name of the referenced symbol
:param addend: offset added to the symbol, normally zero
'''
def __init__(self, symname, addend):
self.addend = addend
self.symname = symname
self._dstsect = None
def __repr__(self):
return '<unresolved: %s+%d>' % (self.symname, self.addend)
def __hash__(self):
return hash((self.symname, self.addend))
class ELFData(object):
'''
Actual data somewhere in the ELF file.
:type dstsect: ELFSubset
:param dstsect: container data area (section or entire file)
:param dstoffs: byte offset into dstsect
:param dstlen: byte size of object, or None if unknown, open-ended or string
'''
def __init__(self, dstsect, dstoffs, dstlen):
self._dstsect = dstsect
self._dstoffs = dstoffs
self._dstlen = dstlen
self.symname = None
def __repr__(self):
return '<ptr: %s+0x%05x/%d>' % (self._dstsect.name, self._dstoffs, self._dstlen or -1)
def __hash__(self):
return hash((self._dstsect, self._dstoffs))
def get_string(self):
'''
Interpret as C string / null terminated UTF-8 and get the actual text.
'''
try:
return self._dstsect[self._dstoffs:str].decode('UTF-8')
except:
import pdb; pdb.set_trace()
def get_data(self, reflen):
'''
Interpret as some structure (and check vs. expected length)
:param reflen: expected size of the object, compared against actual
size (which is only known in rare cases, mostly when directly
accessing a symbol since symbols have their destination object
size recorded)
'''
if self._dstlen is not None and self._dstlen != reflen:
raise ValueError('symbol size mismatch (got %d, expected %d)' % (self._dstlen, reflen))
return self._dstsect[self._dstoffs:self._dstoffs+reflen]
def offset(self, offs, within_symbol=False):
'''
Get another ELFData at an offset
:param offs: byte offset, can be negative (e.g. in container_of)
:param within_symbol: retain length information
'''
if self._dstlen is None or not within_symbol:
return ELFData(self._dstsect, self._dstoffs + offs, None)
else:
return ELFData(self._dstsect, self._dstoffs + offs, self._dstlen - offs)
#
# dissection data items
#
class ELFDissectData(object):
'''
Common bits for ELFDissectStruct and ELFDissectUnion
'''
def __len__(self):
'''
Used for boolean evaluation, e.g. "if struct: ..."
'''
return not (isinstance(self._data, ELFNull) or isinstance(self._data, ELFUnresolved))
def container_of(self, parent, fieldname):
'''
Assume this struct is embedded in a larger struct and get at the larger
Python ``self.container_of(a, b)`` = C ``container_of(self, a, b)``
:param parent: class (not instance) of the larger struct
:param fieldname: fieldname that refers back to this
:returns: instance of parent, with fieldname set to this object
'''
offset = 0
if not hasattr(parent, '_efields'):
parent._setup_efields()
for field in parent._efields[self.elfclass]:
if field[0] == fieldname:
break
offset += struct.calcsize(field[1])
else:
raise AttributeError('%r not found in %r.fields' % (fieldname, parent))
return parent(self._data.offset(-offset), replace = {fieldname: self})
class ELFDissectStruct(ELFDissectData):
'''
Decode and provide access to a struct somewhere in the ELF file
Handles pointers and strings somewhat nicely. Create a subclass for each
struct that is to be accessed, and give a field list in a "fields"
class-member.
:param dataptr: ELFData referring to the data bits to decode.
:param parent: where this was instantiated from; only for reference, has
no functional impact.
:param replace: substitute data values for specific fields. Used by
`container_of` to replace the inner struct when creating the outer
one.
.. attribute:: fields
List of tuples describing the struct members. Items can be:
- ``('name', ELFDissectData)`` - directly embed another struct
- ``('name', 'I')`` - simple data types; second item for struct.unpack
- ``('name', 'I', None)`` - field to ignore
- ``('name', 'P', str)`` - pointer to string
- ``('name', 'P', ELFDissectData)`` - pointer to another struct
``P`` is added as unpack format for pointers (sized appropriately for
the ELF file.)
Refer to tiabwarfo.py for extracting this from ``pahole``.
TBD: replace tuples with a class.
.. attribute:: fieldrename
Dictionary to rename fields, useful if fields comes from tiabwarfo.py.
'''
class Pointer(object):
'''
Quick wrapper for pointers to further structs
This is just here to avoid going into infinite loops when loading
structs that have pointers to each other (e.g. struct xref <-->
struct xrefdata.) The pointer destination is only instantiated when
actually accessed.
'''
def __init__(self, cls, ptr):
self.cls = cls
self.ptr = ptr
def __repr__(self):
return '<Pointer:%s %r>' % (self.cls.__name__, self.ptr)
def __call__(self):
if isinstance(self.ptr, ELFNull):
return None
return self.cls(self.ptr)
def __new__(cls, dataptr, parent = None, replace = None):
if dataptr._dstsect is None:
return super().__new__(cls)
obj = dataptr._dstsect._pointers.get((cls, dataptr))
if obj is not None:
return obj
obj = super().__new__(cls)
dataptr._dstsect._pointers[(cls, dataptr)] = obj
return obj
replacements = 'lLnN'
@classmethod
def _preproc_structspec(cls, elfclass, spec):
elfbits = elfclass
if hasattr(spec, 'calcsize'):
spec = '%ds' % (spec.calcsize(elfclass),)
if elfbits == 32:
repl = ['i', 'I']
else:
repl = ['q', 'Q']
for c in cls.replacements:
spec = spec.replace(c, repl[int(c.isupper())])
return spec
@classmethod
def _setup_efields(cls):
cls._efields = {}
cls._esize = {}
for elfclass in [32, 64]:
cls._efields[elfclass] = []
size = 0
for f in cls.fields:
newf = (f[0], cls._preproc_structspec(elfclass, f[1])) + f[2:]
cls._efields[elfclass].append(newf)
size += struct.calcsize(newf[1])
cls._esize[elfclass] = size
def __init__(self, dataptr, parent = None, replace = None):
if not hasattr(self.__class__, '_efields'):
self._setup_efields()
self._fdata = None
self._data = dataptr
self._parent = parent
self.symname = dataptr.symname
if isinstance(dataptr, ELFNull) or isinstance(dataptr, ELFUnresolved):
self._fdata = {}
return
self._elfsect = dataptr._dstsect
self.elfclass = self._elfsect._elffile.elfclass
self.offset = dataptr._dstoffs
pspecl = [f[1] for f in self._efields[self.elfclass]]
# need to correlate output from struct.unpack with extra metadata
# about the particular fields, so note down byte offsets (in locs)
# and tuple indices of pointers (in ptrs)
pspec = ''
locs = {}
ptrs = set()
for idx, spec in enumerate(pspecl):
if spec == 'P':
ptrs.add(idx)
spec = self._elfsect.ptrtype
locs[idx] = struct.calcsize(pspec)
pspec = pspec + spec
self._total_size = struct.calcsize(pspec)
def replace_ptrs(v):
idx, val = v[0], v[1]
if idx not in ptrs:
return val
return self._elfsect.pointer(self.offset + locs[idx])
data = dataptr.get_data(struct.calcsize(pspec))
unpacked = struct.unpack(self._elfsect.endian + pspec, data)
unpacked = list(map(replace_ptrs, enumerate(unpacked)))
self._fraw = unpacked
self._fdata = OrderedDict()
replace = replace or {}
for i, item in enumerate(unpacked):
name = self.fields[i][0]
if name is None:
continue
if name in replace:
self._fdata[name] = replace[name]
continue
if isinstance(self.fields[i][1], type) and issubclass(self.fields[i][1], ELFDissectData):
dataobj = self.fields[i][1](dataptr.offset(locs[i]), self)
self._fdata[name] = dataobj
continue
if len(self.fields[i]) == 3:
if self.fields[i][2] == str:
self._fdata[name] = item.get_string()
continue
elif self.fields[i][2] is None:
pass
elif issubclass(self.fields[i][2], ELFDissectData):
cls = self.fields[i][2]
dataobj = self.Pointer(cls, item)
self._fdata[name] = dataobj
continue
self._fdata[name] = item
def __getattr__(self, attrname):
if attrname not in self._fdata:
raise AttributeError(attrname)
if isinstance(self._fdata[attrname], self.Pointer):
self._fdata[attrname] = self._fdata[attrname]()
return self._fdata[attrname]
def __repr__(self):
if not isinstance(self._data, ELFData):
return '<%s: %r>' % (self.__class__.__name__, self._data)
return '<%s: %s>' % (self.__class__.__name__,
', '.join(['%s=%r' % t for t in self._fdata.items()]))
@classmethod
def calcsize(cls, elfclass):
'''
Sum up byte size of this struct
Wraps struct.calcsize with some extra features.
'''
if not hasattr(cls, '_efields'):
cls._setup_efields()
pspec = ''.join([f[1] for f in cls._efields[elfclass]])
ptrtype = 'I' if elfclass == 32 else 'Q'
pspec = pspec.replace('P', ptrtype)
return struct.calcsize(pspec)
class ELFDissectUnion(ELFDissectData):
'''
Decode multiple structs in the same place.
Not currently used (and hence not tested.) Worked at some point but not
needed anymore and may be borked now. Remove this comment when using.
'''
def __init__(self, dataptr, parent = None):
self._dataptr = dataptr
self._parent = parent
self.members = []
for name, membercls in self.__class__.members:
item = membercls(dataptr, parent)
self.members.append(item)
setattr(self, name, item)
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, ', '.join([repr(i) for i in self.members]))
@classmethod
def calcsize(cls, elfclass):
return max([member.calcsize(elfclass) for name, member in cls.members])
#
# wrappers for spans of ELF data
#
class ELFSubset(object):
'''
Common abstract base for section-level and file-level access.
'''
def __init__(self):
super().__init__()
self._pointers = WeakValueDictionary()
def __hash__(self):
return hash(self.name)
def __getitem__(self, k):
'''
Read data from slice
Subscript **must** be a slice; a simple index will not return a byte
but rather throw an exception. Valid slice syntaxes are defined by
the C module:
- `this[123:456]` - extract specific range
- `this[123:str]` - extract until null byte. The slice stop value is
the `str` type (or, technically, `unicode`.)
'''
return self._obj[k]
def getreloc(self, offset):
'''
Check for a relocation record at the specified offset.
'''
return self._obj.getreloc(offset)
def iter_data(self, scls, slice_ = slice(None)):
'''
Assume an array of structs present at a particular slice and decode
:param scls: ELFDissectData subclass for the struct
:param slice_: optional range specification
'''
size = scls.calcsize(self._elffile.elfclass)
offset = slice_.start or 0
stop = slice_.stop or self._obj.len
if stop < 0:
stop = self._obj.len - stop
while offset < stop:
yield scls(ELFData(self, offset, size))
offset += size
def pointer(self, offset):
'''
Try to dereference a pointer value
This checks whether there's a relocation at the given offset and
uses that; otherwise (e.g. in a non-PIE executable where the pointer
is already resolved by the linker) the data at the location is used.
:param offset: byte offset from beginning of section,
or virtual address in file
:returns: ELFData wrapping pointed-to object
'''
ptrsize = struct.calcsize(self.ptrtype)
data = struct.unpack(self.endian + self.ptrtype, self[offset:offset + ptrsize])[0]
reloc = self.getreloc(offset)
dstsect = None
if reloc:
# section won't be available in whole-file operation
dstsect = reloc.getsection(data)
addend = reloc.r_addend
if reloc.relative:
# old-style ELF REL instead of RELA, not well-tested
addend += data
if reloc.unresolved and reloc.symvalid:
return ELFUnresolved(reloc.symname, addend)
elif reloc.symvalid:
data = addend + reloc.st_value
else:
data = addend
# 0 could technically be a valid pointer for a shared library,
# since libraries may use 0 as default virtual start address (it'll
# be adjusted on loading)
# That said, if the library starts at 0, that's where the ELF header
# would be so it's still an invalid pointer.
if data == 0 and dstsect == None:
return ELFNull()
# wrap_data is different between file & section
return self._wrap_data(data, dstsect)
class ELFDissectSection(ELFSubset):
'''
Access the contents of an ELF section like ``.text`` or ``.data``
:param elfwrap: ELFDissectFile wrapper for the file
:param idx: section index in section header table
:param section: section object from C module
'''
def __init__(self, elfwrap, idx, section):
super().__init__()
self._elfwrap = elfwrap
self._elffile = elfwrap._elffile
self._idx = idx
self._section = self._obj = section
self.name = section.name
self.ptrtype = elfwrap.ptrtype
self.endian = elfwrap.endian
def _wrap_data(self, data, dstsect):
if dstsect is None:
dstsect = self._elfwrap._elffile.get_section_addr(data)
offs = data - dstsect.sh_addr
dstsect = self._elfwrap.get_section(dstsect.idx)
return ELFData(dstsect, offs, None)
class ELFDissectFile(ELFSubset):
'''
Access the contents of an ELF file.
Note that offsets for array subscript and relocation/pointer access are
based on the file's virtual address space and are NOT offsets to the
start of the file on disk!
(Shared libraries frequently have a virtual address space starting at 0,
but non-PIE executables have an architecture specific default loading
address like 0x400000 on x86.
:param filename: ELF file to open
'''
def __init__(self, filename):
super().__init__()
self.name = filename
self._elffile = self._obj = ELFFile(filename)
self._sections = {}
self.ptrtype = 'I' if self._elffile.elfclass == 32 else 'Q'
self.endian = '>' if self._elffile.bigendian else '<'
@property
def _elfwrap(self):
return self
def _wrap_data(self, data, dstsect):
return ELFData(self, data, None)
def get_section(self, secname):
'''
Look up section by name or index
'''
if isinstance(secname, int):
sh_idx = secname
section = self._elffile.get_section_idx(secname)
else:
section = self._elffile.get_section(secname)
if section is None:
return None
sh_idx = section.idx
if sh_idx not in self._sections:
self._sections[sh_idx] = ELFDissectSection(self, sh_idx, section)
return self._sections[sh_idx]

71
python/clippy/uidhash.py Normal file
View file

@ -0,0 +1,71 @@
# xref unique ID hash calculation
#
# Copyright (C) 2020 David Lamparter for NetDEF, Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; see the file COPYING; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import struct
from hashlib import sha256
def bititer(data, bits, startbit = True):
'''
just iterate the individual bits out from a bytes object
if startbit is True, an '1' bit is inserted at the very beginning
goes <bits> at a time, starts at LSB.
'''
bitavail, v = 0, 0
if startbit and len(data) > 0:
v = data.pop(0)
yield (v & ((1 << bits) - 1)) | (1 << (bits - 1))
bitavail = 9 - bits
v >>= bits - 1
while len(data) > 0:
while bitavail < bits:
v |= data.pop(0) << bitavail
bitavail += 8
yield v & ((1 << bits) - 1)
bitavail -= bits
v >>= bits
def base32c(data):
'''
Crockford base32 with extra dashes
'''
chs = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
o = ''
if type(data) == str:
data = [ord(v) for v in data]
else:
data = list(data)
for i, bits in enumerate(bititer(data, 5)):
if i == 5:
o = o + '-'
elif i == 10:
break
o = o + chs[bits]
return o
def uidhash(filename, hashstr, hashu32a, hashu32b):
'''
xref Unique ID hash used in FRRouting
'''
filename = '/'.join(filename.rsplit('/')[-2:])
hdata = filename.encode('UTF-8') + hashstr.encode('UTF-8')
hdata += struct.pack('>II', hashu32a, hashu32b)
i = sha256(hdata).digest()
return base32c(i)

14
python/runtests.py Normal file
View file

@ -0,0 +1,14 @@
import pytest
import sys
import os
try:
import _clippy
except ImportError:
sys.stderr.write('''these tests need to be run with the _clippy C extension
module available. Try running "clippy runtests.py ...".
''')
sys.exit(1)
os.chdir(os.path.dirname(os.path.abspath(__file__)))
raise SystemExit(pytest.main(sys.argv[1:]))

65
python/test_xrelfo.py Normal file
View file

@ -0,0 +1,65 @@
# some basic tests for xrelfo & the python ELF machinery
#
# Copyright (C) 2020 David Lamparter for NetDEF, Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; see the file COPYING; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import sys
import os
import pytest
from pprint import pprint
root = os.path.dirname(os.path.dirname(__file__))
sys.path.append(os.path.join(root, 'python'))
import xrelfo
from clippy import elf, uidhash
def test_uidhash():
assert uidhash.uidhash("lib/test_xref.c", "logging call", 3, 0) \
== 'H7KJB-67TBH'
def test_xrelfo_other():
for data in [
elf.ELFNull(),
elf.ELFUnresolved('somesym', 0),
]:
dissect = xrelfo.XrefPtr(data)
print(repr(dissect))
with pytest.raises(AttributeError):
dissect.xref
def test_xrelfo_obj():
xrelfo_ = xrelfo.Xrelfo()
edf = xrelfo_.load_elf(os.path.join(root, 'lib/.libs/zclient.o'), 'zclient.lo')
xrefs = xrelfo_._xrefs
with pytest.raises(elf.ELFAccessError):
edf[0:4]
pprint(xrefs[0])
pprint(xrefs[0]._data)
def test_xrelfo_bin():
xrelfo_ = xrelfo.Xrelfo()
edf = xrelfo_.load_elf(os.path.join(root, 'lib/.libs/libfrr.so'), 'libfrr.la')
xrefs = xrelfo_._xrefs
assert edf[0:4] == b'\x7fELF'
pprint(xrefs[0])
pprint(xrefs[0]._data)

195
python/tiabwarfo.py Normal file
View file

@ -0,0 +1,195 @@
# FRR DWARF structure definition extractor
#
# Copyright (C) 2020 David Lamparter for NetDEF, Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; see the file COPYING; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import sys
import os
import subprocess
import re
import argparse
import subprocess
import json
structs = ['xref', 'xref_logmsg', 'xref_threadsched', 'xref_install_element', 'xrefdata', 'xrefdata_logmsg', 'cmd_element']
def extract(filename='lib/.libs/libfrr.so'):
'''
Convert output from "pahole" to JSON.
Example pahole output:
$ pahole -C xref lib/.libs/libfrr.so
struct xref {
struct xrefdata * xrefdata; /* 0 8 */
enum xref_type type; /* 8 4 */
int line; /* 12 4 */
const char * file; /* 16 8 */
const char * func; /* 24 8 */
/* size: 32, cachelines: 1, members: 5 */
/* last cacheline: 32 bytes */
};
'''
pahole = subprocess.check_output(['pahole', '-C', ','.join(structs), filename]).decode('UTF-8')
struct_re = re.compile(r'^struct ([^ ]+) \{([^\}]+)};', flags=re.M | re.S)
field_re = re.compile(r'^\s*(?P<type>[^;\(]+)\s+(?P<name>[^;\[\]]+)(?:\[(?P<array>\d+)\])?;\s*\/\*(?P<comment>.*)\*\/\s*$')
comment_re = re.compile(r'^\s*\/\*.*\*\/\s*$')
pastructs = struct_re.findall(pahole)
out = {}
for name, data in pastructs:
this = out.setdefault(name, {})
fields = this.setdefault('fields', [])
lines = data.strip().splitlines()
for line in lines:
if line.strip() == '':
continue
m = comment_re.match(line)
if m is not None:
continue
m = field_re.match(line)
if m is not None:
offs, size = m.group('comment').strip().split()
offs = int(offs)
size = int(size)
typ_ = m.group('type').strip()
name = m.group('name')
if name.startswith('(*'):
# function pointer
typ_ = typ_ + ' *'
name = name[2:].split(')')[0]
data = {
'name': name,
'type': typ_,
'offset': offs,
'size': size,
}
if m.group('array'):
data['array'] = int(m.group('array'))
fields.append(data)
continue
raise ValueError('cannot process line: %s' % line)
return out
class FieldApplicator(object):
'''
Fill ELFDissectStruct fields list from pahole/JSON
Uses the JSON file created by the above code to fill in the struct fields
in subclasses of ELFDissectStruct.
'''
# only what we really need. add more as needed.
packtypes = {
'int': 'i',
'uint8_t': 'B',
'uint16_t': 'H',
'uint32_t': 'I',
'char': 's',
}
def __init__(self, data):
self.data = data
self.classes = []
self.clsmap = {}
def add(self, cls):
self.classes.append(cls)
self.clsmap[cls.struct] = cls
def resolve(self, cls):
out = []
offset = 0
fieldrename = getattr(cls, 'fieldrename', {})
def mkname(n):
return (fieldrename.get(n, n),)
for field in self.data[cls.struct]['fields']:
typs = field['type'].split()
typs = [i for i in typs if i not in ['const']]
if field['offset'] != offset:
assert offset < field['offset']
out.append(('_pad', '%ds' % (field['offset'] - offset,)))
# pretty hacky C types handling, but covers what we need
ptrlevel = 0
while typs[-1] == '*':
typs.pop(-1)
ptrlevel += 1
if ptrlevel > 0:
packtype = ('P', None)
if ptrlevel == 1:
if typs[0] == 'char':
packtype = ('P', str)
elif typs[0] == 'struct' and typs[1] in self.clsmap:
packtype = ('P', self.clsmap[typs[1]])
elif typs[0] == 'enum':
packtype = ('I',)
elif typs[0] in self.packtypes:
packtype = (self.packtypes[typs[0]],)
elif typs[0] == 'struct':
if typs[1] in self.clsmap:
packtype = (self.clsmap[typs[1]],)
else:
packtype = ('%ds' % field['size'],)
else:
raise ValueError('cannot decode field %s in struct %s (%s)' % (
cls.struct, field['name'], field['type']))
if 'array' in field and typs[0] == 'char':
packtype = ('%ds' % field['array'],)
out.append(mkname(field['name']) + packtype)
elif 'array' in field:
for i in range(0, field['array']):
out.append(mkname('%s_%d' % (field['name'], i)) + packtype)
else:
out.append(mkname(field['name']) + packtype)
offset = field['offset'] + field['size']
cls.fields = out
def __call__(self):
for cls in self.classes:
self.resolve(cls)
def main():
argp = argparse.ArgumentParser(description = 'FRR DWARF structure extractor')
argp.add_argument('-o', dest='output', type=str, help='write JSON output', default='python/xrefstructs.json')
argp.add_argument('-i', dest='input', type=str, help='ELF file to read', default='lib/.libs/libfrr.so')
args = argp.parse_args()
out = extract(args.input)
with open(args.output + '.tmp', 'w') as fd:
json.dump(out, fd, indent=2, sort_keys=True)
os.rename(args.output + '.tmp', args.output)
if __name__ == '__main__':
main()

190
python/xrefstructs.json Normal file
View file

@ -0,0 +1,190 @@
{
"cmd_element": {
"fields": [
{
"name": "string",
"offset": 0,
"size": 8,
"type": "const char *"
},
{
"name": "doc",
"offset": 8,
"size": 8,
"type": "const char *"
},
{
"name": "daemon",
"offset": 16,
"size": 4,
"type": "int"
},
{
"name": "attr",
"offset": 20,
"size": 1,
"type": "uint8_t"
},
{
"name": "func",
"offset": 24,
"size": 8,
"type": "int *"
},
{
"name": "name",
"offset": 32,
"size": 8,
"type": "const char *"
},
{
"name": "xref",
"offset": 40,
"size": 32,
"type": "struct xref"
}
]
},
"xref": {
"fields": [
{
"name": "xrefdata",
"offset": 0,
"size": 8,
"type": "struct xrefdata *"
},
{
"name": "type",
"offset": 8,
"size": 4,
"type": "enum xref_type"
},
{
"name": "line",
"offset": 12,
"size": 4,
"type": "int"
},
{
"name": "file",
"offset": 16,
"size": 8,
"type": "const char *"
},
{
"name": "func",
"offset": 24,
"size": 8,
"type": "const char *"
}
]
},
"xref_install_element": {
"fields": [
{
"name": "xref",
"offset": 0,
"size": 32,
"type": "struct xref"
},
{
"name": "cmd_element",
"offset": 32,
"size": 8,
"type": "const struct cmd_element *"
},
{
"name": "node_type",
"offset": 40,
"size": 4,
"type": "enum node_type"
}
]
},
"xref_logmsg": {
"fields": [
{
"name": "xref",
"offset": 0,
"size": 32,
"type": "struct xref"
},
{
"name": "fmtstring",
"offset": 32,
"size": 8,
"type": "const char *"
},
{
"name": "priority",
"offset": 40,
"size": 4,
"type": "uint32_t"
},
{
"name": "ec",
"offset": 44,
"size": 4,
"type": "uint32_t"
}
]
},
"xref_threadsched": {
"fields": [
{
"name": "xref",
"offset": 0,
"size": 32,
"type": "struct xref"
},
{
"name": "funcname",
"offset": 32,
"size": 8,
"type": "const char *"
},
{
"name": "dest",
"offset": 40,
"size": 8,
"type": "const char *"
},
{
"name": "thread_type",
"offset": 48,
"size": 4,
"type": "uint32_t"
}
]
},
"xrefdata": {
"fields": [
{
"name": "xref",
"offset": 0,
"size": 8,
"type": "const struct xref *"
},
{
"array": 16,
"name": "uid",
"offset": 8,
"size": 16,
"type": "char"
},
{
"name": "hashstr",
"offset": 24,
"size": 8,
"type": "const char *"
},
{
"array": 2,
"name": "hashu32",
"offset": 32,
"size": 8,
"type": "uint32_t"
}
]
}
}

397
python/xrelfo.py Normal file
View file

@ -0,0 +1,397 @@
# FRR ELF xref extractor
#
# Copyright (C) 2020 David Lamparter for NetDEF, Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; see the file COPYING; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import sys
import os
import struct
import re
import traceback
import json
import argparse
from clippy.uidhash import uidhash
from clippy.elf import *
from clippy import frr_top_src
from tiabwarfo import FieldApplicator
try:
with open(os.path.join(frr_top_src, 'python', 'xrefstructs.json'), 'r') as fd:
xrefstructs = json.load(fd)
except FileNotFoundError:
sys.stderr.write('''
The "xrefstructs.json" file (created by running tiabwarfo.py with the pahole
tool available) could not be found. It should be included with the sources.
''')
sys.exit(1)
# constants, need to be kept in sync manually...
XREFT_THREADSCHED = 0x100
XREFT_LOGMSG = 0x200
XREFT_DEFUN = 0x300
XREFT_INSTALL_ELEMENT = 0x301
# LOG_*
priovals = {}
prios = ['0', '1', '2', 'E', 'W', 'N', 'I', 'D']
class XrelfoJson(object):
def dump(self):
pass
def check(self, wopt):
yield from []
def to_dict(self, refs):
pass
class Xref(ELFDissectStruct, XrelfoJson):
struct = 'xref'
fieldrename = {'type': 'typ'}
containers = {}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._container = None
if self.xrefdata:
self.xrefdata.ref_from(self, self.typ)
def container(self):
if self._container is None:
if self.typ in self.containers:
self._container = self.container_of(self.containers[self.typ], 'xref')
return self._container
def check(self, *args, **kwargs):
if self._container:
yield from self._container.check(*args, **kwargs)
class Xrefdata(ELFDissectStruct):
struct = 'xrefdata'
# uid is all zeroes in the data loaded from ELF
fieldrename = {'uid': '_uid'}
def ref_from(self, xref, typ):
self.xref = xref
@property
def uid(self):
if self.hashstr is None:
return None
return uidhash(self.xref.file, self.hashstr, self.hashu32_0, self.hashu32_1)
class XrefPtr(ELFDissectStruct):
fields = [
('xref', 'P', Xref),
]
class XrefThreadSched(ELFDissectStruct, XrelfoJson):
struct = 'xref_threadsched'
Xref.containers[XREFT_THREADSCHED] = XrefThreadSched
class XrefLogmsg(ELFDissectStruct, XrelfoJson):
struct = 'xref_logmsg'
def _warn_fmt(self, text):
yield ((self.xref.file, self.xref.line), '%s:%d: %s (in %s())\n' % (self.xref.file, self.xref.line, text, self.xref.func))
regexes = [
(re.compile(r'([\n\t]+)'), 'error: log message contains tab or newline'),
# (re.compile(r'^(\s+)'), 'warning: log message starts with whitespace'),
(re.compile(r'^((?:warn(?:ing)?|error):\s*)', re.I), 'warning: log message starts with severity'),
]
def check(self, wopt):
if wopt.Wlog_format:
for rex, msg in self.regexes:
if not rex.search(self.fmtstring):
continue
if sys.stderr.isatty():
items = rex.split(self.fmtstring)
out = []
for i, text in enumerate(items):
if (i % 2) == 1:
out.append('\033[41;37;1m%s\033[m' % repr(text)[1:-1])
else:
out.append(repr(text)[1:-1])
excerpt = ''.join(out)
else:
excerpt = repr(self.fmtstring)[1:-1]
yield from self._warn_fmt('%s: "%s"' % (msg, excerpt))
def dump(self):
print('%-60s %s%s %-25s [EC %d] %s' % (
'%s:%d %s()' % (self.xref.file, self.xref.line, self.xref.func),
prios[self.priority & 7],
priovals.get(self.priority & 0x30, ' '),
self.xref.xrefdata.uid, self.ec, self.fmtstring))
def to_dict(self, xrelfo):
jsobj = dict([(i, getattr(self.xref, i)) for i in ['file', 'line', 'func']])
if self.ec != 0:
jsobj['ec'] = self.ec
jsobj['fmtstring'] = self.fmtstring
jsobj['priority'] = self.priority & 7
jsobj['type'] = 'logmsg'
jsobj['binary'] = self._elfsect._elfwrap.orig_filename
if self.priority & 0x10:
jsobj.setdefault('flags', []).append('errno')
if self.priority & 0x20:
jsobj.setdefault('flags', []).append('getaddrinfo')
xrelfo['refs'].setdefault(self.xref.xrefdata.uid, []).append(jsobj)
Xref.containers[XREFT_LOGMSG] = XrefLogmsg
class CmdElement(ELFDissectStruct, XrelfoJson):
struct = 'cmd_element'
cmd_attrs = { 0: None, 1: 'deprecated', 2: 'hidden'}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def to_dict(self, xrelfo):
jsobj = xrelfo['cli'].setdefault(self.name, {}).setdefault(self._elfsect._elfwrap.orig_filename, {})
jsobj.update({
'string': self.string,
'doc': self.doc,
'attr': self.cmd_attrs.get(self.attr, self.attr),
})
if jsobj['attr'] is None:
del jsobj['attr']
jsobj['defun'] = dict([(i, getattr(self.xref, i)) for i in ['file', 'line', 'func']])
Xref.containers[XREFT_DEFUN] = CmdElement
class XrefInstallElement(ELFDissectStruct, XrelfoJson):
struct = 'xref_install_element'
def to_dict(self, xrelfo):
jsobj = xrelfo['cli'].setdefault(self.cmd_element.name, {}).setdefault(self._elfsect._elfwrap.orig_filename, {})
nodes = jsobj.setdefault('nodes', [])
nodes.append({
'node': self.node_type,
'install': dict([(i, getattr(self.xref, i)) for i in ['file', 'line', 'func']]),
})
Xref.containers[XREFT_INSTALL_ELEMENT] = XrefInstallElement
# shove in field defs
fieldapply = FieldApplicator(xrefstructs)
fieldapply.add(Xref)
fieldapply.add(Xrefdata)
fieldapply.add(XrefLogmsg)
fieldapply.add(XrefThreadSched)
fieldapply.add(CmdElement)
fieldapply.add(XrefInstallElement)
fieldapply()
class Xrelfo(dict):
def __init__(self):
super().__init__({
'refs': {},
'cli': {},
})
self._xrefs = []
def load_file(self, filename):
orig_filename = filename
if filename.endswith('.la') or filename.endswith('.lo'):
with open(filename, 'r') as fd:
for line in fd:
line = line.strip()
if line.startswith('#') or line == '' or '=' not in line:
continue
var, val = line.split('=', 1)
if var not in ['library_names', 'pic_object']:
continue
if val.startswith("'") or val.startswith('"'):
val = val[1:-1]
if var == 'pic_object':
filename = os.path.join(os.path.dirname(filename), val)
break
val = val.strip().split()[0]
filename = os.path.join(os.path.dirname(filename), '.libs', val)
break
else:
raise ValueError('could not process libtool file "%s"' % orig_filename)
while True:
with open(filename, 'rb') as fd:
hdr = fd.read(4)
if hdr == b'\x7fELF':
self.load_elf(filename, orig_filename)
return
if hdr[:2] == b'#!':
path, name = os.path.split(filename)
filename = os.path.join(path, '.libs', name)
continue
if hdr[:1] == b'{':
with open(filename, 'r') as fd:
self.load_json(fd)
return
raise ValueError('cannot determine file type for %s' % (filename))
def load_elf(self, filename, orig_filename):
edf = ELFDissectFile(filename)
edf.orig_filename = orig_filename
note = edf._elffile.find_note('FRRouting', 'XREF')
if note is not None:
endian = '>' if edf._elffile.bigendian else '<'
mem = edf._elffile[note]
if edf._elffile.elfclass == 64:
start, end = struct.unpack(endian + 'QQ', mem)
start += note.start
end += note.start + 8
else:
start, end = struct.unpack(endian + 'II', mem)
start += note.start
end += note.start + 4
ptrs = edf.iter_data(XrefPtr, slice(start, end))
else:
xrefarray = edf.get_section('xref_array')
if xrefarray is None:
raise ValueError('file has neither xref note nor xref_array section')
ptrs = xrefarray.iter_data(XrefPtr)
for ptr in ptrs:
if ptr.xref is None:
print('NULL xref')
continue
self._xrefs.append(ptr.xref)
container = ptr.xref.container()
if container is None:
continue
container.to_dict(self)
return edf
def load_json(self, fd):
data = json.load(fd)
for uid, items in data['refs'].items():
myitems = self['refs'].setdefault(uid, [])
for item in items:
if item in myitems:
continue
myitems.append(item)
for cmd, items in data['cli'].items():
self['cli'].setdefault(cmd, {}).update(items)
return data
def check(self, checks):
for xref in self._xrefs:
yield from xref.check(checks)
def main():
argp = argparse.ArgumentParser(description = 'FRR xref ELF extractor')
argp.add_argument('-o', dest='output', type=str, help='write JSON output')
argp.add_argument('--out-by-file', type=str, help='write by-file JSON output')
argp.add_argument('-Wlog-format', action='store_const', const=True)
argp.add_argument('--profile', action='store_const', const=True)
argp.add_argument('binaries', metavar='BINARY', nargs='+', type=str, help='files to read (ELF files or libtool objects)')
args = argp.parse_args()
if args.profile:
import cProfile
cProfile.runctx('_main(args)', globals(), {'args': args}, sort='cumtime')
else:
_main(args)
def _main(args):
errors = 0
xrelfo = Xrelfo()
for fn in args.binaries:
try:
xrelfo.load_file(fn)
except:
errors += 1
sys.stderr.write('while processing %s:\n' % (fn))
traceback.print_exc()
for option in dir(args):
if option.startswith('W'):
checks = sorted(xrelfo.check(args))
sys.stderr.write(''.join([c[-1] for c in checks]))
break
refs = xrelfo['refs']
counts = {}
for k, v in refs.items():
strs = set([i['fmtstring'] for i in v])
if len(strs) != 1:
print('\033[31;1m%s\033[m' % k)
counts[k] = len(v)
out = xrelfo
outbyfile = {}
for uid, locs in refs.items():
for loc in locs:
filearray = outbyfile.setdefault(loc['file'], [])
loc = dict(loc)
del loc['file']
filearray.append(loc)
for k in outbyfile.keys():
outbyfile[k] = sorted(outbyfile[k], key=lambda x: x['line'])
if errors:
sys.exit(1)
if args.output:
with open(args.output + '.tmp', 'w') as fd:
json.dump(out, fd, indent=2, sort_keys=True)
os.rename(args.output + '.tmp', args.output)
if args.out_by_file:
with open(args.out_by_file + '.tmp', 'w') as fd:
json.dump(outbyfile, fd, indent=2, sort_keys=True)
os.rename(args.out_by_file + '.tmp', args.out_by_file)
if __name__ == '__main__':
main()