#!/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 [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 [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()