JNI原理
引言:分析Android源码6.0的过程,一定离不开Java与C/C++代码直接的来回跳转,那么就很有必要掌握JNI,这是链接Java层和Native层的桥梁,本文涉及相关源码:
frameworks/base/core/jni/AndroidRuntime.cpp
libcore/luni/src/main/java/java/lang/System.java
libcore/luni/src/main/java/java/lang/Runtime.java
libnativehelper/JNIHelp.cpp
libnativehelper/include/nativehelper/jni.h
frameworks/base/core/java/android/os/MessageQueue.java
frameworks/base/core/jni/android_os_MessageQueue.cpp
frameworks/base/core/java/android/os/Binder.java
frameworks/base/core/jni/android_util_Binder.cpp
frameworks/base/media/java/android/media/MediaPlayer.java
frameworks/base/media/jni/android_media_MediaPlayer.cpp
JNI(Java Native Interface,Java本地接口),用于打通Java层与Native(C/C++)层。这不是Android系统所独有的,而是Java所有。众所周知,Java语言是跨平台的语言,而这跨平台的背后都是依靠Java虚拟机,虚拟机采用C/C++编写,适配各个系统,通过JNI为上层Java提供各种服务,保证跨平台性。
相信不少经常使用Java的程序员,享受着其跨平台性,可能全然不知JNI的存在。在Android平台,让JNI大放异彩,为更多的程序员所熟知,往往为了提供效率或者其他功能需求,就需要NDK开发。上一篇文章Linux系统调用(syscall)原理,介绍了打通android上层与底层kernel的枢纽syscall,那么本文的目的则是介绍打通android上层中Java层与Native的纽带JNI。
Android系统在启动启动过程中,先启动Kernel创建init进程,紧接着由init进程fork第一个横穿Java和C/C++的进程,即Zygote进程。Zygote启动过程中会AndroidRuntime.cpp
中的startVm
创建虚拟机,VM创建完成后,紧接着调用startReg
完成虚拟机中的JNI方法注册。
[–>AndroidRuntime.cpp]
int AndroidRuntime::startReg(JNIEnv* env)
{
//设置线程创建方法为javaCreateThreadEtc
androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
env->PushLocalFrame(200);
//进程NI方法的注册
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
env->PopLocalFrame(NULL);
return -1;
}
env->PopLocalFrame(NULL);
return 0;
}
register_jni_procs(gRegJNI, NELEM(gRegJNI), env)这行代码的作用就是就是循环调用gRegJNI
数组成员所对应的方法。
static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
for (size_t i = 0; i < count; i++) {
if (array[i].mProc(env) < 0) {
return -1;
}
}
return 0;
}
gRegJNI
数组,有100多个成员变量,定义在AndroidRuntime.cpp
:
static const RegJNIRec gRegJNI[] = {
REG_JNI(register_android_os_MessageQueue),
REG_JNI(register_android_os_Binder),
...
};
该数组的每个成员都代表一个类文件的jni映射,其中REG_JNI是一个宏定义,在Zygote中介绍过,该宏的作用就是调用相应的方法。
当大家在看framework层代码时,经常会看到native方法,这是往往需要查看所对应的C++方法在哪个文件,对应哪个方法?下面从一个实例出发带大家如何查看java层方法所对应的native方法位置。
当分析Android消息机制源码,遇到MessageQueue.java
中有多个native方法,比如:
private native void nativePollOnce(long ptr, int timeoutMillis);
步骤1:MessageQueue.java
的全限定名为android.os.MessageQueue.java,方法名:android.os.MessageQueue.nativePollOnce(),而相对应的native层方法名只是将点号替换为下划线,可得android_os_MessageQueue_nativePollOnce()
。Tips: nativePollOnce ==> android_os_MessageQueue_nativePollOnce()
步骤2:有了native方法,那么接下来需要知道该native方法所在那个文件。前面已经介绍过Android系统启动时就已经注册了大量的JNI方法,见AndroidRuntime.cpp的gRegJNI
数组。这些注册方法命令方式:
register_[包名]_[类名]
那么MessageQueue.java所定义的jni注册方法名应该是register_android_os_MessageQueue
,的确存在于gRegJNI数组,说明这次JNI注册过程是有开机过程完成的。 该方法在AndroidRuntime.cpp
申明为extern方法:
extern int register_android_os_MessageQueue(JNIEnv* env);
这些extern方法绝大多数位于/framework/base/core/jni/
目录,大多数情况下native文件命名方式:
[包名]_[类名].cpp
[包名]_[类名].h
Tips: MessageQueue.java ==> android_os_MessageQueue.cpp
打开android_os_MessageQueue.cpp
文件,搜索android_os_MessageQueue_nativePollOnce方法,这便找到了目标方法:
static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj,
jlong ptr, jint timeoutMillis) {
NativeMessageQueue* nativeMessageQueue = reinterpret_cast(ptr);
nativeMessageQueue->pollOnce(env, obj, timeoutMillis);
}
到这里完成了一次从Java层方法搜索到所对应的C++方法的过程。
对于native文件命名方式,有时并非[包名]_[类名].cpp
,比如Binder.java
Binder.java所对应的native文件:android_util_Binder.cpp
public static final native int getCallingPid();
根据实例(一)方式,找到getCallingPid ==> android_os_Binder_getCallingPid(),并且在AndroidRuntime.cpp中的gRegJNI数组中找到register_android_os_Binder
。
按实例(一)方式则native文名应该为android_os_Binder.cpp,可是在/framework/base/core/jni/
目录下找不到该文件,这是例外的情况。其实真正的文件名为android_util_Binder.cpp
,这就是例外,这一点有些费劲,不明白为何google要如此打破规律的命名。
static jint android_os_Binder_getCallingPid(JNIEnv* env, jobject clazz)
{
return IPCThreadState::self()->getCallingPid();
}
有人可能好奇,既然如何遇到打破常规的文件命令,怎么办?这个并不难,首先,可以尝试在/framework/base/core/jni/
中搜索,对于binder.java,可以直接搜索binder关键字,其他也类似。如果这里也找不到,可以通过grep全局搜索android_os_Binder_getCallingPid
这个方法在哪个文件。
jni存在的常见目录:
/framework/base/core/jni/
/framework/base/services/core/jni/
/framework/base/media/jni/
前面两种都是在Android系统启动之初,便已经注册过JNI所对应的方法。 那么如果程序自己定义的jni方法,该如何查看jni方法所在位置呢?下面以MediaPlayer.java为例,其包名为android.media:
public class MediaPlayer{
static {
System.loadLibrary("media_jni");
native_init();
}
private static native final void native_init();
...
}
通过static静态代码块中System.loadLibrary方法来加载动态库,库名为media_jni
, Android平台则会自动扩展成所对应的libmedia_jni.so
库。 接着通过关键字native
加在native_init方法之前,便可以在java层直接使用native层方法。
接下来便要查看libmedia_jni.so
库定义所在文件,一般都是通过Android.mk
文件定义LOCAL_MODULE:= libmedia_jni,可以采用grep或者mgrep来搜索包含libmedia_jni字段的Android.mk所在路径。
搜索可知,libmedia_jni.so位于/frameworks/base/media/jni/Android.mk。用前面实例(一)中的知识来查看相应的文件和方法名分别为:
android_media_MediaPlayer.cpp
android_media_MediaPlayer_native_init()
再然后,你会发现果然在该Android.mk所在目录/frameworks/base/media/jni/
中找到android_media_MediaPlayer.cpp文件,并在文件中存在相应的方法:
static void
android_media_MediaPlayer_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaPlayer");
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
...
}
Tips:MediaPlayer.java中的native_init方法所对应的native方法位于/frameworks/base/media/jni/
目录下的android_media_MediaPlayer.cpp
文件中的android_media_MediaPlayer_native_init
方法。
JNI作为连接Java世界和C/C++世界的桥梁,很有必要掌握。看完本文,至少能掌握在分析Android源码过程中如何查找native方法。首先要明白native方法名和文件名的命名规律,其次要懂得该如何去搜索代码。 JNI方式注册无非是Android系统启动过程中Zygote注册以及通过System.loadLibrary方式注册,对于系统启动过程注册的,可以通过查询AndroidRuntime.cpp
中的gRegJNI
是否存在对应的register方法,如果不存在,则大多数情况下是通过LoadLibrary方式来注册。
再进一步来分析,Java层与native层方法是如何注册并映射的,继续以MediaPlayer为例。
在文件MediaPlayer.java中调用System.loadLibrary("media_jni")
把libmedia_jni.so动态库加载到内存。接下来,以loadLibrary为起点展开JNI注册流程的过程分析。
[System.java]
public static void loadLibrary(String libName) {
//接下来调用Runtime方法
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
[Runtime.java]
void loadLibrary(String libraryName, ClassLoader loader) {
//loader不会空,则进入该分支
if (loader != null) {
//查找库所在路径
String filename = loader.findLibrary(libraryName);
if (filename == null) {
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
//加载库
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
//loader为空,则会进入该分支
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : mLibPaths) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
//加载库
String error = doLoad(candidate, loader);
if (error == null) {
return;//加载成功
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
真正加载的工作是由doLoad()
,该方法内部增加同步锁,保证并发时一致性。
private String doLoad(String name, ClassLoader loader) {
...
synchronized (this) {
return nativeLoad(name, loader, ldLibraryPath);
}
}
nativeLoad()这是一个native方法,再进入ART虚拟机java_lang_Runtime.cc
,再细讲就要深入剖析虚拟机内部,这里就不再往下深入了,后续博主有空再展开art虚拟机系列的文章,这里直接说结论:
dlopen
函数,打开一个so文件并创建一个handle;dlsym()
函数,查看相应so文件的JNI_OnLoad()
函数指针,并执行相应函数。总之,System.loadLibrary()的作用就是调用相应库中的JNI_OnLoad()方法。接下来说说JNI_OnLoad()过程。
[-> android_media_MediaPlayer.cpp]
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
//【见3.3】 注册JNI方法
if (register_android_media_MediaPlayer(env) < 0) {
goto bail;
}
...
}
[-> android_media_MediaPlayer.cpp]
static int register_android_media_MediaPlayer(JNIEnv *env)
{
//【见3.4】
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaPlayer", gMethods, NELEM(gMethods));
}
其中gMethods
,记录java层和C/C++层方法的一一映射关系。
static JNINativeMethod gMethods[] = {
{"prepare", "()V", (void *)android_media_MediaPlayer_prepare},
{"_start", "()V", (void *)android_media_MediaPlayer_start},
{"_stop", "()V", (void *)android_media_MediaPlayer_stop},
{"seekTo", "(I)V", (void *)android_media_MediaPlayer_seekTo},
{"_release", "()V", (void *)android_media_MediaPlayer_release},
{"native_init", "()V", (void *)android_media_MediaPlayer_native_init},
...
};
这里涉及到结构体JNINativeMethod
,其定义在jni.h
文件:
typedef struct {
const char* name; //Java层native函数名
const char* signature; //Java函数签名,记录参数类型和个数,以及返回值类型
void* fnPtr; //Native层对应的函数指针
} JNINativeMethod;
关于函数签名signature
在下一小节展开说明。
[-> AndroidRuntime.cpp]
int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
//【见3.5】
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
jniRegisterNativeMethods该方法是由Android JNI帮助类JNIHelp.cpp
来完成。
[-> JNIHelp.cpp]
extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
JNIEnv* e = reinterpret_cast(env);
scoped_local_ref c(env, findClass(env, className));
if (c.get() == NULL) {
e->FatalError("");//无法查找native注册方法
}
//【见3.6】 调用JNIEnv结构体的成员变量
if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
e->FatalError("");//native方法注册失败
}
return 0;
}
[-> jni.h]
struct _JNIEnv {
const struct JNINativeInterface* functions;
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
jint nMethods)
{ return functions->RegisterNatives(this, clazz, methods, nMethods); }
...
}
functions是指向JNINativeInterface
结构体指针,也就是将调用下面方法:
struct JNINativeInterface {
jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,jint);
...
}
再往下深入就到了虚拟机内部吧,这里就不再往下深入了。总之,这个过程完成了gMethods
数组中的方法的映射关系,比如java层的native_init()方法,映射到native层的android_media_MediaPlayer_native_init()方法。
虚拟机相关的变量中有两个非常重要的量JavaVM和JNIEnv:
JavaVM
:是指进程虚拟机环境,每个进程有且只有一个JavaVM实例JNIEnv
:是指线程上下文环境,每个线程有且只有一个JNIEnv实例,JNINativeMethod结构体中有一个字段为signature(签名),再介绍signature格式之前需要掌握各种数据类型在Java层、Native层以及签名所采用的签名格式。
Signature格式 | Java | Native |
---|---|---|
B | byte | jbyte |
C | char | jchar |
D | double | jdouble |
F | float | jfloat |
I | int | jint |
S | short | jshort |
J | long | jlong |
Z | boolean | jboolean |
V | void | void |
数组简称则是在前面添加[
:
Signature格式 | Java | Native |
---|---|---|
[B | byte[] | jbyteArray |
[C | char[] | jcharArray |
[D | double[] | jdoubleArray |
[F | float[] | jfloatArray |
[I | int[] | jintArray |
[S | short[] | jshortArray |
[J | long[] | jlongArray |
[Z | boolean[] | jbooleanArray |
对象类型简称:L+classname +;
Signature格式 | Java | Native |
---|---|---|
Ljava/lang/String; | String | jstring |
L+classname +; | 所有对象 | jobject |
[L+classname +; | Object[] | jobjectArray |
Ljava.lang.Class; | Class | jclass |
Ljava.lang.Throwable; | Throwable | jthrowable |
有了前面的铺垫,那么再来通过实例说说函数签名: (输入参数...)返回值参数
,这里用到的便是前面介绍的Signature格式。
Java函数 | 对应的签名 |
---|---|
void foo() | ()V |
float foo(int i) | (I)F |
long foo(int[] i) | ([I)J |
double foo(Class c) | (Ljava/lang/Class;)D |
boolean foo(int[] i,String s) | ([ILjava/lang/String;)Z |
String foo(int i) | (I)Ljava/lang/String; |
(一)垃圾回收对于Java开发人员来说无需关系垃圾回收,完全由虚拟机GC来负责垃圾回收,而对于JNI开发人员,对于内存释放需要谨慎处理,需要的时候申请,使用完记得释放内容,以免发生内存泄露。在JNI提供了三种Reference类型,Local Reference(本地引用), Global Reference(全局引用), Weak Global Reference(全局弱引用)。其中Global Reference如果不主动释放,则一直不会释放;对于其他两个类型的引用都是释放的可能性,那是不是意味着不需要手动释放呢?答案是否定的,不管是这三种类型的那种引用,都尽可能在某个内存不再需要时,立即释放,这对系统更为安全可靠,以减少不可预知的性能与稳定性问题。
另外,ART虚拟机在GC算法有所优化,为了减少内存碎片化问题,在GC之后有可能会移动对象内存的位置,对于Java层程序并没有影响,但是对于JNI程序可要小心了,对于通过指针来直接访问内存对象是,Dalvik能正确运行的程序,ART下未必能正常运行。
(二)异常处理Java层出现异常,虚拟机会直接抛出异常,这是需要try..catch或者继续往外throw。但是对于JNI出现异常时,即执行到JNIEnv中某个函数异常时,并不会立即抛出异常来中断程序的执行,还可以继续执行内存之类的清理工作,直到返回到Java层时才会抛出相应的异常。
另外,Dalvik
虚拟机有些情况下JNI函数出错可能返回NULL,但ART
虚拟机在出错时更多的是抛出异常。这样导致的问题就可能是在Dalvik版本能正常运行的程序,在ART虚拟机上由于没有正确处理异常而崩溃。
本文主要通过实例,基于Android 6.0源码来分析JNI原理,讲述JNI核心功能:
引言:分析Android源码的过程中,要想从上至下完全明白一行代码,往往涉及app、framework、native一直到kernel,可能迷失到代码世界,明白了系统调用原理,或许能帮你峰回路转,找到进入kernel函数的入口。本文主要讲解ARM架构相关源码:
/bionic/libc/kernel/uapi/asm-arm/asm/unistd.h
/bionic/libc/arch-arm/syscalls/kill.S
/kernel/arch/arm/kernel/calls.S
/kernel/arch/arm/include/Uapi/asm/unistd.h
/kernel/include/uapi/asm-generic/unistd.h
/kernel/include/linux/syscalls.h
/kernel/kernel/signal.c
/kernel/arch/arm/kernel/entry-common.S
/kernelarch/arm/kernel/entry-armv.S
内核提供用户空间程序与内核空间进行交互的一套标准接口,这些接口让用户态程序能受限访问硬件设备,比如申请系统资源,操作设备读写,创建新进程等。用户空间发生请求,内核空间负责执行,这些接口便是用户空间和内核空间共同识别的桥梁,这里提到两个字“受限”,是由于为了保证内核稳定性,而不能让用户空间程序随意更改系统,必须是内核对外开放的且满足权限的程序才能调用相应接口。
在用户空间和内核空间之间,有一个叫做Syscall(系统调用, system call)的中间层,是连接用户态和内核态的桥梁。这样即提高了内核的安全型,也便于移植,只需实现同一套接口即可。Linux系统,用户空间通过向内核空间发出Syscall,产生软中断,从而让程序陷入内核态,执行相应的操作。对于每个系统调用都会有一个对应的系统调用号,比很多操作系统要少很多。
安全性与稳定性:内核驻留在受保护的地址空间,用户空间程序无法直接执行内核代码,也无法访问内核数据,通过系统调用
性能:Linux上下文切换时间很短,以及系统调用处理过程非常精简,内核优化得好,所以性能上往往比很多其他操作系统执行要好。
这里以文章理解杀进程的实现原理中的kill()方法为例子,来找一找kill()方法系统调用的过程。
Tips 1: 用户空间的方法xxx
,对应系统调用层方法则是sys_xxx
;
Tips 2: unistd.h
文件记录着系统调用中断号的信息。
故用户空间kill
方法则对应系统调用层便是sys_kill
,这个方法去哪里找呢?从/kernel/include/uapi/asm-generic/unistd.h
等还有很多unistd.h
去慢慢查看,查看关键字sys_kill
,便能看到下面几行:
/* kernel/signal.c */
__SYSCALL(__NR_kill, sys_kill)
根据这个能得到一丝线索,那就是kill对应的方法sys_kill位于/kernel/signal.c
文件。
Tips 3: 宏定义SYSCALL_DEFINEx(xxx,…),展开后对应的方法则是sys_xxx
;
Tips 4: 方法参数的个数x,对应于SYSCALL_DEFINEx。
kill(int pid, int sig)
方法共两个参数,则对应方法于SYSCALL_DEFINE2(kill,...)
,进入signal.c文件,再次搜索关键字,便能看到方法:
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct siginfo info;
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER;
info.si_pid = task_tgid_vnr(current);
info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
return kill_something_info(sig, &info, pid);
}
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
基本等价于asmlinkage long sys_kill(int pid, int sig)
,这里用的是基本等价,往下看会解释原因。
比如kill命令, 有两个参数. 则可以直接在kernel目录下搜索 “SYSCALL_DEFINE2(kill”,即可直接找到,所有对应的Syscall方法位于signal.c
Syscall是通过中断方式实现的,ARM平台上通过swi中断来实现系统调用,实现从用户态切换到内核态,发送软中断swi时,从中断向量表中查看跳转代码,其中异常向量表定义在文件/kernelarch/arm/kernel/entry-armv.S(汇编语言文件)。当执行系统调用时会根据系统调用号从系统调用表中来查看目标函数的入口地址,在calls.S文件中声明了入口地址信息。
总体流程:kill() -> kill.S -> swi陷入内核态 -> 从sys_call_table查看到sys_kill -> ret_fast_syscall -> 回到用户态执行kill()下一行代码。 下面介绍部分核心流程:
3.1: 用户程序通过软中断swi指令切入内核态,执行vector_swi处的指令。vector_swi
在文件/kenel/arch/arm/kernel/entry-common.S
中定义,此处省略。像每一个异常处理程序一样,要做的第一件事当然就是保护现场了。紧接着是获得系统调用的系统调用号
3.2: 仍以kill()函数为例,来详细说说Syscall调用流程,用户空间kill()定义位于文件kill.S
。
#include
ENTRY(kill)
mov ip, r7
ldr r7, =__NR_kill
swi #0
mov r7, ip
cmn r0, #(MAX_ERRNO + 1)
bxls lr
neg r0, r0
b __set_errno_internal
END(kill)
当调用kill时, 系统先保存r7内容, 然后将__NR_kill值放入r7, 再执行swi软中断指令切换进内核态。
3.3: Linux内核中,每个Syscall都有唯一的系统调用号对应,kill的系统调用号为__NR_kill,用户空间的系统调用号定义于/bionic/libc/kernel/uapi/asm-generic/unistd.h
,如下:
#define __NR_kill (__NR_SYSCALL_BASE + 37)
其中__NR_SYSCALL_BASE=0,也就是__NR_kill系统调用号=37。
3.4: 在内核中有与系统调用号对应的系统调用表,定义在文件/kernel/arch/arm/kernel/calls.S
,如下:
/* 35 */ CALL(sys_ni_syscall) /* was sys_ftime */
CALL(sys_sync)
CALL(sys_kill) //此处为37号
CALL(sys_rename)
CALL(sys_mkdir)
到这里可知37号系统调用对应sys_kill(),该方法所对应的函数声明在syscalls.h文件
3.5: 文件/kernel/include/linux/syscalls.h
中有如下声明:
asmlinkage long sys_kill(int pid, int sig);
asmlinkage是gcc标签,代表函数读取的参数来自于栈中,而非寄存器。
sys_kill()定义在内核源码找不到直接定义,而是通过syscalls.h
文件中的SYSCALL_DEFINE宏定义。前面已经讲过sys_kill是通过语句SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
来定义,下面来一层层剖开,这条宏定义的真面目:
等价 1:
syscalls.h
中有大量如下宏定义:
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
asmlinkage long sys_##sname(void)
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
可得出原语句等价:
SYSCALL_DEFINEx(2, _kill, pid_t, pid, int, sig)
等价 2:
syscalls.h
中有如下宏定义:
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
可得出原语句等价:
SYSCALL_METADATA(_kill, 2, pid_t, pid, int, sig)
__SYSCALL_DEFINEx(2, _kill, pid_t, pid, int, sig)
define __SYSCALL_DEFINEx(x, name, …)
等价 3:
syscalls.h
中有如下宏定义:
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(SyS##name)))); \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__)); \
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))
可得出原语句等价:
asmlinkage long sys_kill(__MAP(2,__SC_DECL,__VA_ARGS__)) \
__attribute__((alias(__stringify(SyS_kill)))); \
static inline long SYSC_kill(__MAP(2,__SC_DECL,__VA_ARGS__)); \
asmlinkage long SyS_kill(__MAP(2,__SC_LONG,__VA_ARGS__)); \
asmlinkage long SyS_kill(__MAP(2,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = SYSC_kill(__MAP(2,__SC_CAST,__VA_ARGS__)); \
__MAP(2,__SC_TEST,__VA_ARGS__); \
__PROTECT(2, ret,__MAP(2,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
static inline long SYSC_kill(__MAP(2,__SC_DECL,__VA_ARGS__))
这里__VA_ARGS__
等于 pid_t, pid, int, sig
。
等价 4:
先说说这里涉及的宏定义
__MAP宏定义:
#define __MAP0(m,...)
#define __MAP1(m,t,a) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)
相关宏定义:
#define __SC_DECL(t, a) t a
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __SC_CAST(t, a) (t) a
#define __SC_ARGS(t, a) a
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))
展开:
__MAP(2,__SC_DECL, pid_t, pid, int, sig) //等价于 pid_t pid, int sig
__MAP(2,__SC_LONG,__VA_ARGS__) //等价于 long pid, long sig
__MAP(2,__SC_CAST,__VA_ARGS__) //等价于 (pid_t) pid, (int)sig
__MAP(2,__SC_ARGS,__VA_ARGS__) //等价于 pid, sig
可得出原语句等价:
//函数声明sys_kill(),并别名指向SyS_kill
asmlinkage long sys_kill(pid_t pid, int sig) __attribute__((alias(__stringify(SyS_kill))));
static inline long SYSC_kill(pid_t pid, int sig);
//函数声明SyS_kill()
asmlinkage long SyS_kill(long pid, long sig);
asmlinkage long SyS_kill(long pid, long sig)
{
long ret = SYSC_kill((pid_t) pid, (int)sig);
BUILD_BUG_ON_ZERO(sizeof(pid_t) > sizeof(long));
BUILD_BUG_ON_ZERO(sizeof(int) > sizeof(long));
__PROTECT(2, ret, pid, sig);
return ret;
}
static inline long SYSC_kill(pid_t pid, int sig)
通过以上分析过程:
sys_
前缀,声明sys_kill()函数;看到这或许很多人(包括我)会觉得诧异,为何要如此复杂呢,后来查资料,发现这是由于之前64位Linux存在CVE-2009-2009
的漏洞,简单说就是32位参数存放在64位寄存器,修改符号扩展可能导致产生一个非法内存地址,从而导致系统崩溃或者提升权限。 为了修复这个问题,把寄存器高位清零即可,但做起来比较困难,为了做尽可能少的修改,将调用参数统一采用使用long型来接收,再强转为相应参数。 窥见一斑,可见Linux大师们精湛的宏定义,已经用得出神入化。
如果觉得很复杂,那么可以忽略这个宏定义,只要记住SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
基本等价于asmlinkage long sys_kill(int pid, int sig)
就足够了。
CALL(sys_xxx)
,指定了目标函数的入口地址。#define__NR_xxx (__NR_SYSCALL_BASE+[num])
asmlinkage long sys_xxx(args ...);
SYSCALL_DEFINEx(x, sname, ...)
前面这4步都是在内核空间相关的文件定义,有了这些,那么内核就可以使用相应的系统调用号。
系统调用号的宏定义:位于文件/bionic/libc/kernel/uapi/asm-arm/asm/unistd.h,记录着用户空间的系统调用号,格式为#define__NR_xxx (__NR_SYSCALL_BASE+[num])
。这个文件就是由内核空间同名的头文件自动生成的,所以该文件与内核空间的系统调用号是完全一致。
汇编定义相关函数的中断调用过程:位于文件/bionic/libc/arch-arm/syscalls/xxx.S,比如kill()位于kill.S,格式为:
ENTRY(xxx)
mov ip, r7
ldr r7, =__NR_xxx
swi #0
mov r7, ip
cmn r0, #(MAX_ERRNO + 1)
bxls lr
neg r0, r0
b __set_errno_internal
END(xxx)
当然kill()方法还有函数声明,有了这些,用户空间也能在程序中使用系统调用。明白了这些过程,那么自己新添加系统调用其实也并不是多困难的一件事,新增系统调用号还需要修改syscalls总个数,但强烈不建议自己新增系统调用号,尽量保持与linux kernel主线一致,兼容性更好,所以就不进一步介绍新增流程了。