diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f9e2039..da62e44b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,11 @@ if(NOT CMAKE_BUILD_TYPE) endif() if(WIN32) - if(CMAKE_SIZEOF_VOID_P EQUAL 8) + if(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64") + set(CLINTERCEPT_PLATFORM_NAME "ARM64") + elseif(CMAKE_GENERATOR_PLATFORM STREQUAL "ARM64EC") + set(CLINTERCEPT_PLATFORM_NAME "ARM64EC") + elseif(CMAKE_SIZEOF_VOID_P EQUAL 8) set(CLINTERCEPT_PLATFORM_NAME "x64") elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) set(CLINTERCEPT_PLATFORM_NAME "Win32") diff --git a/cliloader/cliloader.cpp b/cliloader/cliloader.cpp index 0afac23f..51a62aa0 100644 --- a/cliloader/cliloader.cpp +++ b/cliloader/cliloader.cpp @@ -20,6 +20,58 @@ bool debug = false; static std::string commandLine = ""; +// Read the PE header machine type from a file on disk. +static USHORT getFileMachineType(const char* filePath) +{ + HANDLE hFile = CreateFileA( + filePath, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, 0, NULL); + if (hFile == INVALID_HANDLE_VALUE) + { + return IMAGE_FILE_MACHINE_UNKNOWN; + } + + USHORT machine = IMAGE_FILE_MACHINE_UNKNOWN; + IMAGE_DOS_HEADER dosHeader = {}; + DWORD bytesRead = 0; + + if (ReadFile(hFile, &dosHeader, sizeof(dosHeader), &bytesRead, NULL) && + bytesRead == sizeof(dosHeader) && + dosHeader.e_magic == IMAGE_DOS_SIGNATURE) + { + if (SetFilePointer(hFile, dosHeader.e_lfanew, NULL, FILE_BEGIN) != INVALID_SET_FILE_POINTER) + { + DWORD ntSignature = 0; + IMAGE_FILE_HEADER fileHeader = {}; + if (ReadFile(hFile, &ntSignature, sizeof(ntSignature), &bytesRead, NULL) && + bytesRead == sizeof(ntSignature) && + ntSignature == IMAGE_NT_SIGNATURE && + ReadFile(hFile, &fileHeader, sizeof(fileHeader), &bytesRead, NULL) && + bytesRead == sizeof(fileHeader)) + { + machine = fileHeader.Machine; + } + } + } + + CloseHandle(hFile); + return machine; +} + +// Get the PE header machine type of a running process by querying its image path. +static USHORT getProcessMachineType(HANDLE hProcess) +{ + char imagePath[MAX_PATH] = {}; + DWORD size = MAX_PATH; + if (!QueryFullProcessImageNameA(hProcess, 0, imagePath, &size)) + { + return IMAGE_FILE_MACHINE_UNKNOWN; + } + DEBUG("process image path: %s\n", imagePath); + return getFileMachineType(imagePath); +} + static bool checkWow64(HANDLE parent, HANDLE child) { BOOL parentWow64 = FALSE; @@ -37,6 +89,42 @@ static bool checkWow64(HANDLE parent, HANDLE child) return false; } + // IsWow64Process cannot distinguish ARM64EC (PE machine type x64) from pure + // ARM64, so compare PE machine types instead. Injecting across the two fails + // because the LoadLibraryA address is not valid in the other process. + USHORT parentMachineType = getProcessMachineType(parent); + USHORT childMachineType = getProcessMachineType(child); + + DEBUG("parent PE machine type: 0x%04X, child PE machine type: 0x%04X\n", + parentMachineType, childMachineType); + + if (parentMachineType != IMAGE_FILE_MACHINE_UNKNOWN && + childMachineType != IMAGE_FILE_MACHINE_UNKNOWN && + parentMachineType != childMachineType) + { + auto machineStr = [](USHORT machine) -> const char* { + switch (machine) { + case IMAGE_FILE_MACHINE_AMD64: return "x64/ARM64EC"; + case IMAGE_FILE_MACHINE_ARM64: return "ARM64"; + case IMAGE_FILE_MACHINE_I386: return "x86"; + default: return "unknown"; + } + }; + fprintf(stderr, "This is the %s version of cliloader, but the target application is a %s application.\n", + machineStr(parentMachineType), + machineStr(childMachineType)); + fprintf(stderr, "Execution will continue, but intercepting and profiling will be disabled.\n"); + return false; + } + else if (parentMachineType == IMAGE_FILE_MACHINE_UNKNOWN || + childMachineType == IMAGE_FILE_MACHINE_UNKNOWN) + { + // Could be false positive, so we deliberately fail open: proceed with injection + // and let any real incompatibility surface as a CreateRemoteThread / + // LoadLibrary failure later + fprintf(stderr, "Warning: could not determine PE machine type for one or both processes, architecture compatibility check was not applied.\n"); + } + return true; } @@ -678,33 +766,51 @@ int main(int argc, char *argv[]) #if defined(_WIN32) - // Get the existing value of the "SuppressLogging" control. - // We will suppress logging while loading the intercept DLL - // into this process, to avoid seeing loading twice. - char* envVal = NULL; - size_t len = 0; - errno_t err = _dupenv_s( &envVal, &len, "CLI_SuppressLogging" ); - DEBUG("CLI_SuppressLogging is currently: %s\n", envVal ? envVal : ""); - - SETENV("CLI_SuppressLogging", "1"); + // Used below to suppress controls that would cause the intercept DLL to + // create dumpdirectories/files while it is loaded into this (cliloader) process; + // the child (profiled) process must not inherit these overrides + class ScopedEnvOverride + { + public: + ScopedEnvOverride(const char* name, const char* overrideVal) : + m_name(name) + { + size_t len = 0; + _dupenv_s(&m_savedValue, &len, name); + SETENV(name, overrideVal); + } + ~ScopedEnvOverride() + { + SETENV(m_name.c_str(), m_savedValue ? m_savedValue : ""); + if (m_savedValue) free(m_savedValue); + } + ScopedEnvOverride(const ScopedEnvOverride&) = delete; + ScopedEnvOverride& operator=(const ScopedEnvOverride&) = delete; + private: + std::string m_name; + char* m_savedValue = NULL; + }; std::string dllpath = path + "\\opencl.dll"; DEBUG("path to OpenCL.dll is: %s\n", dllpath.c_str()); - // First things first. Load the intercept DLL into this process, and - // try to get the function pointer to the init function. If we can't - // do this, there's no need to go further. - HMODULE dll = LoadLibraryA(dllpath.c_str()); - if( dll == NULL ) + HMODULE dll = NULL; { - die("loading DLL"); - } - DEBUG("loaded DLL\n"); + ScopedEnvOverride suppressLogging("CLI_SuppressLogging", "1"); + ScopedEnvOverride logToFile("CLI_LogToFile", "0"); + ScopedEnvOverride chromeCallLogging("CLI_ChromeCallLogging", "0"); + ScopedEnvOverride chromePerformanceTiming("CLI_ChromePerformanceTiming", "0"); + ScopedEnvOverride reportToFile("CLI_ReportToFile", "0"); + + // Load the intercept DLL into this process, and + // try to get the function pointer to the init function + dll = LoadLibraryA(dllpath.c_str()); + if( dll == NULL ) + { + die("loading DLL"); + } + DEBUG("loaded DLL\n"); - SETENV("CLI_SuppressLogging", envVal ? envVal : ""); - if( envVal ) - { - free( envVal ); } LPTHREAD_START_ROUTINE cliprof_init = (LPTHREAD_START_ROUTINE)GetProcAddress( diff --git a/intercept/CMakeLists.txt b/intercept/CMakeLists.txt index fc163848..2d3c6ef1 100644 --- a/intercept/CMakeLists.txt +++ b/intercept/CMakeLists.txt @@ -204,7 +204,7 @@ endif() if(WIN32) target_compile_options(OpenCL PRIVATE /EHsc) - target_link_libraries(OpenCL SetupAPI Shlwapi) + target_link_libraries(OpenCL SetupAPI Shlwapi Psapi) elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") target_compile_options(OpenCL PRIVATE -Wall) set_target_properties(OpenCL PROPERTIES LINK_FLAGS "-Wl,--version-script -Wl,${CMAKE_CURRENT_SOURCE_DIR}/src/clIntercept.map") diff --git a/intercept/OS/OS_windows.h b/intercept/OS/OS_windows.h index 15324b97..a12282ea 100644 --- a/intercept/OS/OS_windows.h +++ b/intercept/OS/OS_windows.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include @@ -306,9 +307,17 @@ inline void Services::MakeDumpDirectories( pos = fileName.find( "/", ++pos ); while( pos != std::string::npos ) { - CreateDirectoryA( - fileName.substr( 0, pos ).c_str(), - NULL ); + // Use the \\?\ prefix with the wide-character API to support paths + // longer than MAX_PATH (260 characters). The \\?\ prefix requires + // backslashes, so convert any forward slashes in the path. + std::string dirPath = "\\\\?\\" + fileName.substr( 0, pos ); + std::replace( dirPath.begin(), dirPath.end(), '/', '\\' ); + int wLen = MultiByteToWideChar( + CP_UTF8, 0, dirPath.c_str(), -1, NULL, 0 ); + std::wstring wDirPath( wLen, 0 ); + MultiByteToWideChar( + CP_UTF8, 0, dirPath.c_str(), -1, &wDirPath[0], wLen ); + CreateDirectoryW( wDirPath.c_str(), NULL ); pos = fileName.find( "/", ++pos ); } diff --git a/intercept/src/cliprof_init.cpp b/intercept/src/cliprof_init.cpp index d30efe5a..6a5863fd 100644 --- a/intercept/src/cliprof_init.cpp +++ b/intercept/src/cliprof_init.cpp @@ -43,23 +43,33 @@ static void replaceFunction( #define REPLACE_FUNCTION( _name, _fname ) if( !strcmp(_name, #_fname) ) { replaceFunction( firstThunk, _fname ); } -extern "C" __declspec(dllexport) -DWORD cliprof_init( void* dummy ) +// Patch the IAT of a single loaded module for all opencl.dll imports. +static void patchModuleIAT( char* base ) { - char *base = (char*)GetModuleHandle(NULL); + // Validate DOS header: + PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)(base); + if( dosHeader->e_magic != IMAGE_DOS_SIGNATURE ) + { + return; + } // Get pointer to NT headers: - PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)(base); PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)(base + dosHeader->e_lfanew); if( ntHeaders->Signature != IMAGE_NT_SIGNATURE ) { - return false; + return; } // Get pointer to import directory: DWORD importOffset = ntHeaders->OptionalHeader. DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; + + if( importOffset == 0 ) + { + return; + } + PIMAGE_IMPORT_DESCRIPTOR importDesc = (PIMAGE_IMPORT_DESCRIPTOR)(base + importOffset); @@ -220,6 +230,35 @@ DWORD cliprof_init( void* dummy ) } importDesc++; } +} + +extern "C" __declspec(dllexport) +DWORD cliprof_init( void* dummy ) +{ + // Patch the IAT of all loaded modules, not just the main executable. + // This is necessary for applications that use OpenCL indirectly through + // another DLL (e.g., SYCL runtime DLLs like sycl7.dll) rather than + // importing opencl.dll directly in the main executable. + HMODULE hModules[1024] = {}; + DWORD cbNeeded = 0; + + if( EnumProcessModules( + GetCurrentProcess(), + hModules, + sizeof(hModules), + &cbNeeded ) ) + { + DWORD numModules = cbNeeded / sizeof(HMODULE); + for( DWORD i = 0; i < numModules; i++ ) + { + patchModuleIAT( (char*)hModules[i] ); + } + } + else + { + // Fallback: patch only the main executable. + patchModuleIAT( (char*)GetModuleHandle(NULL) ); + } return 0; } diff --git a/intercept/src/intercept.cpp b/intercept/src/intercept.cpp index 6f5a8c61..687e2f9e 100644 --- a/intercept/src/intercept.cpp +++ b/intercept/src/intercept.cpp @@ -180,7 +180,10 @@ CLIntercept::CLIntercept( void* pGlobalData ) CLIntercept::~CLIntercept() { stopAubCapture( NULL ); - report(); + if( m_EnqueueCounter.load(std::memory_order_relaxed) > 0 ) + { + report(); + } std::lock_guard lock(m_Mutex); @@ -8138,7 +8141,8 @@ void CLIntercept::dumpCaptureReplayKernelSource( errorCode = dispatch().clGetProgramInfo(program, CL_PROGRAM_SOURCE, sourceSize, source.data(), nullptr); if( errorCode == CL_SUCCESS ) { - std::ofstream output(dumpDirectory + "kernel.cl", std::ios::out | std::ios::binary); + std::ofstream output; + Utils::OpenOutputFile(output, dumpDirectory + "kernel.cl", std::ios::out | std::ios::binary); output.write(source.data(), sourceSize); } } @@ -8153,7 +8157,8 @@ void CLIntercept::dumpCaptureReplayKernelSource( errorCode = dispatch().clGetProgramInfo(program, CL_PROGRAM_IL, ilSize, il.data(), nullptr); if( errorCode == CL_SUCCESS ) { - std::ofstream output(dumpDirectory + "kernel.spv", std::ios::out | std::ios::binary); + std::ofstream output; + Utils::OpenOutputFile(output, dumpDirectory + "kernel.spv", std::ios::out | std::ios::binary); output.write(il.data(), ilSize); } } @@ -8184,7 +8189,8 @@ void CLIntercept::dumpCaptureReplayKernelSource( { for (size_t device = 0; device != num_devices; ++device) { - std::ofstream output(dumpDirectory + "DeviceBinary" + std::to_string(device) + ".bin", std::ios::out | std::ios::binary); + std::ofstream output; + Utils::OpenOutputFile(output, dumpDirectory + "DeviceBinary" + std::to_string(device) + ".bin", std::ios::out | std::ios::binary); output.write(reinterpret_cast(binaries[device].data()), binaries[device].size()); } } @@ -8201,7 +8207,8 @@ void CLIntercept::dumpCaptureReplayKernelInfo( const size_t* gws, const size_t* lws ) { - std::ofstream output{dumpDirectory + "worksizes.txt"}; + std::ofstream output; + Utils::OpenOutputFile(output, dumpDirectory + "worksizes.txt"); // Print the values of the worksizes and offsets on a line in the order: // gws @@ -8236,7 +8243,8 @@ void CLIntercept::dumpCaptureReplayKernelInfo( dispatch().clGetContextInfo(context, CL_CONTEXT_DEVICES, sizeof(cl_device_id), &device, nullptr); { - std::ofstream outputBuildOptions{dumpDirectory + "buildOptions.txt"}; + std::ofstream outputBuildOptions; + Utils::OpenOutputFile(outputBuildOptions, dumpDirectory + "buildOptions.txt"); size_t buildOptionsSize = 0; dispatch().clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_OPTIONS, 0, nullptr, &buildOptionsSize); @@ -8251,7 +8259,8 @@ void CLIntercept::dumpCaptureReplayKernelInfo( } std::string kernelName = getShortKernelName(kernel); - std::ofstream outputKernelName{dumpDirectory + "kernelName.txt"}; + std::ofstream outputKernelName; + Utils::OpenOutputFile(outputKernelName, dumpDirectory + "kernelName.txt"); outputKernelName << kernelName; cl_uint numArgs = 0; @@ -8259,7 +8268,8 @@ void CLIntercept::dumpCaptureReplayKernelInfo( if( numArgs ) { - std::ofstream outputArgTypes{dumpDirectory + "ArgumentDataTypes.txt"}; + std::ofstream outputArgTypes; + Utils::OpenOutputFile(outputArgTypes, dumpDirectory + "ArgumentDataTypes.txt"); for( cl_uint idx = 0; idx != numArgs; ++idx ) { std::string argTypeName; @@ -8287,7 +8297,8 @@ void CLIntercept::dumpCaptureReplayKernelArguments( const auto index = arg.first; const auto& value = arg.second; std::string fileName{dumpDirectory + "Argument" + std::to_string(index) + ".bin"}; - std::ofstream out{fileName, std::ios::out | std::ios::binary}; + std::ofstream out; + Utils::OpenOutputFile(out, fileName, std::ios::out | std::ios::binary); out.write(reinterpret_cast(value.data()), value.size()); } @@ -8297,7 +8308,8 @@ void CLIntercept::dumpCaptureReplayKernelArguments( const auto index = arg.first; const auto value = arg.second; std::string fileName{dumpDirectory + "Local" + std::to_string(index) + ".txt"}; - std::ofstream out{fileName}; + std::ofstream out; + Utils::OpenOutputFile(out, fileName); out << std::to_string(value); } @@ -8310,7 +8322,8 @@ void CLIntercept::dumpCaptureReplayKernelArguments( { const SImageInfo& info = m_ImageInfoMap[ value ]; std::string fileName{dumpDirectory + "Image_MetaData_" + std::to_string(index) + ".txt"}; - std::ofstream out{fileName}; + std::ofstream out; + Utils::OpenOutputFile(out, fileName); out << info.Region[0] << '\n' << info.Region[1] << '\n' << info.Region[2] << '\n' @@ -8324,7 +8337,8 @@ void CLIntercept::dumpCaptureReplayKernelArguments( else { std::string fileName{dumpDirectory + "SVM_Arg_Offset_" + std::to_string(index) + ".txt"}; - std::ofstream out{fileName}; + std::ofstream out; + Utils::OpenOutputFile(out, fileName); out << arg.second.Offset << '\n'; } } @@ -8335,7 +8349,8 @@ void CLIntercept::dumpCaptureReplayKernelArguments( const auto index = arg.first; const auto& value = arg.second; std::string fileName{dumpDirectory + "Sampler" + std::to_string(index) + ".txt"}; - std::ofstream out{fileName}; + std::ofstream out; + Utils::OpenOutputFile(out, fileName); out << value; } } @@ -9759,12 +9774,14 @@ void CLIntercept::startCaptureReplay( pPythonScript, pythonScriptLength ) ) { - std::ofstream outputPythonScript{fileNamePrefix + "run.py", std::ios::out | std::ios::binary}; + std::ofstream outputPythonScript; + Utils::OpenOutputFile(outputPythonScript, fileNamePrefix + "run.py", std::ios::out | std::ios::binary); outputPythonScript.write(pPythonScript, pythonScriptLength); } { - std::ofstream outputKernelNumber{fileNamePrefix + "enqueueNumber.txt"}; + std::ofstream outputKernelNumber; + Utils::OpenOutputFile(outputKernelNumber, fileNamePrefix + "enqueueNumber.txt"); outputKernelNumber << std::to_string(enqueueCounter) << '\n'; } diff --git a/intercept/src/utils.cpp b/intercept/src/utils.cpp index 69c5d8af..34ab9eac 100644 --- a/intercept/src/utils.cpp +++ b/intercept/src/utils.cpp @@ -10,6 +10,10 @@ #include #include +#if defined(_WIN32) +#include +#endif + #ifdef __cpp_lib_bitops #include #endif @@ -110,4 +114,28 @@ uint32_t CountLeadingZeroes(uint64_t value) #endif } +void OpenOutputFile( + std::ofstream& os, + const std::string& fileName, + std::ios::openmode mode ) +{ +#if defined(_WIN32) + // Use the \\?\ prefix with the wide-character API to support file paths + // longer than MAX_PATH (260 characters). The \\?\ prefix requires + // backslashes, so convert any forward slashes in the path. + { + std::string longPath = "\\\\?\\" + fileName; + std::replace( longPath.begin(), longPath.end(), '/', '\\' ); + int wLen = MultiByteToWideChar( + CP_UTF8, 0, longPath.c_str(), -1, NULL, 0 ); + std::wstring wFileName( wLen, 0 ); + MultiByteToWideChar( + CP_UTF8, 0, longPath.c_str(), -1, &wFileName[0], wLen ); + os.open( wFileName.c_str(), mode ); + } +#else + os.open( fileName.c_str(), mode ); +#endif +} + } // namespace Utils \ No newline at end of file diff --git a/intercept/src/utils.h b/intercept/src/utils.h index 68f1157f..86a40694 100644 --- a/intercept/src/utils.h +++ b/intercept/src/utils.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include namespace Utils @@ -15,4 +16,11 @@ namespace Utils std::string GetUniqueFileName(const std::string& fileName); uint32_t CountLeadingZeroes(uint64_t value); +// Opens an output file stream, using the \\?\ long-path prefix on Windows +// to support paths longer than MAX_PATH (260 characters). +void OpenOutputFile( + std::ofstream& os, + const std::string& fileName, + std::ios::openmode mode = std::ios::out ); + }