From 591ee332c58988222f58c6eadb047890707e7a35 Mon Sep 17 00:00:00 2001 From: Francis Deslauriers Date: Thu, 8 Jun 2017 17:15:06 -0400 Subject: [PATCH] Tests: Add callstack contexts tests MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Tests callstack-user and callstack-kernel contexts by tracing with those contexts an application that calls multiple functions in chain before executing a system call. callstack-user addresses are tested against the output of coreutils' addr2line. callstack-kernel addresses are tested against the addresses of kernel symbols available in the /proc/kallsyms procfile. Both these tests need to be run by root because those contexts are made available by the kernel tracer. The callstack-kernel test also need to access /proc/kallsyms. Signed-off-by: Francis Deslauriers Signed-off-by: Jérémie Galarneau --- .gitignore | 4 +- configure.ac | 1 + tests/regression/kernel/test_callstack | 167 +++++++++++++++++ tests/root_regression | 1 + tests/utils/parse-callstack.py | 170 ++++++++++++++++++ tests/utils/testapp/Makefile.am | 10 +- .../gen-syscall-events-callstack/Makefile.am | 6 + .../gen-syscall-events-callstack.c | 118 ++++++++++++ 8 files changed, 475 insertions(+), 2 deletions(-) create mode 100755 tests/regression/kernel/test_callstack create mode 100755 tests/utils/parse-callstack.py create mode 100644 tests/utils/testapp/gen-syscall-events-callstack/Makefile.am create mode 100644 tests/utils/testapp/gen-syscall-events-callstack/gen-syscall-events-callstack.c diff --git a/.gitignore b/.gitignore index 3f766e3a8..1ad59422b 100644 --- a/.gitignore +++ b/.gitignore @@ -112,6 +112,9 @@ health_check /tests/regression/ust/ust-dl/test_ust-dl /tests/regression/ust/multi-lib/exec-with-callsites /tests/regression/ust/multi-lib/exec-without-callsites +/tests/utils/testapp/gen-syscall-events-callstack/gen-syscall-events-callstack +/tests/utils/testapp/gen-ust-events/gen-ust-events +/tests/utils/testapp/gen-ust-nevents-str/gen-ust-nevents-str /tests/utils/testapp/gen-ust-nevents/gen-ust-nevents /tests/utils/testapp/gen-ust-tracef/gen-ust-tracef /tests/utils/testapp/gen-syscall-events/gen-syscall-events @@ -120,7 +123,6 @@ health_check /tests/perf/find_event /tests/perf/test_perf_raw /tests/unit/test_string_utils -/tests/utils/testapp/gen-ust-nevents-str/gen-ust-nevents-str # man pages /doc/man/*.1 diff --git a/configure.ac b/configure.ac index 6a26e4998..acae225ad 100644 --- a/configure.ac +++ b/configure.ac @@ -1152,6 +1152,7 @@ AC_CONFIG_FILES([ tests/utils/tap/Makefile tests/utils/testapp/Makefile tests/utils/testapp/gen-ust-events/Makefile + tests/utils/testapp/gen-syscall-events-callstack/Makefile tests/utils/testapp/gen-ust-nevents/Makefile tests/utils/testapp/gen-ust-nevents-str/Makefile tests/utils/testapp/gen-syscall-events/Makefile diff --git a/tests/regression/kernel/test_callstack b/tests/regression/kernel/test_callstack new file mode 100755 index 000000000..6cb20bcdb --- /dev/null +++ b/tests/regression/kernel/test_callstack @@ -0,0 +1,167 @@ +#!/bin/bash +# +# Copyright (C) - 2017 Francis Deslauriers +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License, version 2 only, as +# published by the Free Software Foundation. +# +# 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, write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +TEST_DESC="Kernel tracer - Callstack context" + +CURDIR=$(dirname "$0")/ +TESTDIR=$CURDIR/../.. +NUM_TESTS=11 +TEST_APP_USERSPACE="$TESTDIR/utils/testapp/gen-syscall-events-callstack/gen-syscall-events-callstack" +TEST_APP_KERNELSPACE="$TESTDIR/utils/testapp/gen-syscall-events/gen-syscall-events" +PARSE_CALLSTACK="$TESTDIR/utils/parse-callstack.py" + +SESSION_NAME="callstack" +CHANNEL_NAME="chan0" + +source "$TESTDIR/utils/utils.sh" + +function lttng_untrack_all() +{ + lttng_untrack 0 "-s $SESSION_NAME --all --pid -k" +} + +function lttng_track_pid() +{ + local PID=$1 + lttng_track 0 "-s $SESSION_NAME -k --pid=$PID" +} + +function run_workload() +{ + local TEST_APP=$1 + local start_file_sync + start_file_sync=$(mktemp -u) + + lttng_untrack_all + + ./"$TEST_APP" "$start_file_sync" & + PID=$! + lttng_track_pid $PID + + start_lttng_tracing_ok + + # Create start file to launch the execution of the syscall call by the + # test app. + touch "$start_file_sync" + + wait $PID + + stop_lttng_tracing_ok + + # Clean up the synchronization file. + rm -f "$start_file_sync" +} + +function test_user_callstack() +{ + TRACE_PATH=$(mktemp -d) + # This is the expected userspace callstack. (see gen-syscall-events-callstack.c) + USER_CS_EXPECTED="main fct_a fct_b fct_c my_gettid" + EVENT_NAME="gettid" + + diag "Userspace callstack test" + create_lttng_session_ok $SESSION_NAME "$TRACE_PATH" + lttng_enable_kernel_channel_ok "$SESSION_NAME" "$CHANNEL_NAME" + + lttng_enable_kernel_syscall_ok "$SESSION_NAME" "$EVENT_NAME" "$CHANNEL_NAME" + add_context_kernel_ok "$SESSION_NAME" "$CHANNEL_NAME" "callstack-user" + + run_workload $TEST_APP_USERSPACE + + destroy_lttng_session_ok "$SESSION_NAME" + + "$BABELTRACE_BIN" "$TRACE_PATH" | grep $EVENT_NAME | ./"$PARSE_CALLSTACK" --user "$TEST_APP_USERSPACE" $USER_CS_EXPECTED + ok $? "Validate userspace callstack" + + rm -rf "$TRACE_PATH" +} + +function test_kernel_callstack() +{ + TRACE_PATH=$(mktemp -d) + # Those are symbol expected to be present in the kernel callstack. This + # is not an exhaustive list since it's kernel dependent. + + # FIXME: we used to test for the following symbols as well: + # save_stack_trace, lttng_callstack_get_size, but they were removed + # because: + # 1. kernel commit 77072f09 make it so that save_stack_trace is + # omitted from the callstack itself, and + # + # 2. the code (of this commit) can trigger Tail Call Optimization + # which mess up with the stacktrace by omiting the wrong address + # from the stacktrace. + # When this is fixed, we should add both save_stack_trace and + # lttng_callstack_get_size symbols back in the list of expected + # addresses. + KERNEL_CS_EXPECTED="lttng_event_reserve" + EVENT_NAME="read" + + diag "Kernel callstack test" + create_lttng_session_ok $SESSION_NAME "$TRACE_PATH" + lttng_enable_kernel_channel_ok "$SESSION_NAME" "$CHANNEL_NAME" + + lttng_enable_kernel_syscall_ok "$SESSION_NAME" "$EVENT_NAME" "$CHANNEL_NAME" + add_context_kernel_ok "$SESSION_NAME" "$CHANNEL_NAME" "callstack-kernel" + + run_workload $TEST_APP_KERNELSPACE + + destroy_lttng_session_ok "$SESSION_NAME" + + "$BABELTRACE_BIN" "$TRACE_PATH" | grep $EVENT_NAME | ./"$PARSE_CALLSTACK" --kernel $KERNEL_CS_EXPECTED + ok $? "Validate kernel callstack" + + rm -rf "$TRACE_PATH" +} + +# Only run userspace callstack test on x86 +uname -m | grep -E "x86" >/dev/null 2>&1 +if test $? == 0; then + NUM_TESTS=$((NUM_TESTS+11)) + RUN_USERSPACE_TEST=1 +else + RUN_USERSPACE_TEST=0 +fi + +# MUST set TESTDIR before calling those functions +plan_tests $NUM_TESTS + +print_test_banner "$TEST_DESC" + +if [ "$(id -u)" == "0" ]; then + isroot=1 +else + isroot=0 +fi + +skip $isroot "Root access is needed. Skipping all tests." "$NUM_TESTS" || +{ + which "$BABELTRACE_BIN" > /dev/null + test $? -ne 0 + skip $? "Babeltrace binary not found. Skipping callstack tests" "$NUM_TESTS" || + { + start_lttng_sessiond + + if test $RUN_USERSPACE_TEST == 1; then + test_user_callstack + fi + + test_kernel_callstack + + stop_lttng_sessiond + } +} diff --git a/tests/root_regression b/tests/root_regression index f17ac977f..40bb7a59a 100644 --- a/tests/root_regression +++ b/tests/root_regression @@ -5,6 +5,7 @@ regression/kernel/test_clock_override regression/kernel/test_rotation_destroy_flush regression/kernel/test_select_poll_epoll regression/kernel/test_lttng_logger +regression/kernel/test_callstack regression/tools/live/test_kernel regression/tools/live/test_lttng_kernel regression/tools/streaming/test_high_throughput_limits diff --git a/tests/utils/parse-callstack.py b/tests/utils/parse-callstack.py new file mode 100755 index 000000000..da0bab626 --- /dev/null +++ b/tests/utils/parse-callstack.py @@ -0,0 +1,170 @@ +#! /usr/bin/python3 + +# Copyright (C) - 2017 Francis Deslauriers +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation; version 2.1 of the License. +# +# This library 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 Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import sys +import bisect +import subprocess +import re + +def addr2line(executable, addr): + """ + Uses binutils' addr2line to get function containing a given address + """ + cmd =['addr2line'] + + cmd += ['-e', executable] + + # Print function names + cmd += ['--functions'] + + # Expand inlined functions + cmd += ['--addresses', addr] + + addr2line_output = subprocess.getoutput(' '.join(cmd)) + + # Omit the last 2 lines as the caller of main can not be determine + fcts = [addr2line_output.split()[-2]] + + fcts = [ f for f in fcts if '??' not in f] + + return fcts + +def extract_user_func_names(executable, raw_callstack): + """ + Given a callstack from the Babeltrace CLI output, returns a set + containing the name of the functions. This assumes that the binary have + not changed since the execution. + """ + recorded_callstack = set() + + # Remove commas and split on spaces + for index, addr in enumerate(raw_callstack.replace(',', '').split(' ')): + # Consider only the elements starting with '0x' which are the + # addresses recorded in the callstack + if '0x' in addr[:2]: + funcs = addr2line(executable, addr) + recorded_callstack.update(funcs) + + return recorded_callstack + +def extract_kernel_func_names(raw_callstack): + """ + Given a callstack from the Babeltrace CLI output, returns a set + containing the name of the functions. + Uses the /proc/kallsyms procfile to find the symbol associated with an + address. This function should only be used if the user is root or has + access to /proc/kallsyms. + """ + recorded_callstack = set() + syms=[] + addresses=[] + # We read kallsyms file and save the output + with open('/proc/kallsyms') as kallsyms_f: + for line in kallsyms_f: + line_tokens = line.split() + addr = line_tokens[0] + symbol = line_tokens[2] + addresses.append(int(addr, 16)) + syms.append({'addr':int(addr, 16), 'symbol':symbol}) + + # Save the address and symbol in a sorted list of tupple + syms = sorted(syms, key=lambda k:k['addr']) + # We save the list of addresses in a seperate sorted list to easily bisect + # the closer address of a symbol. + addresses = sorted(addresses) + + # Remove commas and split on spaces + for addr in raw_callstack.replace(',', '').split(' '): + if '0x' in addr[:2]: + # Search the location of the address in the addresses list and + # deference this location in the syms list to add the associated + # symbol. + loc = bisect.bisect(addresses, int(addr, 16)) + recorded_callstack.add(syms[loc-1]['symbol']) + + return recorded_callstack + +# Regex capturing the callstack_user and callstack_kernel context +user_cs_rexp='.*callstack_user\ \=\ \[(.*)\]\ .*\}, \{.*\}' +kernel_cs_rexp='.*callstack_kernel\ \=\ \[(.*)\]\ .*\}, \{.*\}' + +def main(): + """ + Reads a line from stdin and expect it to be a wellformed Babeltrace CLI + output containing containing a callstack context of the domain passed + as argument. + """ + expected_callstack = set() + recorded_callstack = set() + cs_type=None + + if len(sys.argv) <= 2: + print(sys.argv) + raise ValueError('USAGE: ./{} (--kernel|--user EXE) FUNC-NAMES'.format(sys.argv[0])) + + # If the `--user` option is passed, save the next argument as the path + # to the executable + argc=1 + executable=None + if sys.argv[argc] in '--kernel': + rexp = kernel_cs_rexp + cs_type='kernel' + elif sys.argv[argc] in '--user': + rexp = user_cs_rexp + cs_type='user' + argc+=1 + executable = sys.argv[argc] + else: + raise Exception('Unknown domain') + + argc+=1 + + # Extract the function names that are expected to be found call stack of + # the current events + for func in sys.argv[argc:]: + expected_callstack.add(func) + + # Read the tested line for STDIN + event_line = None + for line in sys.stdin: + event_line = line + break + + # Extract the userspace callstack context of the event + m = re.match(rexp, event_line) + + # If there is no match, exit with error + if m is None: + raise re.error('Callstack not found in event line') + else: + raw_callstack = str(m.group(1)) + if cs_type in 'user': + recorded_callstack=extract_user_func_names(executable, raw_callstack) + elif cs_type in 'kernel': + recorded_callstack=extract_kernel_func_names(raw_callstack) + else: + raise Exception('Unknown domain') + + # Verify that all expected function are present in the callstack + for e in expected_callstack: + if e not in recorded_callstack: + raise Exception('Expected function name not found in recorded callstack') + + sys.exit(0) + +if __name__ == '__main__': + main() diff --git a/tests/utils/testapp/Makefile.am b/tests/utils/testapp/Makefile.am index 596591265..9bfe53666 100644 --- a/tests/utils/testapp/Makefile.am +++ b/tests/utils/testapp/Makefile.am @@ -1,3 +1,11 @@ -SUBDIRS = gen-ust-events gen-ust-nevents gen-ust-nevents-str gen-ust-tracef gen-syscall-events +SUBDIRS = gen-ust-events \ + gen-ust-nevents \ + gen-ust-nevents-str \ + gen-ust-tracef \ + gen-syscall-events + +if HAVE_MODULES_USERSPACE_CALLSTACK_CONTEXT +SUBDIRS += gen-syscall-events-callstack +endif # HAVE_MODULES_USERSPACE_CALLSTACK_CONTEXT noinst_HEADERS = signal-helper.h diff --git a/tests/utils/testapp/gen-syscall-events-callstack/Makefile.am b/tests/utils/testapp/gen-syscall-events-callstack/Makefile.am new file mode 100644 index 000000000..6e075a0df --- /dev/null +++ b/tests/utils/testapp/gen-syscall-events-callstack/Makefile.am @@ -0,0 +1,6 @@ +AM_CFLAGS += -I$(top_srcdir)/tests/utils/ +AM_CFLAGS += -fno-omit-frame-pointer -no-pie + +noinst_PROGRAMS = gen-syscall-events-callstack +gen_syscall_events_callstack_SOURCES = gen-syscall-events-callstack.c +gen_syscall_events_callstack_LDADD = $(top_builddir)/tests/utils/libtestutils.la diff --git a/tests/utils/testapp/gen-syscall-events-callstack/gen-syscall-events-callstack.c b/tests/utils/testapp/gen-syscall-events-callstack/gen-syscall-events-callstack.c new file mode 100644 index 000000000..48210fab0 --- /dev/null +++ b/tests/utils/testapp/gen-syscall-events-callstack/gen-syscall-events-callstack.c @@ -0,0 +1,118 @@ +/* + * Copyright (C) - 2017 Francis Deslauriers + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation; version 2.1 of the License. + * + * This library 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 Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "utils.h" + +/** + * The process waits for the creation of a file passed as argument from an + * external processes to execute a syscall and exiting. This is useful for tests + * in combinaison with LTTng's PID tracker feature where we can trace the kernel + * events generated by our test process only. + */ + +volatile int val = 0; + +long __attribute__ ((noinline)) +my_gettid(void) +{ + long ret; +#ifdef __x86_64 + asm volatile + ( + "syscall" + : "=a" (ret) + : "0"(__NR_gettid) + : "cc", "rcx", "r11", "memory" + ); +#elif __i386 + asm volatile + ( + "int $0x80" + : "=a" (ret) + : "0"(__NR_gettid) + : "cc", "edi", "esi", "memory" + ); +#else +#error "Userspace callstack test not supported for this architecture." +#endif + return ret; +} + +int __attribute__ ((noinline)) +fct_c(void) +{ + return my_gettid(); +} + +int __attribute__ ((noinline)) +fct_b(void) +{ + val += fct_c(); + return val; +} + +int __attribute__ ((noinline)) +fct_a(void) +{ + val += fct_b(); + return val; +} + +int main(int argc, char **argv) +{ + int ret = 0; + char *start_file; + + if (argc != 2) { + fprintf(stderr, "Error: Missing argument\n"); + fprintf(stderr, "USAGE: %s PATH_WAIT_FILE\n", argv[0]); + ret = -1; + goto error; + } + + start_file = argv[1]; + + /* + * Wait for the start_file to be created by an external process + * (typically the test script) before executing the syscall + */ + ret = wait_on_file(start_file); + if (ret != 0) { + goto error; + } + + /* Start the callchain to the syscall */ + ret = fct_a(); + + /* Return success */ + if (ret >= 0) { + ret = 0; + } + +error: + return ret; +} -- 2.34.1