Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ target
.vs/
.idea/
build/
*.class
*.class
.DS_Store
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ set(CMAKE_VERBOSE_MAKEFILE ON)

project(LocalJStack)

add_subdirectory(src/main/cpp)
add_subdirectory(src/main/cpp)
add_subdirectory(src/test/cpp)
35 changes: 27 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <linker-defined>
```

再是获取`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;

Expand Down
46 changes: 46 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions script/Get-Offset-ThreadDump.ps1
Original file line number Diff line number Diff line change
@@ -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)
}
66 changes: 66 additions & 0 deletions script/testThreadDump.ps1
Original file line number Diff line number Diff line change
@@ -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 ..
}

6 changes: 5 additions & 1 deletion src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
cmake_minimum_required(VERSION 3.16)
project(LibLocalJStack LANGUAGES CXX)

file(GLOB LibLocalJStack_SOURCES *.cpp)
Expand All @@ -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)
Expand Down
39 changes: 11 additions & 28 deletions src/main/cpp/app_LocalJStack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#ifdef OS_WINDOWS
#include <windows.h>
#include <psapi.h>

#elif defined(OS_MACOS)
#include <mach-o/dyld.h>
#include <mach-o/getsect.h>
Expand All @@ -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<uintptr_t>(hModules[i]);
}
}
}
}
HMODULE hModule = GetModuleHandleA("jvm.dll");

// 如果未找到,返回 0
return 0;
return reinterpret_cast<uintptr_t>(hModule);
}
#elif defined(OS_MACOS)
uintptr_t getLibraryBaseAddress(const std::string& libraryName)
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/app/LocalJStack.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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"));
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/app/PrintStack.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading