What will happen is that :
The terms “procedure” and “functions” have the same meaning in this document. They are considered to be parameterized series of executable instructions. The term “symbols” refer to names stored in the libraries. If you create a library with a function named “my_function” or “my_procedure”, the library will contain the “symbol” “my_function” or “my_procedure”.
We assume that Android executes “Java” code, even if it’s not technically the case, since the same JNI conventions and the libraries are used.
This document complements the document : Assemble a native ARMv8 library, and call its procedures from an Android application, using the JNI conventions.
This document assumes that :
In order to assemble the library and install the Android Application, you will need :
All the code written in this document is also on the following Git repository : https://github.com/Miouyouyou/ARMv8a-Call-Java-method-from-Assembly
This example is heavily commented, in order to help people new to ARM Assembly, or the Assembly “language” in general.
So the point of the library assembled in this tutorial is to provide a procedure, that will generate a Java String object containing a specific text defined in the assembly listing. The text itself in encoded in UTF-16. The text contained in this Java String will then be passed to a Java function defined in the Android Application which will display the provided String on the screen, using appropriate Android SDK Widgets.
Now, in order make the logic of the following code clearer, I’ll restate some parts of the ARMv8-A 64 bits Application Procedure Call Standard (AAPCS), which is a convention that should be followed by procedures in order to cooperate smoothly. This document can be read here : http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf
x0..x7 means all the registers between x0 and x7, that is x0, x1, x2, x3, x4, x5, x6 and x7.
When following the AAPCS :
So, if we need to save an argument for later use, x19..x28 is our best bet. The content of x9..x15 will require a push/pop in the stack on every procedure call. We could also directly push/pop x0..x7 when we need to save/load their content again. But x19..x28 only require one push/pop operation in an entire procedure, which is clearly more efficient in our case.
Now, when the Java native method calling convention define the following :
In order to find the native function to execute, the “JVM” will search in the loaded libraries, a symbol formed like this : Java_Package_name_with_dots_replaced_by_underscores_Class_name_Native_function_name For example, here :
Java_Package_name_with_dots_replaced_by_underscores_Class_name_Native_function_name
adventurers.decyphering.secrets.decyphapp
DecypherActivity
decypherArcaneSecrets
Java_adventurers_decyphering_secrets_decyphapp_DecypherActivity_decypherArcaneSecrets
The first two arguments that a native function will always receive are :
_JNIEnv
**JNINativeInterface
*JNIEnv
JNINativeInterface
(#{mangled_parameters_types_names})#{mangled_return_type}
void revealTheSecret(String)
(Ljava/lang/String;)V
String
java.lang.String
Once you understand these parts, the logic of the following assembly code should be easier to understand.
.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) / 2 .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: sub sp, sp, 48 // Prepare to push x19, x20, x21, x22 and lr (x30) // 5 registers of 8 bytes each -> 40 bytes // Unless you like to deal with corner cases, you'll // have to keep the stack aligned on 16 bytes. // 40 % 16 != 0 but 48 % 16 == 0, so we use 48 bytes. stp x19, x20, [sp] stp x21, x22, [sp, 16] stp x23, x30, [sp, 32] // Passed parameters - x0 : *_JNIEnv, x1 : thisObject mov x19, x0 // x19 <- Backup of *JNIEnv as we'll use it very often mov x20, x1 // x20 <- Backup of thisObject as we'll invoke methods on it ldr x21, [x0] // x21 <- Backup of *_JNINativeInterface, located at *_JNIEnv, // since we'll also use it a lot /* Preparing to call NewString(*_JNIEnv : x0, *string_characters : x1, string_length : x2). *_JNIEnv is still in x0. */ adr x1, secret // x1 <- *secret : The UTF16-LE characters composing // the java.lang.String we'll pass to // the Java method called afterwards. mov x2, #secret_len // x2 <- secret_len : The length of that java.lang.String ldr x3, [x21, #1304] // x3 <- *JNINativeInterface->NewString function. // +1304 is NewString's offset in the JNINativeInterface // structure. blr x3 // secret_java_string : x0 <- NewString(*_JNIEnv : x0, // *secret : x1, // secret_len : x2) mov x22, x0 // x22 <- secret_java_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 : x0, thisObject : x1) mov x0, x19 // x0 <- *_JNIEnv (previously saved in x19) mov x1, x20 // x1 <- thisObject (previously saved in x20) ldr x2, [x21, #248] // x2 <- Get *JNINativeInterface->GetObjectClass (*JNINativeInterface+248) blr x2 // jclass : x0 <- GetObjectClass(*JNIEnv : x0, // thisObject : x1) /* Second : We need the JNI ID of the method we want to call Preparing for GetMethodId(*JNIEnv : x0, jclass : x1, method_name : x2, method_signature : x3) */ mov x1, x0 // x1 <- jclass returned by GetObjectClass mov x0, x19 // x0 <- *JNIEnv, previously backed up in x19 adr x2, java_method_name // x2 <- &java_method_name : The method name adr x3, java_method_signature // x3 <- &java_method_signature : The method signature ldr x4, [x21, #264] // Get *JNINativeInterface->GetMethodId blr x4 // revealTheSecretID : x0 <- GetMethodId(*_JNIEnv : x0, // jclass : x1, // &method_name : x2, // &method_signature : x3) // Finally : Call the method. Since it's a method returning void, // we'll use CallVoidMethod. // Preparing to call CallVoidMethod(*_JNIEnv : x0, // thisObject : x1, // revealTheSecretID : x2, // secret_string : x3) mov x2, x0 // x2 <- revealTheSecretID mov x1, x20 // x1 <- thisObject (previously saved in x20) mov x0, x19 // x0 <- *_JNIEnv (previously saved in x19) mov x3, x22 // x3 <- secret_java_string (previously saved in x22) ldr x4, [x21, #488] // x4 <- *_JNINativeInterface->CallVoidMethod (+488). blr x4 // CallVoidMethod(*_JNIEnv : x0, // thisObject : x1, // revealTheSecretID : x2, // the_string : x3) // => Java : revealTheSecret(the_string) ldp x19, x20, [sp] ldp x21, x22, [sp, 16] ldp x23, x30, [sp, 32] add sp, sp, 48 ret
Then assemble and link this example library :
export PREFIX="aarch64-linux-gnu-" $PREFIX-as -o decypherArcane.o decypherArcane.S $PREFIX-ld.gold -shared --dynamic-linker=/system/bin/linker --hash-style=sysv -o libarcane.so decypherArcane.o
Now that our library is assembled, we’ll just need to :
So, to do that we’ll generate a project with :
public void revealTheSecret(String text)
native void decypherArcaneSecrets()
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(); } }
Here’s the XML of the interface used by this Activity
<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>
Create a directory named jniLibs in $YourProjectRootFolder/app/src/main if it doesn’t exist
jniLibs
$YourProjectRootFolder/app/src/main
Then create a directory named arm64-v8 in it. Once done, the following path should exist :
arm64-v8
$YourProjectRootFolder/app/src/main/jniLibs/armv8-64
Copy the previously assembled libarcane.so in that folder.
libarcane.so
Then install the app on your phone, using the standard installation procedure. That is, either :
./gradlew installDebug
Then run it on your select terminal (phone, tablet, emulator) and you should see something like this :
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.
One method is to create a simple Java project and define a Java function using the same signature (same return type, same name, same parameters types). Just provide a minimalist useless implementation, compile your project and use javap on the class containing your method, like this :
javap
javap -s your/package/name/Class
For example, if your class :
Filename.java
void a(int a, long b, String c, HashMap[] d, boolean e)
This should output something like this :
Compiled from "Filename.java" ... void a(int, long, java.lang.String, java.util.HashMap[], boolean); descriptor: (IJLjava/lang/String;[Ljava/util/HashMap;Z)V ...
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 manually.
Java.lang.reflect
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