Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

前两天拜读了章亦春大佬的关于Dynamic Tracing的文章,觉得对现在碰到的一些问题有了一些新的思考,为了能有所产出就先写一点简单的学习记录
首先这个systemtap类似于一个linux系统层面的探针工具,可以让用户去监控系统的各种活动
以阿里云的 ubuntu 22.04 为例,
首先我们需要安装 systemtap-sdt-dev 这个包,
然后因为通过c的方式比较常规,我是想研究下针对像 php 这种怎么去监控
我们需要一个编译参数带上--enable-dtrace 的php版本
然后在编译完后我们写一个简单的测试脚本

1
2
3
4
5
6
<?php
function a($b, $c) {
return $b + $c;
}
sleep(1);
echo a(1, 2);

这其实主要是为了触发系统调用
然后重点就是systemtap的脚本,它有点类似于c的代码,结构主要是 “事件” - “处理器”
比如我监听到了php发起了一个系统调用,那这个时候我想把它记录下来,打印日志这种

1
2
3
4
5
6
probe syscall.*
{
if (execname() == "php") {
printf ("%s(%d) open, call %s \n", execname(), pid(), name)
}
}

这里就是针对所有的系统调用,如果是由php发起调用,那就打印出来pid和系统调用的名字(name就是这里的系统调用名字)

事件

syscall.system_call 只是其中一类事件
还有对虚拟文件系统的操作 vfs.file_operation
内核的方法调用 kernel.function("function")
比如 probe kernel.function("*@net/socket.c") { }
还有可以是内核的一些追踪点
kernel.trace("tracepoint")
比如 kernel.trace("kfree_skb") 这个表示每次当内核中的network buffer被释放的时候触发
还有的是时间时间
timer events
比如

1
2
3
4
probe timer.s(4)
{
printf("hello world\n")
}

每4秒打印一个 hello world
还有这些

1
2
3
4
5
timer.ms(milliseconds)
timer.us(microseconds)
timer.ns(nanoseconds)
timer.hz(hertz)
timer.jiffies(jiffies)

处理器

最常规的一类就是打印信息了
printf ( ) Statements
这个跟c语言也很像,可以用%s%d 作为占位符来填充字符串和数字
然后就是一些定义好的取值方法
execname() (a string with the executable name)
就比如刚才的php,
线程id

1
2
tid()
The ID of the current thread.

用户id

1
2
uid()
The ID of the current user.

比如系统调用就直接用 name
还有很多可以研究,第一篇学习就先到这里

这篇还是javassist的一些使用小技巧,我们可以用javassist来读取java的注解信息
首先我们有这样一个注解

1
2
3
4
public @interface Author {
String name();
int year();
}

注解可以打在类上,那么我们建一个point类

1
2
3
4
@Author(name = "nick", year = 2024)
public class Point {
int x, y;
}

name 是 nick,24年的,

然后我想通过javassist来获取这个类上的注解信息
我们先从对象池中获取这个类

1
CtClass cc = ClassPool.getDefault().get("org.example.Point");

然后获取类上的注解

1
2
Object[] all = cc.getAnnotations();
Author a = (Author)all[0];

将第一个注解强转成Author,因为这里我们知道是只有这个注解,否则可以做的更通用一点
然后把这个主机上的值获取出来

1
2
3
String name = a.name();
int year = a.year();
System.out.println("name: " + name + ", year: " + year);

可以获得结果,非常简单

1
2
3
name: nick, year: 2024

Process finished with exit code 0

为什么讲这么个简单的例子呢,因为这个刚好可以作为一种实现aop的途径,我们可以获取类的信息,包括注解上的,这样我们就可以从源头去理解这些aop啥的可以通过什么方法来实现

上次说了可以改写类,那进一步的我们可以做一下类似于之前提过的通过字节码来做切面的工作
首先我们有一个很简单的类和方法

1
2
3
4
5
6
7
8
public class DemoService {
public List<String> queryList(String name, String title, String n3) {
List<String> list = new ArrayList<>();
list.add(name);
list.add(title);
return list;
}
}

就是一个方法,输入几个字符串,然后加到list里,那么比如我们想知道打印入参和返回结果
这里比较类似于我们在用aop的时候就是织入代码
正好javassist有这样的操作接口
就是我们可以找到方法,然后使用

1
javassist.CtBehavior#insertBefore(java.lang.String)

1
javassist.CtBehavior#insertAfter(java.lang.String)

就可以在我们找到类和方法以后

1
2
3
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(targetClass);
CtMethod method = cc.getDeclaredMethod(targetMethod);

对方法的前后进行逻辑织入

1
2
3
4
5
6
7
8
9
10
11
12
method.insertBefore(
"{ java.lang.StringBuilder _paramValues = new StringBuilder();" +
" Object[] params = $args;" +
" if(params != null) {" +
" for(int i=0; i<params.length; i++) {" +
" if(i > 0) _paramValues.append(\", \");" +
" _paramValues.append(String.valueOf(params[i]));" +
" }" +
" }" +
" System.out.println(\"[Method Entry] " + targetMethod + " params: \" + _paramValues.toString());" +
"}"
);

这里我们使用了直接织入代码的形式,在javassist中可以用 $args 来获取参数,然后循环参数, 把每个参数拼接以后作为字符串输出
然后我们想要在函数返回前获取返回值,这个也是有个语法糖 $_

1
2
3
4
5
6
7
StringBuilder after = new StringBuilder();
after.append("{")
.append(" java.util.List _result = $_;\n")
.append(" String _resultStr = (_result != null ? _result.toString() : \"null\");\n")
.append(" int _size = (_result != null ? _result.size() : 0);\n")
.append(" System.out.println(\"[Method Exit] " + targetMethod + " returns: \" + _resultStr + \" size: \" + _size);\n")
.append("}");

这里刚好我是list返回的,就直接这么处理了, 我们通过一个简单的调用来验证下

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
public static void main(String[] args) throws Exception {
// 假设要监控 org.example.DemoService 类中的 queryList 方法
addMonitoringNew("org.example.DemoService", "queryList");
// 2. 使用自定义类加载器加载增强后的类
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.equals("org.example.DemoService")) {
try {
// 读取增强后的类文件
byte[] bytes = java.nio.file.Files.readAllBytes(
java.nio.file.Paths.get("org/example/DemoService.class")
);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException("Failed to load enhanced class", e);
}
}
return super.loadClass(name);
}
};

// 3. 加载增强后的类
Class<?> enhancedClass = classLoader.loadClass("org.example.DemoService");

// 4. 创建实例
Object instance = enhancedClass.newInstance();

// 5. 获取并调用方法
Method processMethod = enhancedClass.getMethod("queryList", String.class, String.class, String.class); // 假设方法接收一个String参数
Object result = processMethod.invoke(instance, "name1", "title1", "n3");

System.out.println("Method returned: " + result);
}
}
可以看到我们的输出结果
```java
Successfully enhanced method: queryList
[Method Entry] queryList params: name1, title1, n3
[Method Exit] queryList returns: [name1, title1] size: 2
Method returned: [name1, title1]

这里其实核心很依赖两个语法糖,类似这样的还有一些

1
2
3
4
5
6
7
8
9
10
11
12
$0, $1, $2, ...    	this and actual parameters
$args An array of parameters. The type of $args is Object[].
$$ All actual parameters.
For example, m($$) is equivalent to m($1,$2,...)

$cflow(...) cflow variable
$r The result type. It is used in a cast expression.
$w The wrapper type. It is used in a cast expression.
$_ The resulting value
$sig An array of java.lang.Class objects representing the formal parameter types.
$type A java.lang.Class object representing the formal result type.
$class A java.lang.Class object representing the class currently edited.

可以结合文档仔细学习下

前面那篇在讲agent的时候用到了javassist,我们就来简单讲个demo
我想用javassist来创建一个类

1
2
3
4
5
6
7
public static void createEntity() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cl = pool.makeClass("Entity");
CtConstructor cons = new CtConstructor(new CtClass[]{}, cl);
cl.addConstructor(cons);
cl.writeFile();
}

这个代码可以给我们创建一个Entity类, 带有一个无参的构造函数,默认在项目根目录下会生成Entity类的class文件
但是其实这个也是不必要的,默认会给生成一个无参的构造函数
然后我们可以给这个类加个字段, 加在 writeFile 方法调用前

1
2
3
CtField param = new CtField(pool.get("java.lang.String"), "name", cl);
param.setModifiers(Modifier.PRIVATE);
cl.addField(param, CtField.Initializer.constant("ent1"));


我们就生成了这样一个类
javassist也已经提供好了

1
2
cl.addMethod(CtNewMethod.setter("setName", param));
cl.addMethod(CtNewMethod.getter("getName", param));

我们生成的类就变成这样了

接下去我们可以再加一个默认赋值的无参构造函数

1
2
3
4
CtConstructor con = new CtConstructor(new CtClass[]{}, cl);
// 需要转义下
con.setBody("{name = \"nick\";}");
cl.addConstructor(con);


然后我们就有了这样的类,带有一个默认赋值的构造函数,接下去可以来个带参数的构造函数

1
2
3
4
con = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cl);
// $0=this / $1,$2,$3...指的是参数,$1代表第一个参数
con.setBody("{$0.name = $1;}");
cl.addConstructor(con);


最后我们可以再加个打印该字段的方法

1
2
3
4
CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cl);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println(name);}");
cl.addMethod(ctMethod);

这样我们就获得了这样一个类

这只是javassist的简单使用,它还有很多强大的功能可以在官方文档和网上的资料进行学习

在上次的基础上我们可以通过一些方法来获取参数的参数名,以此我们又可以达到类似于切面的功能逻辑,
首先我在之前的代码里做一点修改

1
2
3
4
5
6
7
8
public static void start(String input) {
try {
System.out.println("Demo starting ...");
System.out.println(input);
Thread.sleep(123);
} catch (InterruptedException ignore) {
}
}

就是带了个参数,这边是我把参数打了出来,如果没有打,我想通过agent来知道这个参数值是啥就可以用agent技术
获取代码中的变量名以及类型

1
2
3
4
MethodInfo methodInfo = declaredMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attribute = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
System.out.println(attribute.variableName(0));

这边就是个string类型,我们就能够获取到 input 这个变量名,然后我们就可以在这里插入一段代码来打印这个参数

1
declaredMethod.insertAfter("System.out.println( " + attribute.variableName(0)+ " );");

这样我们就能做到类似于java中切面的技术

0%