Tentative progress in stats

This commit is contained in:
Théophile Bastian 2018-07-17 11:36:56 +02:00
parent 3cb2c508a0
commit 216e442f5b
4 changed files with 359 additions and 127 deletions

View file

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from stats_accu import StatsAccumulator
import gather_stats import gather_stats
import argparse import argparse
@ -15,6 +16,9 @@ class Config:
if args.feature == 'gather': if args.feature == 'gather':
self.output = args.output self.output = args.output
elif args.feature == 'sample':
self.size = int(args.size)
elif args.feature == 'analyze': elif args.feature == 'analyze':
self.data_file = args.data_file self.data_file = args.data_file
@ -34,6 +38,19 @@ class Config:
subparsers = parser.add_subparsers(help='Subcommands') subparsers = parser.add_subparsers(help='Subcommands')
# Sample stats
parser_sample = subparsers.add_parser(
'sample',
help='Same as gather, but for a random subset of files')
parser_sample.set_defaults(feature='sample')
parser_sample.add_argument('--size', '-n',
default=1000,
help=('Pick this number of files'))
parser_sample.add_argument('--output', '-o',
default='elf_data',
help=('Output data to this file. Defaults '
'to "elf_data"'))
# Gather stats # Gather stats
parser_gather = subparsers.add_parser( parser_gather = subparsers.add_parser(
'gather', 'gather',
@ -70,11 +87,17 @@ def main():
if config.feature == 'gather': if config.feature == 'gather':
stats_accu = gather_stats.gather_system_files(config) stats_accu = gather_stats.gather_system_files(config)
stats_accu.serialize(config.output) stats_accu.dump(config.output)
elif config.feature == 'sample':
stats_accu = gather_stats.gather_system_files(
config,
sample_size=config.size)
elif config.feature == 'analyze': elif config.feature == 'analyze':
# TODO # TODO
print("Not implemented", file=sys.stderr) print("Not implemented", file=sys.stderr)
stats_accu = StatsAccumulator.load(config.data_file)
sys.exit(1) sys.exit(1)

View file

@ -1,52 +1,119 @@
from pyelftools_overlay import system_elfs from pyelftools_overlay import system_elfs, get_cfi
import pathos from elftools.dwarf import callframe
import multiprocessing
import signal import signal
import itertools import random
from stats_accu import StatsAccumulator from stats_accu import \
StatsAccumulator, SingleFdeData, \
RegsList, FdeData, DwarfInstr
class FilesProcessor: class FilesProcessor(multiprocessing.Process):
def __init__(self, cores, stats_accu=None): def __init__(self, elf_list, shared_queue):
super().__init__()
self.stop_processing = False self.stop_processing = False
self._processed_counter = itertools.count() self.processed_counter = 0
self.cores = cores self.elf_list = elf_list
self.shared_queue = shared_queue
if stats_accu is None:
stats_accu = StatsAccumulator()
self.stats_accu = stats_accu
def stop_processing_now(self): def stop_processing_now(self):
self.stop_processing = True self.stop_processing = True
def next_counter(self): def run(self):
return self._processed_counter.__next__() pos = 0
for descr in self.elf_list:
if self.stop_processing:
break
self.process_single_file(descr, pos)
pos += 1
def run(self, elf_list): print("=== Finished {} ===".format(self.name))
self.elf_count = len(elf_list) return 0
with pathos.multiprocessing.ProcessPool(nodes=self.cores) as pool:
pool.map(self.process_single_file, elf_list)
def process_single_file(self, elf_path): def process_single_file(self, elf_descr, pos_in_list):
if self.stop_processing: if self.stop_processing:
return return
cur_file_count = self.next_counter() elf_path, elf_type = elf_descr
print('> [{}/{} {:.0f}%] {}'.format(
cur_file_count, self.elf_count, self.processed_counter += 1
cur_file_count / self.elf_count * 100, elf_path)) print('[{}, {}/{}] {}'.format(
self.stats_accu.process_file(elf_path) self.shared_queue.qsize(),
pos_in_list + 1,
len(self.elf_list),
elf_path))
self.process_file(elf_path, elf_type)
def process_file(self, path, elftype):
''' Process a single file '''
cfi = get_cfi(path)
if not cfi:
return None
data = FdeData()
for entry in cfi:
if isinstance(entry, callframe.CIE): # Is a CIE
self.process_cie(entry, data)
elif isinstance(entry, callframe.FDE): # Is a FDE
self.process_fde(entry, data)
out = SingleFdeData(path, elftype, data)
self.shared_queue.put(out)
def incr_cell(self, table, key):
''' Increments table[key], or sets it to 1 if unset '''
if key in table:
table[key] += 1
else:
table[key] = 1
def process_cie(self, cie, data):
''' Process a CIE '''
pass # Nothing needed from a CIE
def process_fde(self, fde, data):
''' Process a FDE '''
data.fde_count += 1
decoded = fde.get_decoded()
row_count = len(decoded.table)
self.incr_cell(data.fde_with_lines, row_count)
for row in decoded.table:
self.process_reg(data.regs.cfa, row['cfa'])
for entry in row:
if isinstance(entry, int):
self.process_reg(data.regs.regs[entry], row[entry])
def process_reg(self, out_reg, reg_def):
''' Process a register '''
if isinstance(reg_def, callframe.CFARule):
if reg_def.reg is not None:
out_reg.regs[reg_def.reg] += 1
else:
pass # TODO exprs
else:
self.incr_cell(out_reg.instrs, DwarfInstr.of_pyelf(reg_def.type))
if reg_def.type == callframe.RegisterRule.REGISTER:
out_reg.regs[reg_def.arg] += 1
elif (reg_def.type == callframe.RegisterRule.EXPRESSION) \
or (reg_def.type == callframe.RegisterRule.VAL_EXPRESSION):
pass # TODO exprs
def gather_system_files(config): def gather_system_files(config, sample_size=None):
stats_accu = StatsAccumulator() stats_accu = StatsAccumulator()
processor = FilesProcessor(config.cores, stats_accu) processors = []
def signal_graceful_exit(sig, frame): def signal_graceful_exit(sig, frame):
''' Stop gracefully now ''' ''' Stop gracefully now '''
nonlocal processor nonlocal processors
print("Stopping after this ELF…") print("Stopping after this ELF…")
for processor in processors:
processor.stop_processing_now() processor.stop_processing_now()
signal.signal(signal.SIGINT, signal_graceful_exit) signal.signal(signal.SIGINT, signal_graceful_exit)
@ -55,6 +122,50 @@ def gather_system_files(config):
for elf_path in system_elfs(): for elf_path in system_elfs():
elf_list.append(elf_path) elf_list.append(elf_path)
processor.run(elf_list) if sample_size is not None:
elf_list_sampled = random.sample(elf_list, sample_size)
elf_list = elf_list_sampled
elf_count = len(elf_list)
elf_per_process = elf_count // config.cores
elf_list_slices = []
for i in range(config.cores - 1):
elf_list_slices.append(
elf_list[i * elf_per_process : (i+1) * elf_per_process])
elf_list_slices.append(
elf_list[(config.cores - 1) * elf_per_process
: config.cores * elf_per_process])
shared_queue = multiprocessing.Queue(elf_count)
for elf_range in elf_list_slices:
processors.append(FilesProcessor(elf_range, shared_queue))
if config.cores > 1:
for processor in processors:
processor.start()
while True:
for processor in processors:
if processor.is_alive():
print("== Waiting {} ({} {}) ==".format(
processor.name, processor.exitcode,
processor.is_alive()))
processor.join(timeout=1)
if processor.exitcode is None:
break # Loop around
print("== Joined {} ==".format(processor.name))
terminated = True
for processor in processors:
if processor.exitcode is None:
terminated = False
if terminated:
break
else:
processors[0].run() # run(), not start(): in the same thread
while not shared_queue.empty(): # Reliable because everything is joined
stats_accu.add_fde(shared_queue.get_nowait())
return stats_accu return stats_accu

View file

@ -2,6 +2,7 @@
from elftools.elf.elffile import ELFFile from elftools.elf.elffile import ELFFile
from elftools.common.exceptions import ELFError, DWARFError from elftools.common.exceptions import ELFError, DWARFError
from stats_accu import ElfType
import os import os
@ -44,20 +45,20 @@ def system_elfs():
os.readlink(path))) os.readlink(path)))
sysbin_dirs = [ sysbin_dirs = [
'/lib', ('/lib', ElfType.ELF_LIB),
'/usr/lib', ('/usr/lib', ElfType.ELF_LIB),
'/usr/local/lib', ('/usr/local/lib', ElfType.ELF_LIB),
'/bin', ('/bin', ElfType.ELF_BINARY),
'/usr/bin', ('/usr/bin', ElfType.ELF_BINARY),
'/usr/local/bin', ('/usr/local/bin', ElfType.ELF_BINARY),
'/sbin', ('/sbin', ElfType.ELF_BINARY),
] ]
to_explore = sysbin_dirs to_explore = sysbin_dirs
seen_elfs = set() seen_elfs = set()
while to_explore: while to_explore:
bindir = to_explore.pop() bindir, elftype = to_explore.pop()
if not os.path.isdir(bindir): if not os.path.isdir(bindir):
continue continue
@ -65,12 +66,23 @@ def system_elfs():
for direntry in os.scandir(bindir): for direntry in os.scandir(bindir):
if not direntry.is_file(): if not direntry.is_file():
if direntry.is_dir(): if direntry.is_dir():
to_explore.append(direntry.path) to_explore.append((direntry.path, elftype))
continue continue
canonical_name = readlink_rec(direntry.path) canonical_name = readlink_rec(direntry.path)
if canonical_name in seen_elfs: if canonical_name in seen_elfs:
continue continue
valid_elf = True
try:
with open(canonical_name, 'rb') as handle:
magic_bytes = handle.read(4)
if magic_bytes != b'\x7fELF':
valid_elf = False
except Exception:
continue
if not valid_elf:
continue
seen_elfs.add(canonical_name) seen_elfs.add(canonical_name)
yield canonical_name yield (canonical_name, elftype)

View file

@ -1,9 +1,9 @@
from elftools.dwarf import callframe from elftools.dwarf import callframe
from pyelftools_overlay import get_cfi import enum
from enum import Enum
import json
import subprocess import subprocess
import re import re
import json
import collections
from math import ceil from math import ceil
@ -69,109 +69,195 @@ def elf_so_deps(path):
"{}.").format(path, exn.returncode)) "{}.").format(path, exn.returncode))
class ElfType(Enum): class ElfType(enum.Enum):
ELF_LIB = auto() ELF_LIB = enum.auto()
ELF_BINARY = auto() ELF_BINARY = enum.auto()
class DwarfInstr(enum.Enum):
@staticmethod
def of_pyelf(val):
_table = {
callframe.RegisterRule.UNDEFINED: DwarfInstr.INSTR_UNDEF,
callframe.RegisterRule.SAME_VALUE: DwarfInstr.INSTR_SAME_VALUE,
callframe.RegisterRule.OFFSET: DwarfInstr.INSTR_OFFSET,
callframe.RegisterRule.VAL_OFFSET: DwarfInstr.INSTR_VAL_OFFSET,
callframe.RegisterRule.REGISTER: DwarfInstr.INSTR_REGISTER,
callframe.RegisterRule.EXPRESSION: DwarfInstr.INSTR_EXPRESSION,
callframe.RegisterRule.VAL_EXPRESSION:
DwarfInstr.INSTR_VAL_EXPRESSION,
callframe.RegisterRule.ARCHITECTURAL:
DwarfInstr.INSTR_ARCHITECTURAL,
}
return _table[val]
INSTR_UNDEF = enum.auto()
INSTR_SAME_VALUE = enum.auto()
INSTR_OFFSET = enum.auto()
INSTR_VAL_OFFSET = enum.auto()
INSTR_REGISTER = enum.auto()
INSTR_EXPRESSION = enum.auto()
INSTR_VAL_EXPRESSION = enum.auto()
INSTR_ARCHITECTURAL = enum.auto()
def intify_dict(d):
out = {}
for key in d:
try:
nKey = int(key)
except Exception:
nKey = key
try:
out[nKey] = int(d[key])
except ValueError:
out[nKey] = d[key]
return out
class RegData:
def __init__(self, instrs=None, regs=None, exprs=None):
if instrs is None:
instrs = {}
if regs is None:
regs = [0]*17
if exprs is None:
exprs = {}
self.instrs = intify_dict(instrs)
self.regs = regs
self.exprs = intify_dict(exprs)
@staticmethod
def map_dict_keys(fnc, dic):
out = {}
for key in dic:
out[fnc(key)] = dic[key]
return out
def dump(self):
return {
'instrs': RegData.map_dict_keys(lambda x: x.value, self.instrs),
'regs': self.regs,
'exprs': self.exprs,
}
@staticmethod
def load(data):
return RegData(
instrs=RegData.map_dict_keys(
lambda x: DwarfInstr(int(x)),
data['instrs']),
regs=data['regs'],
exprs=data['exprs'],
)
class RegsList:
def __init__(self, cfa=None, regs=None):
if cfa is None:
cfa = RegsList.fresh_reg()
if regs is None:
regs = [RegsList.fresh_reg() for _ in range(17)]
self.cfa = cfa
self.regs = regs
@staticmethod
def fresh_reg():
return RegData()
def dump(self):
return {
'cfa': RegData.dump(self.cfa),
'regs': [RegData.dump(r) for r in self.regs],
}
@staticmethod
def load(data):
return RegsList(
cfa=RegData.load(data['cfa']),
regs=[RegData.load(r) for r in data['regs']],
)
class FdeData:
def __init__(self, fde_count=0, fde_with_lines=None, regs=None):
if fde_with_lines is None:
fde_with_lines = {}
if regs is None:
regs = RegsList()
self.fde_count = fde_count
self.fde_with_lines = intify_dict(fde_with_lines)
self.regs = regs
def dump(self):
return {
'fde_count': self.fde_count,
'fde_with_lines': self.fde_with_lines,
'regs': self.regs.dump(),
}
@staticmethod
def load(data):
return FdeData(
fde_count=int(data['fde_count']),
fde_with_lines=data['fde_with_lines'],
regs=RegsList.load(data['regs']))
class SingleFdeData: class SingleFdeData:
def __init__(self, path, elf_type, data): def __init__(self, path, elf_type, data):
self.path = path self.path = path
self.elf_type = elf_type self.elf_type = elf_type
self.data = data self.data = data # < of type FdeData
self.gather_deps() self.gather_deps()
def gather_deps(self): def gather_deps(self):
""" Collect ldd data on the binary """ """ Collect ldd data on the binary """
self.deps = elf_so_deps(self.path) # self.deps = elf_so_deps(self.path)
self.deps = []
def dump(self):
return {
'path': self.path,
'elf_type': self.elf_type.value,
'data': self.data.dump()
}
@staticmethod
def load(data):
return SingleFdeData(
data['path'],
ElfType(int(data['elf_type'])),
FdeData.load(data['data']))
class StatsAccumulator: class StatsAccumulator:
def __init__(self): def __init__(self):
self.elf_count = 0 self.fdes = []
self.fde_count = 0
self.fde_row_count = 0
self.fde_with_n_rows = {}
def serialize(self, path): def add_fde(self, fde_data):
''' Save the gathered data to `stream` ''' self.fdes.append(fde_data)
notable_fields = [ def get_fdes(self):
'elf_count', return self.fdes
'fde_count',
'fde_row_count',
'fde_with_n_rows',
]
out = {}
for field in notable_fields:
out[field] = self.__dict__[field]
with open(path, 'wb') as stream: def add_stats_accu(self, stats_accu):
json.dump(out, stream) for fde in stats_accu.get_fdes():
self.add_fde(fde)
def dump(self, path):
dict_form = [fde.dump() for fde in self.fdes]
print(dict_form)
with open(path, 'w') as handle:
handle.write(json.dumps(dict_form))
@staticmethod @staticmethod
def unserialize(path): def load(path):
with open(path, 'r') as handle:
text = handle.read()
out = StatsAccumulator() out = StatsAccumulator()
with open(path, 'wb') as stream: out.fdes = [SingleFdeData.load(data) for data in json.loads(text)]
data = json.load(stream)
for field in data:
out.field = data[field]
return out return out
def report(self):
''' Report on the statistics gathered '''
self.fde_rows_proportion = ProportionFinder(
self.fde_with_n_rows)
rows = [
("ELFs analyzed", self.elf_count),
("FDEs analyzed", self.fde_count),
("FDE rows analyzed", self.fde_row_count),
("Avg. rows per FDE", self.fde_row_count / self.fde_count),
("Median rows per FDE",
self.fde_rows_proportion.find_at_proportion(0.5)),
("Max rows per FDE", max(self.fde_with_n_rows.keys())),
]
title_size = max(map(lambda x: len(x[0]), rows))
line_format = "{:<" + str(title_size + 1) + "} {}"
for row in rows:
print(line_format.format(row[0], row[1]))
def process_file(self, path):
''' Process a single file '''
cfi = get_cfi(path)
if not cfi:
return
self.elf_count += 1
for entry in cfi:
if isinstance(entry, callframe.CIE): # Is a CIE
self.process_cie(entry)
elif isinstance(entry, callframe.FDE): # Is a FDE
self.process_fde(entry)
def incr_cell(self, table, key):
''' Increments table[key], or sets it to 1 if unset '''
if key in table:
table[key] += 1
else:
table[key] = 1
def process_cie(self, cie):
''' Process a CIE '''
pass # Nothing needed from a CIE
def process_fde(self, fde):
''' Process a FDE '''
self.fde_count += 1
decoded = fde.get_decoded()
row_count = len(decoded.table)
self.fde_row_count += row_count
self.incr_cell(self.fde_with_n_rows, row_count)