本题来自于3W班8月的第二题:请定制ART,完成对APP运行流程中所有的java类函数(包含java函数和jni函数)调用关系的trace。
对于Java层的单纯调用关系,可以比较方便的打印。参数的打印还是比较麻烦,Java对象的地址如何获取属性值等信息还是需要进一步深入研究。
根据之前的课程,可以得知有4种调用方式。其中java->java,java->jni在switch解释器下会通过DoCall方法。而jni->jni,jni->java则会通过反射相关的InvokeWithArgArray最后调用ArtMethod的Invoke方法。
这里考虑先使用frida验证运行逻辑,再修改源码刷机
(这里编译源码将解释器改成Switch方便查看源码)
DoCall只能打印java层发起的调用,Jni反射调用的并不走这个流程。
这里也可以HOOK ArtMethod的Invoke,参数基本上一样,而且ArtMethod的调用会多很多。
InvokeWithArgArray方法有个问题就是,我没有找到调用方的方法,虽然java->jni可以确定是哪里调用,但是jni->jni的可能会导致不知道上方是哪里调用的。
根据源码中这些方法都有Thread参数,所以尝试直接打印出tid,这样就算没有调用方,单一线程也只能同时运行一个方法。
源码中Thread可以直接调用GetTid获取线程ID,frida则可以根据内存分布,得知+0x10就是线程ID
InvokeWithArgArray主要是va_list的处理,我这里先写Demo使用AS确定内存分布。之后读取参数就比较方便了。
这里打印参数就统一打印U32值,也可以根据方法名,判断参数类型,然后自定义打印。
第一个参数是this指针,后续才是参数传递的值。
DoCall函数虽然可以打印调用关系,但是还没有为被调用方法初始化ShadowFrame,所以往下找了一些,最后HOOK了PerformCall里面的ArtInterpreterToInterpreterBridge和ArtInterpreterToCompiledCodeBridge。因为在DoCallCommon函数中初始化了ShadowFrame。
接下来就可以读取参数了,这里是通过修改参数,直接找到偏移进行读取,没有仔细理解ShadowFrame。
关于ArtInterpreterToCompiledCodeBridge的参数个数,不像ArtInterpreterToInterpreterBridge可以直接通过DexFile::CodeItem对象获取参数数量。所以直接读取的ShadowFrame的number_ofvregs打印所有寄存器值
这里就偷懒了,刷机比较慢,只写调用,不读取参数了,只加了2行日志
这里打印反而有问题,刚调用函数打印调用和被调用方法名字一样?没理解什么原因,应该是哪里还要设置ShadowFrame,Frida那边正常,这边也能看到上一步调用,倒也不影响流程查看。
为了测试方便,这里加了过滤,直接hook strstr即可修改过滤字符。
public
int
JavaCallJava(
int
i){
Log.i(
"javajnitrace"
,
"step1->JavaCallJava onEnter"
);
return
JavaCallJni(
0x1111
);
}
public
int
JniCallJava(
int
i){
int
javajnitrace
=
Log.i(
"javajnitrace"
,
"step4->JniCallJava onEnter "
+
i);
return
1
;
}
public String JniCallJavaISS(
int
i, String s1, String s2){
int
javajnitrace
=
Log.i(
"javajnitrace"
,
"stepX->JniCallJavaISS onEnter "
+
i
+
" "
+
s1
+
" "
+
s2);
JavaCallJavaIII(
0x1111
,
0x2222
,
0x3333
);
return
"JniCallJavaISS Called"
;
}
public
int
JavaCallJavaIII(
int
i,
int
j,
int
k){
Log.i(
"javajnitrace"
,
"stepX->JavaCallJavaIII onEnter"
);
/
/
JavaCallJavaI17(
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
,
11
,
12
,
13
,
14
,
15
,
16
,
17
);
return
i
+
j
+
k;
}
public
int
JavaCallJava(
int
i){
Log.i(
"javajnitrace"
,
"step1->JavaCallJava onEnter"
);
return
JavaCallJni(
0x1111
);
}
public
int
JniCallJava(
int
i){
int
javajnitrace
=
Log.i(
"javajnitrace"
,
"step4->JniCallJava onEnter "
+
i);
return
1
;
}
public String JniCallJavaISS(
int
i, String s1, String s2){
int
javajnitrace
=
Log.i(
"javajnitrace"
,
"stepX->JniCallJavaISS onEnter "
+
i
+
" "
+
s1
+
" "
+
s2);
JavaCallJavaIII(
0x1111
,
0x2222
,
0x3333
);
return
"JniCallJavaISS Called"
;
}
public
int
JavaCallJavaIII(
int
i,
int
j,
int
k){
Log.i(
"javajnitrace"
,
"stepX->JavaCallJavaIII onEnter"
);
/
/
JavaCallJavaI17(
1
,
2
,
3
,
4
,
5
,
6
,
7
,
8
,
9
,
10
,
11
,
12
,
13
,
14
,
15
,
16
,
17
);
return
i
+
j
+
k;
}
extern
"C"
JNIEXPORT jint JNICALL
Java_com_cwuzao_javajnitrace_MainActivity_JavaCallJni(JNIEnv
*
env, jobject thiz, jint i) {
__android_log_print(
4
,
"javajnitrace"
,
"step2->JavaCallJni onEnter %0x"
, i);
jclass class_MainActivity
=
env
-
>FindClass(
"com/cwuzao/javajnitrace/MainActivity"
);
jmethodID method_JniCallJni
=
env
-
>GetMethodID(class_MainActivity,
"JniCallJni"
,
"(I)I"
);
int
callResult
=
env
-
>CallIntMethod(thiz, method_JniCallJni,
0x2222
);
return
callResult
+
1
;
}
extern
"C"
JNIEXPORT jint JNICALL
Java_com_cwuzao_javajnitrace_MainActivity_JniCallJni(JNIEnv
*
env, jobject thiz, jint i) {
__android_log_print(
4
,
"javajnitrace"
,
"step3->JniCallJni onEnter %0x"
,i);
jclass class_MainActivity
=
env
-
>FindClass(
"com/cwuzao/javajnitrace/MainActivity"
);
jmethodID method_JniCallJava
=
env
-
>GetMethodID(class_MainActivity,
"JniCallJava"
,
"(I)I"
);
int
callResult
=
env
-
>CallIntMethod(thiz, method_JniCallJava,
0x3333
);
return
callResult
+
1
;
}
extern
"C"
JNIEXPORT jint JNICALL
Java_com_cwuzao_javajnitrace_MainActivity_JavaCallJni(JNIEnv
*
env, jobject thiz, jint i) {
__android_log_print(
4
,
"javajnitrace"
,
"step2->JavaCallJni onEnter %0x"
, i);
jclass class_MainActivity
=
env
-
>FindClass(
"com/cwuzao/javajnitrace/MainActivity"
);
jmethodID method_JniCallJni
=
env
-
>GetMethodID(class_MainActivity,
"JniCallJni"
,
"(I)I"
);
int
callResult
=
env
-
>CallIntMethod(thiz, method_JniCallJni,
0x2222
);
return
callResult
+
1
;
}
extern
"C"
JNIEXPORT jint JNICALL
Java_com_cwuzao_javajnitrace_MainActivity_JniCallJni(JNIEnv
*
env, jobject thiz, jint i) {
__android_log_print(
4
,
"javajnitrace"
,
"step3->JniCallJni onEnter %0x"
,i);
jclass class_MainActivity
=
env
-
>FindClass(
"com/cwuzao/javajnitrace/MainActivity"
);
jmethodID method_JniCallJava
=
env
-
>GetMethodID(class_MainActivity,
"JniCallJava"
,
"(I)I"
);
int
callResult
=
env
-
>CallIntMethod(thiz, method_JniCallJava,
0x3333
);
return
callResult
+
1
;
}
function DoCall_onEnter(args) {
var addr_ArtMethod
=
args[
2
].add(
8
).readPointer();
allocPrettyMethod.writeByteArray(allocPrettyMethodInit);
PrettyMethod(addr_ArtMethod_PrettyMethod, addr_ArtMethod, allocPrettyMethod,
0x100
);
var methodName
=
allocPrettyMethod.readCString();
if
(methodName.indexOf(searchName) >
-
1
){
var addr_Call_ArtMethod
=
args[
0
];
allocPrettyMethod.writeByteArray(allocPrettyMethodInit);
PrettyMethod(addr_ArtMethod_PrettyMethod, addr_Call_ArtMethod, allocPrettyMethod,
0x100
);
var call_methodName
=
allocPrettyMethod.readCString();
console.log(
"DoCall:"
,methodName,
"->"
, call_methodName);
}
}
function DoCall_onEnter(args) {
var addr_ArtMethod
=
args[
2
].add(
8
).readPointer();
allocPrettyMethod.writeByteArray(allocPrettyMethodInit);
PrettyMethod(addr_ArtMethod_PrettyMethod, addr_ArtMethod, allocPrettyMethod,
0x100
);
var methodName
=
allocPrettyMethod.readCString();
if
(methodName.indexOf(searchName) >
-
1
){
var addr_Call_ArtMethod
=
args[
0
];
allocPrettyMethod.writeByteArray(allocPrettyMethodInit);
PrettyMethod(addr_ArtMethod_PrettyMethod, addr_Call_ArtMethod, allocPrettyMethod,
0x100
);
var call_methodName
=
allocPrettyMethod.readCString();
console.log(
"DoCall:"
,methodName,
"->"
, call_methodName);
}
}
DoCall: void com.cwuzao.javajnitrace.MainActivity$
1.onClick
(android.view.View)
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
-
>
int
android.util.Log.i(java.lang.String, java.lang.String)
DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJni(
int
)
addr_InvokeWithArgArray onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJni(
int
)
addr_InvokeWithArgArray onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
-
>
int
android.util.Log.i(java.lang.String, java.lang.String)
addr_InvokeWithArgArray onLeave
addr_InvokeWithArgArray onLeave
DoCall: void com.cwuzao.javajnitrace.MainActivity$
1.onClick
(android.view.View)
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
-
>
int
android.util.Log.i(java.lang.String, java.lang.String)
DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJni(
int
)
addr_InvokeWithArgArray onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJni(
int
)
addr_InvokeWithArgArray onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
-
>
int
android.util.Log.i(java.lang.String, java.lang.String)
addr_InvokeWithArgArray onLeave
addr_InvokeWithArgArray onLeave
Interceptor.attach(addr_InvokeWithArgArray, {
onEnter: function(args){
var addr_ArtMethod
=
args[
1
];
allocPrettyMethod.writeByteArray(allocPrettyMethodInit);
PrettyMethod(addr_ArtMethod_PrettyMethod, addr_ArtMethod, allocPrettyMethod,
0x100
);
var methodName
=
allocPrettyMethod.readCString();
if
(methodName.indexOf(searchName) >
-
1
){
this.methodName
=
methodName;
console.log(
"InvokeWithArgArray->"
,methodName, args[
1
], args[
2
], args[
3
], args[
4
]);
}
},
onLeave: function(retval){
}
});
Interceptor.attach(addr_InvokeWithArgArray, {
onEnter: function(args){
var addr_ArtMethod
=
args[
1
];
allocPrettyMethod.writeByteArray(allocPrettyMethodInit);
PrettyMethod(addr_ArtMethod_PrettyMethod, addr_ArtMethod, allocPrettyMethod,
0x100
);
var methodName
=
allocPrettyMethod.readCString();
if
(methodName.indexOf(searchName) >
-
1
){
this.methodName
=
methodName;
console.log(
"InvokeWithArgArray->"
,methodName, args[
1
], args[
2
], args[
3
], args[
4
]);
}
},
onLeave: function(retval){
}
});
DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJni(
int
)
addr_ArtMethod6Invoke onEnter
addr_InvokeWithArgArray onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJni(
int
)
addr_ArtMethod6Invoke onEnter
addr_InvokeWithArgArray onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
addr_ArtMethod6Invoke onEnter
DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
-
>
int
android.util.Log.i(java.lang.String, java.lang.String)
DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJni(
int
)
addr_ArtMethod6Invoke onEnter
addr_InvokeWithArgArray onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJni(
int
)
addr_ArtMethod6Invoke onEnter
addr_InvokeWithArgArray onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
addr_ArtMethod6Invoke onEnter
DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
-
>
int
android.util.Log.i(java.lang.String, java.lang.String)
[
3741
]DoCall: void com.cwuzao.javajnitrace.MainActivity$
1.onClick
(android.view.View)
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
[
3741
]DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
-
>
int
android.util.Log.i(java.lang.String, java.lang.String)
[
3741
]DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJni(
int
)
[
3741
]addr_ArtMethod6Invoke onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJni(
int
)
[
3741
]addr_ArtMethod6Invoke onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJni(
int
)
[
3741
]addr_ArtMethod6Invoke onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
[
3741
]DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
-
>
int
android.util.Log.i(java.lang.String, java.lang.String)
[
3741
]DoCall: void com.cwuzao.javajnitrace.MainActivity$
1.onClick
(android.view.View)
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
[
3741
]DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
-
>
int
android.util.Log.i(java.lang.String, java.lang.String)
[
3741
]DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJava(
int
)
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJni(
int
)
[
3741
]addr_ArtMethod6Invoke onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JavaCallJni(
int
)
[
3741
]addr_ArtMethod6Invoke onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJni(
int
)
[
3741
]addr_ArtMethod6Invoke onEnter
-
>
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
[
3741
]DoCall:
int
com.cwuzao.javajnitrace.MainActivity.JniCallJava(
int
)
-
>
int
android.util.Log.i(java.lang.String, java.lang.String)
Interceptor.attach(addr_InvokeWithArgArray, {
onEnter: function(args){
var addr_ArtMethod
=
args[
1
];
allocPrettyMethod.writeByteArray(allocPrettyMethodInit);
PrettyMethod(addr_ArtMethod_PrettyMethod, addr_ArtMethod, allocPrettyMethod,
0x100
);
var methodName
=
allocPrettyMethod.readCString();
if
(methodName.indexOf(
"com.cwuzao.javajnitrace"
) >
-
1
){
this.showResult
=
true;
this.methodName
=
methodName;
this.tid
=
args[
0
].readPointer().add(
0x10
).readU32();
var argscount
=
args[
2
].add(
0x8
).readU32();
console.log(
"["
+
this.tid
+
"]InvokeWithArgArray->"
,methodName, args[
1
], args[
2
], args[
3
], args[
4
]);
/
/
console.log(
"["
+
this.tid
+
"]args count:"
, argscount,
"NumBytes:"
, args[
2
].add(
0xC
).readU32());
/
/
console.log(
"addr_InvokeWithArgArray args[0]->"
, hexdump(args[
2
].add(
0x10
).readPointer()));
var argspointer
=
args[
2
].add(
0x10
).readPointer();
for
(var i
=
0
; i< argscount; i
+
+
){
console.log(
"["
+
this.tid
+
"] args_"
, i
+
1
,
"->0x"
+
argspointer.add(i
*
4
).readU32().toString(
16
));
}
}
},
onLeave: function(retval){
if
(this.showResult){
console.log(
"["
+
this.tid
+
"]InvokeWithArgArray onLeave->"
,this.methodName,
"\n result->0x"
+
retval.toString(
16
));
}
}
});
Interceptor.attach(addr_InvokeWithArgArray, {
onEnter: function(args){
var addr_ArtMethod
=
args[
1
];
allocPrettyMethod.writeByteArray(allocPrettyMethodInit);
PrettyMethod(addr_ArtMethod_PrettyMethod, addr_ArtMethod, allocPrettyMethod,
0x100
);
var methodName
=
allocPrettyMethod.readCString();
if
(methodName.indexOf(
"com.cwuzao.javajnitrace"
) >
-
1
){
this.showResult
=
true;
this.methodName
=
methodName;
this.tid
=
args[
0
].readPointer().add(
0x10
).readU32();
var argscount
=
args[
2
].add(
0x8
).readU32();
console.log(
"["
+
this.tid
+
"]InvokeWithArgArray->"
,methodName, args[
1
], args[
2
], args[
3
], args[
4
]);
/
/
console.log(
"["
+
this.tid
+
"]args count:"
, argscount,
"NumBytes:"
, args[
2
].add(
0xC
).readU32());
/
/
console.log(
"addr_InvokeWithArgArray args[0]->"
, hexdump(args[
2
].add(
0x10
).readPointer()));
var argspointer
=
args[
2
].add(
0x10
).readPointer();
for
(var i
=
0
; i< argscount; i
+
+
){
console.log(
"["
+
this.tid
+
"] args_"
, i
+
1
,
"->0x"
+
argspointer.add(i
*
4
).readU32().toString(
16
));
}
}
},
onLeave: function(retval){
if
(this.showResult){
console.log(
"["
+
this.tid
+
"]InvokeWithArgArray onLeave->"
,this.methodName,
"\n result->0x"
+
retval.toString(
16
));
}
}
});
Interceptor.attach(addr_ArtInterpreterToInterpreterBridge, {
onEnter: function(args){
var addr_ArtMethod
=
args[
2
].add(
8
).readPointer();
allocPrettyMethod.writeByteArray(allocPrettyMethodInit);
PrettyMethod(addr_ArtMethod_PrettyMethod, addr_ArtMethod, allocPrettyMethod,
0x100
);
this.methodName
=
allocPrettyMethod.readCString();
if
(this.methodName.indexOf(searchName) >
-
1
){
this.showReuslt
=
true;
/
/
this.result
=
args[
4
];
this.result
=
args[
2
].add(
0x3C
)
this.tid
=
args[
0
].add(
0x10
).readU32();
console.log(
"["
+
this.tid
+
"]ArtInterpreterToInterpreterBridge onEnter->"
,this.methodName);
/
/
var regcount
=
args[
2
].add(
0x30
).readU32();
/
/
var registers_size_
=
args[
1
].readU16();
var ins_size_
=
args[
1
].add(
0x2
).readU16();
var outs_size_
=
args[
1
].add(
0x4
).readU16();
/
/
console.log(
"registers_size_->"
,registers_size_,
"ins_size_->"
,ins_size_,
"outs_size_->"
,outs_size_);
/
/
读取参数
var argsPointer
=
args[
2
].add(
0x3C
+
4
*
outs_size_);
/
/
console.log(hexdump(argsPointer));
for
(var i
=
0
; i< ins_size_; i
+
+
){
console.log(
"["
+
this.tid
+
"] args_"
+
i
+
"->0x"
+
argsPointer.add(i
*
4
).readU32().toString(
16
));
}
}
},
onLeave: function(retval){
if
(this.showReuslt){
console.log(
"["
+
this.tid
+
"]ArtInterpreterToInterpreterBridge onLeave->"
,this.methodName,
"\n result->0x"
+
this.result.readU32().toString(
16
));
}
}
});
Interceptor.attach(addr_ArtInterpreterToCompiledCodeBridge, {
onEnter: function(args){
var addr_ArtMethod
=
args[
3
].add(
8
).readPointer();
allocPrettyMethod.writeByteArray(allocPrettyMethodInit);
PrettyMethod(addr_ArtMethod_PrettyMethod, addr_ArtMethod, allocPrettyMethod,
0x100
);
this.methodName
=
allocPrettyMethod.readCString();
if
(this.methodName.indexOf(searchName) >
-
1
){
this.showReuslt
=
true;
this.result
=
args[
4
];
this.tid
=
args[
0
].add(
0x10
).readU32();
console.log(
"["
+
this.tid
+
"]ArtInterpreterToCompiledCodeBridge onEnter->"
,this.methodName, args[
1
],args[
2
],args[
3
]);
var registers_size_
=
args[
3
].add(
0x30
).readU32();
var argsPointer
=
args[
3
].add(
0x3C
);
/
/
这里并不是参数数量
for
(var i
=
0
; i< registers_size_; i
+
+
){
console.log(
"["
+
this.tid
+
"] args_"
+
i
+
"->0x"
+
argsPointer.add(i
*
4
).readU32().toString(
16
));
}
}
},
onLeave: function(retval){
if
(this.showReuslt){
console.log(
"["
+
this.tid
+
"]ArtInterpreterToCompiledCodeBridge onLeave->"
,this.methodName,
"\n result->0x"
+
this.result.readU16().toString(
16
));
}
}
});
Interceptor.attach(addr_ArtInterpreterToInterpreterBridge, {
onEnter: function(args){
var addr_ArtMethod
=
args[
2
].add(
8
).readPointer();
allocPrettyMethod.writeByteArray(allocPrettyMethodInit);
PrettyMethod(addr_ArtMethod_PrettyMethod, addr_ArtMethod, allocPrettyMethod,
0x100
);
this.methodName
=
allocPrettyMethod.readCString();
if
(this.methodName.indexOf(searchName) >
-
1
){
this.showReuslt
=
true;
/
/
this.result
=
args[
4
];
this.result
=
args[
2
].add(
0x3C
)
this.tid
=
args[
0
].add(
0x10
).readU32();
console.log(
"["
+
this.tid
+
"]ArtInterpreterToInterpreterBridge onEnter->"
,this.methodName);
/
/
var regcount
=
args[
2
].add(
0x30
).readU32();
/
/
var registers_size_
=
args[
1
].readU16();
var ins_size_
=
args[
1
].add(
0x2
).readU16();
var outs_size_
=
args[
1
].add(
0x4
).readU16();
/
/
console.log(
"registers_size_->"
,registers_size_,
"ins_size_->"
,ins_size_,
"outs_size_->"
,outs_size_);
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课