Assemble a native ARMv7 library, and call Android Java methods from its procedures invoked by an Android App, using the JNI conventions.

This document demonstrates how to call the JNI, through a procedure :

  • written with the GNU ARM Assembly syntax,
  • assembled and stored in a ELF shared library,
  • executed by an Android system through an Android application Activity.

The procedure will be called by an Android app and will :

  • build a Java String from an array of 16 bits values representing UTF16-LE encoded characters,
  • retrieve a Java method defined in the Android app, via the mechanisms provided by the JNI,
  • execute this Java method with the built Java String as an argument.

Basically, something like this :

[Our Android App called Decyph'App]
↓
Load the library written in assembly and call its procedures through the JNI
↓
[Our library written in ARMv7 Assembly]
↓
Prepare Java strings and call Decyph'App methods to show these strings on the screen, using the JNI
↓
[Android App]

The executed Java method will display our super secret String on the user’s screen, through the Android App.

All the code used in this document can be found on GitHub and GitLab.

Note that you do not need the NDK to following this example, if you already have a version of GNU Sourceware AS and GNU Binutils able to output ARMv7 machine code and ARMv7 ELF libraries.

This document complements : How to write a native library in ARMv7 assembly, call it from an Android Activity and get back a Java byte[] array.

Quick reminders

The JNI always pass :

  • a pointer to a _JNIEnv structure as the first argument, r0
  • a pointer to the object which received the method call as the second argument, r1

Then, it will pass the other arguments in r2, r3 and the stack (sp) Settings used in this example

This example uses the following settings :

  • Java package name adventurers.decyphering.secrets.decyphapp
  • Java class name DecypherActivity
  • Java native method name decypherArcaneSecrets

Therefore, the method name using the Java_package_name_ClassName_methodName pattern will be :

Java_adventurers_decyphering_secrets_decyphapp_DecypherActivity_decypherArcaneSecrets

GNU ARM Assembly code

decypherArcane.S

.data
java_method_name:
	.asciz "revealTheSecret"
java_method_signature:
	.asciz "(Ljava/lang/String;)V"

// Our UTF16-LE encoded secret message
secret:
	.hword 55357, 56892, 85, 110, 32, 99, 104, 97, 116, 10
	.hword 55357, 56377, 12495, 12512, 12473, 12479, 12540, 10
	.hword 55357, 56360, 27193, 29066, 10
	.hword 55357, 56445, 65, 110, 32, 97, 108, 105, 101, 110, 10
secret_len = . - secret
secret_len = secret_len / 2  /* 2 bytes per character */

.text
.align 2
.globl Java_adventurers_decyphering_secrets_decyphapp_DecypherActivity_decypherArcaneSecrets
.type Java_adventurers_decyphering_secrets_decyphapp_DecypherActivity_decypherArcaneSecrets, %function
Java_adventurers_decyphering_secrets_decyphapp_DecypherActivity_decypherArcaneSecrets:
	push {r4-r7, lr} // Prologue. We will use r4, r5, r6, r7

	// Passed parameters - r0 : *_JNIEnv, r1 : thisObject

	mov r4, r0   // r4 : Backup of *JNIEnv as we'll use it very often
	mov r5, r1   // r5 : Backup of thisObject as we'll invoke methods on it
	ldr r6, [r0] // r6 : Backup of *_JNINativeInterface, located at *_JNIEnv,
	             //      since we'll also use it a lot

	/* Preparing to call NewString(*_JNIEnv : r0, 
	                     *string_characters : r1, 
	                          string_length : r2).
	   *_JNIEnv is still in r0.
	*/

	ldr r1, =secret     // r1 : The UTF16-LE characters composing 
	                    //      the java.lang.String
	mov r2, #secret_len // r2 : The length of the String
	ldr r3, [r6, #652]  // Get *JNINativeInterface->NewString. 
	                    // +652 is NewString's offset
	blx r3              // secret_string : r0 <- NewString(*_JNIEnv : r0, 
	                    //                       *string_characters : r1,
	                    //                            string_length : r2)
	mov r7, r0          // r7 : secret_string
	                    // Keep the returned string for later use

	/* Calling showText(java.lang.String) through the JNI
	
	   First : We need the class of thisObject. We could pass it directly
	   to the procedure but, for learning purposes, we'll use JNI methods
	   to get it.
	*/

	// Preparing to call GetObjectClass(*_JNIEnv : r0, thisObject : r1)
	mov r0, r4         // r0 : *_JNIEnv
	mov r1, r5         // r1 : thisObject
	ldr r2, [r6, #124] // Get *JNINativeInterface->GetObjectClass (+124)
	blx r2             // jclass : r0 <- GetObjectClass(*JNIEnv : r0, 
	                   //                            thisObject : r1)
	/* Second : We need the JNI ID of the method we want to call
	   Preparing for GetMethodId(*JNIEnv : r0, 
	                              jclass : r1, 
	                         method_name : r2, 
	                    method_signature : r3)
	*/

	mov r1, r0 // r1 : jclass returned by GetObjectClass
	mov r0, r4 // r0 : *JNIEnv, previously backed up in r4
	ldr r2, =java_method_name      // r2 : The method name
	ldr r3, =java_method_signature // r3 : The method signature
	ldr r8, [r6, #132]             // Get *JNINativeInterface->GetMethodId
	                               // (+132)
	blx r8     // revealTheSecretID : r0 <- GetMethodId(*_JNIEnv : r0, 
	           //                                         jclass : r1, 
	           //                                    method_name : r2, 
	           //                               method_signature : r3)

	// Finally : Call the method. Since it's a method returning void, 
	// we'll use CallVoidMethod.
	// Preparing to call CallVoidMethod(*_JNIEnv : r0, 
	//                                thisObject : r1,
	//                         revealTheSecretID : r2,
	//                             secret_string : r3)

	mov r2, r0         // r2 : revealTheSecretID
	mov r1, r5         // r1 : thisObject
	mov r0, r4         // r0 : *_JNIEnv
	mov r3, r7         // r3 : secret_string
	ldr r8, [r6, #244] // Get *_JNINativeInterface->CallVoidMethod (+244).
	blx r8 // CallVoidMethod(*_JNIEnv : r0, 
	       //              thisObject : r1,
	       //       revealTheSecretID : r2,
	       //              the_string : r3)
	       // => Java : revealTheSecret(the_string)

	pop {r4-r7, pc} // Restoring the scratch-registers and 
	                // returning by loading the link-register 
	                // into the program-counter

Android’s Java code

DecypherActivity.java

package adventurers.decyphering.secrets.decyphapp;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class DecypherActivity extends AppCompatActivity {

  static { System.loadLibrary("arcane"); }
  native void decypherArcaneSecrets();

  TextView mContentView;

  public void revealTheSecret(String text) {
    mContentView.setText(text);
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_decypher);

    mContentView = (TextView) findViewById(R.id.fullscreen_content);
    decypherArcaneSecrets();
  }
}

Android’s Activity

activity_decypher.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:background="#0099cc"
   tools:context="adventurers.decyphering.secrets.decyphapp.DecypherActivity"
  >

  <!-- The primary full-screen view. This can be replaced with whatever view
  is needed to present your content, e.g. VideoView, SurfaceView,
  TextureView, etc. -->
  <TextView
   android:id="@+id/fullscreen_content"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:gravity="center"
   android:keepScreenOn="true"
   android:textColor="#33b5e5"
   android:textSize="50sp"
   android:textStyle="bold"
   />

</FrameLayout>

Linking and installation from a Unix shell

export PREFIX="armv7a-hardfloat-linux-gnueabi"
export APP_DIR="/tmp/DecyphApp"
export LIBNAME="libarcane.so"
$PREFIX-as -o decypherArcane.o decypherArcane.S
$PREFIX-ld.gold --dynamic-linker=/system/bin/linker -shared --hash-style=sysv -o $LIBNAME decypherArcane.o
mkdir -p $APP_DIR/app/src/main/jniLibs/armeabi{,-v7a}
cp $LIBNAME $APP_DIR/app/src/main/jniLibs/armeabi
cp $LIBNAME $APP_DIR/app/src/main/jniLibs/armeabi-v7a
cd $APP_DIR
./gradlew installDebug

A note on Java method descriptors

As stated in the official documentation, the internal descriptor of Java methods is as follows :

(parameters Types codes without spaces)return Type code

The codes associated to Java Types are :

Java Type Internal Code
void V
boolean Z
byte B
char C
short S
int I
long J
float F
double D
Array [component_code
Class Lfully/qualified/name;

The semicolon after the fully qualified name of a Class is essential.
There is no right square bracket in Array codes.

So, for example, a Java method declared like this :

void a(int a, long b, String c, HashMap[] d, boolean e)

has the following descriptor :

(IJLjava/lang/String;[Ljava/util/HashMap;Z)V

Getting descriptors automatically

Now, even with the whole format provided, finding the Descriptor of a method can be difficult at first. Fortunately, there are two ways to handle this issue.

Using javap to get the descriptor

One way to obtain that kind of informations is to use javap on your class :

javap -s your/package/name/Class

Which will output something like :

    Compiled from "Filename.java"



    ...



    void a(int, long, java.lang.String, java.util.HashMap[], boolean);

        descriptor: (IJLjava/lang/String;[Ljava/util/HashMap;Z)V



    ...

Using Java reflect methods

Now, generating sample Java projects (or test files), just to copy-paste a function, compile it and analyse it through javap can be cumbersome. So another way is to simply use Java.lang.reflect methods, from your application, to get the signatures of every declared function in a Class and rebuild the appropriate Descriptor of these methods programmatically.

Here’s a sample code that help you do that

package your.package.name;

import java.lang.reflect.Method;
import java.util.HashMap;

import android.util.Log;

public class MethodsHelpers {

  static public HashMap<Class, String> primitive_types_codes;
  static public String LOG_TAG = "MY_APP";

  static {
    primitive_types_codes = new HashMap<Class,String>();
    primitive_types_codes.put(void.class,    "V");
    primitive_types_codes.put(boolean.class, "Z");
    primitive_types_codes.put(byte.class,    "B");
    primitive_types_codes.put(short.class,   "S");
    primitive_types_codes.put(char.class,    "C");
    primitive_types_codes.put(int.class,     "I");
    primitive_types_codes.put(long.class,    "J");
    primitive_types_codes.put(float.class,   "F");
    primitive_types_codes.put(double.class,  "D");
  }

  public static String code_of(final Class class_object) {
    final StringBuilder class_name_builder = new StringBuilder(20);
    Class component_class = class_object;
    while (component_class.isArray()) {
      class_name_builder.append("[");
      component_class = component_class.getComponentType();
    }
    if (component_class.isPrimitive())
      class_name_builder.append(primitive_types_codes.get(component_class));
    else {
      class_name_builder.append("L");
      class_name_builder.append(
        component_class.getCanonicalName().replace(".", "/")
      );
      class_name_builder.append(";");
    }
    return class_name_builder.toString();
  }

  public static void print_methods_descriptors_of(Class analysed_class) {
    StringBuilder descriptor_builder = new StringBuilder(32);
    Method[] methods = analysed_class.getDeclaredMethods();
    for (Method meth : methods) {
      descriptor_builder.append("(");

      for (Class param_class : meth.getParameterTypes())
        descriptor_builder.append(code_of(param_class));

      descriptor_builder.append(")");

      descriptor_builder.append(code_of(meth.getReturnType()));

      Log.d(LOG_TAG,
            String.format("%s\n"+
                          "Name       : %s\n"+
                          "Descriptor : %s\n\n",
                          meth.toString(),
                          meth.getName(),
                          descriptor_builder.toString())
      );

      descriptor_builder.delete(0, descriptor_builder.length());
    }
  }

}

Just use it like this :

import static your_package_name.MethodHelpers.print_methods_descriptors_of;

...

print_methods_descriptors_of(AnalysedClass.class);

And then you should see something like on the output :

    D/MY_APP  (22564): void your.package.name.a(int,long,java.lang.String,java.util.HashMap[],boolean)

    D/MY_APP  (22564): Name       : a

    D/MY_APP  (22564): Descriptor : (IJLjava/lang/String;[Ljava/util/HashMap;Z)V

What about constructors ?

In order to construct objects through the JNI, you’ll need to :

  • Get the methodID of the constructor by using GetMethodId(_JNIEnv* : r0, jclass : r1, <init> : r2, constructor_descriptor : r3). Note that constructors always return void.
  • Use NewObject(_JNIEnv* : r0, jclass : r1, jmethodID : r2, ...) (offset +112) to construct the object

How to write a native library in ARMv7 assembly, call it from an Android Activity and get back a Java byte[] array

Here’s a little demonstration about how to assemble a native library, written in the GNU flavor of the ARMv7 assembly, that will be called through the Java Native Interface, using an Android project as an example.

The procedure will return a Java byte[] array object containing the content of a static string, defined in the library.

In most cases, C/C++ will do a far better job. However, for the record, this document provide informations about how to do that without a C compiler (You’ll still need an ARMv7 assembler, though…).

This document complements Assemble a native ARMv7 library, and call Android Java methods from its procedures invoked by an Android App, using the JNI conventions.

The example

Coding the library

This example is heavily commented as I wrote it while learning assembly. This should provide a clear understanding of this example for people new to ARM Assembly.

If you’re a professional, you might find it more comfortable to strip the comments with the editor of your choice.

wild.s

.data

msg:
  .ascii  "A wild Assembly appears !\n"
msg_len = . - msg

.text
.align 2
.globl Java_your_pack_testactivity_TestActivity_testMe
.type Java_your_pack_testactivity_TestActivity_testMe, %function
Java_your_pack_testactivity_TestActivity_testMe:
  stmfd sp!, {r4-r6, lr} // Prologue. We will use r4 and r6. Is push more useful than stmfd ?
  
  // Useful passed parameters - r0 : *_JNIEnv
  mov r4, r0 // Save *_JNIEnv for the second method

  // Preparing to call NewByteArray(*_JNIEnv : r0, size_of_array : r1). *_JNIEnv is already loaded.
  mov r1, #msg_len   // r1 : size_of_array = msg_len
  ldr r5, [r0]       // Getting NewByteArray : Get *JNINativeInterface from *_JNIEnv. *JNINativeInterface is preserved for later use.
  ldr r3, [r5, #704] // Get *JNINativeInterface->NewByteArray. +704 is NewByteArray 's offset
  blx r3             // r0 : *bytearray <- NewByteArray(*_JNIEnv : r0, size_of_array : r1)
  mov r6, r0         // We need to keep *bytearray elsewhere as it will be returned by our procedure. r0 is needed for *_JNIEnv

  /* Note : Calculting offset in a structure containing only function pointers is equivalent to :
     Number of functions pointers declared before the desired function pointer * Size in bytes of a function address (4 in 32-bit)
  
     Preparing to call *JNativeInteface->SetByteArrayRegion(*_JNIEnv : r0, *bytearray r1, 0 : r2, int bytes_to_copy : r3, *from : sp) */

  mov r1, r0         // r1 : *bytearray - The return value of NewByteArray
  mov r0, r4         // r0 : *_JNIEnv - Previously saved in r4
  mov r2, #0         // r2 : 0 - Define the starting index for the array-copy procedure of SetByteArrayRegion
  mov r3, #msg_len   // r3 : bytes_to_copy = msg_len
  sub sp, sp, #4     // Preparing the stack in which we'll store the address of msg
  ldr r4, =msg       // We won't need our previous copy of *_JNIEnv anymore, so we replace it by *msg.
  str r4, [sp]       // sp : *from = msg address - the native byte array to copy inside the Java byte[] array
  ldr r5, [r5, #832] // r5 <- r5 : *JNativeInterface->SetByteArrayRegion (+832). We don't need r5 after this so we store the function address directly in it.
  blx r5             // SetByteArrayRegion(*_JNIEnv : r0, *bytearray : r1, 0 : r2, size_of_msg : r3, *msg : sp)
  
  add sp, sp, #4        // Get our stack space back !
  mov r0, r6             // *bytearray : Our return value
  ldmfd sp!, {r4-r6, pc} // Restoring the scratch-registers and returning by loading the link-register into the program-counter

Then assemble and link this example library :

export PREFIX="armv7a-hardfloat-linux-gnueabi" # Replace this by the prefix of your toolset or remove '$PREFIX-' from the next commands
export DEST="/path/to/your/TestActivityProject/app/src/main/jniLibs" # Skip this if you don't have an Android project
$PREFIX-as -o wild.o wild.s
$PREFIX-ld.gold -shared --dynamic-linker=/system/bin/linker -shared --hash-style=sysv -o libwildAssembly.so wild.o
cp libwildAssembly.so $DEST/armeabi/libwildAssembly.so # Skip this if you don't have an Android project
cp libwildAssembly.so $DEST/armeabi-v7a/libwildAssembly.so # Skip this if you don't have an Android project

Calling this from Android

Generate a project with :

  • the same package name you used in the assembly listing (your.pack.testactivity),
  • an activity named TestActivity

And define native byte[] testMe() in it.

TestActivity.java

package your.pack.testactivity;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;

public class TestActivity extends AppCompatActivity {

  /* Basically, the android system will look for a "libwildAssembly.so" in the 
     app's private and public folders. */
  static { System.loadLibrary("wildAssembly"); }

  /* And then look for a symbol named :
    Java_package_name_ClassName_methodName.
    
    The current package name is : your.pack.testactivity
    The current class name is : TestActivity 
    The method name is testMe
    So the android linker will look for a symbol named :
    Java_your_pack_testactivity_TestActivity_testMe 
    
    There is no signature or return value check in assembly, so your
    java compiler will compile this class EVEN if the library is not
    there or if the symbol name is invalid.
    There is no such things as "return type" or "parameters type" in 
    assembly so no such check will be performed ever. */
  static native byte[] testMe();
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_test);

    TextView mContentView = (TextView) findViewById(R.id.fullscreen_content);
    mContentView.setText(new String(testMe()));

  }

  /* Try it : Redeclare testMe() as 'native int testMe()' and 
     new String(testMe()) by String.format(Locale.C, "%d", testMe()) */
}

activity_test.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             xmlns:tools="http://schemas.android.com/tools"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:background="#0099cc"
             tools:context="your.pack.testactivity.TestActivity"
  >

  <!-- The primary full-screen view. This can be replaced with whatever view
         is needed to present your content, e.g. VideoView, SurfaceView,
         TextureView, etc. -->
  <TextView
    android:id="@+id/fullscreen_content"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:keepScreenOn="true"
    android:text="@string/dummy_content"
    android:textColor="#33b5e5"
    android:textSize="50sp"
    android:textStyle="bold"
    />

</FrameLayout>

Copy the generated native library inside the Android project

Create a directory named jniLibs in $YourProjectRootFolder/app/src/main if it doesn’t exist

Then create two directories armeabi and armeabi-v7a in it so you have :

  • $YourProjectRootFolder/app/src/main/jniLibs/armeabi
  • $YourProjectRootFolder/app/src/main/jniLibs/armeabi-v7a

Copy your library libwildAssembly.so in those folders

Then compile and install the project on your phone, using either : * gradle installDebug * The ‘Run’ button on Android Studio

How it works, basically

For what I understand, when you define the following in a Java class :

package your.package

public class YourClass ... {
  ... {
  System.loadLibrary("name"); 
  }
  ...
  native return_type methodName(parameters...)
  ...
}
  • The JVM (or Dalvik) will first search for the library “name” in a way typical to the current system.
    Using the same example, on Android systems (Linux), Dalvik will search for libname.so in places referenced by the current LD_LIBRARY_PATH.
  • Then, it will look for a symbol following this pattern in the library found :
    Java_your_package_YourClass_methodName
  • Once the symbol found, it will execute the instructions at the symbol address, passing the following arguments using the standard procedure call convention :
    • the address of the data structure representing the current Java environment (*_JNIEnv in C programs) (in r0 on ARM)
    • the address of the data structure representing the current Java object (this) on which the method is called (jobject thisObj) (in r1)
    • the other arguments (in r2, r3 and the stack accessible through sp)

If you look in the jni.h file provided with your NDK, you’ll see that _JNIEnv is a data structure defined like this :

struct _JNIEnv {  
   const struct JNINativeInterface* functions;  
  /* C++ specific hacks around 'functions' */
}

The JNINativeInterface is a data structure composed only by function pointers, plus a starting padding (of 4 void* pointers).

So basically, _JNIEnv* equates to :

*_JNIEnv ->
	*JNINativeInterface ->
		NULL
		NULL
		NULL
		NULL
		*GetVersion
		*DefineClass
		...

Getting the address offset of a function pointer defined in JNINativeInterface tends to boil down to :

Size of a procedure address (4) * number of statements preceding the statement defining the function pointer

For example, the offset of NewByteArray, preceded by 176 statements, is 176*4 = 704.

Note that the JNI documentation actually provides the index of each function pointer in JNINativeInterface.
If you look at the documentation, you’ll see that the index of NewByteArray is 176. Then multiply by 4 octets, the size of an address on ARMv7 architectures, and you’ll get the same offset : 704.

Since the argument provided by the JNI to the native procedure is a pointer to _JNIEnv, calling NewByteArray requires to :

  • Get the data structure pointed by r0
  • Get the data structure pointed in result + 704
  • Prepare to pass the first parameter in r1
  • Call the result

However, note that most of the JNI functions require *_JNIEnv, so you’ll have to save r0 somewhere in order to call the different functions correctly.

Once you know that, the rest is kinda easy.

Look up the appropriate functions to call in the JNI documentation and call them with the right arguments.