Arm Community
Arm Community
  • Site
  • User
  • Site
  • Search
  • User
  • Groups
    • Research Collaboration and Enablement
    • DesignStart
    • Education Hub
    • Innovation
    • Open Source Software and Platforms
  • Forums
    • AI and ML forum
    • Architectures and Processors forum
    • Arm Development Platforms forum
    • Arm Development Studio forum
    • Arm Virtual Hardware forum
    • Automotive forum
    • Compilers and Libraries forum
    • Graphics, Gaming, and VR forum
    • High Performance Computing (HPC) forum
    • Infrastructure Solutions forum
    • Internet of Things (IoT) forum
    • Keil forum
    • Morello Forum
    • Operating Systems forum
    • SoC Design and Simulation forum
    • 中文社区论区
  • Blogs
    • AI and ML blog
    • Announcements
    • Architectures and Processors blog
    • Automotive blog
    • Graphics, Gaming, and VR blog
    • High Performance Computing (HPC) blog
    • Infrastructure Solutions blog
    • Innovation blog
    • Internet of Things (IoT) blog
    • Operating Systems blog
    • Research Articles
    • SoC Design and Simulation blog
    • Tools, Software and IDEs blog
    • 中文社区博客
  • Support
    • Arm Support Services
    • Documentation
    • Downloads
    • Training
    • Arm Approved program
    • Arm Design Reviews
  • Community Help
  • More
  • Cancel
Arm Community blogs
Arm Community blogs
Operating Systems blog Example of calling the JNI directly from ARM Assembly on Android
  • Blogs
  • Mentions
  • Sub-Groups
  • Tags
  • Jump...
  • Cancel
More blogs in Arm Community blogs
  • AI and ML blog

  • Announcements

  • Architectures and Processors blog

  • Automotive blog

  • Embedded blog

  • Graphics, Gaming, and VR blog

  • High Performance Computing (HPC) blog

  • Infrastructure Solutions blog

  • Internet of Things (IoT) blog

  • Operating Systems blog

  • SoC Design and Simulation blog

  • Tools, Software and IDEs blog

Tags
  • Assembly
  • android ndk
  • Android
  • java
  • gnu_assembler
  • Documentation
  • jni
  • java_programming
  • dalvik
  • Arm Assembly
  • native-library
Actions
  • RSS
  • More
  • Cancel
Related blog posts
Related forum threads

Example of calling the JNI directly from ARM Assembly on Android

Myy
Myy
June 25, 2016
6 minute read time.

Here's a little demonstration about how to generate a library, with an assembly procedure 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.

This document complements Example of calling Java methods through the JNI, in ARM Assembly, on Android.

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 (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>

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.

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...)
  ...
}
  1. 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.
  2. Then, it will look for a symbol following this pattern in the library found :
    Java_your_package_YourClass_methodName
  3. 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)

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* ->

    paddingx4

    *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.

This assumes that void* and function pointers are of the same size.

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
  • 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.

Anonymous
  • Myy
    Offline Myy over 6 years ago

    This example is now available here :

    gitlab.com/.../master

    • Cancel
    • Up 0 Down
    • Reply
    • More
    • Cancel
  • Myy
    Offline Myy over 6 years ago

    Example added : Example of calling Java methods through the JNI, in ARM Assembly, on Android .

    • Cancel
    • Up 0 Down
    • Reply
    • More
    • Cancel
  • bestapps2go
    Offline bestapps2go over 6 years ago

    very good explanation!

    • Cancel
    • Up 0 Down
    • Reply
    • More
    • Cancel
  • Myy
    Offline Myy over 6 years ago

    Greetings bestapps2go,

    When entering the procedure testMe, r1 contains *thisObj as :

    • the JNI always pass it as second argument to the procedure called
    • the AAPCS states that integer procedure's arguments are stored in r0, r1, r2, r3 and the stack (sp) before calling it. So, with 32 bits wide or less arguments, r0 contains the first argument, r1 the second, etc...

    However, as you understood, *thisObj is not used at all in this procedure and, therefore, was not backed up in another register, or in the stack.

    When calling  mov r1, #msg_len  , the value in r1 is overwritten with the value msg_len.

    So basically :

    // Before :
    // r1 = *thisObj (0x???????? -> address of the Java object on which this native method was called)
    mov r1, #msg_len
    // After :
    // r1 = value identified by msg_len (26 in this case)
    

    I'll try to add an example using *thisObj in the next days.

    • Cancel
    • Up 0 Down
    • Reply
    • More
    • Cancel
  • bestapps2go
    Offline bestapps2go over 6 years ago

    Hello... thanks for your example. It is very useful.

    I would like to ask that you mentioned:

    mov r1, #msg_len   // r1 : size_of_array = msg_len

    but then you said:

    • the address of the data structure representing the current Java object (this) on which the method is called (jobject thisObj) (in r1)

    so actually r1 is thisObj but since it is not used inside testMe() and reused. Am I right?

    Thanks!


    • Cancel
    • Up 0 Down
    • Reply
    • More
    • Cancel
Operating Systems blog
  • Enhancing Chromium's Control Flow Integrity with Armv9

    Richard Townsend
    Richard Townsend
    This blog explains how Control Flow Integrity, an Armv9 security feature, works on the newly launched Chromium M105.
    • October 11, 2022
  • Playing with Virtual Prototypes: Debugging and testing Android games should be as much fun as playing them!

    Sam Tennent
    Sam Tennent
    One of the most amazing developments in the last few years has been the explosion in mobile gaming. Not so long ago, if you wanted to while away the time playing a game on your phone there were not many…
    • September 21, 2020
  • SUSE Rocks in Nashville!

    P. Robin
    P. Robin
    I attended the 2019 edition of SUSE conference. I take you through the the latest developments in enterprise Linux, OpenStack, CEPH and more, from the conference.
    • May 1, 2019