深入 Android syscall 实现:内联汇编系统调用 + NDK 汇编构建

版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/

什么是系统调用 (syscall)

系统调用是操作系统提供给应用程序的一组接口,允许用户空间程序与内核进行交互。

在 Android(基于 Linux 内核)中,系统调用由 软中断 实现,通常通过 svc 指令(在 ARM 架构中)触发。系统调用会将 CPU 从用户模式切换到内核模式,使得程序可以执行更高权限的操作。

Android 使用的 C 库是 Bionic,它是为移动设备优化的轻量级 C 库。对应的模块为 libc.so

Bionic 提供了对系统调用的封装。大多数标准库函数(如 printf、malloc、pthread_create)都通过 Bionic 实现,底层调用了相应的系统调用。

在 NDK 目录中可以找到相关的系统调用号定义头文件。例如

arduino 复制代码
<NDK_PATH>\27.1.12297006\toolchains\llvm\prebuilt\windows-x86_64\sysroot\usr\include\asm-generic\unistd.h

搜索 bionic 模块 可以找到不同CPU架构下的 syscall 实现

在 bionic/libc/arch-arm64/syscalls/ 可以找到 Android 中所有系统调用的汇编代码文件

cs.android.com/android/pla...

syscall 在 Android 上的应用场景

系统工具和调试:如 strace、lsof 等工具,通过 syscall 获取系统状态。

安全与反调试:某些安全检测和反调试技术会直接使用 syscall 绕过标准的 libc 函数,以防止被 hook。

嵌入式开发:在一些嵌入式系统中,开发者需要直接控制硬件,这时通常会使用 syscall。

如何在 Android 中使用 syscall

假设我们希望通过 syscall 直接读取文件内容,编写 native 方法代码如下

arduino 复制代码
// 引入必要的头文件
#include <jni.h>
#include <string>
#include <fcntl.h>          // 文件控制定义(如 O_RDONLY)
#include <unistd.h>         // 系统调用号(如 __NR_openat)
#include <sys/syscall.h>    // 系统调用函数
#include <android/log.h>

#define LOG_TAG "syscall-lib.cpp"

// 定义 Android 日志宏,用于输出信息级别日志
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

// 使用 extern "C" 告诉编译器按照 C 语言的方式来编译和链接这个函数
extern "C"
JNIEXPORT jstring JNICALL
Java_com_cyrus_example_syscall_SyscallActivity_readFileWithSyscall(JNIEnv *env, jobject,
                                                                   jstring path) {
    // 将 Java 字符串 (jstring) 转换为 C 字符串 (const char *)
    const char *filePath = env->GetStringUTFChars(path, nullptr);

    // 使用 syscall 系统调用打开文件
    // __NR_openat 是 openat() 系统调用的调用号
    // AT_FDCWD 表示使用当前工作目录
    // O_RDONLY 表示以只读方式打开文件
    int fd = syscall(__NR_openat, AT_FDCWD, filePath, O_RDONLY);

    // 如果文件打开失败,返回错误信息
    if (fd < 0) {
        // 释放通过 GetStringUTFChars 分配的资源
        env->ReleaseStringUTFChars(path, filePath);
        return env->NewStringUTF("Failed to open file");
    }

    // 定义一个缓冲区,用于存储文件内容
    char buffer[1024];

    // 使用 syscall 系统调用读取文件内容
    // __NR_read 是 read() 系统调用的调用号
    // 读取的内容存储到 buffer 中,最多读取 sizeof(buffer) - 1 字节
    ssize_t bytesRead = syscall(__NR_read, fd, buffer, sizeof(buffer) - 1);

    // 如果读取失败,返回错误信息
    if (bytesRead < 0) {
        // 关闭文件描述符
        syscall(__NR_close, fd);

        // 释放通过 GetStringUTFChars 分配的资源
        env->ReleaseStringUTFChars(path, filePath);
        return env->NewStringUTF("Failed to read file");
    }

    // 使用 syscall 系统调用关闭文件
    syscall(__NR_close, fd);

    // 释放通过 GetStringUTFChars 分配的资源
    env->ReleaseStringUTFChars(path, filePath);

    // 确保缓冲区以 '\0' 结尾(C 字符串需要以 '\0' 作为结束符)
    buffer[bytesRead] = '\0';

    // 输出读取到的文件内容到控制台
    printf("File content: %s\n", buffer);

    // 将读取到的文件内容转换为 Java 字符串 (jstring) 并返回
    return env->NewStringUTF(buffer);
}

代码中用到的系统调用号 __NR_openat 对应的的 openat 方法签名如下

arduino 复制代码
int openat(int dirfd, const char *pathname, int flags, ... /* mode_t mode */ );

具体可参考 Linux 手册:man7.org/linux/man-p...

调用 native 方法读取文件并显示文件内容

kotlin 复制代码
package com.cyrus.example.syscall

import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.cyrus.example.R

class SyscallActivity : AppCompatActivity() {

    // 加载 native 库
    init {
        System.loadLibrary("syscall-lib")
    }

    external fun readFileWithSyscall(path: String): String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_syscall)

        findViewById<Button>(R.id.button_syscall).setOnClickListener {
            // 指定文件路径
            val filePath = "/data/local/tmp/test.txt"

            // 调用 native 方法读取文件内容
            val fileContent = readFileWithSyscall(filePath)

            // 显示 Toast
            Toast.makeText(this, fileContent, Toast.LENGTH_SHORT).show()
        }
    }

}

配置 CMakeLists.txt

perl 复制代码
cmake_minimum_required(VERSION 3.4.1)

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the NDK library that you want CMake to locate.
        log)

add_library( # 设置库的名称
        syscall-lib

        # 设置库的类型
        SHARED

        # 设置源文件路径
        syscall-lib.cpp)

target_link_libraries( # 将 log 库链接到目标库
        syscall-lib
        ${log-lib})

进入 adb shell,创建文件 test.txt,并输入内容为 hello syscall

javascript 复制代码
adb shell

wayne:/ # cd /data/local/tmp/
wayne:/data/local/tmp # echo "hello syscall" > test.txt

调用测试

内联汇编实现 syscall

直接内联汇编实现 syscall 调用可以让你跳过标准库的封装层,隐藏 syscall 调用,防 hook 增加逆向难度。

1. 找到 syscall 的汇编代码

把 Bionic 模块对应的 libc.so 拉取到本地

ruby 复制代码
// 32 位库
adb pull /system/lib/libc.so  

// 64 位库
adb pull /system/lib64/libc.so
// 或
adb pull /apex/com.android.runtime/lib64/bionic/libc.so  

用 IDA 打开 libc.so,在 Functions 窗口搜索 syscall

得到 syscall 的汇编代码如下

ini 复制代码
MOV             R12, SP            // R12 = SP (保存栈指针)
PUSH            {R4-R7}           // 保存 R4-R7 到栈
MOV             R7, R0            // R7 = 系统调用号
MOV             R0, R1            // R0 = 第一个参数
MOV             R1, R2            // R1 = 第二个参数
MOV             R2, R3            // R2 = 第三个参数
LDM             R12, {R3-R6}      // R3-R6 = 额外参数 (从栈中加载)
SVC             0                 // 触发系统调用
POP             {R4-R7}           // 恢复 R4-R7
CMN             R0, #0x1000       // 检查返回值 (是否小于 0)
BXLS            LR                // 成功则返回调用地址

由于我们内联汇编 syscall 需要和原来的汇编保持一致,不需要编译器自动生成的函数入口代码和退出代码,所有需要用到 "裸函数"(naked function)。

2. 裸函数(naked function)

在 C 和 C++ 编程中,attribute((naked)) 是 GCC(GNU Compiler Collection)和 Clang 编译器提供的一个属性,用于定义一个 "裸函数"(naked function)。

裸函数是一种特殊的函数,它允许你直接控制函数的汇编指令,而不会自动为你生成函数的入口代码(如保存寄存器、调整栈指针)和退出代码(恢复寄存器、恢复栈指针)。

attribute((naked)) 提供了一种方式,让开发者完全掌控函数的汇编指令布局,而不受编译器默认生成的代码影响。它适合在对性能要求极高或者需要直接操作硬件的情况下使用,例如系统调用、中断处理程序和上下文切换函数。

基本语法

scss 复制代码
__attribute__((naked)) void myFunction() {
    // 手动编写汇编指令
}

3. 编写内联汇编代码

swift 复制代码
__attribute__((naked)) long raw_syscall(long __number, ...) {
    __asm__ __volatile__(
            "MOV             R12, SP\n"
            "PUSH            {R4-R7}\n"
            "MOV             R7, R0\n"
            "MOV             R0, R1\n"
            "MOV             R1, R2\n"
            "MOV             R2, R3\n"
            "LDM             R12, {R3-R6}\n"
            "SVC             0\n"
            "POP             {R4-R7}\n"
            "mov             pc, lr");
}

4. 读取文件内容并返回 kotlin 层调用

c 复制代码
// 读取文件内容
std::string read_file(const char *filePath) {
    char buffer[1024] = {0};

    // 调用 raw_syscall 打开文件
    int fd = raw_syscall(SYS_openat, 0, filePath, O_RDONLY, 0);
    if (fd < 0) {
        return "Failed to open file";
    }

    // 调用 raw_syscall 读取文件
    ssize_t bytesRead = raw_syscall(SYS_read, fd, buffer, sizeof(buffer) - 1);
    if (bytesRead < 0) {
        raw_syscall(SYS_close, fd);
        return "Failed to read file";
    }

    // 关闭文件
    raw_syscall(SYS_close, fd);

    // 输出读取到的文件内容到控制台
    LOGI("File content: %s\n", buffer);

    return std::string(buffer);
}


extern "C"
JNIEXPORT jstring JNICALL
Java_com_cyrus_example_syscall_SyscallActivity_readFileWithAssemblySyscall(JNIEnv *env, jobject,
                                                                           jstring path) {
    // 将 Java 字符串 (jstring) 转换为 C 字符串 (const char *)
    const char *filePath = env->GetStringUTFChars(path, nullptr);

    std::string file_content = read_file(filePath);

    // 释放通过 GetStringUTFChars 分配的资源
    env->ReleaseStringUTFChars(path, filePath);

    return env->NewStringUTF(file_content.c_str());
}

CMakeLists.txt 加载汇编文件

按上面的方法找到 ARM64 和 AMR 的 syscall 的汇编代码。

在 cpp 目录 创建 syscall64.s (ARM64)汇编代码文件,定义 raw_syscall 汇编函数

scala 复制代码
    .text                      // 表示接下来的代码段是可执行代码段
    .global raw_syscall        // 将 `raw_syscall` 设为全局符号,使其可以被其他文件引用
    .type raw_syscall, @function // 指定 `raw_syscall` 是一个函数

raw_syscall:
        // 将第一个参数 (系统调用号) 传递给 X8 寄存器
        MOV             X8, X0    // X8 = X0, 系统调用号存储在 X8 中

        // 将其余的参数从 X1-X6 依次向前移动一位 (为系统调用准备参数)
        MOV             X0, X1    // X0 = X1, 系统调用的第一个参数
        MOV             X1, X2    // X1 = X2, 系统调用的第二个参数
        MOV             X2, X3    // X2 = X3, 系统调用的第三个参数
        MOV             X3, X4    // X3 = X4, 系统调用的第四个参数
        MOV             X4, X5    // X4 = X5, 系统调用的第五个参数
        MOV             X5, X6    // X5 = X6, 系统调用的第六个参数

        // 使用 SVC 指令触发系统调用 (Supervisor Call)
        SVC             0         // 发起系统调用,中断进入内核态执行

        RET                      // 返回

在 cpp 目录 创建 syscall32.s (ARM)汇编代码文件,定义 raw_syscall 汇编函数

vbnet 复制代码
    .text
    .global raw_syscall
    .type raw_syscall,%function

raw_syscall:
        MOV             R12, SP
        STMFD           SP!, {R4-R7}
        MOV             R7, R0
        MOV             R0, R1
        MOV             R1, R2
        MOV             R2, R3
        LDMIA           R12, {R3-R6}
        SVC             0
        LDMFD           SP!, {R4-R7}
        mov             pc, lr

在 C++ 代码文件中添加 raw_syscall 函数声明

arduino 复制代码
extern "C" long raw_syscall(long __number, ...);

配置 CMakeLists.txt

perl 复制代码
# 启用 C 和汇编语言的支持
enable_language(C ASM)

# 根据系统处理器架构选择不同的汇编文件
if (CMAKE_SYSTEM_PROCESSOR MATCHES "aarch")  # 检查当前系统是否为 AArch64 (ARM 64-bit) 架构
    # 为 `syscall64.s` 设置编译标志
    # `-x assembler-with-cpp` 表示使用 C 预处理器来编译汇编文件
    set_source_files_properties(syscall64.s PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp")

    # 添加一个共享库 (Shared Library)
    add_library(
            syscall-lib          # 设置库的名称为 `syscall-lib`

            SHARED               # 指定库的类型为共享库

            syscall64.s          # 添加 ARM64 汇编源文件

            syscall-lib.cpp      # 添加 C++ 源文件
    )

    # 如果系统处理器架构为 ARM (ARM 32-bit)
elseif (CMAKE_SYSTEM_PROCESSOR MATCHES "arm")
    # 为 `syscall32.s` 设置编译标志
    # `-x assembler-with-cpp` 表示使用 C 预处理器来编译汇编文件
    set_source_files_properties(syscall32.s PROPERTIES COMPILE_FLAGS "-x assembler-with-cpp")

    # 添加一个共享库 (Shared Library)
    add_library(
            syscall-lib          # 设置库的名称为 `syscall-lib`

            SHARED               # 指定库的类型为共享库

            syscall32.s          # 添加 ARM 32 位汇编源文件

            syscall-lib.cpp      # 添加 C++ 源文件
    )
endif ()

调用 raw_syscall

c 复制代码
// 读取文件内容
std::string read_file(const char *filePath) {
    char buffer[1024] = {0};

    // 调用 raw_syscall 打开文件
    int fd = raw_syscall(SYS_openat, 0, filePath, O_RDONLY, 0);
    if (fd < 0) {
        return "Failed to open file";
    }

    // 调用 raw_syscall 读取文件
    ssize_t bytesRead = raw_syscall(SYS_read, fd, buffer, sizeof(buffer) - 1);
    if (bytesRead < 0) {
        raw_syscall(SYS_close, fd);
        return "Failed to read file";
    }

    // 关闭文件
    raw_syscall(SYS_close, fd);

    // 输出读取到的文件内容到控制台
    LOGI("File content: %s\n", buffer);

    return std::string(buffer);
}


extern "C"
JNIEXPORT jstring JNICALL
Java_com_cyrus_example_syscall_SyscallActivity_readFileWithAssemblySyscall(JNIEnv *env, jobject,
                                                                           jstring path) {
    // 将 Java 字符串 (jstring) 转换为 C 字符串 (const char *)
    const char *filePath = env->GetStringUTFChars(path, nullptr);

    std::string file_content = read_file(filePath);

    // 释放通过 GetStringUTFChars 分配的资源
    env->ReleaseStringUTFChars(path, filePath);

    return env->NewStringUTF(file_content.c_str());
}

最后,运行测试

源码

完整源码地址:github.com/CYRUS-STUDI...

相关推荐
Sugobet39 分钟前
【安卓][Mac/Windows】永久理论免费 无限ip代理池 - 适合临时快速作战
android·tcp/ip·macos·网络安全·渗透测试·ip代理池·接入点
fatiaozhang95275 小时前
创维智能融合终端SK-M424_S905L3芯片_2+8G_安卓9_线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
来来走走5 小时前
Flutter开发 了解Scaffold
android·开发语言·flutter
哆啦A梦的口袋呀7 小时前
Android 底层实现基础
android
闻道且行之7 小时前
Android Studio下载及安装配置
android·ide·android studio
alexhilton7 小时前
初探Compose中的着色器RuntimeShader
android·kotlin·android jetpack
小墙程序员7 小时前
kotlin元编程(二)使用 Kotlin 来生成源代码
android·kotlin·android studio
小墙程序员8 小时前
kotlin元编程(一)一文理解 Kotlin 反射
android·kotlin·android studio
fatiaozhang95279 小时前
创维智能融合终端DT741_移动版_S905L3芯片_安卓9_线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
小林学Android11 小时前
Android四大组件之Activity详解
android