關(guān)鍵詞:JNI Crash,異常檢測(cè)遵绰,信號(hào)量捕獲
在Android JNI開發(fā)中,經(jīng)常會(huì)遇到JNI崩潰的問題增淹,尤其帶代碼量大椿访,或者嵌入了第三方代碼的情況下,很難進(jìn)行問題定位和處理虑润。本文將介紹兩種常見的JNI崩潰處理方法成玫,包括:
- 每個(gè)JNI調(diào)用后進(jìn)行異常檢測(cè)處理(適用于JNI代碼量很小的情況)
- 捕獲系統(tǒng)崩潰的Signal,并進(jìn)行異常處理(適用于JNI代碼量大拳喻,難以每句話后面都進(jìn)行異常檢測(cè)的情況)
下面將分別介紹兩種方法:
方法一:ExceptionCheck機(jī)制
首先需要理解的是哭当,JNI沒有try...catch...finally機(jī)制,不能利用這種方法將整段的代碼進(jìn)行異常捕獲冗澈。
在JNI調(diào)用中钦勘,如果發(fā)生異常,程序并不會(huì)停止執(zhí)行亚亲,而是繼續(xù)執(zhí)行下一句代碼彻采,直到崩潰發(fā)生。正確的處理方法是在每一句JNI調(diào)用后面都通過ExceptionCheck函數(shù)手動(dòng)檢測(cè)是否發(fā)生了異常捌归,如果檢測(cè)到異常肛响,進(jìn)行異常處理。如下:
JNIEXPORT jint JNICALL Java_jack_com_jniexceptionhandler_Calculate_jniDivide
(JNIEnv * env, jobject jobj, jint m, jint n) {
char* a = NULL;
int val1 = a[1] - '0';
// 每句jni執(zhí)行之后都加入異常檢查
if (checkExc(env)) {
LOGE("jni exception happened at p0");
JNU_ThrowByName(env, "java/lang/Exception", "exception from jni: jni exception happened at p0");
return -1;
}
char* b = NULL;
int val2 = b[1] - '0';
// 每句jni執(zhí)行之后都加入異常檢查
if (checkExc(env)) {
LOGE("jni exception happened at p1");
JNU_ThrowByName(env, "java/lang/Exception", "exception from jni: jni exception happened at p1");
return -1;
}
return val1/val2;
}
這里在每次JNI調(diào)用之后都要檢測(cè)是否發(fā)生了異常惜索,檢測(cè)函數(shù)checkExec實(shí)現(xiàn)如下:
int checkExc(JNIEnv *env) {
if(env->ExceptionCheck()) {
env->ExceptionDescribe(); // writes to logcat
env->ExceptionClear();
return 1;
}
return -1;
}
如果檢測(cè)到異常特笋,可以在JNI層將異常拋出到Java層進(jìn)行處理,JNI代碼如下:
void JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg)
{
// 查找異常類
jclass cls = env->FindClass(name);
/* 如果這個(gè)異常類沒有找到巾兆,VM會(huì)拋出一個(gè)NowClassDefFoundError異常 */
if (cls != NULL) {
env->ThrowNew(cls, msg); // 拋出指定名字的異常
}
/* 釋放局部引用 */
env->DeleteLocalRef(cls);
}
這樣猎物,JNI拋出的異常就可以在Java層通過Try...Catch捕獲,并進(jìn)行相應(yīng)的出錯(cuò)提示臼寄,Java層代碼如下:
public static int callJniDivide(int input1, int input2) {
try {
return jniDivide(input1, input2);
} catch (Exception e) {
Log.e("JniExceptionHandler", e.toString());
return -1;
}
}
這種方法適用于JNI代碼完全可控霸奕,并且體量比較小的情況,也就是你可以預(yù)測(cè)到哪些JNI語句可能會(huì)導(dǎo)致異常吉拳,從而在這些語句后面加入異常檢測(cè)和處理质帅。對(duì)于代碼量大,或者JNI里使用了三方代碼的情況留攒,這種異常檢測(cè)的方法很難實(shí)施煤惩,因?yàn)檫@種情況下你可能沒法找出所有可能出異常的點(diǎn),或者你壓根兒不清楚三方庫的代碼邏輯炼邀,也就不能準(zhǔn)確找出插入異常檢測(cè)代碼段的地方魄揉。這時(shí)候可以使用下面我們要介紹的方法二:信號(hào)量捕獲機(jī)制。
方法二:信號(hào)量捕獲機(jī)制
信號(hào)量捕獲機(jī)制是建立在Linux系統(tǒng)底層的信號(hào)機(jī)制之上的方法拭宁,系統(tǒng)層會(huì)在發(fā)生崩潰的時(shí)候發(fā)送一些特定信號(hào)洛退,通過捕獲并處理這些特定信號(hào)瓣俯,我們就能夠避免JNI crash的發(fā)生,從而相對(duì)優(yōu)雅的結(jié)束程序的執(zhí)行兵怯,缺點(diǎn)是我們只知道JNI代碼發(fā)生了崩潰彩匕,沒有辦法知道具體是哪句代碼導(dǎo)致了崩潰。不過當(dāng)面對(duì)龐大復(fù)雜的JNI代碼時(shí)媒区,利用信號(hào)量捕獲無法預(yù)知的崩潰驼仪,從而避免Crash的發(fā)生,也是非常有意義的袜漩。
下面首先介紹一些Linux信號(hào)量機(jī)制的原理和基本的操作方法绪爸。這里有兩個(gè)知識(shí)點(diǎn),一是如何捕獲特定的信號(hào)量宙攻,二是如何實(shí)現(xiàn)代碼的控制點(diǎn)跳轉(zhuǎn)奠货。
基礎(chǔ)知識(shí)一:信號(hào)量機(jī)制
Signal是傳遞到進(jìn)程(Process)的軟件中斷信號(hào),操作系統(tǒng)通過信號(hào)向一個(gè)正在執(zhí)行的程序報(bào)告一些預(yù)期的狀況座掘,比如引用了無效的地址仇味,或者報(bào)告一個(gè)異步事件的完成。
GNU C庫定義了一系列的信號(hào)類型雹顺,有些信號(hào)標(biāo)志著程序無法繼續(xù)正常執(zhí)行丹墨,這些信號(hào)就會(huì)終止程序執(zhí)行,另外一些信號(hào)則可以默認(rèn)忽略掉嬉愧。
如果你的程序有可能觸發(fā)信號(hào)贩挣,那你可以定義一個(gè)handler,當(dāng)信號(hào)發(fā)生時(shí)没酣,調(diào)用這個(gè)handker代碼進(jìn)行處理王财。
一個(gè)進(jìn)程可以向另外一個(gè)進(jìn)程發(fā)送信號(hào),這使得父進(jìn)程可以終止子進(jìn)程裕便,或者兩個(gè)相關(guān)聯(lián)的進(jìn)程可以通過發(fā)送信號(hào)實(shí)現(xiàn)交流和同步绒净。
生成信號(hào)的事件(event)可以歸納為三個(gè)類型:錯(cuò)誤,外部事件偿衰,或者顯示的請(qǐng)求挂疆。
Signal生成(generated)之后變成pending狀態(tài),通常pending很短的時(shí)間之后就會(huì)發(fā)送到訂閱了這個(gè)信號(hào)的進(jìn)程下翎,但是如果這個(gè)Signal被阻塞(blocked)了的話缤言,那就可以長(zhǎng)時(shí)間處于pending狀態(tài),直到取消阻塞(unblock)视事。一旦取消阻塞胆萧,信號(hào)就會(huì)立即被發(fā)送出去。
收到信號(hào)后通常有三種處理俐东,忽略這個(gè)信號(hào)跌穗、采取默認(rèn)的動(dòng)作订晌、或者定義一個(gè)handler進(jìn)行處理。通過signal或者sigaction函數(shù)可以定義進(jìn)行處理的handler蚌吸,我們稱之為handler捕獲了這個(gè)信號(hào)腾仅。
標(biāo)準(zhǔn)的信號(hào)分為七個(gè)類別,包括:
Program Error Signals
Termination Signals
Alarm Signals
Asynchronous I/O Signals
Job Control Signals
Operation Error Signals等
其中我們主要關(guān)注的是Program Error Signals套利。
可以使用signal或sigaction函數(shù)指定處理信號(hào)的動(dòng)作,后者較前者更靈活鹤耍,可以控制的更加細(xì)膩肉迫。
signal函數(shù)使用
sighandler_t signal (int signum, sighandler t action)
上面是signal函數(shù)的定義,第一個(gè)參數(shù)是要捕獲的信號(hào)稿黄,第二個(gè)是采取的動(dòng)作喊衫,上面講到動(dòng)作分三類:忽略、默認(rèn)動(dòng)作或者自定義的handler杆怕,分別對(duì)應(yīng)第二個(gè)參數(shù)為SIG_DFL族购、SIG_IGN或者自定義的handler函數(shù)。函數(shù)返回的是之前對(duì)這個(gè)信號(hào)設(shè)置的動(dòng)作陵珍,注意寝杖,是之前設(shè)置的動(dòng)作,不是本次設(shè)置的動(dòng)作互纯。
自定義hander函數(shù)格式如下:
void handler (int signum) { ... }
使用signal函數(shù)如下:
#include <signal.h>
void termination_handler (int signum)
{
struct temp_file *p;
for (p = temp_file_list; p; p = p->next)
unlink (p->name);
}
int main (void)
{
...
if (signal (SIGINT, termination_handler) == SIG_IGN)
signal (SIGINT, SIG_IGN);
if (signal (SIGHUP, termination_handler) == SIG_IGN)
signal (SIGHUP, SIG_IGN);
if (signal (SIGTERM, termination_handler) == SIG_IGN)
signal (SIGTERM, SIG_IGN);
...
}
在上面的代碼中瑟幕,設(shè)置新的動(dòng)作的之后,判斷舊的動(dòng)作是不是忽略(SIG_IGN)留潦,如果是的話只盹,恢復(fù)成就的動(dòng)作,也就是先設(shè)置了新的動(dòng)作兔院,如果發(fā)現(xiàn)舊的動(dòng)作是忽略殖卑,就又設(shè)置回去。
注意坊萝,在BSD信號(hào)安裝(signal)以后需要顯示的卸載孵稽,而SVID系統(tǒng)(sysv_signal)上則不需要。為了同時(shí)兼容不同的情況十偶,sigaction是個(gè)更好的選擇肛冶,應(yīng)該盡量使用sigaction。
sigaction的使用
sigaction是signal的升級(jí)版扯键,比signal函數(shù)提供更精細(xì)的控制睦袖。跟該功能相關(guān)的有一個(gè)結(jié)構(gòu)體和一個(gè)函數(shù),名稱都叫sigaction(奇怪H傩獭):
struct sigaction {
sighandler_t sa_handler馅笙; // 和signal函數(shù)一樣伦乔,這里可以是默認(rèn)(SIG_DFL), 忽略(SIG_IGN)或者一個(gè)handler函數(shù)指針
sigset_t sa_mask;// handler處理信號(hào)過程中董习,需要被阻塞的信號(hào)集合
int sa_flags烈和; // 提供了多種多樣的標(biāo)志,可以影響信號(hào)的表現(xiàn)
}皿淋;
int sigaction (int signum, const struct sigaction *restrict action, struct sigaction *restrict old-action)
在函數(shù)sigaction中招刹,用到了結(jié)構(gòu)體sigaction。
在結(jié)構(gòu)體sigaction中窝趣,sa_handler可以是默認(rèn)(SIG_DFL), 忽略(SIG_IGN)或者一個(gè)handler函數(shù)指針疯暑,這和signal用法是一樣的。sa_mask是handler處理信號(hào)過程中哑舒,需要被阻塞的信號(hào)集合妇拯,正在被處理的信號(hào)類型不需要加入到這個(gè)集合中,因?yàn)樗鼤?huì)自動(dòng)被阻塞洗鸵,只有正在被處理的信號(hào)之外的類型才需要加入阻塞信號(hào)集中越锈。
在函數(shù)sigaction中,第一個(gè)參數(shù)是要處理的信號(hào)膘滨,第二個(gè)參數(shù)就是上面提到的結(jié)構(gòu)體sigaction甘凭,里面包含了處理該信號(hào)需要執(zhí)行的動(dòng)作,和期間需要阻塞的信號(hào)集火邓。第三個(gè)參數(shù)返回的是舊的sigaction結(jié)構(gòu)體对蒲。第二第三個(gè)參數(shù)可以分別或者同事設(shè)置成NULL,表明同時(shí)設(shè)置新的動(dòng)作贡翘,查詢舊的動(dòng)作蹈矮,或者只執(zhí)行一種。
下面使用sigaction實(shí)現(xiàn)前面signal函數(shù)的動(dòng)能:
#include <signal.h>
void termination_handler (int signum)
{
struct temp_file *p;
for (p = temp_file_list; p; p = p->next)
unlink (p->name);
}
int main (void)
{
...
struct sigaction new_action, old_action;
/* Set up the structure to specify the new action. */
new_action.sa_handler = termination_handler;
sigemptyset (&new_action.sa_mask);
new_action.sa_flags = 0;
sigaction (SIGINT, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN)
sigaction (SIGINT, &new_action, NULL);
sigaction (SIGHUP, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN)
sigaction (SIGHUP, &new_action, NULL);
sigaction (SIGTERM, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN)
sigaction (SIGTERM, &new_action, NULL);
...
}
block signal的兩種方式:sigprocmask或者sigaction的sa_mask鸣驱。兩者的區(qū)別在于block發(fā)生的時(shí)機(jī):
sa_mask方式只會(huì)在handler執(zhí)行的時(shí)候block信號(hào)集中的信號(hào)泛鸟;
sigprocmask方式會(huì)block兩個(gè)sigprocmask之間代碼段執(zhí)行時(shí)的信號(hào)。
優(yōu)先使用sigprocmask和sa_mask方法踊东,代碼更簡(jiǎn)潔北滥,可讀性強(qiáng)。
兩種方式的樣例代碼如下:
使用sigprocmask來阻塞主程序關(guān)鍵代碼執(zhí)行過程中到達(dá)的信號(hào):
/* This variable is set by the SIGALRM signal handler. */
volatile sig_atomic_t flag = 0;
int main (void)
{
sigset_t block_alarm;
...
/* Initialize the signal mask. */
sigemptyset (&block_alarm);
sigaddset(&block_alarm, SIGALRM);
while (1) {
/* Check if a signal has arrived; if so, reset the flag. */
sigprocmask (SIG_BLOCK, &block_alarm, NULL);
if (flag)
{
flag = 0;
}
sigprocmask (SIG_UNBLOCK, &block_alarm, NULL);
...
}
actions-if-not-arrived
}
使用sa_mask阻塞handler函數(shù)處理過程中到達(dá)的信號(hào):
#include <signal.h>
#include <stddef.h>
void catch_stop ();
void install_handler (void)
{
struct sigaction setup_action;
sigset_t block_mask;
sigemptyset (&block_mask);
/* Block other terminal-generated signals while handler runs. */
sigaddset (&block_mask, SIGINT);
sigaddset (&block_mask, SIGQUIT);
setup_action.sa_handler = catch_stop;
setup_action.sa_mask = block_mask;
setup_action.sa_flags = 0;
sigaction (SIGTSTP, &setup_action, NULL);
}
基礎(chǔ)知識(shí)二:Non-Local Exits
Non-Local Exists指的是嵌套很深的jni代碼發(fā)生異常后沒必要一層層的進(jìn)入父函數(shù)進(jìn)行異常處理闸翅,而是可以直接跳轉(zhuǎn)到最外層指定的代碼錨點(diǎn)進(jìn)行異常處理再芋。
有兩種應(yīng)用場(chǎng)景:
場(chǎng)景一:就是上面提到的在嵌套很深的地方發(fā)生異常后,簡(jiǎn)化異常處理坚冀,直接跳到最外層進(jìn)行處理济赎。
場(chǎng)景二:特定信號(hào)的handler捕捉到Signal后,跳轉(zhuǎn)到主函數(shù)的特定代碼段進(jìn)行出錯(cuò)處理。
#include <setjmp.h>
#include <stdlib.h>
#include <stdio.h>
sigjmp_buf main_loop; // 代碼錨點(diǎn)標(biāo)志
int main (void)
{
while (1)
if (sigsetjmp (main_loop)) // 代碼錨點(diǎn)
puts ("Back at main loop....");
else
do_command ();
}
void do_command (void)
{
char buffer[128];
if (fgets (buffer, 128, stdin) == NULL)
siglongjmp (main_loop, -1); // 跳轉(zhuǎn)到錨點(diǎn)執(zhí)行代碼
else
exit (EXIT_SUCCESS);
}
利用上面的兩個(gè)知識(shí)點(diǎn)通過信號(hào)量進(jìn)行Android jni崩潰捕獲和處理
有了上面的基礎(chǔ)司训,我們就可以通過捕捉系統(tǒng)信號(hào)量進(jìn)行JNI崩潰捕獲了构捡。完整的代碼如下:
#include <signal.h>
#include <setjmp.h>
#include <pthread.h>
/*
jni捕獲異常的方法之二:捕捉系統(tǒng)崩潰信號(hào),適用于代碼量大的情況壳猜。
*/
// 定義代碼跳轉(zhuǎn)錨點(diǎn)
sigjmp_buf JUMP_ANCHOR;
volatile sig_atomic_t error_cnt = 0;
void exception_handler(int errorCode){
error_cnt += 1;
LOGE("JNI_ERROR, error code %d, cnt %d", errorCode, error_cnt);
// DO SOME CLEAN STAFF HERE...
// jump to main function to do exception process
siglongjmp(JUMP_ANCHOR, 1);
}
jint process(JNIEnv * env, jobject jobj, jint m, jint n) {
char* a = NULL;
int val1 = a[1] - '0';
char* b = NULL;
int val2 = b[1] - '0';
LOGE("val 1 %d", val1);
return val1/val2;
}
JNIEXPORT jint JNICALL Java_trio_com_jniexceptionhandler_Calculate2_jniDivide
(JNIEnv * env, jobject jobj, jint m, jint n) {
// 注冊(cè)需要捕獲的異常信號(hào)
/*
1 HUP Hangup 33 33 Signal 33
2 INT Interrupt 34 34 Signal 34
3 QUIT Quit 35 35 Signal 35
4 ILL Illegal instruction 36 36 Signal 36
5 TRAP Trap 37 37 Signal 37
6 ABRT Aborted 38 38 Signal 38
7 BUS Bus error 39 39 Signal 39
8 FPE Floating point exception 40 40 Signal 40
9 KILL Killed 41 41 Signal 41
10 USR1 User signal 1 42 42 Signal 42
11 SEGV Segmentation fault 43 43 Signal 43
12 USR2 User signal 2 44 44 Signal 44
13 PIPE Broken pipe 45 45 Signal 45
14 ALRM Alarm clock 46 46 Signal 46
15 TERM Terminated 47 47 Signal 47
16 STKFLT Stack fault 48 48 Signal 48
17 CHLD Child exited 49 49 Signal 49
18 CONT Continue 50 50 Signal 50
19 STOP Stopped (signal) 51 51 Signal 51
20 TSTP Stopped 52 52 Signal 52
21 TTIN Stopped (tty input) 53 53 Signal 53
22 TTOU Stopped (tty output) 54 54 Signal 54
23 URG Urgent I/O condition 55 55 Signal 55
24 XCPU CPU time limit exceeded 56 56 Signal 56
25 XFSZ File size limit exceeded 57 57 Signal 57
26 VTALRM Virtual timer expired 58 58 Signal 58
27 PROF Profiling timer expired 59 59 Signal 59
28 WINCH Window size changed 60 60 Signal 60
29 IO I/O possible 61 61 Signal 61
30 PWR Power failure 62 62 Signal 62
31 SYS Bad system call 63 63 Signal 63
32 32 Signal 32 64 64 Signal 64
*/
// 代碼跳轉(zhuǎn)錨點(diǎn)
if (sigsetjmp(JUMP_ANCHOR, 1) != 0) {
return -1;
}
// 注冊(cè)要捕捉的系統(tǒng)信號(hào)量
struct sigaction sigact;
struct sigaction old_action;
sigaction(SIGABRT, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigset_t block_mask;
sigemptyset(&block_mask);
sigaddset(&block_mask, SIGABRT); // handler處理捕捉到的信號(hào)量時(shí)勾徽,需要阻塞的信號(hào)
sigaddset(&block_mask, SIGSEGV); // handler處理捕捉到的信號(hào)量時(shí),需要阻塞的信號(hào)
sigemptyset(&sigact.sa_mask);
sigact.sa_flags = 0;
sigact.sa_mask = block_mask;
sigact.sa_handler = exception_handler;
sigaction(SIGABRT, &sigact, NULL); // 注冊(cè)要捕捉的信號(hào)
sigaction(SIGSEGV, &sigact, NULL); // 注冊(cè)要捕捉的信號(hào)
}
jint value = process(env, jobj, m, n);
return value;
}
利用上面的兩種方法统扳,我們就可以有的放矢的處理JNI異常了喘帚,既可以在我們預(yù)測(cè)會(huì)發(fā)生異常的地方提前進(jìn)行異常檢測(cè)和處理,又可以全局添加崩潰捕獲咒钟,作為最后的防線吹由,這樣就可以告別JNI Crash問題了。