前记
之前我们实践了如何使用 Rust 进行 JNI 通信,步骤还是比较简单的, 详细可以参考Rust学习笔记-JNI 调用篇。
在 android 官方文档中也有相应的 Rust 开发 Native 侧的教程,也可以参考。
source.android.com/docs/setup/…
本文我们再尝试使用 Rust 来实现 Android 中针对 Linux 的信号捕获功能,业界俗称 Native 安全气囊,哈哈,我也不知道谁是第一个用安全气囊的词。
知识铺垫
众所周知, Android系统运行是基于Linux内核之上的搭载JVM实现的,所以Linux 存在一些标准信号,当发送了这些信号系统会做出相应的行为。
1. SIGHUP (1) 挂起信号。通常在终端断开连接时发送。
2. SIGINT (2) 中断信号。通常在用户按下 Ctrl+C 时发送。
3. SIGQUIT (3) 退出信号。通常在用户按下 Ctrl+\ 时发送,并生成核心转储。
4. SIGILL (4) 非法指令。执行了非法、格式错误或权限错误的指令时发送。
5. SIGTRAP (5) 跟踪陷阱。用于调试。
6. SIGABRT (6) 终止信号。通常在调用 abort 函数时发送。
7. SIGBUS (7) 总线错误。非对齐内存访问或其他内存访问错误时发送。
8. SIGFPE (8) 浮点异常。除零或其他浮点运算错误时发送。
9. SIGKILL (9) 杀死信号。强制终止进程,无法捕获或忽略。
10. SIGUSR1 (10) 用户定义信号1。用户程序可以使用的信号。
11. SIGSEGV (11) 段错误。非法内存访问时发送。
12. SIGUSR2 (12) 用户程序可以使用的信号。
13. SIGPIPE (13) 管道破裂。向无读端的管道写入数据时发送。
14. SIGALRM (14) 定时器信号。定时器到期时发送。
15. SIGTERM (15) 终止信号。请求进程终止,可以捕获和忽略。
当我们发生了一些 Native Crash 基本上都是由于上述信号所引发的。
类似 Java/Kotlin 一样,Native Crash 也具备类似 try/catch 的方式吞掉错误/异常的情况让系统可以继续运行。
注意: 并不是所有信号都可以被捕获,其中SIGKILL(9)优先级最高,所以它不可以被捕获。
基于 Rust 的捕获实现
创建工程
cargo new android_native_airbag
这里推荐使用 idea 作为Rust的开发工具,下载一个 Rust 插件支持即可,主要日常使用 Android Studio 开发,各种快捷键比较熟悉。
配置必要依赖
[dependencies]
jni = "0.21.1" // rust jni 库
log = "0.4"
android_logger = "0.13.3"
lazy_static = "1.4.0" // 懒加载
libc = "0.2.155" // libc 库
libs.rs
extern crate core;
mod native_airbag; // 核心的信号捕获逻辑
mod jni_macro; // 通用宏
mod jni_init; // JNI 初始化逻辑
mod jni_native_airbag; // JNI 触发注册信号逻辑
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
很简单的包管理,我这边工程会创建 4 个 .rs 文件,然后分别 mod 这四个文件。
mod 主要作用
-
命名空间管理:模块为代码提供命名空间,防止命名冲突。
-
代码组织:模块帮助将代码组织成逻辑上相关的部分,使代码更容易理解和维护。
-
可见性控制:通过模块可以控制代码的可见性(即代码是公共的还是私有的)。
jni_init.rs
extern crate android_logger;
extern crate log;
use std::sync::{Arc, Mutex};
use android_logger::Config;
use jni::JavaVM;
use jni::sys::{jint, JNI_VERSION_1_6};
use lazy_static::lazy_static;
use log::{info, LevelFilter};
lazy_static! {
static ref GLOBAL_JVM: Arc<Mutex<Option<JavaVM>>> = Arc::new(Mutex::new(None));
}
#[no_mangle]
pub extern "system" fn JNI_OnLoad(vm: *mut jni::sys::JavaVM, _: *mut std::ffi::c_void) -> jint {
//初始化 android log
android_logger::init_once(Config::default().with_max_level(LevelFilter::Trace));
info!("native_airbag has been initialized");
let java_vm = unsafe { JavaVM::from_raw(vm).expect("Failed to create JavaVM from raw pointer") };
{
let mut global_vm = GLOBAL_JVM.lock().unwrap();
*global_vm = Some(java_vm);
}
JNI_VERSION_1_6
}
#[no_mangle]
pub extern "system" fn JNI_OnUnload(_: *mut jni::sys::JavaVM, _: *mut std::ffi::c_void) {
// 清理全局变量
let mut global_vm = GLOBAL_JVM.lock().unwrap();
*global_vm = None;
}
同 C/C++ 一样,Rust 既可以面向过程也可以面向对象开发。
unsafe 关键字
Rust 是没有空指针的概念的,在Rust的世界中,像我们国家一样非常的安全,但是虽然安全,是因为我们边疆守卫的好,但是总是要和外界的世界进行通信,所以时刻要地方外部不安全的势力入侵,不安全的势力就类似C/C++, 当 Rust 调用第三方库的时候,它是由C/C++实现的,但是也需要对接,所以为了兼容这个场景,Rust 的世界中就开辟了一个 Unsafe 的区域,这个区域是可以不进行编译时检测的,但是在调用这部分 C/C++ 的方法时会提示该类是不安全的警告,所以你要使用 unsafe 关键字包住逻辑。
unsafe 既可以定义一个函数,也可以作为一个代码块等等,所以它的使用比较灵活。
何时使用 unsafe
以下是一些典型情况下使用 unsafe 的场景:
-
解引用原始指针(raw pointers) :原始指针(*const T 和 *mut T)不具备 Rust 的借用检查机制,因此解引用它们需要 unsafe。
-
调用不安全的函数或外部函数接口(FFI) :调用 C 库函数或其他不安全函数需要使用 unsafe。
-
访问或修改静态变量:静态可变变量需要 unsafe,因为它们可能在多个线程间共享。
-
实现不安全的 trait:某些 trait 可能需要手动保证实现的正确性。
-
内联汇编:Rust 提供了内联汇编,需要 unsafe。
在本文主题中,大量使用 unsafe 是因为需要调用 C 库函数。
回到 jni_init.rs,我们对比一下与 C/C++ 的实现有什么不同。
在 C/C++ 中,JNI_OnLoad 通常定义如下:
#include <jni.h>
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
// 初始化逻辑
return JNI_VERSION_1_6;
}
在 Rust 中,JNI_OnLoad 通常定义如下:
#[no_mangle]
pub extern "system" fn JNI_OnLoad(vm: *mut jni::sys::JavaVM, _: *mut std::ffi::c_void) -> jint {
// 初始化逻辑
JNI_VERSION_1_6
}
• #[no_mangle] :该属性用于告诉 Rust 编译器不要对函数名进行名称修饰,以便 JVM 能正确地找到并调用此函数。这相当于 C/C++ 中的 extern "C" 声明。
• pub extern "system" :pub 使函数在模块外可见,extern "system" 指定函数使用系统的调用约定,确保与 JVM 兼容。
- 参数和返回类型:
• 参数类型:
*mut jni::sys::JavaVM 和 *mut std::ffi::c_void 对应于 C/C++ 中的 JavaVM *vm 和 void *reserved。jni::sys 是 jni crate 提供的 JNI 绑定,std::ffi::c_void 是 Rust 对 C 语言中 void 的等价类型。
• 返回类型:jint,对应于 C/C++ 中的 jint,即 JNI 版本。
并不是很理解为什么要用 system 作为调用约定,可能就是确保函数在所有操作系统上都使用正确的调用约定,从而提高跨平台兼容性。
lazy_static!
顾名思义静态懒加载,我们这个场景为啥要使用这个静态懒加载呢?
我一开始也不太理解,因为更多的是面向对象开发,所以针对面向过程开发想实现多个方法共用一个field
就需要一些手段。
• 静态变量:适用于需要在整个程序生命周期内共享的全局状态,但需要注意线程安全性。
• 函数参数传递:适用于在函数之间传递共享字段,避免全局状态。
• 结构体:通过结构体封装共享字段,适用于更复杂的状态管理。
• Rc 或 Arc:适用于需要多个所有者共享字段的场景,适合单线程和多线程环境。
其他的手段我们不展开,只说一下这个静态懒加载
#[macro_export(local_inner_macros)]
macro_rules! lazy_static {
($(#[$attr:meta])* static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
// use `()` to explicitly forward the information about private items
__lazy_static_internal!($(#[$attr])* () static ref $N : $T = $e; $($t)*);
};
($(#[$attr:meta])* pub static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
__lazy_static_internal!($(#[$attr])* (pub) static ref $N : $T = $e; $($t)*);
};
($(#[$attr:meta])* pub ($($vis:tt)+) static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
__lazy_static_internal!($(#[$attr])* (pub ($($vis)+)) static ref $N : $T = $e; $($t)*);
};
() => ()
}
lazy_static 宏允许你定义静态变量,这些变量在第一次访问时被初始化,而不是在程序启动时初始化。这可以用来避免昂贵的初始化开销,尤其是当初始化依赖于运行时信息时。它支持不同的可见性修饰符,包括 pub 和具体可见性(如 pub(crate)),通过宏展开生成相应的代码,保证在多线程环境下的线程安全初始化。
所以整个 jni_init.rs 主要是将虚拟机JavaVM原始指针转换成Rust的 JavaVM 类型,以便在 Rust 中安全使用,之后将这个 Rust 的 JavaVM 类型放到静态懒加载中保存,后续供给其他的.rs使用。
jni_macro.rs
之所以创建一个这个文件,目的是将一些 JNI 类型 转换成 Rust 基本类型或者一些常用的类型。
在研究这部分的时候,由于初学,对 Rust 的 JNI 库中的一些 api 不是很熟悉,花费了很多的时间去研究类型转换。
举个例子。
我们通过在 Java 中调用一个 JNI 函数,函数的入参是一个 String 类型,然后再 Rust中定义的 JNI 方法入参的类型是jni::sys::jstring,但是又想要转换成 Rust 中的 String 类型,这个流程就比较麻烦。
#[macro_export]
macro_rules! jstring_to_string {
($env:expr, $java_string:expr) => {{
let jstring_obj = unsafe { JString::from_raw($java_string) };
let c_str = $env.get_string(&jstring_obj).expect("Couldn't get java string!");
let c_str: &std::ffi::CStr = c_str.as_ref();
let rust_string = c_str.to_str().expect("Couldn't convert CStr to &str!").to_owned();
rust_string
}};
}
我们写了一个宏,专门用于处理这个逻辑。
unsafe 块: 因为 JString::from_raw 需要处理原始指针操作,这是一个不安全的操作,所以使用 unsafe 块。
JString::from_raw: 假设这是一个将 jstring 转换为 JString 对象的方法。JString 是一个用于封装 Java 字符串的结构体,可能来自 jni crate。
$env.get_string: 通过 JNI 环境的 get_string 方法从 JString 对象中获取字符串。返回值是一个 JNIString,它封装了 JNI 中的字符串。
expect: 如果获取字符串失败,则会触发一个 panic,并输出错误信息 "Couldn't get java string!"。
as_ref: 将 JNIString 转换为 CStr 的引用。CStr 是一个在 Rust 中表示 C 风格字符串的类型。
to_str: 将 CStr 转换为 Rust 的字符串切片 &str。如果转换失败,会触发一个 panic,并输出错误信息 "Couldn't convert CStr to &str!"。
to_owned: 将字符串切片 &str 转换为拥有所有权的 String。
| JNI 类型 | Java 类型 | Rust 类型 | C/C++ 类型 |
|---|---|---|---|
jint | int | i32 | int |
jlong | long | i64 | long / int64_t |
jboolean | boolean | u8 (一般使用 jni::sys::jboolean) | unsigned char |
jbyte | byte | i8 | signed char |
jchar | char | u16 (一般使用 jni::sys::jchar) | unsigned short |
jshort | short | i16 | short |
jfloat | float | f32 | float |
jdouble | double | f64 | double |
jstring | String | *mut jni::sys::_jobject | jstring |
jobject | Object | *mut jni::sys::_jobject | jobject |
jclass | Class | *mut jni::sys::_jclass | jclass |
jarray | Array | *mut jni::sys::_jarray | jarray |
jintArray | int[] | *mut jni::sys::_jintArray | jintArray |
jlongArray | long[] | *mut jni::sys::_jlongArray | jlongArray |
jbooleanArray | boolean[] | *mut jni::sys::_jbooleanArray | jbooleanArray |
jbyteArray | byte[] | *mut jni::sys::_jbyteArray | jbyteArray |
jcharArray | char[] | *mut jni::sys::_jcharArray | jcharArray |
jshortArray | short[] | *mut jni::sys::_jshortArray | jshortArray |
jfloatArray | float[] | *mut jni::sys::_jfloatArray | jfloatArray |
jdoubleArray | double[] | *mut jni::sys::_jdoubleArray | jdoubleArray |
jobjectArray | Object[] | *mut jni::sys::_jobjectArray | jobjectArray |
JNIEnv | N/A | *mut jni::sys::JNIEnv | JNIEnv* |
JavaVM | N/A | *mut jni::sys::JavaVM | JavaVM* |
针对数据类型在三个层面都有自己的对应关系,如上表,具体的转换细节就不详细阐述了。
jni_native_airbag.rs
use jni::JNIEnv;
use jni::objects::{JClass, JIntArray, JString};
use jni::sys::{jint, jintArray, jstring};
use log::info;
use crate::jstring_to_string;
use crate::native_airbag::register_native_airbag;
#[no_mangle]
pub extern "C" fn Java_com_example_rust_RustHelper_registerNativeAirbag(mut _env: JNIEnv, _: JClass, mask_signal: jintArray, elf_name: jstring, backtrace: jstring) {
let mut env = _env;
let elf_name: String = jstring_to_string!(&mut env, elf_name);
let backtrace: String = jstring_to_string!(&mut env, backtrace);
let mask_signal_array = unsafe {
// 需要使用 from_raw 进行 转换成 JPrimitiveArray 的结构体
JIntArray::from_raw(mask_signal)
};
let size = env.get_array_length(&mask_signal_array).expect("error length");
info!("masked signal is {}", size);
info!("masked elf is {}", elf_name);
let mut mask_signal_vec: Vec<i32> = vec![0; size as usize];
// 将JPrimitiveArray类型的数组转换成Vec<i32>
env.get_int_array_region(mask_signal_array, 0, &mut mask_signal_vec).expect("Error getting array region");
unsafe { register_native_airbag(env, mask_signal_vec, elf_name, backtrace) }
}
#[no_mangle]
pub extern "C" fn Java_com_example_rust_RustHelper_sendSignal(_env: JNIEnv, _: JClass, mask_signal: jint) {
info!("receive send signal {}", mask_signal);
unsafe { libc::raise(mask_signal); }
}
针对 registerNativeAirbag() 这个 JNI 函数,由于我们需要对多个信号进行捕获,所以这边入参是一个 int型数组,这里边也需要进行类型转换。
使用 env.get_int_array_region() 函数来进行转换,转换的原理就是
通过调用 JNI 的 GetIntArrayRegion 函数,将 Java int 数组中的元素复制到 Rust 的 buf 切片中。这个过程包括:
1. 检查输入数组是否为空。
2. 准备 JNI 调用所需的参数。
3. 进行 JNI 调用,将数据从 Java 数组复制到 Rust 切片。
4. 返回操作结果。
这种方法在处理跨语言的数据传输时非常有用,确保了数据从 Java 到 Rust 的安全、高效传输。
native_airbag.rs
来到核心流程中了,我们直接先上代码:
use std::ptr::{null_mut}; // 引入标准库中的 null_mut 函数
use std::sync::Mutex; // 引入标准库中的 Mutex 结构
use jni::JNIEnv; // 引入 jni crate 中的 JNIEnv 结构
use lazy_static::lazy_static; // 引入 lazy_static 宏,用于懒加载静态变量
use libc::{calloc, raise, SA_ONSTACK, SA_RESTART, SA_SIGINFO, sigaction, sigaltstack, sigemptyset, stack_t}; // 引入 libc crate 中的多种函数和常量
use log::{error, info}; // 引入 log crate 中的 error 和 info 宏
const SIGNAL_CRASH_STACK_SIZE: usize = 1024 * 128; // 定义信号处理栈的大小为 128 KB
// 定义一个结构体,用于存储信号掩码配置
struct NativeAirBagConfig {
mask_signals: Vec<i32>, // 信号列表
}
lazy_static! {
// 定义一个懒加载的静态变量,用于存储可选的 NativeAirBagConfig,使用互斥锁保护
static ref AIR_BAG_CONFIG: Mutex<Option<NativeAirBagConfig>> = Mutex::new(None);
static ref ORIGINAL_SIGACTIONS: Mutex<Vec<(i32, sigaction)>> = Mutex::new(Vec::new());
}
// 定义一个信号处理函数
extern "C" fn sig_handler(sig: i32, _info: *mut libc::siginfo_t, _ptr: *mut libc::c_void) {
// 获取原始的信号处理器列表的克隆
let original_actions = ORIGINAL_SIGACTIONS.lock().unwrap().clone();
// 获取配置的锁
let config_guard = AIR_BAG_CONFIG.lock().unwrap();
// 如果配置存在
if let Some(ref config) = *config_guard {
// 如果捕获的信号在配置的信号列表中
if config.mask_signals.contains(&sig) {
info!("Caught signal {}...", sig);
} else {
// 如果捕获的信号不在配置的信号列表中
error!("Signal {} not caught, delegating to original handler...", sig);
// 遍历原始信号处理器列表
for (orig_sig, orig_action) in original_actions {
if orig_sig == sig {
// 使用原始处理器处理信号
unsafe { sigaction(sig, &orig_action, null_mut()); }
// 重新发送信号
unsafe { raise(sig); }
}
}
}
}
}
// 定义一个函数,用于设置信号处理栈
unsafe fn setup_signal_stack() -> Result<(), &'static str> {
// 分配信号处理栈的内存
let ss_sp = calloc(1, SIGNAL_CRASH_STACK_SIZE);
// 如果内存分配失败
if ss_sp.is_null() {
error!("Failed to allocate stack memory");
return Err("Failed to allocate stack memory");
}
// 设置信号处理栈的结构体
let ss = stack_t {
ss_sp,
ss_size: SIGNAL_CRASH_STACK_SIZE,
ss_flags: 0,
};
// 设置备用信号处理栈
if sigaltstack(&ss, null_mut()) != 0 {
error!("Failed to set alternate stack");
return Err("Failed to set alternate stack");
}
Ok(())
}
// 定义一个函数,用于设置信号处理器
unsafe fn setup_sigaction(mask_signals: &[i32]) -> Result<(), &'static str> {
// 初始化 sigaction 结构体
let mut sigc: sigaction = std::mem::zeroed();
// 设置信号处理函数
sigc.sa_sigaction = sig_handler as usize;
// 清空信号集
sigemptyset(&mut sigc.sa_mask);
// 设置信号处理标志
sigc.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_RESTART;
// 获取原始信号处理器列表的锁
let mut original_sigactions_guard = ORIGINAL_SIGACTIONS.lock().unwrap();
// 遍历需要掩码的信号列表
for &mask_signal in mask_signals {
// 初始化原始 sigaction 结构体
let mut original_sigaction: sigaction = std::mem::zeroed();
// 设置信号处理器
if sigaction(mask_signal, &sigc, &mut original_sigaction) == -1 {
error!("Failed to set signal handler, the signal is {}", mask_signal);
return Err("Failed to set signal handler");
}
// 保存原始信号处理器
original_sigactions_guard.push((mask_signal, original_sigaction));
}
Ok(())
}
// 定义一个函数,用于注册 NativeAirBag
pub unsafe fn register_native_airbag(_env: JNIEnv, mask_signals: Vec<i32>, elf_name: String, backtrace: String) {
{
// 获取配置的锁
let mut config_guard = AIR_BAG_CONFIG.lock().unwrap();
// 设置新的配置
*config_guard = Some(NativeAirBagConfig { mask_signals: mask_signals.clone() });
}
// 设置信号处理栈和信号处理器
let setup_result = setup_signal_stack().and_then(|_| setup_sigaction(&mask_signals));
// 如果设置成功
if setup_result.is_ok() {
info!("Successfully set up signal handlers");
} else {
error!("Failed to set up signal handlers");
}
}
不到 100 行代码,将基本流程搞定,唯一遗留的就是,我这边想先过滤出具体哪个 .so 以及涉及哪些 native trace 进行兜。这部分代码也比较简单,后续有时间我们补充完整。
主要的核心流程就是利用 libc 函数库的 sigaction的能力。
上述代码每一行基本都加了注释,注意几个点。
- 在处理信号时 需要开辟一个栈空间给
sigaction,防止stack over flow。 - 静态懒加载这部分,可能涉及多线程访问,需要加锁。
- 针对函数指针,在 Rust 中这个入参要强转成
usize。 sigemptyset(&mut sigc.sa_mask);作用是清空 sigc 结构体中的 sa_mask 字段,这意味着在信号处理程序执行期间,不会额外阻塞任何信号,以确保信号处理程序能够处理所有可能到来的信号,而不会因为信号屏蔽而丢失信号。通过清空信号集,可以确保信号处理程序的正常执行。- 针对回调
sig_handler函数,为什么要使用extern "C"? 因为要兼容 C 代码所以这个回调是以函数指针转 usize 的方式传递给sigaction中,需要兼容 C 库。 - Rust 的
and_then函数
#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_confusables("flat_map", "flatmap")]
pub fn and_then<U, F: FnOnce(T) -> Result<U, E>>(self, op: F) -> Result<U, E> {
match self {
Ok(t) => op(t),
Err(e) => Err(e),
}
}
and_then 方法用于对 Result 类型的值进行链式操作,如果结果是 Ok,则应用指定的闭包 op,并返回闭包的结果。如果结果是 Err,则直接返回错误。这个方法可以使代码更简洁、更易读,尤其是在处理多个可能出错的操作时。
总结
如上小例子安全气囊的 Rust 版本就差不多了, 对于 Rust 开发我们不能去死抠一些 api等等,要先从大框看起,然后通过具体实践搞起这样学的比较快(个人感觉),本人初学,如有Rust 大佬发现问题请帮忙总结出来,如果能延伸出其他未知的知识点请指出!!万分感谢。