• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright 2017 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Checks the number of static initializers in an APK's library."""
7
8
9import argparse
10import os
11import re
12import subprocess
13import sys
14import tempfile
15import zipfile
16
17from util import build_utils
18
19_DUMP_STATIC_INITIALIZERS_PATH = os.path.join(build_utils.DIR_SOURCE_ROOT,
20                                              'tools', 'linux',
21                                              'dump-static-initializers.py')
22
23
24def _RunReadelf(so_path, options, tool_prefix=''):
25  return subprocess.check_output(
26      [tool_prefix + 'readobj', '--elf-output-style=GNU'] + options +
27      [so_path]).decode('utf8')
28
29
30def _ParseLibBuildId(so_path, tool_prefix):
31  """Returns the Build ID of the given native library."""
32  stdout = _RunReadelf(so_path, ['-n'], tool_prefix)
33  match = re.search(r'Build ID: (\w+)', stdout)
34  return match.group(1) if match else None
35
36
37def _VerifyLibBuildIdsMatch(tool_prefix, *so_files):
38  if len(set(_ParseLibBuildId(f, tool_prefix) for f in so_files)) > 1:
39    raise Exception('Found differing build ids in output directory and apk. '
40                    'Your output directory is likely stale.')
41
42
43def _DumpStaticInitializers(apk_so_name, unzipped_so, out_dir, tool_prefix):
44  so_with_symbols_path = os.path.join(out_dir, 'lib.unstripped',
45                                      os.path.basename(apk_so_name))
46  if not os.path.exists(so_with_symbols_path):
47    raise Exception('Unstripped .so not found. Looked here: %s' %
48                    so_with_symbols_path)
49  _VerifyLibBuildIdsMatch(tool_prefix, unzipped_so, so_with_symbols_path)
50  subprocess.check_call([_DUMP_STATIC_INITIALIZERS_PATH, so_with_symbols_path])
51
52
53def _ReadInitArray(so_path, tool_prefix, expect_no_initializers):
54  stdout = _RunReadelf(so_path, ['-SW'], tool_prefix)
55  # Matches: .init_array INIT_ARRAY 000000000516add0 5169dd0 000010 00 WA 0 0 8
56  match = re.search(r'\.init_array.*$', stdout, re.MULTILINE)
57  if expect_no_initializers:
58    if match:
59      raise Exception(
60          'Expected no initializers for %s, yet some were found' % so_path)
61    return 0
62  if not match:
63    raise Exception('Did not find section: .init_array in {}:\n{}'.format(
64        so_path, stdout))
65  size_str = re.split(r'\W+', match.group(0))[5]
66  return int(size_str, 16)
67
68
69def _CountStaticInitializers(so_path, tool_prefix, expect_no_initializers):
70  # Find the number of files with at least one static initializer.
71  # First determine if we're 32 or 64 bit
72  stdout = _RunReadelf(so_path, ['-h'], tool_prefix)
73  elf_class_line = re.search('Class:.*$', stdout, re.MULTILINE).group(0)
74  elf_class = re.split(r'\W+', elf_class_line)[1]
75  if elf_class == 'ELF32':
76    word_size = 4
77  else:
78    word_size = 8
79
80  # Then find the number of files with global static initializers.
81  # NOTE: this is very implementation-specific and makes assumptions
82  # about how compiler and linker implement global static initializers.
83  init_array_size = _ReadInitArray(so_path, tool_prefix, expect_no_initializers)
84  assert init_array_size % word_size == 0
85  return init_array_size // word_size
86
87
88def _AnalyzeStaticInitializers(apk_or_aab, tool_prefix, dump_sis, out_dir,
89                               ignored_libs, no_initializers_libs):
90  with zipfile.ZipFile(apk_or_aab) as z:
91    so_files = [
92        f for f in z.infolist() if f.filename.endswith('.so')
93        and f.file_size > 0 and os.path.basename(f.filename) not in ignored_libs
94    ]
95    # Skip checking static initializers for secondary abi libs. They will be
96    # checked by 32-bit bots. This avoids the complexity of finding 32 bit .so
97    # files in the output directory in 64 bit builds.
98    has_64 = any('64' in f.filename for f in so_files)
99    files_to_check = [f for f in so_files if not has_64 or '64' in f.filename]
100
101    # Do not check partitioned libs. They have no ".init_array" section since
102    # all SIs are considered "roots" by the linker, and so end up in the base
103    # module.
104    files_to_check = [
105        f for f in files_to_check if not f.filename.endswith('_partition.so')
106    ]
107
108    si_count = 0
109    for f in files_to_check:
110      lib_basename = os.path.basename(f.filename)
111      expect_no_initializers = lib_basename in no_initializers_libs
112      with tempfile.NamedTemporaryFile(prefix=lib_basename) as temp:
113        temp.write(z.read(f))
114        temp.flush()
115        si_count += _CountStaticInitializers(temp.name, tool_prefix,
116                                             expect_no_initializers)
117        if dump_sis:
118          _DumpStaticInitializers(f.filename, temp.name, out_dir, tool_prefix)
119  return si_count
120
121
122def main():
123  parser = argparse.ArgumentParser()
124  parser.add_argument('--touch', help='File to touch upon success')
125  parser.add_argument('--tool-prefix', required=True,
126                      help='Prefix for nm and friends')
127  parser.add_argument('--expected-count', required=True, type=int,
128                      help='Fail if number of static initializers is not '
129                           'equal to this value.')
130  parser.add_argument('apk_or_aab', help='Path to .apk or .aab file.')
131  args = parser.parse_args()
132
133  # TODO(crbug.com/838414): add support for files included via loadable_modules.
134  ignored_libs = {
135      'libarcore_sdk_c.so', 'libcrashpad_handler_trampoline.so',
136      'libsketchology_native.so'
137  }
138  # The chromium linker doesn't have static initializers, which makes the
139  # regular check throw. It should not have any.
140  no_initializers_libs = ['libchromium_android_linker.so']
141
142  si_count = _AnalyzeStaticInitializers(args.apk_or_aab, args.tool_prefix,
143                                        False, '.', ignored_libs,
144                                        no_initializers_libs)
145  if si_count != args.expected_count:
146    print('Expected {} static initializers, but found {}.'.format(
147        args.expected_count, si_count))
148    if args.expected_count > si_count:
149      print('You have removed one or more static initializers. Thanks!')
150      print('To fix the build, update the expectation in:')
151      print('    //chrome/android/static_initializers.gni')
152      print()
153
154    print('Dumping static initializers via dump-static-initializers.py:')
155    sys.stdout.flush()
156    _AnalyzeStaticInitializers(args.apk_or_aab, args.tool_prefix, True, '.',
157                               ignored_libs, no_initializers_libs)
158    print()
159    print('For more information:')
160    print('    https://chromium.googlesource.com/chromium/src/+/main/docs/'
161          'static_initializers.md')
162    sys.exit(1)
163
164  if args.touch:
165    open(args.touch, 'w')
166
167
168if __name__ == '__main__':
169  main()
170