如何取消qq会员-梅林固件

uikit
2023年4月4日发(作者:抖音怎么改名字)

iOS移动端的bug的排查办法,你确定不看吗?

前⾔

不积跬步⽆以⾄千⾥,不积⼩流⽆以成江海。学如逆⽔⾏⾈,不进则退。我是平平⽆奇游荡于各平台的搬运⼯。优秀的⼈已经点赞了。本⽂主要记

录了iOS移动端的⼀个疑难bug的排查过程,以及介绍通过给bitcode打补丁重新⽣成机器码,为有问题的第三⽅库修复bug的⽅法。废话

不多说,直接给⼤家上⼲货,希望能对你有所帮助,

主要涉及到的知识点如下:

ARM汇编

C++运⾏时

静态库⽂件的结构

bitcode及LLVMIR

平台监控找崩溃

通过内部的崩溃监控发现,有⼀个内部App,近期出现了较多的崩溃现象。其中数量占⽐最多的崩溃,其崩溃线程捕获到的调⽤栈如下:

libsystem_0x00000001cc78c414___pthread_kill+7

libsystem_0x00000001a7db2b74_abort+103

App0x2868___48-[BLYLogicManagerabortAfterSendingReportIfNeed]_block_invoke+87

0x000000019e60824c__dispatch_call_block_and_release+31

0x000000019e609db0__dispatch_client_callout+19

0x000000019e61aa68__dispatch_root_queue_drain+655

0x000000019e61b120__dispatch_worker_thread2+115

libsystem_0x00000001ea1e77c8__pthread_wqthread+215

调⽤现场出端倪

这个调⽤栈并没有提供什么有效的信息,只能看出来是bugly框架已经检测到了崩溃创建了新的dispatchqueue并终⽌进程,也就是说,其实

有效的崩溃信息被bugly给吃掉了。

看⼀下其他线程,是否有可⽤的信息,⼀般可以在其他线程的调⽤栈上搜索以下内容:

1.

_ZSt9terminateEv

:C++的终端异常处理(

std::terminate(void)

2.

__sigtramp

:信号中断处理例程⼊⼝

终于搜索到了以下内容:

Thread#52:id=1a6c6,name=

libsystem_0x00000001cc78cf5c___ulock_wait+7

0x000000019e60a528__dispatch_thread_event_wait_slow+55

0x000000019e618708___DISPATCH_WAIT_FOR_QUEUE__+351

0x000000019e6182b0__dispatch_sync_f_slow+147

App0x25f0-[BLYLogicManagerexecuteEmergencyLogic:]+695

App0xb6a8-[BLYCrashManagersendLiveCrashReport]+203

App0xf478_BLYCrashHandlerCallback+5555

App0xbc2c_BLYBSDSignalHandlerCallback+95

libsystem_0x00000001ea1e1290__sigtramp+55

App0x43dc*redacted*

App0x43dc*redacted*

App0xa1918*redacted*

App0xea9c4*redacted*

App0xea794*redacted*

App0xead60*redacted*

libsystem_0x00000001ea1e5b40__pthread_start+319

内部应⽤同时集成了Bugly和⾃有的崩溃捕获,通常情况下Bugly会在⾃⼰捕获完成后,将崩溃现场转交给其他框架,使两次捕获的崩溃现场相

同。⽽这个崩溃则不然,Bugly捕获了崩溃后,直接调⽤

abort

结束了应⽤,导致⾃有崩溃只捕获到了

SIGABRT

通过检查主线程调⽤栈,发现了⼀些不同:

Thread#0:id=1a0d3,name=

libsystem_0x00000001cc78c1ac___psynch_cvwait+7

libc++.0x00000001b3a25328__ZNSt3__118condition_variable4waitERNS_11unique_lockINS_5mutexEEE+27

App0xe5c8*redacted*

App0xe9414*redacted*

App0xe9380*redacted*

libsystem_0x00000001a7d930b8___cxa_finalize_ranges+423

libsystem_0x00000001a7d93400_exit+27

UIKitCore0x00000001a13d4bdc-[UIApplication_terminateWithStatus:]+503

UIKitCore0x00000001a0a23648-[_UISceneLifecycleMultiplexer_evalTransitionToSettings:fromSettings:forceExit:withTransitionStore:]+127

UIKitCore0x00000001a0a23278-[_UISceneLifecycleMultiplexerforceExitWithTransitionContext:scene:]+219

UIKitCore0x00000001a13ca644-[UIApplicationworkspaceShouldExit:withTransitionContext:]+211

FrontBoardServices0x00000001ae6d2780-[FBSUIApplicationWorkspaceShimworkspaceShouldExit:withTransitionContext:]+87

FrontBoardServices0x00000001ae701390___63-[FBSWorkspaceScenesClientwillTerminateWithTransitionContext:]_block_invoke_2+79

FrontBoardServices0x00000001ae6e54a0-[FBSWorkspace_calloutQueue_executeCalloutFromSource:withBlock:]+239

FrontBoardServices0x00000001ae701328___63-[FBSWorkspaceScenesClientwillTerminateWithTransitionContext:]_block_invoke+131

0x000000019e609db0__dispatch_client_callout+19

0x000000019e60d738__dispatch_block_invoke_direct+267

FrontBoardServices0x00000001ae72a250___FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__+47

FrontBoardServices0x00000001ae729ee0-[FBSSerialQueue_targetQueue_performNextIfPossible]+447

FrontBoardServices0x00000001ae72a434-[FBSSerialQueue_performNextFromRunLoopSource]+31

CoreFoundation0x000000019e99176c___CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__+27

CoreFoundation0x000000019e991668___CFRunLoopDoSource0+207

CoreFoundation0x000000019e9909cc___CFRunLoopDoSources0+375

CoreFoundation0x000000019e98aa8c___CFRunLoopRun+823

CoreFoundation0x000000019e98a21c_CFRunLoopRunSpecific+599

GraphicsServices0x00000001b648e784_GSEventRunModal+163

UIKitCore0x00000001a13c8fe0-[UIApplication_run]+1071

UIKitCore0x00000001a13ce854_UIApplicationMain+167

App0xdc93c*redacted*

App0x25c4*redacted*

App0xbc618main(:9:13)

0x000000019e64a6b0_start+3

可以看到调⽤栈中有

exit

,这表明应⽤正在正常退出。

调⽤栈中,在

exit

这⼀项的上⾯,可以看到

__cxa_finalize_ranges

,这是由C++代码产⽣的调⽤,通过

__cxa_atexit

注册回调,在应⽤退出时

调⽤,⽤来正在进⾏全局变量的销毁。

由此可以看出,这是⼀个由于C++全局变量在应⽤退出时销毁,导致其他线程引⽤到了销毁的资源产⽣的崩溃。这也可以解释为什么Bugly没

有把崩溃现场交给其他框架进⾏处理了:Bugly检测到了应⽤正在退出,直接调⽤了

executeEmergencyLogic:

⽅法,优先保证⾃⼰的处理。

全局变量会析构

__cxa_atexit

是ItaniumC++ABI运⾏时规范的⼀部分,它⽤来⽀持C++语法中的全局变量。我们知道,C++对象分为POD和⾮POD两

类,其中POD以及constexpr构造器可以在编译期初始化,⽽⾮constexpr构造的类型只能通过构造器在运⾏期间来构造。C++对于这两类

对象,都⽀持它们作为全局变量并提供初始值,那么这些全局变量就要在dso加载期间调⽤构造器来初始化。

同样地,为了防⽌内存/资源泄漏,C++规定这样初始化的全局变量要在dso卸载时析构。

我们可以通过查看反汇编,对它的实现机制⼀探究竟。

举个例⼦,有如下C++代码:

#include

classTest{

public:

virtual~Test();

Test();

};

Test::Test(){}

Test::~Test(){

std::cout<<"Test:dtor"<

}

staticTestt=Test();

使⽤clang++对以上⽂件进⾏编译,查看⽣成的汇编代码:

xcrunclang++--s-o1.s

.section__TEXT,__StaticInit,regular,pure_instructions

.p2align2;--Beginfunction__cxx_global_var_init

___cxx_global_var_init:;@__cxx_global_var_init

.cfi_startproc

;%bb.0:

subsp,sp,#32;=32

stpx29,x30,[sp,#16];16-byteFoldedSpill

addx29,sp,#16;=16

.cfi_def_cfaw29,16

.cfi_offsetw30,-8

.cfi_offsetw29,-16

adrpx0,__ZL1t@PAGE

addx0,x0,__ZL1t@PAGEOFF

strx0,[sp,#8];8-byteFoldedSpill

bl__ZN4TestC1Ev

ldrx1,[sp,#8];8-byteFoldedReload

adrpx0,__ZN4TestD1Ev@PAGE

addx0,x0,__ZN4TestD1Ev@PAGEOFF

adrpx2,___dso_handle@PAGE

addx2,x2,___dso_handle@PAGEOFF

bl___cxa_atexit;②

ldpx29,x30,[sp,#16];16-byteFoldedReload

addsp,sp,#32;=32

ret

.cfi_endproc

;--Endfunction

.section__TEXT,__StaticInit,regular,pure_instructions

.p2align2;--Beginfunction_GLOBAL__sub_I_

__GLOBAL__sub_I_:;@_GLOBAL__sub_I_

.cfi_startproc

;%bb.0:

stpx29,x30,[sp,#-16]!;16-byteFoldedSpill

movx29,sp

.cfi_def_cfaw29,16

.cfi_offsetw30,-8

.cfi_offsetw29,-16

bl___cxx_global_var_init

ldpx29,x30,[sp],#16;16-byteFoldedReload

ret

.cfi_endproc

;--Endfunction

.section__DATA,__mod_init_func,mod_init_funcs

.p2align3

.quad__GLOBAL__sub_I_;①

从反汇编代码中,可以看出实际的⽅案是:

⽣成⼀个函数⽤来构造当前编译单元内的所有全局(以及静态)变量,将该函数写到

__mod_init_funcs

中去,这样dso加载时,动态链接器

会主动执⾏它们(位于汇编代码①处);

在这个⽣成的函数中,调⽤

__cxa_atexit

,传⼊已经构造的对象的指针和deletingdestructor,这样dso卸载时,这些构造的对象会被销

毁(位于汇编代码②处)。

很多使⽤C++的库都会分配线程资源进⾏并发执⾏。如果这些正在执⾏的线程需要引⽤全局变量,同时触发了dso卸载,那么就会发⽣线程跑

着跑着,全局变量析构了,于是进程就崩溃了。

理论上讲,dso的卸载是可控的,因为我们总可以控制逻辑,让动态库的资源都释放掉以后,再去卸载动态库/退出进程。

但是在iOS移动应⽤上,有⼀个例外——

⽤户可以通过多任务⼿势,杀死应⽤。如果被杀的应⽤恰巧在前台运⾏,那么iOS会给这个应⽤发送

SIGTERM

信号。UIKit收到信号后会调⽤

应⽤代理的

applicationWillTerminate(_:)

⽅法,使得应⽤有机会保存⼀些状态数据,然后正常退出应⽤。

这个时候我们是没有机会释放线程资源的,因为terminate的⽣命周期很短,没有时间给我们等待异步线程结束,所以这个崩溃就⽆法避免了。

所幸的是,这种崩溃并不会被⽤户感知到:即使应⽤不崩溃,也会⽴即正常退出,对于⽤户来说表现是⼀样的。

解决需要重编译?

其实同类的问题以前在该App中也是发⽣过的——我们有⼀个内部SDK同样也是C++写成,拥有全局状态变量,开启异步线程池访问这些变

量,⽤户在前台杀死应⽤时触发崩溃。

当时的解决⽅案是:升级⼯具链。根据Apple发布的Xcode11更新⽇志,appleclang++编译器增加了禁⽤全局变量析构的编译参数

-fno-

c++-static-destructors

。使⽤该标记编译的C++源⽂件,不会⽣成对全局变量进⾏析构的代码。

这对iOS应⽤来说是安全的——因为iOS应⽤⼏乎不会在运⾏时卸载动态库,⽆需考虑动态库卸载的资源泄漏问题。

然⽽这次的问题⼜有所不同——出现问题的是⼀个由第三⽅提供的⼆进制库,我们⼿⾥是没有它的源代码的,也就⽆法通过修改编译参数的⽅式来

重新编译⽣成机器码。

但是我们能否再深⼊⼀下,帮助三⽅库来修复这个bug呢?

修复该问题的直接⽅案,就是修改机器码,消除对

__cxa_atexit

的调⽤。

静态库⾥有什么

⼀个三⽅静态库SDK,⼀般由以下⽂件组成:

⼀组头⽂件,提供了公开的函数/OC类及⽅法声明;

⼀个.a静态库,包含了这个库的代码实现,由多个编译单元⽣成的.o⽬标⽂件打包⽽成;

⼀组资源⽂件,提供代码运⾏时的外部数据(图⽚、以及其他资源)。

⽆论是采⽤零散的⽂件,还是采⽤

.framework

封装,它们的组成基本上是⼀致的。

我们要修改的是它的部分机器码,所以要将其中的.a静态库解开,再进⾏编辑。

⾸先来查看⼀下.a⽂件的内容:

❯lipo-infolibsample.a

Architecturesinthefatfile::armv7arm64

这是⼀个Universalbinary,包含了两种iOS真机的CPU架构的代码。我们先针对主流机型使⽤的arm64架构尝试调整。

使⽤

lipo

命令将arm64架构单独抽取出来:

❯lipo-thinarm64libsample.a-olibsample_arm64.a

只有把Universalbinary中特定的架构抽取出来,才能使⽤ar(1)操作:

❯mkdirobjects

❯cdobjects

#打印.a中包含的⽂件列表

❯art../libsample_arm64.a

__.SYMDEF

sample.o

sample.o

#解包.a⽂件

❯ar-x../libsample_arm64.a

使⽤上述命令进⾏.a⽂件的展开后,出现了⼀个问题:

art

命令中,列出了两个

sample.o

⽂件,但是

arx

命令只解出来了⼀个。这是因为ar

归档中,没有⽬录的概念,不同⽬录下的同名⽬标⽂件,在ar归档的过程中,会被打平,导致ar归档中包含多个同名⽂件。

这会导致我们使⽤

arx

解包的时候,相同的⽂件会被覆盖成⼀个,也没法把它们单独解压出来。

那么如何才能把ar归档中的同名⽂件分别解包出来呢……那么就得提到「不务正业」的7-zip了……

压缩软件有妙⽤

7-zip作为⼀个压缩软件,除了⽀持常规的压缩⽂件格式之外,还⽀持了很多归档⽂件以及PE可执⾏⽂件(特别地,⽀持了部分安装器的SFX

模块)。我们来尝试⼀下它是否⽀持.a归档:

❯7zl./libsample_arm64.a

7-Zip[64]17.04:Copyright(c)1999-2021IgorPavlov:2017-08-28

p7zipVersion17.04(locale=utf8,Utf16=on,HugeFiles=on,64bits,16CPUsx64)

Scanningthedriveforarchives:

1file,78960bytes(78KiB)

Listingarchive:./libsample_arm64.a

--

Path=./libsample_arm64.a

Type=Ar

PhysicalSize=78960

SubType=a:BSD

DateTimeAttrSizeCompressedName

------------------------------------------------------------------------

2021-06-2315:05:09.....

2021-06-2315:03:54......o

2021-06-2315:04:02......o

------------------------------------------------------------------------

2021-06-2315:05:3files

可以看到,7-zip⾃动为.a中的⽂件名进⾏了修正。同时,7-zip在解压的时候遇到同名⽂件,会提供是否覆盖及⾃动重命名⽂件的选项:

❯7zx./libsample_arm64.a

7-Zip[64]17.04:Copyright(c)1999-2021IgorPavlov:2017-08-28

p7zipVersion17.04(locale=utf8,Utf16=on,HugeFiles=on,64bits,16CPUsx64)

Scanningthedriveforarchives:

1file,78960bytes(78KiB)

Extractingarchive:./libsample_arm64.a

--

Path=./libsample_arm64.a

Type=Ar

PhysicalSize=78960

SubType=a:BSD

Wouldyouliketoreplacetheexistingfile:

Path:./sample.o

Size:2736bytes(3KiB)

Modified:2017-05-1511:59:49

withthefilefromarchive:

Path:sample.o

Size:76088bytes(75KiB)

Modified:2017-05-1511:58:47

(Y)es/(N)o/(A)lways/(S)kipall/A(u)torenameall/(Q)uitu

EverythingisOk

Files:3

Size:78942

Compressed:78960

只要我们选择Autorenameall,7-zip就会⾃动帮我们处理⽂件重名的问题了。⽽我们重新打包.a⽂件时,.o⽂件的名称并不重要,可以随便

取,所以这⾥改成其他名字也没有关系。

⼈⾁写出机器码

我们再来回顾⼀下典型的全局变量析构调⽤的注册:

LDRX1,[SP,#0x10+var_8]

ADRPX0,#__ZN4TestD1Ev@PAGE;Test::~Test()

ADDX0,X0,#__ZN4TestD1Ev@PAGEOFF;Test::~Test()

ADRPX2,#___dso_handle@PAGE

ADDX2,X2,#___dso_handle@PAGEOFF

BL___cxa_atexit

LDPX29,X30,[SP,#0x10+var_s0]

ADDSP,SP,#0x20

RET

通过阅读ItaniumC++ABI,可以看到

__cxa_atexit

的函数签名如下:

//3.3.6.3RuntimeAPI

extern_LIBCXXABI_FUNC_VISint__cxa_atexit(void(*f)(void*),void*p,void*d);

对⽐反汇编代码,可以看到X0传⼊了对象类型的删除析构(

...D1Ev

)函数的指针,X1传⼊了对象地址,X2传⼊了dso句柄,与函数签名

相符。

要消除对

__cxa_atexit

的调⽤,只需要把其中的

bl

指令改成

nop

即可。

反汇编软件IDA提供了即时汇编的功能,可以通过⼿写汇编指令,由IDA⽣成机器码直接写⼊⽂件中。可惜这个功能对于arm64架构没有⽀

持,我们需要找另外的⽅法。

好在我们可以查阅AArch64指令集架构⽂档,其中提到:

image

通过⽂档,我们看到了在AArch64架构下,

NOP

指令的具体编码。

由于Applearm64CPU是⼩端序,那么我们应该把

bl

指令对应的四个字节替换为:

1F2003D5;NOP

除此之外,还有数种不同的情况,需要针对性地做不同的修改。以下列出了两种不同情况。

尾调⽤:

;各种填写参数...

B___cxa_atexit

;endoffunction

此时要把

B

指令改为

RETlr

返回值校验:

;各种填写参数...

BL___cxa_atexit

CBZW0,check_pass

BLassert_fail

check_pass:

;...正常逻辑

此时要把

B

指令改为

MOVw0,wzr

,才能通过校验。

⾄此我们可以看出,通过这种⽅式修改机器码,存在很⼤的局限性:

做⼈⾁汇编器真的很难;

对象⽂件中的跳转记录在GOT表中,直接删除它们的引⽤会导致链接失败;

不是所有CPU架构上都存在可以等长替换的指令,因此对于部分CISC指令集架构⽆能为⼒。

那么是否存在更好的解决⽅案呢?

苹果⼜有新科技

在使⽤

otool

检查解包出来的.o⽂件时,发现了如下区段:

Section

sectname__bitcode

segname__LLVM

addr0x0ee8

size0x5f70

offset4928

align2fn:0(1)

reloff0

nreloc0

flags0x00000000

reserved10

reserved20

这意味着,这个⽬标⽂件内嵌了bitcode。众所周知,Clang/LLVM是苹果亲⼉⼦,苹果基于这⼀套体系搞出了许多新鲜玩意⼉,bitcode就是

其中之⼀。

clang编译器会先将源⽂件编译为LLVMIR,再把IR编译到机器码。IR的⼤部分设计都是平台中⽴的,少部分平台相关的代码在CPU架构不

发⽣⼤变化时基本兼容,⽽且从IR⽣成机器码的过程可以单独优化。

LLVMIR有不同的表⽰⽅案,有⽂本形式的IR汇编、⼆进制编码的bitcode。

Apple允许应⽤在编译时将bitcode内嵌在⼆进制⽂件内,随应⽤⼀起提交给Apple。⼀旦Apple推出了效率更⾼的机器码⽣成⽅案,或者是

推出了新款CPU,Apple可以根据你提交的bitcode重新⽣成更⾼效的机器指令,开发者⽆需做任何事即可享受到这个优化。

⽐如iPhoneX的CPU架构有⼩升级,内嵌了bitcode的应⽤就可以免费获得arm64eCPU架构的⽀持。

使⽤开源项⽬LibEBC可以提取.o⽂件中的bitcode:

❯/path/to/ebcutil-e./.o

Mach-Oarm64

Filename:.o

Arch:arm64

UUID:00000000-0000-0000-0000

Wrapper:D809E5ED-7D43-4E42-B829-7EFF246EE28C

IR:250BD0A9-67D6-499B-9E63-9D628FB0D7C7

❯mv./250BD0A9-67D6-499B-9E63-9D628FB0D7C7./

使⽤LLVM项⽬(需要通过Homebrew安装llvm)提供的

llvm-dis

⼯具可以将bc⽂件转换为可读的IR汇编格式:

❯llvm-dis./

这会⽣成⼀个同名的.ll⽂件,可以⽤⽂本编辑器打开。其中关于全局变量初始化的部分如下:

;省略⽆关代码

;FunctionAttrs:noinlinesspuwtable

defineinternalvoid@__cxx_global_var_init()#3section"__TEXT,__StaticInit,regular,pure_instructions"{

%1=call%1*@_ZN7Sample1C1Ev(%1*@_ZL2s1)

%2=calli32@__cxa_atexit(void(i8*)*bitcast(%1*(%1*)*@_ZN7Sample1D1Evtovoid(i8*)*),i8*bitcast(%1*@_ZL2s1to

i8*),i8*@__dso_handle)#4

retvoid

}

;FunctionAttrs:nounwind

declarei32@__cxa_atexit(void(i8*)*,i8*,i8*)#4

IR的详细语法在此就不展开介绍了,有兴趣的同学可以查看LLVM官⽅⽂档。其中⽐较重要的有:

declare

⽤来声明对外部符号的引⽤,例如此处引⽤了外部函数

__cxa_atexit

call

⽤来做函数调⽤

需要注意的是,在IR中,所有

%

加数字组成的标号必须连续。例如如果我注释了上述代码中的

%1

所在的⼀⾏,就会产⽣IR汇编错误,此

时就必须把下⼀⾏的

%2

改成

%1

,才能符合规则汇编通过。

在上述代码中,我们只需要把

%2

所在的⼀⾏给注释掉,即可完成修复。如果⼀个IR函数内有多个调⽤,就需要按照标号连续的规则,将注释

掉的代码后⾯的所有标号依次提前了。

正确的IR操作姿势是写⼀个IRpass,然后通过llvm-opt去加载这个pass,读取.bc⽂件⽽不是⼈类可读的.ll⽂件,来对原有的bitcode

做变换。但是写⼀个pass需要的成本⽐临时修复问题要⾼得多,对于少数⼏个⽬标⽂件的修复,可以通过⽂本替换⼯具或脚本语⾔来替换标

号。例如使⽤:

functionreplaceLabels(from,to,diff){

letsource=leSync('','utf8');

for(leti=from;i<=to;++i){

//修改%变量标号

letre=newRegExp('%'+i+'b','g');

source=e(re,'%'+(i-diff));

//修改jumplabel标号

letre2=newRegExp('b'+i+':','g');

source=e(re2,''+(i-diff)+':');

}

ileSync('',source)

}

重新组装静态库

修改过后的.ll⽂件,可以通过以下⽅式重新⽣成机器码:

#⽣成arm64汇编⽂件

❯llc./

#调⽤汇编器重新⽣成⽬标⽂件

❯xcrun-sdkiphoneosas-archarm64./.s-o./.o

这样做有⼀个缺点,就是⽣成的⽬标⽂件没有内嵌bitcode,以后再想改就不好改了。

好在clangdriver功能齐全,可以直接接受bitcode以及IR汇编⽂件:

❯xcrun-sdkiphoneosclang-archarm64-targetarm64-apple-ios6.0.0-fembed-bitcode-c./-o./.o

对存在问题的.o⽂件打补丁后,即可将所有的.o⽂件重新合成静态库:

❯xcrunlibtool-static-o../libsample_arm64_patched.a*.o

实机验证⼤成功

通过调⽤堆栈,我们已经可以知道这个问题的复现⽅式:

在应⽤中进⼊使⽤该三⽅库内部触发多线程⼯作的场景

直接开启多任务⼿势,杀死应⽤

但是在连接调试器的情况下,通过多任务⼿势杀应⽤会导致调试器断开,不容易观察是否有崩溃的现象。

所以,需要找到⼀个让应⽤正常退出,⽽⼜不影响调试器的⽅法。

通过查询iOSsystemframeworkclassdump,可以知道

UIApplication

有⼀个未公开的⽅法:

ateWithSuccess()

经过实际试验,这个⽅法确实可以使应⽤直接退出。

因此,我们可以修改应⽤代码,在进⼊能够触发问题的场景下,通过代码来让应⽤退出,就可以通过调试器来观察应⽤是否触发崩溃了。分别使⽤

修复前、修复后的库进⾏实机验证,结果为:

使⽤旧版库时,有概率引发调试器由于崩溃触发断点;

使⽤修改机器码的库后,不会触发崩溃;

不影响正常的业务功能。

这表明我们的修复是成功的。

总结

本⽂通过修改bitcode,成功地在没有源码的情况下,修复了⼀个三⽅库的bug。其中⽤到的知识点总结如下:

1.崩溃现场中,在主线程发现

exit

,多半是由于C++全局变量析构+多线程导致的;

2.在有源码的情况下,可以通过调整编译参数消除全局变量析构;

3.使⽤7-zip可以⽆损解包静态库⽂件;

4.使⽤otool可以看到⽬标⽂件是否嵌⼊了bitcode;

5.使⽤llvm提供的⼯具,可以对bitcode进⾏修改、重新⽣成机器码;

6.可以通过私有API来模拟应⽤退出,制造复现场景。

作者

郭同学,便利蜂客户端基础框架团队的⼀名iOS⼯程师,负责移动客户端的基础建设。对跨端技术、App框架及系统有所研究,专治各种客户端

疑难杂症。

推荐观看:Fluuter⼿把⼿教你从⼊门到精通

参考资料

搬运⾃知乎,如有侵犯,请联系⼩编删除哦。

更多推荐

uikit