diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a83734d..8054716 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,34 @@ env: BUILD_TYPE: Release jobs: + build-jar-without-native: + # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. + # You can convert this to a matrix build if you need cross-platform coverage. + # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + - run: mkdir -p build/src/main/cpp + - uses: actions/download-artifact@v4 + with: + path: build/src/main/cpp + pattern: native-* + merge-multiple: true + - run: ls -lh build/src/main/cpp + - run: mvn package + - run: mkdir staging && cp target/*.jar staging + - uses: actions/upload-artifact@v4 + with: + name: jar-without-native + path: staging build-native: + needs: build-jar-without-native runs-on: ${{ matrix.os }} if: ${{!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')}} strategy: @@ -44,16 +71,22 @@ jobs: # Build your program with the given configuration run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - - name: Test + - uses: actions/download-artifact@v4 + with: + path: ${{github.workspace}}/build + name: jar-without-native + + - name: Test Unix working-directory: ${{github.workspace}}/build + if: runner.os != 'Windows' # Execute tests defined by the CMake configuration. # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail run: ctest -C ${{env.BUILD_TYPE}} - - name: Windows tweak + - name: Test Windows if: runner.os == 'Windows' run: | - dir ${{github.workspace}}/build/src/main/cpp/Release/ + dir ${{github.workspace}}/build move ${{github.workspace}}/build/src/main/cpp/Release/localjstack.dll ${{github.workspace}}/build/src/main/cpp/localjstack.dll - name: Archive artifacts diff --git a/.gitignore b/.gitignore index f3303af..c4aca16 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ target .vs/ .idea/ build/ -*.class \ No newline at end of file +*.class +.DS_Store \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index e295538..2903e83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,4 +4,5 @@ set(CMAKE_VERBOSE_MAKEFILE ON) project(LocalJStack) -add_subdirectory(src/main/cpp) \ No newline at end of file +add_subdirectory(src/main/cpp) +add_subdirectory(src/test/cpp) diff --git a/README.md b/README.md index 3ebf6e1..c43a353 100644 --- a/README.md +++ b/README.md @@ -6,21 +6,40 @@ Thread dump like jstack in local JVM process (not interprocess like jstack in jd ## 支持的平台 Supported Platform -- linux-x86_64 -- macos-x86_64 +- linux x64 +- macos x64 +- windows x64 ## 上手 Get Started 1. 首先找到JRE环境的`libjvm.so`中的[thread_dump(AttachOperation*, outputStream*)](https://github.com/openjdk/jdk/blob/742e735d7f6c4ee9ca5a4d290c59d7d6ec1f7635/src/hotspot/share/services/attachListener.cpp#L209)函数的偏移量 - 以21.0.5-ms为例,为`4892880`: + - unix平台,以21.0.5-ms为例,为`4892880`: - ``` - $ nm -t d -C /usr/local/sdkman/candidates/java/21.0.5-ms/lib/server/libjvm.so |grep -F 'thread_dump(AttachOperation*, outputStream*)' - 0000000004892880 t thread_dump(AttachOperation*, outputStream*) - ``` + ```cmd + $ nm -t d -C /usr/local/sdkman/candidates/java/21.0.5-ms/lib/server/libjvm.so |grep -F 'thread_dump(AttachOperation*, outputStream*)' + 0000000004892880 t thread_dump(AttachOperation*, outputStream*) + ``` + + - Windows平台,则需要借助map文件来获取函数偏移量,例如[microsoft-jdk-21.0.3-windows-x64.zip](https://aka.ms/download-jdk/microsoft-jdk-21.0.3-windows-x64.zip),需要下载微软提供的调试信息[microsoft-jdk-debugsymbols-21.0.3-windows-x64.zip](https://aka.ms/download-jdk/microsoft-jdk-debugsymbols-21.0.3-windows-x64.zip),然后解压调试信息 + + 首先是获取`ImageBase`,如下命令的结果第三列`0000000180000000`就是`ImageBase`的Rva+Base 16进制形式: + + ``` + findstr "__ImageBase" C:\jdk-21.0.3+9-debug-symbols\bin\server\jvm.dll.map + 0000:00000000 __ImageBase 0000000180000000 + ``` + + 再是获取`thread_dump(AttachOperation*, outputStream*)`方法在dll中的地址,如下命令的结果第三列`00000001801315e0`就是`thread_dump`函数Rva+Base16进制形式: + + ```cmd + findstr "?thread_dump@@YAJPEAVAttachOperation@@PEAVoutputStream@@@Z" C:\jdk-21.0.3+9-debug-symbols\bin\server\jvm.dll.map | findstr /V unwind + 0001:001305e0 ?thread_dump@@YAJPEAVAttachOperation@@PEAVoutputStream@@@Z 00000001801315e0 f attachListener.obj + ``` + + 然后两者做减法:`00000001801315e0 - 0000000180000000`的结果`1315e0`就是`thread_dump`函数在`jvm.dll`中的偏移量 -3. 使用java.io.Writer来承接线程栈 +2. 使用java.io.Writer来承接线程栈 ```java import java.io.StringWriter; diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..183dcd2 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,46 @@ +# https://github.com/gabime/spdlog/blob/v1.x/appveyor.yml +version: 1.0.{build} + +# Do not build feature branch with open Pull Requests +skip_branch_with_pr: true + +only_commits: + message: /build/ + +image: Visual Studio 2017 +environment: + matrix: + - GENERATOR: '"Visual Studio 17 2022" -A x64' + BUILD_TYPE: Release + CXX_STANDARD: 20 + APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022 +build_script: + - cmd: >- + set + + mkdir build + + cd build + + set PATH=%PATH%;C:\Program Files\Git\usr\bin + + cmake -G %GENERATOR% -D CMAKE_BUILD_TYPE=%BUILD_TYPE% -D CMAKE_CXX_STANDARD=%CXX_STANDARD% .. + + cmake --build . --config %BUILD_TYPE% --target LibLocalJStack + + ls src\main\cpp\Release\ + + cd .. + + mvn -DskipTests=true package + +test_script: + - ps: >- + mkdir -Force ./build/jdks + + ./script/testThreadDump.ps1 -jdkPath ./build/jdks -jdkVersions 11.0.26, 17.0.14, 21.0.3 -jarPath ./target/localjstack-0.0.1.jar -dllPath ./build/src/main/cpp/Release/localjstack.dll -download + +artifacts: + + - path: build\src\main\cpp\Release\localjstack.dll + - path: target\localjstack-0.0.1.jar \ No newline at end of file diff --git a/script/Get-Offset-ThreadDump.ps1 b/script/Get-Offset-ThreadDump.ps1 new file mode 100644 index 0000000..8051a0d --- /dev/null +++ b/script/Get-Offset-ThreadDump.ps1 @@ -0,0 +1,34 @@ +function Get-Offset-ThreadDump{ + param( + [Parameter(Mandatory=$true)][string]$debugSymbolsPath, + [Parameter(Mandatory=$true)][string]$jdkVersion + ) + + $jvmDllMap = If ($jdkVersion.StartsWith("21")) {"jvm.dll.map"} Else {"jvm.map"} + + Write-Host $jvmDllMap + + $foundFiles = Get-ChildItem -Path $debugSymbolsPath -Recurse -Filter $jvmDllMap + + if ($null -eq $foundFiles) { + Write-Host "file not found: $jvmDllMap in $debugSymbolsPath" + exit 1 + } + + $jvmDllMapPath = $foundFiles[0] + Write-Host "jvmDllMap found: "$jvmDllMapPath.FullName + + # __ImageBase + + $imageBaseLine = $(Select-String -SimpleMatch "__ImageBase" -Path $jvmDllMapPath).Line + + $imageBase = $(-split $imageBaseLine)[2] + + $threadDumpAddressLine = $(Select-String -SimpleMatch "?thread_dump@@YAJPEAVAttachOperation@@PEAVoutputStream@@@Z" -Path $jvmDllMapPath | Where-Object { $_ -notmatch "unwind" }).Line + + $threadDumpAddress = $(-split $threadDumpAddressLine)[2] + + $offsetInt = [System.Convert]::ToInt64($threadDumpAddress, 16) - [System.Convert]::ToInt64($imageBase, 16) + + return [System.Convert]::ToString($offsetInt, 16) +} \ No newline at end of file diff --git a/script/testThreadDump.ps1 b/script/testThreadDump.ps1 new file mode 100644 index 0000000..a64112a --- /dev/null +++ b/script/testThreadDump.ps1 @@ -0,0 +1,66 @@ +#.\testThreadDump.ps1 -jdkPath C:\download -jdkVersions 11.0.26, 21.0.3 -jarPath C:\download\localjstack-0.0.1.jar -dllPath C:\download\local_jstack\build\x86_windows_msvc2022_pe_32bit-Debug\src\main\cpp\localjstack.dll + +param( +[Parameter(Mandatory=$true)][string[]]$jdkVersions, +[Parameter(Mandatory=$true)][string]$jarPath, +[Parameter(Mandatory=$true)][string]$dllPath, +[switch]$download, +[Parameter(Mandatory=$true)][string]$jdkPath +) + +$jarPath = resolve-path $jarPath +$dllPath = [System.IO.Path]::GetDirectoryName($(Resolve-Path $dllPath)) + +Write-Host $dllPath + +. ($PSScriptRoot + "\Get-Offset-ThreadDump.ps1") + +cd $jdkPath + +foreach ($i in $jdkVersions) +{ + $jdkUrl = "https://aka.ms/download-jdk/microsoft-jdk-$i-windows-x64.zip" + $debugSymbolsUrl = "https://aka.ms/download-jdk/microsoft-jdk-debugsymbols-$i-windows-x64.zip" + + Write-Host "jdkVersion: $i" + + mkdir -Force $i + cd $i + if ($download) { + Write-Host $jdkUrl + Invoke-WebRequest $jdkUrl -OutFile jdk.zip + 7z x jdk.zip -ojdk + } + + $foundJavaExes = Get-ChildItem -Path jdk -Recurse -Filter java.exe + + if ($null -eq $foundJavaExes) { + Write-Host "file not found: java.exe in jdk" + exit 1 + } + $javaExe = $foundJavaExes[0] + Write-Host "java found: "$javaExe.FullName + + if ($download) { + Write-Host $debugSymbolsUrl + Invoke-WebRequest $debugSymbolsUrl -OutFile debugsymbols.zip + 7z x debugsymbols.zip -odebugsymbols + } + $threadDumpOffset = Get-Offset-ThreadDump -debugSymbolsPath debugsymbols -jdkVersion $i + Write-Host "threadDumpOffset found: $threadDumpOffset" + + $command = "`"-Dlocaljstack.threadDumpOffset=$threadDumpOffset`" `"-Djava.library.path=$dllPath`" -cp $jarPath app.PrintStack" + Write-Host $javaExe.FullName $command + + $proc = Start-Process -FilePath $javaExe.FullName -ArgumentList $command -PassThru -Wait + + if ($proc.ExitCode -ne 0) { + Write-Warning "$i exit with status code $($proc.ExitCode)" + exit $proc.ExitCode + } + + Write-Host "jdkVersion passed: $i" + + cd .. +} + diff --git a/src/main/cpp/CMakeLists.txt b/src/main/cpp/CMakeLists.txt index 3f5d7a5..a7314f6 100644 --- a/src/main/cpp/CMakeLists.txt +++ b/src/main/cpp/CMakeLists.txt @@ -1,4 +1,3 @@ -cmake_minimum_required(VERSION 3.16) project(LibLocalJStack LANGUAGES CXX) file(GLOB LibLocalJStack_SOURCES *.cpp) @@ -10,6 +9,11 @@ if(MSVC) target_link_options(LibLocalJStack PRIVATE /MAP:localjstack.map /MAPINFO:EXPORTS) endif() +# 设置头文件路径 +target_include_directories(LibLocalJStack PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + # link jni set(JAVA_AWT_LIBRARY NotNeeded) set(JAVA_JVM_LIBRARY NotNeeded) diff --git a/src/main/cpp/app_LocalJStack.cpp b/src/main/cpp/app_LocalJStack.cpp index 0a73da9..3279c19 100644 --- a/src/main/cpp/app_LocalJStack.cpp +++ b/src/main/cpp/app_LocalJStack.cpp @@ -18,6 +18,7 @@ #ifdef OS_WINDOWS #include #include + #elif defined(OS_MACOS) #include #include @@ -31,32 +32,15 @@ typedef jint (*ThreadDumpFunc)(AttachOperation *, outputStream *); static ThreadDumpFunc thread_dump = nullptr; +#define MYLIB_EXPORTS + #ifdef OS_WINDOWS uintptr_t getLibraryBaseAddress(const std::string& libraryName) { -// 获取当前进程句柄 - HANDLE hProcess = GetCurrentProcess(); - - // 枚举当前进程加载的模块 - HMODULE hModules[1024]; - DWORD cbNeeded; - if (EnumProcessModules(hProcess, hModules, sizeof(hModules), &cbNeeded)) { - for (DWORD i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) { - char moduleName[MAX_PATH]; - // 获取模块的完整路径 - if (GetModuleFileNameExA(hProcess, hModules[i], moduleName, sizeof(moduleName))) { - // 检查模块名称是否匹配 - std::string currentModuleName(moduleName); - if (currentModuleName.find(libraryName) != std::string::npos) { - // 返回模块的基地址 - return reinterpret_cast(hModules[i]); - } - } - } - } + HMODULE hModule = GetModuleHandleA("jvm.dll"); // 如果未找到,返回 0 - return 0; + return reinterpret_cast(hModule); } #elif defined(OS_MACOS) uintptr_t getLibraryBaseAddress(const std::string& libraryName) @@ -109,17 +93,16 @@ uintptr_t getLibraryBaseAddress(const std::string& libraryName) JNIEXPORT void JNICALL Java_app_LocalJStack_init(JNIEnv *env, jclass cls, jlong threadDumpOffset) { - uintptr_t libjvm_base = #ifdef OS_WINDOWS - getLibraryBaseAddress("libjvm.dll"); + // uintptr_t libjvm_base = getLibraryBaseAddress("jvm.dll"); + HMODULE hModule = GetModuleHandleA("jvm.dll"); + FARPROC funcAddr = (FARPROC)((BYTE*)hModule + threadDumpOffset); + thread_dump = (ThreadDumpFunc)funcAddr; #elif defined(OS_MACOS) - getLibraryBaseAddress("libjvm.dylib"); + thread_dump = (ThreadDumpFunc)(getLibraryBaseAddress("libjvm.dylib") + threadDumpOffset); #elif defined(OS_LINUX) - getLibraryBaseAddress("libjvm.so"); + thread_dump =(ThreadDumpFunc)(getLibraryBaseAddress("libjvm.so") + threadDumpOffset); #endif - uintptr_t threadDumpAddress = threadDumpOffset + libjvm_base; - - thread_dump = (ThreadDumpFunc)threadDumpAddress; } JNIEXPORT jint JNICALL Java_app_LocalJStack_dumpStack(JNIEnv *env, jclass cls, jobject writer) diff --git a/src/main/java/app/LocalJStack.java b/src/main/java/app/LocalJStack.java index 9fb5f6e..75fd46a 100644 --- a/src/main/java/app/LocalJStack.java +++ b/src/main/java/app/LocalJStack.java @@ -52,7 +52,9 @@ private static void loadLib() { static { loadLib(); - long threadDumpOffset = Long.valueOf(System.getProperty("localjstack.threadDumpOffset", "0")); + String threadDumpOffsetInHex = System.getProperty("localjstack.threadDumpOffset"); + + long threadDumpOffset = Long.parseLong(threadDumpOffsetInHex, 16); init(threadDumpOffset); } @@ -83,6 +85,9 @@ private static String getDynamicLibraryName() { case Linux: return "liblocaljstack.so.0.1"; + + case Windows: + return "localjstack.dll"; default: throw new RuntimeException("Unsupported OS: " + System.getProperty("os.name")); diff --git a/src/main/java/app/PrintStack.java b/src/main/java/app/PrintStack.java new file mode 100644 index 0000000..27c3abd --- /dev/null +++ b/src/main/java/app/PrintStack.java @@ -0,0 +1,11 @@ +package app; + +import java.io.StringWriter; + +class PrintStack { + public static void main(String[] args) { + StringWriter writer = new StringWriter();; + LocalJStack.dumpStack(writer); + System.out.println(writer); + } +} diff --git a/src/test/cpp/CMakeLists.txt b/src/test/cpp/CMakeLists.txt new file mode 100644 index 0000000..3b3676f --- /dev/null +++ b/src/test/cpp/CMakeLists.txt @@ -0,0 +1,14 @@ +project(LocalJStackTest LANGUAGES CXX) + +# only test for windows +if(MSVC) + find_package(Java REQUIRED) + find_package(JNI REQUIRED) + include_directories(${JNI_INCLUDE_DIRS}) + + aux_source_directory(. DIR_SRCS) + add_executable(LocalJStackTest main.cpp) + + target_include_directories(LocalJStackTest PUBLIC LibLocalJStack ${JNI_INCLUDE_DIRS}) + target_link_libraries(LocalJStackTest LibLocalJStack ${JNI_LIBRARIES}) +endif() diff --git a/src/test/cpp/main.cpp b/src/test/cpp/main.cpp new file mode 100644 index 0000000..a47191f --- /dev/null +++ b/src/test/cpp/main.cpp @@ -0,0 +1,52 @@ +#include +#include + +#define VM_ARG_nOptions 4 + +int main(int argc, char *argv[]) +{ + printf("main\n"); + JavaVMInitArgs vm_args; + JavaVMOption options[VM_ARG_nOptions]; + + options[0].optionString = "-Dlocaljstack.threadDumpOffset=116b50"; + options[1].optionString = "-Djava.library.path=C:\\download\\local_jstack\\build\\x86_windows_msvc2022_pe_32bit-Debug\\src\\main\\cpp"; /* set native library path */ + options[2].optionString = "-Djava.class.path=C:\\download\\localjstack-0.0.1.jar"; + options[3].optionString = "-verbose:jni"; /* print JNI-related messages */ + + vm_args.version = JNI_VERSION_1_8; + vm_args.options = options; + vm_args.nOptions = VM_ARG_nOptions; + vm_args.ignoreUnrecognized = 0; + + JavaVM *vm; /* denotes a Java VM */ + JNIEnv *env; + + jint res = JNI_CreateJavaVM(&vm, (void **)&env, &vm_args); + + if (res != JNI_OK) { + printf("Failed to create Java VMn"); + return 1; + } + + jclass cls; + jmethodID mid; + + cls = (*env).FindClass("app/PrintStack"); + if (cls == NULL) { + printf("Failed to find app.PrintStack class\n"); + return 1; + } + + mid = (*env).GetStaticMethodID(cls, "main", "([Ljava/lang/String;)V"); + if (mid == NULL) { + printf("Failed to find main function\n"); + return 1; + } + + jstring jstr = (*env).NewStringUTF(""); + jobjectArray main_args = (*env).NewObjectArray(1, (*env).FindClass("java/lang/String"), jstr); + (*env).CallStaticVoidMethod(cls, mid, main_args); + + return 0; +}