]>
git.proxmox.com Git - mirror_frr.git/blob - tools/symalyzer.py
3 # 2019 by David Lamparter, placed in public domain
5 # This tool generates a report of possibly unused symbols in the build. It's
6 # particularly useful for libfrr to find bitrotting functions that aren't even
7 # used anywhere anymore.
9 # Note that the tool can't distinguish between "a symbol is completely unused"
10 # and "a symbol is used only in its file" since file-internal references are
11 # invisible in nm output. However, the compiler will warn you if a static
14 # This tool is only tested on Linux, it probably needs `nm` from GNU binutils
15 # (as opposed to BSD `nm`). Could use pyelftools instead but that's a lot of
18 # This is a developer tool, please don't put it in any packages :)
20 import sys
, os
, subprocess
22 from collections
import namedtuple
26 os
.path
.join(os
.path
.dirname(os
.path
.dirname(os
.path
.abspath(__file__
))), "python"),
29 from makevars
import MakeVars
31 SymRowBase
= namedtuple(
48 class SymRow(SymRowBase
):
50 wrapper around a line of `nm` output
53 lib_re
= re
.compile(r
"/lib[^/]+\.(so|la)$")
56 return self
.klass
.isupper() or self
.klass
in "uvw"
59 if self
.lib_re
.search(self
.target
) is None:
66 FRR-specific list of symbols which are considered "externally used"
68 e.g. hooks are by design APIs for external use, same for qobj_t_*
69 frr_inet_ntop is here because it's used through an ELF alias to
72 if self
.name
in ["main", "frr_inet_ntop", "_libfrr_version"]:
74 if self
.name
.startswith("_hook_"):
76 if self
.name
.startswith("qobj_t_"):
83 dict of all symbols in all libs & executables
86 from_re
= re
.compile(r
"^Symbols from (.*?):$")
87 lt_re
= re
.compile(r
"^(.*/)([^/]+)\.l[oa]$")
92 class ReportSym(object):
93 def __init__(self
, sym
):
97 return "<%-25s %-40s [%s]>" % (
98 self
.__class
__.__name
__ + ":",
103 def __lt__(self
, other
):
104 return self
.sym
.name
.__lt
__(other
.sym
.name
)
106 class ReportSymCouldBeStaticAlreadyLocal(ReportSym
):
108 idlong
= "extrastatic"
109 title
= "symbol is local to library, but only used in its source file (make static?)"
111 class ReportSymCouldBeStatic(ReportSym
):
114 title
= "symbol is only used in its source file (make static?)"
116 class ReportSymCouldBeLibLocal(ReportSym
):
119 title
= "symbol is only used inside of library"
121 class ReportSymModuleAPI(ReportSym
):
124 title
= "symbol (in executable) is referenced externally from a module"
126 class Symbol(object):
127 def __init__(self
, name
):
133 def process(self
, row
):
135 if row
.section
== "*UND*":
136 self
.refs
.append(row
)
138 self
.defs
.setdefault(scope
, []).append(row
)
140 def evaluate(self
, out
):
142 generate output report
144 invoked after all object files have been read in, so it can look
145 at inter-object-file relationships
147 if len(self
.defs
) == 0:
148 out
.extsyms
.add(self
.name
)
151 for scopename
, symdefs
in self
.defs
.items():
153 symdef
for symdef
in symdefs
if symdef
.section
== "*COM*"
156 symdef
for symdef
in symdefs
if symdef
.section
!= "*COM*"
159 if len(proper_defs
) > 1:
160 print(self
.name
, " DUPLICATE")
163 % (scopename
, "\n\t\t".join([repr(s
) for s
in symdefs
]))
165 for syms
in self
.refs
:
166 print("\tR: %s" % (syms
,))
170 primary_def
= proper_defs
[0]
171 elif len(common_defs
):
172 # "common" = global variables without initializer;
173 # they can occur in multiple .o files and the linker will
174 # merge them into one variable/storage location.
175 primary_def
= common_defs
[0]
177 # undefined symbol, e.g. libc
180 if scopename
is not None and len(self
.refs
) > 0:
181 for ref
in self
.refs
:
182 if ref
.target
!= primary_def
.target
and ref
.target
.endswith(
185 outobj
= out
.report
.setdefault(primary_def
.object, [])
186 outobj
.append(out
.ReportSymModuleAPI(primary_def
))
189 if len(self
.refs
) == 0:
190 if primary_def
.is_export():
192 outobj
= out
.report
.setdefault(primary_def
.object, [])
193 if primary_def
.visible
:
194 outobj
.append(out
.ReportSymCouldBeStatic(primary_def
))
197 out
.ReportSymCouldBeStaticAlreadyLocal(primary_def
)
201 if scopename
is None and primary_def
.visible
:
203 for ref
in self
.refs
:
204 if ref
.target
!= primary_def
.target
:
207 outobj
= out
.report
.setdefault(primary_def
.object, [])
208 outobj
.append(out
.ReportSymCouldBeLibLocal(primary_def
))
214 for sym
in self
.values():
217 def load(self
, target
, files
):
218 def libtoolmustdie(fn
):
219 m
= self
.lt_re
.match(fn
)
222 return m
.group(1) + ".libs/" + m
.group(2) + ".o"
224 def libtooltargetmustdie(fn
):
225 m
= self
.lt_re
.match(fn
)
227 a
, b
= fn
.rsplit("/", 1)
228 return "%s/.libs/%s" % (a
, b
)
229 return m
.group(1) + ".libs/" + m
.group(2) + ".so"
231 files
= list(set([libtoolmustdie(fn
) for fn
in files
]))
233 def parse_nm_output(text
):
235 path_rel_to
= os
.path
.dirname(os
.path
.dirname(os
.path
.abspath(__file__
)))
237 for line
in text
.split("\n"):
238 if line
.strip() == "":
240 m
= self
.from_re
.match(line
)
242 filename
= m
.group(1)
244 if line
.startswith("Name"):
247 items
= [i
.strip() for i
in line
.split("|")]
249 if "\t" in items
[-1]:
250 items
[-1], loc
= items
[-1].split("\t", 1)
251 fn
, lno
= loc
.rsplit(":", 1)
252 fn
= os
.path
.relpath(fn
, path_rel_to
)
253 loc
= "%s:%s" % (fn
, lno
)
255 items
[1] = int(items
[1] if items
[1] != "" else "0", 16)
256 items
[4] = int(items
[4] if items
[4] != "" else "0", 16)
258 row
= SymRow(target
, filename
, *items
)
260 if row
.section
== ".group" or row
.name
== "_GLOBAL_OFFSET_TABLE_":
262 if not row
.is_global():
269 # the actual symbol report uses output from the individual object files
270 # (e.g. lib/.libs/foo.o), but we also read the linked binary (e.g.
271 # lib/.libs/libfrr.so) to determine which symbols are actually visible
272 # in the linked result (this covers ELF "hidden"/"internal" linkage)
274 libfile
= libtooltargetmustdie(target
)
275 nmlib
= subprocess
.Popen(
276 ["nm", "-l", "-g", "--defined-only", "-f", "sysv", libfile
],
277 stdout
=subprocess
.PIPE
,
279 out
= nmlib
.communicate()[0].decode("US-ASCII")
281 for row
in parse_nm_output(out
):
282 visible_syms
.add(row
.name
)
284 nm
= subprocess
.Popen(
285 ["nm", "-l", "-f", "sysv"] + files
, stdout
=subprocess
.PIPE
287 out
= nm
.communicate()[0].decode("US-ASCII")
289 for row
in parse_nm_output(out
):
290 row
.visible
= row
.name
in visible_syms
291 sym
= self
.setdefault(row
.name
, self
.Symbol(row
.name
))
295 def write_html_report(syms
):
299 sys
.stderr
.write("jinja2 could not be imported, not writing HTML report!\n")
302 self_path
= os
.path
.dirname(os
.path
.abspath(__file__
))
303 jenv
= jinja2
.Environment(loader
=jinja2
.FileSystemLoader(self_path
))
304 template
= jenv
.get_template("symalyzer.html")
307 for fn
, reports
in syms
.report
.items():
308 dirname
, filename
= fn
.replace(".libs/", "").rsplit("/", 1)
309 dirgroups
.setdefault(dirname
, {})[fn
] = reports
312 "T": "code / plain old regular function (Text)",
313 "D": "global variable, read-write, with nonzero initializer (Data)",
314 "B": "global variable, read-write, with zero initializer (BSS)",
315 "C": "global variable, read-write, with zero initializer (Common)",
316 "R": "global variable, read-only (Rodata)",
319 with
open("symalyzer_report.html.tmp", "w") as fd
:
320 fd
.write(template
.render(dirgroups
=dirgroups
, klasses
=klasses
))
321 os
.rename("symalyzer_report.html.tmp", "symalyzer_report.html")
323 if not os
.path
.exists("jquery-3.4.1.min.js"):
324 url
= "https://code.jquery.com/jquery-3.4.1.min.js"
326 "trying to grab a copy of jquery from %s\nif this fails, please get it manually (the HTML output is done.)\n"
331 r
= requests
.get("https://code.jquery.com/jquery-3.4.1.min.js")
332 if r
.status_code
!= 200:
334 "failed -- please download jquery-3.4.1.min.js and put it next to the HTML report\n"
337 with
open("jquery-3.4.1.min.js.tmp", "w") as fd
:
339 os
.rename("jquery-3.4.1.min.js.tmp", "jquery-3.4.1.min.js")
340 sys
.stderr
.write("done.\n")
343 def automake_escape(s
):
344 return s
.replace(".", "_").replace("/", "_")
347 if __name__
== "__main__":
350 if not (os
.path
.exists("config.version") and os
.path
.exists("lib/.libs/libfrr.so")):
352 "please execute this script in the root directory of an FRR build tree\n"
354 sys
.stderr
.write("./configure && make need to have completed successfully\n")
361 "module_LTLIBRARIES",
365 mv
.getvars(amtargets
)
366 for amtarget
in amtargets
:
368 [item
for item
in mv
[amtarget
].strip().split() if item
!= "tools/ssd"]
371 mv
.getvars(["%s_LDADD" % automake_escape(t
) for t
in targets
])
374 ldadd
= mv
["%s_LDADD" % automake_escape(t
)].strip().split()
376 if item
.startswith("-"):
378 if item
.endswith(".a"):
381 mv
.getvars(["%s_OBJECTS" % automake_escape(o
) for o
in ldobjs
])
386 objs
= mv
["%s_OBJECTS" % automake_escape(t
)].strip().split()
387 ldadd
= mv
["%s_LDADD" % automake_escape(t
)].strip().split()
389 if item
.startswith("-"):
391 if item
.endswith(".a"):
392 objs
.extend(mv
["%s_OBJECTS" % automake_escape(item
)].strip().split())
394 sys
.stderr
.write("processing %s...\n" % t
)
396 # print(t, '\n\t', objs)
401 for obj
, reports
in sorted(syms
.report
.items()):
403 for report
in reports
:
404 print("\t%r" % report
)
406 write_html_report(syms
)