1#!/usr/bin/env python3
2#
3# Copyright (c) 2020 Intel Corporation
4#
5# SPDX-License-Identifier: Apache-2.0
6
7"""
8Dictionary-based Logging Database Generator
9
10This takes the built Zephyr ELF binary and produces a JSON database
11file for dictionary-based logging. This database is used together
12with the parser to decode binary log messages.
13"""
14
15import argparse
16import logging
17import os
18import struct
19import sys
20
21import dictionary_parser.log_database
22from dictionary_parser.log_database import LogDatabase
23
24import elftools
25from elftools.elf.elffile import ELFFile
26from elftools.elf.descriptions import describe_ei_data
27from elftools.elf.sections import SymbolTableSection
28
29
30LOGGER_FORMAT = "%(name)s: %(levelname)s: %(message)s"
31logger = logging.getLogger(os.path.basename(sys.argv[0]))
32
33
34# Sections that contains static strings
35STATIC_STRING_SECTIONS = ['rodata', '.rodata', 'log_strings_sections']
36
37
38def parse_args():
39    """Parse command line arguments"""
40    argparser = argparse.ArgumentParser()
41
42    argparser.add_argument("elffile", help="Zephyr ELF binary")
43    argparser.add_argument("dbfile", help="Dictionary Logging Database file")
44    argparser.add_argument("--build", help="Build ID")
45    argparser.add_argument("--debug", action="store_true",
46                           help="Print extra debugging information")
47    argparser.add_argument("-v", "--verbose", action="store_true",
48                           help="Print more information")
49
50    return argparser.parse_args()
51
52
53def find_elf_sections(elf, sh_name):
54    """Find all sections in ELF file"""
55    for section in elf.iter_sections():
56        if section.name == sh_name:
57            ret = {
58                'name'    : section.name,
59                'size'    : section['sh_size'],
60                'start'   : section['sh_addr'],
61                'end'     : section['sh_addr'] + section['sh_size'] - 1,
62                'data'    : section.data(),
63            }
64
65            return ret
66
67    return None
68
69
70def get_kconfig_symbols(elf):
71    """Get kconfig symbols from the ELF file"""
72    for section in elf.iter_sections():
73        if isinstance(section, SymbolTableSection):
74            return {sym.name: sym.entry.st_value
75                    for sym in section.iter_symbols()
76                       if sym.name.startswith("CONFIG_")}
77
78    raise LookupError("Could not find symbol table")
79
80
81def find_log_const_symbols(elf):
82    """Extract all "log_const_*" symbols from ELF file"""
83    symbol_tables = [s for s in elf.iter_sections()
84                     if isinstance(s, elftools.elf.sections.SymbolTableSection)]
85
86    ret_list = []
87
88    for section in symbol_tables:
89        if not isinstance(section, elftools.elf.sections.SymbolTableSection):
90            continue
91
92        if section['sh_entsize'] == 0:
93            continue
94
95        for symbol in section.iter_symbols():
96            if symbol.name.startswith("log_const_"):
97                ret_list.append(symbol)
98
99    return ret_list
100
101
102def parse_log_const_symbols(database, log_const_section, log_const_symbols):
103    """Find the log instances and map source IDs to names"""
104    if database.is_tgt_little_endian():
105        formatter = "<"
106    else:
107        formatter = ">"
108
109    if database.is_tgt_64bit():
110        # 64-bit pointer to string
111        formatter += "Q"
112    else:
113        # 32-bit pointer to string
114        formatter += "L"
115
116    # log instance level
117    formatter += "B"
118
119    datum_size = struct.calcsize(formatter)
120
121    # Get the address of first log instance
122    first_offset = log_const_symbols[0].entry['st_value']
123    for sym in log_const_symbols:
124        if sym.entry['st_value'] < first_offset:
125            first_offset = sym.entry['st_value']
126
127    first_offset -= log_const_section['start']
128
129    # find all log_const_*
130    for sym in log_const_symbols:
131        # Find data offset in log_const_section for this symbol
132        offset = sym.entry['st_value'] - log_const_section['start']
133
134        idx_s = offset
135        idx_e = offset + datum_size
136
137        datum = log_const_section['data'][idx_s:idx_e]
138
139        if len(datum) != datum_size:
140            # Not enough data to unpack
141            continue
142
143        str_ptr, level = struct.unpack(formatter, datum)
144
145        # Offset to rodata section for string
146        instance_name = database.find_string(str_ptr)
147
148        logger.info("Found Log Instance: %s, level: %d", instance_name, level)
149
150        # source ID is simply the element index in the log instance array
151        source_id = int((offset - first_offset) / sym.entry['st_size'])
152
153        database.add_log_instance(source_id, instance_name, level, sym.entry['st_value'])
154
155
156def extract_elf_information(elf, database):
157    """Extract information from ELF file and store in database"""
158    e_ident = elf.header['e_ident']
159    elf_data = describe_ei_data(e_ident['EI_DATA'])
160
161    if elf_data == elftools.elf.descriptions._DESCR_EI_DATA['ELFDATA2LSB']:
162        database.set_tgt_endianness(LogDatabase.LITTLE_ENDIAN)
163    elif elf_data == elftools.elf.descriptions._DESCR_EI_DATA['ELFDATA2MSB']:
164        database.set_tgt_endianness(LogDatabase.BIG_ENDIAN)
165    else:
166        logger.error("Cannot determine endianness from ELF file, exiting...")
167        sys.exit(1)
168
169
170def process_kconfigs(elf, database):
171    """Process kconfigs to extract information"""
172    kconfigs = get_kconfig_symbols(elf)
173
174    # 32 or 64-bit target
175    database.set_tgt_bits(64 if "CONFIG_64BIT" in kconfigs else 32)
176
177    # Architecture
178    for name, arch in dictionary_parser.log_database.ARCHS.items():
179        if arch['kconfig'] in kconfigs:
180            database.set_arch(name)
181            break
182
183    # Put some kconfigs into the database
184    #
185    # Use 32-bit timestamp? or 64-bit?
186    if "CONFIG_LOG_TIMESTAMP_64BIT" in kconfigs:
187        database.add_kconfig("CONFIG_LOG_TIMESTAMP_64BIT",
188                             kconfigs['CONFIG_LOG_TIMESTAMP_64BIT'])
189
190
191def extract_static_string_sections(elf, database):
192    """Extract sections containing static strings"""
193    string_sections = STATIC_STRING_SECTIONS
194
195    # Some architectures may put static strings into additional sections.
196    # So need to extract them too.
197    arch_data = dictionary_parser.log_database.ARCHS[database.get_arch()]
198    if "extra_string_section" in arch_data:
199        string_sections.extend(arch_data['extra_string_section'])
200
201    for name in string_sections:
202        content = find_elf_sections(elf, name)
203        if content is None:
204            continue
205
206        logger.info("Found section: %s, 0x%x - 0x%x",
207                    name, content['start'], content['end'])
208        database.add_string_section(name, content)
209
210    if not database.has_string_sections():
211        logger.error("Cannot find any static string sections in ELF, exiting...")
212        sys.exit(1)
213
214
215def extract_logging_subsys_information(elf, database):
216    """
217    Extract logging subsys related information and store in database.
218
219    For example, this extracts the list of log instances to establish
220    mapping from source ID to name.
221    """
222    # Extract log constant section for module names
223    section_log_const = find_elf_sections(elf, "log_const_sections")
224    if section_log_const is None:
225        # ESP32 puts "log_const_*" info log_static_section instead of log_const_sections
226        section_log_const = find_elf_sections(elf, "log_static_section")
227
228    if section_log_const is None:
229        logger.error("Cannot find section 'log_const_sections' in ELF file, exiting...")
230        sys.exit(1)
231
232    # Find all "log_const_*" symbols and parse them
233    log_const_symbols = find_log_const_symbols(elf)
234    parse_log_const_symbols(database, section_log_const, log_const_symbols)
235
236
237def main():
238    """Main function of database generator"""
239    args = parse_args()
240
241    # Setup logging
242    logging.basicConfig(format=LOGGER_FORMAT)
243    if args.verbose:
244        logger.setLevel(logging.INFO)
245    elif args.debug:
246        logger.setLevel(logging.DEBUG)
247    else:
248        logger.setLevel(logging.WARNING)
249
250    elffile = open(args.elffile, "rb")
251    if not elffile:
252        logger.error("ERROR: Cannot open ELF file: %s, exiting...", args.elffile)
253        sys.exit(1)
254
255    logger.info("ELF file %s", args.elffile)
256    logger.info("Database file %s", args.dbfile)
257
258    elf = ELFFile(elffile)
259
260    database = LogDatabase()
261
262    if args.build:
263        database.set_build_id(args.build)
264        logger.info("Build ID: %s", args.build)
265
266    extract_elf_information(elf, database)
267
268    process_kconfigs(elf, database)
269
270    logger.info("Target: %s, %d-bit", database.get_arch(), database.get_tgt_bits())
271    if database.is_tgt_little_endian():
272        logger.info("Endianness: Little")
273    else:
274        logger.info("Endianness: Big")
275
276    # Extract sections from ELF files that contain strings
277    extract_static_string_sections(elf, database)
278
279    # Extract information related to logging subsystem
280    extract_logging_subsys_information(elf, database)
281
282    # Write database file
283    if not LogDatabase.write_json_database(args.dbfile, database):
284        logger.error("ERROR: Cannot open database file for write: %s, exiting...", args.dbfile)
285        sys.exit(1)
286
287    elffile.close()
288
289
290if __name__ == "__main__":
291    main()
292