Xposed原理分析
安卓系统启动
什么zygote?
init是内核启动的第一个用户级进程,zygote是由init进程通过解析init.zygote.rc文件而创建的,zygote所对应的具体可执行程序是app_process,所对应的源文件是App_main.cpp,进程名称为zygote。
init.zygote.rc:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
writepid /dev/cpuset/foreground/tasks
安卓应用运行?
在ART模式下,zygote被init进程创建出来,用来孵化和启动其他App。zygote进程具有App所需要的所有核心库。
新的App进程在生成后,就会加载本App的程序代码(apk中的dex文件)
Xposed介绍
Xposed是安卓系统上能够修改系统或三方应用信息的框架。
Xposed构成
名称 | 介绍 |
---|---|
Xposed | Xposed框架Native部分 |
XposedBridge | Xposed向开发者提供的API与相应工具类库 |
XposedInstaller | Xposed框架Android端本地管理,环境框架,以及第三方module资源下载的工具 |
Xposed初始化大体工作流程
(1)xposed的主要接口在XposedBrigde.jar中,核心功能在替换的虚拟机中实现。
(2)app_process是Android App的启动程序(具体形式是zygote fork() 调用app_process作为Android app的载体)。
源码分析
初始化
app_process有两个对应源文件,Android.mk会在编译时根据sdk版本选择对应源文件作为入口(app_main.cpp或app_main2.cpp)
...
ifeq (1,$(strip $(shell expr $(PLATFORM_SDK_VERSION) \>= 21)))
LOCAL_SRC_FILES := app_main2.cpp
LOCAL_MULTILIB := both
LOCAL_MODULE_STEM_32 := app_process32_xposed
LOCAL_MODULE_STEM_64 := app_process64_xposed
else
LOCAL_SRC_FILES := app_main.cpp
LOCAL_MODULE_STEM := app_process_xposed
endif
...
ifeq (1,$(strip $(shell expr $(PLATFORM_SDK_VERSION) \>= 21)))
include frameworks/base/cmds/xposed/ART.mk
else
include frameworks/base/cmds/xposed/Dalvik.mk
endif
app_main#main
在系统开机时,会通过app_process去创建zygote虚拟机,就会调用到app_main2.cpp中的main函数。
main函数中主要做两件事:(1)初始化xposed;(2)创建虚拟机
int main(int argc, char* const argv[])
{
if (xposed::handleOptions(argc, argv))
return 0;
//代码省略...
runtime.mParentDir = parentDir;
// 初始化xposed,主要是将jar包添加至Classpath中
isXposedLoaded = xposed::initialize(zygote, startSystemServer, className, argc, argv);
if (zygote) {
// 如果xposed初始化成功,将zygoteInit 替换为 de.robv.android.xposed.XposedBridge,然后创建虚拟机
runtime.start(isXposedLoaded ? XPOSED_CLASS_DOTS_ZYGOTE : "com.android.internal.os.ZygoteInit",
startSystemServer ? "start-system-server" : "");
}
...
}
app_main#initialize
初始化xposed
(1)初始化xposed内相关变量
(2)调用addJarToClasspath将XposedBridge.jar添加至系统目录。
bool initialize(bool zygote, bool startSystemServer, const char* className, int argc, char* const argv[]) {
...
// 初始化xposed的相关变量
xposed->zygote = zygote;
xposed->startSystemServer = startSystemServer;
xposed->startClassName = className;
xposed->xposedVersionInt = xposedVersionInt;
...
// 打印 release、sdk、manufacturer、model、rom、fingerprint、platform相关数据
printRomInfo();
// 主要在于将jar包加入Classpath
return addJarToClasspath();
}
frameworks.base.core.jni.AndroidRuntime#start
创建对应虚拟机
start做了4件事:
(1)创建虚拟机
(2)初始化虚拟机
(3)传入调用类de.robv.android.xposed.XposedBridge
(4)初始化XposedBridge
/*
* Start the Android runtime. This involves starting the virtual machine
* and calling the "static void main(String[] args)" method in the class
* named by "className".
*
* Passes the main function two arguments, the class name and the specified
* options string.
*/
void AndroidRuntime::start(const char* className, const Vector<String8>& options)
{
/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
//创建虚拟机
if (startVm(&mJavaVM, &env) != 0) {
return;
}
// 初始化虚拟机,xposed对虚拟机进行修改
onVmCreated(env);
// 虚拟机初始化完成后,会调用传入的de.robv.android.xposed.XposedBridge类,初始化java层XposedBridge.jar
char* slashClassName = toSlashClassName(className);
jclass startClass = env->FindClass(slashClassName);
if (startClass == NULL) {
...
} else {
jmethodID startMeth = env->GetStaticMethodID(startClass, "main",
...
}
}
Xposed.cpp#onVmCreated
xposed重写了onVmCreated。
onVmCreated做了什么:
1、xposedInitLib->onVmCreatedCommon->initXposedBridge,初始化XposedBridge
(1)将register_natives_XposedBridge中的函数注册为Native方法
2、xposedInitLib->onVmCreatedCommon->onVmCreated,为xposed_callback_class与xposed_callback_method赋值;
(1)xposed_callback_class和xposed_callback_method变量赋值
3、 de.robv.android.xposed.XposedBridge#main,初始化java层XposedBridge.jar
(1)hook 住系统资源相关的方法;
(2)hook 住zygote 的相关方法;
(3)加载系统中已经安装的xposed 模块。
void onVmCreated(JNIEnv* env) {
// Determine the currently active runtime
...
// Load the suitable libxposed_*.so for it 通过dlopen加载libxposed_art.so
void* xposedLibHandle = dlopen(xposedLibPath, RTLD_NOW);
...
// Initialize the library 初始化xposed相关库
bool (*xposedInitLib)(XposedShared* shared) = NULL;
// 根据动态链接库操作句柄与符号,返回符号对应的地址
*(void **) (&xposedInitLib) = dlsym(xposedLibHandle, "xposedInitLib");
if (!xposedInitLib) {
ALOGE("Could not find function xposedInitLib");
return;
}
...
// xposedInitLib -> onVmCreatedCommon -> initXposedBridge -> 注册Xposed相关Native方法
if (xposedInitLib(xposed)) {
xposed->onVmCreated(env);
}
}
libxposed_art.cpp#xposedInitLib
/** Called by Xposed's app_process replacement. */
bool xposedInitLib(XposedShared* shared) {
xposed = shared;
xposed->onVmCreated = &onVmCreatedCommon;
return true;
}
libxposed_common.cpp#onVmCreatedCommon
void onVmCreatedCommon(JNIEnv* env) {
if (!initXposedBridge(env) || !initZygoteService(env)) {
return;
}
if (!onVmCreated(env)) {
return;
}
xposedLoadedSuccessfully = true;
return;
}
libxposed_common.cpp#initXposedBridge
bool initXposedBridge(JNIEnv* env) {
classXposedBridge = env->FindClass(CLASS_XPOSED_BRIDGE);
...
classXposedBridge = reinterpret_cast<jclass>(env->NewGlobalRef(classXposedBridge));
ALOGI("Found Xposed class '%s', now initializing", CLASS_XPOSED_BRIDGE);
// 将register_natives_XposedBridge中的函数注册为Native方法
if (register_natives_XposedBridge(env, classXposedBridge) != JNI_OK) {
ALOGE("Could not register natives for '%s'", CLASS_XPOSED_BRIDGE);
logExceptionStackTrace();
env->ExceptionClear();
return false;
}
// 获取XposedBridge.jar中的handleHookedMethod方法,并将该方法赋值给methodXposedBridgeHandleHookedMethod,后续会赋值至全局变量中
methodXposedBridgeHandleHookedMethod = env->GetStaticMethodID(classXposedBridge, "handleHookedMethod",
"(Ljava/lang/reflect/Member;ILjava/lang/Object;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
...
return true;
}
libxposed_art.cpp#onVmCreated
/** Called very early during VM startup. */
bool onVmCreated(JNIEnv*) {
// TODO: Handle CLASS_MIUI_RESOURCES?
ArtMethod::xposed_callback_class = classXposedBridge;
ArtMethod::xposed_callback_method = methodXposedBridgeHandleHookedMethod;
return true;
}
de.robv.android.xposed.XposedBridge#main
虚拟机初始化完成后,会调用传入的de.robv.android.xposed.XposedBridge类,初始化java层XposedBridge.jar,调用main函数
(1)hook 系统资源相关的方法;
(2)hook zygote 的相关方法;
(3)加载系统中已经安装的xposed 模块。
protected static void main(String[] args) {
// Initialize the Xposed framework and modules
try {
if (!hadInitErrors()) {
initXResources();
SELinuxHelper.initOnce();
SELinuxHelper.initForProcess(null);
runtime = getRuntime();
XPOSED_BRIDGE_VERSION = getXposedVersion();
if (isZygote) {
XposedInit.hookResources();
XposedInit.initForZygote();
}
XposedInit.loadModules();
} else {
Log.e(TAG, "Not initializing Xposed because of previous errors");
}
}
// Call the original startup code
if (isZygote) {
ZygoteInit.main(args);
} else {
RuntimeInit.main(args);
}
}
初始化结束。
例子
static final String TAG = "XposedTest001";
//final XC_MethodReplacement replacementTrue = XC_MethodReplacement.returnConstant(true);
public CheckSNHook(ClassLoader cl) {
super();
XposedBridge.log("hooking checkSN.");
try {
Class clz = (Class<?>) XposedHelpers.findClass("com.droider.crackme0201.MainActivity", cl);
//XposedBridge.hookAllMethods(clz, "checkSN", replacementTrue);
Log.d(TAG, "hooking clz");
XposedHelpers.findAndHookMethod(clz,
"checkSN",
String.class, String.class,
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param)
throws Throwable {
XposedBridge.log("1CheckSN afterHookedMethod called.");
String s1 = (String) param.args[0];
String s2 = (String) param.args[1];
Log.d(TAG, "s1:" + s1);
Log.d(TAG, "s2:" + s2);
param.setResult(true);
super.afterHookedMethod(param);
}
});
} catch (Exception e) {
e.printStackTrace();
}
XposedBridge.log("1hook checkSN done.");
}
Hook原理分析
XposedBridge#findAndHookMethod
1、根据函数名获取对应Method对象
2、调用XposedBridge.hookMethod函数
public static XC_MethodHook.Unhook findAndHookMethod(Class<?> clazz, String methodName, Object... parameterTypesAndCallback) {
if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length-1] instanceof XC_MethodHook))
throw new IllegalArgumentException("no callback defined");
// 封装回调函数
XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length-1];
// 主要函数Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
Method m = findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback));
// 核心函数
return XposedBridge.hookMethod(m, callback);
}
XposedBridge#hookMethod
1、将回调函数、参数类型、返回类型记录到AdditionalHookInfo中
2、拦截指定函数调用,并使用其他函数替代(native函数)
public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
...
// 将回调函数、参数类型、返回类型记录到AdditionalHookInfo中
AdditionalHookInfo additionalInfo = new AdditionalHookInfo(callbacks, parameterTypes, returnType);
// 拦截指定函数调用,并使用其他函数替代
hookMethodNative(hookMethod, declaringClass, slot, additionalInfo);
}
return callback.new Unhook(hookMethod);
}
private native synchronized static void hookMethodNative(Member method, Class<?> declaringClass, int slot, Object additionalInfo);
libxposed.cpp#hookMethodNative
1、查找我们需要hook的java Method对应的ArtMethod (每一个java层函数在ART下都有一个对应的ArtMethod)
void XposedBridge_hookMethodNative(JNIEnv* env, jclass, jobject javaReflectedMethod,
jobject, jint, jobject javaAdditionalInfo) {
...
// 获取Java层Method对应native层的ArtMethod指针,将java函数描述为ArtMethod,查找我们需要hook的java Method对应的ArtMethod
ArtMethod* artMethod = ArtMethod::FromReflectedMethod(soa, javaReflectedMethod);
// Hook the method
artMethod->EnableXposedHook(soa, javaAdditionalInfo);
}
EnableXposedHook
1、创建原函数备份
2、创建 XposedHookInfo 保存原函数、before函数、after函数
3、设置机器指令入口地址,此时跳入到GetQuickProxyInvokeHandler()地址
void ArtMethod::EnableXposedHook(ScopedObjectAccess& soa, jobject additional_info) {
...
// 创建原函数备份
auto* cl = Runtime::Current()->GetClassLinker();
auto* linear_alloc = cl->GetAllocatorForClassLoader(GetClassLoader());
ArtMethod* backup_method = cl->CreateRuntimeMethod(linear_alloc);
backup_method->CopyFrom(this, cl->GetImagePointerSize());
// 设置标识符kAccXposedOriginalMethod
backup_method->SetAccessFlags(backup_method->GetAccessFlags() | kAccXposedOriginalMethod);
// Create a Method/Constructor object for the backup ArtMethod object
mirror::AbstractMethod* reflected_method;
if (IsConstructor()) {
reflected_method = mirror::Constructor::CreateFromArtMethod(soa.Self(), backup_method);
} else {
reflected_method = mirror::Method::CreateFromArtMethod(soa.Self(), backup_method);
}
reflected_method->SetAccessible<false>(true);
// 创建 XposedHookInfo 保存原函数、before函数、after函数(reflected_method:被hook的函数,XposedHookInfo包含回调函数)
XposedHookInfo* hook_info = reinterpret_cast<XposedHookInfo*>(linear_alloc->Alloc(soa.Self(), sizeof(XposedHookInfo)));
hook_info->reflected_method = soa.Vm()->AddGlobalRef(soa.Self(), reflected_method);
hook_info->additional_info = soa.Env()->NewGlobalRef(additional_info);
hook_info->original_method = backup_method;
...
//将entry_point_from_jni_指针指向hook信息(目的是存储),hook信息包括原函数、before函数、after函数
SetEntryPointFromJniPtrSize(reinterpret_cast<uint8_t*>(hook_info), sizeof(void*));
// 设置机器指令入口地址,此时跳入到GetQuickProxyInvokeHandler()地址
SetEntryPointFromQuickCompiledCode(GetQuickProxyInvokeHandler());
SetCodeItemOffset(0);
// Adjust access flags.
// 进行标志位清除,此时这个ArtMethod对象对应是Hook后的方法,这个方法的实现不是native的
const uint32_t kRemoveFlags = kAccNative | kAccSynchronized | kAccAbstract | kAccDefault | kAccDefaultConflict;
SetAccessFlags((GetAccessFlags() & ~kRemoveFlags) | kAccXposedHookedMethod);
MutexLock mu(soa.Self(), *Locks::thread_list_lock_);
Runtime::Current()->GetThreadList()->ForEach(StackReplaceMethodAndInstallInstrumentation, this);
}
artQuickProxyInvokeHandler
extern "C" uint64_t artQuickProxyInvokeHandler(
ArtMethod* proxy_method, mirror::Object* receiver, Thread* self, ArtMethod** sp)
const bool is_xposed = proxy_method->IsXposedHookedMethod();//判断 GetAccessFlags() 的kAccXposedHookedMethod 字段
......
if (is_xposed) {
jmethodID proxy_methodid = soa.EncodeMethod(proxy_method);
self->EndAssertNoThreadSuspension(old_cause);
JValue result = InvokeXposedHandleHookedMethod(soa, shorty, rcvr_jobj, proxy_methodid, args);
local_ref_visitor.FixupReferences();
return result.GetJ();
}
......
}
InvokeXposedHandleHookedMethod
JValue InvokeXposedHandleHookedMethod(ScopedObjectAccessAlreadyRunnable& soa, const char* shorty, jobject rcvr_jobj, jmethodID method, std::vector<jvalue>& args) {
//获取ArtMethod 的 hookinfo 信息,该信息是EntryPointFromJniPtrSize所指向的信息
const XposedHookInfo* hookInfo = soa.DecodeMethod(method)->GetXposedHookInfo();
//将hookinfo 转为一个数组,以便和java 层进行通信调用
jvalue invocation_args[5];
invocation_args[0].l = hookInfo->reflectedMethod;
invocation_args[1].i = 1;
invocation_args[2].l = hookInfo->additionalInfo;
invocation_args[3].l = rcvr_jobj;
invocation_args[4].l = args_jobj;
//通过CallStaticObjectMethodA 调用 xposed_callback_class 类里面 xposed_callback_method 的方法
//xposed_callback_class: XposedBridge.java
//xposed_callback_method: handleHookedMethod 方法
//ArtMethod 的这两个值,在系统开机时 在 onVmCreated 进行赋值的
jobject result = soa.Env()->CallStaticObjectMethodA(ArtMethod::xposed_callback_class,
ArtMethod::xposed_callback_method,
invocation_args);
}
InvokeXposedHandleHookedMethod
(1)获取ArtMethod 的 hookinfo 信息,该信息是EntryPointFromJniPtrSize所指向的信息
(2)通过CallStaticObjectMethodA 调用 xposed_callback_class 类里面 xposed_callback_method 的方法
(3)此处xposed_callback_class,xposed_callback_method 是libxposed_art****.cpp#onVmCreated重写时做的事
const XposedHookInfo* GetXposedHookInfo() {
DCHECK(IsXposedHookedMethod());
// 前面存储EntryPointFromJniPtrSize指向的信息
return reinterpret_cast<const XposedHookInfo*>(GetEntryPointFromJniPtrSize(sizeof(void*)));
}
GetXposedHookInfo:获取EntryPointFromJniPtrSize存储的信息
Xposed.java#handleHookedMethod
private static Object handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj,
Object thisObject, Object[] args) throws Throwable {
AdditionalHookInfo additionalInfo = (AdditionalHookInfo) additionalInfoObj;
...
// call "before method" callbacks
int beforeIdx = 0;
do {
try {
((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param);
} catch (Throwable t) {
XposedBridge.log(t);
// reset result (ignoring what the unexpectedly exiting callback did)
param.setResult(null);
param.returnEarly = false;
continue;
}
if (param.returnEarly) {
// skip remaining "before" callbacks and corresponding "after" callbacks
beforeIdx++;
break;
}
} while (++beforeIdx < callbacksLength);
// call original method if not requested otherwise
if (!param.returnEarly) {
try {
param.setResult(invokeOriginalMethodNative(method, originalMethodId,
additionalInfo.parameterTypes, additionalInfo.returnType, param.thisObject, param.args));
} catch (InvocationTargetException e) {
param.setThrowable(e.getCause());
}
}
// call "after method" callbacks
int afterIdx = beforeIdx - 1;
do {
Object lastResult = param.getResult();
Throwable lastThrowable = param.getThrowable();
try {
((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param);
} catch (Throwable t) {
XposedBridge.log(t);
// reset to last result (ignoring what the unexpectedly exiting callback did)
if (lastThrowable == null)
param.setResult(lastResult);
else
param.setThrowable(lastThrowable);
}
} while (--afterIdx >= 0);
// return
if (param.hasThrowable())
throw param.getThrowable();
else
return param.getResult();
}
XposedBridge.java 类的handleHookedMethod 方法,真正去处理 before、Original、after 这三个方法的调用关系。
ART函数调用原理
每一个Java函数在ART(虚拟机)内部都由一个ArtMethod对象表示,ArtMethod对象中包含了函数名、参数类型、方法体代码入口地址等。
class ArtMethod {
...
protect:
HeapReference<Class> declaring_class_;
HeapReference<ObjectArray<ArtMethod>> dex_cache_resolved_methods_;
HeapReference<ObjectArray<Class>> dex_cache_resolved_types_;
uint32_t access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint32_t method_index_;
struct PACKED(4) PtrSizedFields {
void* entry_point_from_interpreter_;
// 用于存储jni函数信息,非jni函数的无用,所以经常被hook框架将原方法保存在entry_point_from_jni_
void* entry_point_from_jni_;
// ART HOOK常见的方法是替换入口点,执行hook的函数。(此处指向的是汇编代码,运行的是已经预处理过的机器码)
void* entry_point_from_quick_compiled_code_;
#if defined(ART_USE_PORTABLE_COMPILER)
void* entry_point_from_portable_compiled_code_;
#endif
} ptr_sized_fields_;
static GcRoot<Class> java_lang_reflect_ArtMethod_;
}
替换entrypoint。将原函数对应的ArtMethod对象中entrypoint指向的机器码替换为目标函数的机器码,即可达到hook的目的。
总结
(1)准备包名、函数、参数类型、回调函数调用Hook接口
(2)Xposed在找到art虚拟机中找到方法对应的ArtMethod对象
(3)对ArtMethod对象进行备份
(4)修改备份对象的机器指令入口
(5)回调handleHookedMethod函数
参考
Xposed 源码剖析1(初始话相关):https://blog.csdn.net/xiaolli/article/details/107506138
Xposed 源码剖析2:https://blog.csdn.net/a314131070/article/details/81092526
Xposed 源码剖析3:https://blog.csdn.net/a314131070/article/details/81092548
Xposed 源码剖析4:https://blog.csdn.net/xiaolli/article/details/107517039
Xposed 源码剖析5:https://egguncle.github.io/2018/02/04/xposed-art-hook-%E6%B5%85%E6%9E%90/
Xposed dalvik 源码剖析6:https://bbs.pediy.com/thread-247030.htm
ART入口点替换分析:https://www.jianshu.com/p/820eceabf219
ArtMethod结构:https://zhuanlan.zhihu.com/p/92267192
ArtMethod结构:https://bbs.pediy.com/thread-248898.htm
Dalvik与ART:https://www.jianshu.com/p/59d98244fb52
定制xposed:https://blog.csdn.net/qq_35834055/article/details/103256122
Xposed原理分析的更多相关文章
- xposed 原理分析
1.添加hook方法 首先是init进程打开 app_process,然后进入XposedInit.java main() - > initForZygote() 加入对ActivityThre ...
- 阿里系产品Xposed Hook检测机制原理分析
阿里系产品Xposed Hook检测机制原理分析 导语: 在逆向分析android App过程中,我们时常用的用的Java层hook框架就是Xposed Hook框架了.一些应用程序厂商为了保护自家a ...
- Handler系列之原理分析
上一节我们讲解了Handler的基本使用方法,也是平时大家用到的最多的使用方式.那么本节让我们来学习一下Handler的工作原理吧!!! 我们知道Android中我们只能在ui线程(主线程)更新ui信 ...
- Java NIO使用及原理分析(1-4)(转)
转载的原文章也找不到!从以下博客中找到http://blog.csdn.net/wuxianglong/article/details/6604817 转载自:李会军•宁静致远 最近由于工作关系要做一 ...
- 原子类java.util.concurrent.atomic.*原理分析
原子类java.util.concurrent.atomic.*原理分析 在并发编程下,原子操作类的应用可以说是无处不在的.为解决线程安全的读写提供了很大的便利. 原子类保证原子的两个关键的点就是:可 ...
- Android中Input型输入设备驱动原理分析(一)
转自:http://blog.csdn.net/eilianlau/article/details/6969361 话说Android中Event输入设备驱动原理分析还不如说Linux输入子系统呢,反 ...
- 转载:AbstractQueuedSynchronizer的介绍和原理分析
简介 提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架.该同步器(以下简称同步器)利用了一个int来表示状态,期望它能够成为实现大部分同步需求的基础.使用的方法是继承,子类通过 ...
- Camel运行原理分析
Camel运行原理分析 以一个简单的例子说明一下camel的运行原理,例子本身很简单,目的就是将一个目录下的文件搬运到另一个文件夹,处理器只是将文件(限于文本文件)的内容打印到控制台,首先代码如下: ...
- NOR Flash擦写和原理分析
NOR Flash擦写和原理分析 1. NOR FLASH 的简单介绍 NOR FLASH 是很常见的一种存储芯片,数据掉电不会丢失.NOR FLASH支持Execute On Chip,即程序可以直 ...
随机推荐
- json转化为C#、Java、TypeScript、VisualBasic、Python实体类
效果展示: 源码下载地址:https://github.com/doyoulaikeme/DotNetSample/tree/master/DotNetSample2
- Quartz.Net系列(十一):System.Timers.Timer+WindowsService实现定时任务
1.创建WindowsService项目 2.配置项目 3.AddInstaller(添加安装程序) 4.修改ServiceName(服务名称).StartType(启动类型).Description ...
- SQL列转行,行转列实现
在工作中,大家可能会遇到一些SQL列转行.行转列的问题,恰好,我也遇到了,就在此记录一下.此处所用的是SQLServer2008R2. 行转列,列转行,都要预先知道要要处理多少数据,在此我就以三种方案 ...
- DOM 和 BOM 区别
DOM, DOCUMENT, BOM, WINDOW 区别DOM 是为了操作文档出现的 API,document 是其的一个对象:BOM 是为了操作浏览器出现的 API,window 是其的一个对象. ...
- 数据可视化之PowerQuery篇(十五)如何使用Power BI计算新客户数量?
https://zhuanlan.zhihu.com/p/65119988 每个企业的经营活动都是围绕着客户而开展的,在服务好老客户的同时,不断开拓新客户是每个企业的经营目标之一. 开拓新客户必然要付 ...
- Go的100天之旅-03变量
变量 变量介绍 变量这个词来源于数学,类似方程中的x.y,代表的是存储在计算机中的值.这里主要介绍Go和其它编程语言不一样的地方,在前面我们提到过,Go是一门静态语言.静态语言区别动态语言一个重要的特 ...
- Ant-Design-Vue中关于Table组件的使用
1. 如何自定义表格列头: <a-table :columns="columns" :dataSource="dataSource"> <sp ...
- 史上最全SpringBoot整合Mybatis案例
摘要:如果小编说,SpringBoot是目前为止最好的框架,应该没有人会反驳吧?它的出现使得我们很容易就能搭建一个新应用.那么,SpringBoot与其他第三方框架的整合必定是我们需要关注的重点. 开 ...
- node name配置错误,导致grid日志在报警
[root@aipdb ContentsXML]# cat inventory.xml <?xml version="1.0" standalone="yes&qu ...
- js 或Jquery操作定位元素
属性过滤常用javascript后去DOM对象 id是定位到的是单个element元素对象,其它的都是elements返回的是list对象 1.通过id获取 document.getElementBy ...