emdbg.analyze.inline

Analyze inline function usage

Function inlining generally is a space-time tradeoff where more inlining helps with execution speed but increases FLASH usage. This tool helps to see which functions are inlined where and how much FLASH usage this inlining causes.

Command Line Interface

You can analyze the inline usage like this:

python3 -m emdbg.analyze.inline -f test.elf

The analysis can take some time as it has to traverse all DIEs of the DWARF data.

  1# Copyright (c) 2024, Alexander Lerach
  2# Copyright (c) 2024, Auterion AG
  3# SPDX-License-Identifier: BSD-3-Clause
  4
  5"""
  6# Analyze inline function usage
  7
  8Function inlining generally is a space-time tradeoff where more inlining helps
  9with execution speed but increases FLASH usage. This tool helps to see which
 10functions are inlined where and how much FLASH usage this inlining causes.
 11
 12## Command Line Interface
 13
 14You can analyze the inline usage like this:
 15
 16```sh
 17python3 -m emdbg.analyze.inline -f test.elf
 18```
 19
 20The analysis can take some time as it has to traverse all DIEs of the DWARF data.
 21"""
 22
 23from __future__ import annotations
 24from collections import defaultdict
 25from dataclasses import dataclass
 26from pathlib import Path
 27import argparse
 28import logging
 29import posixpath
 30
 31from elftools.elf.elffile import ELFFile
 32from elftools.dwarf.die import DIE
 33from elftools.dwarf.lineprogram import LineProgram
 34from elftools.dwarf.ranges import RangeEntry, RangeLists
 35
 36import rich
 37from rich.console import Console
 38from rich.table import Table
 39
 40_LOGGER = logging.getLogger(__name__)
 41
 42
 43def _rel_file_name(file_name: Path) -> str:
 44    """Return the filename relative to the CWD"""
 45    try: return str(Path(file_name).relative_to(Path().cwd()))
 46    except ValueError: return str(Path(file_name))
 47
 48# -----------------------------------------------------------------------------
 49@dataclass
 50class InlinedInstance:
 51    """
 52    See `InlinedFunction`.
 53    """
 54    translation_unit: str
 55    """name of the translation unit this instance is used in."""
 56    file_name: Path
 57    """file where the function is inlined to."""
 58    file_line: int
 59    """line within the file where the function is inlined."""
 60    size: int
 61    """amount of flash used by the inlined function in this instance."""
 62
 63
 64# -----------------------------------------------------------------------------
 65@dataclass
 66class InlinedFunction:
 67    """
 68    The `InlinedFunction` represents a function that is inlined into different callers.
 69    It contains a list of `InlinedInstance` which represent the instance that this function is inlined into.
 70    """
 71    file_name: Path
 72    """file where the inline function is declared."""
 73    file_line: int
 74    """line within the file where the inline function is declared."""
 75    total_size: int
 76    """total amount of flash used due to this method being inline."""
 77    total_size_wo_overhead: int
 78    """total amount of flash used due to this method being inline, reduced by the function call instruction overheads."""
 79    inlined_instances: list[InlinedInstance]
 80    """list of instances where this function is inlined to."""
 81
 82    def print(self, console: Console, num_of_instances: int, total_size: int, total_size_wo_overhead: int, call_overhead: int) -> str:
 83        """
 84        Prints the inlined function and its instances to the console in an easy-to-read format.
 85
 86        :param console: console to print the output to.
 87        :param num_of_instances: the number of inlined instances to print. A value of `0` causes all to be displayed.
 88        :param total_size: total amount of FLASH used by inlined functions. Used to calculate the percentage caused by this inlined function.
 89        :param total_size_wo_overhead: total amount of FLASH used by inlined functions, reduced by the function call instruction overheads.
 90        :param call_overhead: expected instruction overhead caused by a function call.
 91        """
 92        console.print((
 93            f"{_rel_file_name(self.file_name)}:{self.file_line}"
 94            f" -- Total Size: {self.total_size}"
 95            f" ({(100 if total_size == 0 else (self.total_size / total_size * 100)):.2f}%)"
 96            f" -- Total Size without instruction overhead: {self.total_size_wo_overhead}"
 97            f" ({(100 if total_size_wo_overhead == 0 else (self.total_size_wo_overhead / total_size_wo_overhead * 100)):.2f}%)"
 98        ))
 99
100        table = Table(box=rich.box.MINIMAL_DOUBLE_HEAD)
101        table.add_column("Translation unit")
102        table.add_column("File name")
103        table.add_column("File line")
104        table.add_column("Size")
105        table.add_column("Call instruction overhead")
106        table.add_column("%")
107
108        for i, inlined_instance in enumerate(self.inlined_instances):
109            if i < num_of_instances or num_of_instances == 0:
110                table.add_row(_rel_file_name(inlined_instance.translation_unit), _rel_file_name(inlined_instance.file_name), str(inlined_instance.file_line),
111                              str(inlined_instance.size), str(call_overhead),
112                              "{:.2f}".format(100 if self.total_size == 0 else inlined_instance.size / self.total_size * 100))
113            else:
114                table.add_row("...", "...", "...")
115                break
116
117        console.print(table)
118
119
120# -----------------------------------------------------------------------------
121@dataclass
122class AnalysisResult:
123    """
124    The class `AnalysisResult` represents the result of an inline analysis performed using `InlineAnalyzer`
125    """
126    inlined_savings_list: list[InlinedFunction]
127    """inlined functions with more than one instance. These allow for potential FLASH savings."""
128    inlined_no_savings_list: list[InlinedFunction]
129    """inlined functions with one instance."""
130    savings_total_size: int
131    """overall FLASH size used by inlined functions with more than one instance."""
132    no_savings_total_size: int
133    """overall FLASH size used by inlined functions with one instance."""
134    savings_total_size_wo_overhead: int
135    """overall FLASH size used by inlined functions with more than one instance, reduced by function call instruction overheads."""
136    no_savings_total_size_wo_overhead: int
137    """overall FLASH size used by inlined functions with one instance, reduced by function call instruction overheads."""
138
139
140# -----------------------------------------------------------------------------
141class InlineAnalyzer:
142    """
143    Analyzes an ELF file and DWARF debugging data to identify inline functions and the instances where they are inlined to.
144    This allows to identify options for a space-time tradeoff.
145
146    :param call_overhead: expected instruction overhead caused by a function call.
147    """
148    def __init__(self, call_overhead: int):
149        self._call_overhead = call_overhead
150        self._raw_inlined_functions = defaultdict(list)
151
152
153    def get_inlined_functions(self, file_name: str) -> AnalysisResult:
154        """
155        Returns the identified `InlinedFunction` in the given ELF file.
156        This is only possible if DWARF debugging data is available which contains debug ranges and line program.
157
158        :param file_name: path to the ELF file to analyze.
159
160        :return: on success, a `AnalysisResult` is returned. In case of errors an exception is raised.
161
162        :raises ValueError: if the debugging data is not sufficient of the analysis.
163        """
164        _LOGGER.info(f"Processing file: {file_name}")
165        self._raw_inlined_functions.clear()
166
167        with open(file_name, "rb") as f:
168            elf_file = ELFFile(f)
169
170            if not elf_file.has_dwarf_info():
171                raise ValueError(f"{file_name} has no DWARF info.")
172
173            dwarf_info = elf_file.get_dwarf_info()
174            range_lists = dwarf_info.range_lists()
175
176            for CU in dwarf_info.iter_CUs():
177                line_program = dwarf_info.line_program_for_CU(CU)
178
179                if line_program is None:
180                    _LOGGER.warning("CU @ {CU.cu_offset} DWARF info is missing line program. Skipping CU.")
181                    continue
182
183                top_die = CU.get_top_DIE()
184                self.die_get_inlined_rec(top_die, line_program, range_lists)
185        return self.raw_inlined_to_output()
186
187
188    def die_get_inlined_rec(self, die: DIE, line_program: LineProgram, range_lists: RangeLists):
189        """
190        Recursively traverse all DIEs of a given top DIE and extract information about inlined functions.
191
192        :param die: DIE to be processed. Is gathered recursively after passing a top DIE.
193        :param line_program: `LineProgram` extracted from the DWARF debugging data.
194        :param range_lists: `RangeLists` extracted from the DWARF debugging data.
195        """
196        if die.tag == "DW_TAG_inlined_subroutine" and {"DW_AT_call_file"} <= die.attributes.keys():
197            call_file = self.get_file_name(die.attributes["DW_AT_call_file"].value, line_program)
198            call_line = die.attributes["DW_AT_call_line"].value
199            size = self.get_size(die, range_lists)
200
201            decl_die = self.resolve_die_ref(die, die.cu.cu_offset + die.attributes["DW_AT_abstract_origin"].value)
202
203            if {"DW_AT_decl_file", "DW_AT_decl_line"} <= decl_die.attributes.keys():
204                decl_file = self.get_file_name(decl_die.attributes["DW_AT_decl_file"].value, line_program)
205                decl_line = decl_die.attributes["DW_AT_decl_line"].value
206                translation_unit_name = self.get_translation_unit_name(die)
207
208                called_function = InlinedInstance(translation_unit_name, call_file, call_line, size)
209                self._raw_inlined_functions[(decl_file, decl_line)].append(called_function)
210
211        # Recurse into the DIE children
212        for child in die.iter_children():
213                self.die_get_inlined_rec(child, line_program, range_lists)
214
215
216    def get_file_name(self, file_idx: int, line_program: LineProgram) -> Path:
217        """
218        Returns a file name given a DIE file index. To perform this mapping the line program is required.
219
220        :param file_idx: DIE file index for which the file name shall be returned.
221        :param line_program: `LineProgram` extracted from the DWARF debugging data.
222
223        :return: the file name for the given DIE file index. This will include the full path if the line program
224                 contains the relevant data. Otherwise, only the file name without path will be returned.
225        """
226        lp_header = line_program.header
227        file_entries = lp_header["file_entry"]
228        file_entry = file_entries[file_idx - 1]
229        dir_index = file_entry["dir_index"]
230
231        if dir_index == 0:
232            return Path(file_entry.name.decode())
233
234        directory = lp_header["include_directory"][dir_index - 1]
235        return Path(directory.decode()) / file_entry.name.decode()
236
237    def get_translation_unit_name(self, die: DIE) -> str:
238        """
239        Returns the name of the translation unit the given DIE is contained in. If the name can't be retrieved,
240        an empty string will be returned.
241
242        :param die: DIE for which the translation unit name shall be returned.
243
244        :return: on success, name of the translation unit. Otherwise, an empty string will be returned.
245        """
246        cu = self.resolve_die_ref(die, die.cu.cu_die_offset)
247
248        if {"DW_AT_name"} <= cu.attributes.keys():
249            return cu.attributes["DW_AT_name"].value.decode()
250        else:
251            return ""
252
253    def get_size(self, die: DIE, range_lists: RangeLists) -> int:
254        """
255        Returns the size required by the given DIE. The function will try different methods to get the size depending on the
256        attributes being present in the DIE. If none of the methods are successful, `0` will be returned.
257
258        :param die: DIE for which the size shall be returned.
259        :param range_lists: `RangeLists` extracted from the DWARF debugging data.
260
261        :return: on success, the size of the DIE. Otherwise, `0` will be returned.
262        """
263        if {"DW_AT_high_pc"} <= die.attributes.keys():
264            return die.attributes["DW_AT_high_pc"].value
265        if {"DW_AT_ranges"} <= die.attributes.keys():
266            if range_lists is None:
267                raise ValueError(f"DWARF info is missing debug ranges, which is required for DIE: {die}.")
268
269            range_list = range_lists.get_range_list_at_offset(die.attributes["DW_AT_ranges"].value)
270            size = 0
271            for entry in range_list:
272                if isinstance(entry, RangeEntry):
273                    size = size + (entry.end_offset - entry.begin_offset)
274            return size
275        return 0
276
277
278    def resolve_die_ref(self, die: DIE, ref_addr: int) -> DIE:
279        """
280        Given a DIE containing a reference address, the DIE referenced by that address will be returned.
281
282        :param die: DIE containing a reference address to be resolved.
283        :param ref_addr: reference address pointing to another DIE.
284
285        :return: referenced DIE.
286        """
287        return die.cu.get_DIE_from_refaddr(ref_addr)
288
289
290    def raw_inlined_to_output(self) -> AnalysisResult:
291        """
292        Performs post-processing on the gathered data about inlined functions to bring it into an easy-to-use format.
293        This includes wrapping the data into classes, grouping into inlined functions with and without FLASH saving potential
294        and sorting by the amount of FLASH used.
295
296        :return: a `AnalysisResult`.
297        """
298        inlined_savings_list = []
299        inlined_no_savings_list = []
300
301        savings_total_size = 0
302        no_savings_total_size = 0
303        savings_total_size_wo_overhead = 0
304        no_savings_total_size_wo_overhead = 0
305
306        for (decl_file, decl_line) in self._raw_inlined_functions:
307            inlined_instances = []
308            total_size = 0
309
310            for inlined_instance in self._raw_inlined_functions[(decl_file, decl_line)]:
311                inlined_instances.append(inlined_instance)
312                total_size = total_size + inlined_instance.size
313            total_size_wo_overhead = max(total_size - (len(inlined_instances) * self._call_overhead), 0)
314            inlined_function = InlinedFunction(decl_file, decl_line, total_size, total_size_wo_overhead, inlined_instances)
315
316            if len(inlined_instances) > 1:
317                inlined_savings_list.append(inlined_function)
318                savings_total_size = savings_total_size + total_size
319                savings_total_size_wo_overhead = savings_total_size_wo_overhead + total_size_wo_overhead
320            else:
321                inlined_no_savings_list.append(inlined_function)
322                no_savings_total_size = no_savings_total_size + total_size
323                no_savings_total_size_wo_overhead = no_savings_total_size_wo_overhead + total_size_wo_overhead
324
325        inlined_savings_list.sort(key=lambda x: x.total_size_wo_overhead, reverse=True)
326        inlined_no_savings_list.sort(key=lambda x: x.total_size_wo_overhead, reverse=True)
327        return AnalysisResult(inlined_savings_list, inlined_no_savings_list, savings_total_size, no_savings_total_size,
328                              savings_total_size_wo_overhead, no_savings_total_size_wo_overhead)
329
330
331# -----------------------------------------------------------------------------
332if __name__ == "__main__":
333    parser = argparse.ArgumentParser(description="Inline function analyzer to identify potential FLASH savings.")
334    parser.add_argument(
335        "-f",
336        dest="file",
337        help="Path to the ELF file to analyze",
338        required=True
339    )
340    parser.add_argument(
341        "-n",
342        help="Number of inlined functions to display. The default value of 0 causes all inlined functions to be displayed.",
343        type=int,
344        default=0
345    )
346    parser.add_argument(
347        "-m",
348        help="Number of inline function instances to show for each inlined function. The default value of 0 causes all instances to be displayed.",
349        type=int,
350        default=0
351    )
352    parser.add_argument(
353        "--overhead",
354        help="Expected instruction overhead caused by a function call, and therefore needs to be removed from the saveable space. This should include at least one branch instruction.",
355        type=int,
356        default=8
357    )
358    parser.add_argument(
359        "--all",
360        action="store_true",
361        default=False,
362        help="Include inlined functions that don't offer FLASH saving potential, i.e. functions that are only inlined once."
363    )
364
365    args = parser.parse_args()
366
367    console = Console()
368    inline_analyzer = InlineAnalyzer(args.overhead)
369    analysis_result = inline_analyzer.get_inlined_functions(args.file)
370
371    for i, inlined_function in enumerate(analysis_result.inlined_savings_list):
372        if i < args.n or args.n == 0:
373            inlined_function.print(console, args.m, analysis_result.savings_total_size, analysis_result.savings_total_size_wo_overhead, args.overhead)
374            console.print("")
375        else:
376            console.print("[...]")
377            break
378
379    if args.all:
380        for i, inlined_function in enumerate(analysis_result.inlined_no_savings_list):
381            if i < args.n or args.n == 0:
382                inlined_function.print(console, args.m, analysis_result.no_savings_total_size, analysis_result.no_savings_total_size_wo_overhead, args.overhead)
383                console.print("")
384            else:
385                console.print("[...]")
386                break
387
388    console.print(f"Total potentially saveable space: {analysis_result.savings_total_size}")
389    console.print(f"Total potentially saveable space without instruction overhead: {analysis_result.savings_total_size_wo_overhead}")
390
391    if args.all:
392        console.print(f"Total non-saveable space: {analysis_result.no_savings_total_size}")
393        console.print(f"Total non-saveable space without instruction overhead: {analysis_result.no_savings_total_size_wo_overhead}")
@dataclass
class InlinedInstance:
50@dataclass
51class InlinedInstance:
52    """
53    See `InlinedFunction`.
54    """
55    translation_unit: str
56    """name of the translation unit this instance is used in."""
57    file_name: Path
58    """file where the function is inlined to."""
59    file_line: int
60    """line within the file where the function is inlined."""
61    size: int
62    """amount of flash used by the inlined function in this instance."""
InlinedInstance( translation_unit: str, file_name: pathlib.Path, file_line: int, size: int)
translation_unit: str

name of the translation unit this instance is used in.

file_name: pathlib.Path

file where the function is inlined to.

file_line: int

line within the file where the function is inlined.

size: int

amount of flash used by the inlined function in this instance.

@dataclass
class InlinedFunction:
 66@dataclass
 67class InlinedFunction:
 68    """
 69    The `InlinedFunction` represents a function that is inlined into different callers.
 70    It contains a list of `InlinedInstance` which represent the instance that this function is inlined into.
 71    """
 72    file_name: Path
 73    """file where the inline function is declared."""
 74    file_line: int
 75    """line within the file where the inline function is declared."""
 76    total_size: int
 77    """total amount of flash used due to this method being inline."""
 78    total_size_wo_overhead: int
 79    """total amount of flash used due to this method being inline, reduced by the function call instruction overheads."""
 80    inlined_instances: list[InlinedInstance]
 81    """list of instances where this function is inlined to."""
 82
 83    def print(self, console: Console, num_of_instances: int, total_size: int, total_size_wo_overhead: int, call_overhead: int) -> str:
 84        """
 85        Prints the inlined function and its instances to the console in an easy-to-read format.
 86
 87        :param console: console to print the output to.
 88        :param num_of_instances: the number of inlined instances to print. A value of `0` causes all to be displayed.
 89        :param total_size: total amount of FLASH used by inlined functions. Used to calculate the percentage caused by this inlined function.
 90        :param total_size_wo_overhead: total amount of FLASH used by inlined functions, reduced by the function call instruction overheads.
 91        :param call_overhead: expected instruction overhead caused by a function call.
 92        """
 93        console.print((
 94            f"{_rel_file_name(self.file_name)}:{self.file_line}"
 95            f" -- Total Size: {self.total_size}"
 96            f" ({(100 if total_size == 0 else (self.total_size / total_size * 100)):.2f}%)"
 97            f" -- Total Size without instruction overhead: {self.total_size_wo_overhead}"
 98            f" ({(100 if total_size_wo_overhead == 0 else (self.total_size_wo_overhead / total_size_wo_overhead * 100)):.2f}%)"
 99        ))
100
101        table = Table(box=rich.box.MINIMAL_DOUBLE_HEAD)
102        table.add_column("Translation unit")
103        table.add_column("File name")
104        table.add_column("File line")
105        table.add_column("Size")
106        table.add_column("Call instruction overhead")
107        table.add_column("%")
108
109        for i, inlined_instance in enumerate(self.inlined_instances):
110            if i < num_of_instances or num_of_instances == 0:
111                table.add_row(_rel_file_name(inlined_instance.translation_unit), _rel_file_name(inlined_instance.file_name), str(inlined_instance.file_line),
112                              str(inlined_instance.size), str(call_overhead),
113                              "{:.2f}".format(100 if self.total_size == 0 else inlined_instance.size / self.total_size * 100))
114            else:
115                table.add_row("...", "...", "...")
116                break
117
118        console.print(table)

The InlinedFunction represents a function that is inlined into different callers. It contains a list of InlinedInstance which represent the instance that this function is inlined into.

InlinedFunction( file_name: pathlib.Path, file_line: int, total_size: int, total_size_wo_overhead: int, inlined_instances: list[InlinedInstance])
file_name: pathlib.Path

file where the inline function is declared.

file_line: int

line within the file where the inline function is declared.

total_size: int

total amount of flash used due to this method being inline.

total_size_wo_overhead: int

total amount of flash used due to this method being inline, reduced by the function call instruction overheads.

inlined_instances: list[InlinedInstance]

list of instances where this function is inlined to.

def print( self, console: rich.console.Console, num_of_instances: int, total_size: int, total_size_wo_overhead: int, call_overhead: int) -> str:
 83    def print(self, console: Console, num_of_instances: int, total_size: int, total_size_wo_overhead: int, call_overhead: int) -> str:
 84        """
 85        Prints the inlined function and its instances to the console in an easy-to-read format.
 86
 87        :param console: console to print the output to.
 88        :param num_of_instances: the number of inlined instances to print. A value of `0` causes all to be displayed.
 89        :param total_size: total amount of FLASH used by inlined functions. Used to calculate the percentage caused by this inlined function.
 90        :param total_size_wo_overhead: total amount of FLASH used by inlined functions, reduced by the function call instruction overheads.
 91        :param call_overhead: expected instruction overhead caused by a function call.
 92        """
 93        console.print((
 94            f"{_rel_file_name(self.file_name)}:{self.file_line}"
 95            f" -- Total Size: {self.total_size}"
 96            f" ({(100 if total_size == 0 else (self.total_size / total_size * 100)):.2f}%)"
 97            f" -- Total Size without instruction overhead: {self.total_size_wo_overhead}"
 98            f" ({(100 if total_size_wo_overhead == 0 else (self.total_size_wo_overhead / total_size_wo_overhead * 100)):.2f}%)"
 99        ))
100
101        table = Table(box=rich.box.MINIMAL_DOUBLE_HEAD)
102        table.add_column("Translation unit")
103        table.add_column("File name")
104        table.add_column("File line")
105        table.add_column("Size")
106        table.add_column("Call instruction overhead")
107        table.add_column("%")
108
109        for i, inlined_instance in enumerate(self.inlined_instances):
110            if i < num_of_instances or num_of_instances == 0:
111                table.add_row(_rel_file_name(inlined_instance.translation_unit), _rel_file_name(inlined_instance.file_name), str(inlined_instance.file_line),
112                              str(inlined_instance.size), str(call_overhead),
113                              "{:.2f}".format(100 if self.total_size == 0 else inlined_instance.size / self.total_size * 100))
114            else:
115                table.add_row("...", "...", "...")
116                break
117
118        console.print(table)

Prints the inlined function and its instances to the console in an easy-to-read format.

Parameters
  • console: console to print the output to.
  • num_of_instances: the number of inlined instances to print. A value of 0 causes all to be displayed.
  • total_size: total amount of FLASH used by inlined functions. Used to calculate the percentage caused by this inlined function.
  • total_size_wo_overhead: total amount of FLASH used by inlined functions, reduced by the function call instruction overheads.
  • call_overhead: expected instruction overhead caused by a function call.
@dataclass
class AnalysisResult:
122@dataclass
123class AnalysisResult:
124    """
125    The class `AnalysisResult` represents the result of an inline analysis performed using `InlineAnalyzer`
126    """
127    inlined_savings_list: list[InlinedFunction]
128    """inlined functions with more than one instance. These allow for potential FLASH savings."""
129    inlined_no_savings_list: list[InlinedFunction]
130    """inlined functions with one instance."""
131    savings_total_size: int
132    """overall FLASH size used by inlined functions with more than one instance."""
133    no_savings_total_size: int
134    """overall FLASH size used by inlined functions with one instance."""
135    savings_total_size_wo_overhead: int
136    """overall FLASH size used by inlined functions with more than one instance, reduced by function call instruction overheads."""
137    no_savings_total_size_wo_overhead: int
138    """overall FLASH size used by inlined functions with one instance, reduced by function call instruction overheads."""

The class AnalysisResult represents the result of an inline analysis performed using InlineAnalyzer

AnalysisResult( inlined_savings_list: list[InlinedFunction], inlined_no_savings_list: list[InlinedFunction], savings_total_size: int, no_savings_total_size: int, savings_total_size_wo_overhead: int, no_savings_total_size_wo_overhead: int)
inlined_savings_list: list[InlinedFunction]

inlined functions with more than one instance. These allow for potential FLASH savings.

inlined_no_savings_list: list[InlinedFunction]

inlined functions with one instance.

savings_total_size: int

overall FLASH size used by inlined functions with more than one instance.

no_savings_total_size: int

overall FLASH size used by inlined functions with one instance.

savings_total_size_wo_overhead: int

overall FLASH size used by inlined functions with more than one instance, reduced by function call instruction overheads.

no_savings_total_size_wo_overhead: int

overall FLASH size used by inlined functions with one instance, reduced by function call instruction overheads.

class InlineAnalyzer:
142class InlineAnalyzer:
143    """
144    Analyzes an ELF file and DWARF debugging data to identify inline functions and the instances where they are inlined to.
145    This allows to identify options for a space-time tradeoff.
146
147    :param call_overhead: expected instruction overhead caused by a function call.
148    """
149    def __init__(self, call_overhead: int):
150        self._call_overhead = call_overhead
151        self._raw_inlined_functions = defaultdict(list)
152
153
154    def get_inlined_functions(self, file_name: str) -> AnalysisResult:
155        """
156        Returns the identified `InlinedFunction` in the given ELF file.
157        This is only possible if DWARF debugging data is available which contains debug ranges and line program.
158
159        :param file_name: path to the ELF file to analyze.
160
161        :return: on success, a `AnalysisResult` is returned. In case of errors an exception is raised.
162
163        :raises ValueError: if the debugging data is not sufficient of the analysis.
164        """
165        _LOGGER.info(f"Processing file: {file_name}")
166        self._raw_inlined_functions.clear()
167
168        with open(file_name, "rb") as f:
169            elf_file = ELFFile(f)
170
171            if not elf_file.has_dwarf_info():
172                raise ValueError(f"{file_name} has no DWARF info.")
173
174            dwarf_info = elf_file.get_dwarf_info()
175            range_lists = dwarf_info.range_lists()
176
177            for CU in dwarf_info.iter_CUs():
178                line_program = dwarf_info.line_program_for_CU(CU)
179
180                if line_program is None:
181                    _LOGGER.warning("CU @ {CU.cu_offset} DWARF info is missing line program. Skipping CU.")
182                    continue
183
184                top_die = CU.get_top_DIE()
185                self.die_get_inlined_rec(top_die, line_program, range_lists)
186        return self.raw_inlined_to_output()
187
188
189    def die_get_inlined_rec(self, die: DIE, line_program: LineProgram, range_lists: RangeLists):
190        """
191        Recursively traverse all DIEs of a given top DIE and extract information about inlined functions.
192
193        :param die: DIE to be processed. Is gathered recursively after passing a top DIE.
194        :param line_program: `LineProgram` extracted from the DWARF debugging data.
195        :param range_lists: `RangeLists` extracted from the DWARF debugging data.
196        """
197        if die.tag == "DW_TAG_inlined_subroutine" and {"DW_AT_call_file"} <= die.attributes.keys():
198            call_file = self.get_file_name(die.attributes["DW_AT_call_file"].value, line_program)
199            call_line = die.attributes["DW_AT_call_line"].value
200            size = self.get_size(die, range_lists)
201
202            decl_die = self.resolve_die_ref(die, die.cu.cu_offset + die.attributes["DW_AT_abstract_origin"].value)
203
204            if {"DW_AT_decl_file", "DW_AT_decl_line"} <= decl_die.attributes.keys():
205                decl_file = self.get_file_name(decl_die.attributes["DW_AT_decl_file"].value, line_program)
206                decl_line = decl_die.attributes["DW_AT_decl_line"].value
207                translation_unit_name = self.get_translation_unit_name(die)
208
209                called_function = InlinedInstance(translation_unit_name, call_file, call_line, size)
210                self._raw_inlined_functions[(decl_file, decl_line)].append(called_function)
211
212        # Recurse into the DIE children
213        for child in die.iter_children():
214                self.die_get_inlined_rec(child, line_program, range_lists)
215
216
217    def get_file_name(self, file_idx: int, line_program: LineProgram) -> Path:
218        """
219        Returns a file name given a DIE file index. To perform this mapping the line program is required.
220
221        :param file_idx: DIE file index for which the file name shall be returned.
222        :param line_program: `LineProgram` extracted from the DWARF debugging data.
223
224        :return: the file name for the given DIE file index. This will include the full path if the line program
225                 contains the relevant data. Otherwise, only the file name without path will be returned.
226        """
227        lp_header = line_program.header
228        file_entries = lp_header["file_entry"]
229        file_entry = file_entries[file_idx - 1]
230        dir_index = file_entry["dir_index"]
231
232        if dir_index == 0:
233            return Path(file_entry.name.decode())
234
235        directory = lp_header["include_directory"][dir_index - 1]
236        return Path(directory.decode()) / file_entry.name.decode()
237
238    def get_translation_unit_name(self, die: DIE) -> str:
239        """
240        Returns the name of the translation unit the given DIE is contained in. If the name can't be retrieved,
241        an empty string will be returned.
242
243        :param die: DIE for which the translation unit name shall be returned.
244
245        :return: on success, name of the translation unit. Otherwise, an empty string will be returned.
246        """
247        cu = self.resolve_die_ref(die, die.cu.cu_die_offset)
248
249        if {"DW_AT_name"} <= cu.attributes.keys():
250            return cu.attributes["DW_AT_name"].value.decode()
251        else:
252            return ""
253
254    def get_size(self, die: DIE, range_lists: RangeLists) -> int:
255        """
256        Returns the size required by the given DIE. The function will try different methods to get the size depending on the
257        attributes being present in the DIE. If none of the methods are successful, `0` will be returned.
258
259        :param die: DIE for which the size shall be returned.
260        :param range_lists: `RangeLists` extracted from the DWARF debugging data.
261
262        :return: on success, the size of the DIE. Otherwise, `0` will be returned.
263        """
264        if {"DW_AT_high_pc"} <= die.attributes.keys():
265            return die.attributes["DW_AT_high_pc"].value
266        if {"DW_AT_ranges"} <= die.attributes.keys():
267            if range_lists is None:
268                raise ValueError(f"DWARF info is missing debug ranges, which is required for DIE: {die}.")
269
270            range_list = range_lists.get_range_list_at_offset(die.attributes["DW_AT_ranges"].value)
271            size = 0
272            for entry in range_list:
273                if isinstance(entry, RangeEntry):
274                    size = size + (entry.end_offset - entry.begin_offset)
275            return size
276        return 0
277
278
279    def resolve_die_ref(self, die: DIE, ref_addr: int) -> DIE:
280        """
281        Given a DIE containing a reference address, the DIE referenced by that address will be returned.
282
283        :param die: DIE containing a reference address to be resolved.
284        :param ref_addr: reference address pointing to another DIE.
285
286        :return: referenced DIE.
287        """
288        return die.cu.get_DIE_from_refaddr(ref_addr)
289
290
291    def raw_inlined_to_output(self) -> AnalysisResult:
292        """
293        Performs post-processing on the gathered data about inlined functions to bring it into an easy-to-use format.
294        This includes wrapping the data into classes, grouping into inlined functions with and without FLASH saving potential
295        and sorting by the amount of FLASH used.
296
297        :return: a `AnalysisResult`.
298        """
299        inlined_savings_list = []
300        inlined_no_savings_list = []
301
302        savings_total_size = 0
303        no_savings_total_size = 0
304        savings_total_size_wo_overhead = 0
305        no_savings_total_size_wo_overhead = 0
306
307        for (decl_file, decl_line) in self._raw_inlined_functions:
308            inlined_instances = []
309            total_size = 0
310
311            for inlined_instance in self._raw_inlined_functions[(decl_file, decl_line)]:
312                inlined_instances.append(inlined_instance)
313                total_size = total_size + inlined_instance.size
314            total_size_wo_overhead = max(total_size - (len(inlined_instances) * self._call_overhead), 0)
315            inlined_function = InlinedFunction(decl_file, decl_line, total_size, total_size_wo_overhead, inlined_instances)
316
317            if len(inlined_instances) > 1:
318                inlined_savings_list.append(inlined_function)
319                savings_total_size = savings_total_size + total_size
320                savings_total_size_wo_overhead = savings_total_size_wo_overhead + total_size_wo_overhead
321            else:
322                inlined_no_savings_list.append(inlined_function)
323                no_savings_total_size = no_savings_total_size + total_size
324                no_savings_total_size_wo_overhead = no_savings_total_size_wo_overhead + total_size_wo_overhead
325
326        inlined_savings_list.sort(key=lambda x: x.total_size_wo_overhead, reverse=True)
327        inlined_no_savings_list.sort(key=lambda x: x.total_size_wo_overhead, reverse=True)
328        return AnalysisResult(inlined_savings_list, inlined_no_savings_list, savings_total_size, no_savings_total_size,
329                              savings_total_size_wo_overhead, no_savings_total_size_wo_overhead)

Analyzes an ELF file and DWARF debugging data to identify inline functions and the instances where they are inlined to. This allows to identify options for a space-time tradeoff.

Parameters
  • call_overhead: expected instruction overhead caused by a function call.
InlineAnalyzer(call_overhead: int)
149    def __init__(self, call_overhead: int):
150        self._call_overhead = call_overhead
151        self._raw_inlined_functions = defaultdict(list)
def get_inlined_functions(self, file_name: str) -> AnalysisResult:
154    def get_inlined_functions(self, file_name: str) -> AnalysisResult:
155        """
156        Returns the identified `InlinedFunction` in the given ELF file.
157        This is only possible if DWARF debugging data is available which contains debug ranges and line program.
158
159        :param file_name: path to the ELF file to analyze.
160
161        :return: on success, a `AnalysisResult` is returned. In case of errors an exception is raised.
162
163        :raises ValueError: if the debugging data is not sufficient of the analysis.
164        """
165        _LOGGER.info(f"Processing file: {file_name}")
166        self._raw_inlined_functions.clear()
167
168        with open(file_name, "rb") as f:
169            elf_file = ELFFile(f)
170
171            if not elf_file.has_dwarf_info():
172                raise ValueError(f"{file_name} has no DWARF info.")
173
174            dwarf_info = elf_file.get_dwarf_info()
175            range_lists = dwarf_info.range_lists()
176
177            for CU in dwarf_info.iter_CUs():
178                line_program = dwarf_info.line_program_for_CU(CU)
179
180                if line_program is None:
181                    _LOGGER.warning("CU @ {CU.cu_offset} DWARF info is missing line program. Skipping CU.")
182                    continue
183
184                top_die = CU.get_top_DIE()
185                self.die_get_inlined_rec(top_die, line_program, range_lists)
186        return self.raw_inlined_to_output()

Returns the identified InlinedFunction in the given ELF file. This is only possible if DWARF debugging data is available which contains debug ranges and line program.

Parameters
  • file_name: path to the ELF file to analyze.
Returns

on success, a AnalysisResult is returned. In case of errors an exception is raised.

Raises
  • ValueError: if the debugging data is not sufficient of the analysis.
def die_get_inlined_rec( self, die: elftools.dwarf.die.DIE, line_program: elftools.dwarf.lineprogram.LineProgram, range_lists: elftools.dwarf.ranges.RangeLists):
189    def die_get_inlined_rec(self, die: DIE, line_program: LineProgram, range_lists: RangeLists):
190        """
191        Recursively traverse all DIEs of a given top DIE and extract information about inlined functions.
192
193        :param die: DIE to be processed. Is gathered recursively after passing a top DIE.
194        :param line_program: `LineProgram` extracted from the DWARF debugging data.
195        :param range_lists: `RangeLists` extracted from the DWARF debugging data.
196        """
197        if die.tag == "DW_TAG_inlined_subroutine" and {"DW_AT_call_file"} <= die.attributes.keys():
198            call_file = self.get_file_name(die.attributes["DW_AT_call_file"].value, line_program)
199            call_line = die.attributes["DW_AT_call_line"].value
200            size = self.get_size(die, range_lists)
201
202            decl_die = self.resolve_die_ref(die, die.cu.cu_offset + die.attributes["DW_AT_abstract_origin"].value)
203
204            if {"DW_AT_decl_file", "DW_AT_decl_line"} <= decl_die.attributes.keys():
205                decl_file = self.get_file_name(decl_die.attributes["DW_AT_decl_file"].value, line_program)
206                decl_line = decl_die.attributes["DW_AT_decl_line"].value
207                translation_unit_name = self.get_translation_unit_name(die)
208
209                called_function = InlinedInstance(translation_unit_name, call_file, call_line, size)
210                self._raw_inlined_functions[(decl_file, decl_line)].append(called_function)
211
212        # Recurse into the DIE children
213        for child in die.iter_children():
214                self.die_get_inlined_rec(child, line_program, range_lists)

Recursively traverse all DIEs of a given top DIE and extract information about inlined functions.

Parameters
  • die: DIE to be processed. Is gathered recursively after passing a top DIE.
  • line_program: LineProgram extracted from the DWARF debugging data.
  • range_lists: RangeLists extracted from the DWARF debugging data.
def get_file_name( self, file_idx: int, line_program: elftools.dwarf.lineprogram.LineProgram) -> pathlib.Path:
217    def get_file_name(self, file_idx: int, line_program: LineProgram) -> Path:
218        """
219        Returns a file name given a DIE file index. To perform this mapping the line program is required.
220
221        :param file_idx: DIE file index for which the file name shall be returned.
222        :param line_program: `LineProgram` extracted from the DWARF debugging data.
223
224        :return: the file name for the given DIE file index. This will include the full path if the line program
225                 contains the relevant data. Otherwise, only the file name without path will be returned.
226        """
227        lp_header = line_program.header
228        file_entries = lp_header["file_entry"]
229        file_entry = file_entries[file_idx - 1]
230        dir_index = file_entry["dir_index"]
231
232        if dir_index == 0:
233            return Path(file_entry.name.decode())
234
235        directory = lp_header["include_directory"][dir_index - 1]
236        return Path(directory.decode()) / file_entry.name.decode()

Returns a file name given a DIE file index. To perform this mapping the line program is required.

Parameters
  • file_idx: DIE file index for which the file name shall be returned.
  • line_program: LineProgram extracted from the DWARF debugging data.
Returns

the file name for the given DIE file index. This will include the full path if the line program contains the relevant data. Otherwise, only the file name without path will be returned.

def get_translation_unit_name(self, die: elftools.dwarf.die.DIE) -> str:
238    def get_translation_unit_name(self, die: DIE) -> str:
239        """
240        Returns the name of the translation unit the given DIE is contained in. If the name can't be retrieved,
241        an empty string will be returned.
242
243        :param die: DIE for which the translation unit name shall be returned.
244
245        :return: on success, name of the translation unit. Otherwise, an empty string will be returned.
246        """
247        cu = self.resolve_die_ref(die, die.cu.cu_die_offset)
248
249        if {"DW_AT_name"} <= cu.attributes.keys():
250            return cu.attributes["DW_AT_name"].value.decode()
251        else:
252            return ""

Returns the name of the translation unit the given DIE is contained in. If the name can't be retrieved, an empty string will be returned.

Parameters
  • die: DIE for which the translation unit name shall be returned.
Returns

on success, name of the translation unit. Otherwise, an empty string will be returned.

def get_size( self, die: elftools.dwarf.die.DIE, range_lists: elftools.dwarf.ranges.RangeLists) -> int:
254    def get_size(self, die: DIE, range_lists: RangeLists) -> int:
255        """
256        Returns the size required by the given DIE. The function will try different methods to get the size depending on the
257        attributes being present in the DIE. If none of the methods are successful, `0` will be returned.
258
259        :param die: DIE for which the size shall be returned.
260        :param range_lists: `RangeLists` extracted from the DWARF debugging data.
261
262        :return: on success, the size of the DIE. Otherwise, `0` will be returned.
263        """
264        if {"DW_AT_high_pc"} <= die.attributes.keys():
265            return die.attributes["DW_AT_high_pc"].value
266        if {"DW_AT_ranges"} <= die.attributes.keys():
267            if range_lists is None:
268                raise ValueError(f"DWARF info is missing debug ranges, which is required for DIE: {die}.")
269
270            range_list = range_lists.get_range_list_at_offset(die.attributes["DW_AT_ranges"].value)
271            size = 0
272            for entry in range_list:
273                if isinstance(entry, RangeEntry):
274                    size = size + (entry.end_offset - entry.begin_offset)
275            return size
276        return 0

Returns the size required by the given DIE. The function will try different methods to get the size depending on the attributes being present in the DIE. If none of the methods are successful, 0 will be returned.

Parameters
  • die: DIE for which the size shall be returned.
  • range_lists: RangeLists extracted from the DWARF debugging data.
Returns

on success, the size of the DIE. Otherwise, 0 will be returned.

def resolve_die_ref( self, die: elftools.dwarf.die.DIE, ref_addr: int) -> elftools.dwarf.die.DIE:
279    def resolve_die_ref(self, die: DIE, ref_addr: int) -> DIE:
280        """
281        Given a DIE containing a reference address, the DIE referenced by that address will be returned.
282
283        :param die: DIE containing a reference address to be resolved.
284        :param ref_addr: reference address pointing to another DIE.
285
286        :return: referenced DIE.
287        """
288        return die.cu.get_DIE_from_refaddr(ref_addr)

Given a DIE containing a reference address, the DIE referenced by that address will be returned.

Parameters
  • die: DIE containing a reference address to be resolved.
  • ref_addr: reference address pointing to another DIE.
Returns

referenced DIE.

def raw_inlined_to_output(self) -> AnalysisResult:
291    def raw_inlined_to_output(self) -> AnalysisResult:
292        """
293        Performs post-processing on the gathered data about inlined functions to bring it into an easy-to-use format.
294        This includes wrapping the data into classes, grouping into inlined functions with and without FLASH saving potential
295        and sorting by the amount of FLASH used.
296
297        :return: a `AnalysisResult`.
298        """
299        inlined_savings_list = []
300        inlined_no_savings_list = []
301
302        savings_total_size = 0
303        no_savings_total_size = 0
304        savings_total_size_wo_overhead = 0
305        no_savings_total_size_wo_overhead = 0
306
307        for (decl_file, decl_line) in self._raw_inlined_functions:
308            inlined_instances = []
309            total_size = 0
310
311            for inlined_instance in self._raw_inlined_functions[(decl_file, decl_line)]:
312                inlined_instances.append(inlined_instance)
313                total_size = total_size + inlined_instance.size
314            total_size_wo_overhead = max(total_size - (len(inlined_instances) * self._call_overhead), 0)
315            inlined_function = InlinedFunction(decl_file, decl_line, total_size, total_size_wo_overhead, inlined_instances)
316
317            if len(inlined_instances) > 1:
318                inlined_savings_list.append(inlined_function)
319                savings_total_size = savings_total_size + total_size
320                savings_total_size_wo_overhead = savings_total_size_wo_overhead + total_size_wo_overhead
321            else:
322                inlined_no_savings_list.append(inlined_function)
323                no_savings_total_size = no_savings_total_size + total_size
324                no_savings_total_size_wo_overhead = no_savings_total_size_wo_overhead + total_size_wo_overhead
325
326        inlined_savings_list.sort(key=lambda x: x.total_size_wo_overhead, reverse=True)
327        inlined_no_savings_list.sort(key=lambda x: x.total_size_wo_overhead, reverse=True)
328        return AnalysisResult(inlined_savings_list, inlined_no_savings_list, savings_total_size, no_savings_total_size,
329                              savings_total_size_wo_overhead, no_savings_total_size_wo_overhead)

Performs post-processing on the gathered data about inlined functions to bring it into an easy-to-use format. This includes wrapping the data into classes, grouping into inlined functions with and without FLASH saving potential and sorting by the amount of FLASH used.

Returns

a AnalysisResult.