1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
|
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Utilities to get and manipulate symbols from a binary."""
import collections
import logging
import os
import re
import subprocess
import sys
import cygprofile_utils
START_OF_TEXT_SYMBOL = 'linker_script_start_of_text'
_SRC_PATH = os.path.abspath(os.path.join(
os.path.dirname(__file__), os.pardir, os.pardir))
sys.path.insert(0, os.path.join(_SRC_PATH, 'build', 'android'))
from pylib.constants import host_paths
_MAX_WARNINGS_TO_PRINT = 200
SymbolInfo = collections.namedtuple('SymbolInfo', ('name', 'offset', 'size',
'section'))
# Unfortunate global variable :-/
_arch = 'arm'
def SetArchitecture(arch):
"""Set the architecture for binaries to be symbolized."""
global _arch
_arch = arch
# Regular expression to match lines printed by 'objdump -t -w'. An example of
# such line looks like this:
# 018db2de l F .text 00000060 .hidden _ZN8SkBitmapC2ERKS_
#
# The regex intentionally allows matching more than valid inputs. This gives
# more protection against potentially incorrectly silently ignoring unmatched
# input lines. Instead a few assertions early in _FromObjdumpLine() check the
# validity of a few parts matched as groups.
_OBJDUMP_LINE_RE = re.compile(r'''
# The offset of the function, as hex.
(?P<offset>^[0-9a-f]+)
# The space character.
[ ]
# The 7 groups of flag characters, one character each.
(
(?P<assert_scope>.) # Global, local, unique local, etc.
(?P<assert_weak_or_strong>.)
(?P<assert_4spaces>.{4}) # Constructor, warning, indirect ref,
# debugger symbol.
(?P<symbol_type>.) # Function, object, file or normal.
)
[ ]
# The section name should start with ".text", can be ".text.foo". With LLD,
# and especially LTO the traces of input sections are not preserved. Support
# ".text.foo" for a little longer time because it is easy.
(?P<section>.text[^0-9a-f]*)
(?P<assert_tab> \s+)
# The size of the symbol, as hex.
(?P<size>[0-9a-f]+)
# Normally separated out by 14 spaces, but some bits in ELF may theoretically
# affect this length.
(?P<assert_14spaces>[ ]+)
# Hidden symbols should be treated as usual.
(.hidden [ ])?
# The symbol name.
(?P<name>.*)
$
''', re.VERBOSE)
def _FromObjdumpLine(line):
"""Create a SymbolInfo by parsing a properly formatted objdump output line.
Args:
line: line from objdump
Returns:
An instance of SymbolInfo if the line represents a symbol, None otherwise.
"""
m = _OBJDUMP_LINE_RE.match(line)
if not m:
return None
# A symbol can be (g)lobal, (l)ocal, or neither (a space). Per objdump's
# manpage, "A symbol can be neither local or global for a variety of reasons".
assert m.group('assert_scope') in set(['g', 'l', ' ']), line
assert m.group('assert_weak_or_strong') in set(['w', ' ']), line
assert m.group('assert_tab') == '\t', line
assert m.group('assert_4spaces') == ' ' * 4, line
assert m.group('assert_14spaces') == ' ' * 14, line
name = m.group('name')
offset = int(m.group('offset'), 16)
# Output the label that contains the earliest offset. It is needed later for
# translating offsets from the profile dumps.
if name == START_OF_TEXT_SYMBOL:
return SymbolInfo(name=name, offset=offset, section='.text', size=0)
# Check symbol type for validity and ignore some types.
# From objdump manual page: The symbol is the name of a function (F) or a file
# (f) or an object (O) or just a normal symbol (a space). The 'normal' symbols
# seens so far has been function-local labels.
symbol_type = m.group('symbol_type')
if symbol_type == ' ':
# Ignore local goto labels. Unfortunately, v8 builtins (like 'Builtins_.*')
# are indistinguishable from labels of size 0 other than by name.
return None
# Guard against file symbols, since they are normally not seen in the
# binaries we parse.
assert symbol_type != 'f', line
# Extract the size from the ELF field. This value sometimes does not reflect
# the real size of the function. One reason for that is the '.size' directive
# in the assembler. As a result, a few functions in .S files have the size 0.
# They are not instrumented (yet), but maintaining their order in the
# orderfile may be important in some cases.
size = int(m.group('size'), 16)
# Forbid ARM mapping symbols and other unexpected symbol names, but allow $
# characters in a non-initial position, which can appear as a component of a
# mangled name, e.g. Clang can mangle a lambda function to:
# 02cd61e0 l F .text 000000c0 _ZZL11get_globalsvENK3$_1clEv
# The equivalent objdump line from GCC is:
# 0325c58c l F .text 000000d0 _ZZL11get_globalsvENKUlvE_clEv
#
# Also disallow .internal and .protected symbols (as well as other flags),
# those have not appeared in the binaries we parse. Rejecting these extra
# prefixes is done by disallowing spaces in symbol names.
assert re.match('^[a-zA-Z0-9_.][a-zA-Z0-9_.$]*$', name), name
return SymbolInfo(name=name, offset=offset, section=m.group('section'),
size=size)
def _SymbolInfosFromStream(objdump_lines):
"""Parses the output of objdump, and get all the symbols from a binary.
Args:
objdump_lines: An iterable of lines
Returns:
A list of SymbolInfo.
"""
name_to_offsets = collections.defaultdict(list)
symbol_infos = []
for line in objdump_lines:
symbol_info = _FromObjdumpLine(line.rstrip('\n'))
if symbol_info is not None:
# On ARM the LLD linker inserts pseudo-functions (thunks) that allow
# jumping distances farther than 16 MiB. Such thunks are known to often
# reside on multiple offsets, they are not instrumented and hence they do
# not reach the orderfiles. Exclude the thunk symbols from the warning.
if not symbol_info.name.startswith('__ThumbV7PILongThunk_'):
name_to_offsets[symbol_info.name].append(symbol_info.offset)
symbol_infos.append(symbol_info)
# Outlined functions are known to be repeated often, so ignore them in the
# repeated symbol count.
repeated_symbols = filter(lambda s: len(name_to_offsets[s]) > 1,
(k for k in name_to_offsets.keys()
if not k.startswith('OUTLINED_FUNCTION_')))
if repeated_symbols:
# Log the first 5 repeated offsets of the first 10 repeated symbols.
logging.warning('%d symbols repeated with multiple offsets:\n %s',
len(repeated_symbols), '\n '.join(
'{} {}'.format(sym, ' '.join(
str(offset) for offset in name_to_offsets[sym][:5]))
for sym in repeated_symbols[:10]))
return symbol_infos
def SymbolInfosFromBinary(binary_filename):
"""Runs objdump to get all the symbols from a binary.
Args:
binary_filename: path to the binary.
Returns:
A list of SymbolInfo from the binary.
"""
command = (host_paths.ToolPath('objdump', _arch), '-t', '-w', binary_filename)
p = subprocess.Popen(command, shell=False, stdout=subprocess.PIPE)
try:
result = _SymbolInfosFromStream(p.stdout)
return result
finally:
p.stdout.close()
p.wait()
_LLVM_NM_LINE_RE = re.compile(
r'^[\-0-9a-f]{8,16}[ ](?P<symbol_type>.)[ ](?P<name>.*)$', re.VERBOSE)
def _SymbolInfosFromLlvmNm(lines):
"""Extracts all defined symbols names from llvm-nm output.
Only defined (weak and regular) symbols are extracted.
Args:
lines: Iterable of lines.
Returns:
[str] A list of symbol names, can be empty.
"""
symbol_names = []
for line in lines:
m = _LLVM_NM_LINE_RE.match(line)
assert m is not None, line
if m.group('symbol_type') not in ['t', 'T', 'w', 'W']:
continue
symbol_names.append(m.group('name'))
return symbol_names
_NM_PATH = os.path.join(_SRC_PATH, 'third_party', 'llvm-build',
'Release+Asserts', 'bin', 'llvm-nm')
def CheckLlvmNmExists():
assert os.path.exists(_NM_PATH), (
'llvm-nm not found. Please run '
'//tools/clang/scripts/update.py --package=objdump to install it.')
def SymbolNamesFromLlvmBitcodeFile(filename):
"""Extracts all defined symbols names from an LLVM bitcode file.
Args:
filename: (str) File to parse.
Returns:
[str] A list of symbol names, can be empty.
"""
command = (_NM_PATH, '-defined-only', filename)
p = subprocess.Popen(command, shell=False, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
try:
result = _SymbolInfosFromLlvmNm(p.stdout)
if not result:
file_size = os.stat(filename).st_size
logging.warning('No symbols for %s (size %d)', filename, file_size)
return result
finally:
_, _ = p.communicate()
p.stdout.close()
assert p.wait() == 0
def GroupSymbolInfosByOffset(symbol_infos):
"""Create a dict {offset: [symbol_info1, ...], ...}.
As several symbols can be at the same offset, this is a 1-to-many
relationship.
Args:
symbol_infos: iterable of SymbolInfo instances
Returns:
a dict {offset: [symbol_info1, ...], ...}
"""
offset_to_symbol_infos = collections.defaultdict(list)
for symbol_info in symbol_infos:
offset_to_symbol_infos[symbol_info.offset].append(symbol_info)
return dict(offset_to_symbol_infos)
def GroupSymbolInfosByName(symbol_infos):
"""Create a dict {name: [symbol_info1, ...], ...}.
A symbol can have several offsets, this is a 1-to-many relationship.
Args:
symbol_infos: iterable of SymbolInfo instances
Returns:
a dict {name: [symbol_info1, ...], ...}
"""
name_to_symbol_infos = collections.defaultdict(list)
for symbol_info in symbol_infos:
name_to_symbol_infos[symbol_info.name].append(symbol_info)
return dict(name_to_symbol_infos)
def CreateNameToSymbolInfo(symbol_infos):
"""Create a dict {name: symbol_info, ...}.
Args:
symbol_infos: iterable of SymbolInfo instances
Returns:
a dict {name: symbol_info, ...}
If a symbol name corresponds to more than one symbol_info, the symbol_info
with the lowest offset is chosen.
"""
# TODO(lizeb,pasko): move the functionality in this method into
# check_orderfile.
symbol_infos_by_name = {}
warnings = cygprofile_utils.WarningCollector(_MAX_WARNINGS_TO_PRINT)
for infos in GroupSymbolInfosByName(symbol_infos).itervalues():
first_symbol_info = min(infos, key=lambda x: x.offset)
symbol_infos_by_name[first_symbol_info.name] = first_symbol_info
if len(infos) > 1:
warnings.Write('Symbol %s appears at %d offsets: %s' %
(first_symbol_info.name,
len(infos),
','.join([hex(x.offset) for x in infos])))
warnings.WriteEnd('symbols at multiple offsets.')
return symbol_infos_by_name
def DemangleSymbol(mangled_symbol):
"""Return the demangled form of mangled_symbol."""
cmd = [host_paths.ToolPath('c++filt', _arch)]
process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
demangled_symbol, _ = process.communicate(mangled_symbol + '\n')
return demangled_symbol
|