mirror of
https://github.com/Mbed-TLS/mbedtls.git
synced 2026-08-03 18:27:42 +08:00
The score is the access cost on Cortex-M0+ thumb builds, expressed in instructions. This commit does not have any provisions to calculate different scores based on the target architecture, so it's only meaningful when running this script with `-t armv6m-unknown-eabi`. Go with a simple model: direct access is possible for the first 128 fields of a structure, direct access costs 1 instruction, indirect access costs 3 instructions. The score calculation assumes that each lexical use in the source code corresponds to one access. This does not take into account unused code, inlining, uses from application code, etc. Signed-off-by: Gilles Peskine <Gilles.Peskine@arm.com>
226 lines
8.9 KiB
Python
Executable File
226 lines
8.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Report on structure types defined in Mbed TLS headers.
|
|
|
|
This script uses the clang python bindings (``pip3 install --user clang``).
|
|
"""
|
|
|
|
# Copyright The Mbed TLS Contributors
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import argparse
|
|
import collections
|
|
import re
|
|
import sys
|
|
from typing import Dict, List, Optional
|
|
|
|
import clang.cindex #type: ignore
|
|
from clang.cindex import Cursor, SourceLocation, TranslationUnit
|
|
from clang.cindex import CursorKind, TypeKind
|
|
|
|
from mbedtls_dev import typing_util
|
|
|
|
|
|
class FieldInfo:
|
|
"""Information about a field of a structure."""
|
|
|
|
def __init__(self, node: Cursor) -> None:
|
|
self.node = node
|
|
self.lexical_uses = 0
|
|
self.size = node.type.get_size()
|
|
self.align = node.type.get_align()
|
|
# Empirically, offsetof is in bits, not bytes. To make the output
|
|
# easier to read, convert to bytes (the same unit as size and
|
|
# alignment), which means that bitfields will be located at their
|
|
# first byte.
|
|
self.offset = node.get_field_offsetof() // 8
|
|
|
|
def record_use(self, _node: Cursor) -> None:
|
|
self.lexical_uses += 1
|
|
|
|
def uses(self) -> int:
|
|
return self.lexical_uses
|
|
|
|
def is_indirect(self) -> bool:
|
|
"""Whether access to this field is indirect on ARM Cortex-M0+."""
|
|
word_offset = self.offset // self.align
|
|
return word_offset >= 128
|
|
|
|
def score(self) -> int:
|
|
"""A field's score is an estimate of its access cost.
|
|
|
|
The cost is calculated as the number of instructions needed to
|
|
access the field on ARM Cortex-M0+, multiplied by the number of uses
|
|
of the field inside the library.
|
|
"""
|
|
cost_per_use = 1 + 2 * self.is_indirect()
|
|
return self.uses() * cost_per_use
|
|
|
|
|
|
class Ast:
|
|
"""Abstract representation of the source code."""
|
|
|
|
def __init__(self, options) -> None:
|
|
"""Prepare for analysis of C source files."""
|
|
if options.clang_library_file:
|
|
clang.cindex.Config.set_library_file(options.clang_library_file)
|
|
self.parse_options = []
|
|
if options.target:
|
|
self.parse_options += ['-target', options.target]
|
|
for d in options.include:
|
|
self.parse_options.append('-I' + d)
|
|
for d in options.define:
|
|
self.parse_options.append('-D' + d)
|
|
self.index = clang.cindex.Index.create()
|
|
self.files = {} #type: Dict[str, TranslationUnit]
|
|
# fields[TYPE_OR_STRUCT_NAME][FIELD_NAME]
|
|
self.fields = {} #type: Dict[str, Dict[str, FieldInfo]]
|
|
|
|
def load(self, *filenames: str) -> None:
|
|
"""Load the AST of the given source files."""
|
|
for filename in filenames:
|
|
self.files[filename] = self.index.parse(filename,
|
|
self.parse_options)
|
|
|
|
def in_interesting_file(self, location: SourceLocation) -> bool:
|
|
"""Whether the given location is in a file that should be analyzed."""
|
|
if not hasattr(location.file, 'name'):
|
|
# Some artificial nodes have associated no file name.
|
|
# Let's hope they're not important.
|
|
return False
|
|
return location.file.name in self.files
|
|
|
|
def read_field_definitions(self, filenames: List[str]) -> None:
|
|
"""Parse structure field definitions in the given C source files (usually headers)."""
|
|
self.load(*filenames)
|
|
for filename in filenames:
|
|
for node in self.files[filename].cursor.walk_preorder():
|
|
if not self.in_interesting_file(node.location):
|
|
continue
|
|
if node.kind == CursorKind.FIELD_DECL:
|
|
type_name = node.lexical_parent.spelling
|
|
# Skip unions
|
|
if node.lexical_parent.kind != CursorKind.STRUCT_DECL:
|
|
continue
|
|
# type_name is the struct name. If there's no struct
|
|
# name, see if there's a typedef name, to cope with
|
|
# "typedef struct { ... } foo;"
|
|
if not type_name:
|
|
type_name = node.lexical_parent.type.spelling
|
|
# Skip anonymous structs for now.
|
|
if '(anonymous ' in type_name:
|
|
continue
|
|
self.fields.setdefault(type_name, collections.OrderedDict())
|
|
self.fields[type_name][node.spelling] = FieldInfo(node)
|
|
|
|
@staticmethod
|
|
def get_underlying_type(typ: clang.cindex.Type) -> Optional[clang.cindex.Type]:
|
|
"""Strip off one level of type indirection.
|
|
|
|
Return None if the type is as primitive as can be.
|
|
"""
|
|
if hasattr(typ, 'get_canonical'):
|
|
lower = typ.get_canonical()
|
|
if lower == typ:
|
|
return None
|
|
else:
|
|
return lower
|
|
if typ.kind == TypeKind.POINTER:
|
|
return typ.get_pointee()
|
|
if hasattr(typ, 'underlying_typedef_type'):
|
|
return typ.underlying_typedef_type
|
|
return None
|
|
|
|
QUALIFIERS_RE = re.compile(r'.* ')
|
|
def get_type_core(self, type: clang.cindex.Type) -> str:
|
|
# pylint: disable=redefined-builtin
|
|
"""Get the base name of a type, without typedefs, qualifiers or pointers."""
|
|
core = type
|
|
lower = type # type: Optional[clang.cindex.Type]
|
|
while lower:
|
|
core, lower = lower, self.get_underlying_type(core)
|
|
# There's no API function to remove qualifiers from a type,
|
|
# so do it textually. Remove 'const', 'restrict', etc.
|
|
# Also remove 'struct', so we'll get the struct name from struct
|
|
# definitions.
|
|
return re.sub(self.QUALIFIERS_RE, r'', core.spelling)
|
|
|
|
def record_field_access(self, node: Cursor) -> None:
|
|
"""Record one location where a field is accessed."""
|
|
field_name = node.spelling
|
|
lhs = next(node.get_children())
|
|
structure_type = self.get_type_core(lhs.type)
|
|
if structure_type not in self.fields:
|
|
# This is not a structure defined by the library
|
|
return
|
|
self.fields[structure_type][field_name].record_use(node)
|
|
|
|
def read_field_usage(self, filenames: List[str]) -> None:
|
|
"""Parse field usage in the given C source files."""
|
|
self.load(*filenames)
|
|
for filename in filenames:
|
|
for node in self.files[filename].cursor.walk_preorder():
|
|
if not self.in_interesting_file(node.location):
|
|
continue
|
|
if node.kind == CursorKind.MEMBER_REF_EXPR:
|
|
self.record_field_access(node)
|
|
|
|
@staticmethod
|
|
def report_field(out: typing_util.Writable,
|
|
prefix: str, field: FieldInfo) -> None:
|
|
"""Print information about a structure field.
|
|
|
|
Format: <type>.<name>,<size>,<alignment>,<offset>,<use_count>,<score>
|
|
"""
|
|
out.write('{},{},{},{},{},{}\n'.format(
|
|
prefix + field.node.spelling,
|
|
field.size, field.align, field.offset,
|
|
field.uses(), field.score()
|
|
))
|
|
|
|
def report_fields(self, out: typing_util.Writable) -> None:
|
|
"""Print information about fields of structures defined by the library."""
|
|
for type_name in sorted(self.fields):
|
|
for field in self.fields[type_name].values():
|
|
self.report_field(out, type_name + '.', field)
|
|
|
|
def run_analysis(self, files) -> None:
|
|
self.read_field_definitions(files)
|
|
self.read_field_usage(files)
|
|
self.report_fields(sys.stdout)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--clang-library-file',
|
|
help="Alternative location of libclang.so")
|
|
parser.add_argument('--define', '-D',
|
|
action='append',
|
|
default=['MBEDTLS_ALLOW_PRIVATE_ACCESS'],
|
|
help="Additional C preprocessor definition")
|
|
parser.add_argument('--include', '-I',
|
|
action='append',
|
|
default=['include'],
|
|
help="Directory to add to the header include path")
|
|
parser.add_argument('--target', '-t',
|
|
help="Target triple to build for (default: native build)")
|
|
parser.add_argument('files', metavar='FILE', nargs='*',
|
|
help="Source files to analyze")
|
|
options = parser.parse_args()
|
|
ast = Ast(options)
|
|
ast.run_analysis(options.files)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|