当前位置:   article > 正文

javaagent技术原理

javaagent技术原理

前言

说道Javaagent是最近经常在使用这个技术,顺便了解了原理与根源,实际上就是jvm开个代理字节码修改的instrument接口。但实际上使用,根据使用的方式不同而略有区别。

1. Javaagent使用

实际上,笔者在前段时间写了arthas的启动原理(83条消息) arthas 启动原理分析_fenglllle的博客-CSDN博客,简单的说明了Javaagent的2种方式,jvm参数方式与动态attach。

以动态attach为例,实际上以jvm参数的agent类似,动态attach支持远程attach。

1.1 agent jar,demo

  1. public class AgentMainDemo {
  2. private static synchronized void main(String args, Instrumentation inst) {
  3. try {
  4. System.out.println("agent exec ......");
  5. inst.addTransformer(new ClassFileTransformer() {
  6. @Override
  7. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  8. //字节码修改,替换
  9. System.out.println("------ byte instead -----");
  10. return new byte[0];
  11. }
  12. }, true);
  13. Class<?> clazz = Class.forName("com.feng.agent.demo.ReTransformDemo");
  14. inst.retransformClasses(clazz);
  15. } catch (ClassNotFoundException | UnmodifiableClassException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. public static void premain(String args, Instrumentation inst) {
  20. main(args, inst);
  21. }
  22. public static void agentmain(String args, Instrumentation inst) {
  23. main(args, inst);
  24. }
  25. }

pom打包manifest支持

  1. <plugin>
  2. <groupId>org.apache.maven.plugins</groupId>
  3. <artifactId>maven-assembly-plugin</artifactId>
  4. <executions>
  5. <execution>
  6. <goals>
  7. <goal>single</goal>
  8. </goals>
  9. <phase>package</phase>
  10. <configuration>
  11. <descriptorRefs>
  12. <descriptorRef>jar-with-dependencies</descriptorRef>
  13. </descriptorRefs>
  14. <archive>
  15. <manifestEntries>
  16. <Premain-Class>com.feng.agent.demo.AgentMainDemo</Premain-Class>
  17. <Agent-Class>com.feng.agent.demo.AgentMainDemo</Agent-Class>
  18. <Can-Retransform-Classes>true</Can-Retransform-Classes>
  19. </manifestEntries>
  20. </archive>
  21. </configuration>
  22. </execution>
  23. </executions>
  24. </plugin>

 1.2 运行的Java应用&tools.jar

  1. public class DemoMain {
  2. public static void main(String[] args) throws InterruptedException {
  3. System.out.println("I'm a app");
  4. Thread.sleep(100000000000l);
  5. }
  6. }

执行,可以debug执行都行。执行后pid笔者为 3041

tools.jar,需要载入才行

  1. public class AttachMain {
  2. public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
  3. VirtualMachine machine = null;
  4. try {
  5. machine = VirtualMachine.attach("3041");
  6. machine.loadAgent("/Users/huahua/IdeaProjects/java-agent-demo/attach-demo/src/main/resources/agent-demo-jar-with-dependencies.jar");
  7. } finally {
  8. if (machine != null) {
  9. machine.detach();
  10. }
  11. }
  12. }
  13. }

 1.3 执行结果

 可以看到agent exec 的字样,说明agent已经load了,且进行了字节码替换。实际上transform的ClassFileTransformer可以形成调用链,一个类可以被多次transform。transform默认是有

ClassFileTransformer的。

2. Javaagent原理

简单介绍Javaagent的原理:Javaagent分为jvm参数方式与动态attach方式

jvm参数方式:这种方式比较常用,因为可以通过启动参数内置

动态attach:这种方式比较灵活,可以多次attach,且可以销毁attach的agent。

实际上jvm加载逻辑差不多,这里以复杂的动态attach为例

关键还是:provider.attachVirtualMachine(id); 

  1. public static VirtualMachine attach(String id)
  2. throws AttachNotSupportedException, IOException
  3. {
  4. if (id == null) {
  5. throw new NullPointerException("id cannot be null");
  6. }
  7. List<AttachProvider> providers = AttachProvider.providers();
  8. if (providers.size() == 0) {
  9. throw new AttachNotSupportedException("no providers installed");
  10. }
  11. AttachNotSupportedException lastExc = null;
  12. for (AttachProvider provider: providers) {
  13. try {
  14. return provider.attachVirtualMachine(id);
  15. } catch (AttachNotSupportedException x) {
  16. lastExc = x;
  17. }
  18. }
  19. throw lastExc;
  20. }

然后进一步跟踪:

可以看到使用了SPI技术,笔者Mac系统,如果是Linux或者win,这里是不同的

 逻辑大同小异:

static {
    System.loadLibrary("attach");
    tmpdir = getTempDir();
}

先load c的lib,然后获取临时目录

  1. BsdVirtualMachine(AttachProvider provider, String vmid)
  2. throws AttachNotSupportedException, IOException
  3. {
  4. super(provider, vmid);
  5. // This provider only understands pids
  6. int pid;
  7. try {
  8. pid = Integer.parseInt(vmid);
  9. } catch (NumberFormatException x) {
  10. throw new AttachNotSupportedException("Invalid process identifier");
  11. }
  12. //这段注释很明显,先找socket文件,找不到就创建attach文件,发送quit信号,再试查找socket文件
  13. // Find the socket file.
  14. // If not found then we attempt to start the attach mechanism in the target VM by sending it a QUIT signal.
  15. // Then we attempt to find the socket file again.
  16. //查找socket文件
  17. path = findSocketFile(pid);
  18. if (path == null) {
  19. File f = new File(tmpdir, ".attach_pid" + pid);
  20. //创建attach文件
  21. createAttachFile(f.getPath());
  22. try {
  23. //发送退出信号,启动attach mechanism连接途径
  24. sendQuitTo(pid);
  25. // give the target VM time to start the attach mechanism
  26. int i = 0;
  27. long delay = 200;
  28. int retries = (int)(attachTimeout() / delay);
  29. do {
  30. try {
  31. Thread.sleep(delay);
  32. } catch (InterruptedException x) { }
  33. //多次查找socket文件
  34. path = findSocketFile(pid);
  35. i++;
  36. } while (i <= retries && path == null);
  37. if (path == null) {
  38. throw new AttachNotSupportedException(
  39. "Unable to open socket file: target process not responding " +
  40. "or HotSpot VM not loaded");
  41. }
  42. } finally {
  43. f.delete();
  44. }
  45. }
  46. // Check that the file owner/permission to avoid attaching to
  47. // bogus process
  48. checkPermissions(path);
  49. // Check that we can connect to the process
  50. // - this ensures we throw the permission denied error now rather than
  51. // later when we attempt to enqueue a command.
  52. //socket创建
  53. int s = socket();
  54. try {
  55. //连接socket,相当于远程(另一个jvm进程)连上了pid
  56. connect(s, path);
  57. } finally {
  58. close(s);
  59. }
  60. }
  61. // Return the socket file for the given process.
  62. // Checks temp directory for .java_pid<pid>.
  63. private String findSocketFile(int pid) {
  64. String fn = ".java_pid" + pid;
  65. File f = new File(tmpdir, fn);
  66. return f.exists() ? f.getPath() : null;
  67. }

 建立socket连接,就进行下一步,loadjar

实际上这里就可以看到是要加载instrument。执行load指令,拿到结果,实际上load jar加载结束,agent就注入生效了,这个过程是JDK触发完成

  1. private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String options)
  2. throws AgentLoadException, AgentInitializationException, IOException
  3. {
  4. InputStream in = execute("load",
  5. agentLibrary,
  6. isAbsolute ? "true" : "false",
  7. options);
  8. try {
  9. int result = readInt(in);
  10. if (result != 0) {
  11. throw new AgentInitializationException("Agent_OnAttach failed", result);
  12. }
  13. } finally {
  14. in.close();
  15. }
  16. }

继续load

  1. InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {
  2. assert args.length <= 3; // includes null
  3. // did we detach?
  4. String p;
  5. synchronized (this) {
  6. if (this.path == null) {
  7. throw new IOException("Detached from target VM");
  8. }
  9. p = this.path;
  10. }
  11. // create UNIX socket
  12. int s = socket();
  13. // connect to target VM
  14. try {
  15. connect(s, p);
  16. } catch (IOException x) {
  17. close(s);
  18. throw x;
  19. }
  20. IOException ioe = null;
  21. // connected - write request
  22. // <ver> <cmd> <args...>
  23. try {
  24. writeString(s, PROTOCOL_VERSION);
  25. writeString(s, cmd);
  26. for (int i=0; i<3; i++) {
  27. if (i < args.length && args[i] != null) {
  28. //把jar的路径写给JVM,就结束了,JVM指令执行load指令
  29. writeString(s, (String)args[i]);
  30. } else {
  31. writeString(s, "");
  32. }
  33. }
  34. } catch (IOException x) {
  35. ioe = x;
  36. }
  37. // Create an input stream to read reply
  38. SocketInputStream sis = new SocketInputStream(s);
  39. // Read the command completion status
  40. int completionStatus;
  41. try {
  42. completionStatus = readInt(sis);
  43. } catch (IOException x) {
  44. sis.close();
  45. if (ioe != null) {
  46. throw ioe;
  47. } else {
  48. throw x;
  49. }
  50. }
  51. if (completionStatus != 0) {
  52. sis.close();
  53. // In the event of a protocol mismatch then the target VM
  54. // returns a known error so that we can throw a reasonable
  55. // error.
  56. if (completionStatus == ATTACH_ERROR_BADVERSION) {
  57. throw new IOException("Protocol mismatch with target VM");
  58. }
  59. // Special-case the "load" command so that the right exception is
  60. // thrown.
  61. if (cmd.equals("load")) {
  62. throw new AgentLoadException("Failed to load agent library");
  63. } else {
  64. throw new IOException("Command failed in target VM");
  65. }
  66. }
  67. // Return the input stream so that the command output can be read
  68. return sis;
  69. }

 jdk里面如何执行的呢,打开OpenJDK InvocationAdapter.c,jvm参数加载的agent执行

Agent_OnLoad函数

而动态attach的agent,执行

Agent_OnAttach函数

之所以读取manifest文件是jdk定义的,这个是动态attach,读取Agent-Class,另外还有 boot-class-path

 

 下面才是核心

 3部曲

1. 创建InstrumentationImpl实例

2. 打开ClassFileLoadHook,这个与字节码替换回调相关

3. 启动agent,实际上是调用第一步创建InstrumentationImpl实例的loadClassAndCallAgentmain方法

  1. private void loadClassAndCallPremain(String var1, String var2) throws Throwable {
  2. this.loadClassAndStartAgent(var1, "premain", var2);
  3. }
  4. private void loadClassAndCallAgentmain(String var1, String var2) throws Throwable {
  5. this.loadClassAndStartAgent(var1, "agentmain", var2);
  6. }

另一个方法就是jvm参数方式的调用函数

3. idea debug

之所以说idea的debug能力是笔者在使用jmx技术时,发现

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/349066
推荐阅读
相关标签