# 3.3 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文件
	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文件是否存在
	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
  • 连接到目标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

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

上面代码主要功能是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

输出结果:

$ ./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

# 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

需要注意的是,在发起attach之前,需要将attach进程的权限设置为与目标JVM权限一致。 jattach给我们编译了各种平台的可执行文件,对于构建跨平台运行时注入工具很有用。我们仅需要使用即可,无需关心里面的实现。