File indexing completed on 2024-05-12 17:15:54

0001 #!/bin/sh
0002 
0003 #
0004 # SPDX-FileCopyrightText: 2014-2021 Milian Wolff <mail@milianw.de>
0005 #
0006 # SPDX-License-Identifier: LGPL-2.1-or-later
0007 #
0008 
0009 usage() {
0010     echo "Usage: $0 [--debug|-d] [--use-inject] [--record-only] DEBUGGEE [ARGUMENT]..."
0011     echo "or:    $0 [--debug|-d] -p PID"
0012     echo "or:    $0 -a FILE"
0013     echo
0014     echo "A heap memory usage profiler. It uses LD_PRELOAD to track all"
0015     echo "calls to the core memory allocation functions and logs these"
0016     echo "occurrences. Additionally, backtraces are obtained and logged."
0017     echo "Combined this can give interesting answers to questions such as:"
0018     echo
0019     echo "  * How much heap memory is my application using?"
0020     echo "  * Where is heap memory being allocated, and how often?"
0021     echo "  * How much space are heap individual allocations requesting?"
0022     echo
0023     echo "To evaluate the generated heaptrack data, use heaptrack_print or heaptrack_gui."
0024     echo
0025     echo "Mandatory arguments to heaptrack:"
0026     echo "  DEBUGGEE       The name or path to the application that should"
0027     echo "                 be run with heaptrack analyzation enabled."
0028     echo
0029     echo "Alternatively, to attach to a running process:"
0030     echo "  -p, --pid PID  The process ID of a running process into which"
0031     echo "                 heaptrack will be injected. This only works with"
0032     echo "                 applications that already link against libdl."
0033     echo "  WARNING: Runtime-attaching heaptrack is UNSTABLE and can lead to CRASHES"
0034     echo "           in your application, especially after you detach heaptrack again."
0035     echo "           You are hereby warned, use it at your own risk!"
0036     echo
0037     echo "Optional arguments to heaptrack:"
0038     echo "  -r, --raw      Only record raw data, do not interpret it."
0039     echo "  -d, --debug    Run the debuggee in GDB and heaptrack."
0040     echo " --use-inject    Use the same heaptrack_inject symbol interception mechanism instead of relying on"
0041     echo "                 the dynamic linker and LD_PRELOAD. This is an experimental flag for now."
0042     echo " --record-only   Only record and interpret the data, do not attempt to analyze it."
0043     echo "  ARGUMENT       Any number of arguments that will be passed verbatim"
0044     echo "                 to the debuggee."
0045     echo "  -h, --help     Show this help message and exit."
0046     echo "  -v, --version  Displays version information."
0047     echo "  -o, --output   Specifies the data-file for the captured data."
0048     echo "                 %h in the file name string is replaced with the hostname of the system."
0049     echo "                 %p in the file name string is replaced with the pid of the application being profiled."
0050     echo "                 Parent directories will be created if output files are under non-existing directories."
0051     echo "                 e.g.,"
0052     echo "                   ./%h/%p/outdat will be translated into ./<hostname>/<pid>/outdat."
0053     echo "                   The directory ./<hostname>/<pid> will be created if it doesn't exist."
0054     echo
0055     echo "Alternatively, to analyze a recorded heaptrack data file:"
0056     echo "  -a, --analyze FILE    Open the heaptrack data file in heaptrack_gui, if available,"
0057     echo "                        or fallback to heaptrack_print otherwise."
0058     echo "                        Any options passed after --analyze will be passed along."
0059     echo
0060     exit 0
0061 }
0062 
0063 debug=
0064 pid=
0065 client=
0066 use_inject_lib=
0067 write_raw_data=
0068 record_only=
0069 
0070 # path to current heaptrack.sh executable
0071 SCRIPT_PATH=$(readlink -f "$0")
0072 SCRIPT_DIR=$(dirname "$SCRIPT_PATH")
0073 EXE_PATH=$(readlink -f "$SCRIPT_DIR")
0074 
0075 openHeaptrackDataFiles() {
0076     if [ -x "$EXE_PATH/heaptrack_gui" ]; then
0077         "$EXE_PATH/heaptrack_gui" "$@"
0078     else
0079         "$EXE_PATH/heaptrack_print" "$@"
0080     fi
0081 }
0082 
0083 ORIG_CMDLINE=$@
0084 
0085 while true; do
0086     case "$1" in
0087         "-d" | "--debug")
0088             if [ -z "$(command -v gdb 2> /dev/null)" ]; then
0089                 echo "GDB is not installed, cannot debug heaptrack."
0090                 exit 1
0091             fi
0092             debug=1
0093             shift 1
0094             ;;
0095         "--use-inject")
0096             use_inject_lib=1
0097             shift 1
0098             ;;
0099         "-r" | "--raw")
0100             write_raw_data=1
0101             shift 1
0102             ;;
0103         "--record-only")
0104             record_only=1
0105             shift 1
0106             ;;
0107         "-h" | "--help")
0108             usage
0109             exit 0
0110             ;;
0111         "-o" | "--output" | "--output-file")
0112             if [ -z "$2" ]; then
0113                 echo "Missing output argument."
0114                 exit 1
0115             fi
0116             output=$(echo $2 | sed "s/%h/$(hostname)/g" | sed "s/%p/$$/g")
0117             if [ -d "$output" ]; then
0118                 echo "Please specify a file-name or a full path-name for output."
0119                 exit 1
0120             fi
0121             if [ ! -d $(dirname $output) ]; then
0122               mkdir -p $(dirname $output)
0123             fi
0124             output=$(readlink -f $output)
0125             shift 2
0126             ;;
0127         "-p" | "--pid")
0128             if [ -z "$(command -v gdb 2> /dev/null)" ]; then
0129                 echo "GDB is not installed, cannot attach to running process."
0130                 exit 1
0131             fi
0132             if [ -f "/proc/sys/kernel/yama/ptrace_scope"  ] && [ "$(cat "/proc/sys/kernel/yama/ptrace_scope")" -gt "0" ]; then
0133                 echo "Cannot runtime-attach, you need to set /proc/sys/kernel/yama/ptrace_scope to 0"
0134                 exit 1
0135             fi
0136             pid=$2
0137             if [ -z "$pid" ]; then
0138                 echo "Missing PID argument."
0139                 exit 1
0140             fi
0141             case $(uname) in
0142                 Linux*)
0143                     client=$(cat "/proc/$pid/comm")
0144                 ;;
0145                 FreeBSD*)
0146                     client=$(awk '{print $1}' < "/proc/$pid/cmdline")
0147                 ;;
0148             esac
0149             if [ -z "$client" ]; then
0150                 echo "Cannot attach to unknown process with PID $pid."
0151                 exit 1
0152             fi
0153             shift 2
0154             echo $@
0155             if [ ! -z "$@" ]; then
0156                 echo "You cannot specify a debuggee and a pid at the same time."
0157                 exit 1
0158             fi
0159             break
0160             ;;
0161         "-v" | "--version")
0162             echo "heaptrack @HEAPTRACK_VERSION_MAJOR@.@HEAPTRACK_VERSION_MINOR@.@HEAPTRACK_VERSION_PATCH@"
0163             exit 0
0164             ;;
0165         "-a" | "--analyze")
0166             shift 1
0167             openHeaptrackDataFiles "$@"
0168             exit
0169             ;;
0170         *)
0171             if [ "$1" = "--" ]; then
0172                 shift 1
0173             fi
0174             if [ ! -x "$(command -v "$1" 2> /dev/null)" ]; then
0175                 if [ -z "$1" ] && [ -x "$EXE_PATH/heaptrack_gui" ]; then
0176                     "$EXE_PATH/heaptrack_gui"
0177                     exit
0178                 fi
0179                 if [ -f "$1" ] && echo "$1" | grep -q "heaptrack."; then
0180                     openHeaptrackDataFiles "$ORIG_CMDLINE"
0181                     exit
0182                 fi
0183 
0184                 if [ ! -e "$1" ]; then
0185                     echo "Error: Debuggee \"$1\" was not found."
0186                 else
0187                     echo "Error: Debuggee \"$1\" is not an executable."
0188                 fi
0189 
0190                 echo
0191                 echo "Usage: $0 [--debug|-d] [--help|-h] DEBUGGEE [ARGS...]"
0192                 exit 1
0193             fi
0194             client="$1"
0195             shift 1
0196             break
0197             ;;
0198     esac
0199 done
0200 
0201 # put output into current pwd
0202 if [ -z "$output" ]; then
0203     output=$(pwd)/heaptrack.$(basename "$client").$$
0204 fi
0205 
0206 # find preload library and interpreter executable using relative paths
0207 LIB_REL_PATH="@LIB_REL_PATH@"
0208 LIBEXEC_REL_PATH="@LIBEXEC_REL_PATH@"
0209 
0210 ENVCHECKER="$EXE_PATH/$LIBEXEC_REL_PATH/heaptrack_env"
0211 if [ ! -f "$ENVCHECKER" ]; then
0212     echo "Could not find heaptrack_env: $ENVCHECKER"
0213     exit 1
0214 fi
0215 ENVCHECKER=$(readlink -f "$ENVCHECKER")
0216 
0217 INTERPRETER="$EXE_PATH/$LIBEXEC_REL_PATH/heaptrack_interpret"
0218 if [ -z "$write_raw_data" ] && [ ! -f "$INTERPRETER" ]; then
0219     echo "Could not find heaptrack interpreter executable: $INTERPRETER"
0220     exit 1
0221 fi
0222 INTERPRETER=$(readlink -f "$INTERPRETER")
0223 
0224 if [ -z "$use_inject_lib" ]; then
0225     LIBHEAPTRACK_PRELOAD="$EXE_PATH/$LIB_REL_PATH/libheaptrack_preload.so"
0226 else
0227     LIBHEAPTRACK_PRELOAD="$EXE_PATH/$LIB_REL_PATH/libheaptrack_inject.so"
0228 fi
0229 if [ ! -f "$LIBHEAPTRACK_PRELOAD" ]; then
0230     echo "Could not find heaptrack preload library $LIBHEAPTRACK_PRELOAD"
0231     exit 1
0232 fi
0233 LIBHEAPTRACK_PRELOAD=$(readlink -f "$LIBHEAPTRACK_PRELOAD")
0234 
0235 LIBHEAPTRACK_INJECT="$EXE_PATH/$LIB_REL_PATH/libheaptrack_inject.so"
0236 if [ ! -f "$LIBHEAPTRACK_INJECT" ]; then
0237     echo "Could not find heaptrack inject library $LIBHEAPTRACK_INJECT"
0238     exit 1
0239 fi
0240 LIBHEAPTRACK_INJECT=$(readlink -f "$LIBHEAPTRACK_INJECT")
0241 
0242 # setup named pipe to read data from
0243 pipe=/tmp/heaptrack_fifo$$
0244 mkfifo $pipe
0245 
0246 # if root is profiling a process for non root
0247 # give profiled process write access to the pipe
0248 if [ ! -z "$pid" ]; then
0249   case $(uname) in
0250     Linux*)
0251       pid_user=$(stat -c %U "/proc/$pid")
0252     ;;
0253     FreeBSD*)
0254       pid_user=$(stat -f %Su "/proc/$pid")
0255     ;;
0256   esac
0257   if [ -z "$pid_user" ]; then
0258     exit 1
0259   fi
0260   chown "$pid_user" "$pipe" || exit 1
0261 fi
0262 
0263 output_suffix="gz"
0264 COMPRESSOR="gzip -c"
0265 UNCOMPRESSOR="gzip -dc"
0266 
0267 if [ "@ZSTD_FOUND@" = "TRUE" ] && [ ! -z "$(command -v zstd 2> /dev/null)" ]; then
0268     output_suffix="zst"
0269     COMPRESSOR="zstd -c"
0270     UNCOMPRESSOR="zstd -dc"
0271 fi
0272 
0273 output_non_raw="$output.$output_suffix"
0274 
0275 if [ ! -z "$write_raw_data" ]; then
0276     output_suffix="raw.$output_suffix"
0277 fi
0278 
0279 # interpret the data and compress the output on the fly
0280 output="$output.$output_suffix"
0281 if [ -z "$write_raw_data" ]; then
0282     "$INTERPRETER" < $pipe | $COMPRESSOR > "$output" &
0283 else
0284     $COMPRESSOR < $pipe > "$output" &
0285 fi
0286 debuggee=$!
0287 
0288 cleanup() {
0289     if [ ! -z "$pid" ] && [ -d "/proc/$pid" ]; then
0290         echo "removing heaptrack injection via GDB, this might take some time..."
0291         gdb --batch-silent -n -iex="set auto-solib-add off" \
0292             -iex="set language c" -p $pid \
0293             --eval-command="sharedlibrary libheaptrack_inject" \
0294             --eval-command="call (void) heaptrack_stop()" \
0295             --eval-command="detach"
0296         # NOTE: we do not call dlclose here, as that has the tendency to trigger
0297         #       crashes in the debuggee. So instead, we keep heaptrack loaded.
0298     fi
0299     rm -f "$pipe"
0300     case $(uname) in
0301         FreeBSD*)
0302             rm -f "$pipe.lock"
0303         ;;
0304     esac
0305     kill "$debuggee" 2> /dev/null
0306 
0307     echo "Heaptrack finished! Now run the following to investigate the data:"
0308     echo
0309 
0310     if [ ! -z "$write_raw_data" ]; then
0311         echo "  $UNCOMPRESSOR < \"$output\" | $INTERPRETER | $COMPRESSOR > \"$output_non_raw\""
0312     else
0313         echo "  heaptrack --analyze \"$output\""
0314     fi
0315 
0316     if [ -z "$record_only" ] && [ -z "$write_raw_data" ] && [ -x "$EXE_PATH/heaptrack_gui" ]; then
0317         echo ""
0318         echo "heaptrack_gui detected, automatically opening the file..."
0319         "$EXE_PATH/heaptrack_gui" "$output"
0320     fi
0321 }
0322 trap cleanup EXIT
0323 
0324 echo "heaptrack output will be written to \"$output\""
0325 
0326 if [ -z "$debug" ] && [ -z "$pid" ]; then
0327   echo "starting application, this might take some time..."
0328   LD_PRELOAD="$LIBHEAPTRACK_PRELOAD${LD_PRELOAD:+:$LD_PRELOAD}" DUMP_HEAPTRACK_OUTPUT="$pipe" "$client" "$@"
0329   EXIT_CODE=$?
0330 else
0331   if [ -z "$pid" ]; then
0332     echo "starting application in GDB, this might take some time..."
0333     gdb --quiet --eval-command="set environment LD_PRELOAD=$LIBHEAPTRACK_PRELOAD" \
0334         --eval-command="set environment DUMP_HEAPTRACK_OUTPUT=$pipe" \
0335         --eval-command="set startup-with-shell off" \
0336         --eval-command="run" --args "$client" "$@"
0337     EXIT_CODE=$?
0338   else
0339     echo "injecting heaptrack into application via GDB, this might take some time..."
0340     dlopen=$($ENVCHECKER dlopen "$LIBHEAPTRACK_INJECT")
0341     if [ -z "$debug" ]; then
0342         unset DEBUGINFOD_URLS
0343         gdb --batch-silent -n -iex="set auto-solib-add off" \
0344             -iex="set language c" -p $pid \
0345             --eval-command="sharedlibrary libc.so" \
0346             --eval-command="call (void) $dlopen" \
0347             --eval-command="sharedlibrary libheaptrack_inject" \
0348             --eval-command="call (void) heaptrack_inject(\"$pipe\")" \
0349             --eval-command="detach"
0350     else
0351         echo $dlopen
0352         gdb --quiet -iex="set language c" -p $pid \
0353             --eval-command="sharedlibrary libc.so" \
0354             --eval-command="print (void*) $dlopen" \
0355             --eval-command="sharedlibrary libheaptrack_inject" \
0356             --eval-command="call (void) heaptrack_inject(\"$pipe\")"
0357     fi
0358     EXIT_CODE=$?
0359     echo "injection finished"
0360   fi
0361 fi
0362 
0363 wait $debuggee
0364 exit $EXIT_CODE
0365 
0366 # kate: hl Bash