jquerybind-显卡是什么

callstack
2023年4月4日发(作者:火炬之光 修改器)

iOSCrash栈的捕获和分析

在iOS应⽤开发和线上运⾏的过程中,我们总会被反馈到各种各样的崩溃。很多崩溃通过case的描述,就能很快的重现并得到修复,但是更

多的崩溃也许这⼀辈⼦就发⽣这么⼀次,也许我们永远不知道它什么时候再会出现。

同时,就算我们捕获到⼀个Crash栈,由于版本环境等种种原因,或者发⽣崩溃的代码我们就⽆法得到它详细的源码,我们往往会对着⼀⽚

全是程序指令偏移量的Crash栈⼀脸蒙蔽。

基于以上事实,我们需要从Crash栈的捕获和分析这两个⾓度进⾏深⼊的了解。

本博客主要内容分为两部分:

OC中的Crash异常的总结和捕获⽅法

利⽤Hopper对Crash栈进⾏分析

OC中的Crash异常的总结和捕获⽅法

相对于java从设计之初就养成的⼀条exception往下流,trycatch到底的作风,在我们iOS开发过程中,oc的异常处理就是⼀个不可逾越的

障碍阻碍着程序的运⾏与调试。因为oc⼀般⽤NSError甩错误,⼀旦遇到异常,⼋成就是⾮常⾮常严重的不可挽回的错误了,并且由于oc

往下直通c层,⾥⾯发⽣的异常简直是多种多样⾮常难以准确定位和分析。因此,我们来总结⼀下常见的异常和抓取处理分析⽅式。

OCException

oc层的异常是ios开发中最最最好抓取和分析的异常了。制造⼀个典型的oc异常简直再简单不过:

NSString*str=nil;

NSDictionary*dic=@{@"key":str};

//or

NSArray*array=@[@"a",@"b",@"c"];

[arrayobjectAtIndex:5];

//or

NSAssert(false,@"OCException");

显然,分别是NSDictionary的value不能为空,和NSArray取数据越界,和最暴⼒的assert直接抛出来的异常。这些在oc层⾯由iOS库或者

各种第三⽅库或者ocruntime验证出错误⽽抛出的异常,就是oc异常了。在debug环境下,oc异常导致的崩溃log中都会输出完整的异常信

息,⽐如:***Terminatingappduetouncaughtexception‘NSInternalInconsistencyException’,reason:‘OCException’。

包括这个Exception的类名和描述,下⾯是这个异常的完整堆栈。所以就算xcode的断点停在了main.m⾥⾯,我们也可以轻易的找到异常

的位置修复问题。

另外,oc异常还有⼀个⾮常好⽤的特性是可以⽤trycatch抓住(虽然苹果并不建议这么使⽤)。例如:

@try{

NSAssert(false,@"OCException");

}@catch(NSException*exception){

NSLog(@"%@",exception);

}

就可以获取到当前抛出异常并且阻⽌异常继续往外抛导致程序崩溃。虽然苹果真的不建议这样做。对于程序真的往外抛出并且我们很难

catch到的异常,⽐如界⾯和第三⽅库中甩出来的异常,我们也有⽅式可以截获到。NSException.m这个⽂件中携带了⼀个void

NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler*_Nullable);的函数可以注册⼀个函数来处理未被捕获的异常。虽

然⽆法阻⽌程序崩溃,但是可以取得异常进⾏⼀些准备和后续处理,使⽤起来这样:

voidHandleException(NSException*exception){

NSArray*stackArray=[exceptioncallStackSymbols];

NSString*reason=[exceptionreason];

NSString*name=[exceptionname];

NSString*exceptionInfo=[NSStringstringWithFormat:@"Exceptionreason:%@nExceptionname:%@nExceptionstack:%@",name,reason,stackArray];

NSLog(@"%@",exceptionInfo);

}

NSSetUncaughtExceptionHandler(&HandleException);

往往我们要做的,是把异常信息保存到本地,等到下次启动的时候进⾏⼀些后续处理。这些就是crash收集⼯具所做的事⼉。当然,如果妄

想在HandleException时拉界⾯的话,就算了吧,这个函数运⾏完成后马上就崩溃了。

MachException

从OC异常往底层⾛,我们看到的是Mach异常。Mach异常是FreeBSD上特有定义的⾼层异常,当然,现在⽹络上能收集到的资料都和

mac和ios开发有关。相关的源码⽹络上可以找到。看到异常定义的名称我们会感觉到异常的亲切——EXC_MASK_开头的异常呢。我们⼀

⼀来总结常见的两个Mach异常吧:

EXC_BAD_ACCESS(BadMemoryAccess)

这是最常见并且我们觉得最头疼的,内存访问错误。这种异常分为两种:

1.访问对象未初始化(SIGBUS信号)

2.访问了什么东西已经被回收掉了(SIGSEGV信号)

当然,事实上到底是怎样的错误⽐上⾯描述的复杂神秘得多,这才是这个最难处理的主要原因。

EXC_BAD_ACCESS同时也提供了辅助的异常code来帮助我们判断到底是什么错误,⽐如KERN_PROTECTION_FAILURE是指的地址⽆

权限访问,KERN_INVALID_ADDRESS是指的地址不可⽤,异常信息中还会包括具体出错的地址。也许可以获得更多的帮助呢。在debug

运⾏是打开内存管理的ZombieObjects可以获得有效的调试信息。

EXC_BAD_INSTRUCTION(IllegalInstruction)

通常通过SIGILL信号触发的异常,很明显,它是在说运⾏了⼀条⾮法的指令。往往错误是这样⼦的:

XC_BAD_INSTRUCTION(code=EXC_I386_INVOP,subcode=0x0)

虽然是这样说,都是编译器编译出来的指令怎么会有⾮法指令嘛。所以事实上遇到这样的问题往往是运⾏指令的参数不对,多半是为0即nil

了。然后我们⼜回到了空指针的问题了~。

当然,除了代码中的问题。更多的是ios开发中的⽞学问题导致的ios本⾝异常和bug,⽐如就是这样。解决这些问题,还是得⽼⽼实实的分

析堆栈猜测和分析了。

其他

其他在实际开发中有可能遇到的并不多,主要是:

_RESOURCE是指的程序到达资源上限,⽐如cpu占⽤过⾼,内存不⾜之类的。这样的问题也没法解决啦。

_GUARD是⼀些c层函数访问错误导致的异常,⽐如fopen⽂件访问错误之类的都会爆出这个。不过我们好好的oc不⽤肯定⼀般

也不会使⽤这些,所以还安好。

3.0x00000020,这些是被FreeBSD定义为⽞学异常的异常都在⾥⾯了,也提供了特殊的code来提供辅助信息。其中其实最常见的

code是0x8badf00d,是主线程阻塞时间太长,程序被os杀了。其他的遇到了就是见⿁了!

UnixSignalExceptions

从Mach异常再往上⾛追根究底,其实,所以异常发⽣的本质途径都是Unix的异常信号。

异常并不是真正的异常,但是当⼀个OC异常被抛出到最外层还没被谁捕获,程序会强⾏发送SIGABRT信号中断程序。

异常没有⽐较⽅便的捕获⽅式,不过由于它本质就是信号,所以这⼀段讲的东西也能包含处理Mach异常。

产⽣⼀个不属于Mach异常的异常信号也是⾮常⾮常简单的事⼉,⽐如:

int*i;

free(i);

总之,c层⾯,runtime或者其他东西控制程序就是通过信号,中断当然也不例外。通过不同的信号,我们也能知道很多不同的东西。在ios

开发环境中,信号枚举在sys/signal.h⽂件中,我们可以看到⼤量的Unix信号罗列其中,参考可以看到各个信号的详解。当然,我们最终关

⼼的是能否捕获这些异常信号来抓住异常和崩溃。对,⽅法是有的,这⾥提供了⼀个叫void(signal(int,void()(int)))(int);的⽅法来注册⼀

个处理函数。

这个⽅法最后吐出来的是当前的信号,没异常信息堆栈怎么办,还好,从execinfo.h中,我们可以取出当然汇编层程序的堆栈情况。这就好

办了,最后处理代码如下:

voidSignalExceptionHandler(intsignal){

NSMutableString*mstr=[[NSMutableStringalloc]init];

[mstrappendString:@"Stack:n"];

void*callstack[128];

inti,frames=backtrace(callstack,128);

char**strs=backtrace_symbols(callstack,frames);

for(i=0;i

[mstrappendFormat:@"%sn",strs[i]];

}

}

voidInstallSignalHandler(void){

signal(SIGHUP,SignalExceptionHandler);

signal(SIGINT,SignalExceptionHandler);

signal(SIGQUIT,SignalExceptionHandler);

signal(SIGABRT,SignalExceptionHandler);

signal(SIGILL,SignalExceptionHandler);

signal(SIGSEGV,SignalExceptionHandler);

signal(SIGFPE,SignalExceptionHandler);

signal(SIGBUS,SignalExceptionHandler);

signal(SIGPIPE,SignalExceptionHandler);

}

要注意的是这⾥获得的堆栈信息是,当前汇编⼦程序的offset+指令offset,要么我们需要符号表,要么我们需要反编译⼀些我们的程序来

对应代码了。

利⽤Hopper对Crash栈进⾏分析

对于上⽂已经获得的crash堆栈,⽆论是否可以通过符号表获得代码实际情况,只要我们没发看到确切的代码,都是⽆法直接通过crash栈

直接进⾏分析。特别是遇到整个crash堆栈⾥⾯完全没有⾃⼰项⽬的代码,或者虽然是我们的项⽬名下的堆栈,却是通过pod引⼊的第三⽅

库。更现实的是,为了加速代码编译或者开发者⼲脆就是闭源的,往往pod引⼊的库都是⼆进制的静态库,所以我们得到的堆栈肯定没有具

体代码⾏数,看到堆栈肯定是⽆计可施。

遇到这样的情况,我们看到的堆栈往往是:0x100072ea40x100050000+143012这样只会有堆栈指令的pc位置或者⽅法名+

offset显⽰出来的pc位置,⽽不是。这样我们需要分析代码,只有通过分析具体的汇编指令才能继续下去。

⽽Hopper这个iOS查看和半反编译⼯具正适合这件事。

准备⼯作

⾸先我们当然要下载⼀个。这个软件demo版可以直接使⽤完整功能,和Charles⼀样每次启动可以使⽤30分钟——对于我们勉强够⽤了,

动⼼了可以买买买~

另外,我们还需要找到⽤于进⾏反编译的程序。理论上,它在ipa包的/Payload//xxx即对应的编译结果,其中在本地xcode编译出

来的app在~/Library/Developer/Xcode/DerivedData下。

最后,我们当然要准备好需要的crash堆栈,另外在旁边准备⼀个科学计算器⽐较好。

另外再⽤浏览器开⼀个ARM汇编指令⼤全吧。

iOS的ARM汇编基础

虽然基本上只需要⼀丁点⼉汇编基础知识就可以开展⼯作,还是有⼀些需要知道的。

寄存器相关:⼀共有31个64位通⽤寄存器,x0~x30。其中x29是framepointer;x30是procedurelinkregister;还有sp和pc。

常⽤的汇编指令我们需要了解的主要是:

movr1,r2把r2的数据赋予r1

ldrr1,r2把r2指向的数据赋予r1

strr1,r2把r1的数据赋予r2指向的地⽅

addsub之类的运算符肯定是需要的

那⼀堆超级⿇烦的跳转判断指令

bl调⽤⼦程序

[r1,0xXXXX]这样offset的⽅法

另外oc⽅法调⽤的情况下:

idvalue=[objmethodKey1:key1andKey2:key2];

编译到c层实际调⽤是:

idvalue=objc_msgSend(obj,@selector(methodKey1:andKey2:),key,key2);

当然,c的函数对应的其实是汇编调⽤⼦函数。因此我们需要的⼊⼝参数obj,selector,key,key2…其实是通过r0,r1,r2…..传输的,特殊情况

下可能会通过堆栈传输,不过⼀般不会~。另外返回值会直接返回到r0⾥边。

嗯,知道这些就可以了。

栗⼦:⼀次完整的分析

这次我们分析的完整的崩溃堆栈是这样的:

ExceptionType:EXC_CRASH(SIGABRT)

ExceptionCodes:0x0000,0x0000

ExceptionNote:EXC_CORPSE_NOTIFY

TriggeredbyThread:8

ApplicationSpecificInformation:

abort()called

Filteredsyslog:

Nonefound

LastExceptionBacktrace:

0CoreFoundation0x18a1151b8__exceptionPreprocess+124

0x188b4c55cobjc_exception_throw+56

2CoreFoundation0x18a11c268-[NSObject(NSObject)doesNotRecognizeSelector:]+140

3CoreFoundation0x18a119270___forwarding___+916

4CoreFoundation0x18a01280c_CF_forwarding_prep_0+92

5kmall0x1004b103c0x100050000+4591676

6kmall0x1003d1ef80x100050000+3677944

7kmall0x1003d23a00x100050000+3679136

0x188f9e1fc_dispatch_call_block_and_release+24

0x188f9e1bc_dispatch_client_callout+16

0x188fac3dc_dispatch_queue_serial_drain+928

0x188fa19a4_dispatch_queue_invoke+652

0x188fac8d8_dispatch_queue_override_invoke+360

0x188fae34c_dispatch_root_queue_drain+572

0x188fae0ac_dispatch_worker_thread3+124

15libsystem_0x1891a72a0_pthread_wqthread+1288

16libsystem_0x1891a6d8cstart_wqthread+4

从堆栈的⾓度,可以看到,倒数第三层调⽤到了doesNotRecognizeSelector⽅法然后抛出了异常,结合上下⽂,可以猜想到应该是某⼀个

object存在,但是调⽤了不存在的⽅法——也许是类型错误,导致了这个崩溃的发⽣。

⽽查询后,kmall的三层均不是我们项⽬代码,⽽是闭源的第三⽅库中抛出来的错误,⽆法得到其他信息。因此现在,只有从kmall最⾼的那

⼀层,即第5层堆栈开始⼊⼿分析汇编代码了。

堆栈第⼀层

我们看到的地址是0x1004b103c0x100050000+4591676,其实就是程序的0x46103C偏移位置。直接⽤hopper打开程序进⾏反

汇编找到对应的⼦函数:

;================BEGINNINGOFPROCEDURE================

+[GuardCommonencrypt:withKey:byAlgorithm:]:

0f1cstpx29,x30,[sp,#-0x10]!;ObjectiveCImplementationdefinedat0x1009fa370(classmethod),DATAXREF=0x1009fa

0f20movx29,sp

0f24subsp,sp,#0x80

0f28subx8,x29,#0x20

0f2cmovzx9,#0x0

0f30sturx0,[x29,#-0x10]

0f34sturx1,[x29,#-0x18]

0f38sturx9,[x29,#-0x20]

0f3cmovx0,x8

0f40movx1,x2

0f44strx3,[sp,#0x40]

0f48strx4,[sp,#0x38]

0f4cblimp___stubs__objc_storeStrong

0f50subx8,x29,#0x28

0f54movzx9,#0x0

0f58sturx9,[x29,#-0x28]

0f5cldrx9,[sp,#0x40]

0f60movx0,x8

0f64movx1,x9

0f68blimp___stubs__objc_storeStrong

...

0fd0adrpx8,#0x100a64000;CODEXREF=+[GuardCommonencrypt:withKey:byAlgorithm:]+156

0fd4addx8,x8,#0x630;objc_cls_ref_GuardEncryptProcessor

0fd8ldrx8,x8

0fdcldurx9,[x29,#-0x20]

0fe0movx0,x9

0fe4strx8,[sp,#0x30]

0fe8blimp___stubs__objc_retainAutorelease

0fecadrpx8,#0x100a53000;@selector(setTitleLabelBackgroundColor:)

0ff0addx8,x8,#0x488;@selector(bytes)

0ff4ldrx1,x8

0ff8blimp___stubs__objc_msgSend

0ffcadrpx8,#0x100a52000

1000addx8,x8,#0x3a0;@selector(length)

1004ldurx9,[x29,#-0x20]

1008ldrx1,x8

100cstrx0,[sp,#0x28]

1010movx0,x9

1014blimp___stubs__objc_msgSend

1018movx2,x0

101cldurx8,[x29,#-0x28]

1020movx0,x8

1024strw2,[sp,#0x24]

1028blimp___stubs__objc_retainAutorelease

102cadrpx8,#0x100a55000;@selector(clickGoPay:)

1030addx8,x8,#0xfd0;@selector(UTF8String)

1034ldrx1,x8

1038blimp___stubs__objc_msgSend

103cldurx8,[x29,#-0x30]

这个⼦函数有点长,我先截取⼀部分看看。⾸先根据hopper部分反编译(其实是数据映射的结果),这个⼦函数对应的⽅法是+

[GuardCommonencrypt:withKey:byAlgorithm:]:。嗯,糟糕,这是⼀个第三⽅库⾥⾯的代码,并且我们找不到源码,到此为⽌我们落实

要通过分析汇编代码的⽅式来查crash了。

然后我们找到⽬标pc地址的上⼀句,是⼀句bl即调⽤⼦函数,hopper⼜很贴⼼的把ios中常见系统⼦函数给反编译告诉我们了,这是⼀句

msgSend,和我们看到堆栈预期的⼀样,调⽤了不存在的⽅法。那么我们⾸先要做的就是找到msgSend的obj和selector,他们应该在调

⽤⼦函数前被放置在了对应的x0和x1处。

往上看,x1很快就找到了。hopper也很贴⼼的把常量指向的字符串在右侧标了出来。x1是从x8加载出来的,x8指向的字符

串“UTF8String”。然后x0呢,在0x461020看到x0是从x8挪过来的,⽽那⾥x8是从[x29,#-0x28]加载出来的。那么我们接下来就是

需要关⼼[x29,#-0x28]是哪⼉来的了。

继续往上看,在⼦函数开始部分0x460f58,把原本x9的数据放⼊了[x29,#-0x28]指向的位置中,但是注意到0x460f50开始的sub最后

得到的x8也是指向的这个位置,所以我们综合看⼀下。那⼀段结束之后调⽤了objc_storeStrong⽅法,我们知道objc_storeStrong是处理

⼊参的持有问题,把⼊参数转换到另⼀个新的id上。因此考虑到分别传⼊了⼀个空的指针和⼀个x0,因此这其实是在对x8做storeStrong

初始化。

那么看到传⼊的x1即原始数据,是从哪⼉来的?在0x460f5c从[sp,#0x40]读出来的,⽽[sp,#0x40]哪⼉来的,就在上⾯⼏⾏从x3中储

存进去的,x3到此为⽌——嗯,x3不就是⼦函数的⼊参么,应该是oc⽅法的第⼆个参数吧。即+[GuardCommon

encrypt:withKey:byAlgorithm:]的key咯。

到此为⽌,我们第⼀层堆栈分析完毕,可以继续往上了。

堆栈第⼆层

然⽽,分析第⼆层我们可见的堆栈⼦程序:

;================BEGINNINGOFPROCEDURE================

-[WindFingerprintGeneratortranformToFingerprint:]:

1db0stpx29,x30,[sp,#-0x10]!;ObjectiveCImplementationdefinedat0x1009e41f8(instancemethod),DATAXREF=0x10

1db4movx29,sp

1db8subsp,sp,#0xb0

1dbcsubx8,x29,#0x30

1dc0movzx9,#0x0

1dc4adrpx10,#0x100918000

1dc8ldrx10,[x10,#0x400];___stack_chk_guard_100918400,___stack_chk_guard

1dccldrx10,x10

1dd0movx3,x10

1dd4sturx10,[x29,#-0x8]

1dd8sturx0,[x29,#-0x20]

...

1e64adrpx8,#0x100a5b000;@selector(readStream)

1e68addx8,x8,#0x270;@selector(aesKey)

1e6csturx0,[x29,#-0x40]

1e70ldurx9,[x29,#-0x20]

1e74ldrx1,x8

1e78movx0,x9

1e7cblimp___stubs__objc_msgSend

1e80movx29,x29

1e84blimp___stubs__objc_retainAutoreleasedReturnValue

1e88strx0,[sp,#0x48]

1e8ccbzx0,loc_100381e9c

1e90ldrx8,[sp,#0x48]

1e94strx8,[sp,#0x40]

1e98bloc_100381eac

loc_100381e9c:

1e9cadrpx8,#0x10092e000;CODEXREF=-[WindFingerprintGeneratortranformToFingerprint:]+220

1ea0addx8,x8,#0x390;_kAESKey

1ea4ldrx8,x8

1ea8strx8,[sp,#0x40]

loc_100381eac:

1eacldrx0,[sp,#0x40];CODEXREF=-[WindFingerprintGeneratortranformToFingerprint:]+232

1eb0blimp___stubs__objc_retain

1eb4sturx0,[x29,#-0x48]

1eb8ldrx0,[sp,#0x48]

1ebcblimp___stubs__objc_release

1ec0adrpx0,#0x10096d000;@"-(int64_t)%@;"

1ec4addx0,x0,#0xc60;@"AES"

1ec8adrpx30,#0x100a5b000;@selector(readStream)

1eccaddx30,x30,#0x278;@selector(encrypt:withKey:byAlgorithm:)

1ed0adrpx8,#0x100a64000

1ed4addx8,x8,#0x338;objc_cls_ref_GuardCommon

1ed8ldrx8,x8

1edcldurx2,[x29,#-0x40]

1ee0ldurx3,[x29,#-0x48]

1ee4ldrx1,x30

1ee8strx0,[sp,#0x38]

1eecmovx0,x8

1ef0ldrx4,[sp,#0x38]

1ef4blimp___stubs__objc_msgSend

1ef8movx29,x29

依然是⼀段分析过后的关键段落截取。⾸先看到的⽅法名-[WindFingerprintGeneratortranformToFingerprint:]:,嗯,不是可见的⽅

法,但是和刚才不同的是这是⼀个实例⽅法了,所以当前对象很重要。另外虽然⽅法没见过,WindFingerprintGenerator却是有暴露给⽤

户使⽤,所以可以找到⼀些有⽤的信息。

然后从堆栈出⼝看,嗯,果然是msgSend⽽且selector对得上,没问题。然后刚才我们注意到的是x3,那在哪⼉放进去的呢?原来是

0x381ee0⾏,从[x29,#-0x48]读取出来的。然后继续往上0x381eb4处,讲0x储存到了[x29,#-0x48]中,⽽x0⼜是从[sp,#0x40]

读取出来的。

然后上⾯这⼀段是⼀个双goto,本质上是⼀个if判断,看⼀下判断指令:cbzx0是否存在?如果存在,往下,0x381e90把[sp,#0x48]读

出来赋予了[sp,#0x40],⽽[sp,#0x48]正好⼜是x0。所以结论是如果x0存在,传给后⾯了x0的值。

另⼀个分⽀,如果x0不存在,0x381ea0开始从⼀个叫_kAESKey的静态变量读取了数据并赋予了[sp,#0x40]。

所以这⼀段其实是:

[sp,#0x40]=x0?x0:_kAESKey;

那关键其实就是x0了。考虑到后⾯的崩溃应该是对象存在但是没有⽅法,因此这⾥要么是x0不存在_kAESKey不对,要不是x0不对,我们

需要继续追踪。

这⾥往上,x0就是0x381e7c中sendMsg的返回值,其中selector是aesKey,⽽对象x0是x9从[x29,#-0x20]来的。继续往上找,

[x29,#-0x20]在0x381dd8从x0赋予,⽽这⾥是x0最早出现的位置,即当前⼦函数的obj。因此完整解释出来,就是:

[sp,#0x40]=?:_kAESKey;

诶,打住,到此为⽌。写过相关代码的同学⽴刻会发现,self,即WindFingerprintGenerator的实例的aesKey好像是暴露出来给⽤户设置

的诶。赶快去看看~~~

⾄此,这次crash分析就结束了,事实上看到的是api希望aesKey是⼀个NSString,⽽我们代码中设置成了NSNumber,由此导致的错

误。

总结

以上Crash捕获处理就可以兜底式的涵盖所有的ios应⽤异常和崩溃的情况,是⾮常有效率。⽽结合hopper帮助给⼦程序映射oc⽅法进⾏拆

分,和对常⽤oc⼦程序进⾏部分反编译之后,阅读iOS的汇编结果进⾏crash堆栈分析并不是什么困难的事情。我们可以得到很多有⽤的信

息,结合传统的crash分析⽅法和经验,可以更可靠有效的解决问题。

通过以上⼀个完整的Crash栈捕获和抓取的流程,我们可以亲⼿抓住iOS应⽤在运⾏中遇到的所有⼤⼤⼩⼩的崩溃情况,并且在⾮常劣势的

条件下,有效的对Crash进⾏分析,解决疑难杂症。

虽然通过各种第三⽅崩溃统计服务,它们可能帮助我们把以上的⼤部分⼯作都完成了。但是最好解决bug的还是我们⾃⼰啊,不知彼知⼰拿

着Crash能不⽅么~

更多推荐

callstack