Java获取函数参数名称的另一种方法

本文介绍了一种在Java 1.8之前版本中获取方法参数名的方法,避免引入额外依赖库。通过分析axis2源码,提取并封装了一个ParameterNames类,方便获取构造函数或方法的参数名。

关于获取java 方法的参数名(这里指java 1.8以前的版本,java 1.8已经提供了相关的原生方法),网上可以找到不少文章,这篇文章讲得比较全面了:《Java获取函数参数名称的几种方法》,无外乎是借用asm,javasist等第三方库。

我的项目中也有此需求,看了这篇文章还是觉得比较麻烦,为了这个小小的需求,要多引入一系列依赖库,有点不划算。

我对axis2比较熟悉,知道axis2中在生成client代码时也需要获取方法的参数名,于是通过分析源码找到了axis2的实现代码。
axis2获取java参数名的实现代码的package为org.apache.axis2.description.java2wsdl.bytecode,在axis2核心jar包axis2-kernel-1.6.2.jar中(源码下载地址:axis2-kernel-1.6.2-sources.jar)
好就好在这个package没有引用package之外的代码,所以可以单独提取出来独立调用。

于是我把这部分代码单独复制出来(完整代码参见gitee仓库:
https://gitee.com/l0km/common-java/tree/master/common-base/src/main/java/org/apache/bytecode),并在此基础上封装了一个类ParameterNames,方便调用:

ParameterNames.java

package net.gdface.utils;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import net.gdface.utils.Assert;

import org.apache.bytecode.ChainedParamReader;

/**
 * 获取构造函数或方法的参数名<br>
 * 当不能获取参数名的情况下,
 * {@link returnFakeNameIfFail}为{@code false}时返回{@code null},否则返回返回arg,arg2...格式的替代名<br>
 * {@link returnFakeNameIfFail}默认为{@code true}
 * @author guyadong
 *
 */
public class ParameterNames {
	private final Map<Class<?>, ChainedParamReader> readers = new HashMap<Class<?>, ChainedParamReader>();
	private final Class<?> clazz;
	/** 当获取无法参数名时是否返回arg,arg2...格式的替代名字 */
	private boolean returnFakeNameIfFail = true;
	public ParameterNames setReturnFakeNameIfFail(boolean returnFakeNameIfFail) {
		this.returnFakeNameIfFail = returnFakeNameIfFail;
		return this;
	}

	/**
	 * @param clazz 要构造函数或方法的参数名的类,为{@code null}时所有getParameterNames方法返回{@code null}
	 */
	public ParameterNames(Class<?> clazz) {
		this.clazz = clazz;
		if(null != clazz){
			try {
				Class<?> c = clazz;
				do {
					readers.put(c, new ChainedParamReader(c));
				} while (null != (c = c.getSuperclass()));
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
	}

	/**
	 * 获取构造函数或方法的参数名
	 * @param reader
	 * @param member 构造函数或方法对象
	 * @return
	 */
	private final String[] getParameterNames(ChainedParamReader reader, Member member) {
		String [] parameterNames = null;
		int paramCount ;
		if (member instanceof Method){
			parameterNames = reader.getParameterNames((Method) member);
			paramCount = ((Method) member).getParameterTypes().length;
		} else if (member instanceof Constructor){
			parameterNames = reader.getParameterNames((Constructor<?>) member);
			paramCount = ((Constructor<?>) member).getParameterTypes().length;
		} else {
			throw new IllegalArgumentException("member type must be Method or Constructor");
		}
		if(this.returnFakeNameIfFail){
			if (null == parameterNames) {
				parameterNames = new String[paramCount];
				for (int i = 0; i < parameterNames.length; i++)
					parameterNames[i] = String.format("arg%d", i);
			}
		}
		return parameterNames;
	}

	/**
	 * 获取构造函数或方法的参数名
	 * @param member 构造函数或方法对象
	 * @return
	 * @see #getParameterNames(ChainedParamReader, Member)
	 */
	public final String[] getParameterNames(Member member) {
		if(null == clazz){
			return null;
		}
		Assert.notNull(member, "member");
		Class<?> declaringClass = member.getDeclaringClass();
		ChainedParamReader reader;
		if (null == (reader = readers.get(declaringClass))) {
			throw new IllegalArgumentException(String.format("%s is not member of %s", member.toString(),
					declaringClass.getName()));
		}
		return getParameterNames(reader, member);
	}
	
	/**
	 * 获取构造函数或方法的参数名<br>
	 * {@code name}为{@code null}时,获取构造函数的参数名
	 * @param name 方法名
	 * @param parameterTypes 构造函数或方法的参数类型
	 * @return
	 * @throws NoSuchMethodException
	 * @see #getParameterNames(String, Class)
	 */
	public final String[] getParameterNames(String name, Class<?>[] parameterTypes) throws NoSuchMethodException {
		if(null == clazz){
			return null;
		}
		try {
			Member member = null == name ? clazz.getConstructor(parameterTypes) : clazz.getMethod(name, parameterTypes);
			return getParameterNames(member);
		} catch (SecurityException e) {
			throw new IllegalArgumentException(e);
		}
	}
}

完整代码参见gitee仓库:https://gitee.com/l0km/common-java/blob/master/common-base/src/main/java/net/gdface/utils/ParameterNames.java

调用示例:

	private static void outputParameterNames(Method method){
		// 抽象方法不能正确获取参数名,只能用假名替代
		System.out.printf("%s abstract = %b\n parameter names:",
				method.getName(),
				Modifier.isAbstract(method.getModifiers()));
		ParameterNames pt = new ParameterNames(method.getDeclaringClass());
		String[] names = pt.getParameterNames(method);
		for(String name:names){
			System.out.print(name + ",");
		}
		System.out.println();
	}

完整的代码参见gitee仓库:
https://gitee.com/l0km/common-java/blob/master/common-base/src/test/java/net/gdface/common/ClassTest.java

<think>我们被要求使用Frida Hook Java Native函数注册方法。在Android中,Java Native函数通过JNI(Java Native Interface)注册,有两种方式:静态注册和动态注册。动态注册是通过JNI_OnLoad函数中调用RegisterNatives函数来注册的。我们想要Hook的是动态注册的过程,特别是RegisterNatives函数。 根据引用[1]和[2],我们知道Frida可以Hook Native层的函数。我们需要找到RegisterNatives函数的地址,然后使用Interceptor.attach来附加钩子。 步骤: 1. 确定目标函数:RegisterNatives,它位于libart.so(或不同Android版本对应的lib)中。 2. 使用Frida的Module.getExportByName获取函数地址。 3. 使用Interceptor.attach附加钩子,在回调函数中打印或处理参数。 RegisterNatives函数原型: jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods); 其中,JNINativeMethod结构体包含: const char* name; const char* signature; void* fnPtr; 因此,在Hook时,我们可以读取第三个参数(methods数组)和第四个参数(nMethods)来获取注册的本地方法信息。 注意:由于不同Android版本中so库的名称和函数符号可能不同,我们需要根据实际情况调整。 下面是一个示例脚本,用于Hook RegisterNatives函数: ```javascript Java.perform(function() { // 获取RegisterNatives函数的地址 var registerNativesAddr = Module.getExportByName('libart.so', 'RegisterNatives'); // 如果找不到,尝试其他库名,例如不同Android版本可能为libart.so, libdvm.so等 if (!registerNativesAddr) { console.log("Failed to find RegisterNatives address."); return; } // 附加钩子 Interceptor.attach(registerNativesAddr, { onEnter: function(args) { // args[0]是JNIEnv* // args[1]是jclass // args[2]是JNINativeMethod*,即方法数组 // args[3]是jint,即方法数量 var methodsPtr = ptr(args[2]); var methodCount = args[3].toInt32(); console.log("RegisterNatives called with " + methodCount + " methods."); // 遍历方法数组 for (var i = 0; i < methodCount; i++) { // JNINativeMethod结构体的大小,在32位上是12字节(3个指针),64位上是24字节(3个指针,每个8字节) var method = methodsPtr.add(i * Process.pointerSize * 3); // 读取name(char*) var namePtr = method.readPointer(); var signaturePtr = method.add(Process.pointerSize).readPointer(); var fnPtr = method.add(Process.pointerSize * 2).readPointer(); var methodName = namePtr.readCString(); var signature = signaturePtr.readCString(); console.log("Method [" + i + "]:"); console.log(" name: " + methodName); console.log(" signature: " + signature); console.log(" function pointer: " + fnPtr); } } }); }); ``` 注意:上述脚本在32位和64位上的结构体大小不同,我们通过Process.pointerSize来适配。每个指针的大小为Process.pointerSize。 但是,需要注意的是,不同Android版本中JNINativeMethod结构体的布局可能一致(都是三个连续的指针),但为了兼容性,我们按照标准布局处理。 外,由于RegisterNatives可能在JNI_OnLoad中被调用,因此我们需要在脚本加载时尽早Hook。 如果目标应用在加载时已经注册了Native方法,我们可能需要通过setTimeout或立即执行来尽早Hook,或者使用Frida的早期注入(如frida -f 包名 --no-pause)来在应用启动时注入。 此外,引用[4]中展示了读取字符串参数的方法,我们这里也使用了readCString来读取方法名和签名。 这个脚本将打印出每次调用RegisterNatives时注册的所有本地方法的信息,包括方法名、签名和函数指针。 但是,请注意,有些应用可能会在运行时动态注册,因此我们可能需要在整个生命周期中监控。 外,我们也可以Hook JNI_OnLoad函数,因为动态注册通常发生在这里。 参考引用[1]中提到的“Hook JNI函数”,这个案例就是直接Hook RegisterNatives。 因此,我们可以使用上述脚本来实现需求。 相关问题: 1. 如何Hook其他JNI函数,比如FindClass或GetMethodID? 2. 如何通过Frida调用一个已经注册的Native函数? 3. 如何Hook一个已经注册的Native函数的具体实现(即替换函数指针)? 下面我们尝试回答这些问题: 问题1:Hook其他JNI函数 类似地,我们可以找到其他JNI函数的地址,然后使用Interceptor.attach。例如,Hook FindClass: ```javascript var findClassAddr = Module.getExportByName('libart.so', 'FindClass'); Interceptor.attach(findClassAddr, { onEnter: function(args) { var classNamePtr = args[1]; // 第一个参数是JNIEnv*,第二个是const char* var className = classNamePtr.readCString(); console.log("FindClass: " + className); } }); ``` 问题2:调用已经注册的Native函数 引用[2]中提到,我们可以使用NativeFunction来调用本地函数。假设我们有一个函数指针,我们可以这样: ```javascript // 假设我们有一个函数指针fnPtr,函数原型为返回int,参数为两个int var func = new NativeFunction(fnPtr, 'int', ['int', 'int']); var result = func(1, 2); console.log("Result: " + result); ``` 问题3:替换已经注册的Native函数的实现 在Hook RegisterNatives时,我们可以修改传入的函数指针,或者在获取到函数指针后,使用Interceptor.replace来替换该函数指针指向的代码。 但是,注意:在RegisterNatives时,我们无法修改传入的methods数组(因为它是只读的?),但我们可以修改结构体中的函数指针指向的内容?实际上,在onEnter时,args[2]指向的数组是可写的吗?这取决于调用者传入的是否是可写内存。通常,这个数组是调用者分配的,可能是栈上或堆上,但修改它可能不安全。 一种方法是,在注册完成后,我们直接Hook每个具体的函数。例如,在打印出函数指针后,我们可以对每个函数指针进行Hook: ```javascript Interceptor.attach(fnPtr, { onEnter: function(args) { console.log("Native method called: " + methodName); } }); ``` 或者,我们可以替换函数: ```javascript var originalFunc = new NativeFunction(fnPtr, 'void', ['pointer', 'pointer']); // 假设返回void,参数两个指针(JNIEnv*, jobject) Interceptor.replace(fnPtr, new NativeCallback(function(env, obj) { console.log("Hooked native method"); return originalFunc(env, obj); // 调用原函数 }, 'void', ['pointer', 'pointer'])); ``` 但是,请注意,替换函数需要知道确切的函数签名(返回类型和参数类型)。因此,我们需要在Hook RegisterNatives时记录每个方法的签名,然后根据签名创建对应的NativeCallback。 由于签名解析比较复杂,这里不展开。 因此,我们回到最初的问题,已经通过示例脚本展示了如何Hook Java Native函数注册方法。 如果用户需要更详细的信息,可以进一步提供具体的Android版本或需求。</think>### Frida Hook Java Native函数注册方法 在Android开发中,Java Native函数通过JNI(Java Native Interface)动态注册时,会调用`RegisterNatives()`函数。使用Frida Hook该函数可以捕获所有动态注册的Native方法信息,以下是完整实现方案: #### 1. Hook RegisterNatives函数 ```javascript Java.perform(function() { // 获取RegisterNatives函数地址 const registerNativesAddr = Module.getExportByName('libart.so', 'RegisterNatives'); Interceptor.attach(registerNativesAddr, { onEnter: function(args) { // args[0]: JNIEnv* // args[1]: jclass // args[2]: JNINativeMethod* (方法数组) // args[3]: jint (方法数量) const methodCount = args[3].toInt32(); const methodsPtr = ptr(args[2]); console.log(`[+] RegisterNatives called (${methodCount} methods)`); // 解析每个JNINativeMethod结构体 for (let i = 0; i < methodCount; i++) { const method = methodsPtr.add(i * Process.pointerSize * 3); // 读取方法名指针 const namePtr = method.readPointer(); // 读取签名指针 const sigPtr = method.add(Process.pointerSize).readPointer(); // 读取函数指针 const fnPtr = method.add(Process.pointerSize * 2).readPointer(); // 读取字符串内容 const methodName = namePtr.readCString(); const signature = sigPtr.readCString(); console.log(`├── Method ${i+1}:`); console.log(`│ ├── Name: ${methodName}`); console.log(`│ ├── Signature: ${signature}`); console.log(`│ └── Function Pointer: ${fnPtr}`); } } }); }); ``` #### 2. 关键技术解析 1. **JNINativeMethod结构体** ```c typedef struct { const char* name; // 方法名 const char* signature; // JNI签名 void* fnPtr; // 函数指针 } JNINativeMethod; ``` 内存布局:每个结构体占用`3 * pointerSize`字节(32位:12字节,64位:24字节) 2. **Hook原理** - 通过`libart.so`导出符号定位`RegisterNatives` - 解析第三个参数`JNINativeMethod*`指向的方法数组 - 遍历读取每个方法名称、签名和函数指针 - 使用`readCString()`获取字符串内容[^2] 3. **多版本适配** ```javascript // 兼容不同Android版本的库名 const libNames = ['libart.so', 'libdvm.so']; let registerNativesAddr = null; libNames.forEach(lib => { if (!registerNativesAddr) { registerNativesAddr = Module.getExportByName(lib, 'RegisterNatives'); } }); ``` #### 3. 实战应用场景 1. **动态注册监控** ```javascript // 记录所有注册的Native方法 const nativeMethods = {}; onEnter: function(args) { // ...解析过程 nativeMethods[methodName] = { signature: signature, address: fnPtr, jclass: args[1] }; } ``` 2. **方法替换** ```javascript // 替换目标函数实现 Interceptor.replace(fnPtr, new NativeCallback( (env, clazz) => { console.log(`[!] ${methodName} called`); return 0; }, 'int', ['pointer', 'pointer'] )); ``` 3. **JNI函数调用追踪** ```javascript // Hook具体Native函数 Interceptor.attach(fnPtr, { onEnter: function(args) { console.log(`[→] Enter ${methodName}`); }, onLeave: function(retval) { console.log(`[←] Exit ${methodName}`); } }); ``` #### 4. 注意事项 1. **时序问题**:需在`JNI_OnLoad`执行前注入,建议使用`frida -f com.app --no-pause` 2. **字符串编码**:非ASCII方法名需使用`readUtf8String()`替代 3. **内存保护**:Hook前检查内存权限`Memory.protect(addr, size, 'rwx')` 4. **64位适配**:使用`Process.pointerSize`确定指针大小 > 通过Hook `RegisterNatives`,我们可以完整掌握JNI动态注册过程,为后续的Native层分析和Hook奠定基础[^1][^2]。实际测试中建议结合`frida-trace`进行调用栈追踪: > `frida-trace -U com.app -i "RegisterNatives"`
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

10km

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值