之前的文章总结了混淆过程中遇到的问题,以及最终的解决方案,接下来将会用三四篇文章来逐一介绍下各种PASS的实现细节与原理,基本的控制流伪造、虚假控制流、字节替换等将不再复述,这些PASS已经有很多成熟的解析文章参考。该系列文章主要探讨Hikari中如何实现反class dump、反debug、反hook等,本文章分析AntiClassDump的实现细节。以下源码参考来自https://github.com/61bcdefg/Hikari-LLVM15 ,感谢Hikari原作者以及更多贡献者的付出。
Objective-C 代码中以 -
开头的方法是实例方法。它属于类的某一个或某几个实例对象,类对象必须实例化后才可以使用。
以 +
开头的方法是类方法。Objc中的类方法类似Java中的static方法,它是属于类本身的方法,不需要实例化类,用类名即可使用。
用 Hopper 查看反编译的 Object-C 项目,对照源代码和 struct __objc_data
、 struct __objc_method
、 struct __objc_method_list
这几个结构体,可以看到:
+initialize 和 +load 方法都是 NSObject 类的初始化方法,调用顺序均为:基类->子类。区别是 +load 在类被添加到 runtime 时调用,+initialize 在类接收到第一条消息时调用。
Category 的作用是为已经存在的类添加方法。Category 声明文件和实现文件统一采用“原类名+Category名”的方式命名。下面的代码定义了一个Category MyAddition
,向类 MyClass
添加了函数 printNameAddition
:
实现相应方法并编译为bitcode assembly查看,发现多了下面一些结构体:
其中,全局变量 @"\01l_OBJC_$_CATEGORY_MyClass_$_MyAddition"
是一个结构体 struct _category_t
,其定义如下:
将程序编译为二进制后再查看,可以发现已经找不到 MyAddition
这一名称,而函数 printNameAddition()
已经在 MyClass
的方法列表中。Category的方法被放到了方法列表的前面。因此由于查找方法时是顺序查找的,category的方法会“覆盖”掉原来类的同名方法。
整体来讲,该PASS的实现原理主要是在ObjC类的初始化函数中插入相关代码以防止反编译,大致步骤如下:
对类的方法进行重命名:可以通过替换类的方法实现,给方法添加新的名字。这样反编译工具在分析代码时会遇到重命名的方法,增加了阅读和理解代码的困难度。
修改类的方法实现:可以向方法的实现中插入一些无用的代码片段,或者进行代码混淆,使得反编译工具难以还原出原始的代码逻辑。
加入代码验证逻辑:可以向方法的实现中插入一些验证逻辑,例如检查函数参数、返回值等,对代码进行验证,防止反编译工具逆向分析。
使用编译器提供的安全特性:例如Apple的ptrauth和Opaque Pointers,可以使得函数指针和类指针更难以被破解和篡改。
总体来说,通过在ObjC类的初始化函数中插入代码,可以增加反编译的难度,使得反编译工具难以还原出原始的代码逻辑和结构,保护代码的安全性和保密性。
doInitialization是Pass的初始化函数,执行时会进行以下操作:
获取和检查目标三元组:
代码首先获取了模块(Module
)的目标三元组信息,并存储在变量triple
中,这通常包括了架构、厂商和操作系统信息。然后,代码检查了这个三元组是否代表苹果公司的架构,因为此Pass专门用于处理苹果的Objective-C实现。
定义基础类型:
接下来,定义了一个指向int8
类型指针的LLVM类型(Int8PtrTy
),这个类型在Objective-C运行时函数声明中会用到。
添加Objective-C运行时函数声明:
然后,代码创建了各种Objective-C运行时函数的类型,并使用这些类型来声明这些函数。这在后续的Pass执行中,可能会用于插入或修改这些函数的调用。
例如,声明了class_replaceMethod
和sel_registerName
函数,这些函数分别用于替换类中的方法和注册方法的选择器(selector)。
判断特性支持:
最后,代码检查当前的模块是否支持Apple的指针认证(appleptrauth
)和LLVM上下文是否支持不透明指针(opaquepointers
)。通过这两个布尔变量将决定后续Pass的行为,尤其是在处理指针和类型时。
返回值:
如果以上步骤成功执行,函数返回true
,表示初始化成功。如果检测到不支持苹果的架构,则输出错误信息并返回false
。
综上所述,doInitialization
函数负责为一个LLVM Pass做准备工作,包括确认目标平台、声明所需的运行时函数,并检查相关特性支持,以便Pass在后续的操作中可以正确地处理Objective-C代码。
runOnModule是PASS的通用框架函数,在Pass运行时执行,这个函数的逻辑如下:
获取全局变量OBJC_LABEL_CLASS_$
:
尝试获得一个名为OBJC_LABEL_CLASS_$
的全局变量,它包含Objective-C类的信息。
检查全局变量:
检查是否成功获取到了这个全局变量,如果没有,输出错误消息并返回false
。
获取全局变量的初始化值:
验证全局变量OLCGV
是否有初始化器,并将其转换为常量数组OBJC_LABEL_CLASS_CDS
。
类信息处理:
定义容器来存储类信息:readyclses
存储可处理的类,tmpclses
临时存储有依赖关系的类,dependency
存储类与父类的依赖关系,GVMapping
将类名映射到相应的全局变量。
遍历处理类信息:
遍历OBJC_LABEL_CLASS_CDS
数组,提取每个类的名称和父类信息,然后根据是否有父类及父类是否已经初始化来决定将类名放入哪个容器。
顺序处理类依赖:
使用一个循环,根据dependency
中的依赖关系对类进行排序,以确保在处理子类之前,其父类已经处理。
处理每个类:
对于readyclses
中的每个类,调用handleClass
函数进行进一步处理。
返回值:
函数返回true
,表示模块处理完成。
总结来说,runOnModule
函数的目的是处理Objective-C模块中所有的类。它首先会找到一个包含类定义的全局变量,然后遍历该变量中的所有类,并根据每个类是否有父类和父类是否已初始化来建立处理顺序。最后,函数会按照确定的顺序处理每个类。
接下来就是核心函数handleClass,该函数主要处理Objective-C类的信息,我们分步骤来进行分析:
类初始化器检查:
首先,确认传入的GlobalVariable
参数GV
(代表一个Objective-C类)有一个初始化器。如果没有,断言将失败。
获取类名和父类名:
读取GV
的初始化值,该值为一个ConstantStruct
类型的常量。接着提取类名和父类名,这些信息用于输出日志,以及后续处理。
元类和类只读结构提取:
从ConstantStruct
中提取出元类(metaclassGV
)和只读类信息(class_ro
),这些可能包含方法列表等重要数据。
方法列表检查:
如果找到了元类中的方法列表,遍历列表并提取方法的名称、类型和实现。
如果方法的名称是initialize
或load
,这意味着找到了现有的初始化器方法,将记录该方法的入口基本块(EntryBB
)。
初始化器方法创建:
如果没有找到现有的初始化器方法,函数将创建一个新的初始化方法,创建一个新的函数,并将其基本块设置为EntryBB
。
IRBuilder初始化:
使用找到或创建的基本块EntryBB
初始化IRBuilder
,用于构建LLVM中间表示(IR)指令。
准备Objective-C API定义:
获取objc_getClass
函数的声明,它将在后续的处理中使用。
处理类和元类的方法列表:
对于元类和类的方法列表,调用HandleMethods
函数添加和处理方法。
如果方法列表不为空,处理实例方法(对于元类)和类方法(对于类本身)。
创建新方法列表:
创建新的方法结构体以及方法列表,
更新类方法映射:
更新类方法映射主要是将新创建的方法列表添加到类的结构体中。这一过程的关键代码如下:
该函数主要包括类结构的验证、类信息的提取、方法列表的处理、新方法的创建和注入,主要是创建新的方法结构并更新类的方法映射,使用bitcast
操作将新的方法结构体变量的地址转换为所需的类型,并更新类结构中的方法列表指针,替换旧的方法列表。
最后我们分析HandleMethods函数,该函数处理Objective-C类(或元类)的方法列表,并且可以替换现有的方法实现。
当然,我们将根据提供的代码逐步分析 HandleMethods
函数的实现,并在每个步骤中突出关键代码。
这里代码获取Objective-C运行时的关键函数指针,这些函数将用来注册选择器、替换方法以及获取类名和元类。
获取表示Objective-C方法列表的结构类型信息
遍历 class_ro
结构体的每个元素,寻找方法列表,并跳过空值元素。
代码检查当前元素是否为方法列表的类型,并处理相应的方法列表。
获取方法列表结构体并验证其是否包含有效的方法。如果方法列表为空,则返回不做进一步处理。
遍历方法列表中的每个方法。对于每个方法,结构体 methodStruct
包含了方法名、类型签名和方法的实现(IMP)。
对于每个方法,提取方法名并使用 sel_registerName
注册其选择器(SEL)。这是Objective-C的消息发送机制中标识方法的关键。
提取该方法的实现(IMP),并将其转换为适当的类型以便进一步使用。
根据是否是元类,使用 class_getName
和 objc_getMetaClass
或直接使用 Class
参数,来准备调用 class_replaceMethod
的参数列表。
从 methodStruct
提取方法类型,并将其转换为全局字符串指针。
使用准备好的参数调用 class_replaceMethod
,实际替换或注册类中的方法实现。
如果启用了 RenameMethodIMP
选项,则将方法实现重命名为 "ACDMethodIMP"
。
在整个 HandleMethods
函数中,分析了如何操作Objective-C的运行时以动态修改类的方法列表。显示了如何构建并使用IRBuilder来创建和调用LLVM IR指令,如何使用Objective-C运行时函数,以及如何处理Objective-C类结构体中的方法列表数据。
该文章以函数为单位进行了逻辑梳理,并列出了对应的关键代码,仅为个人理解,如果存在理解有误或意义不达的地方还请指正。接下来会针对反HOOK、反Debug进行源码分析,感谢。
@interface MyClass : NSObject
- (
void
)printName;
@end
@interface MyClass(MyAddition)
- (
void
)printNameAddition;
@end
@interface MyClass : NSObject
- (
void
)printName;
@end
@interface MyClass(MyAddition)
- (
void
)printNameAddition;
@end
@
"\01l_OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition"
= ...
@
"\01l_OBJC_$_CATEGORY_MyClass_$_MyAddition"
=
private global %struct._category_t { ... }, section
"__DATA, __objc_const"
, align 8
...
@
"OBJC_LABEL_CATEGORY_$"
= private global ...
@
"\01l_OBJC_$_CATEGORY_INSTANCE_METHODS_MyClass_$_MyAddition"
= ...
@
"\01l_OBJC_$_CATEGORY_MyClass_$_MyAddition"
=
private global %struct._category_t { ... }, section
"__DATA, __objc_const"
, align 8
...
@
"OBJC_LABEL_CATEGORY_$"
= private global ...
struct
_category_t {
const
char
*
const
name;
struct
_class_t *
const
cls;
const
struct
_method_list_t *
const
instance_methods;
const
struct
_method_list_t *
const
class_methods;
const
struct
_protocol_list_t *
const
protocols;
const
struct
_prop_list_t *
const
properties;
const
struct
_prop_list_t *
const
class_properties;
const
uint32_t size;
}
struct
_category_t {
const
char
*
const
name;
struct
_class_t *
const
cls;
const
struct
_method_list_t *
const
instance_methods;
const
struct
_method_list_t *
const
class_methods;
const
struct
_protocol_list_t *
const
protocols;
const
struct
_prop_list_t *
const
properties;
const
struct
_prop_list_t *
const
class_properties;
const
uint32_t size;
}
triple = Triple(M.getTargetTriple());
if
(triple.getVendor() != Triple::VendorType::Apple) {
return
false
;
}
triple = Triple(M.getTargetTriple());
if
(triple.getVendor() != Triple::VendorType::Apple) {
return
false
;
}
Type *Int8PtrTy = Type::getInt8PtrTy(M.getContext());
Type *Int8PtrTy = Type::getInt8PtrTy(M.getContext());
FunctionType *IMPType =
FunctionType::get(Int8PtrTy, {Int8PtrTy, Int8PtrTy},
true
);
PointerType *IMPPointerType = PointerType::getUnqual(IMPType);
FunctionType *class_replaceMethod_type = FunctionType::get(
IMPPointerType, {Int8PtrTy, Int8PtrTy, IMPPointerType, Int8PtrTy},
false
);
M.getOrInsertFunction(
"class_replaceMethod"
, class_replaceMethod_type);
FunctionType *sel_registerName_type =
FunctionType::get(Int8PtrTy, {Int8PtrTy},
false
);
M.getOrInsertFunction(
"sel_registerName"
, sel_registerName_type);
FunctionType *IMPType =
FunctionType::get(Int8PtrTy, {Int8PtrTy, Int8PtrTy},
true
);
PointerType *IMPPointerType = PointerType::getUnqual(IMPType);
FunctionType *class_replaceMethod_type = FunctionType::get(
IMPPointerType, {Int8PtrTy, Int8PtrTy, IMPPointerType, Int8PtrTy},
false
);
M.getOrInsertFunction(
"class_replaceMethod"
, class_replaceMethod_type);
FunctionType *sel_registerName_type =
FunctionType::get(Int8PtrTy, {Int8PtrTy},
false
);
M.getOrInsertFunction(
"sel_registerName"
, sel_registerName_type);
appleptrauth = hasApplePtrauth(&M);
opaquepointers = !M.getContext().supportsTypedPointers();
appleptrauth = hasApplePtrauth(&M);
opaquepointers = !M.getContext().supportsTypedPointers();
GlobalVariable *OLCGV = M.getGlobalVariable(
"OBJC_LABEL_CLASS_$"
,
true
);
GlobalVariable *OLCGV = M.getGlobalVariable(
"OBJC_LABEL_CLASS_$"
,
true
);
if
(!OLCGV) {
errs() <<
"No ObjC Class Found in :"
<< M.getSourceFileName() <<
"\n"
;
return
false
;
}
if
(!OLCGV) {
errs() <<
"No ObjC Class Found in :"
<< M.getSourceFileName() <<
"\n"
;
return
false
;
}
assert
(OLCGV->hasInitializer() &&
"OBJC_LABEL_CLASS_$ Doesn't Have Initializer."
);
ConstantArray *OBJC_LABEL_CLASS_CDS =
dyn_cast<ConstantArray>(OLCGV->getInitializer());
assert
(OLCGV->hasInitializer() &&
"OBJC_LABEL_CLASS_$ Doesn't Have Initializer."
);
ConstantArray *OBJC_LABEL_CLASS_CDS =
dyn_cast<ConstantArray>(OLCGV->getInitializer());
std::vector<std::string> readyclses;
std::deque<std::string> tmpclses;
std::map<std::string
, std::string
> dependency;
std::map<std::string
, GlobalVariable *> GVMapping;
std::vector<std::string> readyclses;
std::deque<std::string> tmpclses;
std::map<std::string
, std::string
> dependency;
std::map<std::string
, GlobalVariable *> GVMapping;
for
(...) {
...
if
(supclsName ==
""
|| (SuperClassGV && !SuperClassGV->hasInitializer())) {
readyclses.emplace_back(clsName);
}
else
{
tmpclses.emplace_back(clsName);
}
}
for
(...) {
...
if
(supclsName ==
""
|| (SuperClassGV && !SuperClassGV->hasInitializer())) {
readyclses.emplace_back(clsName);
}
else
{
tmpclses.emplace_back(clsName);
}
}
while
(tmpclses.size()) {
std::string clstmp = tmpclses.front();
tmpclses.pop_front();
std::string SuperClassName = dependency[clstmp];
if
(SuperClassName !=
""
&&
std::find(readyclses.begin(), readyclses.end(), SuperClassName) ==
readyclses.end()) {
tmpclses.emplace_back(clstmp);
}
else
{
readyclses.emplace_back(clstmp);
}
}
while
(tmpclses.size()) {
std::string clstmp = tmpclses.front();
tmpclses.pop_front();
std::string SuperClassName = dependency[clstmp];
if
(SuperClassName !=
""
&&
std::find(readyclses.begin(), readyclses.end(), SuperClassName) ==
readyclses.end()) {
tmpclses.emplace_back(clstmp);
}
else
{
readyclses.emplace_back(clstmp);
}
}
for
(std::string className : readyclses) {
handleClass(GVMapping[className], &M);
}
for
(std::string className : readyclses) {
handleClass(GVMapping[className], &M);
}
assert
(GV->hasInitializer() &&
"ObjC Class Structure's Initializer Missing"
);
assert
(GV->hasInitializer() &&
"ObjC Class Structure's Initializer Missing"
);
ConstantStruct *CS = dyn_cast<ConstantStruct>(GV->getInitializer());
StringRef ClassName = GV->getName();
ClassName = ClassName.substr(
strlen
(
"OBJC_CLASS_$_"
));
StringRef SuperClassName =
readPtrauth(
cast<GlobalVariable>(CS->getOperand(1)->stripPointerCasts()))
->getName();
SuperClassName = SuperClassName.substr(
strlen
(
"OBJC_CLASS_$_"
));
errs() <<
"Handling Class:"
<< ClassName
<<
" With SuperClass:"
<< SuperClassName <<
"\n"
;
ConstantStruct *CS = dyn_cast<ConstantStruct>(GV->getInitializer());
StringRef ClassName = GV->getName();
ClassName = ClassName.substr(
strlen
(
"OBJC_CLASS_$_"
));
StringRef SuperClassName =
readPtrauth(
cast<GlobalVariable>(CS->getOperand(1)->stripPointerCasts()))
->getName();
SuperClassName = SuperClassName.substr(
strlen
(
"OBJC_CLASS_$_"
));
errs() <<
"Handling Class:"
<< ClassName
<<
" With SuperClass:"
<< SuperClassName <<
"\n"
;
GlobalVariable *metaclassGV = readPtrauth(
cast<GlobalVariable>(CS->getOperand(0)->stripPointerCasts()));
GlobalVariable *class_ro = readPtrauth(
cast<GlobalVariable>(CS->getOperand(4)->stripPointerCasts()));
assert
(metaclassGV->hasInitializer() &&
"MetaClass GV Initializer Missing"
);
GlobalVariable *metaclass_ro = readPtrauth(cast<GlobalVariable>(
metaclassGV->getInitializer()
->getOperand(metaclassGV->getInitializer()->getNumOperands() - 1)
->stripPointerCasts()));
GlobalVariable *metaclassGV = readPtrauth(
cast<GlobalVariable>(CS->getOperand(0)->stripPointerCasts()));
GlobalVariable *class_ro = readPtrauth(
cast<GlobalVariable>(CS->getOperand(4)->stripPointerCasts()));
assert
(metaclassGV->hasInitializer() &&
"MetaClass GV Initializer Missing"
);
GlobalVariable *metaclass_ro = readPtrauth(cast<GlobalVariable>(
metaclassGV->getInitializer()
->getOperand(metaclassGV->getInitializer()->getNumOperands() - 1)
->stripPointerCasts()));
if
(Info.find(
"METHODLIST"
) != Info.end()) {
ConstantArray *method_list = cast<ConstantArray>(Info[
"METHODLIST"
]);
for
(unsigned i = 0; i < method_list->getNumOperands(); i++) {
ConstantStruct *methodStruct =
cast<ConstantStruct>(method_list->getOperand(i));
GlobalVariable *SELNameGV = cast<GlobalVariable>(
opaquepointers ? methodStruct->getOperand(0)
: methodStruct->getOperand(0)->getOperand(0));
ConstantDataSequential *SELNameCDS =
cast<ConstantDataSequential>(SELNameGV->getInitializer());
StringRef selname = SELNameCDS->getAsCString();
if
((selname ==
"initialize"
&& UseInitialize) ||
(selname ==
"load"
&& !UseInitialize)) {
Function *IMPFunc = cast<Function>(readPtrauth(cast<GlobalVariable>(
methodStruct->getOperand(2)->stripPointerCasts())));
errs() <<
"Found Existing initializer\n"
;
EntryBB = &(IMPFunc->getEntryBlock());
}
}
}
else
{
errs() <<
"Didn't Find ClassMethod List\n"
;
}
if
(Info.find(
"METHODLIST"
) != Info.end()) {
ConstantArray *method_list = cast<ConstantArray>(Info[
"METHODLIST"
]);
for
(unsigned i = 0; i < method_list->getNumOperands(); i++) {
ConstantStruct *methodStruct =
cast<ConstantStruct>(method_list->getOperand(i));
GlobalVariable *SELNameGV = cast<GlobalVariable>(
opaquepointers ? methodStruct->getOperand(0)
: methodStruct->getOperand(0)->getOperand(0));
ConstantDataSequential *SELNameCDS =
cast<ConstantDataSequential>(SELNameGV->getInitializer());
StringRef selname = SELNameCDS->getAsCString();
if
((selname ==
"initialize"
&& UseInitialize) ||
(selname ==
"load"
&& !UseInitialize)) {
Function *IMPFunc = cast<Function>(readPtrauth(cast<GlobalVariable>(
methodStruct->getOperand(2)->stripPointerCasts())));
errs() <<
"Found Existing initializer\n"
;
EntryBB = &(IMPFunc->getEntryBlock());
}
}
}
else
{
errs() <<
"Didn't Find ClassMethod List\n"
;
}
if
(!EntryBB) {
FunctionType *InitializerType = FunctionType::get(
Type::getVoidTy(M->getContext()), ArrayRef<Type *>(),
false
);
Function *Initializer = Function::Create(
InitializerType, GlobalValue::LinkageTypes::PrivateLinkage,
"AntiClassDumpInitializer"
, M);
EntryBB = BasicBlock::Create(M->getContext(),
""
, Initializer);
ReturnInst::Create(M->getContext(), EntryBB);
}
if
(!EntryBB) {
FunctionType *InitializerType = FunctionType::get(
Type::getVoidTy(M->getContext()), ArrayRef<Type *>(),
false
);
Function *Initializer = Function::Create(
InitializerType, GlobalValue::LinkageTypes::PrivateLinkage,
"AntiClassDumpInitializer"
, M);
EntryBB = BasicBlock::Create(M->getContext(),
""
, Initializer);
ReturnInst::Create(M->getContext(), EntryBB);
}
IRBuilder<> *IRB =
new
IRBuilder<>(EntryBB, EntryBB->getFirstInsertionPt());
IRBuilder<> *IRB =
new
IRBuilder<>(EntryBB, EntryBB->getFirstInsertionPt());
Function *objc_getClass = M->getFunction(
"objc_getClass"
);
Value *ClassNameGV = IRB->CreateGlobalStringPtr(ClassName);
CallInst *Class = IRB->CreateCall(objc_getClass, {ClassNameGV});
Function *objc_getClass = M->getFunction(
"objc_getClass"
);
Value *ClassNameGV = IRB->CreateGlobalStringPtr(ClassName);
CallInst *Class = IRB->CreateCall(objc_getClass, {ClassNameGV});
ConstantStruct *metaclassCS =
cast<ConstantStruct>(class_ro->getInitializer());
ConstantStruct *classCS =
cast<ConstantStruct>(metaclass_ro->getInitializer());
if
(!metaclassCS->getAggregateElement(5)->isNullValue()) {
errs() <<
"Handling Instance Methods For Class:"
<< ClassName <<
"\n"
;
HandleMethods(metaclassCS, IRB, M, Class,
false
);
}
GlobalVariable *methodListGV = nullptr;
if
(!classCS->getAggregateElement(5)->isNullValue()) {
errs() <<
"Handling Class Methods For Class:"
<< ClassName <<
"\n"
;
HandleMethods(classCS, IRB, M, Class,
true
);
methodListGV = readPtrauth(cast<GlobalVariable>(
classCS->getAggregateElement(5)->stripPointerCasts()));
}
ConstantStruct *metaclassCS =
cast<ConstantStruct>(class_ro->getInitializer());
ConstantStruct *classCS =
cast<ConstantStruct>(metaclass_ro->getInitializer());
if
(!metaclassCS->getAggregateElement(5)->isNullValue()) {
errs() <<
"Handling Instance Methods For Class:"
<< ClassName <<
"\n"
;
HandleMethods(metaclassCS, IRB, M, Class,
false
);
}
GlobalVariable *methodListGV = nullptr;
if
(!classCS->getAggregateElement(5)->isNullValue()) {
errs() <<
"Handling Class Methods For Class:"
<< ClassName <<
"\n"
;
HandleMethods(classCS, IRB, M, Class,
true
);
methodListGV = readPtrauth(cast<GlobalVariable>(
classCS->getAggregateElement(5)->stripPointerCasts()));
}
Type *objc_method_type =
StructType::getTypeByName(M->getContext(),
"struct._objc_method"
);
Constant *newMethod = ConstantStruct::get(
cast<StructType>(objc_method_type),
ArrayRef<Constant *>(methodStructContents));
ArrayType *AT = ArrayType::get(objc_method_type, 1);
Constant *newMethodList = ConstantArray::get(
AT, ArrayRef<Constant *>(newMethod));
Type *objc_method_type =
StructType::getTypeByName(M->getContext(),
"struct._objc_method"
);
Constant *newMethod = ConstantStruct::get(
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2024-1-9 17:07
被ElainaDaemon编辑
,原因: 添加原创标签