Port bloat from nm/linux to ld64 -map/mac.
diff --git a/README.markdown b/README.markdown index aab7cbe..8508f82 100644 --- a/README.markdown +++ b/README.markdown
@@ -12,14 +12,14 @@ 2) Build your binary with the `-g` flag to get symbols. -3) Run `./bloat.py --help` and generate `nm.out` as instructed there. +3) Run `./bloat.py --help` and generate `a.out.map` as instructed there. 4) Example command line: - ./bloat.py --strip-prefix=/path/to/src syms > bloat.json + ./bloat.py syms --map-output ~/src/llvm-svn//tools/clang/cf.map > bloat.json ## Misc other feature Dump large symbols: - $ ./bloat.py dump | head -20 + ./bloat.py dump | head -20
diff --git a/bloat.py b/bloat.py index 6abf55c..afc214b 100755 --- a/bloat.py +++ b/bloat.py
@@ -48,54 +48,70 @@ }[type] -def parse_nm(input): - """Parse nm output. +def parse_map(input): + """Parse ld64 -map output. - Argument: an iterable over lines of nm output. + Argument: an iterable over lines of ld64's -map output. Yields: (symbol name, symbol type, symbol size, source file path). - Path may be None if nm couldn't figure out the source file. + Path may be None if we couldn't figure out the source file. """ - # Match lines with size + symbol + optional filename. - sym_re = re.compile(r'^[0-9a-f]+ ([0-9a-f]+) (.) ([^\t]+)(?:\t(.*):\d+)?$') + START, OBJFILES, SECTIONS, SYMBOLS = range(4) + mode = START - # Match lines with addr but no size. - addr_re = re.compile(r'^[0-9a-f]+ (.) ([^\t]+)(?:\t.*)?$') - # Match lines that don't have an address at all -- typically external symbols. - noaddr_re = re.compile(r'^ + (.) (.*)$') + objfiles = [] + # matches: + # [ 1] /Volumes/MacintoshHD2/../ang-format/Release+Asserts/ClangFormat.o + obj_re = re.compile(r'^\[\s*(\d+)\] (.+)$') + + # matches: + # 0x100000EA0 0x000015B7 [ 1] __ZN5cl....N4llvm9StringRefE + sym_re = re.compile(r'^(0x[0-9A-F]+)\t(0x[0-9A-F]+)\t\[\s*(\d+)\] (.+)$') for line in input: line = line.rstrip() - match = sym_re.match(line) - if match: - size, type, sym = match.groups()[0:3] + if line == '# Object files:': + mode = OBJFILES + continue + if line == '# Sections:': + mode = SECTIONS + continue + if line == '# Symbols:': + mode = SYMBOLS + continue + if line.startswith('#'): + if line.endswith(':'): + raise Exception('Unknown section "%s"' % line) + continue # Ignore comments. + + if mode == OBJFILES: + match = obj_re.match(line) + if match: + index, path = match.groups() + index = int(index) + assert index == len(objfiles), '%d vs %d' % (index, len(objfiles)) + objfiles.append(path) + continue + elif mode == SECTIONS: + continue + elif mode == SYMBOLS: + match = sym_re.match(line) + if match: + start, size, file_index, sym = match.groups() + start = int(start, 16) size = int(size, 16) - type = type.lower() - if type in ['u', 'v']: - type = 'w' # just call them all weak - if type == 'b': - continue # skip all BSS for now - path = match.group(4) - yield sym, type, size, path + file_index = int(file_index) + path = objfiles[file_index] + yield sym, 't', size, path continue - match = addr_re.match(line) - if match: - type, sym = match.groups()[0:2] - # No size == we don't care. - continue - match = noaddr_re.match(line) - if match: - type, sym = match.groups() - if type in ('U', 'w'): - # external or weak symbol - continue print >>sys.stderr, 'unparsed:', repr(line) + def demangle(ident, cppfilt): - if cppfilt and ident.startswith('_Z'): - # Demangle names when possible. Mangled names all start with _Z. + if cppfilt and ident.startswith('__Z'): + # Demangle names when possible. Mangled names all start with __Z. ident = subprocess.check_output([cppfilt, ident]).strip() return ident @@ -215,7 +231,12 @@ path = path[1:] path = ['[path]'] + path.split('/') - parts = parse_cpp_name(sym, cppfilt) + if sym.startswith('literal string: '): + parts = sym.split(':', 1) + parts[1] = repr(parts[1]) + else: + sym = demangle(sym, cppfilt) + parts = parse_cpp_name(sym, cppfilt) if len(parts) == 1: if path: # No namespaces, group with path. @@ -301,85 +322,24 @@ } -def dump_nm(nmfile, strip_prefix, cppfilt): - dirs = treeify_syms(parse_nm(nmfile), strip_prefix, cppfilt) +def dump_map(mapfile, strip_prefix, cppfilt): + dirs = treeify_syms(parse_map(mapfile), strip_prefix, cppfilt) print ('var kTree = ' + json.dumps(jsonify_tree(dirs, '[everything]'), indent=2)) -def parse_objdump(input): - """Parse objdump -h output.""" - sec_re = re.compile('^\d+ (\S+) +([0-9a-z]+)') - sections = [] - debug_sections = [] - - for line in input: - line = line.strip() - match = sec_re.match(line) - if match: - name, size = match.groups() - if name.startswith('.'): - name = name[1:] - if name.startswith('debug_'): - name = name[len('debug_'):] - debug_sections.append((name, int(size, 16))) - else: - sections.append((name, int(size, 16))) - continue - return sections, debug_sections - - -def jsonify_sections(name, sections): - children = [] - total = 0 - for section, size in sections: - children.append({ - 'name': section + ' ' + format_bytes(size), - 'data': { '$area': size } - }) - total += size - - children.sort(key=lambda child: -child['data']['$area']) - - return { - 'name': name + ' ' + format_bytes(total), - 'data': { '$area': total }, - 'children': children - } - - -def dump_sections(objdump): - sections, debug_sections = parse_objdump(objdump) - sections = jsonify_sections('sections', sections) - debug_sections = jsonify_sections('debug', debug_sections) - size = sections['data']['$area'] + debug_sections['data']['$area'] - print 'var kTree = ' + json.dumps({ - 'name': 'top ' + format_bytes(size), - 'data': { '$area': size }, - 'children': [ debug_sections, sections ]}) - - usage="""%prog [options] MODE Modes are: syms: output symbols json suitable for a treemap dump: print symbols sorted by size (pipe to head for best output) - sections: output binary sections json suitable for a treemap -nm output passed to --nm-output should from running a command -like the following (note, can take a long time -- 30 minutes): - nm -C -S -l /path/to/binary > nm.out - -objdump output passed to --objdump-output should be from a command -like: - objdump -h /path/to/binary > objdump.out""" +ld64 -map output passed to the linker: + ld64 -o /path/to/binary *.o -Wl,-map,a.out.map""" parser = optparse.OptionParser(usage=usage) -parser.add_option('--nm-output', action='store', dest='nmpath', - metavar='PATH', default='nm.out', - help='path to nm output [default=nm.out]') -parser.add_option('--objdump-output', action='store', dest='objdumppath', - metavar='PATH', default='objdump.out', - help='path to objdump output [default=objdump.out]') +parser.add_option('--map-output', action='store', dest='mappath', + metavar='PATH', default='a.out.map', + help='path to ld64 -map output [default=a.out.map]') parser.add_option('--strip-prefix', metavar='PATH', action='store', help='strip PATH prefix from paths; e.g. /path/to/src/root') parser.add_option('--filter', action='store', @@ -396,7 +356,7 @@ mode = args[0] if mode == 'syms': - nmfile = open(opts.nmpath, 'r') + mapfile = open(opts.mappath, 'r') try: res = subprocess.check_output([opts.cppfilt, 'main']) if res.strip() != 'main': @@ -407,13 +367,10 @@ print >>sys.stderr, ("Could not find c++filt at %s, " "output won't be demangled." % opt.cppfilt) opts.cppfilt = None - dump_nm(nmfile, strip_prefix=opts.strip_prefix, cppfilt=opts.cppfilt) -elif mode == 'sections': - objdumpfile = open(opts.objdumppath, 'r') - dump_sections(objdumpfile) + dump_map(mapfile, strip_prefix=opts.strip_prefix, cppfilt=opts.cppfilt) elif mode == 'dump': - nmfile = open(opts.nmpath, 'r') - syms = list(parse_nm(nmfile)) + mapfile = open(opts.mappath, 'r') + syms = list(parse_map(mapfile)) # a list of (sym, type, size, path); sort by size. syms.sort(key=lambda x: -x[2]) total = 0