summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--README.rst14
-rw-r--r--linkermapviz/__init__.py161
-rw-r--r--setup.py21
4 files changed, 199 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c0bc0dd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+__pycache__
+*.sw?
+*.egg-info
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..f638ee6
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,14 @@
+linkermapviz
+============
+
+Interactive visualization of GNU ld’s linker map with a tree map. Usage:
+
+.. code:: bash
+
+ pip install .
+ gcc -Wl,-Map,output.map -o output input.c
+ linkermapviz < output.map
+
+Works best with ``-ffunction-sections -fdata-sections`` and statically linked
+code (e.g. for bare-metal embedded systems).
+
diff --git a/linkermapviz/__init__.py b/linkermapviz/__init__.py
new file mode 100644
index 0000000..525dfc1
--- /dev/null
+++ b/linkermapviz/__init__.py
@@ -0,0 +1,161 @@
+import sys, re, os
+from itertools import chain
+import squarify
+
+from bokeh.plotting import figure, show, output_file, ColumnDataSource
+from bokeh.models import HoverTool, LabelSet
+from bokeh.models.mappers import CategoricalColorMapper
+from bokeh.palettes import Category10
+from bokeh.layouts import column
+
+class Objectfile:
+ def __init__ (self, section, offset, size, comment):
+ self.section = section.strip ()
+ self.offset = offset
+ self.size = size
+ self.path = (None, None)
+ if comment:
+ self.path = re.match (r'^(.+?)(?:\(([^\)]+)\))?$', comment).groups ()
+ self.children = []
+
+ def __repr__ (self):
+ return '<Objectfile {} {:x} {:x} {} {}>'.format (self.section, self.offset, self.size, self.path, repr (self.children))
+
+def parseSections (fd):
+ """
+ Quick&Dirty parsing for GNU ld’s linker map output, needs LANG=C, because
+ some messages are localized.
+ """
+
+ sections = []
+
+ # skip until memory map is found
+ found = False
+ for l in sys.stdin:
+ if l.strip () == 'Memory Configuration':
+ found = True
+ break
+ if not found:
+ return None
+
+ # long section names result in a linebreak afterwards
+ sectionre = re.compile ('(?P<section>.+?|.{14,}\n)[ ]+0x(?P<offset>[0-9a-f]+)[ ]+0x(?P<size>[0-9a-f]+)(?:[ ]+(?P<comment>.+))?\n+', re.I)
+ subsectionre = re.compile ('[ ]{16}0x(?P<offset>[0-9a-f]+)[ ]+(?P<function>.+)\n+', re.I)
+ s = sys.stdin.read ()
+ pos = 0
+ while True:
+ m = sectionre.match (s, pos)
+ if not m:
+ # skip that line
+ try:
+ nextpos = s.index ('\n', pos)+1
+ pos = nextpos
+ continue
+ except ValueError:
+ break
+ pos = m.end ()
+ section = m.group ('section')
+ v = m.group ('offset')
+ offset = int (v, 16) if v is not None else None
+ v = m.group ('size')
+ size = int (v, 16) if v is not None else None
+ comment = m.group ('comment')
+ if section != '*default*' and size > 0:
+ of = Objectfile (section, offset, size, comment)
+ if section.startswith (' '):
+ sections[-1].children.append (of)
+ while True:
+ m = subsectionre.match (s, pos)
+ if not m:
+ break
+ pos = m.end ()
+ offset, function = m.groups ()
+ offset = int (offset, 16)
+ if sections and sections[-1].children:
+ sections[-1].children[-1].children.append ((offset, function))
+ else:
+ sections.append (of)
+
+ return sections
+
+def main ():
+ sections = parseSections (sys.stdin)
+ if sections is None:
+ print ('start of memory config not found, did you invoke the compiler/linker with LANG=C?')
+ return
+
+ sectionWhitelist = {'.text', '.data', '.bss'}
+ plots = []
+ whitelistedSections = list (filter (lambda x: x.section in sectionWhitelist, sections))
+ allObjects = list (chain (*map (lambda x: x.children, whitelistedSections)))
+ allFiles = list (set (map (lambda x: os.path.basename (x.path[0]) if x.path[0] else None, allObjects)))
+ for s in whitelistedSections:
+ objects = s.children
+ objects.sort (reverse=True, key=lambda x: x.size)
+ values = list (map (lambda x: x.size, objects))
+ totalsize = sum (values)
+
+ x = 0
+ y = 0
+ width = 1000
+ height = 1000
+ values = squarify.normalize_sizes (values, width, height)
+ rects = squarify.squarify(values, x, y, width, height)
+ padded_rects = squarify.padded_squarify(values, x, y, width, height)
+
+ # plot with bokeh
+ output_file('linkermap.html', title='Linker map')
+
+ top = list (map (lambda x: x['y'], padded_rects))
+ bottom = list (map (lambda x: x['y']+x['dy'], padded_rects))
+ left = list (map (lambda x: x['x'], padded_rects))
+ right = list (map (lambda x: x['x']+x['dx'], padded_rects))
+ files = list (map (lambda x: os.path.basename (x.path[0]) if x.path[0] else None, objects))
+ size = list (map (lambda x: x.size, objects))
+ children = list (map (lambda x: ','.join (map (lambda x: x[1], x.children)) if x.children else x.section, objects))
+ source = ColumnDataSource(data=dict(
+ top=top,
+ bottom=bottom,
+ left=left,
+ right=right,
+ file=files,
+ size=size,
+ children=children,
+ ))
+
+ hover = HoverTool(tooltips=[
+ ("size", "@size"),
+ ("file", "@file"),
+ ])
+
+
+ p = figure(title='Linker map for section {} ({} bytes)'.format (s.section, totalsize),
+ plot_width=width, plot_height=height,
+ tools=[hover,'pan','wheel_zoom','box_zoom','reset'],
+ x_range=(0, width), y_range=(0, height))
+
+ p.xaxis.visible = False
+ p.xgrid.visible = False
+ p.yaxis.visible = False
+ p.ygrid.visible = False
+
+ palette = Category10[10]
+ mapper = CategoricalColorMapper (palette=palette, factors=allFiles)
+ p.quad (top='top', bottom='bottom', left='left', right='right', source=source, color={'field': 'file', 'transform': mapper}, legend='file')
+ labels = LabelSet(x='left', y='top', text='children', level='glyph',
+ x_offset=5, y_offset=5, source=source, render_mode='canvas')
+ p.add_layout (labels)
+ labels = LabelSet(x='left', y='top', text='size', level='glyph',
+ x_offset=5, y_offset=20, source=source, render_mode='canvas')
+ p.add_layout (labels)
+
+ # set up legend, must be done after plotting
+ p.legend.location = "top_left"
+ p.legend.orientation = "horizontal"
+
+ plots.append (p)
+ show (column (*plots, responsive=True))
+
+if __name__ == '__main__':
+ main ()
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..0e05383
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,21 @@
+from distutils.core import setup
+
+setup(
+ name='linkermapviz',
+ version='0.1.0',
+ author='Lars-Dominik Braun',
+ author_email='lars+linkermapviz@6xq.net',
+ packages=['linkermapviz'],
+ license='LICENSE.txt',
+ description='Visualize GNU ld’s linker map with a tree map.',
+ install_requires=[
+ 'bokeh',
+ 'squarify',
+ ],
+ entry_points={
+ 'console_scripts': [
+ 'linkermapviz = linkermapviz:main',
+ ],
+ },
+)
+