赞
踩
说道Javaagent是最近经常在使用这个技术,顺便了解了原理与根源,实际上就是jvm开个代理字节码修改的instrument接口。但实际上使用,根据使用的方式不同而略有区别。
实际上,笔者在前段时间写了arthas的启动原理(83条消息) arthas 启动原理分析_fenglllle的博客-CSDN博客,简单的说明了Javaagent的2种方式,jvm参数方式与动态attach。
以动态attach为例,实际上以jvm参数的agent类似,动态attach支持远程attach。
- public class AgentMainDemo {
- private static synchronized void main(String args, Instrumentation inst) {
- try {
- System.out.println("agent exec ......");
- inst.addTransformer(new ClassFileTransformer() {
- @Override
- public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
- //字节码修改,替换
- System.out.println("------ byte instead -----");
- return new byte[0];
- }
- }, true);
- Class<?> clazz = Class.forName("com.feng.agent.demo.ReTransformDemo");
- inst.retransformClasses(clazz);
- } catch (ClassNotFoundException | UnmodifiableClassException e) {
- e.printStackTrace();
- }
- }
-
- public static void premain(String args, Instrumentation inst) {
- main(args, inst);
- }
-
- public static void agentmain(String args, Instrumentation inst) {
- main(args, inst);
- }
- }

pom打包manifest支持
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-assembly-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>single</goal>
- </goals>
- <phase>package</phase>
- <configuration>
- <descriptorRefs>
- <descriptorRef>jar-with-dependencies</descriptorRef>
- </descriptorRefs>
- <archive>
- <manifestEntries>
- <Premain-Class>com.feng.agent.demo.AgentMainDemo</Premain-Class>
- <Agent-Class>com.feng.agent.demo.AgentMainDemo</Agent-Class>
- <Can-Retransform-Classes>true</Can-Retransform-Classes>
- </manifestEntries>
- </archive>
- </configuration>
- </execution>
- </executions>
- </plugin>

- public class DemoMain {
- public static void main(String[] args) throws InterruptedException {
- System.out.println("I'm a app");
- Thread.sleep(100000000000l);
- }
- }
执行,可以debug执行都行。执行后pid笔者为 3041
tools.jar,需要载入才行
- public class AttachMain {
- public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
- VirtualMachine machine = null;
- try {
- machine = VirtualMachine.attach("3041");
- machine.loadAgent("/Users/huahua/IdeaProjects/java-agent-demo/attach-demo/src/main/resources/agent-demo-jar-with-dependencies.jar");
- } finally {
- if (machine != null) {
- machine.detach();
- }
- }
-
- }
- }
可以看到agent exec 的字样,说明agent已经load了,且进行了字节码替换。实际上transform的ClassFileTransformer可以形成调用链,一个类可以被多次transform。transform默认是有
ClassFileTransformer的。
简单介绍Javaagent的原理:Javaagent分为jvm参数方式与动态attach方式
jvm参数方式:这种方式比较常用,因为可以通过启动参数内置
动态attach:这种方式比较灵活,可以多次attach,且可以销毁attach的agent。
实际上jvm加载逻辑差不多,这里以复杂的动态attach为例
关键还是:provider.attachVirtualMachine(id);
- public static VirtualMachine attach(String id)
- throws AttachNotSupportedException, IOException
- {
- if (id == null) {
- throw new NullPointerException("id cannot be null");
- }
- List<AttachProvider> providers = AttachProvider.providers();
- if (providers.size() == 0) {
- throw new AttachNotSupportedException("no providers installed");
- }
- AttachNotSupportedException lastExc = null;
- for (AttachProvider provider: providers) {
- try {
- return provider.attachVirtualMachine(id);
- } catch (AttachNotSupportedException x) {
- lastExc = x;
- }
- }
- throw lastExc;
- }

然后进一步跟踪:
可以看到使用了SPI技术,笔者Mac系统,如果是Linux或者win,这里是不同的
逻辑大同小异:
static { System.loadLibrary("attach"); tmpdir = getTempDir(); }
先load c的lib,然后获取临时目录
- BsdVirtualMachine(AttachProvider provider, String vmid)
- throws AttachNotSupportedException, IOException
- {
- super(provider, vmid);
-
- // This provider only understands pids
- int pid;
- try {
- pid = Integer.parseInt(vmid);
- } catch (NumberFormatException x) {
- throw new AttachNotSupportedException("Invalid process identifier");
- }
-
- //这段注释很明显,先找socket文件,找不到就创建attach文件,发送quit信号,再试查找socket文件
- // Find the socket file.
- // If not found then we attempt to start the attach mechanism in the target VM by sending it a QUIT signal.
- // Then we attempt to find the socket file again.
- //查找socket文件
- path = findSocketFile(pid);
- if (path == null) {
- File f = new File(tmpdir, ".attach_pid" + pid);
- //创建attach文件
- createAttachFile(f.getPath());
- try {
- //发送退出信号,启动attach mechanism连接途径
- sendQuitTo(pid);
-
- // give the target VM time to start the attach mechanism
- int i = 0;
- long delay = 200;
- int retries = (int)(attachTimeout() / delay);
- do {
- try {
- Thread.sleep(delay);
- } catch (InterruptedException x) { }
- //多次查找socket文件
- path = findSocketFile(pid);
- i++;
- } while (i <= retries && path == null);
- if (path == null) {
- throw new AttachNotSupportedException(
- "Unable to open socket file: target process not responding " +
- "or HotSpot VM not loaded");
- }
- } finally {
- f.delete();
- }
- }
-
- // Check that the file owner/permission to avoid attaching to
- // bogus process
- checkPermissions(path);
-
- // Check that we can connect to the process
- // - this ensures we throw the permission denied error now rather than
- // later when we attempt to enqueue a command.
- //socket创建
- int s = socket();
- try {
- //连接socket,相当于远程(另一个jvm进程)连上了pid
- connect(s, path);
- } finally {
- close(s);
- }
- }
-
- // Return the socket file for the given process.
- // Checks temp directory for .java_pid<pid>.
- private String findSocketFile(int pid) {
- String fn = ".java_pid" + pid;
- File f = new File(tmpdir, fn);
- return f.exists() ? f.getPath() : null;
- }

建立socket连接,就进行下一步,loadjar
实际上这里就可以看到是要加载instrument。执行load指令,拿到结果,实际上load jar加载结束,agent就注入生效了,这个过程是JDK触发完成
- private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String options)
- throws AgentLoadException, AgentInitializationException, IOException
- {
- InputStream in = execute("load",
- agentLibrary,
- isAbsolute ? "true" : "false",
- options);
- try {
- int result = readInt(in);
- if (result != 0) {
- throw new AgentInitializationException("Agent_OnAttach failed", result);
- }
- } finally {
- in.close();
-
- }
- }

继续load
- InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {
- assert args.length <= 3; // includes null
-
- // did we detach?
- String p;
- synchronized (this) {
- if (this.path == null) {
- throw new IOException("Detached from target VM");
- }
- p = this.path;
- }
-
- // create UNIX socket
- int s = socket();
-
- // connect to target VM
- try {
- connect(s, p);
- } catch (IOException x) {
- close(s);
- throw x;
- }
-
- IOException ioe = null;
-
- // connected - write request
- // <ver> <cmd> <args...>
- try {
- writeString(s, PROTOCOL_VERSION);
- writeString(s, cmd);
-
- for (int i=0; i<3; i++) {
- if (i < args.length && args[i] != null) {
- //把jar的路径写给JVM,就结束了,JVM指令执行load指令
- writeString(s, (String)args[i]);
- } else {
- writeString(s, "");
- }
- }
- } catch (IOException x) {
- ioe = x;
- }
-
-
- // Create an input stream to read reply
- SocketInputStream sis = new SocketInputStream(s);
-
- // Read the command completion status
- int completionStatus;
- try {
- completionStatus = readInt(sis);
- } catch (IOException x) {
- sis.close();
- if (ioe != null) {
- throw ioe;
- } else {
- throw x;
- }
- }
-
- if (completionStatus != 0) {
- sis.close();
-
- // In the event of a protocol mismatch then the target VM
- // returns a known error so that we can throw a reasonable
- // error.
- if (completionStatus == ATTACH_ERROR_BADVERSION) {
- throw new IOException("Protocol mismatch with target VM");
- }
-
- // Special-case the "load" command so that the right exception is
- // thrown.
- if (cmd.equals("load")) {
- throw new AgentLoadException("Failed to load agent library");
- } else {
- throw new IOException("Command failed in target VM");
- }
- }
-
- // Return the input stream so that the command output can be read
- return sis;
- }

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方法
- private void loadClassAndCallPremain(String var1, String var2) throws Throwable {
- this.loadClassAndStartAgent(var1, "premain", var2);
- }
-
- private void loadClassAndCallAgentmain(String var1, String var2) throws Throwable {
- this.loadClassAndStartAgent(var1, "agentmain", var2);
- }
另一个方法就是jvm参数方式的调用函数
之所以说idea的debug能力是笔者在使用jmx技术时,发现
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。