SELinux是Linux的一个分支,是Linux的一个内核模块,也是Linux的一个安全子系统。
其最主要的作用最大限度地减小系统中服务进程可访问的资源
在普通的Linux系统当中,决定某个资源能被访问的因素是某个用户是否拥有要访问资源所对应的权限。而root用户不受任何管制,可以访问系统上任意资源。这种权限管理机制的主体是用户,也称为自主访问控制(DAC)
在SELinux系统中,决定一个资源除了判断某个用户是否拥有所要访问资源所对应的权限以外,还会判断某个进程是否有权限访问对应资源。
手机一般默认强制模式,当然也有的手机会默认为宽容模式。
setenforce 1|0
,为1时切换强制模式,为0时切换为宽容模式。
SVC(systemcall)是Linux内核的入口,当代码进入内核态时,就开始执行内核的代码,用户是无法对内核进行操作的。
内核函数,诸如open、read函数这些Linux底层的函数,最终会调用system_call函数切换到内核态,也就是SVC指令,在ARM汇编进行反编译的时候直接进入内核模式。在用户空间时,代码是不安全的,有可能被hook,而在代码通过SVC进入内核空间,代码是不能被hook的,因为进入内核的内存是无法进行读写的。所以很多读取设备信息都通过SVC指令进行读取,无法被HOOK,这种也叫做SVC穿透(可以通过ptrace函数来解决穿透问题,ptrace是Linux调试的函数,它的权限是最高的,它连内核的函数都可以拦截的)
在C/C++代码当中插入汇编代码就叫做内联汇编。
grep -ril "frida"
和grep -ril "frida-server"
该so就是我们需要进行反反frida调试的so。
Shift+F12
打开String windos来找到字符串frida的位置。F5
来把反汇编代码转换成C伪代码。通过frida脚本想hook access函数
发现在access函数输出frida字符串之前,frida就直接崩掉了,说明,检测frida的进程有比access函数更早的地方。
来看看这个地方。
如果熟悉端口检测的话可以看到5D8A
和69A2
和69A3
这三个16进制数分别对应10进制数,23946
、27042
、27043
。
这三个端口号第一个是android-server默认占用的端口,27042是frida默认占用的端口.
端口检测这个很好解决,要么修改检测的端口号,要么修改frida的默认占用的端口
这样设置即可。也可以头铁,通过hook这里的strstr函数,把检测的端口号进行hook修改。我们这里头铁看看吧(笑)。
哈哈,成功了铁头!
但是还是崩。。。还输出了这段带有frida的字符串。嗯看来还有地方比对frida的字符串。
再改改strstr的第二个参数,假如包含frida字样我们就改掉他,(注意需要改变一下返回值)
好嘞反调试就这么过了
抓包发现只有这三个参数是变化的,那我们的破解参数主要为st
、sv
、sign
st: 盲猜时间戳
sv: 暂定
sign: 32位,神似md5
首先搜索一下sign=
14个结果,而且有我们刚才抓包看到的其他参数,看来我们要找的地方就在这附近。
Objection hook com.jingdong.jdsdk.network.toolbox.HttpSetting.getMd5这个函数看看有没有经过这个。
有经过
然后可以看到实际上sign是在这函数经过之前就已经生成了。
看看调用栈。经静态分析,这些之前都是些调用发送包的函数。
那只有一种可能sign的生成函数在getMd5这个函数内部当中被调用。那么极有可能是getUrl()里面。
既然有getUrl,那肯定有setUrl我们通过Objection来hook看看能不能看到有价值的信息。
跟随调用栈来到这里
跟进去
好家伙又接着hook setSignature看看调用栈。
可以看到参数传进来时已经有sign和其他两个我们需要的参数,那跟随调用栈往上跟。
掉进signature里发现
发现这是一个接口类。通过frida脚本找到其实现类。
1 | Java.enumerateLoadedClasses({ |
服了没找到。在看看刚才的地方
一路追,看到这个。。。看来也不是他,佛了,换换别的反编译器GDA看看。
这种情况下可以试试看在so当中碰碰运气。
看来我们应该找到了。。。
HOOK看看通过不。
有经过,且结果跟我们刚才分析的相同,所以这里就是我们想要找到的sign的位置。com.jingdong.common.utils.BitmapkitUtils.getSignFromJni
1 | function hook(){ |
通过自己编译且运行Socket示例的客户端(手机)与服务端代码,通过hook的方式来快速分析Socket发送流程。
客户端代码:
1 | import android.util.Log; |
服务端代码:
1 | # coding=utf-8 |
先通过objection的android hooking search 来查找相应的Socket类,并复制这些类到一个txt文件当中然后通过Alt+Shift+鼠标拖拽光标
的方式给所有类加上一句android hooking watch class
通过objection -g 包名 explore -c 文件名.txt
来在启动objection的时候批量hook这些类,当然这过程当中很可能经常会崩,把崩的那句一条条的删掉。
来到这里
虽然objection崩了,但是还是hook到了关键类的信息
1 | Called java.net.SocketOutputStream.write([B) |
java.net.AbstractPlainSocketImpl看到这个是一个抽象类就不用管这个类了。直接看SocketOutputStream
SocketOutputStream,通过Wallbreaker在内存当中搜索一下这个类的其中一个实例看看有没有我们需要的东西。
该实例的域里存在着两个对象,通过这两个对象可以拿到目标的ip(addr)、目标的port(port)、本地的发送端口(localPort)
那我们再看看java.net.SocketOutputStream
的函数的一些信息。
可以看到所有的函数最后都会经过private native void socketWrite0(FileDescriptor fd, byte[] b, int off, int len) throws IOException;
我们通过frida来打印一下它的参数是什么。
此处可以看到byte[]数组就是我们发送的数据~
此处附上打印byte数组的方法:
1 | function printByteArray(byteArray){ |
因为其是一个native函数,说明该函数是Java层与JNI层的一个“桥梁”,也就是Java的Socket最终发送到JNI的那么一个点。可以通杀Java层。
把刚才的那些hook点在文本当中去掉再用objection hook一遍,以便我们拿到服务端返回的信息的Hook点。
直接看源码。
看了源码之后发现,其最终走的是private native int socketRead0(FileDescriptor fd, byte b[], int off, int len, int timeout)
通过对比socketWrite0和SocketOutputStream的名称发现,这两个类可以说是成对出现的,即写——读
成对。
以上是SocketOutputStream和SocketInputStream两个hook点socketWrite0
和socketRead0
的分析流程。
按照上面的做法进行android hooking search ssl
,找到所有关于ssl的类,并把垃圾类都删掉之后进行objection的trace。时间关系这边直接给出那个待trace的类的log
1 | android hooking watch class android.net.SSLCertificateSocketFactory |
然后找到带有read或者write方法的类,只有这些类才是我们想要的类。
注意,可能也不会走这个ssl类,在OpenSSLSocketFactoryImpl.java有另一个ssl分支
而在android8.1以下没有这种情况,这是由于8.1以上这部分开始换成工厂模式重新编写了一下,android8.1以下ssl hook这个点OpenSSLSocketImpl即可。
使用NDK开发的so不再具有跨平台特性,需要编译提供不同平台支持ABI:ApplicationBinary Interface。
new Project选择Native C++
Next
Next
Finsh。
可以看到项目的默认全貌是这样的。
cpp包下就是我们要编写的JNI代码存放的地方,native-lib.cpp是默认生成的cpp文件,先挨个解释一下
JNIEXPORT是一个宏定义,其作用为告知编译器保留其函数名称,即保留导出函数名。只要设置这个之后我们可以通过IDA可以直接的搜索到其函数名。
告诉编译器该函数按照C函数的形式进行编译,为什么强调是按照C函数的形式进行编译?因为C++函数名会进行name mangling
操作,该函数名被编译的时候会跟我们编写的时候不一样,以下给出一个简单的示例
c++:
c:
android studio帮我们生成的函数是一个静态注册的函数,静态注册的函数有几个特征。
其函数名为Java_包名_类名_函数名为函数的名称
,并且在相应的Java类当中还能找到其加载的相应so与静态注册的函数。
和非静态函数不同的是,由于静态函数可以直接通过类名进行直接的调用,所以和非静态函数相比其第二个参数不再是this,也就是jobject,而应该是jclass
对于一个JNI函数来说,其参数肯定是不少于两个的,从第三个开始才是JNI函数自己的参数。
Java Native Interface Environment
,JNI接口指针,指向本地方法的一个函数表,该函数表中的每一个成员指向了一个JNI函数![]()
JNIEnv表示Java调用native语言的环境,是一个封装了大量JNI方法的指针。
JNIEnv只在创建它的线程生效,如需要访问JNI,必须调用AttachCurrentThread关联,并使用DetachCurrentThread解除链接
引入一个C写的头文件是有讲究的,前面有提到函数要编译成C函数需要添加extern “C”,而引入C的.h(即头文件)有要注意的点么?有!
以下给出一个示例
1 | extern "C"{ |
#include <android/log.h>
int __android_log_print(int prio, const char* tag, const char* fmt, ...)
该函数用于直接的日志打印第一个参数: 按需从以下参数当中选择(包含在<android/log.h>当中):
1 | /** For internal use only. */ |
第二个参数: tag
第三个参数则为要输出的内容
以下结合以上的知识点给出示例截图
tips:
添加一个源码文件需要让其在CMakeLists.txt当中的add_library
里添加具体的源码文件名.后缀
一个小demo
函数使用给定的C字符串创建一个新的JNI字符串(jstring),不能为构造的java.lang.String分配足够的内存,则会抛出OutOfMemoryError异常,并返回null
函数可用于从给定的Java的jstring创建新的C字符串并且要为新诞生的UTF-8字符串分配内存,如果无法分配内存,则该函数返回NULL。该操作有可能因为内存太少失败,失败时会返回NULL并抛出OutOfMemoryError异常,在不使用该函数返回的字符串时,需要释放内存和引用以便对其进行垃圾回收,因此需要始终调用ReleaseStringUTFChars()
用于获取jstring的长度。
有两种方法
1 | jclass jclazz = env->FindClass("com/example/juziss/ReflectionClass"); |
使用AllocObject可以根据传入的jclass创建一个Java对象,但是该对象的状态是非初始化的,在使用这个对象之前必须要用CallNovirtualVoidMethod来进行初始化,这样可以延迟构造函数的调用,该方式使用的很少。
1 | jclass jclazz = env->FindClass("com/example/juziss/ReflectionClass"); |
将原报名+类名改成原包名的.改成/ + 类名
,例如com/example/juziss/ReflectionClass
将参数类型的域描述符按照申明顺序放入一对括号中后跟返回值类型的域描述符例如public static void publicStaticMethod() -> ()V ;例如int func(int a, Object obj) -> (ILjava/lang/Object;)I
JNI调用Java其实符合反射调用的步骤流程
找到需要调用的类
env-> FindClass(“xx/xx/xx”)
获取想要调用的类的属性id
获取想要调用的类的方法ID
调用
方法调用时env->Call开头的函数,属性调用env->GetxxField,属性调用和函数调用无需考虑Java反射当中的安全检查!
示例:
tips:
要想调用一个jobject对象的属性可以使用GetObjectXXX/SetObjectXXX
env->GetArrayLength(jarray)
env->GetIntArrayElements(jarray, 0);
1 | for (int i = 0;i < length;i++){ |
CallXXMethodA:
CallXXMethod与CallXXMethodV:
可以看到CallXXMethod最终调用的是“V”,而“A”跟前两个的区别在于其传参的形式不一样而已。
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *unused)
动态注册主要依赖于JNI_Onload
函数进行函数的注册,该函数被调用的时机早于静态注册的函数,其在System.loadLibary/load这两个函数执行时将被执行,所以时机早于静态注册。
RegisterNatives
方法手动完成native方法和so中的方法的绑定,这样虚拟机就可以通过这个函数映射关系直接找到相应的方法了。返回值:成功则返回JNI_OK(0),失败则返回一个负值。
需要三个参数:要注册的jni函数所属的jclass, JNINativeMethod数组指针,注册的jni函数个数。
该变量的意思是jni函数在运行过程中是否要进行线程的切换。
constructor标记的是定义该函数是在init_array当中注册的。init_array比init执行慢一点,可以在init_array可以放一堆函数,而_init只能注册一个函数。
constructor还可以标记注册顺序。
JavaVM是虚拟机在JNI层的代表,一个进程只有一个JavaVM,所有的线程共用一个JavaVM。
在JNI_OnLoad中作为参数获得,该函数由ART负责自动化查找和传入参数进行调用。
通过JNIEnv的GetJavaVM
函数来获取。
1 | JNIEnv* env; |
tips:
通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass、NewCharArray等)。会阻止GC回收所引用的对象。局部引用只能在当前函数中使用,函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef手动释放。 因此,局部引用不能跨函数使用,不能跨线程使用。
tips:
Java层函数在调用本地jni代码的时候,会维护一个局部引用表(该引用表并不是无限的),一般jni函数调用结束后,ART会释放这个引用,如果是简单的函数就不需要注意这些问题,让他自己释放,基本没有什么问题,但是如果函数里面有诸如大量的循环的操作的话,那么程序可能就会因为局部引用太多而出现异常情况。
调用NewGlobalRef基于局部引用创建,会阻止GC回收所引用的对象。全局引用可以跨函数、跨线程使用。ART不会自动释放,必须调用DeleteGlobalRef手动释放DeleteGlobalRef(g_cls_string),否则会出现内存泄漏。
3、弱全局引用:调用NewWeakGlobalRef基于局部或全局引用创建,不会阻止Gc回收所引用的对象,可以跨方法、跨线程使用。但与全局引用很重要不同的一点是,弱引用不会阻止GC回收它引用的对象。但是引用也不会自动释放,在ART认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放,或调用DeleteWeakGlobalRef手动释放。
ARM 处理器是典型的 RISC 处理器,因为它们执行的是加载/存储体系结构。只
有加载和存储指令才能访问内存。数据处理指令只操作寄存器的内容。
正执行 ARM 指令的处理器在 ARM 状态 下工作。正执行 Thumb 指令的处理器在
Thumb 状态 下工作。在其中一种状态下工作的处理器不能执行不同指令集内的指令。例如,处于ARM 状态下的处理器不能执行 Thumb 指令,而处于 Thumb 状态下的处理器不能执行 ARM 指令。
每种指令集都包含用于更改处理器状态的指令。
若要在 ARM 和 Thumb 状态之间进行转换,必须切换汇编程序模式,以便使用ARM 或 THUMB 指令生成正确的操作代码。若要生成 Thumb-2EE 代码,请使用THUMBX。
ARM 处理器支持不同的处理器模式,具体取决于体系结构的版本
除用户模式之外,其他所有模式统称为特权模式。它们具有对系统资源的完全访问权限,并可随意更改模式。
需要任务保护的应用程序通常在用户模式下执行。有些嵌入式应用程序可能完全运行在超级用户模式或系统模式下。
ARM 处理器拥有 37 个寄存器。这些寄存器按部分重叠组方式加以排列。每个处理器模式都有一个不同的寄存器组。编组的寄存器为处理处理器异常和特权操作提供了快速的上下文切换。
在任一时刻都存在十五个通用寄存器,即 r0、r1… r13、r14,具体取决于当前的处理器模式。
r13 是堆栈指针 (sp)。 C 和 C++ 编译器始终将 r13 用作堆栈指针。在 Thumb-2 中,sp 被严格定义为堆栈指针,因此如果使用 r13,则在堆栈操作中用处不大的许多指令会产生不可预测的结果。
在用户模式下,r14 被用作链接寄存器 (lr),用于存储调用子例程时的返回地址。如果返回地址存储在堆栈上,则也可将 r14 用作通用寄存器。
在异常处理模式下,r14 存放异常的返回地址;如果在一个异常内执行子例程调用,则 r14 存放子例程的返回地址。如果返回地址存储在堆栈上,则可将 r14 用
作通用寄存器。
程序计数器被当作 r15(或 pc)来加以访问。它在 ARM 状态下以一个字(四字节)为增量,在 Thumb 状态下则按指令的大小执行。跳转指令将目标地址加载到 pc 中。可以使用数据操作指令来直接加载 PC。
在执行期间,r15 (pc) 不包含当前执行的指令的地址。在 ARM 状态下,当前执行的指令的地址通常是 pc-8,而在 Thumb 状态下通常是 pc-4。
APSR存放算术逻辑单元 (ALU) 状态标记的副本。这些标记用于确定是否执行条件指令。
在 ARMv5TE 和 ARMv6 及更高版本中,APSR 还存放 Q 标记。
在 ARMv6 及更高版本中,APSR 还存放 GE 标记。
可在所有模式下使用 MSR 和 MRS 指令访问这些标记。
CPSR 存放下列内容:
在支持 Thumb 或 Jazelle® 的处理器上,CPSR 还存放当前处理器状态(ARM、Thumb、ThumbEE 或 Jazelle)。
在 ARMv6T2 及更高版本中,Thumb-2 为 CPSR 引入了新的状态位。IT 指令使用这些位来控制 IT 块的条件执行.
在所有模式下均可访问的标记只有 APSR 标记。对于 CPSR 的其余位,只能在特权模式下使用 MSR 和 MRS 指令访问它们。
当发生异常时,使用 SPSR 来存储 CPSR。在每种异常处理模式下,可访问一个SPSR。用户模式和系统模式没有 SPSR,因为二者不是异常处理模式。
所有 ARM 指令的长度都是 32 位。这些指令是按字对齐方式存储的,因此在ARM 状态下,指令地址的两个最低有效位始终为零。
Thumb、Thumb-2 和 Thumb-2EE 指令的长度是 16 位或 32 位。这些指令按半字对齐方式存储。其中有些指令使用最低有效位来确定跳转到的目标代码是 Thumb 代码还是 ARM 代码。
在引入 Thumb-2 之前,Thumb 指令集只是 ARM 指令集功能的一个限定的子集。几乎所有 Thumb 指令都是 16 位。 Thumb-2 指令集的功能与 ARM 指令集的功能几乎相同。
ARM 和 Thumb 指令可划分为多个功能组
此类指令用于对通用寄存器执行运算。它们可对两个寄存器的内容执行加法、减法或按位逻辑等运算,并将结果存放到第三个寄存器中。此外,它们还可以对单个寄存器中的值执行运算,或者对寄存器中的值与指令中提供的常数(立即值)执行运算。
长乘指令用两个寄存器提供 64 位的结果。
此类指令用于从内存加载单个寄存器的值,或者在内存中存储单个寄存器的值。它们可加载或存储 32 位字、16 位半字或 8 位无符号字节。可以用符号或零扩展字节和半字加载以填充 32 位寄存器。
还定义了几个可将 64 位双字值加载或存储到两个 32 位寄存器的指令。
此类指令可从内存加载通用寄存器的任何子集,或者在内存中存储这样的子集。
状态寄存器访问指令
此类指令向通用寄存器或者从通用寄存器往外移动状态寄存器的内容。
协处理器指令
此类指令支持一种用于扩展 ARM 体系结构的通用方式。
条件执行
可以根据 APSR 中 ALU 状态标记的值,有条件地执行几乎所有 ARM 指令。虽然不需要使用跳转来跳过条件指令,但当一系列指令依赖于相同的条件时,这样做的效果会更好。
在没有 Thumb-2 的处理器上的 Thumb 状态下,条件跳转是提供条件执行的唯一机制。大多数数据处理指令会更新 ALU 标记。通常不能指定指令是否更新 ALU 标记的状态。
Thumb-2 通过使用 IT (If-Then) 指令和同样的 ALU 标记为条件执行提供了另一种机制。IT 是一个 16 位指令,最多可为后面的四个指令提供条件执行。
在 ARM 和 Thumb-2 代码中,可以指定数据处理指令是否更新 ALU 标记。可以使用一个指令所设置的标记来控制其他指令的执行,即使在它们之间有很多非标记设置指令也是如此。
在 ARM 状态下以及具有 Thumb-2 的处理器上的 Thumb 状态下,大多数数据处理指令都具有一个选项,该选项可根据运算结果来更新应用程序状态寄存器(APSR) 中的 ALU 状态标记。有些指令会更新所有标记,而有些指令仅更新部分标记。如果某一标记未得到更新,则会保留其原始值。每个指令的描述详细介绍了它对这些标记所具有的影响。未执行的条件指令对这些标记没有影响。
执行时间为:
在更新这些标记的指令后立即执行
在尚未更新这些标记的任何数目的插入指令之后执行。
可以根据 APSR 中的 ALU 状态标记的状态有条件地执行几乎所有 ARM 指令。下图为条件代码后缀
在 Thumb 状态下,使用条件跳转是一种条件执行机制。
在具有 Thumb-2 的处理器上的 Thumb 状态下,可以使用特殊的 IT (If-Then) 指令使指令有条件地执行。此外,还可以使用 CBZ(零条件跳转)和 CBNZ 指令将寄存器值与零进行比较。
寄存器访问
在 ARM 状态下,所有指令都可访问 r0 到 r14,并且大多数指令也可访问 r15 (pc)**。MRS 和 MSR 指令可将状态寄存器的内容移到通用寄存器中**,在通用寄存器中可以用普通的数据处理操作来处理这些内容。
Thumb-2 处理器上的 Thumb 状态提供了同样的功能,但会禁止一些对 r13 和 r15 的无用访问。
在没有 Thumb-2 的处理器上的 Thumb 状态下,大多数指令只能访问 r0 到 r7。只有少数指令能够访问 r8 到 r15。**寄存器 r0 到 r7 称为低位寄存器。寄存器 r8 到r15 称为高位寄存器**。
访问内联的滚筒式移位器
ARM 算术逻辑单元具有一个 32 位滚筒式移位器,可执行移位和循环操作。
对于所有 ARM 和 Thumb-2 数据处理指令和单寄存器数据传送指令的第二个操作数,可以在执行数据处理或数据传送之前,将其作为指令的一部分执行移位操作。
此操作支持(但不限于):
比例寻址
乘以一个常数
构造常数。
Thumb-2 指令与 ARM 指令对滚筒式移位器的访问方式几乎相同。
16 位 Thumb 指令集只允许使用单独的指令来访问滚筒式移位器。
将 PSR 的内容移到通用寄存器中
MRS{cond} Rd, psr
cond: 是一个可选的条件代码
Rd: 是目标寄存器。Rd 不能为 r15(pc)。
psr: 是下列项之一:
Mpsr: 是下列项之一:
IPSR、EPSR、IEPSR、IAPSR、EAPSR、PSR、MSP、PSP、DSP、
PRIMASK、BASEPRI、BASEPRI_MAX 或 CONTROL
可将 MRS 与 MSR 结合使用,创建一个更新 PSR 的读- 改- 写序列,例如更改处理器模式或清除 Q 标记。
在进程交换代码中,必须保存程序员的换出进程的模型状态,包括 PSR 的相关内容。同样,也必须恢复换入进程的状态。这些操作使用的是 MRS/存储和加载/MSR 指令序列。
注意:
当处理器处于用户或系统模式时,请不要访问 SPSR。这是您的责任。汇编程序无法就此发出警告,因为它不知道执行过程中的处理器模式。如果在处理器处于用户模式或系统模式时,尝试访问 SPSR,则结果将是无法预料的。
仅当处理器处于调试状态、暂停调试模式时,才能读取 CPSR 执行状态位。否则,CPSR 中的执行状态位的读取结果将会为零。条件标记的读取不受模式和处理器的限制。应使用 APSR,而不是 CPSR。
此指令不更改标记。
此 ARM 指令可用于所有版本的 ARM 体系结构。这些 32 位 Thumb 指令可用于 ARMv6T2 和 ARMv7。此指令无 16 位 Thumb 版本。
将通用寄存器的立即数或内容加载到程序状态寄存器 (PSR) 的指定位段中。
MSR{cond} APSR_flags, #constant
MSR{cond} APSR_flags, Rm
MSR{cond} psr_fields, #constant
MSR{cond} psr_fields, Rm
其中:
cond: 是一个可选的条件代码
flags: 指定要移动的 APSR 标记。flags 可以是以下的一个或多个指令:
constant: 是取值为常数的一个表达式。该常数必须对应于一个 8 位结构,可通过在 32 位字内循环移动偶数位而得到。在 Thumb 中不可用。
Rm: 是源寄存器。
psr: 是下列项之一:
fields: 指定要移动的 SPSR 或 CPSR 位段。fields 可以是以下一个或多个值:
MSR{cond} psr, Rm
cond: 是一个可选的条件代码
Rm: 是源寄存器。
psr: 是下列项之一:APSR、IPSR、EPSR、IEPSR、IAPSR、EAPSR、PSR、MSP、PSP、DSP、PRIMASK、BASEPRI、BASEPRI_MAX 或 CONTROL。仅限 ARMv7-M。
在用户模式中:
如果在用户或系统模式时尝试访问 SPSR,则结果将是无法预料的。
注意:
汇编语言是指 ARM 汇编程序 (armasm) 进行分析并汇编生成对象代码的语言。缺省情况下,汇编程序应使用 ARM 汇编语言编写源代码。
armasm 支持用旧版本的 ARM 汇编语言编写的源代码。在这种情况下,它无需获得相应的通知。
armasm 还可支持用旧版本的 Thumb 汇编语言编写的源代码。在这种情况下,必须在源代码中使用 –16 命令行选项或 CODE16 指令通知 armasm。旧版本的 Thumb 汇编语言不支持 Thumb-2 指令。
汇编语言的源代码行的一般格式是{label} {instruction|directive|pseudo-instruction} {;comment}
下图为官方的arm汇编示例
注意:
即使没有标签,指令、伪指令和指令前面也必须使用空格或制表符等留出空白。
源代码行的所有三部分都是可选的。使用空行可使代码更具可读性。
指令助记符、指令和符号寄存器名称可以用大写或小写编写,但不能混合使用大小写。
行长度的最大值为 4095 个字符,包括使用反斜杠的任何扩展在内。
ELF 节 是独立的、已命名的、不可分割的代码或数据序列。单个代码节是生成应用程序的最低要求。
汇编或编译的输出内容可包括:
链接器依照节位置规则,将每个节放在一个程序映像中。对于在源文件中相邻的节,在应用程序映像中不一定相邻。
在源文件中,AREA 指令标记一节的开始。该指令对节进行命名并设置其属性。属性放在名称后面,之间用逗号分隔。
可以为节选择任何名称。但是,以任何非字母字符开头的名称必须括在竖线内,否则会生成 AREA name missing 错误。例如,|1_DataArea|。
ENTRY 指令标记的是第一个要执行的指令。在包含 C 代码的应用程序中,在 C 库初始化代码中也包含一个入口点。初始化代码和异常处理程序也包含入口点。
应用程序代码在标签 start 处开始执行,
官方示例图中
start开始并将
进制值 10 和 3 加载到寄存器 r0 和 r1 中。这些寄存器将一起相加,并且结果将存放到 r0 中。
在执行主代码后,应用程序会将控制权返回调试器,以此来终止执行。
此指令指示汇编程序停止处理此源文件。每个汇编语言源模块必须以仅包括 END指令的一行结束。
若要调用子例程,应使用跳转和链接指令,其语法是:BL destination
其中,destination 通常是位于子例程的第一个指令处的标签。destination 也可以是程序相对表达式。
作用:
在执行子例程代码后,可以使用 BX lr 指令返回。按照约定,寄存器 r0 到 r3 用于将参数传递给子例程,并且 r0 还用于将结果传递回调用方。
在 ARMv5TE 和 ARMv6 及更高版本中,当饱和算术指令(在 ARMv5TE 和 ARMv6 及更高版本中,当饱和算术指令)中出现饱和或某些乘法指令(SMULxy 和 SMLAxy 和第4-79 页的SMULWy 和 SMLAWy)中出现溢出时,会记录 Q 标记。
Q 标记是一种粘性 标记。虽然这些指令可以设置该标记,但不能清除它。
要清除 Q 标记,则请使用 MSR 指令
不能直接用条件代码来测试 Q 标记的状态。若要读取 Q 标记的状态,请使用 MRS指令
tag:
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true