📜 ⬆️ ⬇️

Under the hood: a patch for Dalvik from Facebook for Android

Facebook is one of the most functional apps available on Android. With features such as push notifications, news feeds and a built-in version of Facebook Messenger (in fact, a full-fledged application) that work simultaneously in real time, the complexity and amount of code generates a number of technical difficulties encountered by other Android developers. - especially on older versions of the platform. (Our latest applications support the old version of Android 2.2 - Froyo, which is almost 3 years old).

One such problem is how the Android virtual machine, Dalvik, handles Java methods. At the end of last year, we completed the processing of our Android application , which included the translation of a large amount of JavaScript code in Java, as well as the use of new abstractions that generated a large number of small methods (in most cases, this is considered a good programming practice). Unfortunately, this led to a sharp increase in the number of methods in our application.


As we found out, the problem first manifested itself, as described in this bug, which made it impossible to install our application on older versions of Android. During standard installation, the dexopt program is launched to prepare the application for installation on a specific phone. dexopt uses a fixed-size buffer (the “LinearAlloc” buffer) to store information about all methods of the application. The latest versions of Android use a buffer of 8 or 16 MB in size, but Froyo and Gingerbread (versions 2.2 and 2.3, respectively) have a limit of only 5 MB. Therefore, a large number of our methods led to the excess of its size and dexopt crash on older versions of Android.
')
After a little panic, we realized that we could try to avoid this problem by breaking our application into several dex files using the technique described here , which is to use additional dex files for expansion modules, but not the main part of the program.

However, this was not the way in which we could break our application — too many classes refer directly to the Android framework. Instead, it was necessary to inject our additional dex files directly into the system class loader. This cannot be done in a standard way, but we examined the Android source code and used reflection to directly modify some of its internal structures. Of course, we are very pleased and grateful that Android is an open source project, otherwise these changes would have been impossible.

But as soon as we came closer to the launch of our updated application, we faced a new problem. LinearAlloc buffer is used not only in dexopt - it exists in every running Android program. While dexopt uses LinearAlloc to store information about all the methods of your dex file, the running application only needs it for the class methods that are currently being used. Unfortunately, now we use too many methods for Android versions down to Gingerbread, and our application began to fall almost immediately after the start.

There was no way around this problem with dex files, since all our classes were loaded in one process and we could not find information about someone who had encountered this problem before (for it is possible if you use multiple dex files, which in itself is a complicated technique). We were left to ourselves.

We tried various ways to get memory, including aggressive use of ProGuard and reworking the source code in order to reduce the number of methods. We even developed a profiler using LinearAlloc to find its largest consumers. Nothing helped, and we still needed to write more methods to support different types of content in our improved news feed and timeline.

The release of the long-awaited version 2.0 of Facebook for Android is under threat. It seemed we had to choose: either significantly reduce the functionality of the application or deliver our application only for the latest versions of Android (Ice Cream Sandwich and above). Neither was acceptable. We needed the best solution.

And so, we again turned to Android source code. Looking at the declaration of the LinearAlloc buffer , we realized that if it were possible to increase the buffer size from 5 to 8 MB, we would be saved!

That's when we got the idea to use the JNI extension to replace the existing buffer with a larger buffer. At first glance, this idea seems completely insane. Modifying the insides of the Java class loader is one thing, but changing the Dalvik virtual machine while it is executing our code can be very dangerous.
But as soon as we mastered the code, analyzing the use of LinearAlloc, we began to understand that it should be safe if done at the very beginning of the program. All we had to do was find the LinearAllocHdr object, block it and replace the buffer.

The search turned out to be the most difficult part. This is where the object is located , inside the DvmGlobals object, about 700 bytes from the beginning. Finding an entire object can be risky, but fortunately, we had a pivot point - a vmList object just a few bytes earlier. It contains a value that we could compare to a JavaVM pointer accessible via JNI.

The plan finally came together: find the right value from vmList, find a match in DvmGlobals, jump a few bytes back to the LinearAlloc header and replace the buffer. Thus, we developed the JNI extension, embedded it in our application and ... saw how our application runs on a Gingerbread phone for the first time in weeks. The plan worked.

But for some reason it did not launch on the Samsung Galaxy S II ...
The most popular gingerbread phone ...
All the times ...

It seems that Samsung made a small change in Android, which misled our code. Other manufacturers could do the same, so we decided to make our code more reliable.

Manual verification of the GSII code showed that the LinearAlloc buffer was only 4 bytes from where we were waiting for it, so we rewrote our code so that we could look at a few bytes in each direction if LinearAlloc could not be found in its intended location. This made it necessary to parse the memory table of our process to make sure that we do not make any references to non-valid memory (which can lead to instant crash), as well as to build a strong heuristic algorithm to be sure that we will recognize LinearAlloc when we find it . Finally, we found the safest way to scan the entire hip process to find the buffer.

Now we had a version of the code that worked on several popular phones — but we needed more than just a few. Therefore, we have embedded our code in a test application that could run the same procedure that we use in our Facebook application, and simply show a green or red block in case of success or failure.

We used manual testing, DeviceAnywhere and a test lab provided by Google to test our application on 70 different devices and fortunately it worked on each one of them!

We released this code along with Facebook for Android 2.0 in December. Now, it works on hundreds of different phone models. A big speed boost in this release would have been impossible without this crazy hack. And, of course, without the source code of Android, we would not have had the opportunity to release our best version of the application. Android provides extensive development capabilities and we are excited to bring Facebook to an increasing number of devices.

Source: https://habr.com/ru/post/171623/


All Articles