| #!/usr/bin/env python3 |
| # |
| # Build a systemtap script for a given image, kernel |
| # |
| # Effectively script extracts needed information from set of |
| # 'bitbake -e' commands and contructs proper invocation of stap on |
| # host to build systemtap script for a given target. |
| # |
| # By default script will compile scriptname.ko that could be copied |
| # to taget and activated with 'staprun scriptname.ko' command. Or if |
| # --remote user@hostname option is specified script will build, load |
| # execute script on target. |
| # |
| # This script is very similar and inspired by crosstap shell script. |
| # The major difference that this script supports user-land related |
| # systemtap script, whereas crosstap could deal only with scripts |
| # related to kernel. |
| # |
| # Copyright (c) 2018, Cisco Systems. |
| # |
| # SPDX-License-Identifier: GPL-2.0-only |
| # |
| |
| import sys |
| import re |
| import subprocess |
| import os |
| import optparse |
| |
| class Stap(object): |
| def __init__(self, script, module, remote): |
| self.script = script |
| self.module = module |
| self.remote = remote |
| self.stap = None |
| self.sysroot = None |
| self.runtime = None |
| self.tapset = None |
| self.arch = None |
| self.cross_compile = None |
| self.kernel_release = None |
| self.target_path = None |
| self.target_ld_library_path = None |
| |
| if not self.remote: |
| if not self.module: |
| # derive module name from script |
| self.module = os.path.basename(self.script) |
| if self.module[-4:] == ".stp": |
| self.module = self.module[:-4] |
| # replace - if any with _ |
| self.module = self.module.replace("-", "_") |
| |
| def command(self, args): |
| ret = [] |
| ret.append(self.stap) |
| |
| if self.remote: |
| ret.append("--remote") |
| ret.append(self.remote) |
| else: |
| ret.append("-p4") |
| ret.append("-m") |
| ret.append(self.module) |
| |
| ret.append("-a") |
| ret.append(self.arch) |
| |
| ret.append("-B") |
| ret.append("CROSS_COMPILE=" + self.cross_compile) |
| |
| ret.append("-r") |
| ret.append(self.kernel_release) |
| |
| ret.append("-I") |
| ret.append(self.tapset) |
| |
| ret.append("-R") |
| ret.append(self.runtime) |
| |
| if self.sysroot: |
| ret.append("--sysroot") |
| ret.append(self.sysroot) |
| |
| ret.append("--sysenv=PATH=" + self.target_path) |
| ret.append("--sysenv=LD_LIBRARY_PATH=" + self.target_ld_library_path) |
| |
| ret = ret + args |
| |
| ret.append(self.script) |
| return ret |
| |
| def additional_environment(self): |
| ret = {} |
| ret["SYSTEMTAP_DEBUGINFO_PATH"] = "+:.debug:build" |
| return ret |
| |
| def environment(self): |
| ret = os.environ.copy() |
| additional = self.additional_environment() |
| for e in additional: |
| ret[e] = additional[e] |
| return ret |
| |
| def display_command(self, args): |
| additional_env = self.additional_environment() |
| command = self.command(args) |
| |
| print("#!/bin/sh") |
| for e in additional_env: |
| print("export %s=\"%s\"" % (e, additional_env[e])) |
| print(" ".join(command)) |
| |
| class BitbakeEnvInvocationException(Exception): |
| def __init__(self, message): |
| self.message = message |
| |
| class BitbakeEnv(object): |
| BITBAKE="bitbake" |
| |
| def __init__(self, package): |
| self.package = package |
| self.cmd = BitbakeEnv.BITBAKE + " -e " + self.package |
| self.popen = subprocess.Popen(self.cmd, shell=True, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT) |
| self.__lines = self.popen.stdout.readlines() |
| self.popen.wait() |
| |
| self.lines = [] |
| for line in self.__lines: |
| self.lines.append(line.decode('utf-8')) |
| |
| def get_vars(self, vars): |
| if self.popen.returncode: |
| raise BitbakeEnvInvocationException( |
| "\nFailed to execute '" + self.cmd + |
| "' with the following message:\n" + |
| ''.join(self.lines)) |
| |
| search_patterns = [] |
| retdict = {} |
| for var in vars: |
| # regular not exported variable |
| rexpr = "^" + var + "=\"(.*)\"" |
| re_compiled = re.compile(rexpr) |
| search_patterns.append((var, re_compiled)) |
| |
| # exported variable |
| rexpr = "^export " + var + "=\"(.*)\"" |
| re_compiled = re.compile(rexpr) |
| search_patterns.append((var, re_compiled)) |
| |
| for line in self.lines: |
| for var, rexpr in search_patterns: |
| m = rexpr.match(line) |
| if m: |
| value = m.group(1) |
| retdict[var] = value |
| |
| # fill variables values in order how they were requested |
| ret = [] |
| for var in vars: |
| ret.append(retdict.get(var)) |
| |
| # if it is single value list return it as scalar, not the list |
| if len(ret) == 1: |
| ret = ret[0] |
| |
| return ret |
| |
| class ParamDiscovery(object): |
| SYMBOLS_CHECK_MESSAGE = """ |
| WARNING: image '%s' does not have dbg-pkgs IMAGE_FEATURES enabled and no |
| "image-combined-dbg" in inherited classes is specified. As result the image |
| does not have symbols for user-land processes DWARF based probes. Consider |
| adding 'dbg-pkgs' to EXTRA_IMAGE_FEATURES or adding "image-combined-dbg" to |
| USER_CLASSES. I.e add this line 'USER_CLASSES += "image-combined-dbg"' to |
| local.conf file. |
| |
| Or you may use IMAGE_GEN_DEBUGFS="1" option, and then after build you need |
| recombine/unpack image and image-dbg tarballs and pass resulting dir location |
| with --sysroot option. |
| """ |
| |
| def __init__(self, image): |
| self.image = image |
| |
| self.image_rootfs = None |
| self.image_features = None |
| self.image_gen_debugfs = None |
| self.inherit = None |
| self.base_bindir = None |
| self.base_sbindir = None |
| self.base_libdir = None |
| self.bindir = None |
| self.sbindir = None |
| self.libdir = None |
| |
| self.staging_bindir_toolchain = None |
| self.target_prefix = None |
| self.target_arch = None |
| self.target_kernel_builddir = None |
| |
| self.staging_dir_native = None |
| |
| self.image_combined_dbg = False |
| |
| def discover(self): |
| if self.image: |
| benv_image = BitbakeEnv(self.image) |
| (self.image_rootfs, |
| self.image_features, |
| self.image_gen_debugfs, |
| self.inherit, |
| self.base_bindir, |
| self.base_sbindir, |
| self.base_libdir, |
| self.bindir, |
| self.sbindir, |
| self.libdir |
| ) = benv_image.get_vars( |
| ("IMAGE_ROOTFS", |
| "IMAGE_FEATURES", |
| "IMAGE_GEN_DEBUGFS", |
| "INHERIT", |
| "base_bindir", |
| "base_sbindir", |
| "base_libdir", |
| "bindir", |
| "sbindir", |
| "libdir" |
| )) |
| |
| benv_kernel = BitbakeEnv("virtual/kernel") |
| (self.staging_bindir_toolchain, |
| self.target_prefix, |
| self.target_arch, |
| self.target_kernel_builddir |
| ) = benv_kernel.get_vars( |
| ("STAGING_BINDIR_TOOLCHAIN", |
| "TARGET_PREFIX", |
| "TRANSLATED_TARGET_ARCH", |
| "B" |
| )) |
| |
| benv_systemtap = BitbakeEnv("systemtap-native") |
| (self.staging_dir_native |
| ) = benv_systemtap.get_vars(["STAGING_DIR_NATIVE"]) |
| |
| if self.inherit: |
| if "image-combined-dbg" in self.inherit.split(): |
| self.image_combined_dbg = True |
| |
| def check(self, sysroot_option): |
| ret = True |
| if self.image_rootfs: |
| sysroot = self.image_rootfs |
| if not os.path.isdir(self.image_rootfs): |
| print("ERROR: Cannot find '" + sysroot + |
| "' directory. Was '" + self.image + "' image built?") |
| ret = False |
| |
| stap = self.staging_dir_native + "/usr/bin/stap" |
| if not os.path.isfile(stap): |
| print("ERROR: Cannot find '" + stap + |
| "'. Was 'systemtap-native' built?") |
| ret = False |
| |
| if not os.path.isdir(self.target_kernel_builddir): |
| print("ERROR: Cannot find '" + self.target_kernel_builddir + |
| "' directory. Was 'kernel/virtual' built?") |
| ret = False |
| |
| if not sysroot_option and self.image_rootfs: |
| dbg_pkgs_found = False |
| |
| if self.image_features: |
| image_features = self.image_features.split() |
| if "dbg-pkgs" in image_features: |
| dbg_pkgs_found = True |
| |
| if not dbg_pkgs_found \ |
| and not self.image_combined_dbg: |
| print(ParamDiscovery.SYMBOLS_CHECK_MESSAGE % (self.image)) |
| |
| if not ret: |
| print("") |
| |
| return ret |
| |
| def __map_systemtap_arch(self): |
| a = self.target_arch |
| ret = a |
| if re.match('(athlon|x86.64)$', a): |
| ret = 'x86_64' |
| elif re.match('i.86$', a): |
| ret = 'i386' |
| elif re.match('arm$', a): |
| ret = 'arm' |
| elif re.match('aarch64$', a): |
| ret = 'arm64' |
| elif re.match('mips(isa|)(32|64|)(r6|)(el|)$', a): |
| ret = 'mips' |
| elif re.match('p(pc|owerpc)(|64)', a): |
| ret = 'powerpc' |
| return ret |
| |
| def fill_stap(self, stap): |
| stap.stap = self.staging_dir_native + "/usr/bin/stap" |
| if not stap.sysroot: |
| if self.image_rootfs: |
| if self.image_combined_dbg: |
| stap.sysroot = self.image_rootfs + "-dbg" |
| else: |
| stap.sysroot = self.image_rootfs |
| stap.runtime = self.staging_dir_native + "/usr/share/systemtap/runtime" |
| stap.tapset = self.staging_dir_native + "/usr/share/systemtap/tapset" |
| stap.arch = self.__map_systemtap_arch() |
| stap.cross_compile = self.staging_bindir_toolchain + "/" + \ |
| self.target_prefix |
| stap.kernel_release = self.target_kernel_builddir |
| |
| # do we have standard that tells in which order these need to appear |
| target_path = [] |
| if self.sbindir: |
| target_path.append(self.sbindir) |
| if self.bindir: |
| target_path.append(self.bindir) |
| if self.base_sbindir: |
| target_path.append(self.base_sbindir) |
| if self.base_bindir: |
| target_path.append(self.base_bindir) |
| stap.target_path = ":".join(target_path) |
| |
| target_ld_library_path = [] |
| if self.libdir: |
| target_ld_library_path.append(self.libdir) |
| if self.base_libdir: |
| target_ld_library_path.append(self.base_libdir) |
| stap.target_ld_library_path = ":".join(target_ld_library_path) |
| |
| |
| def main(): |
| usage = """usage: %prog -s <systemtap-script> [options] [-- [systemtap options]] |
| |
| %prog cross compile given SystemTap script against given image, kernel |
| |
| It needs to run in environtment set for bitbake - it uses bitbake -e |
| invocations to retrieve information to construct proper stap cross build |
| invocation arguments. It assumes that systemtap-native is built in given |
| bitbake workspace. |
| |
| Anything after -- option is passed directly to stap. |
| |
| Legacy script invocation style supported but depreciated: |
| %prog <user@hostname> <sytemtap-script> [systemtap options] |
| |
| To enable most out of systemtap the following site.conf or local.conf |
| configuration is recommended: |
| |
| # enables symbol + target binaries rootfs-dbg in workspace |
| IMAGE_GEN_DEBUGFS = "1" |
| IMAGE_FSTYPES_DEBUGFS = "tar.bz2" |
| USER_CLASSES += "image-combined-dbg" |
| |
| # enables kernel debug symbols |
| KERNEL_EXTRA_FEATURES_append = " features/debug/debug-kernel.scc" |
| |
| # minimal, just run-time systemtap configuration in target image |
| PACKAGECONFIG_pn-systemtap = "monitor" |
| |
| # add systemtap run-time into target image if it is not there yet |
| IMAGE_INSTALL_append = " systemtap" |
| """ |
| option_parser = optparse.OptionParser(usage=usage) |
| |
| option_parser.add_option("-s", "--script", dest="script", |
| help="specify input script FILE name", |
| metavar="FILE") |
| |
| option_parser.add_option("-i", "--image", dest="image", |
| help="specify image name for which script should be compiled") |
| |
| option_parser.add_option("-r", "--remote", dest="remote", |
| help="specify username@hostname of remote target to run script " |
| "optional, it assumes that remote target can be accessed through ssh") |
| |
| option_parser.add_option("-m", "--module", dest="module", |
| help="specify module name, optional, has effect only if --remote is not used, " |
| "if not specified module name will be derived from passed script name") |
| |
| option_parser.add_option("-y", "--sysroot", dest="sysroot", |
| help="explicitely specify image sysroot location. May need to use it in case " |
| "when IMAGE_GEN_DEBUGFS=\"1\" option is used and recombined with symbols " |
| "in different location", |
| metavar="DIR") |
| |
| option_parser.add_option("-o", "--out", dest="out", |
| action="store_true", |
| help="output shell script that equvivalent invocation of this script with " |
| "given set of arguments, in given bitbake environment. It could be stored in " |
| "separate shell script and could be repeated without incuring bitbake -e " |
| "invocation overhead", |
| default=False) |
| |
| option_parser.add_option("-d", "--debug", dest="debug", |
| action="store_true", |
| help="enable debug output. Use this option to see resulting stap invocation", |
| default=False) |
| |
| # is invocation follow syntax from orignal crosstap shell script |
| legacy_args = False |
| |
| # check if we called the legacy way |
| if len(sys.argv) >= 3: |
| if sys.argv[1].find("@") != -1 and os.path.exists(sys.argv[2]): |
| legacy_args = True |
| |
| # fill options values for legacy invocation case |
| options = optparse.Values |
| options.script = sys.argv[2] |
| options.remote = sys.argv[1] |
| options.image = None |
| options.module = None |
| options.sysroot = None |
| options.out = None |
| options.debug = None |
| remaining_args = sys.argv[3:] |
| |
| if not legacy_args: |
| (options, remaining_args) = option_parser.parse_args() |
| |
| if not options.script or not os.path.exists(options.script): |
| print("'-s FILE' option is missing\n") |
| option_parser.print_help() |
| else: |
| stap = Stap(options.script, options.module, options.remote) |
| discovery = ParamDiscovery(options.image) |
| discovery.discover() |
| if not discovery.check(options.sysroot): |
| option_parser.print_help() |
| else: |
| stap.sysroot = options.sysroot |
| discovery.fill_stap(stap) |
| |
| if options.out: |
| stap.display_command(remaining_args) |
| else: |
| cmd = stap.command(remaining_args) |
| env = stap.environment() |
| |
| if options.debug: |
| print(" ".join(cmd)) |
| |
| os.execve(cmd[0], cmd, env) |
| |
| main() |