1#!/usr/bin/env python3 2# SPDX-License-Identifier: Apache-2.0 3# ----------------------------------------------------------------------------- 4# Copyright 2019-2020 Arm Limited 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); you may not 7# use this file except in compliance with the License. You may obtain a copy 8# of the License at: 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15# License for the specific language governing permissions and limitations 16# under the License. 17# ----------------------------------------------------------------------------- 18""" 19The ``astc_size_binary`` utility provides a wrapper around the Linux ``size`` 20utility to view binary section sizes, and optionally compare the section sizes 21of two binaries. Section sizes are given for code (``.text``), read-only data 22(``.rodata``), and zero initialized data (``.bss``). All other sections are 23ignored. 24 25A typical report comparing the size of a new binary against a reference looks 26like this: 27 28.. code-block:: 29 30 Code RO Data ZI Data 31 Ref 411298 374560 128576 32 New 560530 89552 31744 33 Abs D 149232 -285008 -96832 34 Rel D 36.28% -76.09% -75.31% 35""" 36 37 38import argparse 39import platform 40import shutil 41import subprocess as sp 42import sys 43 44 45def run_size_linux(binary): 46 """ 47 Run size on a single binary. 48 49 Args: 50 binary (str): The path of the binary file to process. 51 52 Returns: 53 tuple(int, int, int): A triplet of code size, read-only data size, and 54 zero-init data size, all in bytes. 55 56 Raises: 57 CalledProcessException: The ``size`` subprocess failed for any reason. 58 """ 59 args = ["size", "--format=sysv", binary] 60 result = sp.run(args, stdout=sp.PIPE, stderr=sp.PIPE, 61 check=True, universal_newlines=True) 62 63 data = {} 64 patterns = {"Code": ".text", "RO": ".rodata", "ZI": ".bss"} 65 66 lines = result.stdout.splitlines() 67 for line in lines: 68 for key, value in patterns.items(): 69 if line.startswith(value): 70 size = float(line.split()[1]) 71 data[key] = size 72 73 return (data["Code"], data["RO"], data["ZI"]) 74 75 76def run_size_macos(binary): 77 """ 78 Run size on a single binary. 79 80 Args: 81 binary (str): The path of the binary file to process. 82 83 Returns: 84 tuple(int, int, int): A triplet of code size, read-only data size, and 85 zero-init data size, all in bytes. 86 87 Raises: 88 CalledProcessException: The ``size`` subprocess failed for any reason. 89 """ 90 args = ["size", "-m", binary] 91 result = sp.run(args, stdout=sp.PIPE, stderr=sp.PIPE, 92 check=True, universal_newlines=True) 93 94 code = 0 95 dataRO = 0 96 dataZI = 0 97 98 currentSegment = None 99 100 lines = result.stdout.splitlines() 101 for line in lines: 102 line = line.strip() 103 104 if line.startswith("Segment"): 105 parts = line.split() 106 assert len(parts) >= 3, parts 107 108 currentSegment = parts[1] 109 size = int(parts[2]) 110 111 if currentSegment == "__TEXT:": 112 code += size 113 114 if currentSegment == "__DATA_CONST:": 115 dataRO += size 116 117 if currentSegment == "__DATA:": 118 dataZI += size 119 120 if line.startswith("Section"): 121 parts = line.split() 122 assert len(parts) >= 3, parts 123 124 section = parts[1] 125 size = int(parts[2]) 126 127 if currentSegment == "__TEXT:" and section == "__const:": 128 code -= size 129 dataRO += size 130 131 return (code, dataRO, dataZI) 132 133 134def parse_command_line(): 135 """ 136 Parse the command line. 137 138 Returns: 139 Namespace: The parsed command line container. 140 """ 141 parser = argparse.ArgumentParser() 142 143 parser.add_argument("bin", type=argparse.FileType("r"), 144 help="The new binary to size") 145 146 parser.add_argument("ref", nargs="?", type=argparse.FileType("r"), 147 help="The reference binary to compare against") 148 149 return parser.parse_args() 150 151 152def main(): 153 """ 154 The main function. 155 156 Returns: 157 int: The process return code. 158 """ 159 args = parse_command_line() 160 161 # Preflight - check that size exists. Note that size might still fail at 162 # runtime later, e.g. if the binary is not of the correct format 163 path = shutil.which("size") 164 if not path: 165 print("ERROR: The 'size' utility is not installed on the PATH") 166 return 1 167 168 if platform.system() == "Darwin": 169 run_size = run_size_macos 170 else: 171 run_size = run_size_linux 172 173 # Collect the data 174 try: 175 newSize = run_size(args.bin.name) 176 if args.ref: 177 refSize = run_size(args.ref.name) 178 except sp.CalledProcessError as ex: 179 print("ERROR: The 'size' utility failed") 180 print(" %s" % ex.stderr.strip()) 181 return 1 182 183 # Print the basic table of absolute values 184 print("%8s % 8s % 8s % 8s" % ("", "Code", "RO Data", "ZI Data")) 185 if args.ref: 186 print("%8s % 8u % 8u % 8u" % ("Ref", *refSize)) 187 print("%8s % 8u % 8u % 8u" % ("New", *newSize)) 188 189 # Print the difference if we have a reference 190 if args.ref: 191 diffAbs = [] 192 diffRel = [] 193 for refVal, newVal in zip(refSize, newSize): 194 diff = newVal - refVal 195 diffAbs.append(diff) 196 diffRel.append((diff / refVal) * 100.0) 197 198 dat = ("Abs D", diffAbs[0], diffAbs[1], diffAbs[2]) 199 print("%8s % 8u % 8u % 8u" % dat) 200 dat = ("Rel D", diffRel[0], diffRel[1], diffRel[2]) 201 print("%8s % 7.2f%% % 7.2f%% % 7.2f%%" % dat) 202 203 return 0 204 205 206if __name__ == "__main__": 207 sys.exit(main()) 208