Native Libraries for Android

In this article, we'll look at working with native libraries that Android apps may contain. Native libraries are code that a developer has written and then compiled for a specific computer architecture. Most often, this code is written in C or C++. The most common reasons a developer might do this are for mathematically complex or time-consuming operations, such as working with graphics libraries.

It is worth noting that malware developers have also begun to move to machine code, as reverse engineering of compiled binaries is generally less common than analyzing DEX bytecode. This is largely due to the fact that DEX bytecode can be decompiled into Java, while native, compiled code often has to be analyzed as assembly language.

In this article, we will talk about using native libraries for ARM and x86, so that each user can choose the architecture that is more convenient for him to work with.

Introduction to Java Native Interface

JNI (Java Native Interface) is a programming interface in the Java programming language that allows interaction with applications and libraries written in other programming languages, such as C, C++, or assembly. This interface allows developers to declare Java methods that are implemented in machine code (usually also compiled in C/C++). The JNI interface is not specific to Android, but is generally available to Java applications running on different platforms.

The Android Native Development Kit (NDK) is a set of tools for Android designed specifically for JNI. This set of tools allows developers to write C and C++ code for their Android applications.

Together, JNI and NDK allow Android developers to implement some of their app's functionality in native code. Java (or Kotlin) code will call a native method declared in Java, which is implemented in a compiled native library.

The most important advantage of JNI is that it does not impose restrictions on the implementation of the underlying Java Virtual Machine. Therefore, Java VM providers can add support for JNI without affecting other parts of the virtual machine. Programmers can write one version of their own application or library and expect it to work with all Java Virtual Machines that support JNI.

Analyzing Android Native Libraries

Next, we will focus on how to reverse engineer the functionality of an app that was implemented in Android native libraries. First of all, let's understand what we mean when we say “Android native libraries”?

If you look at the structure of an Android app, the native Android libraries are included in APK files as .so, shared object libraries, in the ELF file format. If you've analyzed Linux binaries before, this is the same format.

These libraries are included by default in the APK file at /lib//lib.so. This is the default path, but developers can also include the built-in library at /assets/ if they wish.

Because machine code is compiled for specific processors, if a developer wants their app to run on more than one type of hardware, they must include each of these compiled native library versions in the app. The default path above includes a directory for each processor type officially supported by Android.

Below are the default directory paths for different processors:

CPU       Native Library Path

“generic” 32-bit ARM     lib/armeabi/libcalc.so

x86        lib/x86/libcalc.so

x64        lib/x86_64/libcalc.so

ARMv7  lib/armeabi-v7a/libcalc.so

ARM64  lib/arm64-v8a/libcalc.so

We execute the code

Before an Android application can call and execute any code implemented in a native library, the application (Java code) must load the library into memory. There are two different API calls for this:

System.loadLibrary("calc")

or

System.load("lib/armeabi/libcalc.so")

The difference between the two API calls presented is that LoadLibrary only takes the short name of the library as an argument (i.e. libcalc.so = “calc” & libinit.so = “init”) and the system will correctly detect the architecture it is currently running on and therefore the correct file to use. On the other hand, LoadLibrary requires the full path to the library. This means that the application developer must determine the architecture and therefore the correct library file to load.

When Java code calls one of these two APIs (LoadLibrary or load), the native library passed as an argument executes its JNI_OnLoad if it has implemented it.

Recall that before executing any native methods, you must load your native library by calling System.LoadLibrary or System.load in your Java code. When either of these two APIs is executed, the JNI_OnLoad function in the native library is also executed.

Connecting Java and Native Code

Now let's look at the interactions between Java and native libraries. To execute a function from a native library, there must be a method declared in Java that can be called in Java code. When this method is called, the “paired” function from the native library (ELF/.so) is executed.

In the code, a Java method called Native appears declared as shown below. It looks like any other Java method except that it includes the native keyword and does not contain any code in its implementation, since its code is actually in the compiled native library.

public native String doThingsInNativeLibrary(int var0);

In theory, to call this native method, Java code would call it just like any other Java method. But on the server side, the JNI and NDK need to implement the corresponding function in the native library. To do this, it needs to know the mapping between the native method declared in Java and the function in the native library.

There are 2 different ways to perform this pairing or linking:

  • Dynamic binding using JNI native method name resolution

  • Static linking using registerNatives API call

Let's consider these methods in more detail.

Dynamic linking

To dynamically bind a Java-declared native method and function in a native library, the developer names the method and function according to the specifications so that the JNI system can perform the binding dynamically.

According to the specification, the developer must name the function as follows so that the system can dynamically bind the native method and the function. The name of the native method is composed of the following components:

  • prefix Java_

  • full class name

  • underscore separator (“_”)

  • method name.

  • for overridden native methods, two underscores (“__”) are used, followed by the argument signature

To perform dynamic linking for the following native method declared in Java and assume it is in the class com.android.interesting.Stuff

 public native String doThingsInNativeLibrary(int var0);

A function in a native library can be called as follows

Java_com_android_interesting_Stuff_doThingsInNativeLibrary

If there is no function with that name in the native library, it means that the application must perform static linking, which is discussed next.

Static linking

If a developer does not want or cannot name native functions according to the specification (for example, wants to remove debug symbols), then they must use static linking with the registerNatives API to perform the mapping between a Java-declared native method and a function in a native library. The registerNatives function is called from native code, not Java code, and is most often called in the JNI_OnLoad function, since registerNatives must be executed before a Java-declared native method is called.

 jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);

 

typedef struct { 

    char *name; 

    char *signature; 

    void *fnPtr; 

} JNINativeMethod;

Thus, static and dynamic binding are used in Java to determine which method will be called during program execution. Static binding occurs at the time of code compilation based on the type of variable or reference, and dynamic binding occurs at the time of program execution.

Conclusion

The methods presented in the article allow you to expand the functionality of Android applications by using native code. As a result, applications can perform more complex or resource-intensive tasks.

You can upgrade your Android app development skills to Middle/Senior level at online course “Android Developer. Professional” under the guidance of experts in the field.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *