当前位置:首页 » 安卓系统 » androiddlopen

androiddlopen

发布时间: 2023-03-25 19:53:11

㈠ Android硬件抽象层模块编写规范

硬件抽象层模块编写规范

硬件抽象层最终都会生成.so文件,放到系统对应的目录中。在系统使用的时候,系统会去对应目录下加载so文件,实现硬件抽象层的功能。因此硬件抽象层的加载过程就是我们使用so的一个接口。先了解加载过程从源头了解抽象层模块儿的编写规范。

1、硬件抽象层加载过程

系统在加载so的过程中,会去两个目录下查找对应id的so文件。这两个目录分别是/system/lib/hw和/vendor/lib/hw。

so文件的名字分为两个部分例如id.prop.so,第一部分是模块id。第二部分是系统prop的值,获取顺序为“ro.hardware”、“ro.procat.board”、“ro.board.platform”、“ro.arch”,如果prop都找不到的话,就用default。(不是找不到prop的值,是找不到prop值对应的so文件)。

负责加载硬件抽象层模块的函数是hw_get_mole,所在的文件是/hardware/libhardware/hardware.c如下:

/** Base path of the hal moles */

#if defined(__LP64__)

#define HAL_LIBRARY_PATH1 "/system/lib64/hw"

#define HAL_LIBRARY_PATH2 "/vendor/lib64/hw"

#else

#define HAL_LIBRARY_PATH1 "/system/lib/hw"

#define HAL_LIBRARY_PATH2 "/vendor/lib/hw"

#endif

/**

 * There are a set of variant filename for moles. The form of the filename

* is ".variant.so" so for the led mole the Dream variants 

 * of base "ro.proct.board", "ro.board.platform" and "ro.arch" would be:

 *

 * led.trout.so

 * led.msm7k.so

 * led.ARMV6.so

 * led.default.so

 */

static const char *variant_keys[] = {

    "ro.hardware",  /* This goes first so that it can pick up a different

                       file on the emulator. */

    "ro.proct.board",

    "ro.board.platform",

    "ro.arch"

};

static const int HAL_VARIANT_KEYS_COUNT =

    (sizeof(variant_keys)/sizeof(variant_keys[0]));

/**

 * Load the file defined by the variant and if successful

 * return the dlopen handle and the hmi.

 * @return 0 = success, !0 = failure.

 */

static int load(const char *id,

        const char *path,

        const struct hw_mole_t **pHmi)

{

    int status;

    void *handle;

    struct hw_mole_t *hmi;

    /*

     * load the symbols resolving undefined symbols before

     * dlopen returns. Since RTLD_GLOBAL is not or'd in with

     * RTLD_NOW the external symbols will not be global

     */

    handle = dlopen(path, RTLD_NOW);

    if (handle == NULL) {

        char const *err_str = dlerror();

        ALOGE("load: mole=%s\n%s", path, err_str?err_str:"unknown");

        status = -EINVAL;

        goto done;

    }

    /* Get the address of the struct hal_mole_info. */

    const char *sym = HAL_MODULE_INFO_SYM_AS_STR;

    hmi = (struct hw_mole_t *)dlsym(handle, sym);

    if (hmi == NULL) {

        ALOGE("load: couldn't find symbol %s", sym);

        status = -EINVAL;

        goto done;

    }

    /* Check that the id matches */

    if (strcmp(id, hmi->id) != 0) {

        ALOGE("load: id=%s != hmi->id=%s", id, hmi->id);

        status = -EINVAL;

        goto done;

    }

    hmi->dso = handle;

    /* success */

    status = 0;

    done:

    if (status != 0) {

        hmi = NULL;

        if (handle != NULL) {

            dlclose(handle);

            handle = NULL;

        }

    } else {

        ALOGV("loaded HAL id=%s path=%s hmi=%p handle=%p",

                id, path, *pHmi, handle);

    }

    *pHmi = hmi;

    return status;

}

/*

 * Check if a HAL with given name and subname exists, if so return 0, otherwise

 * otherwise return negative.  On success path will contain the path to the HAL.

 */

static int hw_mole_exists(char *path, size_t path_len, const char *name,

                            const char *subname)

{

    snprintf(path, path_len, "%s/%s.%s.so",

             HAL_LIBRARY_PATH2, name, subname);

    if (access(path, R_OK) == 0)

        return 0;

    snprintf(path, path_len, "%s/%s.%s.so",

             HAL_LIBRARY_PATH1, name, subname);

    if (access(path, R_OK) == 0)

        return 0;

    return -ENOENT;

}

int hw_get_mole_by_class(const char *class_id, const char *inst,

                           const struct hw_mole_t **mole)

{

    int i;

    char prop[PATH_MAX];

    char path[PATH_MAX];

    char name[PATH_MAX];

    char prop_name[PATH_MAX];

    if (inst)

        snprintf(name, PATH_MAX, "%s.%s", class_id, inst);

    else

        strlcpy(name, class_id, PATH_MAX);

    /*

     * Here we rely on the fact that calling dlopen multiple times on

     * the same .so will simply increment a refcount (and not load

     * a new of the library).

     * We also assume that dlopen() is thread-safe.

     */

    /* First try a property specific to the class and possibly instance */

    snprintf(prop_name, sizeof(prop_name), "ro.hardware.%s", name);

    if (property_get(prop_name, prop, NULL) > 0) {

        if (hw_mole_exists(path, sizeof(path), name, prop) == 0) {

            goto found;

        }

    }

    /* Loop through the configuration variants looking for a mole */

    for (i=0 ; i<HAL_VARIANT_KEYS_COUNT; i++) {

        if (property_get(variant_keys[i], prop, NULL) == 0) {

            continue;

        }

        if (hw_mole_exists(path, sizeof(path), name, prop) == 0) {

            goto found;

        }

    }

    /* Nothing found, try the default */

    if (hw_mole_exists(path, sizeof(path), name, "default") == 0) {

        goto found;

    }

    return -ENOENT;

found:

    /* load the mole, if this fails, we're doomed, and we should not try

     * to load a different variant. */

    return load(class_id, path, mole);

}

int hw_get_mole(const char *id, const struct hw_mole_t **mole)

{

    return hw_get_mole_by_class(id, NULL, mole);

}

找到so文件之后,调用方法load方法去加载对应的so文件,并返回hw_mole_t结构体。load方法源码在上面程序中。

load方法首先调用dlopen加载对应的so文件到内存中。然后用dlsym方法找到变量HAL_MODULE_INFO_SYM_AS_STR符号对应的地址,这个地址也就是一个hw_mole_t结构体,然后从这个结构体中拿出id比对load方法出入的id是否一致,如果是的话表示打开成功。加载过程完成。

HAL_MODULE_INFO_SYM_AS_STR这个符号值为HMI,也就是必须要保证这个符号之后是一个hw_mole_t。接下来的规范中有这个要求。

到此,模块加载完成

2、硬件抽象层模块编写规范

硬件抽象层有两个结构体,一个是hw_mole_t和hw_device_t,定义在hardware.h中。

首先说一下hw_mole_t的编写规范。

1、必须要有一个“自定义硬件抽象层结构体”,且结构体第一个变量类型要为hw_mole_t。

2、必须存在一个HARDWARE_MODULE_INFO_TAG的符号,且指向“自定义硬件抽象层结构体”。在加载的时候根据这个符号找到地址,并把地址的转变为hw_mole_t,这也是为什么第一条中hw_mole_t必须要在第一个的原因。

3、hw_mole_t的tag必须为HARDWARE_MODULE_TAG

4、结构体中要有一个方法列表,其中要有一个open方法。用open方法获得hw_device_t

接下来说一下hw_device_t的编写规范

1、必须要有一个“自定义硬件设备结构体”,且结构体第一个变量类型要为hw_device_t。

2、hw_device_t的tag必须为HARDWARE_DEVICE_TAG

3、要有一个close函数指针,来关闭设备

按照上面规范编写的硬件抽象层就可以由系统加载并正确获取到device。具体的应用层逻辑在device中实现。

㈡ 如何调试Android SO中的init函数

1.Root设备

Root权限下才能快乐调试。
使用市面上的各种Root师傅工具。

2.连接设备

将设备打开调试模式在开发者选项里。
将IDA安装目录中dbgsrv文件夹下的android_server推送到设备系统目录并赋可执行权限。在高于IDA6.6版本才能调试高版本android,此时调试低版本Android SO时,需要使用的是android_nonpipe。
在PC端输入命令:

adb shell su
adb shell android_server的路径/android_server

保持上面窗口,在命令行窗口进行端口转发:

adb forward tcp:23946 tcp:23946

为什么是23946呢,IDA和push进设备的android_server默认用都用23946端口进行通讯。当然可以修改。

3.打开IDA

附加或者启动进程的过程不再多言。

4.定位INIT函数

比较便捷的方法是找一份与设备同系统版本号的android源码。解析执行SO文件的地方在linker.c(cpp)中。
因为不同版本有差异,我就不上图了。

高版本时在do_dlopen()下的CallConstructors()里面,但是编译系统时往往将其和find_library融合在其父函数中,查找时需注意。一个简便方法是源码中搜索“INIT”四个字.
先将设备中的linker pull出来用IDA分析来确定调用INIT的具体位置。
因为linker在Android进程中加载非常早,所以它在IDA中的地址可以不用修正直接拿来用。

5.下断在INIT

下断点后,执行Apk中触发加载该SO的功能。
正常情况下就能停在该SO的INIT前了。

㈢ 手机调试Android程序出异常时不打印堆栈信息

打印堆栈是调试的常用方法,一般在系统异常时,我们可以将异常情况下的堆栈打印出来,这样十分方便错误查找。实际上还有另外一个非常有用的功能:分析代码的行为。android代码太过庞大复杂了,完全的静态分析经常是无从下手,因此通过打印堆栈的动态分析也十分必要。

Android打印堆栈的方法,简单归类一下
1. zygote的堆栈mp
实际上这个可以同时mp java线程及native线程的堆栈,对于java线程,java堆栈和native堆栈都可以得到。
使用方法很简单,直接在adb shell或串口中输入:
[plain] view plain
kill -3 <pid>
输出的trace会保存在 /data/anr/traces.txt文件中。这个需要注意,如果没有 /data/anr/这个目录或/data/anr/traces.txt这个文件,需要手工创建一下,并设置好读写权限。
如果需要在代码中,更容易控制堆栈的输出时机,可以用以下命令获取zygote的core mp:
[java] view plain
Process.sendSignal(pid, Process.SIGNAL_QUIT);
原理和命令行是一样的。
不过需要注意两点:
adb shell可能会没有权限,需要root。
android 4.2中关闭了native thread的堆栈打印,详见 dalvik/vm/Thread.cpp的mpNativeThread方法:
[cpp] view plain
dvmPrintDebugMessage(target,
"\"%s\" sysTid=%d nice=%d sched=%d/%d cgrp=%s\n",
name, tid, getpriority(PRIO_PROCESS, tid),
schedStats.policy, schedStats.priority, schedStats.group);
mpSchedStat(target, tid);
// Temporarily disabled collecting native stacks from non-Dalvik
// threads because sometimes they misbehave.
//dvmDumpNativeStack(target, tid);
Native堆栈的打印被关掉了!不过对于大多数情况,可以直接将这个注释打开。

2. debuggerd的堆栈mp
debuggerd是android的一个daemon进程,负责在进程异常出错时,将进程的运行时信息mp出来供分析。debuggerd生 成的coremp数据是以文本形式呈现,被保存在 /data/tombstone/ 目录下(名字取的也很形象,tombstone是墓碑的意思),共可保存10个文件,当超过10个时,会覆盖重写最早生成的文件。从4.2版本开 始,debuggerd同时也是一个实用工具:可以在不中断进程执行的情况下打印当前进程的native堆栈。使用方法是:
[plain] view plain
debuggerd -b <pid>
这可以协助我们分析进程执行行为,但最最有用的地方是:它可以非常简单的定位到native进程中锁死或错误逻辑引起的死循环的代码位置。

3. java代码中打印堆栈
Java代码打印堆栈比较简单, 堆栈信息获取和输出,都可以通过Throwable类的方法实现。目前通用的做法是在java进程出现需要注意的异常时,打印堆栈,然后再决定退出或挽救。通常的方法是使用exception的printStackTrace()方法:
[java] view plain
try {
...
} catch (RemoteException e) {
e.printStackTrace();
...
}
当然也可以只打印堆栈不退出,这样就比较方便分析代码的动态运行情况。Java代码中插入堆栈打印的方法如下:
[java] view plain
Log.d(TAG,Log.getStackTraceString(new Throwable()));

4. C++代码中打印堆栈
C++也是支持异常处理的,异常处理库中,已经包含了获取backtrace的接口,Android也是利用这个接口来打印堆栈信息的。在Android的C++中,已经集成了一个工具类CallStack,在libutils.so中。使用方法:
[cpp] view plain
#include <utils/CallStack.h>
...
CallStack stack;
stack.update();
stack.mp();
使用方式比较简单。目前Andoid4.2版本已经将相关信息解析的很到位,符号表查找,demangle,偏移位置校正都做好了。
[plain] view plain

5. C代码中打印堆栈
C代码,尤其是底层C库,想要看到调用的堆栈信息,还是比较麻烦的。 CallStack肯定是不能用,一是因为其实C++写的,需要重新封装才能在C中使用,二是底层库反调上层库的函数,会造成链接器循环依赖而无法链接。 不过也不是没有办法,可以通过android工具类CallStack实现中使用的unwind调用及符号解析函数来处理。
这里需要注意的是,为解决链接问题,最好使用dlopen方式,查找需要用到的接口再直接调用,这样会比较简单。如下为相关的实现代码,只需要在要 打印的文件中插入此部分代码,然后调用getCallStack()即可,无需包含太多的头文件和修改Android.mk文件:
[cpp] view plain
#define MAX_DEPTH 31
#define MAX_BACKTRACE_LINE_LENGTH 800
#define PATH "/system/lib/libcorkscrew.so"

typedef ssize_t (*unwindFn)(backtrace_frame_t*, size_t, size_t);
typedef void (*unwindSymbFn)(const backtrace_frame_t*, size_t, backtrace_symbol_t*);
typedef void (*unwindSymbFreeFn)(backtrace_symbol_t*, size_t);

static void *gHandle = NULL;

static int getCallStack(void){
ssize_t i = 0;
ssize_t result = 0;
ssize_t count;
backtrace_frame_t mStack[MAX_DEPTH];
backtrace_symbol_t symbols[MAX_DEPTH];

unwindFn unwind_backtrace = NULL;
unwindSymbFn get_backtrace_symbols = NULL;
unwindSymbFreeFn free_backtrace_symbols = NULL;

// open the so.
if(gHandle == NULL) gHandle = dlopen(PATH, RTLD_NOW);

// get the interface for unwind and symbol analyse
if(gHandle != NULL) unwind_backtrace = (unwindFn)dlsym(gHandle, "unwind_backtrace");
if(gHandle != NULL) get_backtrace_symbols = (unwindSymbFn)dlsym(gHandle, "get_backtrace_symbols");
if(gHandle != NULL) free_backtrace_symbols = (unwindSymbFreeFn)dlsym(gHandle, "free_backtrace_symbols");

if(!gHandle ||!unwind_backtrace ||!get_backtrace_symbols || !free_backtrace_symbols ){
ALOGE("Error! cannot get unwind info: handle:%p %p %p %p",
gHandle, unwind_backtrace, get_backtrace_symbols, free_backtrace_symbols );
return result;
}

count= unwind_backtrace(mStack, 1, MAX_DEPTH);
get_backtrace_symbols(mStack, count, symbols);

for (i = 0; i < count; i++) {
char line[MAX_BACKTRACE_LINE_LENGTH];

const char* mapName = symbols[i].map_name ? symbols[i].map_name : "<unknown>";
const char* symbolName =symbols[i].demangled_name ? symbols[i].demangled_name : symbols[i].symbol_name;
size_t fieldWidth = (MAX_BACKTRACE_LINE_LENGTH - 80) / 2;

if (symbolName) {
uint32_t pc_offset = symbols[i].relative_pc - symbols[i].relative_symbol_addr;
if (pc_offset) {
snprintf(line, MAX_BACKTRACE_LINE_LENGTH, "#%02d pc %08x %.*s (%.*s+%u)",
i, symbols[i].relative_pc, fieldWidth, mapName,
fieldWidth, symbolName, pc_offset);
} else {
snprintf(line, MAX_BACKTRACE_LINE_LENGTH, "#%02d pc %08x %.*s (%.*s)",
i, symbols[i].relative_pc, fieldWidth, mapName,
fieldWidth, symbolName);
}
} else {
snprintf(line, MAX_BACKTRACE_LINE_LENGTH, "#%02d pc %08x %.*s",
i, symbols[i].relative_pc, fieldWidth, mapName);
}

ALOGD("%s", line);
}

free_backtrace_symbols(symbols, count);

return result;
}
对sched_policy.c的堆栈调用分析如下,注意具体是否要打印,在哪里打印,还可以通过pid、uid、property等来控制一下,这样就不会被淹死在trace的汪洋大海中。
[plain] view plain
D/SchedPolicy( 1350): #00 pc 0000676c /system/lib/libcutils.so
D/SchedPolicy( 1350): #01 pc 00006b3a /system/lib/libcutils.so (set_sched_policy+49)
D/SchedPolicy( 1350): #02 pc 00010e82 /system/lib/libutils.so (androidSetThreadPriority+61)
D/SchedPolicy( 1350): #03 pc 00068104 /system/lib/libandroid_runtime.so (android_os_Process_setThreadPriority(_JNIEnv*, _jobject*, int, int)+7)
D/SchedPolicy( 1350): #04 pc 0001e510 /system/lib/libdvm.so (dvmPlatformInvoke+112)
D/SchedPolicy( 1350): #05 pc 0004d6aa /system/lib/libdvm.so (dvmCallJNIMethod(unsigned int const*, JValue*, Method const*, Thread*)+417)
D/SchedPolicy( 1350): #06 pc 00027920 /system/lib/libdvm.so
D/SchedPolicy( 1350): #07 pc 0002b7fc /system/lib/libdvm.so (dvmInterpret(Thread*, Method const*, JValue*)+184)
D/SchedPolicy( 1350): #08 pc 00060c30 /system/lib/libdvm.so (dvmCallMethodV(Thread*, Method const*, Object*, bool, JValue*, std::__va_list)+271)
D/SchedPolicy( 1350): #09 pc 0004cd34 /system/lib/libdvm.so
D/SchedPolicy( 1350): #10 pc 00049382 /system/lib/libandroid_runtime.so
D/SchedPolicy( 1350): #11 pc 00065e52 /system/lib/libandroid_runtime.so
D/SchedPolicy( 1350): #12 pc 0001435e /system/lib/libbinder.so (android::BBinder::transact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+57)
D/SchedPolicy( 1350): #13 pc 00016f5a /system/lib/libbinder.so (android::IPCThreadState::executeCommand(int)+513)
D/SchedPolicy( 1350): #14 pc 00017380 /system/lib/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+183)
D/SchedPolicy( 1350): #15 pc 0001b160 /system/lib/libbinder.so
D/SchedPolicy( 1350): #16 pc 00011264 /system/lib/libutils.so (android::Thread::_threadLoop(void*)+111)
D/SchedPolicy( 1350): #17 pc 000469bc /system/lib/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+63)
D/SchedPolicy( 1350): #18 pc 00010dca /system/lib/libutils.so
D/SchedPolicy( 1350): #19 pc 0000e3d8 /system/lib/libc.so (__thread_entry+72)
D/SchedPolicy( 1350): #20 pc 0000dac4 /system/lib/libc.so (pthread_create+160)
D/SchedPolicy( 1350): #00 pc 0000676c /system/lib/libcutils.so
D/SchedPolicy( 1350): #01 pc 00006b3a /system/lib/libcutils.so (set_sched_policy+49)
D/SchedPolicy( 1350): #02 pc 00016f26 /system/lib/libbinder.so (android::IPCThreadState::executeCommand(int)+461)
D/SchedPolicy( 1350): #03 pc 00017380 /system/lib/libbinder.so (android::IPCThreadState::joinThreadPool(bool)+183)
D/SchedPolicy( 1350): #04 pc 0001b160 /system/lib/libbinder.so
D/SchedPolicy( 1350): #05 pc 00011264 /system/lib/libutils.so (android::Thread::_threadLoop(void*)+111)
D/SchedPolicy( 1350): #06 pc 000469bc /system/lib/libandroid_runtime.so (android::AndroidRuntime::javaThreadShell(void*)+63)
D/SchedPolicy( 1350): #07 pc 00010dca /system/lib/libutils.so
D/SchedPolicy( 1350): #08 pc 0000e3d8 /system/lib/libc.so (__thread_entry+72)
D/SchedPolicy( 1350): #09 pc 0000dac4 /system/lib/libc.so (pthread_create+160)

6. 其它堆栈信息查询

㈣ Android Native库的加载及动态链接

我们从一个简单的NDK Demo开始分析。

下面从 System.loadLibrary() 开始分析。

下面看 loadLibrary0()

参数 loader 为Android的应用类加载器,它是 PathClassLoader 类型的对象,继承自 BaseDexClassLoader 对象,下面看 BaseDexClassLoader 的 findLibrary() 方法。

下面看 DexPathList 的 findLibrary() 方法

回到 loadLibrary0() ,有了动态库的全路径名就可以装载库了,下面看 doLoad() 。

nativeLoad() 最终调用 LoadNativeLibrary() ,下面直接分析 LoadNativeLibrary() 。

对于JNI注册,这里暂不讨论,下面看 OpenNativeLibrary() 的实现。

下面看 android_dlopen_ext() 的实现

接下来就Android链接器linker的工作了。

下面从 do_dlopen() 开始分析。

find_library() 当参数translated_name不为空时,直接调用 find_libraries() ,这是装载链接的关键函数,下面看它的实现。

find_libraries() 中动态库的装载可以分为两部分

下面从 find_library_internal() 开始分析。

下面分析 load_library()

下面看另一个 load_library() 的实现

下面分析ELF文件头以及段信息的读取过程,也就是LoadTask的 read() ,它直接调用ElfReader的 Read() 方法。

动态库的装载在LoadTask的 load() 中实现。

下面看ElfReader的 Load() 方法

动态库的装载已经完成,下面看链接过程。

下面看 prelink_image()

链接主要完成符号重定位工作,下面从 link_image() 开始分析

下面以函数引用重定位为例分析 relocate() 方法

㈤ android注入so怎么使用

dhrurthr64uhtdh

㈥ Android UART 串口通信

最近有项目需要实现windows机器和Android开发版进行UART串口通信,经过3天查找尝试,特记录一下最终方案,希望之后的同行少走弯路,最后在git上回开源我最终的方案希望大家支持。

Android 3.0.1
Gradle 4.1
ARM开发版 : RK3399
PC机器:Win10
开发机器:MAC 10.13.3

先上图

由于 android-serialport-api 项目中的so使用较old的ndk编译,所以在对于Android 6.0 以上版本兼容的时候会报错 dlopen failed: "has text relocations" 。且使用的mk进行编译,特升级为用cmake编译。

升级 android-serialport-api

项目结构:

app对应原项目中的各个Activity, androidserial 是mole 对应编译之前的so,还有API的封装。可以直接引用androidserial,调用方法参考app目录下的activity。

注意 关于权限!

当接入开发板后如果发现 Error You do not have read/write permission to the serial port 需要root 权限 ,在开发者模式中开启root 权限 adb和应用

使用一下命令开启Android对串口的读写权限

setenforce 0 : 关闭防火墙,有人说关键是这,但是我的环境不用关闭,只要给权限就可以

注意 关于ttyS1 - 6 ttyS1 - 6 对应的是 UART 串口1-6 一般都是一一对应的。这个具体要看一下开发板的说明。

记录的比较糙,还请见谅,如有问题请留言,我看到后肯定回复。项目主要看结构,剩下的都是复制黏贴的事。 git地址:https://github.com/braincs/AndroidSerialLibrary

㈦ 安卓4.4.2系统使用sqlcipher时,出现dlopen failed: cannot locate symbol referenced by...

好像是路径错了。

㈧ 在Android中怎么使用system/lib下的库中的函数

android.mk
LOCAL_LDLIBS := -llog -lz 像这样-l加库名称
如果没头文件
看是否可此哗亩以动态调用
例如:

void (*ucnv_convert)(const char *,const char *, char * , int32_t , const char *, int32_t,int32_t*)=0;

void* pDL = dlopen("/system/lib/libicuuc.so", RTLD_LAZY);

ucnv_convert = (void (*)(const char *, const char *, char * , int32_t , const char *, int32_t,int32_t*))dlsym(pDL, "ucnv_convert_44");

ucnv_convert 就可以芦乱当函数森森使用了

热点内容
ie10缓存 发布:2025-05-13 23:10:09 浏览:458
安卓手机图标怎么设置提示 发布:2025-05-13 23:07:56 浏览:809
香蕉FTP下载 发布:2025-05-13 23:07:11 浏览:940
for循环sql语句 发布:2025-05-13 22:45:49 浏览:19
python实用代码 发布:2025-05-13 22:19:41 浏览:843
dede数据库的配置文件 发布:2025-05-13 22:19:08 浏览:970
给字符加密 发布:2025-05-13 22:12:32 浏览:975
数据库系统实现答案 发布:2025-05-13 22:11:57 浏览:143
哪个软件可以共存安卓 发布:2025-05-13 22:10:15 浏览:555
上传宦妃天下野泉肉肉 发布:2025-05-13 22:10:10 浏览:411