#!/bin/sh # A Poor (but Free) Man's dtrace # # Copyright (C) 2014-2023 Free Software Foundation, Inc. # # Contributed by Oracle, Inc. # # This file is part of GDB. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see # . # DISCLAIMER DISCLAIMER DISCLAIMER # This script is a test tool. As such it is in no way intended to # replace the "real" dtrace command for any practical purpose, apart # from testing the DTrace USDT probes support in GDB. # that said... # # pdtrace is a limited dtrace program, implementing a subset of its # functionality: # # - The generation of an ELF file containing an embedded dtrace # program. Equivalent to dtrace -G. # # - The generation of a header file with definitions for static # probes. Equivalent to dtrace -h. # # This allows to generate DTrace static probes without having to use # the user-level DTrace components. The generated objects are 100% # compatible with DTrace and can be traced by the dtrace kernel module # like if they were generated by dtrace. # # Some of the known limitations of this implementation are: # - The input d-script must describe one provider, and only one. # - The "probe " directives in the d-file must not include argument # names, just the types. Thus something like `char *' is valid, but # `char *name' is not. # - The command line options must precede other arguments, since the # script uses the (more) portable getopts. # - Each probe header in the d-script must be contained in # a single line. # - strip -K removes the debugging information from the input object # file. # - The supported target platforms are i[3456]86 and x86_64. # # Please keep this code as portable as possible. Restrict yourself to # POSIX sh. # This script uses the following external programs, defined in # variables. Some of them are substituted by autoconf. TR=tr NM=@NM_TRANSFORM_NAME@ EGREP=egrep SED=sed CUT=cut READELF=@READELF_TRANSFORM_NAME@ SORT=sort EXPR=expr WC=wc UNIQ=uniq HEAD=head SEQ=seq AS=@GAS_TRANSFORM_NAME@ STRIP=@STRIP_TRANSFORM_NAME@ TRUE=true # Sizes for several DOF structures, in bytes. # # See linux/dtrace/dof.h for the definition of the referred # structures. dof_hdrsize=64 # sizeof(dtrace_dof_hdr) dof_secsize=32 # sizeof(dtrace_dof_sect) dof_probesize=48 # sizeof(dtrace_dof_probe) dof_providersize=44 # sizeof(dtrace_dof_provider) # Types for the several DOF sections. # # See linux/dtrace/dof_defines.h for a complete list of section types # along with their values. dof_sect_type_strtab=8 dof_sect_type_provider=15 dof_sect_type_probes=16 dof_sect_type_prargs=17 dof_sect_type_proffs=18 dof_sect_type_prenoffs=26 ### Functions # Write a message to the standard error output and exit with an error # status. # # Arguments: # $1 error message. f_panic() { echo "error: $1" 1>&2; exit 1 } # Write a usage message to the standard output and exit with an error # status. f_usage() { printf "Usage: pdtrace [-32|-64] [-GhV] [-o output] [-s script] [ args ... ]\n\n" printf "\t-32 generate 32-bit ELF files\n" printf "\t-64 generate 64-bit ELF files\n\n" printf "\t-G generate an ELF file containing embedded dtrace program\n" printf "\t-h generate a header file with definitions for static probes\n" printf "\t-o set output file\n" printf "\t-s handle probes according to the specified D script\n" printf "\t-V report the DTrace API version implemented by the tool\n" exit 2 } # Write a version message to the standard output and exit with a # successful status. f_version() { echo "pdtrace: Sun D 1.6.3" exit } # Add a new record to a list and return it. # # Arguments: # $1 is the list. # $2 is the new record f_add_record() { rec=$1 test -n "$rec" && \ { rec=$(printf %s\\n "$rec"; echo x); rec=${rec%x}; } printf %s "$rec$2" } # Collect the providers and probes information from the input object # file. # # This function sets the values of the following global variables. # The values are structured in records, each record in a line. The # fields of each record are separated in some cases by white # characters and in other cases by colon (:) characters. # # The type codes in the line format descriptors are: # S: string, D: decimal number # # probes # Regular probes and is-enabled probes. # TYPE(S) PROVIDER(S) NAME(S) OFFSET(D) BASE(D) BASE_SYM(S) # base_probes # Base probes, i.e. probes sharing provider, name and container. # PROVIDER(S) NAME(S) BASE(D) BASE_SYM(S) # providers # List of providers. # PROVIDER(S) # All the offsets are expressed in bytes. # # Input globals: # objfile # Output globals: # probes, base_probes, providers probes= base_probes= providers= probes_args= f_collect_probes() { # Probe points are function calls to undefined functions featuring # distinct names for both normal probes and is-enabled probes. PROBE_REGEX="(__dtrace_([a-zA-Z_]+)___([a-zA-Z_]+))" EPROBE_REGEX="(__dtraceenabled_([a-zA-Z_]+)___([a-zA-Z_]+))" while read type symbol provider name; do test -z "$type" && f_panic "No probe points found in $objfile" provider=$(printf %s $provider | $TR -s _) name=$(printf %s $name | $TR -s _) # Search the object file for relocations defined for the # probe symbols. Then calculate the base address of the # probe (along with the symbol associated with that base # address) and the offset of the probe point. for offset in $($READELF -W -r $objfile | $EGREP $symbol | $CUT -d' ' -f1) do # Figure out the base address for the probe. This is # done finding the function name in the text section of # the object file located above the probed point. But # note that the relocation is for the address operand of # the call instruction, so we have to subtract 1 to find # the real probed point. offset=$((0x$offset - 1)) # The addresses of is-enabled probes must point to the # first NOP instruction in their patched instructions # sequences, so modify them (see f_patch_objfile for the # instruction sequences). if test "$type" = "e"; then if test "$objbits" -eq "32"; then offset=$((offset + 2)) else # 64 bits offset=$((offset + 3)) fi fi # Determine the base address of the probe and its # corresponding function name. funcs=$($NM -td $objfile | $EGREP "^[0-9]+ T " \ | $CUT -d' ' -f1,3 | $SORT -n -r | $TR ' ' :) for fun in $funcs; do func_off=$(printf %s $fun | $CUT -d: -f1) func_sym=$(printf %s $fun | $CUT -d: -f2) # Note that `expr' is used to remove leading zeros # to avoid FUNC_OFF to be interpreted as an octal # number in arithmetic contexts. test "$func_off" -le "$offset" && \ { base=$($EXPR $func_off + 0); break; } done test -n "$base" || \ f_panic "could not find base address for probe at $objfile($o)" # Emit the record for the probe. probes=$(f_add_record "$probes" \ "$type $provider $name $(($offset - $base)) $base $func_sym") done done < loadable section. f_gen_asm ".4byte 1\t/* uint32_t dofs_flags */" f_gen_asm ".4byte $4\t/* uint32_t dofs_entsize */" f_gen_asm ".8byte $5\t/* uint64_t dofs_offset */" f_gen_asm ".8byte $6\t/* uint64_t dofs_size */" } # Generate a DOF program and assembly it in the output file. # # The DOF program generated by this function has the following # structure: # # HEADER # STRTAB OFFTAB EOFFTAB [PROBES PROVIDER]... # STRTAB_SECT OFFTAB_SECT EOFFTAB_SECT ARGTAB_SECT [PROBES_SECT PROVIDER_SECT]... # # Input globals: # probes, base_probes, providers, probes_args, BCOUNT f_gen_dof_program() { ###### Variables used to cache information needed later. # Number of section headers in the generated DOF program. dof_secnum=0 # Offset of section headers in the generated DOF program, in bytes. dof_secoff=0 # Sizes of the STRTAB, OFFTAB and EOFFTAB sections, in bytes. strtab_size=0 offtab_size=0 eofftab_size=0 # Offsets of the STRTAB, OFFTAB EOFFTAB and PROBES sections in the # generated DOF program. In bytes. strtab_offset=0 offtab_offset=0 eofftab_offset=0 argtab_offset=0 probes_offset=0 # Indexes of the section headers of the STRTAB, OFFTAB, EOFFTAB and # PROBES sections in the sections array. strtab_sect_index=0 offtab_sect_index=0 eofftab_sect_index=0 argtab_sect_index=0 probes_sect_index=0 # First offsets and eoffsets of the base-probes. # Lines: PROVIDER(S) NAME(S) BASE(D) (DOF_OFFSET(D)|DOF_EOFFSET(D)) probes_dof_offsets= probes_dof_eoffsets= # Offsets in the STRTAB section for the first type of base probes. # Record per line: PROVIDER(S) NAME(S) BASE(D) OFFSET(D) probes_dof_types= # Offsets of the provider names in the provider's STRTAB section. # Lines: PROVIDER(S) OFFSET(D) providers_dof_names= # Offsets of the base-probe names in the provider's STRTAB section. # Lines: PROVIDER(S) NAME(S) BASE(D) OFFSET(D) probes_dof_names= # Offsets of the provider sections in the DOF program. # Lines: PROVIDER(S) OFFSET(D) providers_offsets= ###### Generation phase. # The header of the DOF program contains a `struct # dtrace_dof_hdr'. Record its size, but it is written at the end # of the function. f_incr_bcount $dof_hdrsize; f_align_bcount 8 # The STRTAB section immediately follows the header. It contains # the following set of packed null-terminated strings: # # [PROVIDER [BASE_PROBE_NAME [BASE_PROBE_ARG_TYPE...]]...]... strtab_offset=$BCOUNT strtab_sect_index=$dof_secnum dof_secnum=$((dof_secnum + 1)) f_gen_asm "" f_gen_asm "/* The STRTAB section. */" f_gen_asm ".balign 8" # Add the provider names. off=0 while read provider; do strtab_size=$(($strtab_size + ${#prov} + 1)) # Note the funny mangling... f_gen_asm ".asciz \"$(printf %s $provider | $TR _ -)\"" providers_dof_names=$(f_add_record "$providers_dof_names" \ "$provider $off") off=$(($off + ${#provider} + 1)) # Add the base-probe names. while read p_provider name base base_sym; do test "$p_provider" = "$provider" || continue # And yes, more funny mangling... f_gen_asm ".asciz \"$(printf %s $name | $TR _ -)\"" probes_dof_names=$(f_add_record "$probes_dof_names" \ "$p_provider $name $base $off") off=$(($off + ${#name} + 1)) while read args; do a_provider=$(printf %s "$args" | $CUT -d: -f1) a_name=$(printf %s "$args" | $CUT -d: -f2) test "$a_provider" = "$p_provider" \ && test "$a_name" = "$name" \ || continue probes_dof_types=$(f_add_record "$probes_dof_types" \ "$a_provider $name $base $off") nargs=$(printf %s "$args" | $CUT -d: -f3) for n in $($SEQ $nargs); do arg=$(printf %s "$args" | $CUT -d: -f$(($n + 3))) f_gen_asm ".asciz \"${arg}\"" off=$(($off + ${#arg} + 1)) done done < /dev/null break fi done < /dev/null) test "$byte" = "$x86_op_jmp32" && nopret="$x86_op_ret" # Determine the patching sequence. It depends on the type of # probe at hand (regular or is-enabled) and also if # manipulating a 32bit or 64bit binary. patchseq= case $type in p) patchseq=$(printf %s%s%s%s%s \ "$nopret" \ "$x86_op_nop" \ "$x86_op_nop" \ "$x86_op_nop" \ "$x86_op_nop") ;; e) test "$objbits" -eq 64 && \ patchseq=$(printf %s%s%s%s%s \ "$x86_op_rex_rax" \ "$x86_op_xor_eax_0" \ "$x86_op_xor_eax_1" \ "$nopret" \ "$x86_op_nop") test "$objbits" -eq 32 && \ patchseq=$(printf %s%s%s%s%s \ "$x86_op_xor_eax_0" \ "$x86_op_xor_eax_1" \ "$nopret" \ "$x86_op_nop" \ "$x86_op_nop") ;; *) f_panic "internal error: wrong probe type $type";; esac # Patch! printf %s "$patchseq" \ | dd of=$objfile conv=notrunc count=5 ibs=1 bs=1 seek=$probe_off 2> /dev/null done <\n" printf "#include \n" printf \\n\\n printf "#ifdef __cplusplus\nextern \"C\" {\n#endif\n" printf "#define _DTRACE_VERSION 1\n\n" provider=$(cat $dfile | $EGREP "^ *provider +([a-zA-Z_]+)" \ | $SED -E -e 's/^ *provider +([a-zA-Z]+).*/\1/') test -z "$provider" \ && f_panic "unable to parse the provider name from $dfile." u_provider=$(printf %s "$provider" | $TR a-z A-Z | $TR -s _) cat $dfile | $EGREP "^ *probe +[a-zA-Z_]+ *\(.*\);" | \ while read line; do # Extract the probe name. name=$(printf %s "$line" \ | $SED -E -e 's/^ *probe +([a-zA-Z_]+).*/\1/') u_name=$(printf %s "$name" | $TR a-z A-Z | $TR -s _) # Generate an arg1,arg2,...,argN line for the probe. args=""; nargs=0; aline=$(printf %s "$line" | $SED -e 's/.*(\(.*\)).*/\1/') set -f; IFS=, for arg in $aline; do args="${args}arg${nargs}," nargs=$((nargs + 1)) done set +f; unset IFS args=${args%,} echo "#if _DTRACE_VERSION" echo "" # Emit the macros for the probe. echo "#define ${u_provider}_${u_name}($args) \\" echo " __dtrace_${provider}___${name}($args)" echo "#define ${u_provider}_${u_name}_ENABLED() \\" echo " __dtraceenabled_${provider}___${name}()" # Emit the extern definitions for the probe dummy # functions. echo "" printf %s\\n "$line" \ | $SED -E -e "s/^ *probe +/extern void __dtrace_${provider}___/" echo "extern int __dtraceenabled_${provider}___${name}(void);" printf "\n#else\n" # Emit empty macros for the probe echo "#define ${u_provider}_${u_name}($args)" echo "#define ${u_provider}_${u_name}_ENABLED() (0)" printf "\n#endif /* _DTRACE_VERSION */\n" done printf "#ifdef __cplusplus\n}\n#endif\n\n" printf "#endif /* _${guard}_H */\n" } ### Main program. # Process command line arguments. test "$#" -eq "0" && f_usage genelf=0 genheader=0 objbits=64 ofile= dfile= while getopts VG3264hs:o: name; do case $name in V) f_version;; s) dfile="$OPTARG"; test -f "$dfile" || f_panic "cannot read $dfile";; o) ofile="$OPTARG";; G) genelf=1;; h) genheader=1;; # Note the trick to support -32 3) objbits=666;; 2) test "$objbits" -eq 666 || f_usage; objbits=32;; # Likewise for -64 6) objbits=777;; 4) test "$objbits" -eq 777 || f_usage; objbits=64;; ?) f_usage;; esac done shift $(($OPTIND - 1)) test "$objbits" -eq "32" || test "$objbits" -eq "64" \ || f_usage test $((genelf + genheader)) -gt 1 && \ { echo "Please use either -G or -h."; f_usage; } test -n "$dfile" || { echo "Please specify a .d file with -s."; exit 2; } if test "$genelf" -gt 0; then # In this mode there must be a remaining argument: the name of the # object file to inspect for probed points. test "$#" -ne "1" && f_usage test -f "$1" || f_panic "cannot read $1" objfile=$1 # Collect probe information from the input object file and the # d-script. f_collect_probes $objfile f_collect_probes_args $dfile # Generate the assembly code and assemble the DOF program in # OFILE. Then patch OBJFILE to remove the dummy probe calls. f_gen_dof_program f_patch_objfile $objfile fi if test "$genheader" -gt 0; then test -n "$ofile" || { echo "Please specify an output file with -o."; exit 2; } # In this mode no extra arguments shall be present. test "$#" -ne "0" && f_usage f_gen_header_file > $ofile fi # pdtrace ends here.