1#!/usr/bin/env python3 2# 3# Build a systemtap script for a given image, kernel 4# 5# Effectively script extracts needed information from set of 6# 'bitbake -e' commands and contructs proper invocation of stap on 7# host to build systemtap script for a given target. 8# 9# By default script will compile scriptname.ko that could be copied 10# to taget and activated with 'staprun scriptname.ko' command. Or if 11# --remote user@hostname option is specified script will build, load 12# execute script on target. 13# 14# This script is very similar and inspired by crosstap shell script. 15# The major difference that this script supports user-land related 16# systemtap script, whereas crosstap could deal only with scripts 17# related to kernel. 18# 19# Copyright (c) 2018, Cisco Systems. 20# 21# SPDX-License-Identifier: GPL-2.0-only 22# 23 24import sys 25import re 26import subprocess 27import os 28import optparse 29 30class Stap(object): 31 def __init__(self, script, module, remote): 32 self.script = script 33 self.module = module 34 self.remote = remote 35 self.stap = None 36 self.sysroot = None 37 self.runtime = None 38 self.tapset = None 39 self.arch = None 40 self.cross_compile = None 41 self.kernel_release = None 42 self.target_path = None 43 self.target_ld_library_path = None 44 45 if not self.remote: 46 if not self.module: 47 # derive module name from script 48 self.module = os.path.basename(self.script) 49 if self.module[-4:] == ".stp": 50 self.module = self.module[:-4] 51 # replace - if any with _ 52 self.module = self.module.replace("-", "_") 53 54 def command(self, args): 55 ret = [] 56 ret.append(self.stap) 57 58 if self.remote: 59 ret.append("--remote") 60 ret.append(self.remote) 61 else: 62 ret.append("-p4") 63 ret.append("-m") 64 ret.append(self.module) 65 66 ret.append("-a") 67 ret.append(self.arch) 68 69 ret.append("-B") 70 ret.append("CROSS_COMPILE=" + self.cross_compile) 71 72 ret.append("-r") 73 ret.append(self.kernel_release) 74 75 ret.append("-I") 76 ret.append(self.tapset) 77 78 ret.append("-R") 79 ret.append(self.runtime) 80 81 if self.sysroot: 82 ret.append("--sysroot") 83 ret.append(self.sysroot) 84 85 ret.append("--sysenv=PATH=" + self.target_path) 86 ret.append("--sysenv=LD_LIBRARY_PATH=" + self.target_ld_library_path) 87 88 ret = ret + args 89 90 ret.append(self.script) 91 return ret 92 93 def additional_environment(self): 94 ret = {} 95 ret["SYSTEMTAP_DEBUGINFO_PATH"] = "+:.debug:build" 96 return ret 97 98 def environment(self): 99 ret = os.environ.copy() 100 additional = self.additional_environment() 101 for e in additional: 102 ret[e] = additional[e] 103 return ret 104 105 def display_command(self, args): 106 additional_env = self.additional_environment() 107 command = self.command(args) 108 109 print("#!/bin/sh") 110 for e in additional_env: 111 print("export %s=\"%s\"" % (e, additional_env[e])) 112 print(" ".join(command)) 113 114class BitbakeEnvInvocationException(Exception): 115 def __init__(self, message): 116 self.message = message 117 118class BitbakeEnv(object): 119 BITBAKE="bitbake" 120 121 def __init__(self, package): 122 self.package = package 123 self.cmd = BitbakeEnv.BITBAKE + " -e " + self.package 124 self.popen = subprocess.Popen(self.cmd, shell=True, 125 stdout=subprocess.PIPE, 126 stderr=subprocess.STDOUT) 127 self.__lines = self.popen.stdout.readlines() 128 self.popen.wait() 129 130 self.lines = [] 131 for line in self.__lines: 132 self.lines.append(line.decode('utf-8')) 133 134 def get_vars(self, vars): 135 if self.popen.returncode: 136 raise BitbakeEnvInvocationException( 137 "\nFailed to execute '" + self.cmd + 138 "' with the following message:\n" + 139 ''.join(self.lines)) 140 141 search_patterns = [] 142 retdict = {} 143 for var in vars: 144 # regular not exported variable 145 rexpr = "^" + var + "=\"(.*)\"" 146 re_compiled = re.compile(rexpr) 147 search_patterns.append((var, re_compiled)) 148 149 # exported variable 150 rexpr = "^export " + var + "=\"(.*)\"" 151 re_compiled = re.compile(rexpr) 152 search_patterns.append((var, re_compiled)) 153 154 for line in self.lines: 155 for var, rexpr in search_patterns: 156 m = rexpr.match(line) 157 if m: 158 value = m.group(1) 159 retdict[var] = value 160 161 # fill variables values in order how they were requested 162 ret = [] 163 for var in vars: 164 ret.append(retdict.get(var)) 165 166 # if it is single value list return it as scalar, not the list 167 if len(ret) == 1: 168 ret = ret[0] 169 170 return ret 171 172class ParamDiscovery(object): 173 SYMBOLS_CHECK_MESSAGE = """ 174WARNING: image '%s' does not have dbg-pkgs IMAGE_FEATURES enabled and no 175"image-combined-dbg" in inherited classes is specified. As result the image 176does not have symbols for user-land processes DWARF based probes. Consider 177adding 'dbg-pkgs' to EXTRA_IMAGE_FEATURES or adding "image-combined-dbg" to 178USER_CLASSES. I.e add this line 'USER_CLASSES += "image-combined-dbg"' to 179local.conf file. 180 181Or you may use IMAGE_GEN_DEBUGFS="1" option, and then after build you need 182recombine/unpack image and image-dbg tarballs and pass resulting dir location 183with --sysroot option. 184""" 185 186 def __init__(self, image): 187 self.image = image 188 189 self.image_rootfs = None 190 self.image_features = None 191 self.image_gen_debugfs = None 192 self.inherit = None 193 self.base_bindir = None 194 self.base_sbindir = None 195 self.base_libdir = None 196 self.bindir = None 197 self.sbindir = None 198 self.libdir = None 199 200 self.staging_bindir_toolchain = None 201 self.target_prefix = None 202 self.target_arch = None 203 self.target_kernel_builddir = None 204 205 self.staging_dir_native = None 206 207 self.image_combined_dbg = False 208 209 def discover(self): 210 if self.image: 211 benv_image = BitbakeEnv(self.image) 212 (self.image_rootfs, 213 self.image_features, 214 self.image_gen_debugfs, 215 self.inherit, 216 self.base_bindir, 217 self.base_sbindir, 218 self.base_libdir, 219 self.bindir, 220 self.sbindir, 221 self.libdir 222 ) = benv_image.get_vars( 223 ("IMAGE_ROOTFS", 224 "IMAGE_FEATURES", 225 "IMAGE_GEN_DEBUGFS", 226 "INHERIT", 227 "base_bindir", 228 "base_sbindir", 229 "base_libdir", 230 "bindir", 231 "sbindir", 232 "libdir" 233 )) 234 235 benv_kernel = BitbakeEnv("virtual/kernel") 236 (self.staging_bindir_toolchain, 237 self.target_prefix, 238 self.target_arch, 239 self.target_kernel_builddir 240 ) = benv_kernel.get_vars( 241 ("STAGING_BINDIR_TOOLCHAIN", 242 "TARGET_PREFIX", 243 "TRANSLATED_TARGET_ARCH", 244 "B" 245 )) 246 247 benv_systemtap = BitbakeEnv("systemtap-native") 248 (self.staging_dir_native 249 ) = benv_systemtap.get_vars(["STAGING_DIR_NATIVE"]) 250 251 if self.inherit: 252 if "image-combined-dbg" in self.inherit.split(): 253 self.image_combined_dbg = True 254 255 def check(self, sysroot_option): 256 ret = True 257 if self.image_rootfs: 258 sysroot = self.image_rootfs 259 if not os.path.isdir(self.image_rootfs): 260 print("ERROR: Cannot find '" + sysroot + 261 "' directory. Was '" + self.image + "' image built?") 262 ret = False 263 264 stap = self.staging_dir_native + "/usr/bin/stap" 265 if not os.path.isfile(stap): 266 print("ERROR: Cannot find '" + stap + 267 "'. Was 'systemtap-native' built?") 268 ret = False 269 270 if not os.path.isdir(self.target_kernel_builddir): 271 print("ERROR: Cannot find '" + self.target_kernel_builddir + 272 "' directory. Was 'kernel/virtual' built?") 273 ret = False 274 275 if not sysroot_option and self.image_rootfs: 276 dbg_pkgs_found = False 277 278 if self.image_features: 279 image_features = self.image_features.split() 280 if "dbg-pkgs" in image_features: 281 dbg_pkgs_found = True 282 283 if not dbg_pkgs_found \ 284 and not self.image_combined_dbg: 285 print(ParamDiscovery.SYMBOLS_CHECK_MESSAGE % (self.image)) 286 287 if not ret: 288 print("") 289 290 return ret 291 292 def __map_systemtap_arch(self): 293 a = self.target_arch 294 ret = a 295 if re.match('(athlon|x86.64)$', a): 296 ret = 'x86_64' 297 elif re.match('i.86$', a): 298 ret = 'i386' 299 elif re.match('arm$', a): 300 ret = 'arm' 301 elif re.match('aarch64$', a): 302 ret = 'arm64' 303 elif re.match('mips(isa|)(32|64|)(r6|)(el|)$', a): 304 ret = 'mips' 305 elif re.match('p(pc|owerpc)(|64)', a): 306 ret = 'powerpc' 307 return ret 308 309 def fill_stap(self, stap): 310 stap.stap = self.staging_dir_native + "/usr/bin/stap" 311 if not stap.sysroot: 312 if self.image_rootfs: 313 if self.image_combined_dbg: 314 stap.sysroot = self.image_rootfs + "-dbg" 315 else: 316 stap.sysroot = self.image_rootfs 317 stap.runtime = self.staging_dir_native + "/usr/share/systemtap/runtime" 318 stap.tapset = self.staging_dir_native + "/usr/share/systemtap/tapset" 319 stap.arch = self.__map_systemtap_arch() 320 stap.cross_compile = self.staging_bindir_toolchain + "/" + \ 321 self.target_prefix 322 stap.kernel_release = self.target_kernel_builddir 323 324 # do we have standard that tells in which order these need to appear 325 target_path = [] 326 if self.sbindir: 327 target_path.append(self.sbindir) 328 if self.bindir: 329 target_path.append(self.bindir) 330 if self.base_sbindir: 331 target_path.append(self.base_sbindir) 332 if self.base_bindir: 333 target_path.append(self.base_bindir) 334 stap.target_path = ":".join(target_path) 335 336 target_ld_library_path = [] 337 if self.libdir: 338 target_ld_library_path.append(self.libdir) 339 if self.base_libdir: 340 target_ld_library_path.append(self.base_libdir) 341 stap.target_ld_library_path = ":".join(target_ld_library_path) 342 343 344def main(): 345 usage = """usage: %prog -s <systemtap-script> [options] [-- [systemtap options]] 346 347%prog cross compile given SystemTap script against given image, kernel 348 349It needs to run in environtment set for bitbake - it uses bitbake -e 350invocations to retrieve information to construct proper stap cross build 351invocation arguments. It assumes that systemtap-native is built in given 352bitbake workspace. 353 354Anything after -- option is passed directly to stap. 355 356Legacy script invocation style supported but depreciated: 357 %prog <user@hostname> <sytemtap-script> [systemtap options] 358 359To enable most out of systemtap the following site.conf or local.conf 360configuration is recommended: 361 362# enables symbol + target binaries rootfs-dbg in workspace 363IMAGE_GEN_DEBUGFS = "1" 364IMAGE_FSTYPES_DEBUGFS = "tar.bz2" 365USER_CLASSES += "image-combined-dbg" 366 367# enables kernel debug symbols 368KERNEL_EXTRA_FEATURES:append = " features/debug/debug-kernel.scc" 369 370# minimal, just run-time systemtap configuration in target image 371PACKAGECONFIG:pn-systemtap = "monitor" 372 373# add systemtap run-time into target image if it is not there yet 374IMAGE_INSTALL:append = " systemtap" 375""" 376 option_parser = optparse.OptionParser(usage=usage) 377 378 option_parser.add_option("-s", "--script", dest="script", 379 help="specify input script FILE name", 380 metavar="FILE") 381 382 option_parser.add_option("-i", "--image", dest="image", 383 help="specify image name for which script should be compiled") 384 385 option_parser.add_option("-r", "--remote", dest="remote", 386 help="specify username@hostname of remote target to run script " 387 "optional, it assumes that remote target can be accessed through ssh") 388 389 option_parser.add_option("-m", "--module", dest="module", 390 help="specify module name, optional, has effect only if --remote is not used, " 391 "if not specified module name will be derived from passed script name") 392 393 option_parser.add_option("-y", "--sysroot", dest="sysroot", 394 help="explicitely specify image sysroot location. May need to use it in case " 395 "when IMAGE_GEN_DEBUGFS=\"1\" option is used and recombined with symbols " 396 "in different location", 397 metavar="DIR") 398 399 option_parser.add_option("-o", "--out", dest="out", 400 action="store_true", 401 help="output shell script that equvivalent invocation of this script with " 402 "given set of arguments, in given bitbake environment. It could be stored in " 403 "separate shell script and could be repeated without incuring bitbake -e " 404 "invocation overhead", 405 default=False) 406 407 option_parser.add_option("-d", "--debug", dest="debug", 408 action="store_true", 409 help="enable debug output. Use this option to see resulting stap invocation", 410 default=False) 411 412 # is invocation follow syntax from orignal crosstap shell script 413 legacy_args = False 414 415 # check if we called the legacy way 416 if len(sys.argv) >= 3: 417 if sys.argv[1].find("@") != -1 and os.path.exists(sys.argv[2]): 418 legacy_args = True 419 420 # fill options values for legacy invocation case 421 options = optparse.Values 422 options.script = sys.argv[2] 423 options.remote = sys.argv[1] 424 options.image = None 425 options.module = None 426 options.sysroot = None 427 options.out = None 428 options.debug = None 429 remaining_args = sys.argv[3:] 430 431 if not legacy_args: 432 (options, remaining_args) = option_parser.parse_args() 433 434 if not options.script or not os.path.exists(options.script): 435 print("'-s FILE' option is missing\n") 436 option_parser.print_help() 437 else: 438 stap = Stap(options.script, options.module, options.remote) 439 discovery = ParamDiscovery(options.image) 440 discovery.discover() 441 if not discovery.check(options.sysroot): 442 option_parser.print_help() 443 else: 444 stap.sysroot = options.sysroot 445 discovery.fill_stap(stap) 446 447 if options.out: 448 stap.display_command(remaining_args) 449 else: 450 cmd = stap.command(remaining_args) 451 env = stap.environment() 452 453 if options.debug: 454 print(" ".join(cmd)) 455 456 os.execve(cmd[0], cmd, env) 457 458main() 459