天天看點

android中ndk的開發

前言(其實是吐槽)

這是我看(android應用安全防護和逆向分析)遇到的第一個坑了,在章節2.1和2.2裡,雖然作者很貼心的給了步驟教你如何搭建ndk的開發環境,但是,我要說的是,如果按照作者在2.1.2的五個步驟按部就班的來,你絕對!不可能!完成!

主要的原因我就不再分析了,大約就是少了一堆亂七八糟的說明和步驟,這裡我重新寫一遍ndk開發相關。(如果你不信,可以嘗試隻按照2.1.2章節的五步來嘗試)

搭建NDK開發環境

NDK相關概念

首先,普及一下ndk的概念,何謂ndk開發呢?

簡而言之,就是讓安卓(java)可以調用前人用c語言完成的庫,這麼做的好處主要有兩個,

第一,節約代碼量,提高工作(運作)效率,可以用之前c寫好的很多很棒的庫

第二,防止應用被逆向,因為java層的代碼很容易被反編譯逆向突破。

再介紹幾個ndk裡相關概念名詞,

.c,.cpp,這幾個不用多說了吧,就是c或者c++等檔案的字尾名;

.so,這個是編譯c檔案得到的庫檔案的字尾名,.so檔案大概就相當于windows上的.dll檔案,他可以友善讓别人調用;

.h,也就是通過javah指令編譯類(class)檔案編譯出來的頭檔案,這玩意開始在c裡用作聲明管理,現在在jni裡已經意義不大了,你可以當做沒啥用;

jni,全名 java native interface,由他提供java和c之間的通信;

一個NDK開發的流程

編寫Java代碼(.java) —————> 編譯生成位元組碼檔案(.class) —————> 産生C頭檔案(.h) —————> 編寫jni實作代碼(.c) —————> 編譯成連結庫(.so)

基于Android Studio3 & CMAKE的NDK環境搭建
  1. 打開你的as(Android Studio),建立一個支援c++的項目(如圖),然後一路next到結束;
    android中ndk的開發
    建立一個支援c++的項目.png
  1. 此時一個基本工程環境已經建立好了,不需要任何改動,我們直接可以點選build生成一個系統自帶的示例so庫也就是cpp目錄裡的native-lib,但在此之前,我們先看一下工程目錄:
    android中ndk的開發
    工程目錄.png

可以看到,相對于普通項目,我們的ndk的項目其實就是多了cpp目錄,多了CMakelists.txt檔案,此外build.gradle裡多了一些配置;

  1. 因為編譯出so庫,主要看的是CMakelists檔案裡的配置,下面我們重點關注一下CMakelists檔案的内容,如圖:
    android中ndk的開發
    CMakeLists.png

預設的CMakelists檔案裡,主要有三個配置需要關注,分别是

add_library:

主要用作給生成的so庫做配置,在三個注釋的下面,每一個都代表了一個配置項(其實注釋的英文已經說明一切了),

Sets the name of the library下,填寫你希望生成的so庫的名稱,這裡注意的是,你填寫的名稱,最終會被自動加上lib的字首和.so的字尾,是以你隻需要基礎的名稱就夠了,最終生成的so檔案會在工程的app-build-intermediates-cmake-debug-obj目錄下,每個abi環境都有個檔案夾,裡面放着對應的so庫;

Sets the library as a shared library下,填寫你要生成的so庫的類型,這裡的SHARED是指動态連結庫類型;

Provides a relative path to your source file(s)下,填寫你要編譯成so檔案的對應的c源檔案的路徑;

find_library:

主要是用作添加我們本地庫的依賴庫,

Sets the name of the path variable下,填寫你引用的庫的别名,便于引用;

you want CMake to locate下,填寫你要引用的依賴庫;

target_link_libraries:

是為了關聯我們自己的庫和一些第三方庫或者系統庫,

Specifies the target library下,填寫本地庫的名稱;

included in the NDK下,填寫你要關聯的庫的名稱。

  1. ok,現在讓我們點選一下工具欄的錘子圖示,就可以生成so庫了。
補充一些tips

關于環境

android ndk開發需要設定環境變量以及安裝sdk,ndk,cmake,lldb,如果沒有,可以在as的tools選項裡,點選sdk manager,在sdk tools欄裡,把這些都勾上,點選确定即可;

關于預設生成的例子

按照正常的ndk開發流程,應該是先寫java類,這個類裡可能會引用到native的方法也就是拿c實作的方法;然後javah指令編譯這個java類,生成.h的頭檔案(這一步可以不用);再寫一個.c或者.cpp實作在你java類裡引用的這個native方法;最後編譯你的c檔案,生成最終的so庫。

在系統預設的例子裡,你點一下編譯按鈕就能得到so庫,可能對這個流程不太明白,下面我們具體說一下:

先看一下MainActivity類的代碼:

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}
           

你可以當做這是系統給你先寫好的一個引用了native方法的java類,也就是說這就是ndk第一步編寫java類的産物;

在static裡,System.loadLibrary("native-lib")這句會先于onCreate函數觸發,這句是載入我們生成的動态連結庫.so檔案,這裡隻需要填寫CMakeLists裡面填寫的名稱即可;然後你會看到下面引用了這個庫裡的native方法,也就是這句public native String stringFromJNI();這就是系統幫你做好的第一步編寫java類;

然後我們需要編寫實作這個native方法的.c檔案,這裡系統也幫你寫好了就是在cpp檔案夾裡的那個native-lib.cpp的檔案,這個檔案裡實作了stringFromJNI()這個方法;

最後,我們點選編譯,成功生成了so檔案,這就是系統的例子裡,ndk開發的完整流程。

Hello World 工程引發的被坑事件

前面是介紹了如何在as3的環境下進行ndk開發以及解析了系統自帶的例子,接下來我們肯定要仿照系統的例子,自己走一遍ndk開發的流程,當然第一個工程肯定就是通用的HelloWorld了。

在小黃書(android應用安全防護和逆向分析)裡,也有對helloworld工程的舉例,流程是:建立一個java類,在java類裡申明一個native方法,再在main方法裡載入動态連結庫,建立一個執行個體調用native方法;然後使用vc6.0編譯器編寫好native方法,生成.dll動态連結庫檔案;最後把該dll檔案寫入環境變量内,運作java類;具體代碼和流程參見小黃書p14-p15頁。

不過,其實不用如此麻煩,之是以用vc寫c的内容,是因為當時as編譯器對c的編寫支援的還不夠好,但as3以後,這塊已經開始逐漸完善了,是以,我們可以不用按照書裡的流程,直接使用as3來寫c的内容,不過,這裡有一個坑,開始我是想仿造小黃書的示例代碼,單獨寫一個具有main方法的java類來調用本地方法,大概是這樣:

package com.example.test.testndk;

public class HelloWorld {

    public native String stringFromJNITest();
    static {
        
        System.loadLibrary("JNITest");
    }


    public static void main(String[] args){
        HelloWorld hello = new HelloWorld();
        String hi = hello.stringFromJNITest();
        System.out.println(hi);
    }
}
           

JNITest是我事先寫好的cpp檔案編譯成的.so庫,裡面實作了stringFromJNITest()方法,.cpp内容是這樣的:

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring

JNICALL
Java_com_example_test_testndk_MainActivity_stringFromJNITest(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello world";
    return env->NewStringUTF(hello.c_str());
}
           

以上其實就是我們自己實作一個ndk開發的流程,即先寫一個java類,在類中聲明一個native方法,然後寫.cpp實作這個native方法,編譯cpp成so庫,在java類裡用loadlibrary加載這個so庫。

本例中,這是一個簡單的native方法就是傳回了一串字元串“Hello World”而已,原本我以為到此結束,點選運作這個java類我們的Hello World之旅就結束了,但是沒想到報了個錯:java.lang.UnsatisfiedLinkError: no JNITest in java.library.path。

開始我懷疑是不是CMakelists.txt的配置問題,但是我把這個so庫在android工程的mainactivity裡引用,居然沒有問題,也就是說,應該不是配置的路徑問題。經過在群裡讨論以及百度,又開始懷疑是不是java.library.path的問題,因為據了解,其實你用as3運作android工程的mainactivity,編譯器是會自動替你生成so庫用的(隻要你CMakelists配置好了),是以不會有路徑問題,但是當你建立一個java類,寫上main方法,獨立運作這個java類的時候,其實是按java工程的方式找你的動态連結庫檔案的,而這個方式,在win下,是在環境變量path裡找;在linux下,是在系統變量LD_LIBRARY_PATH裡找,為了證明這個結論,我在系統的環境變量path裡,添加了so庫所在的目錄路徑。

但是,依舊報錯。。no JNITest in java.library.path;這時群友又提醒我,我是win的系統,而win的動态連結庫的格式是.dll,是以光配置了路徑,但因為我的格式是 .so,是以也找不到;現搭一個linux系統來做實驗成本太高,我采用了一個取巧的方式,直接把生成的.so檔案字尾改為了dll,因為如果是字尾的原因,那麼會報錯,但肯定不會再報找不到的錯,算是側面論證。

我把字尾改為dll之後,再次運作!奇迹發生了,果然,還是報on JNITest in java.library.path,為什麼呢,我猜可能是win下和android下,尋找規則不同,在android工程裡,隻需要寫庫名,不需要寫字首lib和字尾 .so,在win下,則需要寫上字首lib;最後,我把 System.loadLibrary("JNITest");改為System.loadLibrary("libJNITest");再次點選運作!

這次報錯不同了,不再是找不到,而是Can't load this .dll (machine code=0x34) on a AMD 64-bit platform;終于,本次Hello World被坑事件的坑算是完美填上。