JNI背光调节-JNI.Makefile.SELinux/SEAndroid综合总结
一、JNI
JNI:Java Native Interface,Java本地接口的缩写。在Android中,Java层主要负责UI功能的实现,而C\C++层则完成一些复杂的算法以及和底层的交互功能。
1、装载JNI动态库
为了使用JNI,在调用本地方法前必须把C/C++代码所在的动态库装载到进程的内存文件中。
MainActivity.java:
// Used to load the 'jnilcdctrl' library on application startup.
static {
System.loadLibrary("jnilcdctrl");
}
其调用的是System类的LoadLibaray()方法:
public static void loadLibrary(String libName)
loadLibrary()方法的参数是动态文件库名称的一部分,这里传入的参数是动态库名称去掉前缀“lib”和后缀“.so”的中间部分。因为JNI本来是Java的产物,Java希望代码跨平台应用,不同平台后缀并不一样,所以这里传入的参数省略了和系统相关的部分。
2、定义native方法
在Java类中定义native方法只需要在方法前面加上“native”关键词:
MainActivity.java:
/**
* A native method that is implemented by the 'jnilcdctrl' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
public native int changeLcd(String progress);
public native String getLcd();
在native中,可以使用任何类型作为参数。
changeLcd(String progress)函数使用String类型作为参数,用于改变Lcd写入文件的值,返回int类型0或者1。
getLcd()用于获取当前Lcd的写入文件信息,返回String类型。
3、编写JNI库文件
javac编译生成“.class”文件,在class文件生成的相应目录执行命令“javah”生成对应的C++“.h”文件。将“.h”文件改写成“.c/cpp”文件实现native方法。
文件中:
Java_com_example_liyao_jniledctrl_MainActivity_stringFromJNI
Java_com_example_liyao_jniledctrl_MainActivity_changeLcd
Java_com_example_liyao_jniledctrl_MainActivity_getLcd
方法是将来与动态链接库交互的接口,需要名字保持一致。JNI函数名称分为三部分:首先是Java关键字,供Java虚拟机识别;然后是调用者类名称(全限定的类名,其中用下划线代替名称分隔符);最后是对应的方法名称,各段名称之间用下划线分割。
JNI函数的参数也由三部分组成:
首先是JNIEnv*,一个纸箱JNI运行环境的指针;
第二个参数随本地方法是静态还是非静态而有所不同:非静态本地方法的第二个参数是对对象的引用,而静态本地方法的第二个参数是对其Java类的引用;
其余的参数对应通常Java方法的参数,参数类型需要根据一定的规则进行映射。
native-lib.c:
#include
#include
#include
#define LCD_FILE "/sys/class/leds/lcd-backlight/brightness"
JNIEXPORT jstring JNICALL
Java_com_example_liyao_jniledctrl_MainActivity_stringFromJNI(
JNIEnv *env,
jobject obj/* this */) {
//普通JNI测试函数
jstring hello = "Hello from C";
//定义jstring类型变量hello值为“Hello from C”
return (*env)->NewStringUTF(env, hello);
//返回hello
}
JNIEXPORT jint JNICALL
Java_com_example_liyao_jniledctrl_MainActivity_changeLcd(
JNIEnv *env,
jobject obj,
jstring progress ) {
FILE *fp;
const char *str;
str = (*env) -> GetStringUTFChars(env ,progress, 0);
//获取Java层传入的String类型参数progress,对应JNI层的jstring类型;使用前对jstring进行转换
if ( (fp = fopen(LCD_FILE, "w")) == NULL )
//开始对LCD_FILE文件进行写操作,背光调节实际上就是对相应驱动文件的读写,只是需要通过底层来对文件进行访问
return 0;
else {
fwrite( str , sizeof(char) , strlen(str) ,fp);
//写入文件数据
fclose(fp);
return 1;
}
(*env) -> ReleaseStringUTFChars(env ,progress, (const char *)str );
//jstring转换并且使用后释放内存
}
JNIEXPORT jstring JNICALL
Java_com_example_liyao_jniledctrl_MainActivity_getLcd(
JNIEnv *env,
jobject obj/* this */) {
FILE *fp;
if ( (fp = fopen(LCD_FILE, "r")) == NULL)
//开始对LCD_FILE文件进行读操作,在这里获取驱动文件的数据,就是相应的背光亮度的数值
return NULL;
else {
char str[4];
fgets(str, 4, fp);
//按行读取
fclose(fp);
strtok(str, "\n");
//去掉str末尾的换行符
return (*env)->NewStringUTF(env, str);
//返回读取到的数值
}
}
在JNI环境中,定义了一些对象来和Java中的基本类型相匹配,如jboolean、jchar、jbyte等。可以很容易查阅到这些对应关系。
4、JNI环境的理解
JNIEnv是代表JNI环境的结构体:
struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv; //C++中JNIEnv的类型
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv; //C中JNIEnv的类型
typedef const struct JNIInvokeInterface* JavaVM;
#endif
JNI的定义区分了C和C++。C++中的JNIEnv是_JNIENv结构体:
struct _JNIEnv {
const struct JNINativeInterface* functions;
#if defined(_cplusplus)
jint GetVersion() {
return functions->GetVersion(this);
}
jclass DefineClass(const char *name, jobject loader, const jbyte* buf, jsize bufLen) {
return functions->DefineClass(this, name, loader, buf, bufLen);
}
jclass FindClass(const char* name){
return functions->FindClass(this, name);
}
......
#endif
}
其中,functions是指向结构体JNINativeInterface的指针,而C中JNIEnv的类型就是JNINativeInterface* :
struct JNINativeInterface {
void* reserved0;
void* reserved1;
void* reserved2;
void* reserved3;
jint (*GetVersion)(JNIEnv *);
jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize);
jclass (*FindClass)(JNIEnv*, const char*);
}
结构体中定义的都是函数指针,而通过这些定义,JNI层事实上就获得了对DVM的引用,通过定义的这些函数指针,能够定位到虚拟机中的JNI函数表,从而实现JNI层在DVM中的函数调用。所以可以理解为,JNIEnv是对DVM执行环境中的C/C++函数的一个引用,所以当C/C++想要在DVM中调用函数的时候,由于其是在DVM的环境中,所以它们必须通过JNIEnv* 这个参数来获得这些方法,之后才可以使用。
二、Makefile
完成了native层代码的工作,完善背光调节UI层的逻辑,这里PopupWindow来实现背光调节栏的显示,以及简单的事件和Toast。在完成以上内容以后,开始将代码部署到Android7.0的源码目录结构下,首先开始编写native-lib.c的Android.mk文件,也就是Makefile脚本文件。可以理解为,Makefile就是实现代码自动化编译的脚本,按照某种语法进行编写,说明如何编译各个源文件并且连接生成可执行文件。并要求定义源文件之间的依赖关系。
1、native-lib.c的Android.mk
#每个Android.mk文件必须以LOCAL_PATH开始,用于在开发tree中查找源文件。宏my-dir则由Build System提供,返回包含Android.mk的目录路径。
LOCAL_PATH:= $(call my-dir)
#CLEAR_VARS变量由Build System提供。并指向一个指定的GNU Makefile,由它负责清理很多LOCAL_xxx,但不清理LOCAL_PATH。
#这个清理动作是必须的,因为所有的编译控制文件由同一个GNU Make解析和执行,其变量是全局的。所以清理后才能避免相互影响。
include $(CLEAR_VARS)
#LOCAL_MODULE模块必须定义,以表示Android.mk中的每一个模块。名字必须唯一且不包含空格。
LOCAL_MODULE:=jniledctrl
#LOCAL_SRC_FILES变量必须包含将要打包如模块的C/C++ 源码。不必列出头文件,build System 会自动帮我们找出依赖文件。
LOCAL_SRC_FILES:= \
native-lib.c
#要链接到本模块的动态库。
LOCAL_SHARED_LIBRARIES :=
#要链接到本模块的静态库。
LOCAL_STATIC_LIBRARIES :=
# Also need the JNI headers. 一个可选的path列表。相对于NDK ROOT 目录。编译时,将会把这些目录附上。
LOCAL_C_INCLUDES += \
$(JNI_H_INCLUDE)
#BUILD_SHARED_LIBRARY:是Build System提供的一个变量,指向一个GNU Makefile Script。
#它负责收集自从上次调用include$(CLEAR_VARS)后的所有LOCAL_XXX信息。并决定编译为动态库。
include $(BUILD_SHARED_LIBRARY)
2、JniLedCtrl的Android.mk
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
#call all-subdir-java-files 包含要打包的Java源码,如果没有就向子目录深入。
LOCAL_SRC_FILES := $(call all-subdir-java-files)
#指定APP应用名称。
LOCAL_PACKAGE_NAME := JniLedCtrl
#定义了要包含的so库文件的名字。
LOCAL_JNI_SHARED_LIBRARIES := jniledctrl
#指定该APK项目所需的SDK版本。
LOCAL_SDK_VERSION := current
#编译打包成APK文件。
include $(BUILD_PACKAGE)
3、对Makefile主要内容的理解
make命令执行时,需要一个Makefile文件,用以告诉make命令要怎么样来编译,make命令会自动智能地根据当前文件的修改情况来确定哪些文件需要重新编译从而自己编译所需要的文件和链接目标程序。
Makefile的规则,即Makefile的核心内容:
target ... : prerequisites ...
command
...
...
target:是一个目标文件,可以是ObjectFile,也可以是可执行文件,还可以是标签(Label)。
prerequisites:生成target所需要的文件或者目标。
command:就是make需要执行的命令。(任意的shell命令)
这是一个文件的依赖关系,也就是说,target这一个或多个目标文件依赖于prerequisites中的文件,其生成规则定义在command中。也就是说,prerequisites中的文件有一个以上的文件比target文件要新的话,command所定义的命令就会执行,这就是Makefile的规则,也就是Makefile的核心内容。
在默认方式下,我们只输入make命令:
1)make会在当前目录下寻找名字叫“makefile”或者“Makefile”的文件。
2)如果找到,它会找文件中的第一个目标文件(target),并把这个文件作为最终的目标文件。
3)如果目标文件不存在,或者目标文件所依赖的后面的依赖文件修改时间要比目标文件新,那么它将会执行后面所定义的命令(command)来生成目标文件。
4)如果目标文件的依赖文件存在,那么它将把当前依赖文件作为新的目标文件在当前文件中寻找它的依赖性,如果找到就根据规则生成文件。这类似一个堆栈的过程。
5)所有依赖文件存在,就会完成make的终极任务,生成最终的目标文件。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在寻找的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错;而对于所定义的命令的错误,或是编译不成功,make不会理会,它只管文件的依赖性,即如果在我找了依赖关系以后冒号后面的文件还是不存在那我就不工作了。
三、SELinux/SEAndroid
在做JNI背光调节应用练习的过程中,降低难度使用了命令: setenforce 0
这是一个和SELinux相关的操作,它的作用是将SELinux的模式设置为宽容模式(permissive)。关于SELinux的三种模式:
enforcing:强制模式,代表 SELinux 运作中,且已经正确的开始限制 domain/type 了;
permissive:宽容模式:代表 SELinux 运作中,不过仅会有警告讯息并不会实际限制domain/type的存取,这种模式可以运来作为SELinux的debug之用;
disabled:关闭,SELinux 并没有实际运作。
而SEAndroid是Google在Android 4.4上正式推出的一套以SELinux为基础于核心的系统安全机制。
1、DAC和MAC
DAC:自主访问控制(Discretionary Access Control)机制,Linux传统的安全机制,主要对各分组权限访问进行控制,分为用户,同组用户,其他用户三组,只能控制这三组的读写和执行权限。
MAC:强势访问控制(Mandatory Access Control)机制,它将系统中的资源分为密级和类别进行控制,只有明确授权可以访问的资源,用户才能够访问。
MAC是在在DAC之外,设计的一个新的安全模型,它的处世哲学非常简单:即任何进程想在SELinux系统中干任何事情,都必须先在安全策略配置文件中赋予权限。Linux系统先做DAC检查。如果没有通过DAC权限检查,则操作直接失败。通过DAC检查之后,再做MAC权限检查。所以关于SELinux/SEAndroid的目标是看懂现有的安全策略文件也要编写符合特定需求的安全策略文件。
2、SELinux Policy语言
编写安全策略文件的一套规则。
SELinux中,每种东西都会被赋予一个安全属性,Security Context,安全上下文。
安全上下文的格式:
USER:ROLE:TYPE[:LEVEL]
其主要有四部分组成,第四项为可选项。
USER:指定用户,常用的3种用户类型,user_u(普通用户,限制权限),system_u(系统级别进程),root(root用户)。
ROLE:指定角色,不同的角色有不同的权限。文件,目录,socket等客体角色通常是object_r,主体进程的角色通常是r。一个用户通常有多个角色,但同一时间只能使用一个。
TYPE:定义客体和主体所属类型。对于进程而言,它的type也就是domain。类型是安全上下文最重要的内容。
LEVEL:定义安全等级,只用于MLS(将安全策略和实现分离出来的安全模型)策略中,可能值是s0-s15。
查看进程的安全上下文:
adb shell
ps -Z
LABEL USER PID PPID VSIZE RSS WCHAN PC NAME
u:r:init:s0 root 1 0 24488 2216 SyS_epoll_ 0008e54c S /init
u:r:kernel:s0 root 2 0 0 0 kthreadd 00000000 S kthreadd
u:r:kernel:s0 root 3 2 0 0 smpboot_th 00000000 S ksoftirqd/0
u:r:kernel:s0 root 5 2 0 0 worker_thr 00000000 S kworker/0:0H
u:r:kernel:s0 root 7 2 0 0 rcu_gp_kth 00000000 S rcu_preempt
...
查看文件的安全上下文:
adb shell
ls -Z:
u:object_r:cgroup:s0 acct
u:object_r:rootfs:s0 bugreports
u:object_r:cache_file:s0 cache
u:object_r:rootfs:s0 charger
u:object_r:configfs:s0 config
...
3、访问规则
SELinux还定义了主体对客体访问规则,这种访问规则一般存放在te文件中。目前SELinux策略语言支持4种类型的访问规则:
allow:表示允许主体对客体执行指定的操作。
dontaudit:表示不记录某条违反规则的决策信息。
auditallow:记录决策信息,通常SELinux只记录失败的决策信息,应用这条规则后也会记录成功的决策信息。
neverallow:表示不允许主体对客体执行指定的操作。
访问规则语法:
allow source_type target_type:class Permission
源类型(source type):通常是某种属性为domain的类型,代表主体。
目标类型(target type):允许访问的客体的类型,目标类型可以同时指定多个。
客体类别(object class):对客体目标类型进行限制,目标类型可能涵盖file,dir,socket等多个类别,如果将class设置为file,则主体只拥有访问客体file的权限。
许可(permission):主体可以对客体执行的操作的种类。
//SEAndroid中的安全策略文件policy.conf
allow zygote init:process sigchld;
#允许zygote域中的进程向init type的进程(Object Class为process)发送sigchld信号
allow zygote appdomain:dir { getattr search };
允许zygote域中的进程search或getattr类型为appdomain的目录。注意,多个perm_set用{}括起来
...
1)SEAndroid不允许时,log记录在哪里?
SEAndroid的审计不通过时,log记录在dmesg中,dmesg是kernel的log,如果想要获取该log,可以使用如下命令:
adb shell su -c dmesg ----- 获取kernel log
2)查看SEAndroid不允许的log
SEAndroid审计不通过的log,带有"avc:" 所以,用如下命令即可搜集到审计不同的log:
adb shell su -c dmesg | grep 'avc:' ---- 得到审计不通过的log信息
...
<5> type=1400 audit: avc: denied { read write } for pid=177 comm="rmt_storage" name="mem" dev="tmpfs" ino=6004 scontext=u:r:rmt:s0 tcontext=u:object_r:kmem_device:s0 tclass=chr_file
...
不被允许的操作是:read 和write
访问者是:u:r:rmt:s0
被访问者是:u:object_r:kmem_device:s0
操作对象是:chr_file
相当于在sepolicy 策略语言中,缺乏这样的语句:allow rmt kmem_device:chr_file {read write}
使用audit2allow工具生成log中提示的缺少的策略语言:
sudo apt-get install policycoreutils
adb shell su -c dmesg | audit2allow
3)sepolicy中加上这样的策略语句消除错误。