ld 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. #!/usr/bin/python3
  2. # Copyright 2020-2021 Yury Gribov
  3. #
  4. # Use of this source code is governed by MIT license that can be
  5. # found in the LICENSE.txt file.
  6. """
  7. Wrapper for ld which automatically calls implib-gen.
  8. To enable, add it to PATH.
  9. Flags are passed via env variable IMPLIBSO_LD_OPTIONS.
  10. """
  11. import sys
  12. import os
  13. import os.path
  14. import re
  15. import subprocess
  16. import argparse
  17. import tempfile
  18. import atexit
  19. import shutil
  20. me = os.path.basename(__file__)
  21. v = 0
  22. def warn(msg):
  23. """
  24. Print nicely formatted warning message.
  25. """
  26. sys.stderr.write(f'{me}: warning: {msg}\n')
  27. def error(msg):
  28. """
  29. Print nicely formatted error message and exit.
  30. """
  31. sys.stderr.write(f'{me}: error: {msg}\n')
  32. sys.exit(1)
  33. def note(msg):
  34. """
  35. Print nicely formatted message.
  36. """
  37. sys.stderr.write(f'{me}: {msg}\n')
  38. def run(cmd):
  39. """
  40. Simple wrapper for subprocess.Popen,
  41. """
  42. if isinstance(cmd, str):
  43. cmd = cmd.split(' ')
  44. if v > 0:
  45. note(f"running {cmd}...")
  46. with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
  47. out, err = p.communicate()
  48. out = out.decode('utf-8')
  49. err = err.decode('utf-8')
  50. if p.returncode != 0:
  51. sys.stdout.write(out)
  52. sys.stderr.write(err)
  53. sys.exit(p.returncode)
  54. return p.returncode, out, err
  55. def which(exe, alll=False):
  56. """
  57. Analog if which(1).
  58. """
  59. paths = os.environ['PATH'].split(os.pathsep)
  60. exes = []
  61. for p in paths:
  62. f = os.path.join(p, exe)
  63. if os.path.isfile(f):
  64. exes.append(f)
  65. first = exes[0] if exes else None
  66. return exes if alll else first
  67. def is_system_library(name):
  68. """
  69. Is library a basic one (and thus should not be wrapped)?
  70. """
  71. return name in ['c', 'm', 'dl', 'rt', 'pthread', 'gcc', 'gcc_s']
  72. def main():
  73. "Entry point"
  74. parser = argparse.ArgumentParser(description="""\
  75. Ld wrapper to simplify application of implib-gen.
  76. Flags can be specified directly or via IMPLIBSO_LD_OPTIONS environment variable.
  77. """)
  78. parser.add_argument('--verbose', '-v',
  79. help="Print diagnostic info.",
  80. action='count',
  81. default=0)
  82. parser.add_argument('--outdir', '-o',
  83. help="Directory for temp files.",
  84. default='')
  85. parser.add_argument('--wrap-libs',
  86. help="Wrap only libraries in comma-separated list.",
  87. default='')
  88. parser.add_argument('--no-wrap-libs',
  89. help="Do not wrap only libraries in comma-separated list.",
  90. default='')
  91. env_default = '--help' if len(sys.argv) <= 1 else ''
  92. env_opts = os.environ.get('IMPLIBSO_LD_OPTIONS', env_default)
  93. env_argv = env_opts.split(' ') if env_opts else []
  94. env_args = parser.parse_args(env_argv)
  95. global v # pylint: disable=global-statement
  96. v = env_args.verbose
  97. if env_args.outdir:
  98. tmpd = env_args.outdir
  99. else:
  100. tmpd = tempfile.mkdtemp('implib-gen-ld')
  101. atexit.register(lambda: shutil.rmtree(tmpd))
  102. wrap_libs = set()
  103. no_wrap_libs = set()
  104. for obj, param in [(wrap_libs, env_args.wrap_libs),
  105. (no_wrap_libs, env_args.no_wrap_libs)]:
  106. for lib in param.split(','):
  107. if lib:
  108. lib = re.sub(r'^(lib)?(.*)(\.so).*', r'\1', lib)
  109. obj.add(lib)
  110. args = []
  111. out_filename = 'a.out'
  112. for i in range(len(sys.argv)):
  113. arg = sys.argv[i]
  114. if arg == '-o':
  115. i += 1
  116. out_filename = sys.argv[i]
  117. elif arg.startswith('-o'):
  118. out_filename = arg[2:]
  119. all_lds = which(sys.argv[0], alll=True)
  120. if not all_lds:
  121. error("no ld executables in PATH")
  122. elif len(all_lds) == 1:
  123. error("real ld executable not found in PATH")
  124. real_ld = all_lds[1]
  125. this_ld = os.path.realpath(__file__)
  126. if os.path.realpath(real_ld) == os.path.realpath(this_ld):
  127. error("ld wrapper is not the first executable in PATH")
  128. sys.argv[0] = real_ld
  129. # Generate normal output first
  130. rc, out, err = run(sys.argv)
  131. if rc != 0:
  132. sys.stdout.write(out)
  133. sys.stderr.write(err)
  134. sys.exit(rc)
  135. # Analyze ldd output to see which runtime libs were linked
  136. rc, out, err = run(f'ldd {out_filename}')
  137. os.unlink(out_filename) # Remove output in case we fail later
  138. if rc != 0:
  139. error(f"ldd failed: {err}")
  140. sys.stderr.write(err)
  141. class WrapperInfo:
  142. "Holds info about replaceable library"
  143. def __init__(self, name, path):
  144. self.name = name
  145. self.path = path
  146. self.wrappers = []
  147. self.replaced = False
  148. libs = {}
  149. for l in out.split('\n'):
  150. l = l.strip()
  151. if not l:
  152. continue
  153. if re.search(r'linux-vdso|ld-linux', l):
  154. if v > 0:
  155. note(f"skipping system library: {l}")
  156. continue
  157. m = re.search(r'^lib(\S*)\.so(\.[0-9]+)? => (\S*)', l)
  158. if m is None:
  159. warn(f"failed to parse ldd output: {l}")
  160. continue
  161. name = m.group(1)
  162. path = m.group(3)
  163. if is_system_library(name):
  164. if v > 0:
  165. note(f"skipping system library: {l}")
  166. continue
  167. if wrap_libs and name not in wrap_libs:
  168. if v > 0:
  169. note(f"skipping library not in used-defined whitelist: {l}")
  170. continue
  171. if no_wrap_libs and name in no_wrap_libs:
  172. if v > 0:
  173. note(f"skipping library in used-defined blacklist: {l}")
  174. continue
  175. if v > 0:
  176. note(f"wrappable library: {name} ({path})")
  177. libs[name] = WrapperInfo(name, path)
  178. # Compile wrappers
  179. for _, info in sorted(libs.items()):
  180. rc, out, err = run(f'implib-gen.py --outdir {tmpd} {info.path}')
  181. if rc != 0:
  182. error(f"implib-gen failed: {err}")
  183. sys.stderr.write(err)
  184. for l in out.split('\n'):
  185. l = l.strip()
  186. if not l:
  187. continue
  188. m = re.match(r'^Generating (.*)\.\.\.', l)
  189. if m is not None:
  190. f = m.group(1)
  191. o, _ = os.path.splitext(f)
  192. o += '.o'
  193. if v > 0:
  194. note(f"compiling wrapper for {f} in {o}")
  195. rc, out, err = run(f'gcc -Wall -Wextra -O2 -c -o {o} {f}')
  196. if rc != 0:
  197. error(f"implib-gen failed: {err}")
  198. sys.stderr.write(err)
  199. info.wrappers.append(o)
  200. # Relink with wrapped code
  201. args = []
  202. changed = False
  203. for i in range(len(sys.argv)):
  204. arg = sys.argv[i]
  205. if arg == '-l':
  206. i += 1
  207. name = sys.argv[i]
  208. elif arg.startswith('-l'):
  209. name = arg[2:]
  210. else:
  211. args.append(arg)
  212. continue
  213. if is_system_library(name):
  214. args.append(arg)
  215. continue
  216. info = libs.get(name)
  217. if info is None:
  218. args.append(arg)
  219. continue
  220. for o in info.wrappers:
  221. changed = True
  222. args.append(o)
  223. info.replaced = True
  224. if changed:
  225. args.append('-ldl')
  226. for name, info in sorted(libs.items()):
  227. if not info.replaced:
  228. warn("failed to replace library %s")
  229. rc, out, err = run(args)
  230. sys.stdout.write(out)
  231. sys.stderr.write(err)
  232. sys.exit(rc)
  233. if __name__ == '__main__':
  234. main()