background
When we debug React Native or Weex programs, we can implement dynamic code injection and hot update debugging with the help of dynamic execution capability of JavaScript, thus greatly improving the efficiency of UI and logic debugging. On the contrary, in Native code programming, it is generally necessary to constantly restart the App to debug the new code, which undoubtedly reduces the development efficiency for some projects with complicated compilation and linking scripts. At this time, dynamic library loading and logic replacement at runtime can be realized by using dlopen’s idea of opening dynamic library and section programming. To achieve dynamic code injection. It should be noted that this method is explicitly forbidden in the App released to the App Store, and the real machine cannot open a dynamic library that is not signed with the App through Dlopen, so this method can only be used for simulator debugging.
Based on the above principles, the author has implemented a debugging framework for the hot deployment of Native code named Dyamk. This paper will introduce its principle and usage.
The effect
The following GIF demonstrates a simple code injection.
The source code
Github.com/Soulghost/D…
The principle of
An overview of the
The diagram above shows the architecture and workflow flow chart of Dyamk. Dyamk mainly consists of two parts: one is DyamkInjector, which is used to create and distribute dynamic libraries, and the other is DyamkClient, which runs in the host Main App.
DyamkInjector is an iOS dynamic library project. When the dynamic library is compiled, a series of scripts will be run to sign the dynamic library, move it to the shared directory, and notify DyamkClient through Socket that there is a new dynamic library to load.
After receiving the Socket message, DyamkClient in the host Main App will load the newly generated dynamic library from the shared directory. Since Dyamk has agreed on the execution mode of dynamic library in section, the dynamic library will execute according to the agreed interface after loading, so as to dynamically modify the existing logic. Implement dynamic Native code debugging.
Injector part
The injectors consist of two targets, one is DyamkInjector, Xcode’s dynamic library project, which builds and builds dynamic libraries, and the other is the former’s Aggregate object BuildMe, which implements movement and notification after the dynamic library signature. The reason why an Aggregate object is used here is to ensure that the dynamic library signature is completed before the subsequent script is executed.
In the DyamkInjector project, a pre-compile script, Do Symbol replace, is used to implement dynamic symbol replacement by replacing the class name of the dynamic library source code with the Objective C runtime dynamic library loading restrictions. In the Objective – C using dlopen opens the dynamic library, cannot be shut off by dlclose, also cannot be achieved by dlopen covering the same name, the relevant content can be reference stackoverflow.com/questions/8… . Therefore, every time the dynamic library is generated, the dynamic library name and the class name in the dynamic library are dynamically replaced by providing a counting suffix, such as SomeClass_1 and SomeClass_2.
To ensure that the dynamic library and its symbols generated by the injector are consistent with the relevant content read by DyamkClient in the host App, a shared file is required to record the name of the current dynamic library and the symbol name. This file is named framework_version and stores the current symbol suffix value digitally. This file is saved in the same directory as the dynamic library to be shared by the injector and the Client in the host. In Dyamk, /opt/Dyamk/dylib is used as the shared folder to take advantage of the iOS emulator’s ability to read the MacOS file system.
From the above description, it becomes clear that the Do Symbol replace script needs to read the framework_version file under the shared file and complete the symbol replacement of the dynamic library.
#! /bin/sh
# path to concatenate framework_version
cd /opt/Dyamk/dylib
path=`pwd`'/'
number_name='framework_version'
number=$path$number_name
v=0
Check whether the file exists
if [ -e $number ]; then
If it exists, read it directly
v=`cat $number_name`
else
If no, proceed as 0
echo 0 > $number_name
fi
# Replace symbols in dynamic library source code dynamically with regular expressions
sed -i -e 's/DyamkNativeInjector_[0-9]*/DyamkNativeInjector_'$v'/g' ${SRCROOT}'/DyamkInjector/core/DyamkNativeInjector.m'
Copy the code
There are four scripts included in the Aggregate object BuildMe that are executed after the dynamic library has compiled, linked, and signed.
-
Delete old dylib
This script is used to delete the dynamic libraries that have been generated in the shared directory to ensure that the newly generated ones can replace them correctly.
-
Copy dylib
The script uses Xcode’s Copy File Phase feature to Copy the newly generated dynamic library to a shared directory.
-
Process with dylib
This script is used to replace the name of the dynamic library, and is consistent with the symbol change logic in the DyamkInjector object. After the dynamic library name is changed, the framework_version is increased by one to ensure that the new name and symbol can be used next time.
#! /bin/sh cd /opt/Dyamk/dylib path=`pwd`'/' number_name='framework_version' number=$path$number_name v=0 if [ -e $number ]; then v=`cat $number_name` else echo 0 > $number_name fi Get and replace the dynamic library name from="DyamkInjector.framework/DyamkInjector" to="DyamkInjector.framework/DyamkInjector_"$v mv $from $to # increase the dynamic library symbol count in the framework_version file v="$(($v+1))" echo $v > $number_name Copy the code
-
Trig Update
This script is used to notify DyamkClient in the host that there is a new dynamic library to load and the notification channel is Socket.
# -*- coding: utf-8 -*- import socket import sys def conn(a): args = sys.argv ip = args[1] port = int(args[2]) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((ip, port)) The notification message contains the current dynamic library version number f = open('/opt/Dyamk/dylib/framework_version'.'r') number = int(f.readlines()[0]) if number > 0: number -= 1 msg = "{}".format(number) s.send(msg.encode()) s.close() if __name__ == '__main__': conn() Copy the code
It can be known from the above content that DyammInjector has completed the generation and processing of the dynamic library and the notification of the Client in the host App. This is also the most complex part of Dyamk. The Client part only needs to listen to Socket messages and complete the loading of the dynamic library, so the logic will become relatively simple.
The Client part
Client implements dynamic library loading by adding a non-intrusive DyamkClient framework, which has been packaged as a CocoaPods library for easy use.
CocoaAsyncSocket is used here to implement this function. The Socket monitoring code is not described here. The code related to dynamic library loading is mainly introduced here.
// This method is called after the Socket receives the message and stores the current dynamic library version number in the '_currentDylibNo' member variable before calling it
- (void)performDylib {
// The dylib root in the shared directory
NSString *libPath = @"/opt/Dyamk/dylib/DyamkInjector.framework";
// Concatenate the dynamic library binary path in the shared directory
libPath = [libPath stringByAppendingPathComponent:[NSString stringWithFormat:@"DyamkInjector_%@"@ (self.currentDylibNo)]];
// Open the dynamic library
void *handle = dlopen(libPath.UTF8String, RTLD_NOW);
if(! handle) {NSLog(@"Error: cannot find <%@>", libPath);
return;
}
// Concatenate dynamic library symbols
NSString *className = [NSString stringWithFormat:@"DyamkNativeInjector_%@"@ (self.currentDylibNo)];
// Class loading and section method execution
Class class = NSClassFromString(className);
if (class= =nil) {
NSLog(@"Error: cannot find class %@", className);
dlclose(handle);
return;
}
[class performSelector:@selector(run)];
// Close the dynamic library. Due to Objective-C runtime limitations, this sentence does not actually unload the dynamic library
dlclose(handle);
}
Copy the code
Whenever the DyamkInjector project’s Target BuildMe is compiled, the Client is notified through the Socket, the dynamic library is read and loaded, and the aspect method is executed to complete the dynamic code injection.
Section programming section
In DyamkInjector’s project, there is a DyamkCodePlayground. M file, in which the __dyamk_debug_code_goes_here function is the starting point of dynamic library running, and all the codes that need dynamic injection need to be written here. Since all of the code exists in the form of facets, runtime method addition is required when handling event binding, as follows.
Handle dynamic event binding
-
Create a function whose first two arguments are of type id and SEL, determined by Objective-C’s message forwarding mechanism. The first argument id is the message receiver, and the second argument SEL is the method selector. It takes an argument n to add up the counter V within the class.
void __SomeClass__add(id self, SEL _cmd, int n) { self.v += n; } Copy the code
-
Methods are added or replaced by class_replaceMethod. Replace is used instead of add because you need to overwrite previously added methods when loading multiple times.
class_replaceMethod(NSStringFromClass(@"SomeClass"), @selector(add:), (IMP)__SomeClass__add, "v@:i"); Copy the code
Note the last parameter here, which is the Type Encoding of the method, available at nshipster.com/type-encodi… Learn more.
-
Once you’ve done that, you can dynamically add event handlers to an instance as a slice, and then bind them to a particular event as a selector. Because you can’t check for dynamically bound selectors at compile time, you get a warning, So the __dyamk_debug_code_goes_here function uses precompiled instructions to eliminate this warning.
#pragma clang diagnostic push #pragma clang diagnostic ignored "-Wundeclared-selector" void __dyamk_debug_code_goes_here() { // code goes here } #pragma clang diagnostic pop Copy the code
Simplify operations by using macro functions
The above event binding process is very inconvenient to use, and tedious and lengthy prefixes need to be added in order to avoid symbol conflicts. To solve this problem, the author encapsulates a series of macro functions to solve this problem. For example, the function definition can be simplified through macro functions.
// The original implementation
void __SomeClass__add(id self, SEL _cmd, int n) {
self.v += n;
}
// through macro functions
Dyamk_Method_1(void, add, int, n) {
self.v += n;
}
Copy the code
The macro function abstracts the common parts of each function used for objective-C message receipt. The developer simply fills in the return value type, function name, and argument list. The argument list is type, name, type, name… The N in Dyamk_Method_N represents the number of arguments to the defined function excluding the first two public arguments.
Similarly, dynamic method addition is simplified by using macro functions.
// The original implementation
class_replaceMethod(NSStringFromClass(@"SomeClass"), @selector(add:), (IMP)__SomeClass__add, "v@:i");
// through macro functions
Dyamk_AddMethod(SomeClass, @selector(add:), add, v@:i);
Copy the code
Using the tutorial
For documentation, see the Dyamk Wiki on GitHub, which is still a work in progress.
Deficiencies and Prospects
The author tried to transfer dylib use a network to iOS true machine during real machine sandbox dynamic debugging, but the true machine dlopen function always fail, if the same dynamic libraries with static packaging App can to load, so the author speculation about signature mechanism, this mechanism causes the framework can only be used on the simulator for the time being.
For jailbreak development, every time dylib is modified, deb must be packaged and re-installed, as well as App restart. For some large apps, such as Springboard. App, it will take a lot of time. If Dyamk can be used for dynamic debugging of jailbreak device plug-ins, development efficiency will be greatly improved.