From 8760fb49760b4d46c396655f4c472de1710253f6 Mon Sep 17 00:00:00 2001 From: Mark Meserve Date: Thu, 15 Jan 2026 14:32:14 -0600 Subject: [PATCH] attach: Formalize ROCAttach API (#1653) * attach: Formalize ROCAttach API - Make ROCAttach public with public headers - Change detach to take a PID - attach and detach are now reentrant - Cleanup of states and signal handling in ptrace session - Fixes mixed up definition of ROCPROF_ATTACH_TOOL_LIBRARY - ROCPROF_ATTACH_TOOL_LIBRARY now always means the tool library loaded by the attachment target - ROCPROF_ATTACH_LIBRARY refers to the library used to perform attachment - Add direct call of rocprof-attach - Fix python library call of rocprof-attach - Function now named attach(), changed from main() * attach: rocprof-compute ROCAttach updates - Update to new library names - Correct usage of C lib detach * attach: add test for rocattach - Disable ASan, TSan, and UBSan for the new parallel-attach test - Lower log level for LSan tests, existing behavior from other tests --------- Co-authored-by: Ammar ELWazir --- .../profiler_rocprofiler_sdk.py | 9 +- .../rocprofiler-compute/src/utils/utils.py | 4 +- projects/rocprofiler-sdk/CMakeLists.txt | 1 + .../build-config.cmake.in | 9 + .../rocprofiler-sdk-rocattach/config.cmake.in | 73 ++ ...rocprofiler_config_install_rocattach.cmake | 106 ++ .../source/bin/rocprof-attach.py | 129 +- .../rocprofiler-sdk/source/bin/rocprofv3.py | 4 +- .../docs/api-reference/process_attachment.rst | 46 +- .../source/include/CMakeLists.txt | 1 + .../rocprofiler-sdk-rocattach/CMakeLists.txt | 18 + .../rocprofiler-sdk-rocattach/defines.h | 116 ++ .../rocprofiler-sdk-rocattach/rocattach.h | 136 ++ .../include/rocprofiler-sdk-rocattach/types.h | 81 ++ .../rocprofiler-sdk-rocattach/version.h.in | 115 ++ .../rocprofiler-sdk-rocattach/CMakeLists.txt | 17 +- .../lib/rocprofiler-sdk-rocattach/auxv.cpp | 88 ++ .../lib/rocprofiler-sdk-rocattach/auxv.hpp | 35 + .../ptrace_session.cpp | 1147 ++++++++--------- .../ptrace_session.hpp | 130 +- .../rocprofiler-sdk-rocattach/rocattach.cpp | 463 +++++-- .../symbol_lookup.cpp | 260 ++++ .../symbol_lookup.hpp | 38 + projects/rocprofiler-sdk/tests/CMakeLists.txt | 3 + .../tests/rocattach/CMakeLists.txt | 68 + .../rocprofiler-sdk/tests/rocattach/main.cpp | 124 ++ 26 files changed, 2426 insertions(+), 795 deletions(-) create mode 100644 projects/rocprofiler-sdk/cmake/Templates/rocprofiler-sdk-rocattach/build-config.cmake.in create mode 100644 projects/rocprofiler-sdk/cmake/Templates/rocprofiler-sdk-rocattach/config.cmake.in create mode 100644 projects/rocprofiler-sdk/cmake/rocprofiler_config_install_rocattach.cmake create mode 100644 projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/CMakeLists.txt create mode 100644 projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/defines.h create mode 100644 projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/rocattach.h create mode 100644 projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/types.h create mode 100644 projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/version.h.in create mode 100644 projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/auxv.cpp create mode 100644 projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/auxv.hpp create mode 100644 projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/symbol_lookup.cpp create mode 100644 projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/symbol_lookup.hpp create mode 100644 projects/rocprofiler-sdk/tests/rocattach/CMakeLists.txt create mode 100644 projects/rocprofiler-sdk/tests/rocattach/main.cpp diff --git a/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprofiler_sdk.py b/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprofiler_sdk.py index a42bf56a70..f976c99d0d 100644 --- a/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprofiler_sdk.py +++ b/projects/rocprofiler-compute/src/rocprof_compute_profile/profiler_rocprofiler_sdk.py @@ -80,19 +80,20 @@ class rocprofiler_sdk_profiler(RocProfCompute_Base): }) if args.attach_pid: - # In attach mode, tools are provided using ROCP_TOOL_LIBRARIES + # In attach mode, tools are provided using ROCPROF_ATTACH_TOOL_LIBRARY # instead of LD_PRELOAD. options.update({ - "ROCP_TOOL_LIBRARIES": ":".join(ld_preload), + "ROCPROF_ATTACH_TOOL_LIBRARY": ":".join(ld_preload), }) options.pop("LD_PRELOAD", None) - rocprofiler_attach_tool_path = str( + rocprofiler_attach_library_path = str( Path(args.rocprofiler_sdk_tool_path).parent.parent / "librocprofiler-sdk-rocattach.so" ) options.update({ - "ROCPROF_ATTACH_TOOL_LIBRARY": rocprofiler_attach_tool_path, + "ROCPROF_ATTACH_TOOL_LIBRARY": rocprofiler_sdk_tool_path, + "ROCPROF_ATTACH_LIBRARY": rocprofiler_attach_library_path, "ROCPROF_ATTACH_PID": args.attach_pid, }) diff --git a/projects/rocprofiler-compute/src/utils/utils.py b/projects/rocprofiler-compute/src/utils/utils.py index a6c5e01484..c85471edae 100644 --- a/projects/rocprofiler-compute/src/utils/utils.py +++ b/projects/rocprofiler-compute/src/utils/utils.py @@ -803,7 +803,7 @@ def run_prof( os.environ.update(original_env) with temporary_env(new_env): - libname = options["ROCPROF_ATTACH_TOOL_LIBRARY"] + libname = options["ROCPROF_ATTACH_LIBRARY"] c_lib = ctypes.CDLL(libname) if c_lib is None: console_error(f"Error opening {libname}") @@ -829,7 +829,7 @@ def run_prof( f"detach will happen in {duration} milliseconds...\033[0m" ) time.sleep(int(duration) / 1000) - c_lib.detach() + c_lib.detach(int(pid)) else: if app_cmd is None: diff --git a/projects/rocprofiler-sdk/CMakeLists.txt b/projects/rocprofiler-sdk/CMakeLists.txt index 5a12086ad3..bd62096a9d 100644 --- a/projects/rocprofiler-sdk/CMakeLists.txt +++ b/projects/rocprofiler-sdk/CMakeLists.txt @@ -121,6 +121,7 @@ add_subdirectory(source) include(rocprofiler_config_install) include(rocprofiler_config_install_roctx) include(rocprofiler_config_install_rocpd) +include(rocprofiler_config_install_rocattach) include(rocprofiler_config_install_tests) if(ROCPROFILER_BUILD_TESTS) diff --git a/projects/rocprofiler-sdk/cmake/Templates/rocprofiler-sdk-rocattach/build-config.cmake.in b/projects/rocprofiler-sdk/cmake/Templates/rocprofiler-sdk-rocattach/build-config.cmake.in new file mode 100644 index 0000000000..a914160cb4 --- /dev/null +++ b/projects/rocprofiler-sdk/cmake/Templates/rocprofiler-sdk-rocattach/build-config.cmake.in @@ -0,0 +1,9 @@ +# Config file for @PACKAGE_NAME@ and its component libraries in the build tree +# + +list(APPEND @PACKAGE_NAME@_INCLUDE_DIR @CMAKE_BINARY_DIR@/source/include) + +foreach(COMP @PROJECT_BUILD_TREE_TARGETS@) + list(APPEND @PACKAGE_NAME@_LIBRARIES ${COMP}) + target_link_libraries(@PACKAGE_NAME@::@PACKAGE_NAME@ INTERFACE ${COMP}) +endforeach() diff --git a/projects/rocprofiler-sdk/cmake/Templates/rocprofiler-sdk-rocattach/config.cmake.in b/projects/rocprofiler-sdk/cmake/Templates/rocprofiler-sdk-rocattach/config.cmake.in new file mode 100644 index 0000000000..f12f3647cf --- /dev/null +++ b/projects/rocprofiler-sdk/cmake/Templates/rocprofiler-sdk-rocattach/config.cmake.in @@ -0,0 +1,73 @@ +# - Config file for @PACKAGE_NAME@ and its component libraries +# It defines the following variables: +# +# @PACKAGE_NAME@_VERSION +# @PACKAGE_NAME@_INCLUDE_DIR +# @PACKAGE_NAME@_LIB_DIR +# @PACKAGE_NAME@_LIBRARIES +# +# It provides the following interface libraries: +# +# @PACKAGE_NAME@::@PACKAGE_NAME@ +# + +# prevent "target already exists" error +include_guard(DIRECTORY) + +# compute paths +get_filename_component(@PACKAGE_NAME@_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) + +@PACKAGE_INIT@ + +set_and_check(@PACKAGE_NAME@_INCLUDE_DIR "@PACKAGE_INCLUDE_INSTALL_DIR@") +set_and_check(@PACKAGE_NAME@_LIB_DIR "@PACKAGE_LIB_INSTALL_DIR@") +get_filename_component(@PACKAGE_NAME@_ROOT_DIR ${@PACKAGE_NAME@_INCLUDE_DIR} PATH) +set_and_check(@PACKAGE_NAME@_ROOT "${@PACKAGE_NAME@_ROOT_DIR}") + +# extra validation +foreach(_@PACKAGE_NAME@_SUBDIR @PROJECT_EXTRA_DIRS@) + set_and_check(_@PACKAGE_NAME@_SUBDIR_CHECK + "${PACKAGE_PREFIX_DIR}/${_@PACKAGE_NAME@_SUBDIR}") + unset(_@PACKAGE_NAME@_SUBDIR_CHECK) +endforeach() + +set(@PACKAGE_NAME@_LIBRARIES) + +# add interface library +add_library(@PACKAGE_NAME@::@PACKAGE_NAME@ INTERFACE IMPORTED) + +if(@PACKAGE_NAME@_BUILD_TREE + AND EXISTS "${CMAKE_CURRENT_LIST_DIR}/@PACKAGE_NAME@-build-config.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/@PACKAGE_NAME@-build-config.cmake") +else() + include("${@PACKAGE_NAME@_CMAKE_DIR}/@PACKAGE_NAME@-targets.cmake") + + # Library dependencies + if(@PACKAGE_NAME@_FIND_COMPONENTS) + foreach(COMP ${@PACKAGE_NAME@_FIND_COMPONENTS}) + set(TARG @PACKAGE_NAME@::@PACKAGE_NAME@-${COMP}) + if(TARGET ${TARG}) + set(@PACKAGE_NAME@_${COMP}_FOUND 1) + list(APPEND @PACKAGE_NAME@_LIBRARIES ${TARG}) + target_link_libraries(@PACKAGE_NAME@::@PACKAGE_NAME@ INTERFACE ${TARG}) + else() + set(@PACKAGE_NAME@_${COMP}_FOUND 0) + endif() + endforeach() + else() + foreach(TARG @PROJECT_BUILD_TARGETS@) + set(TARG @PACKAGE_NAME@::${TARG}) + list(APPEND @PACKAGE_NAME@_LIBRARIES ${TARG}) + target_link_libraries(@PACKAGE_NAME@::@PACKAGE_NAME@ INTERFACE ${TARG}) + endforeach() + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args( + @PACKAGE_NAME@ + FOUND_VAR @PACKAGE_NAME@_FOUND + VERSION_VAR @PACKAGE_NAME@_VERSION + REQUIRED_VARS @PACKAGE_NAME@_ROOT_DIR @PACKAGE_NAME@_INCLUDE_DIR + @PACKAGE_NAME@_LIB_DIR @PACKAGE_NAME@_LIBRARIES @PACKAGE_NAME@_VERSION + HANDLE_COMPONENTS HANDLE_VERSION_RANGE) diff --git a/projects/rocprofiler-sdk/cmake/rocprofiler_config_install_rocattach.cmake b/projects/rocprofiler-sdk/cmake/rocprofiler_config_install_rocattach.cmake new file mode 100644 index 0000000000..9938c995b2 --- /dev/null +++ b/projects/rocprofiler-sdk/cmake/rocprofiler_config_install_rocattach.cmake @@ -0,0 +1,106 @@ +# include guard +include_guard(GLOBAL) + +include(CMakePackageConfigHelpers) + +set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME rocattach) +set(SDK_PACKAGE_NAME "${PROJECT_NAME}") +set(PACKAGE_NAME "${PROJECT_NAME}-rocattach") + +install( + EXPORT ${PACKAGE_NAME}-targets + FILE ${PACKAGE_NAME}-targets.cmake + NAMESPACE ${SDK_PACKAGE_NAME}:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PACKAGE_NAME} + COMPONENT rocattach) + +rocprofiler_install_env_setup_files( + NAME ${PACKAGE_NAME} + VERSION ${PROJECT_VERSION} + INSTALL_DIR ${CMAKE_INSTALL_DATAROOTDIR} + COMPONENT rocattach) + +# ------------------------------------------------------------------------------# +# install tree +# +set(PROJECT_INSTALL_DIR ${CMAKE_INSTALL_PREFIX}) +set(INCLUDE_INSTALL_DIR ${CMAKE_INSTALL_INCLUDEDIR}) +set(LIB_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR}) +set(PROJECT_BUILD_TARGETS ${PACKAGE_NAME}-shared-library) +set(PROJECT_EXTRA_DIRS "${CMAKE_INSTALL_INCLUDEDIR}/${PACKAGE_NAME}") + +configure_package_config_file( + ${PROJECT_SOURCE_DIR}/cmake/Templates/${PACKAGE_NAME}/config.cmake.in + ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/${PACKAGE_NAME}/${PACKAGE_NAME}-config.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PACKAGE_NAME} + INSTALL_PREFIX ${CMAKE_INSTALL_PREFIX} + PATH_VARS PROJECT_INSTALL_DIR INCLUDE_INSTALL_DIR LIB_INSTALL_DIR) + +write_basic_package_version_file( + ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/${PACKAGE_NAME}/${PACKAGE_NAME}-config-version.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion) + +install( + FILES + ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/${PACKAGE_NAME}/${PACKAGE_NAME}-config.cmake + ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/${PACKAGE_NAME}/${PACKAGE_NAME}-config-version.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PACKAGE_NAME} + COMPONENT rocattach) + +export(PACKAGE ${PACKAGE_NAME}) + +# ------------------------------------------------------------------------------# +# build tree +# +set(${PACKAGE_NAME}_BUILD_TREE + ON + CACHE BOOL "" FORCE) + +set(PROJECT_BUILD_TREE_TARGETS ${SDK_PACKAGE_NAME}::${PACKAGE_NAME}-shared-library + ${SDK_PACKAGE_NAME}::${SDK_PACKAGE_NAME}-stack-protector) + +configure_file( + ${PROJECT_SOURCE_DIR}/cmake/Templates/${PACKAGE_NAME}/build-config.cmake.in + ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/${PACKAGE_NAME}/${PACKAGE_NAME}-build-config.cmake + @ONLY) + +file(RELATIVE_PATH rocp_bin2src_rel_path ${PROJECT_BINARY_DIR} ${PROJECT_SOURCE_DIR}) +string(REPLACE "//" "/" rocp_inc_rel_path "${rocp_bin2src_rel_path}/source/include") + +set(_BUILDTREE_EXPORT_DIR + "${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/cmake/${PACKAGE_NAME}") + +execute_process( + COMMAND ${CMAKE_COMMAND} -E create_symlink ${rocp_inc_rel_path} + ${PROJECT_BINARY_DIR}/include WORKING_DIRECTORY ${PROJECT_BINARY_DIR}) + +if(NOT EXISTS "${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}") + file(MAKE_DIRECTORY "${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}") +endif() + +if(NOT EXISTS "${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/${PACKAGE_NAME}") + file(MAKE_DIRECTORY "${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}/${PACKAGE_NAME}") +endif() + +if(NOT EXISTS "${_BUILDTREE_EXPORT_DIR}") + file(MAKE_DIRECTORY "${_BUILDTREE_EXPORT_DIR}") +endif() + +if(NOT EXISTS "${_BUILDTREE_EXPORT_DIR}/${PACKAGE_NAME}-targets.cmake") + file(TOUCH "${_BUILDTREE_EXPORT_DIR}/${PACKAGE_NAME}-targets.cmake") +endif() + +export( + EXPORT ${PACKAGE_NAME}-targets + NAMESPACE ${PACKAGE_NAME}:: + FILE "${_BUILDTREE_EXPORT_DIR}/${PACKAGE_NAME}-targets.cmake") + +set(${PACKAGE_NAME}_DIR + "${_BUILDTREE_EXPORT_DIR}" + CACHE PATH "${PACKAGE_NAME} build tree install" FORCE) + +install( + FILES ${PROJECT_SOURCE_DIR}/LICENSE.md + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/doc/${PACKAGE_NAME} + COMPONENT rocattach) diff --git a/projects/rocprofiler-sdk/source/bin/rocprof-attach.py b/projects/rocprofiler-sdk/source/bin/rocprof-attach.py index 6b064abd1e..72306ca176 100755 --- a/projects/rocprofiler-sdk/source/bin/rocprof-attach.py +++ b/projects/rocprofiler-sdk/source/bin/rocprof-attach.py @@ -22,6 +22,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import argparse import ctypes import os import signal @@ -30,27 +31,110 @@ import time ROCPROF_ATTACH_DIR = os.path.dirname(os.path.realpath(__file__)) ROCM_DIR = os.path.dirname(ROCPROF_ATTACH_DIR) -ROCPROF_ATTACH_TOOL_LIBRARY = f"{ROCM_DIR}/lib/rocprofiler-sdk-rocattach.so" +ROCPROF_ATTACH_LIBRARY = f"{ROCM_DIR}/lib/librocprofiler-sdk-rocattach.so" -def main( - pid=os.environ.get("ROCPROF_ATTACH_PID", None), - attach_library=os.environ.get( - "ROCPROF_ATTACH_TOOL_LIBRARY", ROCPROF_ATTACH_TOOL_LIBRARY - ), - duration=os.environ.get("ROCPROF_ATTACH_DURATION", None), +def parse_arguments(args=None): + + def format_help(formatter, w=120, h=40): + """Return a wider HelpFormatter, if possible.""" + try: + kwargs = {"width": w, "max_help_position": h} + formatter(None, **kwargs) + return lambda prog: formatter(prog, **kwargs) + except TypeError: + return formatter + + usage_examples = """ + +%(prog)s, e.g. + + $ rocprof-attach -p -t [-a -d ] + $ rocprof-attach -p 12345 -t path/to/your-tool-library.so -d 5000 + +""" + parser = argparse.ArgumentParser( + description="rocprofiler-sdk attachment profiler for custom tool libraries ", + usage="%(prog)s [options] ", + epilog=usage_examples, + formatter_class=format_help(argparse.RawTextHelpFormatter), + ) + + parser.add_argument( + "-p", + "--pid", + "--attach", + help="""Attachment target's process identifier (PID). + Can also be specified in environment variable ROCPROF_ATTACH_PID. This option overrides the environment variable if both are set.""", + type=int, + required=False, + default=os.environ.get("ROCPROF_ATTACH_PID", None), + ) + + parser.add_argument( + "-t", + "--attach-tool-library", + help="""Comma delimited list of tool libraries to use during attachment. + Can also be specified in environment variable ROCPROF_ATTACH_TOOL_LIBRARY. This option overrides the environment variable if both are set.""", + type=str, + required=False, + default=os.environ.get("ROCPROF_ATTACH_TOOL_LIBRARY", None), + ) + + parser.add_argument( + "-d", + "--attach-duration-msec", + help="""Sets the amount of time in milliseconds the profiler will be attached before detaching. When unset, the profiler will wait until Enter is pressed or SIGINT (Ctrl+C) to detach. + Can also be specified in environment variable ROCPROF_ATTACH_DURATION. This option overrides the environment variable if both are set.""", + type=int, + required=False, + default=os.environ.get("ROCPROF_ATTACH_DURATION", None), + ) + + advanced_options = parser.add_argument_group("Advanced options") + + advanced_options.add_argument( + "--attach-library", + help="""Library used to attach and detach from the target process. Default will work for nearly all configurations. + Defaults to rocprofiler-sdk-rocattach.so from this ROCm install, i.e. /lib/rocprofiler-sdk-rocattach.so + Can also be specified in environment variable ROCPROF_ATTACH_LIBRARY. This option overrides the environment variable if both are set.""", + type=str, + required=False, + default=os.environ.get("ROCPROF_ATTACH_LIBRARY", ROCPROF_ATTACH_LIBRARY), + ) + return parser.parse_args(args) + + +def attach( + pid, + attach_tool_library, + attach_duration_msec, + attach_library=ROCPROF_ATTACH_LIBRARY, ): + if pid is None: - raise RuntimeError("rocprof_attach called with no PID specified") + raise RuntimeError("rocprof-attach called with no PID specified") + if attach_tool_library is None: + raise RuntimeError("rocprof-attach called with no tool libraries specified") + + tool_libraries_tokens = attach_tool_library.split(":") + for lib in tool_libraries_tokens: + if not os.path.exists(lib): + raise RuntimeError(f"rocprof-attach could not find tool library {lib}") + + # Program option overrides environment variable. This is consumed by rocprofiler-sdk on the target program side. + os.environ["ROCPROF_ATTACH_TOOL_LIBRARY"] = attach_tool_library print(f"Attaching to PID {pid} using library {attach_library}") # Load the shared library into ctypes and attach try: c_lib = ctypes.CDLL(attach_library) - c_lib.attach.restype = ctypes.c_int - c_lib.attach.argtypes = [ctypes.c_uint] - attach_status = c_lib.attach(int(pid)) + c_lib.rocattach_attach.restype = ctypes.c_int + c_lib.rocattach_attach.argtypes = [ctypes.c_int] + c_lib.rocattach_detach.restype = ctypes.c_int + c_lib.rocattach_detach.argtypes = [ctypes.c_int] + attach_status = c_lib.rocattach_attach(pid) except Exception as e: raise RuntimeError(f"Exception during library load and attachment: {e}") @@ -65,7 +149,7 @@ def main( print("Detaching. Please wait, this can take up to 1-2 minutes") sys.stdout.flush() try: - detach_status = c_lib.detach() + detach_status = c_lib.rocattach_detach(int(pid)) except Exception as e: print(f"Exception during detachment: {e}") @@ -83,17 +167,30 @@ def main( signal.signal(signal.SIGINT, signal_handler) - if duration is None: + if attach_duration_msec is None: sys.stdout.write("Press Enter to detach...") sys.stdout.flush() # Force the prompt to appear immediately input() # Now wait for input else: - print(f"Attaching for {duration} msec...\n") + print(f"Attaching for {attach_duration_msec} msec...\n") sys.stdout.flush() - time.sleep(int(duration) / 1000) + time.sleep(int(attach_duration_msec) / 1000) detach() +def main(cmd_args=None): + args = parse_arguments(cmd_args) + + attach( + pid=args.pid, + attach_tool_library=args.attach_tool_library, + attach_duration_msec=args.attach_duration_msec, + attach_library=args.attach_library, + ) + return 0 + + if __name__ == "__main__": - main() + ec = main(sys.argv[1:]) + sys.exit(ec) diff --git a/projects/rocprofiler-sdk/source/bin/rocprofv3.py b/projects/rocprofiler-sdk/source/bin/rocprofv3.py index e7fbd35c19..b8a0b0bb3b 100755 --- a/projects/rocprofiler-sdk/source/bin/rocprofv3.py +++ b/projects/rocprofiler-sdk/source/bin/rocprofv3.py @@ -1158,7 +1158,9 @@ def run(app_args, args, **kwargs): ROCPROF_LIST_AVAIL_TOOL_LIBRARY = ( f"{ROCM_DIR}/lib/rocprofiler-sdk/librocprofv3-list-avail.so" ) - ROCPROF_ATTACH_TOOL_LIBRARY = f"{ROCM_DIR}/lib/librocprofiler-sdk-rocattach.so" + ROCPROF_ATTACH_TOOL_LIBRARY = ( + f"{ROCM_DIR}/lib/rocprofiler-sdk/librocprofiler-sdk-tool.so" + ) ROCPROF_TOOL_LIBRARY = resolve_library_path(ROCPROF_TOOL_LIBRARY, args) ROCPROF_SDK_LIBRARY = resolve_library_path(ROCPROF_SDK_LIBRARY, args) diff --git a/projects/rocprofiler-sdk/source/docs/api-reference/process_attachment.rst b/projects/rocprofiler-sdk/source/docs/api-reference/process_attachment.rst index 0d543698d1..8f9924bbf7 100644 --- a/projects/rocprofiler-sdk/source/docs/api-reference/process_attachment.rst +++ b/projects/rocprofiler-sdk/source/docs/api-reference/process_attachment.rst @@ -13,35 +13,51 @@ Overview This document provides the technical details needed to implement a process attachment tool similar to ``rocprofv3 --attach``. Process attachment allows profiling tools to dynamically attach to running GPU applications without requiring application restart. The implementation can use either the provided python or exported C functions. +Direct Python Execution +=================================== + +The python file ``rocprof-attach`` can be called directly to attach to a specific PID and use custom tools within the attachment target. + +.. code-block:: bash + + $ rocprof-attach -p 12345 -t path/to/your-tool-library.so -d 5000 + +In this example, the process with PID 12345 will be attached to and the library path/to/your-tool-library.so will be loaded by rocprofiler-sdk from within that process. After 5000 milliseconds have passed, detach will be called and ``rocprof-attach`` will exit when detachment is complete. + +More information can be found by invoking ``rocprof-attach -h`` + + Python Functions =================================== -The python file ``rocprof-attach`` defines a main function that can be used for attachment: +The python file ``rocprof-attach`` defines an attach function that can be used for attachment: .. code-block:: python - def main( - pid=os.environ.get("ROCPROF_ATTACH_PID", None), - attach_library=os.environ.get( - "ROCPROF_ATTACH_TOOL_LIBRARY", ROCPROF_ATTACH_TOOL_LIBRARY - ), - duration=os.environ.get("ROCPROF_ATTACH_DURATION", None), - ) + def attach( + pid, + attach_tool_library, + attach_duration_msec, + attach_library=ROCPROF_ATTACH_LIBRARY, + ): **Function Details** -The main function performs the entire attachment process, including attaching and detaching, and provides the ability to use custom tools. It also has simple control flow intended for direct calling from python. For more complex control, it is recommended to instead use the explicit attach and detach functions provided by the ``librocprofiler-sdk-rocattach.so`` binary. +The attach function performs the entire attachment process, including attaching and detaching, and provides the ability to use custom tools via the tool_libraries parameter. It also has simple control flow intended for direct calling from python. For more complex control, it is recommended to instead use the explicit attach and detach functions provided by the ``librocprofiler-sdk-rocattach.so`` binary. **Parameters** - **pid**: Required - PID of process to attach to - Defaults to environment variable ROCPROF_ATTACH_PID -- **attach_library**: Optional - Colon delimited list of tool libraries to use +- **attach_tool_library**: Colon delimited list of tool libraries to use - Defaults to environment variable ROCPROF_ATTACH_TOOL_LIBRARY - - If unspecified, defaults to the absolute path of librocprofiler-sdk-rocattach.so -- **duration**: Optional - Length of time in milliseconds to profile for +- **attach_duration_msec**: Optional - Length of time in milliseconds to profile for - Defaults to environment variable ROCPROF_ATTACH_DURATION - If unspecified, attachment will run until Enter is pressed or SIGINT (Ctrl+C) is received +- **attach_tool_library**: Optional - Tool library to use for attachment and detachment + - Default will work for nearly all applications + - Defaults to environment variable ROCPROF_ATTACH_LIBRARY + - If unspecified, defaults to the absolute path of librocprofiler-sdk-rocattach.so C Functions =================================== @@ -55,7 +71,7 @@ The C library ``librocprofiler-sdk-rocattach.so`` defines an attach and detach f void attach(uint32_t pid) ROCPROFILER_EXPORT; // Detach from target process and cleanup - void detach() ROCPROFILER_EXPORT; + void detach(uint32_t pid) ROCPROFILER_EXPORT; } **Function Details:** @@ -65,8 +81,10 @@ The C library ``librocprofiler-sdk-rocattach.so`` defines an attach and detach f - Initiates ptrace-based attachment sequence - Custom tool libraries can be specified in a colon delimited list with the environment variable ROCPROF_ATTACH_TOOL_LIBRARY -- **detach()**: Entry point for detaching from the target process +- **detach(uint32_t pid)**: Entry point for detaching from the target process + - Takes the target process ID as a parameter - Cleans up attachment resources and terminates profiling + - A PID of 0 can be specified to detach from all processes Function Call Sequence ====================== diff --git a/projects/rocprofiler-sdk/source/include/CMakeLists.txt b/projects/rocprofiler-sdk/source/include/CMakeLists.txt index eb0f78e63c..53c680b64f 100644 --- a/projects/rocprofiler-sdk/source/include/CMakeLists.txt +++ b/projects/rocprofiler-sdk/source/include/CMakeLists.txt @@ -4,5 +4,6 @@ set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME "development") add_subdirectory(rocprofiler-sdk) +add_subdirectory(rocprofiler-sdk-rocattach) add_subdirectory(rocprofiler-sdk-roctx) add_subdirectory(rocprofiler-sdk-rocpd) diff --git a/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/CMakeLists.txt b/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/CMakeLists.txt new file mode 100644 index 0000000000..ad2b08340b --- /dev/null +++ b/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/CMakeLists.txt @@ -0,0 +1,18 @@ +# +# +# Installation of public headers +# +# +configure_file(${CMAKE_CURRENT_LIST_DIR}/version.h.in + ${CMAKE_CURRENT_BINARY_DIR}/version.h @ONLY) + +set(ROCATTACH_HEADER_FILES + # core headers + rocattach.h + # secondary headers + defines.h types.h ${CMAKE_CURRENT_BINARY_DIR}/version.h) + +install( + FILES ${ROCATTACH_HEADER_FILES} + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/rocprofiler-sdk-rocattach + COMPONENT rocattach) diff --git a/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/defines.h b/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/defines.h new file mode 100644 index 0000000000..d4f8b6dc02 --- /dev/null +++ b/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/defines.h @@ -0,0 +1,116 @@ +// MIT License +// +// Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +/** + * @defgroup SYMBOL_VERSIONING_GROUP Symbol Versions + * + * @brief The names used for the shared library versioned symbols. + * + * Every function is annotated with one of the version macros defined in this + * section. Each macro specifies a corresponding symbol version string. After + * dynamically loading the shared library with @p dlopen, the address of each + * function can be obtained using @p dlsym with the name of the function and + * its corresponding symbol version string. An error will be reported by @p + * dlvsym if the installed library does not support the version for the + * function specified in this version of the interface. + * + * @{ + */ + +/** + * @brief The function was introduced in version 0.0 of the interface and has the + * symbol version string of ``"ROCPROFILER_SDK_ROCATTACH_0.0"``. + */ +#define ROCPROFILER_SDK_ROCATTACH_VERSION_0_0 + +/** @} */ + +#if !defined(ROCATTACH_ATTRIBUTE) +# if defined(_MSC_VER) +# define ROCATTACH_ATTRIBUTE(...) __declspec(__VA_ARGS__) +# else +# define ROCATTACH_ATTRIBUTE(...) __attribute__((__VA_ARGS__)) +# endif +#endif + +#if !defined(ROCATTACH_PUBLIC_API) +# if defined(_MSC_VER) +# define ROCATTACH_PUBLIC_API ROCATTACH_ATTRIBUTE(dllexport) +# else +# define ROCATTACH_PUBLIC_API ROCATTACH_ATTRIBUTE(visibility("default")) +# endif +#endif + +#if !defined(ROCATTACH_HIDDEN_API) +# if defined(_MSC_VER) +# define ROCATTACH_HIDDEN_API +# else +# define ROCATTACH_HIDDEN_API ROCATTACH_ATTRIBUTE(visibility("hidden")) +# endif +#endif + +#if !defined(ROCATTACH_EXPORT_DECORATOR) +# define ROCATTACH_EXPORT_DECORATOR ROCATTACH_PUBLIC_API +#endif + +#if !defined(ROCATTACH_IMPORT_DECORATOR) +# if defined(_MSC_VER) +# define ROCATTACH_IMPORT_DECORATOR ROCATTACH_ATTRIBUTE(dllimport) +# else +# define ROCATTACH_IMPORT_DECORATOR +# endif +#endif + +#define ROCATTACH_EXPORT ROCATTACH_EXPORT_DECORATOR +#define ROCATTACH_IMPORT ROCATTACH_IMPORT_DECORATOR + +#if !defined(ROCATTACH_API) +# if defined(ROCATTACH_EXPORTS) +# define ROCATTACH_API ROCATTACH_EXPORT +# else +# define ROCATTACH_API ROCATTACH_IMPORT +# endif +#endif + +#if defined(__has_attribute) +# if __has_attribute(nonnull) +# define ROCATTACH_NONNULL(...) __attribute__((nonnull(__VA_ARGS__))) +# else +# define ROCATTACH_NONNULL(...) +# endif +#else +# if defined(__GNUC__) +# define ROCATTACH_NONNULL(...) __attribute__((nonnull(__VA_ARGS__))) +# else +# define ROCATTACH_NONNULL(...) +# endif +#endif + +#ifdef __cplusplus +# define ROCATTACH_EXTERN_C_INIT extern "C" { +# define ROCATTACH_EXTERN_C_FINI } +#else +# define ROCATTACH_EXTERN_C_INIT +# define ROCATTACH_EXTERN_C_FINI +#endif diff --git a/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/rocattach.h b/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/rocattach.h new file mode 100644 index 0000000000..4ca39d2eed --- /dev/null +++ b/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/rocattach.h @@ -0,0 +1,136 @@ +// MIT License +// +// Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +/** + * @file rocattach.h + * @brief rocAttach API interface for AMD profiling data analysis + * + * @mainpage rocAttach API Specification + * + */ + +#include "rocprofiler-sdk-rocattach/defines.h" +#include "rocprofiler-sdk-rocattach/types.h" + +/** + * @defgroup VERSIONING_GROUP Library Versioning + * @brief Version information about the interface and the associated installed library. + * + * The semantic version of the interface following semver.org rules. A context + * that uses this interface is only compatible with the installed library if + * the major version numbers match and the interface minor version number is + * less than or equal to the installed library minor version number. + * + * @{ + */ + +#include "rocprofiler-sdk-rocattach/version.h" + +ROCATTACH_EXTERN_C_INIT + +/** + * @fn rocattach_status_t rocattach_get_version(uint32_t* major, uint32_t* minor, uint32_t* + * patch) + * @brief Query the version of the installed library. + * + * Returns the version of the rocprofiler-sdk library loaded at runtime. This can be used to check + * if the runtime version is equal to or compatible with the version of rocprofiler-sdk used during + * compilation time. This function can be invoked before tool initialization. + * + * @param [out] major The major version number is stored if non-NULL. + * @param [out] minor The minor version number is stored if non-NULL. + * @param [out] patch The patch version number is stored if non-NULL. + * @return ::rocattach_status_t + * @retval ::ROCATTACH_STATUS_SUCCESS Always returned + */ +rocattach_status_t +rocattach_get_version(uint32_t* major, uint32_t* minor, uint32_t* patch) ROCATTACH_API; + +/** + * @brief Simplified alternative to ::rocattach_get_version + * + * Returns the version of the rocprofiler-sdk library loaded at runtime. This can be used to check + * if the runtime version is equal to or compatible with the version of rocprofiler-sdk used during + * compilation time. This function can be invoked before tool initialization. + * + * @param [out] info Pointer to version triplet struct which will be populated by the function call. + * @return ::rocattach_status_t + * @retval ::ROCATTACH_STATUS_SUCCESS Always returned + */ +rocattach_status_t +rocattach_get_version_triplet(rocattach_version_triplet_t* info) ROCATTACH_API ROCATTACH_NONNULL(1); + +/** + * @brief Attach to a process ID + * + * Attempts to attach to a rocm process at the given process identifier (PID). If successful, the + * target process will then load rocprofiler-sdk, which will subsequently load any tool libraries + * given in the environment variable ROCP_TOOL_LIBRARIES. + * + * @param [in] pid Process ID to attach to + * @return ::rocattach_status_t + * @retval ::ROCATTACH_STATUS_SUCCESS Attachment successful + */ +rocattach_status_t +rocattach_attach(int pid) ROCATTACH_API; + +/** + * @brief Detach a previous attachment + * + * Detaches from a previous + * + * @param [in] pid Process ID to detach from + * @return ::rocattach_status_t + * @retval ::ROCATTACH_STATUS_SUCCESS Attachment successful + */ +rocattach_status_t +rocattach_detach(int pid) ROCATTACH_API; + +/** + * @defgroup MISCELLANEOUS_GROUP Miscellaneous Utility Functions + * @brief utility functions for library + * @{ + */ + +/** + * @fn const char* rocattach_get_status_name(rocattach_status_t status) + * @brief Return the string encoding of ::rocattach_status_t value + * @param [in] status error code value + * @return Will return a nullptr if invalid/unsupported ::rocattach_status_t value is provided. + */ +const char* +rocattach_get_status_name(rocattach_status_t status) ROCATTACH_API; + +/** + * @fn const char* rocattach_get_status_string(rocattach_status_t status) + * @brief Return the message associated with ::rocattach_status_t value + * @param [in] status error code value + * @return Will return a nullptr if invalid/unsupported ::rocattach_status_t value is provided. + */ +const char* +rocattach_get_status_string(rocattach_status_t status) ROCATTACH_API; + +/** @} */ + +ROCATTACH_EXTERN_C_FINI diff --git a/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/types.h b/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/types.h new file mode 100644 index 0000000000..7abba406e8 --- /dev/null +++ b/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/types.h @@ -0,0 +1,81 @@ +// MIT License +// +// Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include "rocprofiler-sdk-rocattach/defines.h" + +#include + +/** @defgroup DATA_TYPE rocAttach Data types + * + * Data types defined or aliased by rocAttach + * + * @{ + */ + +//--------------------------------------------------------------------------------------// +// +// ENUMERATIONS +// +//--------------------------------------------------------------------------------------// + +/** + * @defgroup BASIC_DATA_TYPES Basic data types + * @brief Basic data types and typedefs + * + * @{ + */ + +/** + * @brief Status codes. + */ +typedef enum rocattach_status_t // NOLINT(performance-enum-size) +{ + ROCATTACH_STATUS_SUCCESS = 0, ///< No error occurred + ROCATTACH_STATUS_ERROR, ///< Generalized error + ROCATTACH_STATUS_ERROR_INVALID_ARGUMENT, ///< Invalid function argument + ROCATTACH_STATUS_ERROR_NOT_SUPPORTED, ///< Attachment is not supported on this platform + ROCATTACH_STATUS_ERROR_PTRACE_ERROR, ///< General ptrace error + ROCATTACH_STATUS_ERROR_PTRACE_OPERATION_NOT_PERMITTED, ///< ptrace returned EPERM, operation + ///< not permitted + ROCATTACH_STATUS_ERROR_PTRACE_PROCESS_NOT_FOUND, ///< ptrace returned ESRCH, no such process + ROCATTACH_STATUS_LAST, +} rocattach_status_t; + +//--------------------------------------------------------------------------------------// +// +// STRUCTS +// +//--------------------------------------------------------------------------------------// + +/** + * @brief Versioning info. + */ +typedef struct rocattach_version_triplet_t +{ + uint32_t major; + uint32_t minor; + uint32_t patch; +} rocattach_version_triplet_t; + +/** @} */ diff --git a/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/version.h.in b/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/version.h.in new file mode 100644 index 0000000000..b00593b305 --- /dev/null +++ b/projects/rocprofiler-sdk/source/include/rocprofiler-sdk-rocattach/version.h.in @@ -0,0 +1,115 @@ +// MIT License +// +// Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +/** + * @def ROCATTACH_IS_ROCPROFILER_SDK + * @brief Preprocessor define indicating the rocattach header is a rocprofiler-sdk project + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_VERSION_MAJOR + * @brief The major version of the interface as a macro so it can be used + * by the preprocessor. + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_VERSION_MINOR + * @brief The minor version of the interface as a macro so it can be used + * by the preprocessor. + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_VERSION_PATCH + * @brief The patch version of the interface as a macro so it can be used + * by the preprocessor. + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_VERSION + * @brief Numerically increasing version number encoding major, minor, and patch via + computing `((10000 * ) + (100 * ) + )`. + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_SOVERSION + * @brief Shared object versioning value whose value is at least `(10000 * )`. + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_VERSION_STRING + * @brief Version string in form: `..`. + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_GIT_DESCRIBE + * @brief String encoding of `git describe --tags` when rocprofiler was built. + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_GIT_REVISION + * @brief String encoding of `git rev-parse HEAD` when rocprofiler was built. + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_LIBRARY_ARCH + * @brief Architecture triplet of rocprofiler build. + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_SYSTEM_NAME + * @brief Target operating system for rocprofiler build, e.g. Linux. + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_SYSTEM_PROCESSOR + * @brief Target architecture for rocprofiler build. + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_SYSTEM_VERSION + * @brief Version of the operating system which built rocprofiler + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_COMPILER_ID + * @brief C++ compiler identifier which built rocprofiler, e.g., GNU + * @addtogroup VERSIONING_GROUP + * + * @def ROCATTACH_COMPILER_VERSION + * @brief C++ compiler version which built rocprofiler + * @addtogroup VERSIONING_GROUP + */ + +#define ROCATTACH_IS_ROCPROFILER_SDK 1 + +// clang-format off +#define ROCATTACH_VERSION_MAJOR @PROJECT_VERSION_MAJOR@ +#define ROCATTACH_VERSION_MINOR @PROJECT_VERSION_MINOR@ +#define ROCATTACH_VERSION_PATCH @PROJECT_VERSION_PATCH@ +#define ROCATTACH_SOVERSION (10000 * @PROJECT_VERSION_MAJOR@) +#define ROCATTACH_VERSION_STRING "@FULL_VERSION_STRING@" +#define ROCATTACH_GIT_DESCRIBE "@ROCPROFILER_SDK_GIT_DESCRIBE@" +#define ROCATTACH_GIT_REVISION "@ROCPROFILER_SDK_GIT_REVISION@" + +// system info during compilation +#define ROCATTACH_LIBRARY_ARCH "@CMAKE_LIBRARY_ARCHITECTURE@" +#define ROCATTACH_SYSTEM_NAME "@CMAKE_SYSTEM_NAME@" +#define ROCATTACH_SYSTEM_PROCESSOR "@CMAKE_SYSTEM_PROCESSOR@" +#define ROCATTACH_SYSTEM_VERSION "@CMAKE_SYSTEM_VERSION@" + +// compiler information +#define ROCATTACH_COMPILER_ID "@CMAKE_CXX_COMPILER_ID@" +#define ROCATTACH_COMPILER_VERSION "@CMAKE_CXX_COMPILER_VERSION@" + +// clang-format on + +#define ROCATTACH_VERSION \ + ((10000 * ROCATTACH_VERSION_MAJOR) + (100 * ROCATTACH_VERSION_MINOR) + ROCATTACH_VERSION_PATCH) diff --git a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/CMakeLists.txt b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/CMakeLists.txt index f8e6d91550..91e5f80657 100644 --- a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/CMakeLists.txt +++ b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/CMakeLists.txt @@ -23,8 +23,15 @@ rocprofiler_activate_clang_tidy() add_library(rocprofiler-sdk-rocattach-shared-library SHARED) -target_sources(rocprofiler-sdk-rocattach-shared-library PRIVATE rocattach.cpp - ptrace_session.cpp) +add_library(rocprofiler-sdk::rocprofiler-sdk-rocattach-shared-library ALIAS + rocprofiler-sdk-rocattach-shared-library) +target_sources(rocprofiler-sdk-rocattach-shared-library + PRIVATE rocattach.cpp auxv.cpp ptrace_session.cpp symbol_lookup.cpp) +target_include_directories( + rocprofiler-sdk-rocattach-shared-library + INTERFACE $ + $ + $) target_link_libraries( rocprofiler-sdk-rocattach-shared-library @@ -40,8 +47,10 @@ set_target_properties( LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR} SOVERSION ${PROJECT_VERSION_MAJOR} VERSION ${PROJECT_VERSION} - BUILD_RPATH "\$ORIGIN:\$ORIGIN/.." - INSTALL_RPATH "\$ORIGIN:\$ORIGIN/..") + SKIP_BUILD_RPATH OFF + BUILD_RPATH "\$ORIGIN" + INSTALL_RPATH "\$ORIGIN" + DEFINE_SYMBOL ROCATTACH_EXPORTS) install( TARGETS rocprofiler-sdk-rocattach-shared-library diff --git a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/auxv.cpp b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/auxv.cpp new file mode 100644 index 0000000000..b63ea4f46a --- /dev/null +++ b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/auxv.cpp @@ -0,0 +1,88 @@ +// MIT License +// +// Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "lib/common/filesystem.hpp" +#include "lib/common/logging.hpp" + +#include "auxv.hpp" + +#include + +namespace fs = rocprofiler::common::filesystem; + +namespace rocprofiler +{ +namespace rocattach +{ +// When injecting assembly into a process, existing instructions are partially or completely +// overwritten. Other threads will continue running and may execute our injected code, resulting in +// illegal instructions, segmentation faults, or worse. A common technique to avoid this is to +// inject code at the program entry address, as this is extremely unlikely to be called again in a +// multithreaded process. To determine this address, we inspect the auxv file for the target +// process. + +// Each entry in the auxv file will match this format. +typedef struct +{ + uint64_t type; + void* value; +} auxv_pair_t; + +rocattach_status_t +get_auxv_entry(int pid, void*& entry_addr) +{ + constexpr int AT_ENTRY = 9; // Type number for program entry point + constexpr int auxv_pair_size = sizeof(auxv_pair_t); + + auto filename = fs::path{"/proc"} / std::to_string(pid) / "auxv"; + std::ifstream auxv(filename, std::ios::in | std::ios::binary); + + if(!auxv.is_open()) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Unable to open auxv file " << filename; + return ROCATTACH_STATUS_ERROR; + } + + std::vector auxv_buffer(auxv_pair_size); + + entry_addr = nullptr; + while(auxv.read(auxv_buffer.data(), auxv_pair_size) && entry_addr == nullptr) + { + auxv_pair_t* const aux = reinterpret_cast(auxv_buffer.data()); + + if(aux->type == AT_ENTRY) + { + entry_addr = aux->value; + } + } + + if(entry_addr == nullptr) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Unexpected mising AT_ENTRY for " << filename; + return ROCATTACH_STATUS_ERROR; + } + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Entry address found to be " << entry_addr << " from " + << filename; + return ROCATTACH_STATUS_SUCCESS; +} +} // namespace rocattach +} // namespace rocprofiler diff --git a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/auxv.hpp b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/auxv.hpp new file mode 100644 index 0000000000..4e0a21b2d6 --- /dev/null +++ b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/auxv.hpp @@ -0,0 +1,35 @@ +// MIT License +// +// Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include + +namespace rocprofiler +{ +namespace rocattach +{ +rocattach_status_t +get_auxv_entry(int pid, void*& entry_addr); + +} +} // namespace rocprofiler diff --git a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/ptrace_session.cpp b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/ptrace_session.cpp index aff7de6fe2..b6569ebc12 100644 --- a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/ptrace_session.cpp +++ b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/ptrace_session.cpp @@ -21,13 +21,12 @@ // SOFTWARE. #include "ptrace_session.hpp" +#include "auxv.hpp" +#include "symbol_lookup.hpp" -#include "lib/common/filesystem.hpp" #include "lib/common/logging.hpp" -#include #include -#include #include #include #include @@ -36,35 +35,15 @@ #include #include +#include #include -#define AT_ENTRY 9 /* Entry point of program */ - -// ptrace memory operations use "word length" which is dependent on system architecture. -static_assert(sizeof(void*) == 8); - -// In addition, this file uses x64 assembly which is inherently platform dependent. -#ifndef __x86_64__ -static_assert(false); -#endif - -namespace fs = rocprofiler::common::filesystem; - +namespace rocprofiler +{ +namespace rocattach +{ namespace { -/* Copied from glibc's elf.h. */ -typedef struct -{ - uint64_t a_type; /* Entry type */ - union - { - uint64_t a_val; /* Integer value */ - /* We use to have pointer elements added here. We cannot do that, - though, since it does not work when using 32-bit definitions - on 64-bit platforms and vice versa. */ - } a_un; -} Elf64_auxv_t; - // Very limited list of operations for logging only. constexpr const char* ptrace_op_name(__ptrace_request op) @@ -83,203 +62,326 @@ ptrace_op_name(__ptrace_request op) } } +// Translates ptrace errno into rocattach status errors +rocattach_status_t +convert_ptrace_error(int error) +{ + switch(error) + { + case EPERM: return ROCATTACH_STATUS_ERROR_PTRACE_OPERATION_NOT_PERMITTED; + case ESRCH: return ROCATTACH_STATUS_ERROR_PTRACE_PROCESS_NOT_FOUND; + default: return ROCATTACH_STATUS_ERROR_PTRACE_ERROR; + } +} + // Boilerplate around ptrace calls. -// If an error occurs, logs the error and returns false. +// If an error occurs, logs the error and returns an appropriate rocattach_status_t. #define PTRACE_CALL(op, pid, addr, data) \ - ROCP_TRACE << "ptrace call params(" << ptrace_op_name(op) << "(" << op << "), " << pid << ", " \ - << (uint64_t) addr << ", " << (uint64_t) data << ")"; \ + ROCP_TRACE << "[rocprofiler-sdk-rocattach] ptrace call params(" << ptrace_op_name(op) << "(" \ + << op << "), " << pid << ", " << (uint64_t) addr << ", " << (uint64_t) data << ")"; \ if(errno = 0, ptrace(op, pid, addr, data); errno != 0) \ { \ - ROCP_ERROR << "ptrace call failed. errno: " << errno << " - " << strerror(errno) \ - << " params(" << ptrace_op_name(op) << "(" << op << "), " << pid << ", " \ - << (uint64_t) addr << ", " << (uint64_t) data << ")"; \ - return false; \ + ROCP_ERROR << "[rocprofiler-sdk-rocattach] ptrace call failed. errno: " << errno << " - " \ + << strerror(errno) << ". params(" << ptrace_op_name(op) << "(" << op << "), " \ + << pid << ", " << (uint64_t) addr << ", " << (uint64_t) data << ")"; \ + return convert_ptrace_error(errno); \ } // Changes the order of parameters for PEEKDATA so it can be used like other operations. -// value should be uint64_t +// value must be uint64_t #define PTRACE_PEEK(pid, addr, read_value) \ static_assert(std::is_same::value); \ - ROCP_TRACE << "ptrace call params(PTRACE_PEEKDATA(2), " << pid << ", " << (uint64_t) addr \ - << ", 0)"; \ + ROCP_TRACE << "[rocprofiler-sdk-rocattach] ptrace call params(PTRACE_PEEKDATA(2), " << pid \ + << ", " << (uint64_t) addr << ", 0)"; \ if(errno = 0, read_value = ptrace(PTRACE_PEEKDATA, pid, addr, NULL); errno != 0) \ { \ - ROCP_ERROR << "ptrace call failed. errno: " << errno << " params(PTRACE_PEEKDATA(2), " \ - << pid << ", " << (uint64_t) addr << ", 0)"; \ - return false; \ + ROCP_ERROR << "[rocprofiler-sdk-rocattach] ptrace call failed. errno: " << errno \ + << ". params(PTRACE_PEEKDATA(2), " << pid << ", " << (uint64_t) addr << ", 0)"; \ + return convert_ptrace_error(errno); \ } -using open_modes_vec_t = std::vector; +// Helper macro for the signal_handler where cont is called but will not return inside the macro +// error is left in errno for processing +#define PTRACE_CONT_NO_RETURN(pid, addr, data) \ + ROCP_TRACE << "[rocprofiler-sdk-rocattach] ptrace call params(PTRACE_CONT(7), " << pid << ", " \ + << (uint64_t) addr << ", " << (uint64_t) data << ")"; \ + if(errno = 0, ptrace(PTRACE_CONT, pid, addr, data); errno != 0) \ + { \ + ROCP_ERROR << "[rocprofiler-sdk-rocattach] ptrace call failed. errno: " << errno \ + << ". params(PTRACE_CONT(7), " << pid << ", " << (uint64_t) addr << ", " \ + << (uint64_t) data << ")"; \ + } -void -get_auxv_entry(int pid, size_t& entry_addr) +// Helper macro for handling any rocattach_status returning call +#define ROCATTACH_CALL(func) \ + { \ + auto status = func; \ + if(status != ROCATTACH_STATUS_SUCCESS) \ + { \ + ROCP_ERROR << "[rocprofiler-sdk-rocattach] rocattach call failed. error: " << status \ + << ", invocation: " << #func; \ + return status; \ + } \ + } + +// How long to wait for a process to start or stop after a ptrace operation +// This could wait for a long time due to detach triggering (and blocking for) output generation. +// TODO: When output generation is non-blocking or shorter, reduce this +constexpr size_t PTRACE_BREAKPOINT_TIMEOUT_MS = 1800000; +// How long to wait for the signal handler thread to start or stop +constexpr size_t PTRACE_HANDLER_START_STOP_TIMEOUT_MS = 10000; + +template +bool +wait_for_ne(std::atomic& flag, T condition, size_t timeout_ms) { - char filename[PATH_MAX]; - int fd{}; - const int auxv_size = sizeof(Elf64_auxv_t); - char buf[sizeof(Elf64_auxv_t)]; /* The larger of the two. */ + auto start_time = std::chrono::steady_clock::now(); + auto timeout_duration = std::chrono::milliseconds(timeout_ms); + auto end_time = start_time + timeout_duration; - snprintf(filename, sizeof filename, "/proc/%d/auxv", pid); - - fd = open(filename, O_RDONLY); - if(fd < 0) ROCP_ERROR << "Unable to open auxv file " << filename; - - entry_addr = 0; - while(read(fd, buf, auxv_size) == auxv_size && entry_addr == 0) + while(std::chrono::steady_clock::now() < end_time) { - Elf64_auxv_t* const aux = (Elf64_auxv_t*) buf; - - if(aux->a_type == AT_ENTRY) + if(flag.load() != condition) { - entry_addr = aux->a_un.a_val; + return true; } + std::this_thread::yield(); } - - close(fd); - - if(entry_addr == 0) - { - ROCP_ERROR << "Unexpected mising AT_ENTRY for " << filename; - } - ROCP_TRACE << "Entry address found to be " << entry_addr << " from " << filename; -} - -std::optional -get_linked_path(std::string_view _name, open_modes_vec_t&& _open_modes) -{ - const open_modes_vec_t default_link_open_modes = {(RTLD_LAZY | RTLD_NOLOAD)}; - if(_name.empty()) return fs::current_path().string(); - - if(_open_modes.empty()) _open_modes = default_link_open_modes; - - void* _handle = nullptr; - bool _noload = false; - for(auto _mode : _open_modes) - { - _handle = dlopen(_name.data(), _mode); - _noload = (_mode & RTLD_NOLOAD) == RTLD_NOLOAD; - if(_handle) break; - } - - if(_handle) - { - struct link_map* _link_map = nullptr; - dlinfo(_handle, RTLD_DI_LINKMAP, &_link_map); - if(_link_map != nullptr && !std::string_view{_link_map->l_name}.empty()) - { - return fs::absolute(fs::path{_link_map->l_name}).string(); - } - if(_noload == false) dlclose(_handle); - } - - return std::nullopt; -} - -auto -get_this_library_path() -{ - auto _this_lib_path = - get_linked_path("librocprofiler-sdk-rocattach.so.1", {RTLD_NOLOAD | RTLD_LAZY}); - LOG_IF(FATAL, !_this_lib_path) << "librocprofiler-sdk-rocattach.so.1" - << " could not locate itself in the list of loaded libraries"; - return fs::path{*_this_lib_path}.parent_path().string(); -} - -void* -get_library_handle(std::string_view _lib_name) -{ - void* _lib_handle = nullptr; - - if(_lib_name.empty()) return nullptr; - - auto _lib_path = fs::path{_lib_name}; - auto _lib_path_fname = _lib_path.filename(); - auto _lib_path_abs = - (_lib_path.is_absolute()) ? _lib_path : (fs::path{get_this_library_path()} / _lib_path); - - // check to see if the rocprofiler library is already loaded - _lib_handle = dlopen(_lib_path.c_str(), RTLD_NOLOAD | RTLD_LAZY); - - if(_lib_handle) - { - LOG(INFO) << "loaded " << _lib_name << " library at " << _lib_path.string() - << " (handle=" << _lib_handle << ") via RTLD_NOLOAD | RTLD_LAZY"; - } - - // try to load with the given path - if(!_lib_handle) - { - _lib_handle = dlopen(_lib_path.c_str(), RTLD_GLOBAL | RTLD_LAZY); - - if(_lib_handle) - { - LOG(INFO) << "loaded " << _lib_name << " library at " << _lib_path.string() - << " (handle=" << _lib_handle << ") via RTLD_GLOBAL | RTLD_LAZY"; - } - } - - // try to load with the absoulte path - if(!_lib_handle) - { - _lib_path = _lib_path_abs; - _lib_handle = dlopen(_lib_path.c_str(), RTLD_GLOBAL | RTLD_LAZY); - } - - // try to load with the basename path - if(!_lib_handle) - { - _lib_path = _lib_path_fname; - _lib_handle = dlopen(_lib_path.c_str(), RTLD_GLOBAL | RTLD_LAZY); - } - - LOG(INFO) << "loaded " << _lib_name << " library at " << _lib_path.string() - << " (handle=" << _lib_handle << ")"; - - LOG_IF(WARNING, _lib_handle == nullptr) << _lib_name << " failed to load\n"; - - return _lib_handle; + // Last chance check in case we were scheduled after timeout + return flag.load() != condition; } } // namespace -namespace rocprofiler -{ -namespace attach -{ PTraceSession::PTraceSession(int _pid) : m_pid{_pid} {} -PTraceSession::~PTraceSession() -{ - if(m_attached) - { - detach(); - } -} +PTraceSession::~PTraceSession() { detach(); } bool +PTraceSession::is_supported() +{ + // This file uses x64 assembly which is inherently platform dependent. +#ifdef __x86_64__ + const bool arch_supported = true; +#else + const bool arch_supported = false; +#endif + // ptrace memory operations use "word length" which is dependent on system architecture. + const bool word_size_supported = (sizeof(void*) == 8); + + return (arch_supported && word_size_supported); +} + +rocattach_status_t PTraceSession::attach() { + if(m_state != PTRACE_SESSION_STATE_INITIAL) + { + return ROCATTACH_STATUS_ERROR; + } + // SEIZE attaches without stopping the process PTRACE_CALL(PTRACE_SEIZE, m_pid, NULL, NULL); - ROCP_INFO << "Successfully attached to pid " << m_pid; - m_attached = true; - return true; + ROCP_INFO << "[rocprofiler-sdk-rocattach] Successfully attached to pid " << m_pid; + ROCATTACH_CALL(start_signal_handler()); + m_state = PTRACE_SESSION_STATE_RUNNING; + return ROCATTACH_STATUS_SUCCESS; } -bool +rocattach_status_t PTraceSession::detach() { - m_attached = false; + if(m_state == PTRACE_SESSION_STATE_INITIAL || m_state == PTRACE_SESSION_STATE_DETACHED) + { + return ROCATTACH_STATUS_ERROR; + } + + if(m_state == PTRACE_SESSION_STATE_RUNNING) + { + // Must be stopped to use PTRACE_DETACH + stop(); + } + + ROCATTACH_CALL(stop_signal_handler()); PTRACE_CALL(PTRACE_DETACH, m_pid, NULL, NULL); - ROCP_INFO << "Detached from pid " << m_pid; - return true; + m_state = PTRACE_SESSION_STATE_DETACHED; + ROCP_INFO << "[rocprofiler-sdk-rocattach] Detached from pid " << m_pid; + return ROCATTACH_STATUS_SUCCESS; +} + +rocattach_status_t +PTraceSession::start_signal_handler() +{ + if(m_ptrace_signal_handler_state != PTRACE_SIGNAL_HANDLER_STATE_INITIAL) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] PTraceSession signal handler was in an " + "unexpected state when started"; + return ROCATTACH_STATUS_ERROR; + } + m_ptrace_signal_handler_thread = std::thread(ptrace_signal_handler_func, + m_pid, + std::ref(m_ptrace_signal_handler_state), + std::ref(m_ptrace_signal_handler_error)); + if(!wait_for_ne(m_ptrace_signal_handler_state, + PTRACE_SIGNAL_HANDLER_STATE_INITIAL, + PTRACE_HANDLER_START_STOP_TIMEOUT_MS)) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] PTraceSession signal handler failed to start"; + return ROCATTACH_STATUS_ERROR; + } + return ROCATTACH_STATUS_SUCCESS; +} + +rocattach_status_t +PTraceSession::stop_signal_handler() +{ + auto status = ROCATTACH_STATUS_SUCCESS; + + // The lock-step provided in attach should prevent a state where state is RUNNING and + // the signal handler state is INITIAL. Accessing the atomic twice here is OK because + // of that lock-step. + if(m_ptrace_signal_handler_state.load() == PTRACE_SIGNAL_HANDLER_STATE_INITIAL) + { + // The signal handler thread was never started. + m_ptrace_signal_handler_state.store(PTRACE_SIGNAL_HANDLER_STATE_FINAL); + } + else if(m_ptrace_signal_handler_state.load() != PTRACE_SIGNAL_HANDLER_STATE_FINAL) + { + m_ptrace_signal_handler_state.store(PTRACE_SIGNAL_HANDLER_STATE_DETACHING); + if(!wait_for_ne(m_ptrace_signal_handler_state, + PTRACE_SIGNAL_HANDLER_STATE_DETACHING, + PTRACE_HANDLER_START_STOP_TIMEOUT_MS)) + { + status = m_ptrace_signal_handler_error.load(); + ROCP_ERROR << "[rocprofiler-sdk-rocattach] PTraceSession signal handler failed to stop " + "when requested. Last status: " + << status; + return status; + } + m_ptrace_signal_handler_thread.join(); + status = m_ptrace_signal_handler_error.load(); + } + return status; +} + +// While we are attached, we must monitor the target process for: +// - Process exits (WIFEXITED) +// - Process is killed (WIFSIGNALED) +// - Process is stopped (WIFSTOPPED) +// When the process exits or is killed, we simply report the status change and end the signal +// handling function. When the process is stopped, we use our current state to determine what to do +// - If ATTACHED, call PTRACE_CONT with the signal to resume the process +// - If WAITING_FOR_BREAKPOINT, leave the process stopped and transition our state to ATTACHED to +// signal to the main thread which is awaiting a breakpoint +// See the comment on ptrace_session_signal_handler_state_t for more information. +void +PTraceSession::ptrace_signal_handler_func( + int _pid, + std::atomic& _state, + std::atomic& _error) +{ + _state.store(PTRACE_SIGNAL_HANDLER_STATE_ATTACHED); + while(_state.load() != PTRACE_SIGNAL_HANDLER_STATE_DETACHING) + { + int status{0}; + int retval{0}; + + // make a non-blocking call to waitpid to check on our tracee process + retval = waitpid(_pid, &status, WNOHANG); + // if retval is 0, no error occured and no state change was observed + if(retval == 0) + { + std::this_thread::yield(); + continue; + } + else if(retval == -1) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] waitpid failed in " + "ptrace_signal_handler_func for pid " + << _pid; + _error.store(ROCATTACH_STATUS_ERROR); + _state.store(PTRACE_SIGNAL_HANDLER_STATE_FINAL); + return; + } + + if(status != 0 && WIFEXITED(status)) + { + // Process exited normally, report status and end this thread. + ROCP_ERROR << "[rocprofiler-sdk-rocattach] process " << _pid + << " exited, status=" << WEXITSTATUS(status); + _error.store(ROCATTACH_STATUS_SUCCESS); + _state.store(PTRACE_SIGNAL_HANDLER_STATE_FINAL); + return; + } + else if(status != 0 && WIFSIGNALED(status)) + { + // Process was killed, report signal and end this thread. + ROCP_ERROR << "[rocprofiler-sdk-rocattach] process " << _pid << " killed by signal " + << WTERMSIG(status); + _error.store(ROCATTACH_STATUS_SUCCESS); + _state.store(PTRACE_SIGNAL_HANDLER_STATE_FINAL); + return; + } + else if(status != 0 && WIFSTOPPED(status)) + { + // Process was stopped, handle the signal + auto sig = WSTOPSIG(status); + ROCP_TRACE << "[rocprofiler-sdk-rocattach] process " << _pid << " stopped by signal " + << sig; + // If the signal is SIGTRAP (5) AND we were expecting a breakpoint, change state to + // signal the update, otherwise continue to forward it to the process. NOTE: If our + // injection causes a SIGSEGV, we can technically recover by handling the signal and + // restoring the code, which would allow the user code to continue normally. For now, + // this will crash the user app. + ptrace_session_signal_handler_state_t expected_state = + PTRACE_SIGNAL_HANDLER_STATE_WAITING_FOR_BREAKPOINT; + if(sig == SIGTRAP && + _state.compare_exchange_strong(expected_state, PTRACE_SIGNAL_HANDLER_STATE_ATTACHED)) + { + ROCP_TRACE << "[rocprofiler-sdk-rocattach] process " << _pid + << " hit expected breakpoint."; + } + else + { + PTRACE_CONT_NO_RETURN(_pid, NULL, sig); + if(errno) + { + _error.store(convert_ptrace_error(errno)); + _state.store(PTRACE_SIGNAL_HANDLER_STATE_FINAL); + return; + } + } + } + + std::this_thread::yield(); + } + + // While loop ended because we are detaching, close out gracefully. + _error.store(ROCATTACH_STATUS_SUCCESS); + _state.store(PTRACE_SIGNAL_HANDLER_STATE_FINAL); +} + +rocattach_status_t +PTraceSession::write(size_t addr, const std::vector& data, size_t size) +{ + if(m_state != PTRACE_SESSION_STATE_RUNNING) + { + // If process is already stopped, use write_internal instead. + return ROCATTACH_STATUS_ERROR; + } + + ROCATTACH_CALL(stop()); + ROCATTACH_CALL(write_internal(addr, data, size)); + ROCATTACH_CALL(cont()); + return ROCATTACH_STATUS_SUCCESS; } // pre-cond: process must be stopped -bool -PTraceSession::write(size_t addr, const std::vector& data, size_t size) const +rocattach_status_t +PTraceSession::write_internal(size_t addr, const std::vector& data, size_t size) const { + // Write each word one at a time constexpr size_t word_size = sizeof(void*); size_t word_iter = 0; for(word_iter = 0; word_iter < (size / word_size); ++word_iter) @@ -290,7 +392,7 @@ PTraceSession::write(size_t addr, const std::vector& data, size_t size) PTRACE_CALL(PTRACE_POKEDATA, m_pid, addr + offset, word); } - // If not divisible, get the last word to do a partial write correctly. + // If not evenly divisible, read the last word to do a masked partial write. size_t remainder = size % word_size; if(remainder != 0u) { @@ -300,14 +402,30 @@ PTraceSession::write(size_t addr, const std::vector& data, size_t size) std::memcpy(&last_word, data.data() + offset, remainder); PTRACE_CALL(PTRACE_POKEDATA, m_pid, addr + offset, last_word); } - ROCP_TRACE << "ptrace wrote " << size << " bytes at " << addr; - return true; + ROCP_TRACE << "[rocprofiler-sdk-rocattach] ptrace wrote " << size << " bytes at " << addr; + return ROCATTACH_STATUS_SUCCESS; +} + +rocattach_status_t +PTraceSession::read(size_t addr, std::vector& data, size_t size) +{ + if(m_state != PTRACE_SESSION_STATE_RUNNING) + { + // If process is already stopped, use read_internal instead. + return ROCATTACH_STATUS_ERROR; + } + + ROCATTACH_CALL(stop()); + ROCATTACH_CALL(read_internal(addr, data, size)); + ROCATTACH_CALL(cont()); + return ROCATTACH_STATUS_SUCCESS; } // pre-cond: process must be stopped -bool -PTraceSession::read(size_t addr, std::vector& data, size_t size) const +rocattach_status_t +PTraceSession::read_internal(size_t addr, std::vector& data, size_t size) const { + // Read each word one at a time data.clear(); data.resize(size); constexpr size_t word_size = sizeof(void*); @@ -319,6 +437,8 @@ PTraceSession::read(size_t addr, std::vector& data, size_t size) const PTRACE_PEEK(m_pid, addr + offset, word); std::memcpy(data.data() + offset, &word, word_size); } + + // If not evenly divisible, read the last word and mask off the remainder size_t remainder = size % word_size; if(remainder != 0u) { @@ -327,61 +447,173 @@ PTraceSession::read(size_t addr, std::vector& data, size_t size) const PTRACE_PEEK(m_pid, addr + offset, last_word); std::memcpy(data.data() + offset, &last_word, remainder); } - ROCP_TRACE << "ptrace read " << size << " bytes at " << addr; - return true; + ROCP_TRACE << "[rocprofiler-sdk-rocattach] ptrace read " << size << " bytes at " << addr; + return ROCATTACH_STATUS_SUCCESS; } -// pre-cond: process must be stopped -bool +rocattach_status_t PTraceSession::swap(size_t addr, const std::vector& in_data, std::vector& out_data, - size_t size) const + size_t size) { - if(!read(addr, out_data, size)) + if(m_state != PTRACE_SESSION_STATE_RUNNING) { - return false; + // If process is already stopped, use swap_internal instead. + return ROCATTACH_STATUS_ERROR; } - return write(addr, in_data, size); + + ROCATTACH_CALL(stop()); + ROCATTACH_CALL(swap_internal(addr, in_data, out_data, size)); + ROCATTACH_CALL(cont()); + return ROCATTACH_STATUS_SUCCESS; } -bool -PTraceSession::simple_mmap(void*& addr, size_t length) const +// pre-cond: process must be stopped +rocattach_status_t +PTraceSession::swap_internal(size_t addr, + const std::vector& in_data, + std::vector& out_data, + size_t size) const { - if(!m_attached) + ROCATTACH_CALL(read_internal(addr, out_data, size)); + ROCATTACH_CALL(write_internal(addr, in_data, size)); + return ROCATTACH_STATUS_SUCCESS; +} + +// Helper function which updates states and communicates with the signal handler thread to await a +// single breakpoint. Updates the state to STOPPED when complete. Returns an error if the signal +// handler or ptrace fail unexpectedly. +rocattach_status_t +PTraceSession::wait_for_breakpoint() +{ + ROCP_TRACE << "[rocprofiler-sdk-rocattach] waiting for breakpoint after trap instruction added"; + + // Enforce transition from ATTACHED to WAITING_FOR_BREAKPOINT + ptrace_session_signal_handler_state_t expected_state = PTRACE_SIGNAL_HANDLER_STATE_ATTACHED; + if(!m_ptrace_signal_handler_state.compare_exchange_strong( + expected_state, PTRACE_SIGNAL_HANDLER_STATE_WAITING_FOR_BREAKPOINT)) { - ROCP_ERROR << "simple_mmap called while not attached"; - return false; + ROCP_ERROR << "[rocprofiler-sdk-rocattach] signal handler thread was in an unexpected " + "state when waiting for stop. " + "State code: " + << expected_state; + return ROCATTACH_STATUS_ERROR; } - if(!stop()) + // Continue until breakpoint is hit + ROCATTACH_CALL(cont()); + if(!wait_for_ne(m_ptrace_signal_handler_state, + PTRACE_SIGNAL_HANDLER_STATE_WAITING_FOR_BREAKPOINT, + PTRACE_BREAKPOINT_TIMEOUT_MS)) { - return false; + auto status = m_ptrace_signal_handler_error.load(); + ROCP_ERROR << "[rocprofiler-sdk-rocattach] signal handler thread did not receive expected " + "breakpoint within timeout. Last status: " + << status; + return status; } - // Create a system call to mmap: - // mmap(NULL, length, prot, flags, -1, 0); + // If signal handler is not ATTACHED, error has occurred + if(m_ptrace_signal_handler_state.load() != PTRACE_SIGNAL_HANDLER_STATE_ATTACHED) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] signal handler thread was in an unexpected " + "state after waiting for stop. State code: " + << m_ptrace_signal_handler_state.load(); + return m_ptrace_signal_handler_error.load(); + } + + // Manually set state to stopped + // usually stop() handles this, but this stop was triggered manually in assembly code + m_state = PTRACE_SESSION_STATE_STOPPED; + return ROCATTACH_STATUS_SUCCESS; +} + +// Helper function which updates states and communicates with the signal handler thread to await a +// single stop. Updates the state to STOPPED when complete. Returns an error if the signal handler +// or ptrace fail unexpectedly. +rocattach_status_t +PTraceSession::wait_for_stop() +{ + ROCP_TRACE << "[rocprofiler-sdk-rocattach] waiting for stop after PTRACE_INTERRUPT"; + // Enforce transition from ATTACHED to WAITING_FOR_BREAKPOINT + ptrace_session_signal_handler_state_t expected_state = PTRACE_SIGNAL_HANDLER_STATE_ATTACHED; + if(!m_ptrace_signal_handler_state.compare_exchange_strong( + expected_state, PTRACE_SIGNAL_HANDLER_STATE_WAITING_FOR_BREAKPOINT)) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] signal handler thread was in an unexpected " + "state when waiting for " + "breakpoint. State code: " + << expected_state; + return ROCATTACH_STATUS_ERROR; + } + + // Call interrupt and wait until process is stopped + PTRACE_CALL(PTRACE_INTERRUPT, m_pid, NULL, NULL); + if(!wait_for_ne(m_ptrace_signal_handler_state, + PTRACE_SIGNAL_HANDLER_STATE_WAITING_FOR_BREAKPOINT, + PTRACE_BREAKPOINT_TIMEOUT_MS)) + { + auto status = m_ptrace_signal_handler_error.load(); + ROCP_ERROR << "[rocprofiler-sdk-rocattach] signal handler thread did not receive expected " + "breakpoint within timeout. Last status: " + << status; + return status; + } + + // If signal handler is not ATTACHED, error has occurred + if(m_ptrace_signal_handler_state.load() != PTRACE_SIGNAL_HANDLER_STATE_ATTACHED) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] signal handler thread was in an unexpected " + "state after waiting for breakpoint " + << m_ptrace_signal_handler_state.load(); + return m_ptrace_signal_handler_error.load(); + } + + // Set state to stopped now that process is stopped. + m_state = PTRACE_SESSION_STATE_STOPPED; + return ROCATTACH_STATUS_SUCCESS; +} + +// Makes a syscall to mmap in the target process. +// Some sensible default parameters are used that are suitable for most applications: +// prot = PROT_READ | PROT_WRITE +// flags = MAP_PRIVATE | MAP_ANONYMOUS +rocattach_status_t +PTraceSession::simple_mmap(void*& addr, size_t length) +{ + if(m_state != PTRACE_SESSION_STATE_RUNNING) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] simple_mmap called in invalid state: " + << m_state; + return ROCATTACH_STATUS_ERROR; + } + + // Stop the process + ROCATTACH_CALL(stop()); + // Get entry address for safe injection of op codes - size_t entry_addr{0}; - get_auxv_entry(m_pid, entry_addr); + void* entry_addr = nullptr; + ROCATTACH_CALL(get_auxv_entry(m_pid, entry_addr)); // Save current register file struct user_regs_struct oldregs; PTRACE_CALL(PTRACE_GETREGS, m_pid, NULL, &oldregs); - // Set register file for call + // Set register file for system call to mmap: + // mmap(addr=NULL, length, prot, flags, -1, 0); struct user_regs_struct newregs = oldregs; - newregs.rax = 9; // calling convention: syscall ID for mmap - newregs.rdi = 0; // addr + newregs.rax = 9; // calling convention: 9 is syscall ID for mmap + newregs.rdi = 0; // addr=NULL newregs.rsi = length; // length newregs.rdx = PROT_READ | PROT_WRITE; // prot newregs.r10 = MAP_PRIVATE | MAP_ANONYMOUS; // flags newregs.r8 = -1; // fd (unused) newregs.r9 = 0; // offset - newregs.rip = entry_addr; - newregs.rsp = oldregs.rsp - 128; // move sp by 128 to not clobber redlined functions - newregs.rsp -= (newregs.rsp % 16); - + newregs.rip = + reinterpret_cast(entry_addr); // safe injection addr given by get_auxv_entry + newregs.rsp = oldregs.rsp - 128; // move sp by at least 128 to not clobber redlined functions + newregs.rsp -= (newregs.rsp % 16); // base sp should be on 16-byte boundary // Set syscall registers PTRACE_CALL(PTRACE_SETREGS, m_pid, NULL, &newregs); @@ -392,80 +624,62 @@ PTraceSession::simple_mmap(void*& addr, size_t length) const std::vector old_code; // Write in new opcodes - if(!swap(entry_addr, new_code, old_code, 3)) - { - return false; - } + ROCATTACH_CALL(swap_internal(reinterpret_cast(entry_addr), new_code, old_code, 3)); - ROCP_TRACE << "Attempting to execute mmap syscall"; - // Resume execution - if(!cont()) - { - return false; - } - - // Wait for int3 breakpoint to be hit - int status; - if(waitpid(m_pid, &status, WUNTRACED) == -1) - { - return false; - } + // Execute + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Attempting to execute mmap syscall"; + ROCATTACH_CALL(wait_for_breakpoint()); // Get registers to see mmap's return values struct user_regs_struct returnregs; PTRACE_CALL(PTRACE_GETREGS, m_pid, NULL, &returnregs); // Write in old opcodes - if(!write(entry_addr, old_code, 3)) - { - return false; - } + ROCATTACH_CALL(write_internal(reinterpret_cast(entry_addr), old_code, 3)); // Restore register file PTRACE_CALL(PTRACE_SETREGS, m_pid, NULL, &oldregs); + // Restart execution - if(!cont()) - { - return false; - } + ROCATTACH_CALL(cont()); addr = reinterpret_cast(returnregs.rax); // NOLINT(performance-no-int-to-ptr) - return true; + return ROCATTACH_STATUS_SUCCESS; } -bool -PTraceSession::simple_munmap(void*& addr, size_t length) const +// Makes a syscall to munmap in the target process. +// addr and length should match (or be a subset of) addr and length given to a previous mmap call. +rocattach_status_t +PTraceSession::simple_munmap(void*& addr, size_t length) { - if(!m_attached) + if(m_state != PTRACE_SESSION_STATE_RUNNING) { - ROCP_ERROR << "simple_munmap called while not attached"; - return false; + ROCP_ERROR << "[rocprofiler-sdk-rocattach] simple_munmap called in invalid state: " + << m_state; + return ROCATTACH_STATUS_ERROR; } // Stop the process - if(!stop()) - { - return false; - } + ROCATTACH_CALL(stop()); - // Create a system call to mumap: - // mumap(NULL, length, prot, flags, -1, 0); // Get entry address for safe injection of op codes - size_t entry_addr{0}; - get_auxv_entry(m_pid, entry_addr); + void* entry_addr = nullptr; + ROCATTACH_CALL(get_auxv_entry(m_pid, entry_addr)); // Save current register file struct user_regs_struct oldregs; PTRACE_CALL(PTRACE_GETREGS, m_pid, NULL, &oldregs); - // Set register file for call + // Set register file for system call to munmap: + // munmap(addr, length); struct user_regs_struct newregs = oldregs; - newregs.rax = 11; // calling convention: syscall ID for mumap + newregs.rax = 11; // calling convention: 11 is syscall ID for mumap newregs.rdi = reinterpret_cast(addr); // addr newregs.rsi = length; // length - newregs.rip = entry_addr; - newregs.rsp = oldregs.rsp - 128; // move sp by 128 to not clobber redlined functions - newregs.rsp -= (newregs.rsp % 16); + newregs.rip = + reinterpret_cast(entry_addr); // safe injection addr given by get_auxv_entry + newregs.rsp = oldregs.rsp - 128; // move sp by at least 128 to not clobber redlined functions + newregs.rsp -= (newregs.rsp % 16); // base sp should be on 16-byte boundary // Set syscall registers PTRACE_CALL(PTRACE_SETREGS, m_pid, NULL, &newregs); @@ -476,172 +690,80 @@ PTraceSession::simple_munmap(void*& addr, size_t length) const std::vector old_code; // Write in new opcodes - if(!swap(entry_addr, new_code, old_code, 3)) - { - return false; - } + ROCATTACH_CALL(swap_internal(reinterpret_cast(entry_addr), new_code, old_code, 3)); - ROCP_TRACE << "Attempting to execute munmap syscall"; - // Restart execution - if(!cont()) - { - return false; - } + // Execute + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Attempting to execute munmap syscall"; + ROCATTACH_CALL(wait_for_breakpoint()); - // Wait for int3 breakpoint to be hit - int status; - if(waitpid(m_pid, &status, WUNTRACED) == -1) - { - return false; - } - - // Get registers to see munmap's return values + // Get registers to see mmap's return values struct user_regs_struct returnregs; PTRACE_CALL(PTRACE_GETREGS, m_pid, NULL, &returnregs); // Write in old opcodes - if(!write(entry_addr, old_code, 3)) - { - return false; - } + ROCATTACH_CALL(write_internal(reinterpret_cast(entry_addr), old_code, 3)); + // Restore register file PTRACE_CALL(PTRACE_SETREGS, m_pid, NULL, &oldregs); + // Restart execution - if(!cont()) - { - return false; - } + ROCATTACH_CALL(cont()); - return true; + return ROCATTACH_STATUS_SUCCESS; } -bool -PTraceSession::call_function(const std::string& library, const std::string& symbol) -{ - return call_function(library, symbol, nullptr); -} - -// This supports calling a dynamically loaded function with at most 1 parameter. -// More parameters could be supported, but this is good enough for now. -// Correctly implementing this would require duplicating the x64 calling convention. Probably not -// worth it. -bool +// Makes a call to library::symbol() in the target process +rocattach_status_t PTraceSession::call_function(const std::string& library, const std::string& symbol, + uint64_t& ret_value) +{ + return call_function(library, symbol, ret_value, nullptr, nullptr); +} + +// Makes a call to library::symbol(first_param) in the target process +rocattach_status_t +PTraceSession::call_function(const std::string& library, + const std::string& symbol, + uint64_t& ret_value, void* first_param) { - if(!m_attached) - { - ROCP_ERROR << "call_function called while not attached"; - return false; - } - - // Stop the process - if(!stop()) - { - return false; - } - - void* target_addr; - if(!find_symbol(target_addr, library, symbol)) - { - return false; - } - - // Get entry address for safe injection of op codes - size_t entry_addr{0}; - get_auxv_entry(m_pid, entry_addr); - - // Save current register file - struct user_regs_struct oldregs; - PTRACE_CALL(PTRACE_GETREGS, m_pid, NULL, &oldregs); - - // Construct registers to call a function with 1 parameter - // symbol(first_param) - struct user_regs_struct newregs = oldregs; - newregs.rax = reinterpret_cast(target_addr); // target function - newregs.rdi = reinterpret_cast(first_param); // first parameter - newregs.rip = entry_addr; - newregs.rsp = oldregs.rsp - 128; // move sp by 128 to not clobber redlined functions - newregs.rsp -= (newregs.rsp % 16); - - // x64 assembly to call a function by register and breakpoint when done - // ff d0 call rax - // cc int3 - std::vector new_code({0xff, 0xd0, 0xcc}); - std::vector old_code; - - // Write in new opcodes - if(!swap(entry_addr, new_code, old_code, 3)) - { - return false; - } - // Set syscall registers - PTRACE_CALL(PTRACE_SETREGS, m_pid, NULL, &newregs); - - ROCP_TRACE << "Attempting to execute " << library << "::" << symbol << "(" << first_param - << ")"; - // Restart execution - if(!cont()) - { - return false; - } - - // Wait for int3 to be hit - if(waitpid(m_pid, nullptr, WSTOPPED) == -1) - { - return false; - } - - // Get registers to see return values - struct user_regs_struct returnregs; - PTRACE_CALL(PTRACE_GETREGS, m_pid, NULL, &returnregs); - - // Write in old opcodes - if(!write(entry_addr, old_code, 3)) - { - return false; - } - // Restore register file - PTRACE_CALL(PTRACE_SETREGS, m_pid, NULL, &oldregs); - // Restart execution - if(!cont()) - { - return false; - } - - return true; + return call_function(library, symbol, ret_value, first_param, nullptr); } +// Makes a call to library::symbol(first_param, second_param) in the target process. // This supports calling a dynamically loaded function with at most 2 parameters. -// Uses x64 calling convention: RDI for first param, RSI for second param -bool +// Uses x64 calling convention: RAX for return value, RDI for first param, RSI for second param +rocattach_status_t PTraceSession::call_function(const std::string& library, const std::string& symbol, + uint64_t& ret_value, void* first_param, void* second_param) { - if(!m_attached) + if(m_state != PTRACE_SESSION_STATE_RUNNING) { - ROCP_ERROR << "call_function called while not attached"; - return false; + ROCP_ERROR << "[rocprofiler-sdk-rocattach] call_function called in invalid state: " + << m_state; + return ROCATTACH_STATUS_ERROR; } // Stop the process - if(!stop()) - { - return false; - } + ROCATTACH_CALL(stop()); + // Find address in target program to call void* target_addr = nullptr; - if(!find_symbol(target_addr, library, symbol)) + if(!find_symbol(m_pid, target_addr, library, symbol)) { - return false; + ROCP_ERROR + << "[rocprofiler-sdk-rocattach] call_function failed to find target symbol address for " + << library << "::" << symbol; + return ROCATTACH_STATUS_ERROR; } // Get entry address for safe injection of op codes - size_t entry_addr{0}; - get_auxv_entry(m_pid, entry_addr); + void* entry_addr = nullptr; + ROCATTACH_CALL(get_auxv_entry(m_pid, entry_addr)); // Save current register file struct user_regs_struct oldregs; @@ -650,12 +772,16 @@ PTraceSession::call_function(const std::string& library, // Construct registers to call a function with 2 parameters // symbol(first_param, second_param) struct user_regs_struct newregs = oldregs; - newregs.rax = reinterpret_cast(target_addr); // target function - newregs.rdi = reinterpret_cast(first_param); // first parameter - newregs.rsi = reinterpret_cast(second_param); // second parameter - newregs.rip = entry_addr; - newregs.rsp = oldregs.rsp - 128; // move sp by 128 to not clobber redlined functions - newregs.rsp -= (newregs.rsp % 16); + + newregs.rax = reinterpret_cast(target_addr); // target function + newregs.rdi = reinterpret_cast(first_param); // first parameter + newregs.rsi = reinterpret_cast(second_param); // second parameter + newregs.rip = + reinterpret_cast(entry_addr); // safe injection addr given by get_auxv_entry + newregs.rsp = oldregs.rsp - 128; // move sp by at least 128 to not clobber redlined functions + newregs.rsp -= (newregs.rsp % 16); // base sp should be on 16-byte boundary + // Set function registers + PTRACE_CALL(PTRACE_SETREGS, m_pid, NULL, &newregs); // x64 assembly to call a function by register and breakpoint when done // ff d0 call rax @@ -664,225 +790,64 @@ PTraceSession::call_function(const std::string& library, std::vector old_code; // Write in new opcodes - if(!swap(entry_addr, new_code, old_code, 3)) - { - return false; - } - // Set syscall registers - PTRACE_CALL(PTRACE_SETREGS, m_pid, NULL, &newregs); + ROCATTACH_CALL(swap_internal(reinterpret_cast(entry_addr), new_code, old_code, 3)); - ROCP_TRACE << "Attempting to execute " << library << "::" << symbol << "(" << first_param - << ", " << second_param << ")"; - // Restart execution - if(!cont()) - { - return false; - } + // Execute + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Attempting to execute " << library << "::" << symbol + << "(" << first_param << ", " << second_param << ")"; + ROCATTACH_CALL(wait_for_breakpoint()); - // Wait for int3 to be hit - if(waitpid(m_pid, nullptr, WSTOPPED) == -1) - { - return false; - } - - // Get registers to see return values + // Get registers to see return values struct user_regs_struct returnregs; PTRACE_CALL(PTRACE_GETREGS, m_pid, NULL, &returnregs); // Write in old opcodes - if(!write(entry_addr, old_code, 3)) - { - return false; - } + ROCATTACH_CALL(write_internal(reinterpret_cast(entry_addr), old_code, 3)); + // Restore register file PTRACE_CALL(PTRACE_SETREGS, m_pid, NULL, &oldregs); + // Restart execution - if(!cont()) - { - return false; - } + ROCATTACH_CALL(cont()); - return true; + ret_value = returnregs.rax; + return ROCATTACH_STATUS_SUCCESS; } -bool -PTraceSession::find_library(void*& addr, int inpid, const std::string& library) +// Calls PTRACE_STOP and waits for the stop to complete. +// Target process will be stopped after this call. +rocattach_status_t +PTraceSession::stop() { - std::stringstream searchname; - searchname << inpid << "::" << library; - // TODO: add this back - // if (target_library_addrs.find(searchname.str()) != target_library_addrs.end()) - //{ - // return target_library_addrs[searchname.str()]; - //} - - // uses "maps" file to find where library has been loaded in target process - // does not require this process to be attached - std::stringstream filename; - filename << "/proc/" << inpid << "/maps"; - std::ifstream maps(filename.str().c_str()); - - if(!maps) + if(m_state != PTRACE_SESSION_STATE_RUNNING) { - ROCP_ERROR << "Couldn't open " << filename.str(); - return false; + ROCP_ERROR << "[rocprofiler-sdk-rocattach] stop called in invalid state: " << m_state; + return ROCATTACH_STATUS_ERROR; } - std::string line; - while(std::getline(maps, line)) - { - if(line.find(library) != std::string::npos) - { - ROCP_TRACE << "entry in pid " << inpid << " maps file is: " << line; - break; - } - } + // Stop the process and update state + ROCATTACH_CALL(wait_for_stop()); - if(!maps) - { - ROCP_ERROR << "Couldn't find library " << library << " in " << filename.str(); - return false; - } - - // NOLINTNEXTLINE(performance-no-int-to-ptr) - addr = reinterpret_cast(std::stoull(line, nullptr, 16)); - // target_library_addrs[searchname.str()] = addr; - return true; + ROCP_TRACE << "[rocprofiler-sdk-rocattach] ptrace stopped pid " << m_pid; + return ROCATTACH_STATUS_SUCCESS; } -bool -PTraceSession::find_symbol(void*& addr, const std::string& library, const std::string& symbol) +// Calls PTRACE_CONT. +// Target process will be running after this call. +rocattach_status_t +PTraceSession::cont() { - auto searchname = std::stringstream{}; - searchname << library << "::" << symbol; - if(auto itr = m_target_symbol_addrs.find(searchname.str()); itr != m_target_symbol_addrs.end()) + if(m_state != PTRACE_SESSION_STATE_STOPPED) { - ROCP_TRACE << "found symbol for " << searchname.str() << " at " << itr->second; - return itr->second != nullptr; - } - - void* libraryaddr = nullptr; - void* symboladdr = nullptr; - - // Load the library in our process to determine the offset of the requested symbol from the - // start address of the library - addr = nullptr; - libraryaddr = get_library_handle(library); - - if(!libraryaddr) - { - ROCP_ERROR << "host couldn't dlopen " << library; - return false; - } - - symboladdr = dlsym(libraryaddr, symbol.c_str()); - if(!symboladdr) - { - ROCP_ERROR << "host couldn't dlsym " << symbol; - return false; - } - - // Find the start address of the library in our process - void* hostlibraryaddr; - if(!find_library(hostlibraryaddr, getpid(), library)) - { - ROCP_ERROR << "couldn't determine where " << library << " was loaded for host"; - return false; - } - - // Caluclate the offset - size_t offset = - reinterpret_cast(symboladdr) - reinterpret_cast(hostlibraryaddr); - ROCP_TRACE << "offset of " << symbol << " into " << library << " calculated as " << offset; - - // Find the start address of the library in the target process - void* targetlibraryaddr; - if(!find_library(targetlibraryaddr, m_pid, library)) - { - ROCP_ERROR << "couldn't determine where " << library << " was loaded for target"; - return false; - } - - // Calculate address of symbol in the target process using the offset - // NOLINTNEXTLINE(performance-no-int-to-ptr) - addr = reinterpret_cast(reinterpret_cast(targetlibraryaddr) + offset); - m_target_symbol_addrs[searchname.str()] = addr; - ROCP_TRACE << "found symbol for " << searchname.str() << " at " << addr; - return true; -} - -bool -PTraceSession::stop() const -{ - if(!m_attached) - { - ROCP_ERROR << "stop called while not attached"; - return false; - } - - // Stop the process - PTRACE_CALL(PTRACE_INTERRUPT, m_pid, NULL, NULL); - - // Wait for the stop - if(waitpid(m_pid, nullptr, WSTOPPED) == -1) - { - return false; - } - ROCP_TRACE << "ptrace stopped pid " << m_pid; - return true; -} - -bool -PTraceSession::cont() const -{ - if(!m_attached) - { - ROCP_ERROR << "cont called while not attached"; - return false; + ROCP_ERROR << "[rocprofiler-sdk-rocattach] cont called in invalid state: " << m_state; + return ROCATTACH_STATUS_ERROR; } PTRACE_CALL(PTRACE_CONT, m_pid, NULL, NULL); - ROCP_TRACE << "ptrace resumed pid " << m_pid; - return true; + m_state = PTRACE_SESSION_STATE_RUNNING; + ROCP_TRACE << "[rocprofiler-sdk-rocattach] ptrace resumed pid " << m_pid; + return ROCATTACH_STATUS_SUCCESS; } -bool -PTraceSession::handle_signals() const -{ - while(!m_detaching_ptrace_session.load()) - { - int status{0}; - if(waitpid(m_pid, &status, WNOHANG) == -1) - { - ROCP_ERROR << "waitpid failed in handle_signal for pid " << m_pid; - return false; - } - if(status != 0 && WIFEXITED(status)) - { - ROCP_ERROR << "process " << m_pid << " exited, status=" << WEXITSTATUS(status); - return false; - } - else if(status != 0 && WIFSIGNALED(status)) - { - ROCP_ERROR << "process " << m_pid << " killed by signal " << WTERMSIG(status); - return false; - } - else if(status != 0 && WIFSTOPPED(status)) - { - auto sig = WSTOPSIG(status); - ROCP_TRACE << "process " << m_pid << "stopped by signal " << sig; - PTRACE_CALL(PTRACE_CONT, m_pid, NULL, sig); - } - std::this_thread::yield(); - } - return true; -} - -void -PTraceSession::detach_ptrace_session() -{ - m_detaching_ptrace_session.store(true); -} - -} // namespace attach +} // namespace rocattach } // namespace rocprofiler diff --git a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/ptrace_session.hpp b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/ptrace_session.hpp index fe7e4eaad8..597916460a 100644 --- a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/ptrace_session.hpp +++ b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/ptrace_session.hpp @@ -22,6 +22,7 @@ #pragma once +#include #include #include @@ -35,7 +36,7 @@ namespace rocprofiler { -namespace attach +namespace rocattach { class PTraceSession { @@ -43,45 +44,110 @@ public: explicit PTraceSession(int); ~PTraceSession(); - bool attach(); - bool detach(); - bool simple_mmap(void*& addr, size_t length) const; - bool simple_munmap(void*& addr, size_t length) const; + static bool is_supported(); - bool write(size_t addr, const std::vector& data, size_t size) const; - bool read(size_t addr, std::vector& data, size_t size) const; - bool swap(size_t addr, - const std::vector& in_data, - std::vector& out_data, - size_t size) const; + rocattach_status_t attach(); + rocattach_status_t detach(); + rocattach_status_t simple_mmap(void*& addr, size_t length); + rocattach_status_t simple_munmap(void*& addr, size_t length); + + rocattach_status_t write(size_t addr, const std::vector& data, size_t size); + rocattach_status_t read(size_t addr, std::vector& data, size_t size); + rocattach_status_t swap(size_t addr, + const std::vector& in_data, + std::vector& out_data, + size_t size); int get_pid() const { return m_pid; } - bool call_function(const std::string& library, const std::string& symbol); - bool call_function(const std::string& library, const std::string& symbol, void* first); - bool call_function(const std::string& library, - const std::string& symbol, - void* first, - void* second); - - bool stop() const; - bool cont() const; - bool handle_signals() const; - void detach_ptrace_session(); - - std::atomic m_setup_status = ROCPROFILER_STATUS_SUCCESS; + rocattach_status_t call_function(const std::string& library, + const std::string& symbol, + uint64_t& ret_value); + rocattach_status_t call_function(const std::string& library, + const std::string& symbol, + uint64_t& ret_value, + void* first); + rocattach_status_t call_function(const std::string& library, + const std::string& symbol, + uint64_t& ret_value, + void* first, + void* second); private: - static bool find_library(void*& addr, int inpid, const std::string& library); - bool find_symbol(void*& addr, const std::string& library, const std::string& symbol); + rocattach_status_t start_signal_handler(); + rocattach_status_t stop_signal_handler(); + rocattach_status_t wait_for_breakpoint(); + rocattach_status_t wait_for_stop(); - std::unordered_map m_target_library_addrs = {}; - std::unordered_map m_target_symbol_addrs = {}; + rocattach_status_t stop(); + rocattach_status_t cont(); + rocattach_status_t write_internal(size_t addr, + const std::vector& data, + size_t size) const; + rocattach_status_t read_internal(size_t addr, std::vector& data, size_t size) const; + rocattach_status_t swap_internal(size_t addr, + const std::vector& in_data, + std::vector& out_data, + size_t size) const; - const int m_pid = -1; - bool m_attached = false; - std::atomic m_detaching_ptrace_session = false; + enum ptrace_session_state_t + { + // When the session class is created, this is the initial state + // Can transition to attached by calling attach() + PTRACE_SESSION_STATE_INITIAL = 0, + // State after attach() + // Most functions can be run in this state + // Can transition to stopped by calling stop() + // Can transition to detached by calling detach() + PTRACE_SESSION_STATE_RUNNING, + // State after stop() + // Required state for some internal functions, like write_internal + // Can transition to running by calling cont() + PTRACE_SESSION_STATE_STOPPED, + // State after detach() + // Class is left to be cleaned up + PTRACE_SESSION_STATE_DETACHED + }; + + // This is the state of the signal handler thread. This thread intercepts signals that the + // attachee is sent. Based on the current state, these signals are either consumed by the + // attacher or forwarded to the attachee. Typically, the main thread updates this state. The + // main thread can update this state only if the program is stopped (i.e. stop() has been + // called). The main thread must also verify the state is not FINAL using a check-and-set + // operation. The signal handler will initially transition the state from INITIAL to ATTACHED. + // Otherwise, the signal handler update thread will only update this state to FINAL. This occurs + // when the target program ends, an unexpected error occurs, or if a stop has been requested by + // the main program (by transitioning this state to DETACHING). The main thread can check the + // error state for more information. + enum ptrace_session_signal_handler_state_t + { + PTRACE_SIGNAL_HANDLER_STATE_INITIAL = 0, + // The signal handler is running normally. All signals are forwarded to the attachee. + PTRACE_SIGNAL_HANDLER_STATE_ATTACHED, + // The signal handler is running normally. A WUNTRACED stop will transition the state to + // ATTACHED. All other signals are forwarded to the attachee. + PTRACE_SIGNAL_HANDLER_STATE_WAITING_FOR_BREAKPOINT, + // The signal handler will end operation. State is transitioned to FINAL when complete. + PTRACE_SIGNAL_HANDLER_STATE_DETACHING, + // The signal handler is not running, either by request or because some error has occurred. + // If an error occurred, more information will be in m_signal_handler_error + PTRACE_SIGNAL_HANDLER_STATE_FINAL + }; + + static void ptrace_signal_handler_func( + int pid, + std::atomic& state, + std::atomic& error); + + ptrace_session_state_t m_state = PTRACE_SESSION_STATE_INITIAL; + std::atomic m_ptrace_signal_handler_state = + PTRACE_SIGNAL_HANDLER_STATE_INITIAL; + std::atomic m_ptrace_signal_handler_error = ROCATTACH_STATUS_SUCCESS; + + std::thread m_ptrace_signal_handler_thread; + + const int m_pid = -1; }; -} // namespace attach +} // namespace rocattach } // namespace rocprofiler diff --git a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/rocattach.cpp b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/rocattach.cpp index 49e19af67f..090f472906 100644 --- a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/rocattach.cpp +++ b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/rocattach.cpp @@ -26,77 +26,121 @@ #include "lib/common/logging.hpp" #include "lib/common/static_object.hpp" -#include +#include +#include +#include -#include -#include +#include +#include +#include extern char** environ; -namespace common = ::rocprofiler::common; - +namespace rocprofiler +{ +namespace rocattach +{ namespace { -std::unique_ptr ptrace_session; -std::thread ptrace_thread; -std::atomic finished_setup(false); -} // namespace +using session_t = rocprofiler::rocattach::PTraceSession; +using session_list_t = std::map; -ROCPROFILER_EXTERN_C_INIT -int -attach(uint32_t pid) ROCPROFILER_EXPORT; +#define ROCATTACH_STATUS_STRING(CODE, MSG) \ + template <> \ + struct status_string \ + { \ + static constexpr auto name = #CODE; \ + static constexpr auto value = MSG; \ + }; -int -detach() ROCPROFILER_EXPORT; -ROCPROFILER_EXTERN_C_FINI +template +struct status_string; + +ROCATTACH_STATUS_STRING(ROCATTACH_STATUS_SUCCESS, "Success") +ROCATTACH_STATUS_STRING(ROCATTACH_STATUS_ERROR, "General error") +ROCATTACH_STATUS_STRING(ROCATTACH_STATUS_ERROR_INVALID_ARGUMENT, "Invalid function argument") +ROCATTACH_STATUS_STRING(ROCATTACH_STATUS_ERROR_NOT_SUPPORTED, + "Attachment not supported on this platform") +ROCATTACH_STATUS_STRING(ROCATTACH_STATUS_ERROR_PTRACE_ERROR, "General ptrace error") +ROCATTACH_STATUS_STRING(ROCATTACH_STATUS_ERROR_PTRACE_OPERATION_NOT_PERMITTED, + "ptrace returned EPERM, operation not permitted") +ROCATTACH_STATUS_STRING(ROCATTACH_STATUS_ERROR_PTRACE_PROCESS_NOT_FOUND, + "ptrace returned ESRCH, no such process") + +template +const char* +get_status_name(rocattach_status_t status, std::index_sequence) +{ + if(status == Idx) return status_string::name; + // recursion until tail empty + if constexpr(sizeof...(Tail) > 0) + return get_status_name(status, std::index_sequence{}); + return nullptr; +} + +template +const char* +get_status_string(rocattach_status_t status, std::index_sequence) +{ + if(status == Idx) return status_string::value; + // recursion until tail empty + if constexpr(sizeof...(Tail) > 0) + return get_status_string(status, std::index_sequence{}); + return nullptr; +} void initialize_logging() { auto logging_cfg = rocprofiler::common::logging_config{.install_failure_handler = true}; - common::init_logging("ROCPROF", logging_cfg); + common::init_logging("ROCATTACH", logging_cfg); FLAGS_colorlogtostderr = true; } -namespace +session_list_t* +get_sessions() { + static auto*& session_list = rocprofiler::common::static_object::construct(); + return session_list; +} + +std::lock_guard +get_sessions_lock_guard() +{ + static auto*& m = rocprofiler::common::static_object::construct(); + return std::lock_guard(*CHECK_NOTNULL(m)); +} + // Helper function to allocate memory in target process and write data -bool -write_data_to_target(const std::string& description, +rocattach_status_t +write_data_to_target(session_t& session, + const std::string& description, const std::vector& data, void*& allocated_addr) { // Allocate memory in target process - if(!ptrace_session->simple_mmap(allocated_addr, data.size())) + auto status = ROCATTACH_STATUS_SUCCESS; + status = session.simple_mmap(allocated_addr, data.size()); + if(status != ROCATTACH_STATUS_SUCCESS) { - ROCP_ERROR << "Failed to allocate memory for " << description << " in target process"; - return false; - } - ROCP_TRACE << "Allocated memory for " << description << " at " << allocated_addr; - - // Stop target process for writing - if(!ptrace_session->stop()) - { - ROCP_ERROR << "Failed to stop target process for " << description << " writing"; - return false; + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Failed to allocate memory for " << description + << " in target process pid " << session.get_pid(); + return status; } + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Allocated memory for " << description << " at " + << allocated_addr << " in target process pid " << session.get_pid(); // Write data to target process memory - if(!ptrace_session->write(reinterpret_cast(allocated_addr), data, data.size())) + status = session.write(reinterpret_cast(allocated_addr), data, data.size()); + if(status != ROCATTACH_STATUS_SUCCESS) { - ROCP_ERROR << "Failed to write " << description << " to target process"; - return false; + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Failed to write " << description + << " to target process pid " << session.get_pid(); + return status; } - - // Continue target process - if(!ptrace_session->cont()) - { - ROCP_ERROR << "Failed to continue target process after " << description << " writing"; - return false; - } - - ROCP_TRACE << "Wrote " << description << " to target process"; - return true; + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Wrote " << description << " to target process pid " + << session.get_pid(); + return status; } // Helper function to build environment buffer @@ -112,11 +156,12 @@ build_environment_buffer() const char* var = *invars; if(strncmp("ROCP", var, 4) != 0) { + // only take envvars starting with ROCP continue; } var_count++; - ROCP_TRACE << "Adding to environment buffer: " << var; + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Adding to environment buffer: " << var; // Add variable name while(*var != '=') @@ -140,119 +185,275 @@ build_environment_buffer() return environment_buffer; } -} // anonymous namespace -ROCPROFILER_EXTERN_C_INIT - -void -handle_ptrace_operations(uint32_t pid) +rocattach_status_t +setup(int pid) { // Setup attachement for rocprofiler - ROCP_TRACE << "Attachment library called for pid " << pid; - ptrace_session = std::make_unique(pid); - ROCP_TRACE << "Attempting attachment to pid " << pid; - if(!ptrace_session->attach()) + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Attachment library rocattach_attach function called " + "for pid " + << pid; + + auto* sessions = CHECK_NOTNULL(get_sessions()); + session_t* session; { - ROCP_ERROR << "Attachment failed to pid " << pid; - ptrace_session->m_setup_status.store(ROCPROFILER_STATUS_ERROR_INVALID_ARGUMENT); - finished_setup.store(true); - return; + auto lg = get_sessions_lock_guard(); + if(sessions->count(pid) > 0) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] rocattach_attach called for pid " << pid + << ", which already has an active " + "attachment session."; + return ROCATTACH_STATUS_ERROR_INVALID_ARGUMENT; + } + + sessions->emplace(pid, pid); + session = &(sessions->at(pid)); } - ROCP_TRACE << "Attachment success to pid " << pid; + auto status = ROCATTACH_STATUS_SUCCESS; + + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Attempting attachment to pid " << pid; + status = session->attach(); + if(status != ROCATTACH_STATUS_SUCCESS) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Attachment failed to pid " << pid + << " with status code " << status; + return status; + } + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Attachment success to pid " << pid; // Build and write environment buffer to target process auto environment_buffer = build_environment_buffer(); void* environment_buffer_addr = nullptr; - if(!write_data_to_target("environment buffer", environment_buffer, environment_buffer_addr)) + status = write_data_to_target( + *session, "environment buffer", environment_buffer, environment_buffer_addr); + if(status != ROCATTACH_STATUS_SUCCESS) { - ptrace_session->m_setup_status.store(ROCPROFILER_STATUS_ERROR); - finished_setup.store(true); - return; + return status; } // Build and write tool library path to target process auto tool_lib_path_env = rocprofiler::common::get_env("ROCPROF_ATTACH_TOOL_LIBRARY", "librocprofiler-sdk-tool.so"); const char* tool_lib_path = tool_lib_path_env.c_str(); - ROCP_TRACE << "Tool library path: " << tool_lib_path; + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Tool library path: " << tool_lib_path; size_t tool_lib_path_len = strlen(tool_lib_path) + 1; std::vector tool_lib_buffer(tool_lib_path, tool_lib_path + tool_lib_path_len); void* tool_lib_path_addr = nullptr; - if(!write_data_to_target("tool library path", tool_lib_buffer, tool_lib_path_addr)) + status = + write_data_to_target(*session, "tool library path", tool_lib_buffer, tool_lib_path_addr); + if(status != ROCATTACH_STATUS_SUCCESS) { - ptrace_session->m_setup_status.store(ROCPROFILER_STATUS_ERROR); - finished_setup.store(true); - return; - } - - // Execute the attach function with both parameters - if(!ptrace_session->call_function("librocprofiler-register.so", - "rocprofiler_register_attach", - environment_buffer_addr, - tool_lib_path_addr)) - { - ROCP_ERROR << "Failed to call attach function in target process " << pid; - ptrace_session->m_setup_status.store(ROCPROFILER_STATUS_ERROR); - finished_setup.store(true); - return; - } - - // Clean up - free the tool library path memory in target process - if(!ptrace_session->simple_munmap(tool_lib_path_addr, tool_lib_path_len)) - { - ROCP_ERROR << "Failed to free tool library path memory in target process"; - // Continue anyway since the main operation succeeded - } - ROCP_TRACE << "Cleaned up tool library path memory in target process"; - - // Allow main thread to continue - finished_setup.store(true); - if(!ptrace_session->handle_signals()) - { - ROCP_ERROR << "Signal handling loop terminated unexepectedly for pid " << pid; - // don't return, try to detach anyways - } - // Detach rocprofiler - ROCP_TRACE << "Detaching rocprofiler from pid " << pid; - if(!ptrace_session->call_function("librocprofiler-register.so", "rocprofiler_register_detach")) - { - ROCP_ERROR << "Failed to call detach function in target process"; - // don't return, try to detach anyways - } - ptrace_session->stop(); - ptrace_session->detach(); - ptrace_session.reset(); -} - -int -attach(uint32_t pid) -{ - initialize_logging(); - ptrace_thread = std::thread(handle_ptrace_operations, pid); - // Wait for ptrace thread to finish setting up - while(!finished_setup.load()) - std::this_thread::yield(); - - auto status = ptrace_session->m_setup_status.load(); - if(status != ROCPROFILER_STATUS_SUCCESS) - { - ROCP_ERROR << "ptrace session failed with error code " << ptrace_session->m_setup_status; - ptrace_thread.join(); - finished_setup.store(false); return status; } - return ROCPROFILER_STATUS_SUCCESS; + + uint64_t retval = 0; + // Execute the attach function with both parameters + status = session->call_function("librocprofiler-register.so", + "rocprofiler_register_attach", + retval, + environment_buffer_addr, + tool_lib_path_addr); + if(status != ROCATTACH_STATUS_SUCCESS) + { + ROCP_ERROR + << "[rocprofiler-sdk-rocattach] Failed to call " + "rocprofiler-register::rocprofiler_register_attach function in target process " + << pid << ". status: " << status; + return status; + } + else if(retval != 0) + { + ROCP_ERROR + << "[rocprofiler-sdk-rocattach] rocprofiler-register::rocprofiler_register_attach " + "function returned non-zero status in target process " + << pid << ". return: " << retval; + return ROCATTACH_STATUS_ERROR; + } + + // Clean up - free the environment buffer and tool library path memory in target process + status = session->simple_munmap(environment_buffer_addr, environment_buffer.size()); + if(status != ROCATTACH_STATUS_SUCCESS) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Failed to free environment buffer memory in " + "target process " + << pid << ", continuing..."; + // Continue anyway since the main operation succeeded + } + ROCP_TRACE + << "[rocprofiler-sdk-rocattach] Cleaned up tool environment memory in target process " + << pid; + + status = session->simple_munmap(tool_lib_path_addr, tool_lib_path_len); + if(status != ROCATTACH_STATUS_SUCCESS) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Failed to free tool library path memory in " + "target process " + << pid << ", continuing..."; + // Continue anyway since the main operation succeeded + } + ROCP_TRACE + << "[rocprofiler-sdk-rocattach] Cleaned up tool library path memory in target process " + << pid; + return ROCATTACH_STATUS_SUCCESS; } -int -detach() +rocattach_status_t +teardown(int pid) { - ptrace_session->detach_ptrace_session(); - ptrace_thread.join(); - finished_setup.store(false); - return ROCPROFILER_STATUS_SUCCESS; + // Setup attachement for rocprofiler + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Attachment library rocattach_detach function called " + "for pid " + << pid; + + auto* sessions = CHECK_NOTNULL(get_sessions()); + session_t* session; + { + auto lg = get_sessions_lock_guard(); + if(sessions->count(pid) == 0) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] rocattach_detach called for pid " << pid + << ", which has no active " + "attachment session."; + return ROCATTACH_STATUS_ERROR_INVALID_ARGUMENT; + } + + session = &(sessions->at(pid)); + } + auto status = ROCATTACH_STATUS_SUCCESS; + + uint64_t retval = 0; + // Execute the attach function with both parameters + status = + session->call_function("librocprofiler-register.so", "rocprofiler_register_detach", retval); + if(status != ROCATTACH_STATUS_SUCCESS) + { + ROCP_ERROR + << "[rocprofiler-sdk-rocattach] Failed to call " + "rocprofiler-register::rocprofiler_register_detach function in target process " + << pid << ". status: " << status; + // continue to detach anyways + } + else if(retval != 0) + { + ROCP_ERROR + << "[rocprofiler-sdk-rocattach] rocprofiler-register::rocprofiler_register_detach " + "function returned non-zero status in target process " + << pid << ". return: " << retval; + // continue to detach anyways + } + + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Attempting detachment to pid " << pid; + status = session->detach(); + if(status != ROCATTACH_STATUS_SUCCESS) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Detachment failed from pid " << pid; + return status; + } + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Detachment success from pid " << pid; + + { + auto lg = get_sessions_lock_guard(); + sessions->erase(pid); + } + + return ROCATTACH_STATUS_SUCCESS; } -ROCPROFILER_EXTERN_C_FINI +} // namespace +} // namespace rocattach +} // namespace rocprofiler + +ROCATTACH_EXTERN_C_INIT + +rocattach_status_t +rocattach_attach(int pid) +{ + rocprofiler::rocattach::initialize_logging(); + + if(!rocprofiler::rocattach::PTraceSession::is_supported()) + { + ROCP_ERROR << "[rocprofiler-sdk-attach] rocattach is not supported on this platform."; + return ROCATTACH_STATUS_ERROR_NOT_SUPPORTED; + } + + auto status = rocprofiler::rocattach::setup(pid); + if(status != ROCATTACH_STATUS_SUCCESS) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] rocattach_attach failed with error code " + << status; + return status; + } + return ROCATTACH_STATUS_SUCCESS; +} + +rocattach_status_t +rocattach_detach(int pid) +{ + rocprofiler::rocattach::initialize_logging(); + if(pid != 0) + { + auto status = rocprofiler::rocattach::teardown(pid); + if(status != ROCATTACH_STATUS_SUCCESS) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] rocattach_detach failed with error code " + << status; + return status; + } + return ROCATTACH_STATUS_SUCCESS; + } + else + { + ROCP_INFO << "[rocprofiler-sdk-rocattach] rocattach_detach received pid=0, detaching from " + "ALL sessions"; + std::vector pids; + { + auto lg = rocprofiler::rocattach::get_sessions_lock_guard(); + for(auto& pair_itr : *(CHECK_NOTNULL(rocprofiler::rocattach::get_sessions()))) + { + pids.emplace_back(pair_itr.first); + } + } + + for(int pid_itr : pids) + { + rocprofiler::rocattach::teardown(pid_itr); + } + return ROCATTACH_STATUS_SUCCESS; + } +} + +rocattach_status_t +rocattach_get_version(uint32_t* major, uint32_t* minor, uint32_t* patch) +{ + *CHECK_NOTNULL(major) = ROCATTACH_VERSION_MAJOR; + *CHECK_NOTNULL(minor) = ROCATTACH_VERSION_MINOR; + *CHECK_NOTNULL(patch) = ROCATTACH_VERSION_PATCH; + return ROCATTACH_STATUS_SUCCESS; +} + +rocattach_status_t +rocattach_get_version_triplet(rocattach_version_triplet_t* info) +{ + *CHECK_NOTNULL(info) = {.major = ROCATTACH_VERSION_MAJOR, + .minor = ROCATTACH_VERSION_MINOR, + .patch = ROCATTACH_VERSION_PATCH}; + return ROCATTACH_STATUS_SUCCESS; +} + +const char* +rocattach_get_status_name(rocattach_status_t status) +{ + return rocprofiler::rocattach::get_status_name( + status, std::make_index_sequence{}); +} + +const char* +rocattach_get_status_string(rocattach_status_t status) +{ + return rocprofiler::rocattach::get_status_string( + status, std::make_index_sequence{}); +} + +ROCATTACH_EXTERN_C_FINI diff --git a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/symbol_lookup.cpp b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/symbol_lookup.cpp new file mode 100644 index 0000000000..97465da6e6 --- /dev/null +++ b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/symbol_lookup.cpp @@ -0,0 +1,260 @@ +// MIT License +// +// Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "symbol_lookup.hpp" + +#include "lib/common/filesystem.hpp" +#include "lib/common/logging.hpp" + +#include +#include + +#include +#include +#include + +namespace fs = rocprofiler::common::filesystem; + +namespace rocprofiler +{ +namespace rocattach +{ +namespace +{ +constexpr char ROCATTACH_LIBRARY_NAME[] = "librocprofiler-sdk-rocattach.so.1"; +std::unordered_map m_target_library_addrs = {}; +std::unordered_map m_target_symbol_addrs = {}; + +using open_modes_vec_t = std::vector; + +std::optional +get_linked_path(std::string_view _name, open_modes_vec_t&& _open_modes) +{ + const open_modes_vec_t default_link_open_modes = {(RTLD_LAZY | RTLD_NOLOAD)}; + if(_name.empty()) return fs::current_path().string(); + + if(_open_modes.empty()) _open_modes = default_link_open_modes; + + void* _handle = nullptr; + bool _noload = false; + for(auto _mode : _open_modes) + { + _handle = dlopen(_name.data(), _mode); + _noload = (_mode & RTLD_NOLOAD) == RTLD_NOLOAD; + if(_handle) break; + } + + if(_handle) + { + struct link_map* _link_map = nullptr; + dlinfo(_handle, RTLD_DI_LINKMAP, &_link_map); + if(_link_map != nullptr && !std::string_view{_link_map->l_name}.empty()) + { + return fs::absolute(fs::path{_link_map->l_name}).string(); + } + if(_noload == false) dlclose(_handle); + } + + return std::nullopt; +} + +auto +get_this_library_path() +{ + auto _this_lib_path = get_linked_path(ROCATTACH_LIBRARY_NAME, {RTLD_NOLOAD | RTLD_LAZY}); + LOG_IF(FATAL, !_this_lib_path) << "[rocprofiler-sdk-rocattach] " << ROCATTACH_LIBRARY_NAME + << " could not locate itself in the list of loaded libraries"; + return fs::path{*_this_lib_path}.parent_path().string(); +} + +void* +get_library_handle(std::string_view _lib_name) +{ + void* _lib_handle = nullptr; + + if(_lib_name.empty()) return nullptr; + + auto _lib_path = fs::path{_lib_name}; + auto _lib_path_fname = _lib_path.filename(); + auto _lib_path_abs = + (_lib_path.is_absolute()) ? _lib_path : (fs::path{get_this_library_path()} / _lib_path); + + // check to see if the rocprofiler library is already loaded + _lib_handle = dlopen(_lib_path.c_str(), RTLD_NOLOAD | RTLD_LAZY); + + if(_lib_handle) + { + LOG(INFO) << "[rocprofiler-sdk-rocattach] loaded " << _lib_name << " library at " + << _lib_path.string() << " (handle=" << _lib_handle + << ") via RTLD_NOLOAD | RTLD_LAZY"; + } + + // try to load with the given path + if(!_lib_handle) + { + _lib_handle = dlopen(_lib_path.c_str(), RTLD_GLOBAL | RTLD_LAZY); + + if(_lib_handle) + { + LOG(INFO) << "[rocprofiler-sdk-rocattach] loaded " << _lib_name << " library at " + << _lib_path.string() << " (handle=" << _lib_handle + << ") via RTLD_GLOBAL | RTLD_LAZY"; + } + } + + // try to load with the absoulte path + if(!_lib_handle) + { + _lib_path = _lib_path_abs; + _lib_handle = dlopen(_lib_path.c_str(), RTLD_GLOBAL | RTLD_LAZY); + } + + // try to load with the basename path + if(!_lib_handle) + { + _lib_path = _lib_path_fname; + _lib_handle = dlopen(_lib_path.c_str(), RTLD_GLOBAL | RTLD_LAZY); + } + + LOG(INFO) << "[rocprofiler-sdk-rocattach] loaded " << _lib_name << " library at " + << _lib_path.string() << " (handle=" << _lib_handle << ")"; + + LOG_IF(WARNING, _lib_handle == nullptr) << _lib_name << " failed to load\n"; + + return _lib_handle; +} +} // namespace + +bool +find_library(void*& addr, int inpid, const std::string& library) +{ + std::stringstream searchname; + searchname << inpid << "::" << library; + // TODO: add this back + // if (target_library_addrs.find(searchname.str()) != target_library_addrs.end()) + //{ + // return target_library_addrs[searchname.str()]; + //} + + // uses "maps" file to find where library has been loaded in target process + // does not require this process to be attached + std::stringstream filename; + filename << "/proc/" << inpid << "/maps"; + std::ifstream maps(filename.str().c_str()); + + if(!maps) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Couldn't open " << filename.str(); + return false; + } + + std::string line; + while(std::getline(maps, line)) + { + if(line.find(library) != std::string::npos) + { + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Entry in pid " << inpid + << " maps file is: " << line; + break; + } + } + + if(!maps) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Couldn't find library " << library << " in " + << filename.str(); + return false; + } + + // NOLINTNEXTLINE(performance-no-int-to-ptr) + addr = reinterpret_cast(std::stoull(line, nullptr, 16)); + // target_library_addrs[searchname.str()] = addr; + return true; +} + +bool +find_symbol(int target_pid, void*& addr, const std::string& library, const std::string& symbol) +{ + auto searchname = std::stringstream{}; + searchname << target_pid << "::" << library << "::" << symbol; + if(auto itr = m_target_symbol_addrs.find(searchname.str()); itr != m_target_symbol_addrs.end()) + { + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Found symbol for " << searchname.str() << " at " + << itr->second; + return itr->second != nullptr; + } + + void* libraryaddr = nullptr; + void* symboladdr = nullptr; + + // Load the library in our process to determine the offset of the requested symbol from the + // start address of the library + addr = nullptr; + libraryaddr = get_library_handle(library); + + if(!libraryaddr) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Host couldn't dlopen " << library; + return false; + } + + symboladdr = dlsym(libraryaddr, symbol.c_str()); + if(!symboladdr) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Host couldn't dlsym " << symbol; + return false; + } + + // Find the start address of the library in our process + void* hostlibraryaddr; + if(!find_library(hostlibraryaddr, getpid(), library)) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Couldn't determine where " << library + << " was loaded for host"; + return false; + } + + // Caluclate the offset + size_t offset = + reinterpret_cast(symboladdr) - reinterpret_cast(hostlibraryaddr); + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Offset of " << symbol << " into " << library + << " calculated as " << offset; + + // Find the start address of the library in the target process + void* targetlibraryaddr; + if(!find_library(targetlibraryaddr, target_pid, library)) + { + ROCP_ERROR << "[rocprofiler-sdk-rocattach] Couldn't determine where " << library + << " was loaded for target"; + return false; + } + + // Calculate address of symbol in the target process using the offset + // NOLINTNEXTLINE(performance-no-int-to-ptr) + addr = reinterpret_cast(reinterpret_cast(targetlibraryaddr) + offset); + m_target_symbol_addrs[searchname.str()] = addr; + ROCP_TRACE << "[rocprofiler-sdk-rocattach] Found symbol for " << searchname.str() << " at " + << addr; + return true; +} +} // namespace rocattach +} // namespace rocprofiler diff --git a/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/symbol_lookup.hpp b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/symbol_lookup.hpp new file mode 100644 index 0000000000..53366e1587 --- /dev/null +++ b/projects/rocprofiler-sdk/source/lib/rocprofiler-sdk-rocattach/symbol_lookup.hpp @@ -0,0 +1,38 @@ +// MIT License +// +// Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include + +namespace rocprofiler +{ +namespace rocattach +{ +bool +find_library(void*& addr, int inpid, const std::string& library); + +bool +find_symbol(int target_pid, void*& addr, const std::string& library, const std::string& symbol); + +} // namespace rocattach +} // namespace rocprofiler diff --git a/projects/rocprofiler-sdk/tests/CMakeLists.txt b/projects/rocprofiler-sdk/tests/CMakeLists.txt index 571ca7cfe5..29818a8716 100644 --- a/projects/rocprofiler-sdk/tests/CMakeLists.txt +++ b/projects/rocprofiler-sdk/tests/CMakeLists.txt @@ -92,6 +92,9 @@ add_subdirectory(code-object-multi-threaded) # rocpd validation tests add_subdirectory(rocpd) +# rocattach validation tests +add_subdirectory(rocattach) + # rocprofv3 validation tests add_subdirectory(rocprofv3) diff --git a/projects/rocprofiler-sdk/tests/rocattach/CMakeLists.txt b/projects/rocprofiler-sdk/tests/rocattach/CMakeLists.txt new file mode 100644 index 0000000000..2ab13c5d6f --- /dev/null +++ b/projects/rocprofiler-sdk/tests/rocattach/CMakeLists.txt @@ -0,0 +1,68 @@ +# +# rocprofv3 tool test +# +cmake_minimum_required(VERSION 3.21.0 FATAL_ERROR) + +project( + rocprofiler-sdk-tests-rocattach + LANGUAGES CXX + VERSION 0.0.0) + +find_package(rocprofiler-sdk REQUIRED) +find_package(rocprofiler-sdk-rocattach REQUIRED) + +# TODO: sanitizer tests do not cooperate with attachment code injection +set(ROCPROFILER_MEMCHECK_TYPES "AddressSanitizer" "UndefinedBehaviorSanitizer" + "ThreadSanitizer") + +if(ROCPROFILER_MEMCHECK AND ROCPROFILER_MEMCHECK IN_LIST ROCPROFILER_MEMCHECK_TYPES) + set(IS_DISABLED ON) +else() + set(IS_DISABLED OFF) +endif() + +if(ROCPROFILER_MEMCHECK STREQUAL "LeakSanitizer") + set(LOG_LEVEL "warning") # info produces memory leak +else() + set(LOG_LEVEL "trace") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_executable(rocattach-parallel-attach-test) +target_sources(rocattach-parallel-attach-test PRIVATE main.cpp) +target_link_libraries( + rocattach-parallel-attach-test + PRIVATE rocprofiler-sdk::rocprofiler-sdk rocprofiler-sdk::tests-build-flags + rocprofiler-sdk::tests-common-library + rocprofiler-sdk::rocprofiler-sdk-rocattach-shared-library) + +add_test(NAME rocattach-parallel-attach-test-execute + COMMAND $ + $ $) + +set(rocattach-test-env + "${ROCPROFILER_MEMCHECK_PRELOAD_ENV}" + "ROCP_TOOL_ATTACH=1" + "ROCPROFILER_REGISTER_LOG_LEVEL=${LOG_LEVEL}" + "ROCPROFILER_LOG_LEVEL=${LOG_LEVEL}" + "ROCATTACH_LOG_LEVEL=${LOG_LEVEL}" + "LD_LIBRARY_PATH=$:$ENV{LD_LIBRARY_PATH}" + ) + +set_tests_properties( + rocattach-parallel-attach-test-execute + PROPERTIES TIMEOUT + 45 + LABELS + "integration-tests" + ENVIRONMENT + "${rocattach-test-env}" + PASS_REGULAR_EXPRESSION + "Test C tool \\(priority=0\\) is using rocprofiler-sdk v" + FAIL_REGULAR_EXPRESSION + "${ROCPROFILER_DEFAULT_FAIL_REGEX}" + DISABLED + "${IS_DISABLED}") diff --git a/projects/rocprofiler-sdk/tests/rocattach/main.cpp b/projects/rocprofiler-sdk/tests/rocattach/main.cpp new file mode 100644 index 0000000000..5349cfb927 --- /dev/null +++ b/projects/rocprofiler-sdk/tests/rocattach/main.cpp @@ -0,0 +1,124 @@ +// MIT License +// +// Copyright (c) 2025 Advanced Micro Devices, Inc. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#define ROCATTACH_CALL(func) \ + { \ + rocattach_status_t status = func; \ + if(status != ROCATTACH_STATUS_SUCCESS) \ + { \ + std::cout << "error: call to " #func " returned non zero status " << status \ + << std::endl; \ + return 1; \ + } \ + else \ + { \ + std::cout << "call to " #func " successful " << std::endl; \ + } \ + } + +int +main(int argc, char** argv) +{ + if(argc != 3) + { + std::cout << "error: wrong number of arguments\n"; + return 1; + } + + pid_t pid1 = fork(); + if(pid1 < 0) + { + std::cout << "error: Fork 1 failed.\n"; + return 1; + } + + pid_t pid2 = 0; + if(pid1 > 0) + { + // Parent process, will fork again to spawn 2 processes + pid2 = fork(); + } + + if(pid2 < 0) + { + std::cout << "error: Fork 2 failed.\n"; + return 1; + } + + if(pid1 == 0 || pid2 == 0) + { + // Child process + std::cout << "child executing " << argv[1] << std::endl; + int ret = execl(argv[1], argv[1], nullptr); + if(ret == -1) + { + std::cout << "error in execl(), errno=" << errno << std::endl; + return 1; + } + } + else + { + // Wait a small amount of time for child processes to start executing + std::this_thread::sleep_for(std::chrono::milliseconds(2500)); + + setenv("ROCPROF_ATTACH_TOOL_LIBRARY", argv[2], true); + + ROCATTACH_CALL(rocattach_attach(pid1)); + ROCATTACH_CALL(rocattach_attach(pid2)); + + // Wait a small amount of time for child processes to continue executing + std::this_thread::sleep_for(std::chrono::milliseconds(5000)); + + ROCATTACH_CALL(rocattach_detach(pid1)); + ROCATTACH_CALL(rocattach_detach(pid2)); + + int pid1status = 0; + waitpid(pid1, &pid1status, 0); + int pid2status = 0; + waitpid(pid2, &pid2status, 0); + + if(pid1status != 0) + { + std::cout << "error in pid1, returned non-zero status: " << pid1status; + return 1; + } + if(pid2status != 0) + { + std::cout << "error in pid2, returned non-zero status: " << pid2status; + return 1; + } + } + return 0; +}