GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

开发一个无root的Android SO动态加载与内存Dump工具

 

我将为你开发一个完整的Android APK,支持图形化选择SO文件、动态加载到内存、以及内存dump到本地文件。

项目结构

text
SOLoader/
├── app/
│   ├── src/main/
│   │   ├── java/com/soloader/
│   │   │   ├── MainActivity.java
│   │   │   ├── SOFileAdapter.java
│   │   │   ├── SOLoaderEngine.java
│   │   │   ├── MemoryDumper.java
│   │   │   ├── LoadedSOInfo.java
│   │   │   ├── LoadedSOAdapter.java
│   │   │   └── FilePickerActivity.java
│   │   ├── jni/
│   │   │   ├── Android.mk
│   │   │   ├── Application.mk
│   │   │   └── native_loader.c
│   │   ├── res/
│   │   │   ├── layout/
│   │   │   │   ├── activity_main.xml
│   │   │   │   ├── activity_file_picker.xml
│   │   │   │   ├── item_so_file.xml
│   │   │   │   ├── item_loaded_so.xml
│   │   │   │   └── dialog_memory_info.xml
│   │   │   ├── values/
│   │   │   │   ├── strings.xml
│   │   │   │   ├── colors.xml
│   │   │   │   └── styles.xml
│   │   │   └── drawable/
│   │   │       ├── bg_card.xml
│   │   │       ├── bg_button_primary.xml
│   │   │       ├── bg_button_danger.xml
│   │   │       └── bg_button_secondary.xml
│   │   └── AndroidManifest.xml
│   └── build.gradle
├── build.gradle
└── settings.gradle

1. AndroidManifest.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.soloader">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:requestLegacyExternalStorage="true"
        android:theme="@style/AppTheme">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name=".FilePickerActivity"
            android:theme="@style/AppTheme" />
    </application>
</manifest>

2. build.gradle (app)

groovy
plugins {
    id 'com.android.application'
}

android {
    compileSdkVersion 33
    
    defaultConfig {
        applicationId "com.soloader"
        minSdkVersion 24
        targetSdkVersion 33
        versionCode 1
        versionName "1.0"
        
        ndk {
            abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
        }
    }

    buildTypes {
        release {
            minifyEnabled false
        }
    }
    
    externalNativeBuild {
        ndkBuild {
            path 'src/main/jni/Android.mk'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.9.0'
    implementation 'androidx.recyclerview:recyclerview:1.3.1'
    implementation 'androidx.cardview:cardview:1.0.0'
}

3. JNI Native代码

jni/Android.mk

Makefile
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE    := native_loader
LOCAL_SRC_FILES := native_loader.c
LOCAL_LDLIBS    := -llog -ldl
LOCAL_CFLAGS    := -Wall -Wextra
include $(BUILD_SHARED_LIBRARY)

jni/Application.mk

Makefile
APP_ABI := arm64-v8a armeabi-v7a x86 x86_64
APP_PLATFORM := android-24
APP_STL := c++_static

jni/native_loader.c

C
#include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <android/log.h>

#define TAG "NativeLoader"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)

#define MAX_LOADED_LIBS 64

typedef struct {
    void* handle;
    char path[512];
    unsigned long base_addr;
    unsigned long size;
    int is_loaded;
} LoadedLib;

static LoadedLib loaded_libs[MAX_LOADED_LIBS];
static int lib_count = 0;

// 从 /proc/self/maps 中解析SO的基地址和大小
static int find_lib_in_maps(const char* lib_name, unsigned long* base, unsigned long* end) {
    FILE* fp = fopen("/proc/self/maps", "r");
    if (!fp) {
        LOGE("Cannot open /proc/self/maps: %s", strerror(errno));
        return -1;
    }

    char line[1024];
    *base = 0;
    *end = 0;
    int found = 0;

    while (fgets(line, sizeof(line), fp)) {
        if (strstr(line, lib_name)) {
            unsigned long start, e;
            if (sscanf(line, "%lx-%lx", &start, &e) == 2) {
                if (!found) {
                    *base = start;
                    found = 1;
                }
                *end = e;  // 持续更新end以获取最大范围
            }
        }
    }

    fclose(fp);

    if (found) {
        LOGI("Found %s: base=0x%lx, end=0x%lx, size=%lu",
             lib_name, *base, *end, *end - *base);
        return 0;
    }

    return -1;
}

// 加载SO文件
JNIEXPORT jlong JNICALL
Java_com_soloader_SOLoaderEngine_nativeLoadSO(JNIEnv* env, jobject thiz, jstring path) {
    const char* lib_path = (*env)->GetStringUTFChars(env, path, NULL);
    if (!lib_path) {
        LOGE("Failed to get path string");
        return 0;
    }

    LOGI("Loading SO: %s", lib_path);

    // 使用 dlopen 加载
    void* handle = dlopen(lib_path, RTLD_NOW);
    if (!handle) {
        LOGE("dlopen failed: %s", dlerror());
        (*env)->ReleaseStringUTFChars(env, path, lib_path);
        return 0;
    }

    LOGI("SO loaded successfully, handle=%p", handle);

    // 查找基地址
    unsigned long base = 0, end = 0;
    
    // 提取文件名用于在maps中搜索
    const char* filename = strrchr(lib_path, '/');
    if (filename) {
        filename++;
    } else {
        filename = lib_path;
    }

    if (find_lib_in_maps(filename, &base, &end) == 0) {
        if (lib_count < MAX_LOADED_LIBS) {
            loaded_libs[lib_count].handle = handle;
            strncpy(loaded_libs[lib_count].path, lib_path, sizeof(loaded_libs[lib_count].path) - 1);
            loaded_libs[lib_count].base_addr = base;
            loaded_libs[lib_count].size = end - base;
            loaded_libs[lib_count].is_loaded = 1;
            lib_count++;
        }
    }

    (*env)->ReleaseStringUTFChars(env, path, lib_path);
    return (jlong)(uintptr_t)handle;
}

// 卸载SO文件
JNIEXPORT jboolean JNICALL
Java_com_soloader_SOLoaderEngine_nativeUnloadSO(JNIEnv* env, jobject thiz, jlong handle) {
    void* h = (void*)(uintptr_t)handle;
    
    for (int i = 0; i < lib_count; i++) {
        if (loaded_libs[i].handle == h && loaded_libs[i].is_loaded) {
            int ret = dlclose(h);
            if (ret == 0) {
                loaded_libs[i].is_loaded = 0;
                LOGI("SO unloaded: %s", loaded_libs[i].path);
                return JNI_TRUE;
            } else {
                LOGE("dlclose failed: %s", dlerror());
                return JNI_FALSE;
            }
        }
    }

    // 如果没在记录中找到,直接尝试关闭
    int ret = dlclose(h);
    return ret == 0 ? JNI_TRUE : JNI_FALSE;
}

// 获取SO的基地址
JNIEXPORT jlong JNICALL
Java_com_soloader_SOLoaderEngine_nativeGetBaseAddress(JNIEnv* env, jobject thiz, jlong handle) {
    void* h = (void*)(uintptr_t)handle;
    
    for (int i = 0; i < lib_count; i++) {
        if (loaded_libs[i].handle == h && loaded_libs[i].is_loaded) {
            return (jlong)loaded_libs[i].base_addr;
        }
    }
    
    return 0;
}

// 获取SO的内存大小
JNIEXPORT jlong JNICALL
Java_com_soloader_SOLoaderEngine_nativeGetSOSize(JNIEnv* env, jobject thiz, jlong handle) {
    void* h = (void*)(uintptr_t)handle;
    
    for (int i = 0; i < lib_count; i++) {
        if (loaded_libs[i].handle == h && loaded_libs[i].is_loaded) {
            return (jlong)loaded_libs[i].size;
        }
    }
    
    return 0;
}

// Dump内存到文件
JNIEXPORT jboolean JNICALL
Java_com_soloader_MemoryDumper_nativeDumpMemory(JNIEnv* env, jobject thiz,
    jlong address, jlong size, jstring output_path) {
    
    const char* out_path = (*env)->GetStringUTFChars(env, output_path, NULL);
    if (!out_path) {
        LOGE("Failed to get output path");
        return JNI_FALSE;
    }

    unsigned long addr = (unsigned long)address;
    unsigned long dump_size = (unsigned long)size;

    LOGI("Dumping memory: addr=0x%lx, size=%lu, output=%s", addr, dump_size, out_path);

    FILE* fp = fopen(out_path, "wb");
    if (!fp) {
        LOGE("Cannot create output file: %s, error: %s", out_path, strerror(errno));
        (*env)->ReleaseStringUTFChars(env, output_path, out_path);
        return JNI_FALSE;
    }

    // 逐页读取,跳过不可读的页
    unsigned long page_size = sysconf(_SC_PAGESIZE);
    unsigned long current = addr;
    unsigned long remaining = dump_size;
    unsigned long written = 0;
    unsigned long skipped = 0;

    while (remaining > 0) {
        unsigned long chunk = remaining < page_size ? remaining : page_size;
        
        // 检查内存是否可读 - 通过尝试读取 mincore 或直接尝试
        // 使用 mincore 检查页面是否映射
        unsigned char vec;
        unsigned long aligned_addr = current & ~(page_size - 1);
        
        // 尝试直接写入,如果失败则写入零
        // 使用信号处理来捕获 SIGSEGV 太复杂,改用 /proc/self/mem
        FILE* mem_fp = fopen("/proc/self/mem", "rb");
        if (mem_fp) {
            if (fseeko(mem_fp, (off_t)current, SEEK_SET) == 0) {
                unsigned char* buf = (unsigned char*)malloc(chunk);
                if (buf) {
                    size_t read_bytes = fread(buf, 1, chunk, mem_fp);
                    if (read_bytes == chunk) {
                        fwrite(buf, 1, chunk, fp);
                        written += chunk;
                    } else {
                        // 不可读,写零
                        memset(buf, 0, chunk);
                        fwrite(buf, 1, chunk, fp);
                        skipped += chunk;
                    }
                    free(buf);
                }
            }
            fclose(mem_fp);
        }

        current += chunk;
        remaining -= chunk;
    }

    fclose(fp);

    LOGI("Dump complete: written=%lu, skipped=%lu", written, skipped);

    (*env)->ReleaseStringUTFChars(env, output_path, out_path);
    return JNI_TRUE;
}

// Dump指定SO的内存
JNIEXPORT jboolean JNICALL
Java_com_soloader_MemoryDumper_nativeDumpSO(JNIEnv* env, jobject thiz,
    jlong handle, jstring output_path) {
    
    void* h = (void*)(uintptr_t)handle;
    
    for (int i = 0; i < lib_count; i++) {
        if (loaded_libs[i].handle == h && loaded_libs[i].is_loaded) {
            return Java_com_soloader_MemoryDumper_nativeDumpMemory(
                env, thiz,
                (jlong)loaded_libs[i].base_addr,
                (jlong)loaded_libs[i].size,
                output_path);
        }
    }
    
    LOGE("SO handle not found in loaded list");
    return JNI_FALSE;
}

// 获取maps信息
JNIEXPORT jstring JNICALL
Java_com_soloader_MemoryDumper_nativeGetMapsInfo(JNIEnv* env, jobject thiz, jstring filter) {
    const char* filter_str = NULL;
    if (filter != NULL) {
        filter_str = (*env)->GetStringUTFChars(env, filter, NULL);
    }

    FILE* fp = fopen("/proc/self/maps", "r");
    if (!fp) {
        if (filter_str) (*env)->ReleaseStringUTFChars(env, filter, filter_str);
        return (*env)->NewStringUTF(env, "Cannot read /proc/self/maps");
    }

    // 动态分配结果缓冲区
    size_t buf_size = 65536;
    char* result = (char*)malloc(buf_size);
    if (!result) {
        fclose(fp);
        if (filter_str) (*env)->ReleaseStringUTFChars(env, filter, filter_str);
        return (*env)->NewStringUTF(env, "Memory allocation failed");
    }
    result[0] = '\0';
    size_t used = 0;

    char line[1024];
    while (fgets(line, sizeof(line), fp)) {
        if (filter_str == NULL || strlen(filter_str) == 0 || strstr(line, filter_str)) {
            size_t line_len = strlen(line);
            if (used + line_len + 1 >= buf_size) {
                buf_size *= 2;
                char* new_buf = (char*)realloc(result, buf_size);
                if (!new_buf) break;
                result = new_buf;
            }
            strcat(result + used, line);
            used += line_len;
        }
    }

    fclose(fp);
    if (filter_str) (*env)->ReleaseStringUTFChars(env, filter, filter_str);

    jstring jresult = (*env)->NewStringUTF(env, result);
    free(result);
    return jresult;
}

// 按地址范围dump(自定义范围)
JNIEXPORT jboolean JNICALL
Java_com_soloader_MemoryDumper_nativeDumpRange(JNIEnv* env, jobject thiz,
    jlong start_addr, jlong end_addr, jstring output_path) {
    
    if (end_addr <= start_addr) {
        LOGE("Invalid range: start=0x%lx, end=0x%lx", (unsigned long)start_addr, (unsigned long)end_addr);
        return JNI_FALSE;
    }

    jlong size = end_addr - start_addr;
    return Java_com_soloader_MemoryDumper_nativeDumpMemory(env, thiz, start_addr, size, output_path);
}

// 查找符号
JNIEXPORT jlong JNICALL
Java_com_soloader_SOLoaderEngine_nativeFindSymbol(JNIEnv* env, jobject thiz,
    jlong handle, jstring symbol_name) {
    
    void* h = (void*)(uintptr_t)handle;
    const char* sym = (*env)->GetStringUTFChars(env, symbol_name, NULL);
    if (!sym) return 0;

    dlerror(); // 清除错误
    void* addr = dlsym(h, sym);
    const char* err = dlerror();
    
    if (err) {
        LOGE("dlsym failed for '%s': %s", sym, err);
        (*env)->ReleaseStringUTFChars(env, symbol_name, sym);
        return 0;
    }

    LOGI("Symbol '%s' found at %p", sym, addr);
    (*env)->ReleaseStringUTFChars(env, symbol_name, sym);
    return (jlong)(uintptr_t)addr;
}

// 获取已加载库的数量
JNIEXPORT jint JNICALL
Java_com_soloader_SOLoaderEngine_nativeGetLoadedCount(JNIEnv* env, jobject thiz) {
    int count = 0;
    for (int i = 0; i < lib_count; i++) {
        if (loaded_libs[i].is_loaded) count++;
    }
    return count;
}

4. Java类

LoadedSOInfo.java

Java
package com.soloader;

public class LoadedSOInfo {
    private String filePath;
    private String fileName;
    private long handle;
    private long baseAddress;
    private long memorySize;
    private long loadTime;
    private boolean isLoaded;

    public LoadedSOInfo(String filePath, long handle, long baseAddress, long memorySize) {
        this.filePath = filePath;
        this.handle = handle;
        this.baseAddress = baseAddress;
        this.memorySize = memorySize;
        this.loadTime = System.currentTimeMillis();
        this.isLoaded = true;

        int lastSlash = filePath.lastIndexOf('/');
        this.fileName = lastSlash >= 0 ? filePath.substring(lastSlash + 1) : filePath;
    }

    public String getFilePath() { return filePath; }
    public String getFileName() { return fileName; }
    public long getHandle() { return handle; }
    public long getBaseAddress() { return baseAddress; }
    public long getMemorySize() { return memorySize; }
    public long getLoadTime() { return loadTime; }
    public boolean isLoaded() { return isLoaded; }
    public void setLoaded(boolean loaded) { isLoaded = loaded; }

    public String getBaseAddressHex() {
        return String.format("0x%X", baseAddress);
    }

    public String getMemorySizeFormatted() {
        if (memorySize < 1024) return memorySize + " B";
        else if (memorySize < 1024 * 1024) return String.format("%.2f KB", memorySize / 1024.0);
        else return String.format("%.2f MB", memorySize / (1024.0 * 1024.0));
    }

    public String getHandleHex() {
        return String.format("0x%X", handle);
    }
}

SOLoaderEngine.java

Java
package com.soloader;

import android.util.Log;

public class SOLoaderEngine {
    private static final String TAG = "SOLoaderEngine";
    private static boolean isNativeLoaded = false;

    static {
        try {
            System.loadLibrary("native_loader");
            isNativeLoaded = true;
            Log.i(TAG, "Native loader library loaded successfully");
        } catch (UnsatisfiedLinkError e) {
            Log.e(TAG, "Failed to load native_loader: " + e.getMessage());
        }
    }

    public static boolean isReady() {
        return isNativeLoaded;
    }

    /**
     * 加载SO文件
     * @param path SO文件的完整路径
     * @return LoadedSOInfo 如果成功,null如果失败
     */
    public LoadedSOInfo loadSO(String path) {
        if (!isNativeLoaded) {
            Log.e(TAG, "Native loader not ready");
            return null;
        }

        try {
            long handle = nativeLoadSO(path);
            if (handle == 0) {
                Log.e(TAG, "Failed to load SO: " + path);
                return null;
            }

            long baseAddr = nativeGetBaseAddress(handle);
            long size = nativeGetSOSize(handle);

            LoadedSOInfo info = new LoadedSOInfo(path, handle, baseAddr, size);
            Log.i(TAG, String.format("Loaded: %s, handle=0x%X, base=0x%X, size=%d",
                    path, handle, baseAddr, size));

            return info;
        } catch (Exception e) {
            Log.e(TAG, "Exception loading SO: " + e.getMessage());
            return null;
        }
    }

    /**
     * 卸载SO文件
     */
    public boolean unloadSO(LoadedSOInfo info) {
        if (!isNativeLoaded || info == null) return false;

        boolean result = nativeUnloadSO(info.getHandle());
        if (result) {
            info.setLoaded(false);
        }
        return result;
    }

    /**
     * 查找符号
     */
    public long findSymbol(long handle, String symbolName) {
        if (!isNativeLoaded) return 0;
        return nativeFindSymbol(handle, symbolName);
    }

    // Native方法声明
    private native long nativeLoadSO(String path);
    private native boolean nativeUnloadSO(long handle);
    private native long nativeGetBaseAddress(long handle);
    private native long nativeGetSOSize(long handle);
    private native long nativeFindSymbol(long handle, String symbolName);
    private native int nativeGetLoadedCount();
}

MemoryDumper.java

Java
package com.soloader;

import android.os.Environment;
import android.util.Log;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class MemoryDumper {
    private static final String TAG = "MemoryDumper";
    private static final String DUMP_DIR = "SOLoader_Dumps";

    /**
     * Dump已加载SO的内存到文件
     */
    public String dumpSO(LoadedSOInfo info) {
        if (info == null || !info.isLoaded()) {
            Log.e(TAG, "Invalid SO info or SO not loaded");
            return null;
        }

        String outputPath = generateDumpPath(info.getFileName());
        boolean result = nativeDumpSO(info.getHandle(), outputPath);

        if (result) {
            Log.i(TAG, "SO dumped to: " + outputPath);
            return outputPath;
        } else {
            Log.e(TAG, "Failed to dump SO");
            return null;
        }
    }

    /**
     * Dump指定地址范围的内存
     */
    public String dumpRange(long startAddr, long endAddr, String name) {
        String outputPath = generateDumpPath(name != null ? name : "range_dump");
        boolean result = nativeDumpRange(startAddr, endAddr, outputPath);

        if (result) {
            Log.i(TAG, "Range dumped to: " + outputPath);
            return outputPath;
        } else {
            Log.e(TAG, "Failed to dump range");
            return null;
        }
    }

    /**
     * Dump指定地址和大小的内存
     */
    public String dumpMemory(long address, long size, String name) {
        String outputPath = generateDumpPath(name != null ? name : "mem_dump");
        boolean result = nativeDumpMemory(address, size, outputPath);

        if (result) {
            Log.i(TAG, "Memory dumped to: " + outputPath);
            return outputPath;
        } else {
            Log.e(TAG, "Failed to dump memory");
            return null;
        }
    }

    /**
     * 获取进程的maps信息
     */
    public String getMapsInfo(String filter) {
        return nativeGetMapsInfo(filter);
    }

    /**
     * 生成dump文件路径
     */
    private String generateDumpPath(String baseName) {
        File dumpDir = new File(Environment.getExternalStorageDirectory(), DUMP_DIR);
        if (!dumpDir.exists()) {
            dumpDir.mkdirs();
        }

        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault());
        String timestamp = sdf.format(new Date());

        // 清理文件名
        String cleanName = baseName.replaceAll("[^a-zA-Z0-9._-]", "_");

        return new File(dumpDir, cleanName + "_" + timestamp + ".bin").getAbsolutePath();
    }

    public static String getDumpDirectory() {
        File dumpDir = new File(Environment.getExternalStorageDirectory(), DUMP_DIR);
        return dumpDir.getAbsolutePath();
    }

    // Native方法声明
    private native boolean nativeDumpMemory(long address, long size, String outputPath);
    private native boolean nativeDumpSO(long handle, String outputPath);
    private native boolean nativeDumpRange(long startAddr, long endAddr, String outputPath);
    private native String nativeGetMapsInfo(String filter);
}

FilePickerActivity.java

Java
package com.soloader;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class FilePickerActivity extends AppCompatActivity {
    public static final String RESULT_FILE_PATH = "selected_file_path";

    private RecyclerView recyclerView;
    private TextView tvCurrentPath;
    private File currentDir;
    private SOFileAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file_picker);

        recyclerView = findViewById(R.id.rv_files);
        tvCurrentPath = findViewById(R.id.tv_current_path);

        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        adapter = new SOFileAdapter(new SOFileAdapter.OnFileClickListener() {
            @Override
            public void onFileClick(File file) {
                if (file.isDirectory()) {
                    navigateTo(file);
                } else if (file.getName().endsWith(".so")) {
                    Intent result = new Intent();
                    result.putExtra(RESULT_FILE_PATH, file.getAbsolutePath());
                    setResult(Activity.RESULT_OK, result);
                    finish();
                }
            }
        });

        recyclerView.setAdapter(adapter);

        // 返回上级目录按钮
        findViewById(R.id.btn_go_up).setOnClickListener(v -> {
            if (currentDir != null && currentDir.getParentFile() != null) {
                navigateTo(currentDir.getParentFile());
            }
        });

        // 导航到常用目录
        findViewById(R.id.btn_sdcard).setOnClickListener(v ->
                navigateTo(Environment.getExternalStorageDirectory()));

        findViewById(R.id.btn_data).setOnClickListener(v ->
                navigateTo(new File("/data/local/tmp")));

        findViewById(R.id.btn_app_dir).setOnClickListener(v ->
                navigateTo(getFilesDir()));

        // 默认从SDCard开始
        navigateTo(Environment.getExternalStorageDirectory());
    }

    private void navigateTo(File dir) {
        if (dir == null || !dir.exists()) {
            Toast.makeText(this, "目录不存在或无权限访问", Toast.LENGTH_SHORT).show();
            return;
        }

        currentDir = dir;
        tvCurrentPath.setText(dir.getAbsolutePath());

        File[] files = dir.listFiles();
        List<File> fileList = new ArrayList<>();

        if (files != null) {
            for (File f : files) {
                if (f.isDirectory() || f.getName().endsWith(".so")) {
                    fileList.add(f);
                }
            }
        }

        // 排序:目录优先,然后按名称
        Collections.sort(fileList, (a, b) -> {
            if (a.isDirectory() && !b.isDirectory()) return -1;
            if (!a.isDirectory() && b.isDirectory()) return 1;
            return a.getName().compareToIgnoreCase(b.getName());
        });

        adapter.setFiles(fileList);
    }
}

SOFileAdapter.java

Java
package com.soloader;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class SOFileAdapter extends RecyclerView.Adapter<SOFileAdapter.ViewHolder> {

    public interface OnFileClickListener {
        void onFileClick(File file);
    }

    private List<File> files = new ArrayList<>();
    private OnFileClickListener listener;

    public SOFileAdapter(OnFileClickListener listener) {
        this.listener = listener;
    }

    public void setFiles(List<File> files) {
        this.files = files;
        notifyDataSetChanged();
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_so_file, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        File file = files.get(position);
        holder.tvName.setText(file.getName());

        if (file.isDirectory()) {
            holder.tvIcon.setText("📁");
            holder.tvSize.setText("");
        } else {
            holder.tvIcon.setText("📦");
            long size = file.length();
            if (size < 1024) holder.tvSize.setText(size + " B");
            else if (size < 1024 * 1024) holder.tvSize.setText(String.format("%.1f KB", size / 1024.0));
            else holder.tvSize.setText(String.format("%.1f MB", size / (1024.0 * 1024.0)));
        }

        holder.itemView.setOnClickListener(v -> {
            if (listener != null) listener.onFileClick(file);
        });
    }

    @Override
    public int getItemCount() {
        return files.size();
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        TextView tvIcon, tvName, tvSize;

        ViewHolder(View view) {
            super(view);
            tvIcon = view.findViewById(R.id.tv_icon);
            tvName = view.findViewById(R.id.tv_file_name);
            tvSize = view.findViewById(R.id.tv_file_size);
        }
    }
}

LoadedSOAdapter.java

Java
package com.soloader;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;
import java.util.List;

public class LoadedSOAdapter extends RecyclerView.Adapter<LoadedSOAdapter.ViewHolder> {

    public interface OnSOActionListener {
        void onDump(LoadedSOInfo info);
        void onUnload(LoadedSOInfo info);
        void onViewInfo(LoadedSOInfo info);
        void onFindSymbol(LoadedSOInfo info);
    }

    private List<LoadedSOInfo> loadedList = new ArrayList<>();
    private OnSOActionListener listener;

    public LoadedSOAdapter(OnSOActionListener listener) {
        this.listener = listener;
    }

    public void addItem(LoadedSOInfo info) {
        loadedList.add(info);
        notifyItemInserted(loadedList.size() - 1);
    }

    public void removeItem(LoadedSOInfo info) {
        int index = loadedList.indexOf(info);
        if (index >= 0) {
            loadedList.remove(index);
            notifyItemRemoved(index);
        }
    }

    public void updateItem(LoadedSOInfo info) {
        int index = loadedList.indexOf(info);
        if (index >= 0) {
            notifyItemChanged(index);
        }
    }

    public List<LoadedSOInfo> getLoadedList() {
        return loadedList;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_loaded_so, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        LoadedSOInfo info = loadedList.get(position);

        holder.tvFileName.setText(info.getFileName());
        holder.tvBaseAddr.setText("Base: " + info.getBaseAddressHex());
        holder.tvSize.setText("Size: " + info.getMemorySizeFormatted());
        holder.tvHandle.setText("Handle: " + info.getHandleHex());

        boolean loaded = info.isLoaded();
        holder.tvStatus.setText(loaded ? "● 已加载" : "○ 已卸载");
        holder.tvStatus.setTextColor(loaded ? 0xFF4CAF50 : 0xFFFF5722);

        holder.btnDump.setEnabled(loaded);
        holder.btnUnload.setEnabled(loaded);
        holder.btnSymbol.setEnabled(loaded);

        holder.btnDump.setOnClickListener(v -> {
            if (listener != null) listener.onDump(info);
        });

        holder.btnUnload.setOnClickListener(v -> {
            if (listener != null) listener.onUnload(info);
        });

        holder.btnInfo.setOnClickListener(v -> {
            if (listener != null) listener.onViewInfo(info);
        });

        holder.btnSymbol.setOnClickListener(v -> {
            if (listener != null) listener.onFindSymbol(info);
        });
    }

    @Override
    public int getItemCount() {
        return loadedList.size();
    }

    static class ViewHolder extends RecyclerView.ViewHolder {
        TextView tvFileName, tvBaseAddr, tvSize, tvHandle, tvStatus;
        Button btnDump, btnUnload, btnInfo, btnSymbol;

        ViewHolder(View view) {
            super(view);
            tvFileName = view.findViewById(R.id.tv_loaded_name);
            tvBaseAddr = view.findViewById(R.id.tv_base_addr);
            tvSize = view.findViewById(R.id.tv_mem_size);
            tvHandle = view.findViewById(R.id.tv_handle);
            tvStatus = view.findViewById(R.id.tv_status);
            btnDump = view.findViewById(R.id.btn_dump);
            btnUnload = view.findViewById(R.id.btn_unload);
            btnInfo = view.findViewById(R.id.btn_view_info);
            btnSymbol = view.findViewById(R.id.btn_find_symbol);
        }
    }
}

MainActivity.java

Java
package com.soloader;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.Settings;
import android.text.method.ScrollingMovementMethod;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class MainActivity extends AppCompatActivity {
    private static final int REQUEST_PICK_FILE = 1001;
    private static final int REQUEST_PERMISSIONS = 1002;
    private static final int REQUEST_MANAGE_STORAGE = 1003;

    private SOLoaderEngine loaderEngine;
    private MemoryDumper memoryDumper;
    private RecyclerView rvLoaded;
    private LoadedSOAdapter loadedAdapter;
    private TextView tvLog;
    private ScrollView scrollLog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        loaderEngine = new SOLoaderEngine();
        memoryDumper = new MemoryDumper();

        initViews();
        requestPermissions();
    }

    private void initViews() {
        // 加载按钮
        Button btnLoad = findViewById(R.id.btn_load_so);
        btnLoad.setOnClickListener(v -> openFilePicker());

        // 查看Maps按钮
        Button btnMaps = findViewById(R.id.btn_view_maps);
        btnMaps.setOnClickListener(v -> showMapsDialog());

        // 自定义地址Dump按钮
        Button btnCustomDump = findViewById(R.id.btn_custom_dump);
        btnCustomDump.setOnClickListener(v -> showCustomDumpDialog());

        // 清除日志按钮
        Button btnClearLog = findViewById(R.id.btn_clear_log);
        btnClearLog.setOnClickListener(v -> tvLog.setText(""));

        // 日志区域
        tvLog = findViewById(R.id.tv_log);
        tvLog.setMovementMethod(new ScrollingMovementMethod());
        scrollLog = findViewById(R.id.scroll_log);

        // 已加载SO列表
        rvLoaded = findViewById(R.id.rv_loaded);
        rvLoaded.setLayoutManager(new LinearLayoutManager(this));

        loadedAdapter = new LoadedSOAdapter(new LoadedSOAdapter.OnSOActionListener() {
            @Override
            public void onDump(LoadedSOInfo info) {
                performDump(info);
            }

            @Override
            public void onUnload(LoadedSOInfo info) {
                performUnload(info);
            }

            @Override
            public void onViewInfo(LoadedSOInfo info) {
                showSOInfoDialog(info);
            }

            @Override
            public void onFindSymbol(LoadedSOInfo info) {
                showFindSymbolDialog(info);
            }
        });

        rvLoaded.setAdapter(loadedAdapter);

        // 检查native库状态
        if (SOLoaderEngine.isReady()) {
            appendLog("✅ Native引擎加载成功");
        } else {
            appendLog("❌ Native引擎加载失败");
        }

        appendLog("📁 Dump输出目录: " + MemoryDumper.getDumpDirectory());
    }

    private void requestPermissions() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            if (!Environment.isExternalStorageManager()) {
                try {
                    Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
                    intent.setData(Uri.parse("package:" + getPackageName()));
                    startActivityForResult(intent, REQUEST_MANAGE_STORAGE);
                } catch (Exception e) {
                    Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
                    startActivityForResult(intent, REQUEST_MANAGE_STORAGE);
                }
            }
        } else {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED ||
                ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED) {

                ActivityCompat.requestPermissions(this,
                        new String[]{
                                Manifest.permission.READ_EXTERNAL_STORAGE,
                                Manifest.permission.WRITE_EXTERNAL_STORAGE
                        },
                        REQUEST_PERMISSIONS);
            }
        }
    }

    private void openFilePicker() {
        Intent intent = new Intent(this, FilePickerActivity.class);
        startActivityForResult(intent, REQUEST_PICK_FILE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == REQUEST_PICK_FILE && resultCode == Activity.RESULT_OK && data != null) {
            String path = data.getStringExtra(FilePickerActivity.RESULT_FILE_PATH);
            if (path != null) {
                loadSOFile(path);
            }
        }
    }

    private void loadSOFile(String path) {
        appendLog("⏳ 正在加载: " + path);

        new Thread(() -> {
            LoadedSOInfo info = loaderEngine.loadSO(path);

            runOnUiThread(() -> {
                if (info != null) {
                    loadedAdapter.addItem(info);
                    appendLog("✅ 加载成功: " + info.getFileName());
                    appendLog("   Base: " + info.getBaseAddressHex());
                    appendLog("   Size: " + info.getMemorySizeFormatted());
                    appendLog("   Handle: " + info.getHandleHex());
                } else {
                    appendLog("❌ 加载失败: " + path);
                    Toast.makeText(this, "SO加载失败", Toast.LENGTH_SHORT).show();
                }
            });
        }).start();
    }

    private void performDump(LoadedSOInfo info) {
        appendLog("⏳ 正在Dump: " + info.getFileName());

        new Thread(() -> {
            String dumpPath = memoryDumper.dumpSO(info);

            runOnUiThread(() -> {
                if (dumpPath != null) {
                    appendLog("✅ Dump成功: " + dumpPath);
                    Toast.makeText(this, "Dump成功!\n" + dumpPath, Toast.LENGTH_LONG).show();
                } else {
                    appendLog("❌ Dump失败: " + info.getFileName());
                    Toast.makeText(this, "Dump失败", Toast.LENGTH_SHORT).show();
                }
            });
        }).start();
    }

    private void performUnload(LoadedSOInfo info) {
        new AlertDialog.Builder(this)
                .setTitle("确认卸载")
                .setMessage("确定要卸载 " + info.getFileName() + " 吗?")
                .setPositiveButton("确定", (dialog, which) -> {
                    boolean result = loaderEngine.unloadSO(info);
                    if (result) {
                        loadedAdapter.updateItem(info);
                        appendLog("✅ 已卸载: " + info.getFileName());
                    } else {
                        appendLog("❌ 卸载失败: " + info.getFileName());
                    }
                })
                .setNegativeButton("取消", null)
                .show();
    }

    private void showSOInfoDialog(LoadedSOInfo info) {
        // 获取该SO在maps中的信息
        String mapsInfo = memoryDumper.getMapsInfo(info.getFileName());

        String infoText = String.format(
                "文件名: %s\n\n" +
                "完整路径: %s\n\n" +
                "Handle: %s\n\n" +
                "基地址: %s\n\n" +
                "内存大小: %s (%d bytes)\n\n" +
                "状态: %s\n\n" +
                "--- Maps信息 ---\n%s",
                info.getFileName(),
                info.getFilePath(),
                info.getHandleHex(),
                info.getBaseAddressHex(),
                info.getMemorySizeFormatted(), info.getMemorySize(),
                info.isLoaded() ? "已加载" : "已卸载",
                mapsInfo
        );

        View dialogView = getLayoutInflater().inflate(R.layout.dialog_memory_info, null);
        TextView tvInfo = dialogView.findViewById(R.id.tv_memory_info);
        tvInfo.setText(infoText);
        tvInfo.setMovementMethod(new ScrollingMovementMethod());

        new AlertDialog.Builder(this)
                .setTitle("SO信息 - " + info.getFileName())
                .setView(dialogView)
                .setPositiveButton("关闭", null)
                .show();
    }

    private void showFindSymbolDialog(LoadedSOInfo info) {
        EditText editText = new EditText(this);
        editText.setHint("输入符号名 (如: JNI_OnLoad)");

        new AlertDialog.Builder(this)
                .setTitle("查找符号 - " + info.getFileName())
                .setView(editText)
                .setPositiveButton("查找", (dialog, which) -> {
                    String symbol = editText.getText().toString().trim();
                    if (!symbol.isEmpty()) {
                        long addr = loaderEngine.findSymbol(info.getHandle(), symbol);
                        if (addr != 0) {
                            String msg = String.format("符号 '%s' 地址: 0x%X", symbol, addr);
                            appendLog("🔍 " + msg);
                            Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
                        } else {
                            appendLog("🔍 符号 '" + symbol + "' 未找到");
                            Toast.makeText(this, "符号未找到", Toast.LENGTH_SHORT).show();
                        }
                    }
                })
                .setNegativeButton("取消", null)
                .show();
    }

    private void showMapsDialog() {
        View dialogView = getLayoutInflater().inflate(R.layout.dialog_memory_info, null);
        TextView tvInfo = dialogView.findViewById(R.id.tv_memory_info);

        // 添加过滤输入框
        EditText editFilter = new EditText(this);
        editFilter.setHint("输入过滤关键字(留空显示全部.so)");

        new AlertDialog.Builder(this)
                .setTitle("过滤Maps")
                .setView(editFilter)
                .setPositiveButton("查看", (dialog, which) -> {
                    String filter = editFilter.getText().toString().trim();
                    if (filter.isEmpty()) filter = ".so";

                    String mapsInfo = memoryDumper.getMapsInfo(filter);
                    tvInfo.setText(mapsInfo);
                    tvInfo.setMovementMethod(new ScrollingMovementMethod());

                    new AlertDialog.Builder(this)
                            .setTitle("Memory Maps")
                            .setView(dialogView)
                            .setPositiveButton("关闭", null)
                            .show();
                })
                .setNegativeButton("取消", null)
                .show();
    }

    private void showCustomDumpDialog() {
        View dialogView = getLayoutInflater().inflate(R.layout.dialog_memory_info, null);

        // 复用dialog布局但添加输入框
        AlertDialog.Builder builder = new AlertDialog.Builder(this);

        View customView = getLayoutInflater().inflate(R.layout.dialog_memory_info, null);
        TextView tvHint = customView.findViewById(R.id.tv_memory_info);

        // 创建自定义布局
        android.widget.LinearLayout layout = new android.widget.LinearLayout(this);
        layout.setOrientation(android.widget.LinearLayout.VERTICAL);
        layout.setPadding(40, 20, 40, 20);

        EditText etStartAddr = new EditText(this);
        etStartAddr.setHint("起始地址 (十六进制, 如: 7A1B2C3D00)");
        layout.addView(etStartAddr);

        EditText etSize = new EditText(this);
        etSize.setHint("大小 (十进制, bytes)");
        etSize.setInputType(android.text.InputType.TYPE_CLASS_NUMBER);
        layout.addView(etSize);

        EditText etName = new EditText(this);
        etName.setHint("输出文件名前缀 (可选)");
        layout.addView(etName);

        builder.setTitle("自定义内存Dump")
                .setView(layout)
                .setPositiveButton("Dump", (dialog, which) -> {
                    try {
                        String addrStr = etStartAddr.getText().toString().trim()
                                .replace("0x", "").replace("0X", "");
                        long addr = Long.parseUnsignedLong(addrStr, 16);
                        long size = Long.parseLong(etSize.getText().toString().trim());
                        String name = etName.getText().toString().trim();
                        if (name.isEmpty()) name = "custom_dump";

                        appendLog(String.format("⏳ Dump地址: 0x%X, 大小: %d", addr, size));

                        final String fname = name;
                        new Thread(() -> {
                            String path = memoryDumper.dumpMemory(addr, size, fname);
                            runOnUiThread(() -> {
                                if (path != null) {
                                    appendLog("✅ Dump成功: " + path);
                                    Toast.makeText(this, "Dump成功!", Toast.LENGTH_SHORT).show();
                                } else {
                                    appendLog("❌ Dump失败");
                                }
                            });
                        }).start();

                    } catch (NumberFormatException e) {
                        Toast.makeText(this, "地址或大小格式错误", Toast.LENGTH_SHORT).show();
                    }
                })
                .setNegativeButton("取消", null)
                .show();
    }

    private void appendLog(String msg) {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
        String time = sdf.format(new Date());
        String logLine = "[" + time + "] " + msg + "\n";

        tvLog.append(logLine);

        // 自动滚动到底部
        scrollLog.post(() -> scrollLog.fullScroll(View.FOCUS_DOWN));
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_PERMISSIONS) {
            boolean allGranted = true;
            for (int result : grantResults) {
                if (result != PackageManager.PERMISSION_GRANTED) {
                    allGranted = false;
                    break;
                }
            }
            if (!allGranted) {
                Toast.makeText(this, "需要存储权限才能正常工作", Toast.LENGTH_LONG).show();
            }
        }
    }
}

5. 布局文件

res/layout/activity_main.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#1A1A2E">

    <!-- 标题栏 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#16213E"
        android:padding="16dp"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="🔧 SO Loader &amp; Dumper"
            android:textColor="#E94560"
            android:textSize="20sp"
            android:textStyle="bold" />
    </LinearLayout>

    <!-- 操作按钮区 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="8dp"
        android:gravity="center">

        <Button
            android:id="@+id/btn_load_so"
            android:layout_width="0dp"
            android:layout_height="44dp"
            android:layout_weight="1"
            android:layout_margin="4dp"
            android:text="📂 加载SO"
            android:textSize="13sp"
            android:textColor="#FFFFFF"
            android:background="@drawable/bg_button_primary" />

        <Button
            android:id="@+id/btn_view_maps"
            android:layout_width="0dp"
            android:layout_height="44dp"
            android:layout_weight="1"
            android:layout_margin="4dp"
            android:text="🗺 Maps"
            android:textSize="13sp"
            android:textColor="#FFFFFF"
            android:background="@drawable/bg_button_secondary" />

        <Button
            android:id="@+id/btn_custom_dump"
            android:layout_width="0dp"
            android:layout_height="44dp"
            android:layout_weight="1"
            android:layout_margin="4dp"
            android:text="💾 地址Dump"
            android:textSize="13sp"
            android:textColor="#FFFFFF"
            android:background="@drawable/bg_button_danger" />
    </LinearLayout>

    <!-- 已加载SO列表 -->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="  📋 已加载的SO库"
        android:textColor="#0F3460"
        android:textSize="14sp"
        android:textStyle="bold"
        android:paddingLeft="12dp"
        android:paddingTop="8dp"
        android:paddingBottom="4dp"
        android:background="#E0E0E0" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_loaded"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:padding="4dp"
        android:clipToPadding="false" />

    <!-- 日志区域 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:background="#0F3460"
        android:padding="4dp"
        android:gravity="center_vertical">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="  📜 操作日志"
            android:textColor="#FFFFFF"
            android:textSize="13sp"
            android:textStyle="bold" />

        <Button
            android:id="@+id/btn_clear_log"
            android:layout_width="wrap_content"
            android:layout_height="30dp"
            android:text="清除"
            android:textSize="11sp"
            android:textColor="#FFFFFF"
            android:background="@drawable/bg_button_danger"
            android:paddingLeft="12dp"
            android:paddingRight="12dp" />
    </LinearLayout>

    <ScrollView
        android:id="@+id/scroll_log"
        android:layout_width="match_parent"
        android:layout_height="180dp"
        android:background="#0A0A1A"
        android:padding="8dp">

        <TextView
            android:id="@+id/tv_log"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="#00FF41"
            android:textSize="12sp"
            android:fontFamily="monospace"
            android:text="" />
    </ScrollView>
</LinearLayout>

res/layout/activity_file_picker.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="#1A1A2E">

    <!-- 标题 -->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="选择SO文件"
        android:textColor="#E94560"
        android:textSize="20sp"
        android:textStyle="bold"
        android:padding="16dp"
        android:background="#16213E" />

    <!-- 当前路径 -->
    <TextView
        android:id="@+id/tv_current_path"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="#AAAAAA"
        android:textSize="12sp"
        android:padding="8dp"
        android:background="#111133"
        android:singleLine="true"
        android:ellipsize="start" />

    <!-- 快捷导航按钮 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="4dp">

        <Button
            android:id="@+id/btn_go_up"
            android:layout_width="0dp"
            android:layout_height="36dp"
            android:layout_weight="1"
            android:layout_margin="2dp"
            android:text="⬆ 上级"
            android:textSize="12sp"
            android:textColor="#FFFFFF"
            android:background="@drawable/bg_button_secondary" />

        <Button
            android:id="@+id/btn_sdcard"
            android:layout_width="0dp"
            android:layout_height="36dp"
            android:layout_weight="1"
            android:layout_margin="2dp"
            android:text="💾 SDCard"
            android:textSize="12sp"
            android:textColor="#FFFFFF"
            android:background="@drawable/bg_button_secondary" />

        <Button
            android:id="@+id/btn_data"
            android:layout_width="0dp"
            android:layout_height="36dp"
            android:layout_weight="1"
            android:layout_margin="2dp"
            android:text="📁 /data"
            android:textSize="12sp"
            android:textColor="#FFFFFF"
            android:background="@drawable/bg_button_secondary" />

        <Button
            android:id="@+id/btn_app_dir"
            android:layout_width="0dp"
            android:layout_height="36dp"
            android:layout_weight="1"
            android:layout_margin="2dp"
            android:text="📱 App"
            android:textSize="12sp"
            android:textColor="#FFFFFF"
            android:background="@drawable/bg_button_secondary" />
    </LinearLayout>

    <!-- 文件列表 -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_files"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:padding="4dp" />
</LinearLayout>

res/layout/item_so_file.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="center_vertical"
    android:padding="12dp"
    android:layout_margin="2dp"
    android:background="@drawable/bg_card">

    <TextView
        android:id="@+id/tv_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        android:paddingRight="12dp" />

    <TextView
        android:id="@+id/tv_file_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:textColor="#FFFFFF"
        android:textSize="14sp"
        android:singleLine="true"
        android:ellipsize="middle" />

    <TextView
        android:id="@+id/tv_file_size"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#888888"
        android:textSize="12sp" />
</LinearLayout>

res/layout/item_loaded_so.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="4dp"
    app:cardBackgroundColor="#16213E"
    app:cardCornerRadius="8dp"
    app:cardElevation="4dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="12dp">

        <!-- 文件名和状态 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:gravity="center_vertical">

            <TextView
                android:id="@+id/tv_loaded_name"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:textColor="#E94560"
                android:textSize="16sp"
                android:textStyle="bold"
                android:singleLine="true"
                android:ellipsize="end" />

            <TextView
                android:id="@+id/tv_status"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="12sp"
                android:textStyle="bold" />
        </LinearLayout>

        <!-- 信息行 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginTop="6dp">

            <TextView
                android:id="@+id/tv_base_addr"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:textColor="#AAAAAA"
                android:textSize="11sp"
                android:fontFamily="monospace" />

            <TextView
                android:id="@+id/tv_mem_size"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:textColor="#AAAAAA"
                android:textSize="11sp"
                android:fontFamily="monospace" />
        </LinearLayout>

        <TextView
            android:id="@+id/tv_handle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="#888888"
            android:textSize="11sp"
            android:fontFamily="monospace"
            android:layout_marginTop="2dp" />

        <!-- 操作按钮 -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginTop="8dp"
            android:gravity="center">

            <Button
                android:id="@+id/btn_dump"
                android:layout_width="0dp"
                android:layout_height="36dp"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:text="💾 Dump"
                android:textSize="11sp"
                android:textColor="#FFFFFF"
                android:background="@drawable/bg_button_primary" />

            <Button
                android:id="@+id/btn_find_symbol"
                android:layout_width="0dp"
                android:layout_height="36dp"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:text="🔍 符号"
                android:textSize="11sp"
                android:textColor="#FFFFFF"
                android:background="@drawable/bg_button_secondary" />

            <Button
                android:id="@+id/btn_view_info"
                android:layout_width="0dp"
                android:layout_height="36dp"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:text="ℹ 信息"
                android:textSize="11sp"
                android:textColor="#FFFFFF"
                android:background="@drawable/bg_button_secondary" />

            <Button
                android:id="@+id/btn_unload"
                android:layout_width="0dp"
                android:layout_height="36dp"
                android:layout_weight="1"
                android:layout_margin="2dp"
                android:text="❌ 卸载"
                android:textSize="11sp"
                android:textColor="#FFFFFF"
                android:background="@drawable/bg_button_danger" />
        </LinearLayout>
    </LinearLayout>
</androidx.cardview.widget.CardView>

res/layout/dialog_memory_info.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="400dp"
    android:background="#1A1A2E"
    android:padding="16dp">

    <TextView
        android:id="@+id/tv_memory_info"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="#00FF41"
        android:textSize="11sp"
        android:fontFamily="monospace"
        android:textIsSelectable="true" />
</ScrollView>

6. Drawable资源

res/drawable/bg_card.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#16213E" />
    <corners android:radius="6dp" />
</shape>

res/drawable/bg_button_primary.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="#80FFFFFF">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="#0F3460" />
            <corners android:radius="6dp" />
        </shape>
    </item>
</ripple>

res/drawable/bg_button_secondary.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="#80FFFFFF">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="#533483" />
            <corners android:radius="6dp" />
        </shape>
    </item>
</ripple>

res/drawable/bg_button_danger.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="#80FFFFFF">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="#E94560" />
            <corners android:radius="6dp" />
        </shape>
    </item>
</ripple>

7. Values资源

res/values/strings.xml

XML
<resources>
    <string name="app_name">SO Loader</string>
</resources>

res/values/colors.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#0F3460</color>
    <color name="colorPrimaryDark">#16213E</color>
    <color name="colorAccent">#E94560</color>
    <color name="background">#1A1A2E</color>
    <color name="textPrimary">#FFFFFF</color>
    <color name="textSecondary">#AAAAAA</color>
    <color name="terminal_green">#00FF41</color>
</resources>

res/values/styles.xml

XML
<resources>
    <style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        <item name="android:windowBackground">@color/background</item>
        <item name="android:statusBarColor">@color/colorPrimaryDark</item>
    </style>
</resources>

8. 根目录 build.gradle

groovy
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:7.4.2'
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

settings.gradle

groovy
include ':app'
rootProject.name = "SOLoader"

功能总结

功能说明
📂 图形化文件浏览 内置文件选择器,支持快捷导航到SDCard、/data/local/tmp、App目录,自动过滤只显示.so文件和目录
🔧 动态加载SO 通过dlopen加载选中的.so文件到内存,自动解析基地址和内存大小
💾 内存Dump 一键将已加载SO的完整内存映射dump到本地文件,输出到/sdcard/SOLoader_Dumps/
🗺 自定义地址Dump 输入任意起始地址和大小,dump指定内存区域
🔍 符号查找 在已加载的SO中通过dlsym查找符号地址
📋 Maps查看 读取/proc/self/maps,支持关键字过滤,查看内存映射详情
❌ 卸载SO 通过dlclose卸载已加载的SO
📜 操作日志 实时显示所有操作的详细日志,终端风格绿色文字

使用注意

  1. 权限: Android 11+ 需要授予"所有文件访问"权限
  2. SELinux: 某些设备可能需要宽松SELinux策略才能加载任意路径的SO
  3. ABI匹配: 确保加载的SO文件与设备CPU架构匹配(arm64-v8a/armeabi-v7a等)
  4. Dump输出: 文件保存在 /sdcard/SOLoader_Dumps/ 目录下

posted on 2026-03-23 23:05  GKLBB  阅读(8)  评论(0)    收藏  举报