85 state_syms = collections.defaultdict(list)
86 calls = collections.defaultdict(set)
90 sys.exit(f
"error: no .o files under {build_dir}")
97 row = re.compile(
r"^[0-9a-fA-F]+\s+(?P<flags>.{7})\s+(?P<section>\S+)\t[0-9a-fA-F]+\s+"
98 r"(?:\.(?:hidden|protected|internal)\s+)?(?P<name>\S+)")
105 out = subprocess.run([
"objdump",
"-t", obj], capture_output=
True, text=
True).stdout
106 except FileNotFoundError:
107 sys.exit(
"error: objdump not found")
108 for line
in out.splitlines():
112 name, section, flags = m.group(
"name"), m.group(
"section"), m.group(
"flags")
113 if NOISE.match(name):
115 if section ==
"*ABS*" or name == section:
117 if section ==
"*UND*":
119 elif READONLY_SECTIONS.match(section):
122 defines.setdefault(name, src)
123 elif MUTABLE_SECTIONS.match(section):
125 state_syms[src].append(name)
126 elif section.startswith(
".text"):
127 defines.setdefault(name, src)
128 return defines, state_syms, calls
132 """A mutable symbol defined in more than one object came from a header.
134 `static const char *dt_supported_extensions[]` in config.h and `loaders_info[]` in
135 common/image.h are emitted into every translation unit that includes them, so they show up
136 as "own state" for 170 files that never heard of them. They are still mutable storage --
137 `const char *` makes the pointee const, not the array -- and each copy is independently
138 writable, which is worth fixing at the header. But they are the header's problem, not each
139 includer's, and counting them per-includer buries every real finding.
141 seen = collections.Counter()
142 for syms
in state_syms.values():
143 seen.update(set(syms))
149 for dirpath, dirnames, filenames
in os.walk(os.path.join(REPO,
"src")):
150 dirnames[:] = [d
for d
in dirnames
if d
not in (
"external",
"build")]
152 if fn.endswith((
".h",
".hpp",
".cmake.h")):
154 with open(os.path.join(dirpath, fn), errors=
"ignore")
as fh:
155 header_text.append(fh.read())
158 headers =
"\n".join(header_text)
160 header_defined = set()
161 for sym, n
in seen.items():
164 bare = re.sub(
r"^_ZL\d+",
"", sym)
165 bare = re.sub(
r"\.\d+$",
"", bare)
168 r"^\s*(?:static\s+)?[A-Za-z_][\w \t*]*\b" + re.escape(bare) +
r"\s*(?:\[|=)",
170 r"^\s*}\s*" + re.escape(bare) +
r"\s*\[",
172 if any(re.search(pat, headers, re.M)
for pat
in patterns):
173 header_defined.add(sym)
174 own = {src: [s
for s
in syms
if s
not in header_defined]
175 for src, syms
in state_syms.items()}
176 return {k: v
for k, v
in own.items()
if v}, sorted(header_defined)
198 build =
"build-debug"
199 if "--build" in sys.argv:
200 build = sys.argv[sys.argv.index(
"--build") + 1]
202 if "--dir" in sys.argv:
203 only = sys.argv[sys.argv.index(
"--dir") + 1]
205 defines, raw_state, calls =
scan(os.path.join(REPO, build))
207 stateful =
propagate(defines, state_syms, calls)
209 files = sorted(set(list(calls) + list(state_syms)))
211 files = [f
for f
in files
if f.startswith(only)]
213 if "--json" in sys.argv:
214 print(json.dumps({f: {
"stateful": f
in stateful,
215 "own_state": sorted(state_syms.get(f, [])),
216 "chain": stateful.get(f, [])}
for f
in files}, indent=2))
219 direct = [f
for f
in files
if state_syms.get(f)]
220 indirect = [f
for f
in files
if f
in stateful
and not state_syms.get(f)]
221 clean = [f
for f
in files
if f
not in stateful]
223 scope = only
or "src/"
224 print(f
"{len(files)} translation unit(s) under {scope}: "
225 f
"{len(clean)} stateless, {len(direct)} with own state, {len(indirect)} reaching state\n")
227 if header_defined
and not only:
228 print(f
"--- MUTABLE DATA DEFINED IN HEADERS ({len(header_defined)}) ---")
229 print(
" One writable copy per including translation unit. Not any single file's")
230 print(
" state; fix at the header (usually a missing second const).")
231 for sym
in header_defined:
236 print(
"--- OWN STATE (mutable storage at file scope) ---")
239 print(f
" {f} ({len(syms)}): {', '.join(sorted(syms)[:6])}"
240 + (
" ..." if len(syms) > 6
else ""))
243 print(
"--- REACHES STATE (through what it calls) ---")
247 print(f
" via {' -> '.join(chain[1:4])}" + (
" ..." if len(chain) > 4
else ""))
250 print(f
"--- STATELESS ({len(clean)}) ---")