本文首发于安全客

链接:https://www.anquanke.com/post/id/219094

0x1 前言

在Android平台上,程序员编写的Java代码最终将被编译成字节码在Android虚拟机上运行。自从Android进入大众的视野后,apktool,jadx等反编译工具也层出不穷,功能也越来越强大,由Java编译成的字节码在这些反编译工具面前变得不堪一击,这相当于一个人裸奔在茫茫人海,身体的各个部位被众人一览无余。一种事物的出现,也会有与之对立的事物出现。有反编译工具的出现,当然也会有反反编译工具的出现,这种技术一般我们加固技术。APP经过加固,就相当于给那个裸奔的人穿了衣服,“衣服”在一定程度上保护了APP,使APP没那么容易被反编译。当然,有加固技术的出现,也会有反加固技术的出现,即本文要分析的脱壳技术。

Android经过多个版本的更迭,它无论在外观还是内在都有许多改变,早期的Android使用的是dalvik虚拟机,Android4.4开始加入ART虚拟机,但不默认启用。从Android5.0开始,ART取代dalvik,成为默认虚拟机。由于dalvik和ART运行机制的不同,在它们内部脱壳原理也不太相同,本文分析的是ART下的脱壳方案:FART。它的整体思路是通过主动调用的方式来实现脱壳,项目地址:https://github.com/hanbinglengyue/FART 。FART的代码是通过修改少量Android源码文件而成的,经过修改的Android源码编译成系统镜像,刷入手机,这样的手机启动后,就成为一台可以用于脱壳的脱壳机。

0x2 流程分析

FART的入口在frameworks\base\core\java\android\app\ActivityThread.java的performLaunchActivity函数中,即APP的Activity启动的时候执行fartthread

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
Log.e("ActivityThread","go into performLaunchActivity");
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
......
//开启fart线程
fartthread();
......
}

fartthread函数开启一个线程,休眠一分钟后调用fart函数

public static void fartthread() {
new Thread(new Runnable() { @Override
public void run() {
try {
Log.e("ActivityThread", "start sleep,wait for fartthread start......");
Thread.sleep(1 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("ActivityThread", "sleep over and start fartthread");
fart();
Log.e("ActivityThread", "fart run over"); }
}).start();
}

fart函数中,获取Classloader,反射获取一些类。反射调用dalvik.system.DexPathList的dexElements字段得到dalvik.system.DexPathList$Element类对象数组,Element类存储着dex的路径等信息。接下来通过遍历dexElements,得到每一个Element对象中的DexFile对象,再获取DexFile对象中的mCookie字段值,调用DexFile类中的String[] getClassNameList(Object cookie)函数并传入获取到mCookie,以得到dex文件中所有的类名。随后,遍历dex中的所有类名,传入loadClassAndInvoke函数。

public static void fart() {
ClassLoader appClassloader = getClassloader();
List<Object> dexFilesArray = new ArrayList<Object>();
Field pathList_Field = (Field) getClassField(appClassloader, "dalvik.system.BaseDexClassLoader", "pathList");
Object pathList_object = getFieldOjbect("dalvik.system.BaseDexClassLoader", appClassloader, "pathList");
Object[] ElementsArray = (Object[]) getFieldOjbect("dalvik.system.DexPathList", pathList_object, "dexElements");
Field dexFile_fileField = null;
try {
dexFile_fileField = (Field) getClassField(appClassloader, "dalvik.system.DexPathList$Element", "dexFile");
} catch (Exception e) {
e.printStackTrace();
}
Class DexFileClazz = null;
try {
DexFileClazz = appClassloader.loadClass("dalvik.system.DexFile");
} catch (Exception e) {
e.printStackTrace();
}
Method getClassNameList_method = null;
Method defineClass_method = null;
Method dumpDexFile_method = null;
Method dumpMethodCode_method = null; for (Method field : DexFileClazz.getDeclaredMethods()) {
if (field.getName().equals("getClassNameList")) {
getClassNameList_method = field;
getClassNameList_method.setAccessible(true);
}
if (field.getName().equals("defineClassNative")) {
defineClass_method = field;
defineClass_method.setAccessible(true);
}
if (field.getName().equals("dumpMethodCode")) {
dumpMethodCode_method = field;
dumpMethodCode_method.setAccessible(true);
}
}
Field mCookiefield = getClassField(appClassloader, "dalvik.system.DexFile", "mCookie");
for (int j = 0; j < ElementsArray.length; j++) {
Object element = ElementsArray[j];
Object dexfile = null;
try {
dexfile = (Object) dexFile_fileField.get(element);
} catch (Exception e) {
e.printStackTrace();
}
if (dexfile == null) {
continue;
}
if (dexfile != null) {
dexFilesArray.add(dexfile);
Object mcookie = getClassFieldObject(appClassloader, "dalvik.system.DexFile", dexfile, "mCookie");
if (mcookie == null) {
continue;
}
String[] classnames = null;
try {
classnames = (String[]) getClassNameList_method.invoke(dexfile, mcookie);
} catch (Exception e) {
e.printStackTrace();
continue;
} catch (Error e) {
e.printStackTrace();
continue;
}
if (classnames != null) {
for (String eachclassname : classnames) {
loadClassAndInvoke(appClassloader, eachclassname, dumpMethodCode_method);
}
} }
}
return;
}

loadClassAndInvoke除了传入上面提到的类名,还传入ClassLoader对象和dumpMethodCode函数的Method对象,看上面的代码可以知道,dumpMethodCode函数来自DexFile,原本的DexFile类没有这个函数,是FART加上去的。dumpMethodCode究竟做了什么我们待会再来看,先把loadClassAndInvoke函数看完。loadClassAndInvoke工作也很简单,根据传入的类名来加载类,再从加载的类获取它的所有的构造函数和函数,然后调用dumpMethodCode,传入Constructor对象或者Method对象

public static void loadClassAndInvoke(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) {
Log.i("ActivityThread", "go into loadClassAndInvoke->" + "classname:" + eachclassname);
Class resultclass = null;
try {
resultclass = appClassloader.loadClass(eachclassname);
} catch (Exception e) {
e.printStackTrace();
return;
} catch (Error e) {
e.printStackTrace();
return;
}
if (resultclass != null) {
try {
Constructor<?> cons[] = resultclass.getDeclaredConstructors();
for (Constructor<?> constructor : cons) {
if (dumpMethodCode_method != null) {
try {
dumpMethodCode_method.invoke(null, constructor);
} catch (Exception e) {
e.printStackTrace();
continue;
} catch (Error e) {
e.printStackTrace();
continue;
}
} else {
Log.e("ActivityThread", "dumpMethodCode_method is null ");
} }
} catch (Exception e) {
e.printStackTrace();
} catch (Error e) {
e.printStackTrace();
}
try {
Method[] methods = resultclass.getDeclaredMethods();
if (methods != null) {
for (Method m : methods) {
if (dumpMethodCode_method != null) {
try {
dumpMethodCode_method.invoke(null, m);
} catch (Exception e) {
e.printStackTrace();
continue;
} catch (Error e) {
e.printStackTrace();
continue;
}
} else {
Log.e("ActivityThread", "dumpMethodCode_method is null ");
}
}
}
} catch (Exception e) {
e.printStackTrace();
} catch (Error e) {
e.printStackTrace();
}
}
}

上面提到dumpMethodCode函数在DexFile类中,DexFile的完整路径为:libcore\dalvik\src\main\java\dalvik\system\DexFile.java,它是这么定义的:

private static native void dumpMethodCode(Object m);

可见,它是一个native方法,它的实际代码在:art\runtime\native\dalvik_system_DexFile.cc,代码为:

static void DexFile_dumpMethodCode(JNIEnv* env, jclass,jobject method) {
ScopedFastNativeObjectAccess soa(env);
if(method!=nullptr)
{
ArtMethod* artmethod = ArtMethod::FromReflectedMethod(soa, method);
myfartInvoke(artmethod);
} return;
}

DexFile_dumpMethodCode函数中,method是loadClassAndInvoke函数传过来的java.lang.reflect.Method对象,传进来的Java层Method对象传入FromReflectedMethod函数得到ArtMethod结构指针,再将ArtMethod结构指针传入myfartInvoke函数。

myfartInvoke实际代码在art/runtime/art_method.cc文件里

extern "C" void myfartInvoke(ArtMethod * artmethod)
SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
JValue *result = nullptr;
Thread *self = nullptr;
uint32_t temp = 6;
uint32_t *args = &temp;
uint32_t args_size = 6;
artmethod->Invoke(self, args, args_size, result, "fart");
}

在myfartInvoke函数中,值得关注的是self被设置为空指针,并传入ArtMethod的Invoke函数。

Invoke函数也是在art/runtime/art_method.cc文件里,在Invoke函数开头,它对self参数做了个判断,如果self为空,说明Invoke函数是被FART所调用的,反之则是系统本身的调用。self为空的时候,调用dumpArtMethod函数,并立即返回

void ArtMethod::Invoke(Thread * self, uint32_t * args,
uint32_t args_size, JValue * result,
const char *shorty) { if (self == nullptr) {
dumpArtMethod(this);
return;
}
......
}

dumpArtMethod函数这里就到了dump dex的代码了。

extern "C" void dumpArtMethod(ArtMethod * artmethod)
SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
char *dexfilepath = (char *) malloc(sizeof(char) * 2000);
if (dexfilepath == nullptr) {
LOG(INFO) <<
"ArtMethod::dumpArtMethodinvoked,methodname:"
<< PrettyMethod(artmethod).
c_str() << "malloc 2000 byte failed";
return;
}
int fcmdline = -1;
char szCmdline[64] = { 0 };
char szProcName[256] = { 0 };
int procid = getpid();
sprintf(szCmdline, "/proc/%d/cmdline", procid);
fcmdline = open(szCmdline, O_RDONLY, 0644);
if (fcmdline > 0) {
read(fcmdline, szProcName, 256);
close(fcmdline);
} if (szProcName[0]) { const DexFile *dex_file = artmethod->GetDexFile();
const char *methodname =
PrettyMethod(artmethod).c_str();
const uint8_t *begin_ = dex_file->Begin();
size_t size_ = dex_file->Size(); memset(dexfilepath, 0, 2000);
int size_int_ = (int) size_; memset(dexfilepath, 0, 2000);
sprintf(dexfilepath, "%s", "/sdcard/fart");
mkdir(dexfilepath, 0777); memset(dexfilepath, 0, 2000);
sprintf(dexfilepath, "/sdcard/fart/%s",
szProcName);
mkdir(dexfilepath, 0777); memset(dexfilepath, 0, 2000);
sprintf(dexfilepath,
"/sdcard/fart/%s/%d_dexfile.dex",
szProcName, size_int_);
int dexfilefp = open(dexfilepath, O_RDONLY, 0666);
if (dexfilefp > 0) {
close(dexfilefp);
dexfilefp = 0; } else {
dexfilefp =
open(dexfilepath, O_CREAT | O_RDWR,
0666);
if (dexfilefp > 0) {
write(dexfilefp, (void *) begin_,
size_);
fsync(dexfilefp);
close(dexfilefp);
} }
//下半部分开始
const DexFile::CodeItem * code_item =
artmethod->GetCodeItem(); // (1)
if (LIKELY(code_item != nullptr)) {
int code_item_len = 0;
uint8_t *item = (uint8_t *) code_item;
if (code_item->tries_size_ > 0) { // (2)
const uint8_t *handler_data = (const uint8_t *) (DexFile::GetTryItems(*code_item,code_item->tries_size_));
uint8_t *tail = codeitem_end(&handler_data);
code_item_len = (int)(tail - item);
} else {
code_item_len =
16 +
code_item->
insns_size_in_code_units_ * 2;
}
memset(dexfilepath, 0, 2000);
int size_int = (int) dex_file->Size(); // Length of data
uint32_t method_idx =
artmethod->get_method_idx();
sprintf(dexfilepath,
"/sdcard/fart/%s/%d_%ld.bin",
szProcName, size_int, gettidv1());
int fp2 =
open(dexfilepath,
O_CREAT | O_APPEND | O_RDWR,
0666);
if (fp2 > 0) {
lseek(fp2, 0, SEEK_END);
memset(dexfilepath, 0, 2000);
int offset = (int) (item - begin_);
sprintf(dexfilepath,
"{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:",
methodname, method_idx,
offset, code_item_len);
int contentlength = 0;
while (dexfilepath[contentlength]
!= 0)
contentlength++;
write(fp2, (void *) dexfilepath,
contentlength);
long outlen = 0;
char *base64result =
base64_encode((char *) item,
(long)
code_item_len,
&outlen);
write(fp2, base64result, outlen);
write(fp2, "};", 2);
fsync(fp2);
close(fp2);
if (base64result != nullptr) {
free(base64result);
base64result = nullptr;
}
} } } if (dexfilepath != nullptr) {
free(dexfilepath);
dexfilepath = nullptr;
} }

dumpArtMethod函数开始先通过/proc/<pid>/cmdline虚拟文件读取进程pid对应的进程名,根据得到的进程名在sdcard下创建目录,所以在脱壳之前要给APP写入外部存储的权限。之后通过ArtMethod的GetDexFile函数得到DexFile指针,即ArtMethod所在的dex的指针,再从DexFile的Begin函数和Size函数得到dex文件在内存中起始的地址和dex文件的大小,接着用write函数把内存中的dex写到文件名以_dexfile.dex的文件中。

但该函数还没完,dumpArtMethod函数的下半部分,对函数的CodeItem进行dump。可能有些人就有疑问了,函数的上半部分不是把dex给dump了吗,为什么还需要取函数的CodeItem进行dump呢?对于某些壳,dumpArtMethod的上半部分已经能对dex进行整体dump,但是对于部分抽取壳,dex即使被dump下来,函数体还是以nop填充,即空函数体,FART还把函数的CodeItem给dump下来是让用户手动来修复这些dump下来的空函数。

我们来看dumpArtMethod函数的下半部分,这里将会涉及dex文件的结构,如果不了解请结合文档来看。注释(1)处,从ArtMethod中得到一个CodeItem。注释(2)处,根据CodeItem的tries_size_,即try_item的数量来计算CodeItem的大小:

(1)如果tries_size_不为0,说明这个CodeItem有try_item,那么去把CodeItem的结尾地址给算出来

const uint8_t *handler_data = (const uint8_t *) (DexFile::GetTryItems(*code_item,code_item->tries_size_));
uint8_t *tail = codeitem_end(&handler_data);
code_item_len = (int)(tail - item);

codeitem_end函数怎么算出CodeItem的结束地址呢?

GetTryItems第二参数传入tries_size_,即跳过所有的try_item,得到encoded_catch_handler_list的地址,然后传入codeitem_end函数

uint8_t *codeitem_end(const uint8_t ** pData) {
uint32_t num_of_list = DecodeUnsignedLeb128(pData);
for (; num_of_list > 0; num_of_list--) {
int32_t num_of_handlers =
DecodeSignedLeb128(pData);
int num = num_of_handlers;
if (num_of_handlers <= 0) {
num = -num_of_handlers;
}
for (; num > 0; num--) {
DecodeUnsignedLeb128(pData);
DecodeUnsignedLeb128(pData);
}
if (num_of_handlers <= 0) {
DecodeUnsignedLeb128(pData);
}
}
return (uint8_t *) (*pData);
}

codeitem_end函数的开头读取encoded_catch_handler_list结构中包含多少个encoded_catch_handler结构,如果不为0,遍历所有encoded_catch_handler结构,读取encoded_catch_handler结构中有多少encoded_type_addr_pair结构,有的话全部跳过,即跳过了整个encoded_catch_handler_list结构。最后函数返回的pData即为CodeItem的结尾地址。

得到了CodeItem结尾地址,用CodeItem结尾的地址减去CodeItem的起始地址得到CodeItem的真实大小。

(2)如果tries_size_为0,那么就没有try_item,直接就能把CodeItem的大小计算出来:

code_item_len = 16 + code_item->insns_size_in_code_units_ * 2;

CodeItem的大小计算出来之后,接下来可以看到,有几个变量以格式化的方式打印到dexfilepath

sprintf(dexfilepath,
"{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:",
methodname,
method_idx,
offset,
code_item_len
);
  • name 函数的名称
  • method_idx 来源FART新增的函数:uint32_t get_method_idx(){ return dex_method_index_; },函数返回dex_method_index_,dex_method_index_是函数在method_ids中的索引
  • offset 是该函数的CodeItem相对于dex文件开始的偏移
  • code_item_len CodeItem的长度

数据组装好之后,写入到以.bin为后缀的文件中:

write(fp2, (void *) dexfilepath,
contentlength);
long outlen = 0;
char *base64result =
base64_encode((char *) item,
(long)
code_item_len,
&outlen);
write(fp2, base64result, outlen);
write(fp2, "};", 2);

对于上面的dexfilepath,它们是明文字符,直接写入即可。而对于CodeItem中的bytecode这种非明文字符,直接写入不太好看,所以FART选择对它们进行base64编码后再写入。

分析到这里好像已经结束了,从主动调用,到dex整体dump,再到函数CodeItem的dump,都已经分析了。但是FART中确实还有一部分逻辑是没有分析的。如果你使用过FART来脱过壳,会发现它dump下来的dex中还有以_execute.dex结尾的dex文件。这种dex是怎么生成的呢?

这一部分的代码也是在art\runtime\art_method.cc文件中

	extern "C" void dumpDexFileByExecute(ArtMethod * artmethod)
SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
char *dexfilepath = (char *) malloc(sizeof(char) * 2000);
if (dexfilepath == nullptr) {
LOG(INFO) <<
"ArtMethod::dumpDexFileByExecute,methodname:"
<< PrettyMethod(artmethod).
c_str() << "malloc 2000 byte failed";
return;
}
int fcmdline = -1;
char szCmdline[64] = { 0 };
char szProcName[256] = { 0 };
int procid = getpid();
sprintf(szCmdline, "/proc/%d/cmdline", procid);
fcmdline = open(szCmdline, O_RDONLY, 0644);
if (fcmdline > 0) {
read(fcmdline, szProcName, 256);
close(fcmdline);
} if (szProcName[0]) { const DexFile *dex_file = artmethod->GetDexFile();
const uint8_t *begin_ = dex_file->Begin(); // Start of data.
size_t size_ = dex_file->Size(); // Length of data. memset(dexfilepath, 0, 2000);
int size_int_ = (int) size_; memset(dexfilepath, 0, 2000);
sprintf(dexfilepath, "%s", "/sdcard/fart");
mkdir(dexfilepath, 0777); memset(dexfilepath, 0, 2000);
sprintf(dexfilepath, "/sdcard/fart/%s",
szProcName);
mkdir(dexfilepath, 0777); memset(dexfilepath, 0, 2000);
sprintf(dexfilepath,
"/sdcard/fart/%s/%d_dexfile_execute.dex",
szProcName, size_int_);
int dexfilefp = open(dexfilepath, O_RDONLY, 0666);
if (dexfilefp > 0) {
close(dexfilefp);
dexfilefp = 0; } else {
dexfilefp =
open(dexfilepath, O_CREAT | O_RDWR,
0666);
if (dexfilefp > 0) {
write(dexfilefp, (void *) begin_,
size_);
fsync(dexfilefp);
close(dexfilefp);
} }
} if (dexfilepath != nullptr) {
free(dexfilepath);
dexfilepath = nullptr;
} }

可以看到,dumpDexFileByExecute函数有点像dumpArtMethod函数的上半部分,即对dex文件的整体dump。那么,dumpDexFileByExecute在哪里被调用呢?

通过搜索,在art\runtime\interpreter\interpreter.cc文件的开始,看到了FART在art命名空间下定义了一个dumpDexFileByExecute函数

namespace art {
extern "C" void dumpDexFileByExecute(ArtMethod* artmethod);
namespace interpreter {
......
}
}

同时在文件其中找到了对dumpDexFileByExecute函数的调用:

static inline JValue Execute(Thread* self, const DexFile::CodeItem* code_item,
ShadowFrame& shadow_frame, JValue result_register) {
if(strstr(PrettyMethod(shadow_frame.GetMethod()).c_str(),"<clinit>")!=nullptr)
{
dumpDexFileByExecute(shadow_frame.GetMethod());
}
......
}

在Execute函数中,通过判断函数名称中是否为<clinit>决定要不要调用dumpDexFileByExecute,即判断传入的是否为静态代码块,对于加了壳的App来说静态代码块是肯定存在的。如果Execute传入的是静态代码块则调用dumpDexFileByExecute函数,并传入一个ArtMethod指针。

dumpDexFileByExecute中对dex进行了整体dump,可以把它看作是dumpArtMethod方式的互补,有时dumpArtMethod中得不到想得到的dex,用dumpDexFileByExecute或许能得到惊喜。

0x3 结语

非常感谢FART作者能够开源FART,这使得人们对抗ART环境下App壳得到了良好的思路。FART脱壳机理论上来讲能脱大多数壳,但是仍有例外,需要自行摸索。

0x4 参考

Android FART脱壳机流程分析的更多相关文章

  1. Gradle之Android Gradle Plugin 主要流程分析(二)

    [Android 修炼手册]Gradle 篇 -- Android Gradle Plugin 主要流程分析 预备知识 理解 gradle 的基本开发 了解 gradle task 和 plugin ...

  2. Android 4.4KitKat AudioRecord 流程分析

    Android是架构分为三层: 底层      Linux Kernel 中间层  主要由C++实现 (Android 60%源码都是C++实现) 应用层  主要由JAVA开发的应用程序 应用程序执行 ...

  3. Cocos2d-x3.3RC0的Android编译Activity启动流程分析

    本文将从引擎源代码Jni分析Cocos2d-x3.3RC0的Android Activity的启动流程,以下是具体分析. 1.引擎源代码Jni.部分Java层和C++层代码分析 watermark/2 ...

  4. Android恢复出厂设置流程分析【Android源码解析十】

    最近看恢复出厂的一个问题,以前也查过这方面的流程,所以这里整理一些AP+framework层的流程: 在setting-->备份与重置--->恢复出厂设置--->重置手机---> ...

  5. Openstack之Nova创建虚机流程分析

    前言        Openstack作为一个虚拟机管理平台,核心功能自然是虚拟机的生命周期的管理,而负责虚机管理的模块就是Nova. 本文就是openstack中Nova模块的分析,所以本文重点是以 ...

  6. Android 9.0 关机流程分析

    极力推荐文章:欢迎收藏 Android 干货分享 阅读五分钟,每日十点,和您一起终身学习,这里是程序员Android 本篇文章主要介绍 Android 开发中的部分知识点,通过阅读本篇文章,您将收获以 ...

  7. 【转载】Android 中 View 绘制流程分析

    创建Window 在Activity的attach方法中通过调用PolicyManager.makeNewWindo创建Window,将一个View add到WindowManager时,Window ...

  8. Android View 绘制刷新流程分析

    Android中对View的更新有很多种方式,使用时要区分不同的应用场合.1.不使用多线程和双缓冲      这种情况最简单,一般只是希望在View发生改变时对UI进行重绘.你只需显式地调用View对 ...

  9. Android 4.4KitKat AudioTrack 流程分析

    Android Audio 系统的主要内容: AudioManager:这个主要是用来管理Audio系统的,需要考虑整个系统上声音的策略问题,例如来电话铃声,短信铃声等,主要是策略上的问题. Audi ...

随机推荐

  1. 一个工作三年左右的Java程序员和大家谈谈从业心得

    转发链接地址:https://mp.weixin.qq.com/s/SSh9HcA5PgMHv7xiolQkig 貌似这一点适应的行业最广,但是我可以很肯定的说:当你从事web开发一年后,重新找工作时 ...

  2. ServerLess之云函数实践-天气API

    关注我的个人博客,发掘更多的内容 ServerLess之云函数实践-天气API 前言 云计算是大势所趋 Serverless 架构即"⽆服务器"架构,它是一种全新的架构方式,是云计 ...

  3. 这次齐了!Java面向对象、类的定义、对象的使用,全部帮你搞定

    概述 Java语言是一种面向对象的程序设计语言,而面向对象思想是一种程序设计思想,我们在面向对象思想的指引下, 使用Java语言去设计.开发计算机程序. 这里的对象泛指现实中一切事物,每种事物都具备自 ...

  4. 深度分析:那些Java中你一定遇到过的问题,一次性帮你搞定!深度分析:那些Java中你一定遇到过的问题,一次性帮你搞定!

    1.java中==和equals和hashCode的区别 基本数据类型的比较的值相等.类的比较的内存的地址,即是否是同一个对象,在不覆盖equals的情况下,同比较内存地址,原实现也为 == ,如St ...

  5. How to realize one's ambition

    Work Overtime Can it work? To some extent, it parhaps works very well. What if you do little job and ...

  6. FL Studio采样器设置的功能是什么

    FL Studio是一款很集成化的软件,它和很多其他的软件不同,FL Studio的通道中集成了很多采样器.这种整合的方式不但功能强大而且也很便捷.今天小编就来为大家详细的简介下FL Studio采样 ...

  7. guitar pro 系列教程(十八):Guitar Pro怎么设置吉他谱的局部速度?

    关于Guitar Pro的使用功能我们在前面的文章也有讲了不少,对于新手的小伙伴,就小编个人而言,在吉他编曲,演绎方面遇到的困难不是一点两点,我们只有通过学习了解他的全部,才能在以后的吉他创作中得心印 ...

  8. Mac中的格式转换如何用读写工具Tuxera NTFS完成

    Tuxera NTFS for Mac是一款专门为Mac用户提供的NTFS驱动软件,它不仅可以进行磁盘文件的访问.编辑.传输和存储,还可以对硬盘进行维修检查以及修复. 今天小编就给大家简单介绍一下Tu ...

  9. Java基础知识面试题(最详细版)

    刚刚经历过秋招,看了大量的面经,顺便将常见的Java常考知识点总结了一下,并根据被问到的频率大致做了一个标注.一颗星表示知识点需要了解,被问到的频率不高,面试时起码能说个差不多.两颗星表示被问到的频率 ...

  10. JavaSE 学习笔记06丨并发

    Chapter 12. 并发 12.1 并发与并行 并发:指两个或多个事件在同一个时间段内发生. 并行:指两个或多个事件在同一时刻发生(同时发生). 在操作系统中,并发指的是在一段时间内宏观上有多个程 ...