#!/usr/bin/env python3 # # Copyright (c) 2020 Intel Corporation # # SPDX-License-Identifier: Apache-2.0 """ Dictionary-based Logging Database Generator This takes the built Zephyr ELF binary and produces a JSON database file for dictionary-based logging. This database is used together with the parser to decode binary log messages. """ import argparse import logging import os import struct import sys import dictionary_parser.log_database from dictionary_parser.log_database import LogDatabase import elftools from elftools.elf.elffile import ELFFile from elftools.elf.descriptions import describe_ei_data from elftools.elf.sections import SymbolTableSection LOGGER_FORMAT = "%(name)s: %(levelname)s: %(message)s" logger = logging.getLogger(os.path.basename(sys.argv[0])) # Sections that contains static strings STATIC_STRING_SECTIONS = ['rodata', '.rodata', 'log_strings_sections'] def parse_args(): """Parse command line arguments""" argparser = argparse.ArgumentParser() argparser.add_argument("elffile", help="Zephyr ELF binary") argparser.add_argument("dbfile", help="Dictionary Logging Database file") argparser.add_argument("--build", help="Build ID") argparser.add_argument("--debug", action="store_true", help="Print extra debugging information") argparser.add_argument("-v", "--verbose", action="store_true", help="Print more information") return argparser.parse_args() def find_elf_sections(elf, sh_name): """Find all sections in ELF file""" for section in elf.iter_sections(): if section.name == sh_name: ret = { 'name' : section.name, 'size' : section['sh_size'], 'start' : section['sh_addr'], 'end' : section['sh_addr'] + section['sh_size'] - 1, 'data' : section.data(), } return ret return None def get_kconfig_symbols(elf): """Get kconfig symbols from the ELF file""" for section in elf.iter_sections(): if isinstance(section, SymbolTableSection): return {sym.name: sym.entry.st_value for sym in section.iter_symbols() if sym.name.startswith("CONFIG_")} raise LookupError("Could not find symbol table") def find_log_const_symbols(elf): """Extract all "log_const_*" symbols from ELF file""" symbol_tables = [s for s in elf.iter_sections() if isinstance(s, elftools.elf.sections.SymbolTableSection)] ret_list = [] for section in symbol_tables: if not isinstance(section, elftools.elf.sections.SymbolTableSection): continue if section['sh_entsize'] == 0: continue for symbol in section.iter_symbols(): if symbol.name.startswith("log_const_"): ret_list.append(symbol) return ret_list def parse_log_const_symbols(database, log_const_section, log_const_symbols): """Find the log instances and map source IDs to names""" if database.is_tgt_little_endian(): formatter = "<" else: formatter = ">" if database.is_tgt_64bit(): # 64-bit pointer to string formatter += "Q" else: # 32-bit pointer to string formatter += "L" # log instance level formatter += "B" datum_size = struct.calcsize(formatter) # Get the address of first log instance first_offset = log_const_symbols[0].entry['st_value'] for sym in log_const_symbols: if sym.entry['st_value'] < first_offset: first_offset = sym.entry['st_value'] first_offset -= log_const_section['start'] # find all log_const_* for sym in log_const_symbols: # Find data offset in log_const_section for this symbol offset = sym.entry['st_value'] - log_const_section['start'] idx_s = offset idx_e = offset + datum_size datum = log_const_section['data'][idx_s:idx_e] if len(datum) != datum_size: # Not enough data to unpack continue str_ptr, level = struct.unpack(formatter, datum) # Offset to rodata section for string instance_name = database.find_string(str_ptr) logger.info("Found Log Instance: %s, level: %d", instance_name, level) # source ID is simply the element index in the log instance array source_id = int((offset - first_offset) / sym.entry['st_size']) database.add_log_instance(source_id, instance_name, level, sym.entry['st_value']) def extract_elf_information(elf, database): """Extract information from ELF file and store in database""" e_ident = elf.header['e_ident'] elf_data = describe_ei_data(e_ident['EI_DATA']) if elf_data == elftools.elf.descriptions._DESCR_EI_DATA['ELFDATA2LSB']: database.set_tgt_endianness(LogDatabase.LITTLE_ENDIAN) elif elf_data == elftools.elf.descriptions._DESCR_EI_DATA['ELFDATA2MSB']: database.set_tgt_endianness(LogDatabase.BIG_ENDIAN) else: logger.error("Cannot determine endianness from ELF file, exiting...") sys.exit(1) def process_kconfigs(elf, database): """Process kconfigs to extract information""" kconfigs = get_kconfig_symbols(elf) # 32 or 64-bit target database.set_tgt_bits(64 if "CONFIG_64BIT" in kconfigs else 32) # Architecture for name, arch in dictionary_parser.log_database.ARCHS.items(): if arch['kconfig'] in kconfigs: database.set_arch(name) break # Put some kconfigs into the database # # Use 32-bit timestamp? or 64-bit? if "CONFIG_LOG_TIMESTAMP_64BIT" in kconfigs: database.add_kconfig("CONFIG_LOG_TIMESTAMP_64BIT", kconfigs['CONFIG_LOG_TIMESTAMP_64BIT']) def extract_static_string_sections(elf, database): """Extract sections containing static strings""" string_sections = STATIC_STRING_SECTIONS # Some architectures may put static strings into additional sections. # So need to extract them too. arch_data = dictionary_parser.log_database.ARCHS[database.get_arch()] if "extra_string_section" in arch_data: string_sections.extend(arch_data['extra_string_section']) for name in string_sections: content = find_elf_sections(elf, name) if content is None: continue logger.info("Found section: %s, 0x%x - 0x%x", name, content['start'], content['end']) database.add_string_section(name, content) if not database.has_string_sections(): logger.error("Cannot find any static string sections in ELF, exiting...") sys.exit(1) def extract_logging_subsys_information(elf, database): """ Extract logging subsys related information and store in database. For example, this extracts the list of log instances to establish mapping from source ID to name. """ # Extract log constant section for module names section_log_const = find_elf_sections(elf, "log_const_sections") if section_log_const is None: # ESP32 puts "log_const_*" info log_static_section instead of log_const_sections section_log_const = find_elf_sections(elf, "log_static_section") if section_log_const is None: logger.error("Cannot find section 'log_const_sections' in ELF file, exiting...") sys.exit(1) # Find all "log_const_*" symbols and parse them log_const_symbols = find_log_const_symbols(elf) parse_log_const_symbols(database, section_log_const, log_const_symbols) def main(): """Main function of database generator""" args = parse_args() # Setup logging logging.basicConfig(format=LOGGER_FORMAT) if args.verbose: logger.setLevel(logging.INFO) elif args.debug: logger.setLevel(logging.DEBUG) else: logger.setLevel(logging.WARNING) elffile = open(args.elffile, "rb") if not elffile: logger.error("ERROR: Cannot open ELF file: %s, exiting...", args.elffile) sys.exit(1) logger.info("ELF file %s", args.elffile) logger.info("Database file %s", args.dbfile) elf = ELFFile(elffile) database = LogDatabase() if args.build: database.set_build_id(args.build) logger.info("Build ID: %s", args.build) extract_elf_information(elf, database) process_kconfigs(elf, database) logger.info("Target: %s, %d-bit", database.get_arch(), database.get_tgt_bits()) if database.is_tgt_little_endian(): logger.info("Endianness: Little") else: logger.info("Endianness: Big") # Extract sections from ELF files that contain strings extract_static_string_sections(elf, database) # Extract information related to logging subsystem extract_logging_subsys_information(elf, database) # Write database file if not LogDatabase.write_json_database(args.dbfile, database): logger.error("ERROR: Cannot open database file for write: %s, exiting...", args.dbfile) sys.exit(1) elffile.close() if __name__ == "__main__": main()