preface
Short step without thousands of miles, not small streams into rivers and seas. Learning is like rowing upstream; not to advance is to drop back. I’m a hauler who drifts from platform to platform. The good ones have already liked it. This paper mainly records the troubleshooting process of a difficult bug in iOS mobile terminal, and introduces the method of fixing bug for the faulty third-party library by patching bitcode to regenerate machine code. Without further ado, I will directly give you dry goods, and I hope it will help you,
The main knowledge points involved are as follows:
- ARM assembly
- C + + runtime
- Structure of static library files
- Bitcode and LLVM IR
\
Platform monitor search crashed
Through the internal crash monitoring, it is found that there is an internal App, there are many crashes recently. The crash thread with the largest number of crashes captures the following call stack:
libsystem_kernel.dylib 0x00000001cc78c414 ___pthread_kill + 7 libsystem_c.dylib 0x00000001a7db2b74 _abort + 103 App 0x0000000103092868 ___48-[BLYLogicManager abortAfterSendingReportIfNeed]_block_invoke + 87 libdispatch.dylib 0x000000019e60824c __dispatch_call_block_and_release + 31 libdispatch.dylib 0x000000019e609db0 __dispatch_client_callout + 19 libdispatch.dylib 0x000000019e61aa68 __dispatch_root_queue_drain + 655 libdispatch.dylib 0x000000019e61b120 __dispatch_worker_thread2 + 115 libsystem_pthread.dylib 0x00000001ea1e77c8 __pthread_wqthread + 215Copy the code
\
Call the scene out
The call stack does not provide any useful information, except that the Bugly framework has detected the crash, created a new Dispatch queue and terminated the process, which means that the valid crash information has been eaten by Bugly.
To look at other threads and see if there is information available, you can generally search for the following on the call stack of other threads:
_ZSt9terminateEv
: C++ terminal exception handling (std::terminate(void)
)__sigtramp
: signal interrupt processing routine entry
Finally, I found the following:
Thread #52: id=1a6c6, name= libsystem_kernel.dylib 0x00000001cc78cf5c ___ulock_wait + 7 libdispatch.dylib 0x000000019e60a528 __dispatch_thread_event_wait_slow + 55 libdispatch.dylib 0x000000019e618708 ___DISPATCH_WAIT_FOR_QUEUE__ + 351 libdispatch.dylib 0x000000019e6182b0 __dispatch_sync_f_slow + 147 App 0x00000001030925f0 -[BLYLogicManager executeEmergencyLogic:] + 695 App 0x000000010308b6a8 -[BLYCrashManager sendLiveCrashReport] + 203 App 0x000000010305f478 _BLYCrashHandlerCallback + 5555 App 0x000000010305bc2c _BLYBSDSignalHandlerCallback + 95 libsystem_platform.dylib 0x00000001ea1e1290 __sigtramp + 55 App 0x00000001029543dc *redacted* App 0x00000001029543dc *redacted* App 0x00000001028a1918 *redacted* App 0x00000001027ea9c4 *redacted* App 0x00000001027ea794 *redacted* App 0x00000001027ead60 *redacted* libsystem_pthread.dylib 0x00000001ea1e5b40 __pthread_start + 319Copy the code
The internal application integrates both Bugly and its own crash capture, and typically after Bugly has captured its own crash scene, it transfers the crash scene to the other framework, making the crash scene the same in both captures. Bugly caught the crash and called Abort to terminate the application, resulting in its own crash catching only SIGABRT.
By examining the main thread call stack, we found some differences:
Thread #0: id=1a0d3, name= libsystem_kernel.dylib 0x00000001cc78c1ac ___psynch_cvwait + 7 libc++.1.dylib 0x00000001b3a25328 __ZNSt3__118condition_variable4waitERNS_11unique_lockINS_5mutexEEE + 27 App 0x000000010280e5c8 *redacted* App 0x00000001027e9414 *redacted* App 0x00000001027e9380 *redacted* libsystem_c.dylib 0x00000001a7d930b8 ___cxa_finalize_ranges + 423 libsystem_c.dylib 0x00000001a7d93400 _exit + 27 UIKitCore 0x00000001a13d4bdc -[UIApplication _terminateWithStatus:] + 503 UIKitCore 0x00000001a0a23648 -[_UISceneLifecycleMultiplexer _evalTransitionToSettings:fromSettings:forceExit:withTransitionStore:] + 127 UIKitCore 0x00000001a0a23278 -[_UISceneLifecycleMultiplexer forceExitWithTransitionContext:scene:] + 219 UIKitCore 0x00000001a13ca644 -[UIApplication workspaceShouldExit:withTransitionContext:] + 211 FrontBoardServices 0x00000001ae6d2780 -[FBSUIApplicationWorkspaceShim workspaceShouldExit:withTransitionContext:] + 87 FrontBoardServices 0x00000001ae701390 ___63-[FBSWorkspaceScenesClient willTerminateWithTransitionContext:]_block_invoke_2 + 79 FrontBoardServices 0x00000001ae6e54a0 -[FBSWorkspace _calloutQueue_executeCalloutFromSource:withBlock:] + 239 FrontBoardServices 0x00000001ae701328 ___63-[FBSWorkspaceScenesClient willTerminateWithTransitionContext:]_block_invoke + 131 libdispatch.dylib 0x000000019e609db0 __dispatch_client_callout + 19 libdispatch.dylib 0x000000019e60d738 __dispatch_block_invoke_direct + 267 FrontBoardServices 0x00000001ae72a250 ___FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 47 FrontBoardServices 0x00000001ae729ee0 -[FBSSerialQueue _targetQueue_performNextIfPossible] + 447 FrontBoardServices 0x00000001ae72a434 -[FBSSerialQueue _performNextFromRunLoopSource] + 31 CoreFoundation 0x000000019e99176c ___CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 27 CoreFoundation 0x000000019e991668 ___CFRunLoopDoSource0 + 207 CoreFoundation 0x000000019e9909cc ___CFRunLoopDoSources0 + 375 CoreFoundation 0x000000019e98aa8c ___CFRunLoopRun + 823 CoreFoundation 0x000000019e98a21c _CFRunLoopRunSpecific + 599 GraphicsServices 0x00000001b648e784 _GSEventRunModal + 163 UIKitCore 0x00000001a13c8fe0 -[UIApplication _run] + 1071 UIKitCore 0x00000001a13ce854 _UIApplicationMain + 167 App 0x00000001037dc93c *redacted* App 0x00000001032625c4 *redacted* App 0x00000001027bc618 main (main.swift:9:13) libdyld.dylib 0x000000019e64a6b0 _start + 3Copy the code
You can see exit in the call stack, indicating that the application is exiting normally.
In the call stack, above the exit entry, you can see __cxa_finalize_ranges, which is the call generated by C++ code through the __cxa_atexit registration callback, called when the application exits and used to do global variable destruction.
As you can see, this is a crash caused by C++ global variables being destroyed when the application exits, causing other threads to reference the destroyed resource. This also explains why Bugly didn’t hand off the crash scene to another framework: Bugly detected an application exiting and called the executeEmergencyLogic: method directly, prioritising its own processing.
\
Global variables are destructed
__cxa_atexit is part of the Itanium C++ ABI runtime specification that is used to support global variables in C++ syntax. As we know, C++ objects are divided into POD and non-pod classes, where POD and constexpr constructors can be initialized at compile time, while non-constexpr constructs can only be constructed at run time by the constructor. For both types of objects, C++ supports them as global variables and provides initial values, which are initialized by calling the constructor during dso loading.
Similarly, to prevent memory/resource leaks, C++ specifies that global variables so initialized should be destructed when dso is unloaded.
We can see how it works by looking at disassembly.
For example, here is the C++ code:
#include <iostream>
class Test {
public:
virtual ~Test();
Test();
};
Test::Test() {}
Test::~Test() {
std::cout << "Test: dtor" << std::endl;
}
static Test t = Test();
Copy the code
Compile the above file using clang++ to see the generated assembly code:
xcrun clang++ -sdk iphoneos -arch arm64 1.cc -s -o 1.s .section __TEXT,__StaticInit,regular,pure_instructions .p2align 2 ; -- Begin function __cxx_global_var_init ___cxx_global_var_init: ; @__cxx_global_var_init .cfi_startproc ; %bb.0: sub sp, sp, #32 ; =32 stp x29, x30, [sp, #16] ; 16-byte Folded Spill add x29, sp, #16 ; =16 .cfi_def_cfa w29, 16 .cfi_offset w30, -8 .cfi_offset w29, -16 adrp x0, __ZL1t@PAGE add x0, x0, __ZL1t@PAGEOFF str x0, [sp, #8] ; 8-byte Folded Spill bl __ZN4TestC1Ev ldr x1, [sp, #8] ; 8-byte Folded Reload adrp x0, __ZN4TestD1Ev@PAGE add x0, x0, __ZN4TestD1Ev@PAGEOFF adrp x2, ___dso_handle@PAGE add x2, x2, ___dso_handle@PAGEOFF bl ___cxa_atexit ; ② LDP X29, X30, [SP, #16]; 16-byte Folded Reload add sp, sp, #32 ; =32 ret .cfi_endproc ; -- End function .section __TEXT,__StaticInit,regular,pure_instructions .p2align 2 ; -- Begin function _GLOBAL__sub_I_1.cc __GLOBAL__sub_I_1.cc: ; @_GLOBAL__sub_I_1.cc .cfi_startproc ; %bb.0: stp x29, x30, [sp, #-16]! ; 16-byte Folded Spill mov x29, sp .cfi_def_cfa w29, 16 .cfi_offset w30, -8 .cfi_offset w29, -16 bl ___cxx_global_var_init ldp x29, x30, [sp], #16 ; 16-byte Folded Reload ret .cfi_endproc ; -- End function .section __DATA,__mod_init_func,mod_init_funcs .p2align 3 .quad __GLOBAL__sub_I_1.cc ; 1.Copy the code
From the disassembly code, it can be seen that the actual scheme is:
- Generates a function to construct all global (and static) variables in the current compilation unit, and writes the function to
__mod_init_funcs
So that when the DSO loads, the dynamic linker actively executes them (in assembly code ①); - In the generated function, call
__cxa_atexit
Pass in Pointers to constructed objects and deleting destructor so that the constructed objects will be destroyed (in assembly code ②) when DSO uninstalls.
Many libraries that use C++ allocate thread resources for concurrent execution. If these executing threads need to reference global variables and trigger A DSO unload, then the thread runs, the global variables are destructed, and the process crashes.
In theory, dSO uninstallation is manageable because we can always control the logic to unload the dynamic library/exit the process after the dynamic library has been freed.
But on iOS mobile apps, there is an exception
Users can kill apps with multi-tasking gestures. If the killed app happens to be running in the foreground, iOS sends a SIGTERM signal to the app. UIKit calls the applicationWillTerminate(_:) method of the application proxy upon receiving the signal, giving the application a chance to save some state data and exit the application as normal.
There is no chance of releasing thread resources at this point, because the terminate life is short and there is no time to wait for the asynchronous thread to terminate, so the crash is inevitable.
Fortunately, this crash is not perceived by the user: even if the application does not crash, it will exit immediately and normally, and it will behave the same to the user.
\
Need to recompile?
In fact, the same problem has happened in this App before — we have an internal SDK also written in C++, with global state variables, enabling asynchronous thread pool access to these variables, and a crash when the user kills the App in the foreground.
The solution: upgrade the toolchain. According to the Xcode 11 update log published by Apple, the Apple clang++ compiler added the compile parameter -fno-c++-static-destructors to disable global variable destructors. C++ source files compiled with this tag do not generate code to destruct global variables.
This is safe for iOS apps — iOS apps almost never uninstall dynamic libraries at runtime without worrying about resource leaks from dynamic library uninstalls.
However, this time the problem is different – the problem is a binary library provided by a third party. We do not have its source code, so we cannot recompile the machine code by changing the compilation parameters.
But can we dig a little deeper and help the tripartite library fix this bug?
The immediate solution to fix this problem is to modify the machine code to eliminate calls to __cxA_atexit.
\
What’s in the static library
A tripartite static library SDK, typically consisting of the following files:
- A set of header files that provide public function /OC classes and method declarations;
- A static library containing the code implementation of the library, packaged with.o object files generated by multiple compilation units;
- A set of resource files that provide external data (images, and other resources) while the code is running.
The composition is basically the same, whether you use a fragmented file or a.framework wrapper.
What we need to modify is part of its machine code, so we need to unpack the.A static library and then edit it.
Let’s first look at the contents of the.a file:
❯ lipo-info libsample. A Architectures in the fat file: libsample. A are: armv7 arm64Copy the code
This is a Universal binary that contains the code for both CPU architectures of iOS real machines. We first try to adjust the ARM64 architecture used by mainstream models.
Use the lipo command to extract the ARM64 architecture separately:
❯ lipo-thin arm64 libsample.a -o libsample_arm64.aCopy the code
The ar(1) operation can only be used if the specific schema in the Universal binary is extracted:
❯ mkdir objects ❯ CD objects # list of files contained in ❯ ar t.. /libsample_arm64.a __.symdef sample.o sample.o sample.o # unpack. A file ❯ ar-x.. /libsample_arm64.aCopy the code
After using the above command to expand the. A file, a problem appeared: in the ar t command, two sample.o files were listed, but only one was solved in the ar x command. This is because there is no concept of directories in an AR archive. Files with the same name in different directories are leveled during ar archiving. As a result, the AR archive contains multiple files with the same name.
As a result, when we unpack using AR X, the same files will be overwritten into one, and we can’t unpack them separately.
How to unpack the files with the same name in the AR archive separately… Which brings us to the “no-nonsense” 7-Zip…
\
Compression software is useful
As a compression software, 7-Zip supports many archive files and PE executables (in particular, some installers’ SFX modules) in addition to the normal compressed file formats. Let’s try to see if it supports.a archiving:
❯ 7z L. /libsample_arm64.a 7-zip [64] 17.04: Copyright (c) 1999-2021 Igor Pavlov: 2017-08-28 P7ZIP Version 17.04 (locale= UTF8,Utf16= ON,HugeFiles= ON,64 bits,16 CPUs x64) Scanning the drive for archives: 1 file, 78960 bytes (78 KiB) Listing archive: ./libsample_arm64.a -- Path = ./libsample_arm64.a Type = Ar Physical Size = 78960 SubType = a:BSD Date Time Attr Size Compressed Name ------------------- ----- ------------ ------------ ------------------------ 2021-06-23 15:05:09 ..... 1710 1710 1.txt 2021-06-23 15:03:54 ..... 38616 38616 1.sample.o 2021-06-23 15:04:02 ..... 38616 38616 2.sample.o ------------------- ----- ------------ ------------ ------------------------ 2021-06-23 15:05:09 78942 78942 3 filesCopy the code
As you can see, 7-zip automatically corrects the file names in.a. In addition, if 7-zip encounters a file with the same name during decompression, it will provide the option of overwriting and automatically renaming the file:
❯ 7z x./libsample_arm64.a 7-zip [64] 17.04: Copyright (c) 1999-2021 Igor Pavlov: 2017-08-28 P7ZIP Version 17.04 (locale= UTF8,Utf16= ON,HugeFiles= ON,64 bits,16 CPUs x64) Scanning the drive for archives: 1 file, 78960 bytes (78 KiB) Extracting archive: ./libsample_arm64.a -- Path = ./libsample_arm64.a Type = Ar Physical Size = 78960 SubType = a:BSD Would you like to replace the existing file: Path: ./sample.o Size: 2736 bytes (3 KiB) Modified: 2017-05-15 11:59:49 with the file from archive: Path: sample.o Size: 76088 bytes (75 KiB) Modified: The 2017-05-15 11:58:47? (Y)es / (N)o / (A)lways / (S)kip all / A(u)to rename all / (Q)uit? u Everything is Ok Files: 3 Size: 78942 Compressed: 78960Copy the code
As soon as we select Auto Rename All, 7-Zip will automatically handle file name duplication for us. When we repackage the.a file, the name of the.o file is not important and can be arbitrarily chosen, so it doesn’t matter if we change it to another name.
\
Human flesh writes machine code
Let’s revisit the registration of a typical global destructor call:
LDR X1, [SP,#0x10+var_8]
ADRP X0, #__ZN4TestD1Ev@PAGE ; Test::~Test()
ADD X0, X0, #__ZN4TestD1Ev@PAGEOFF ; Test::~Test()
ADRP X2, #___dso_handle@PAGE
ADD X2, X2, #___dso_handle@PAGEOFF
BL ___cxa_atexit
LDP X29, X30, [SP,#0x10+var_s0]
ADD SP, SP, #0x20
RET
Copy the code
By reading the Itanium C++ ABI, you can see that __cxa_atexit has the following function signature:
// 3.3.6.3 Runtime API
extern _LIBCXXABI_FUNC_VIS int __cxa_atexit(void (*f)(void *), void *p, void *d);
Copy the code
In contrast to disassembly code, you can see that X0 passes in a delete destructor for object types (… D1Ev), X1 passes in the object address, X2 passes in the Dso handle, matching the function signature.
To eliminate the call to __cxA_atexit, simply change the bl instruction to NOP.
The disassembly software IDA provides the function of instant assembly, which can be written directly into a file by writing machine code generated by IDA through hand-written assembly instructions. Unfortunately this feature is not supported for arm64 architecture, we need to find another way.
Fortunately, we can refer to the AArch64 instruction set architecture documentation, which states:
Through the document, we see the specific encoding of NOP instructions in AArch64 architecture.
Since the Apple ARM64 CPU is small encoder, we should replace the four bytes corresponding to the BL instruction with:
1F 20 03 D5 ; NOP
Copy the code
In addition, there are several different situations that require specific modifications. Two different situations are listed below.
The tail call:
; Various filling parameters... B ___cxa_atexit ; end of functionCopy the code
At this point, change instruction B to RET LR.
Return value verification:
; Various filling parameters... BL ___cxa_atexit CBZ W0, check_pass BL assert_fail check_pass: ; . Normal logicCopy the code
At this time, B instruction should be changed to MOV W0, WZR to pass the verification.
So far we can see the limitations of modifying machine code in this way:
- A human flesh assembler is really hard;
- In the global offset table, delete the reference to the jump records in the object file, causing the link to fail.
- Not all CPU architectures have instructions that can be replaced with equal length, so there is nothing you can do about some CISC instruction set architectures.
Is there a better solution?
\
Apple has new technology
Using otool to check the unpacked.o file, the following sections are found:
Section
sectname __bitcode
segname __LLVM
addr 0x0000000000000ee8
size 0x0000000000005f70
offset 4928
align 2fn:0 (1)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
Copy the code
This means that the object file has bitcode embedded in it. Clang/LLVM is known to be Apple’s son, and Bitcode is one of the many new things Apple has done with this system.
The CLang compiler first compiles the source file to LLVM IR, and then compiles the IR to machine code. Most of the IR design is platform neutral, a small percentage of platform-specific code is generally compatible without major changes to the CPU architecture, and the process of generating machine code from IR can be optimized separately.
LLVM IR has different representation schemes, including IR assembly in text form and bitcode encoded in binary.
Apple allows an app to build bitcode into a binary file that is submitted with the app to Apple. Once Apple comes out with a more efficient machine code generation scheme, or a new CPU, Apple can re-generate more efficient machine instructions based on the bitcode you submit, and developers don’t have to do anything to take advantage of this optimization.
For example, the CPU architecture of the iPhone X has been slightly upgraded, and apps with embedded Bitcode can get the ARM64E CPU architecture for free.
Using the open source project LibEBC you can extract bitcode from.o files:
❯ /path/to/ ebcutil-e./1.sample.o Mach -o arm64 File name: 1.sample.o Arch: arm64 UUID: 00000000-0000-0000-0000-000000000000 Wrapper: D809E5ED-7D43-4E42-B829-7EFF246EE28C IR: 250bd0a9-67d6-499b-9e63-9d628fb0d7c7 ❯ mv. / 250b0a9-67d6-499b-9e63-9d628fb0d7c7./1.sample.bcCopy the code
BC files can be converted into readable IR assembly format using the llVM-dis tool provided by the LLVM project (LLVM is required to be installed through Homebrew) :
❯ LLVM - dis. / 1. Sample. BCCopy the code
This generates a.ll file with the same name that can be opened with a text editor. The section about global variable initialization is as follows:
; Omit extraneous code; Function Attrs: noinline ssp uwtable define internal void @__cxx_global_var_init() #3 section "__TEXT,__StaticInit,regular,pure_instructions" { %1 = call %class.Sample1* @_ZN7Sample1C1Ev(%class.Sample1* @_ZL2s1) %2 = call i32 @__cxa_atexit(void (i8*)* bitcast (%class.Sample1* (%class.Sample1*)* @_ZN7Sample1D1Ev to void (i8*)*), i8* bitcast (%class.Sample1* @_ZL2s1 to i8*), i8* @__dso_handle) #4 ret void } ; Function Attrs: nounwind declare i32 @__cxa_atexit(void (i8*)*, i8*, i8*) #4Copy the code
The syntax of IR is not explained here. Interested students can refer to the official documentation of LLVM. Some of the most important ones are:
declare
Used to declare a reference to an external symbol, such as a reference to an external function__cxa_atexit
。call
It’s used to make function calls
Note that in IR, all % plus digit labels must be consecutive. For example, if I comment a line of %1 in the above code, an IR assembly error will occur and I will have to change the next line %2 to %1 in order for the regular assembly to pass.
In the above code, all we need to do is comment out the line where %2 is in order to fix it. If there are multiple calls to an IR function, all labels following the commented code need to be brought forward in order, following the rule of label continuity.
The correct IR gesture is to write an IR pass and then load the pass through llvm-opt, reading.bc files instead of human-readable.ll files, to transform the original bitcode. But writing a pass can be much more expensive than temporary fixes, and for a few object file fixes, labels can be replaced with text replacement tools or scripting languages. For example, using Node.js:
function replaceLabels(from, to, diff) { let source = fs.readFileSync('tmp.ll', 'utf8'); for (let i = from; i <= to; + + I) {/ / modify % variable label let re = new RegExp (' % '+ I +' \ b ', 'g'); source = source.replace(re, '%'+(i - diff)); Let re2 = new RegExp('\b'+ I +':', 'g'); source = source.replace(re2, ''+(i - diff)+':'); } fs.writeFileSync('tmp.ll', source) }Copy the code
\
Reassemble the static library
After the modification of the.ll file, you can use the following methods to regenerate machine code:
# create arm64 assembler ❯ llC. /1.sample.ll # create arm64 assembler ❯ xcrun - SDK iphoneOS as-arch arm64. /1.sample.s -o. /1.sample.oCopy the code
A disadvantage of this is that the generated object file does not have embedded Bitcode, which is difficult to change later.
The Clang Driver is fully functional and can accept bitcode and IR assembler files directly:
❯ xcrun - SDK iphoneOS clang -arch arm64- Target arm64-apple-ios6.0.0 -fembed-bitcode -c./1.sample.ll -o./1.sample.oCopy the code
After patching the problematic.o files, you can recompose all.o files into a static library:
❯ xcrun libtool -static -o ../libsample_arm64_patched.a *.o
Copy the code
\
The real machine verification was successful
From the call stack, we can already see how this problem can reproduce:
- Enter the scenario of using the tripartite library to trigger multithreading inside the application
- Kill the app by simply turning on the multi-task gesture
However, in the case of connecting to the debugger, killing the application with a multi-task gesture will cause the debugger to disconnect, and it is not easy to see if there is a crash.
So, you need to find a way to exit the application without affecting the debugger.
By querying the iOS system framework class dump, you can know there is an undisclosed UIApplication method: UIApplication. TerminateWithSuccess ().
After practical tests, this method can indeed make the application directly exit.
Therefore, we can modify the application code to exit the application in a situation where it can trigger a problem, and we can use the debugger to see if the application triggers a crash. Real machine verification was carried out using the pre-repair and post-repair libraries respectively, and the results were as follows:
- When using older libraries, it is possible to cause the debugger to trigger breakpoints due to crashes.
- [Fixed] Using a library that changes machine code does not trigger a crash
- Normal service functions are not affected.
This shows that our restoration was successful.
conclusion
This article successfully fixed a tripartite library bug without source code by modifying bitcode. The knowledge points used are summarized as follows:
- Crash scene, found in the main thread
exit
, mostly due to C++ global variable destructor + multithreading; - In the case of source code, global variable destructor can be eliminated by adjusting compilation parameters;
- Static library files can be unpacked losslessly using 7-zip.
- Using otool, you can see if the target file is embedded with bitcode.
- Bitcode can be modified and machine code regenerated using tools provided by LLVM.
- A private API can be used to simulate an application exit and create a replay scenario.
The author
Guo, an iOS engineer in the client infrastructure framework team of Convenience Bee, is responsible for the infrastructure construction of mobile clients. Research on cross-terminal technology, App framework and system, specializing in the treatment of various difficult and complicated client.
Recommend to watch:Fluuter hand in hand teach you from entry to mastery
The resources
[1] the Xcode update log: 11 developer.apple.com/documentati…
C + + ABI: [2] Itanium Itanium – CXX – ABI. Making. IO/CXX – ABI/ABI…
[3] AArch64 instruction set architecture documentation: developer.arm.com/architectur…
[4] LibEBC: github.com/Guardsquare…
[5]LLVM Official documentation: llvm.org/docs/LangRe…
[6]iOS System Framework Class Dump: developer.limneos.net/?ios=14.4&f…
Handling zizhihu, if there is violation, please contact xiaobian to delete oh.