# 3.4 Attach开源工具
# 3.3.1 使用golang实现Attach注入工具
上一节中,详细分析了Attach通信建立和发送数据全过程,本节将使用Golang语言构建实现一个轻量级的Attach工具,并使用Attach工具获取目标JVM的堆栈信息。代码来源于开源项目:https://github.com/tokuhirom/go-hsperfdata
# 3.3.1.1 建立通信
- 执行attach
代码位置:attach/attach_linux.go
// 执行attach
func force_attach(pid int) error {
// 进程的工作目录下创建.attach_pid<pid>文件
attach_file := fmt.Sprintf("/proc/%d/cwd/.attach_pid%d", pid, pid)
f, err := os.Create(attach_file)
if err != nil {
return fmt.Errorf("Canot create file:%v:%v", attach_file, err)
}
f.Close()
// 给目标JVM发送SIGQUIT信号
err = syscall.Kill(pid, syscall.SIGQUIT)
if err != nil {
return fmt.Errorf("Canot send sigkill:%v:%v", pid, err)
}
// 检查.java_pid<pid>文件是否存在
sockfile := filepath.Join(os.TempDir(), fmt.Sprintf(".java_pid%d", pid))
for i := 0; i < 10; i++ {
if exists(sockfile) {
return nil
}
time.Sleep(200 * time.Millisecond)
}
return fmt.Errorf("Canot attach process:%v", pid)
}
// 建立与目标JVM的UDS通信
func GetSocketFile(pid int) (string, error) {
sockfile := filepath.Join(os.TempDir(), fmt.Sprintf(".java_pid%d", pid))
if !exists(sockfile) {
err := force_attach(pid)
if err != nil {
return "", err
}
}
return sockfile, nil
}
func exists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- 连接到目标JVM的UDS上
代码位置:attach/attach_linux.go
// 连接UDS
func New(pid int) (*Socket, error) {
sockfile, err := GetSocketFile(pid)
if err != nil {
return nil, err
}
addr, err := net.ResolveUnixAddr("unix", sockfile)
if err != nil {
return nil, err
}
c, err := net.DialUnix("unix", nil, addr)
if err != nil {
return nil, err
}
return &Socket{c}, nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
force_attach
方法创建attach_pid 文件并向目标JVM发送kill -3信号,然后连接到目标JVM创建的UDS上。
# 3.3.1.2 发送命令和参数
代码位置:attach/attach.go
const PROTOCOL_VERSION = "1"
const ATTACH_ERROR_BADVERSION = 101
type Socket struct {
sock *net.UnixConn
}
// 执行命令
func (sock *Socket) Execute(cmd string, args ...string) error {
// 写入协议版本
err := sock.writeString(PROTOCOL_VERSION)
if err != nil {
return err
}
// 写入命令字符串
err = sock.writeString(cmd)
if err != nil {
return err
}
// 写入参数
for i := 0; i < 3; i++ {
if len(args) > i {
err = sock.writeString(args[i])
if err != nil {
return err
}
} else {
err = sock.writeString("")
if err != nil {
return err
}
}
}
// 读取执行结果
i, err := sock.readInt()
if i != 0 {
if i == ATTACH_ERROR_BADVERSION {
return fmt.Errorf("Protocol mismatch with target VM")
} else {
return fmt.Errorf("Command failed in target VM")
}
}
return err
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
上面代码主要功能是Execute
方法, 该方法向socket写入指定的字符序列。
# 3.3.1.3 获取目标JVM的堆栈信息
再来看下main方法,接受pid参数并dump目标jvm的堆栈信息。
// threaddump
func main() {
if len(os.Args) == 1 {
fmt.Printf("Usage: jstack pid\n")
os.Exit(1)
}
pid, err := strconv.Atoi(os.Args[1])
if err != nil {
log.Fatal("invalid pid: %v", err)
}
sock, err := attach.New(pid)
if err != nil {
log.Fatalf("cannot open unix socket: %s", err)
}
err = sock.Execute("threaddump")
if err != nil {
log.Fatalf("cannot write to unix socket: %s", err)
}
stack, err := sock.ReadString()
fmt.Printf("%s\n", stack)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
输出结果:
$ ./main 75193
2023-07-29 01:58:32
Full thread dump Java HotSpot(TM) 64-Bit Server VM (11.0.2+9-LTS mixed mode):
Threads class SMR info:
_java_thread_list=0x00007fc8a5f83fe0, length=11, elements={
0x00007fc8a68e4800, 0x00007fc8a68e9800, 0x00007fc8a705f000, 0x00007fc8a7055000,
0x00007fc8a7062000, 0x00007fc8a68f3800, 0x00007fc8a6068800, 0x00007fc8a8043800,
0x00007fc8a68e6800, 0x00007fc8a9813800, 0x00007fc8a71ac000
}
"Signal Dispatcher" #4 daemon prio=9 os_prio=31 cpu=12.90ms elapsed=236130.65s tid=0x00007fc8a705f000 nid=0x3c03 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #5 daemon prio=9 os_prio=31 cpu=1845.75ms elapsed=236130.65s tid=0x00007fc8a7055000 nid=0x3d03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
No compile task
// 篇幅有限省略...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 3.3.2 jattach
# 3.3.2.1 简介
jattach是一个不依赖于jdk/jre的运行时注入工具,并且具备jmap、jstack、jcmd和jinfo等功能,同时支持linux、windows和macos等操作系统。项目地址:https://github.com/jattach/jattach
# 3.3.2.2 源码解析
代码位置:src/posix/jattach.c
int jattach(int pid, int argc, char** argv) {
// 获取attach进程和目标JVM进程的用户权限
uid_t my_uid = geteuid();
gid_t my_gid = getegid();
uid_t target_uid = my_uid;
gid_t target_gid = my_gid;
int nspid;
if (get_process_info(pid, &target_uid, &target_gid, &nspid) < 0) {
fprintf(stderr, "Process %d not found\n", pid);
return 1;
}
// Container support: switch to the target namespaces.
// Network and IPC namespaces are essential for OpenJ9 connection.
enter_ns(pid, "net");
enter_ns(pid, "ipc");
int mnt_changed = enter_ns(pid, "mnt");
// In HotSpot, dynamic attach is allowed only for the clients with the same euid/egid.
// If we are running under root, switch to the required euid/egid automatically.
// 这里做进程权限切换
// 在HotSpot虚拟机上,动态attach需要发起attach的进程与目标进程具备相同的权限
// 如果attach进程权限是root(特权进程),可以实现自动切换到目标进程权限
if ((my_gid != target_gid && setegid(target_gid) != 0) ||
(my_uid != target_uid && seteuid(target_uid) != 0)) {
perror("Failed to change credentials to match the target process");
return 1;
}
get_tmp_path(mnt_changed > 0 ? nspid : pid);
// Make write() return EPIPE instead of abnormal process termination
signal(SIGPIPE, SIG_IGN);
if (is_openj9_process(nspid)) {
return jattach_openj9(pid, nspid, argc, argv);
} else {
return jattach_hotspot(pid, nspid, argc, argv);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
需要注意的是,在发起attach之前,需要将attach进程的权限设置为与目标JVM权限一致。 jattach给我们编译了各种平台的可执行文件,对于构建跨平台运行时注入工具很有用。我们仅需要使用即可,无需关心里面的实现。