前言
本篇文章主要讲解👇
- 状态寄存器
- 判断、选择和循环
一、状态寄存器(CPSR)
什么是状态寄存器?👇
CPU内部的寄存器中,有一种特殊的寄存器(对于不同的处理器,个数和结构都可能不同)。这种寄存器在ARM中,被称为状态寄存器CPSR(current program status register)。
与其它寄存区的区别👇
- 其它寄存器是用来存放数据的,整个寄存器只具有
一个含义。 - CPSR寄存器是
按位起作用的,也就是说它的每一位都有专门的含义,记录特定的信息。
位域分布
CPSR寄存器是32位的,其分布大致如下👇
- CPSR的
低8位(包括I、F、T和M[4:0])称为控制位,程序无法修改,除非CPU运行于特权模式下,程序才能修改控制位! -
8~27位为保留位。 -
N、Z、C、V均为条件码标志位。它们的内容可被算术或逻辑运算的结果所改变,并且可以决定某条指令是否被执行!意义重大!
整体分布如下图👇

示例查看CPSR
接下来,我们通过一个简单的示例,看看控制台中CPSR的值,示例👇
void funcA() {
int a = 1;
int b = 2;
if (a == b) {
printf("a == b");
} else {
printf("error");
}
}
查看汇编👇

接下来通过lldb修改cpsr的值👇

可以看到,我们通过修改cpsr的值,强行改变了代码的执行逻辑,最终执行了printf("a == b");。
内联汇编
我们想在oc文件中写汇编的代码,除了在01-汇编基础(1)中新建汇编.s格式的文件这种方式外,还有一种方式 👉 内联汇编。
在C/OC代码中嵌入汇编需要使用asm关键字(也可以使用asm、__asm。这个和编译器有关,在iOS环境下它们等价。),在asm的修饰下,代码列表、输出运算符列表、输入运算符列表和被更改的资源列表这4个部分被3个“:”分隔👇
asm(
代码列表
: 输出运算符列表
: 输入运算符列表
: 被更改资源列表
);
swift中貌似没有办法直接
内联汇编,但可以通过和OC的桥接去处理。
接下来我们开看看CPSR中最高的4位 N Z C V,每个位所表示的具体含义👇

1.1 N(Negative)(符号标志位)
CPSR的第31位是 N 👉 符号标志位。它记录相关指令执行后,其结果是否为负。
- 如果为负 N = 1
- 如果是非负数 N = 0
示例演示
接下来我们执行一个简单的汇编指令,看看N符号标志位的值👇
void funcA() {
asm(
"mov w0,#0xffffffff\n"
"adds w0,w0,#0x0\n"
);
}


- 执行adds指令前,cpsr为
0x60000000,高4位为0110,那么N = 0👇

- 执行adds指令完成后,cpsr =
0x80000000,高4位为1000,N = 1👇

⚠️注意:在ARM64的指令集中,有的指令的执行时影响状态寄存器的,比如
add\sub\or等,他们大都是运算指令(进行逻辑或算数运算);
1.2 Z(Zero)(0标志位)
CPSR的第30位是 Z 👉 0标志位。它记录相关指令执行后,其结果是否为0。
- 如果结果为0,那么 Z = 1
- 如果结果不为0,那么 Z = 0
⚠️注意,结果值 和 Z值 是
反的!
我们可以这么理解👇
- 在计算机中
1表示逻辑真,表示肯定;0表示逻辑假,表示否定。 - 当
结果为0的时候表示结果是0这个条件是肯定的为真,所有Z = 1 -
结果不为0的时候表示结果是0这个条件是否定的为假,所以Z = 0
示例演示
看下面这个示例👇
void funcA() {
asm(
"mov w0,#0x0\n"
"adds w0,w0,#0x0\n"
);
}
- adds执行前👇


cpsr值 👉 0x60000000,其中 Z = 1
- adds执行后👇


cpsr值 👉 0x40000000,其中 Z = 0
修改一下示例代码👇
void funcA() {
asm(
"mov w0,#0x0\n"
"adds w0,w0,#0x1\n"
);
}
读者可自行操作,查看该情况下cpsr值的变化。
同样的操作adds断点前后cpsr分别为:cpsr = 0x60000000和cpsr = 0x00000000 👉 对应N = 1和 N = 0。
1.3 C(Carry)(进位标志位)
CPSR的第29位是C 👉 进位标志位。一般情况下,进行无符号数的运算。
-
加法运算 👉 当运算结果产生了进位时(无符号数溢出),C=1,否则C=0。 -
减法运算(包括CMP) 👉 当运算时产生了借位时(无符号数溢出),C=0,否则C=1。
对于位数为N的无符号数来说,其对应的二进制信息的最高位,即第N - 1位,就是它的最高有效位,而假想存在的第N位,就是相对于最高有效位的更高位。如下图所示👇

进位 & 借位
上面提到了进位和借位,我们先来解释下它们。
进位
先看看进位的情况,我们知道,当两个数据相加的时候,有可能产生从最高有效位向更高位的进位。
比如两个32位数据:0xaaaaaaaa + 0xaaaaaaaa,将产生进位。由于这个进位值在32位中无法保存,我们就只是简单的说这个进位值丢失了。其实CPU在运算的时候,并不丢弃这个进位制,而是记录在一个特殊的寄存器的某一位上。ARM下就用C位来记录这个进位值。比如,下面的指令👇
void funcA() {
asm(
"mov w0,#0xaaaaaaaa\n"//0xa 的二进制是 1010
"adds w0,w0,w0\n" // 执行后 相当于 1010 << 1 进位1(无符号溢出) 所以C标记 为 1
"adds w0,w0,w0\n" // 执行后 相当于 0101 << 1 进位0(无符号没溢出) 所以C标记 为 0
"adds w0,w0,w0\n" // 重复上面操作
"adds w0,w0,w0\n"
);
}
- adds执行前

- 第1次adds执行后

- 第2次adds执行后

- 第3次adds执行后

- 第4次adds执行后

综上,cpsr的值是这么变化的👇
0x60000000 👉 0x30000000 👉 0x90000000 👉 0x30000000 👉 0x90000000
对应的高4位的值的变化就是
0110 👉 0011 👉 1001 👉 0011 👉 1001
所以,第1次和第3次相加,无符号溢出了,所以 C = 1;而第2次和第4次相加,无符号没有溢出,所有C = 0。
借位
再开看看借位的情况,当两个数据做减法的时候,有可能向更高位借位。
比如两个32位数据:0x00000000 - 0x000000ff将产生借位,借位后相当于计算0x100000000 - 0x000000ff 👉 得到0xffffff01这个值。由于借了一位,C位用来标记借位,所以 C = 0。
比如下面指令👇
void funcA() {
asm(
"mov w0,#0x0\n"
"subs w0,w0,#0xff\n"
"subs w0,w0,#0xff\n"
"subs w0,w0,#0xff\n"
);
}
和进位的情况一样的调试,这里就不做演示了,得到的结果👇
cpsr的值是这么变化的👇
0x60000000 👉 0x80000000 👉 0xa0000000 👉 0xa0000000
对应的高4位的值的变化就是
0110 👉 1000 👉 1010 👉 1010
所以,第一次相减借了一位,无符号溢出了,C=0;第2次和第3次相减时,无符号没有溢出,所以C=1。
1.4 V(Overflow)(溢出标志)
CPSR的第28位是V 👉 溢出标志位。在进行有符号数运算的时候,如果超过了机器所能标识的范围,称为溢出。
- 正数 + 正数 为负数 溢出
- 负数 + 负数 为正数 溢出
- 正数 + 负数 不可能溢出
- 溢出
V = 1,不溢出V = 0
由于CPU并不知道有没有符号,所以CPSR寄存器CV同时标记,C标记无符号,V标记有符号。标志位会同时返回。
理解起来不难,这里就不做示例演示了。
二、判断、选择和循环
在讲判断、选择和循环之前,我们先来看看内存的五大分区👇
-
栈区:参数、局部变量、临时数据。可短可写 -
堆区:动态申请。可读可写 -
全局静态区:可读可写 -
常量区:只读 -
代码区:存放代码,可读可执行
详细的说明可以参考我之前写的内存五大分区。
2.1 基础知识点
全局变量和常量
全局变量和常量,在汇编中是怎么读取值的?我们先来看看下面这个例子👇
int g = 12;
int func(int a, int b) {
printf("test");
int c = a + g + b;
return c;
}
查看汇编👇

上图我们通过查看x0寄存器值可知,printf函数的参数来源为👇
0x102c6dc54 <+20>: adrp x0, 1
0x102c6dc58 <+24>: add x0, x0, #0x5ec ; =0x5ec
X0存储的是一个地址,为字符串常量区。那么以上两条指令是怎么计算得出0x0000000102c6e5ec这个值?👇
-
adrp👉Address Page内存地址以页寻址。 -
0x102c6dc54 <+20>: adrp x0, 1👉 定位到某一页数据的开始(文件起始位置)- 将
1的值左移12位变成0x1000 - 当前pc的值低12位清零。
0x102c6dc54 -> 0x102c6d000。 -
0x102c6d000 + 0x1000得到0x102c6e000。相当于pc后3位置为0,第4位加上x0后跟的值。
- 将
-
0x102c6dc58 <+24>: add x0, x0, #0x5ec👉 偏移地址(当前代码偏移)-
0x102c6e000 + 0x5ec得到0x102c6e5ec
-
这样就得到了常量区字符串"test"的地址了。
其中,0x102c6d000尾数为000意味着000~fff -> 0~4095大小为4096也就是4k。也就是定位到某一页数据的开始。
- mac中 pagesize 4k
- iOS中 pagesize 16k。这里是兼容的 👉 4k * 4 = 16k。
我们继续调试,查看全局变量g的汇编处理👇

上图红框处的指令,和上面一样的,最终计算出x9最终的值为0x0000000102c715f0,也就是全局变量g的值。
综上所述,全局变量和常量都是通过一个
基地址 + 偏移获取。
反汇编工具还原
接下来,我们使用反汇编工具,演示一下将汇编代码还原成高级代码。
- 首先,编译要还原的工程,进入.app找到macho文件并拖入
Hopper中👇

- 反汇编工具
Hopper分析完成后,搜索要分析的函数👇

- 先看看汇编的代码👇
0000000100005c40 sub sp, sp, #0x20 ; CODE XREF=-[ViewController viewDidLoad]+76
0000000100005c44 stp x29, x30, [sp, #0x10]
0000000100005c48 add x29, sp, #0x10
0000000100005c4c stur w0, [x29, #-0x4]
0000000100005c50 str w1, [sp, #0x8]
0000000100005c54 adrp x0, #0x100006000 ; argument #1 for method imp___stubs__printf
0000000100005c58 add x0, x0, #0x5ec ; "test"
0000000100005c5c bl imp___stubs__printf
0000000100005c60 ldur w8, [x29, #-0x4]
0000000100005c64 adrp x9, #0x100009000
0000000100005c68 add x9, x9, #0x5f0 ; _g
0000000100005c6c ldr w10, [x9] ; _g
0000000100005c70 add w8, w8, w10
0000000100005c74 ldr w10, [sp, #0x8]
0000000100005c78 add w8, w8, w10
0000000100005c7c str w8, [sp, #0x4]
0000000100005c80 ldr w8, [sp, #0x4]
0000000100005c84 mov x0, x8
0000000100005c88 ldp x29, x30, [sp, #0x10]
0000000100005c8c add sp, sp, #0x20
0000000100005c90 ret
上面的"test"那行是0x100006000 + 0x5ec = 0x1000065ec。
- 可以通过MachOView查找
0x1000065ec👇

- 同理,查看
全局变量g
0000000100005c64 adrp x9, #0x100009000
0000000100005c68 add x9, x9, #0x5f0 ; _g
全局变量g的地址就是0x1000095f0👇

- 接着,我们将上面的汇编代码还原👇
0000000100005c40 sub sp, sp, #0x20 ; CODE XREF=-[ViewController viewDidLoad]+76
// 拉伸栈空间
0000000100005c44 stp x29, x30, [sp, #0x10]
0000000100005c48 add x29, sp, #0x10
// 参数入栈w0 w1
0000000100005c4c stur w0, [x29, #-0x4]
0000000100005c50 str w1, [sp, #0x8]
// 取常量“test”
0000000100005c54 adrp x0, #0x100006000 ; argument #1 for method imp___stubs__printf
0000000100005c58 add x0, x0, #0x5ec ; "test"
// 调用printf("test")
0000000100005c5c bl imp___stubs__printf
// w8 = a;
0000000100005c60 ldur w8, [x29, #-0x4]
// 取全局变量g
0000000100005c64 adrp x9, #0x100009000
0000000100005c68 add x9, x9, #0x5f0 ; _g
// w10 = g
0000000100005c6c ldr w10, [x9] ; _g
// w8 += w10
0000000100005c70 add w8, w8, w10
// w10 = b
0000000100005c74 ldr w10, [sp, #0x8]
// w8 += w10
0000000100005c78 add w8, w8, w10
// 返回值存入x0
0000000100005c7c str w8, [sp, #0x4]
0000000100005c80 ldr w8, [sp, #0x4]
0000000100005c84 mov x0, x8
0000000100005c88 ldp x29, x30, [sp, #0x10]
// 栈平衡
0000000100005c8c add sp, sp, #0x20
0000000100005c90 ret
经过上面的还原,不就是之前的原石代码么。。。结果完全一样!大家可以照着这个示例实操一遍,加深印象!
2.2 判断
接下来,我们来看看判断逻辑在汇编中是怎么执行的。
if
先看看我们最为熟悉的if判断,例如👇
int g = 12;
void func(int a, int b) {
if (a > b) {
g = a;
} else {
g = b;
}
}
我们直接用上面的反汇编工具Hopper来查看汇编👇

上图的汇编难吗?其实很简单,做了一个cmp比较,然后执行代码块1和代码块2,就是满足了if的代码块和else的代码块。
cmp指令
cmp 把一个寄存器的内容和另一个寄存器的内容或立即数进行比较,但不存储结果,只是正确的更改标志(cpsr)。
一般cmp做完判断后会进行跳转,后面通常会跟上b指令!
b 跳转指令
b本身代表跳转,后面跟标号会有其他操作:
| 指令名称 | 指令含义 |
|---|---|
bl |
跳转到标号处执行,并且影响lr寄存器的值。用于函数返回。 |
br |
根据寄存器中的值跳转。 |
b.gt |
比较结果是 大于(greater than) 执行标号,否则不跳转。 |
b.ge |
比较结果是 大于等于(greater than or equal to) 执行标号,否则不跳转。 |
b.lt |
比较结果是 小于(less than) 执行标号,否则不跳转。 |
b.le |
比较结果是 小于等于(less than or equal to) 执行标号,否则不跳转。 |
b.eq |
比较结果是 等于(equal) 执行标号,否则不跳转。 |
b.ne |
比较结果是 不等于(not equal) 执行标号,否则不跳转。 |
b.hi |
比较结果是 无符号大于 执行标号,否则不跳转。 |
b.hs |
比较结果是 无符号大于等于 执行标号,否则不跳转。 |
b.lo |
比较结果是 无符号小于 执行标号,否则不跳转。 |
b.ls |
比较结果是 无符号小于等于 执行标号,否则不跳转。 |
⚠️注意:
cmp后跟的标号条件是else。
再回过头看示例的汇编👇
0000000100005c7c b.le loc_100005c94执行的是b.le,所以代码块1就是满足if大于的情况,代码块2就是else的情况。
2.3 循环
然后,我们看看循环的逻辑在汇编中执行的是什么指令。
2.3.1 do-while
首先看看do-while循环,例如👇
void func() {
int nSum = 0;
int i = 0;
do {
nSum = nSum + 1;
i++;
} while (i < 100);
}
Hopper汇编👇

上图汇编也很简单👇
- 先初始化2个变量,2变量的地址是
0xc和0x8,刚好每个4字节(对应int类型) - 接着执行循环
do的部分 -
cmp就是while的判断条件,b.lt就是满足条件后跳转到do部分
2.3.2 while
一样,先看示例👇
void func() {
int nSum = 0;
int i = 0;
while (i < 100) {
nSum = nSum + 1;
i++;
}
}

2.3.3 for
最后我们来看看最常用的for循环,例子👇
void func() {
int nSum = 0;
for (int i = 0; i < 100; i++) {
nSum = nSum + 1;
}
}

⚠️注意:
for和while的汇编中,条件都是通过b.ge来判断的。
2.4 选择
最后我们来看看选择逻辑在汇编中执行的是什么指令。
Switch 选择
void func(int a) {
switch (a) {
case 1:
printf("case 1");
break;
case 2:
printf("case 2");
break;
case 3:
printf("case 3");
break;
default:
printf("case default");
break;
}
}

case > 3个的情况
void func(int a) {
switch (a) {
case 1:
printf("case 1");
break;
case 2:
printf("case 2");
break;
case 3:
printf("case 3");
break;
case 4:
printf("case 4");
break;
default:
printf("case default");
break;
}
}

上图是Hopper中分析的汇编代码,除了开始做的w8-=1之外,其余代码块的代码,接下来我们仔细分析一下👇
- 代码块1
0000000100005be0 mov x9, x8
0000000100005be4 ubfx x9, x9, #0x0, #0x20
0000000100005be8 cmp x9, #0x3
0000000100005bec str x9, [sp]
-
mov x9, x8👉 x8寄存器的值给x9寄存器,就是参数的值。 -
ubfx x9, x9, #0x0, #0x20👉ubfx的意思是针对位来清零(⚠️从高位开始),那么就是将x9的地址值中的0~32位清零(0x0十进制即0,0x20十进制即32) -
cmp x9, #0x3👉 比较 x9 和 0x3 的值。这里0x3是最大 case - 最小 case的差值。 -
str x9, [sp]👉 x9入栈,也就是x8的低32位入栈。
综上,就是参数 - 最小case - (最大case - 最小case),如果b.hi无符号大于了,就直接跳转去default分支。
- 代码块2
0000000100005bf4 adrp x8, #0x100005000
0000000100005bf8 add x8, x8, #0xc64
这里很简单,根据上篇文章02-汇编基础(2)中对adrp指令的分析可知,x8中存储的地址就是0x100005c64。
- 代码块3
0000000100005bfc ldr x11, [sp]
0000000100005c00 ldrsw x10, [x8, x11, lsl #2]
0000000100005c04 add x9, x8, x10
0000000100005c08 br x9
-
ldr x11, [sp]👉 从栈中取数据给x11,栈中目前是x9。x9为x8的低32位 -
ldrsw x10, [x8, x11, lsl #2]👉lsl #2左移2位的意思,那么这句的意思就是x10 = x8 + (x11 << 2) -
add x9, x8, x10👉 很简单,x9 = x8 + x10 -
br x9根据x9寄存器中的值进行跳转。
举例算x9
现在,我们来分析下x9中的值的计算过程(假如参数是2,即调用的func(2))👇
- x9最开始跟x8(即入参值)有关,,那么x9是x8的低32位,x9的值就是1(经过了
subs w8, w8, #0x1减1了),那么ldr x11, [sp]x11也是1 - 接着经过
ldrsw x10, [x8, x11, lsl #2],1(x11) << 2 = 4,然后4 + 0x100005c64(x8的地址) = 0x100005c68(x10的值),查询代码块5,可知x10的值是0xffffffb8👇

0xffffffb8对应的十进制是-72👇

- 接着
add x9, x8, x10,因为add指令算的是十六进制,x10是-72,对应的十六进制是0x48,所以x8+x10 = 0x100005c64 - 0x48(负数变减法) = 0x100005C1C = x9,最终x9就是0x100005C1C
- 代码块4
0000000100005c0c adrp x0, #0x100006000
0000000100005c10 add x0, x0, #0x5c8
0000000100005c14 bl imp___stubs__printf
0000000100005c18 b _func+144
0000000100005c1c adrp x0, #0x100006000
0000000100005c20 add x0, x0, #0x5cf
0000000100005c24 bl imp___stubs__printf
0000000100005c28 b _func+144
0000000100005c2c adrp x0, #0x100006000
0000000100005c30 add x0, x0, #0x5d6
0000000100005c34 bl imp___stubs__printf
0000000100005c38 b _func+144
0000000100005c3c adrp x0, #0x100006000
0000000100005c40 add x0, x0, #0x5dd
0000000100005c44 bl imp___stubs__printf
0000000100005c48 b _func+144
很明显,该代码块的汇编是在执行case代码块的逻辑,上面例子中最终得到x9的值是0x100005C1C,正好跳去case 2的汇编👇

- 代码块5
0000000100005c64 db 0xa8 ; '.' ; DATA XREF=_func+48
0000000100005c65 db 0xff ; '.'
0000000100005c66 db 0xff ; '.'
0000000100005c67 db 0xff ; '.'
0000000100005c68 db 0xb8 ; '.'
0000000100005c69 db 0xff ; '.'
0000000100005c6a db 0xff ; '.'
0000000100005c6b db 0xff ; '.'
0000000100005c6c db 0xc8 ; '.'
0000000100005c6d db 0xff ; '.'
0000000100005c6e db 0xff ; '.'
0000000100005c6f db 0xff ; '.'
0000000100005c70 db 0xd8 ; '.'
0000000100005c71 db 0xff ; '.'
0000000100005c72 db 0xff ; '.'
0000000100005c73 db 0xff ; '.'
该代码块就好比是一张表,可以根据地址查到该地址对应存储的值。
汇编执行过程总结
- 首先通过
参数 - 最小case得到表中index -
index与(最大case - 最小case)无符号比较判断是否在区间内。-
不在区间内 👉跳转defalult -
在区间内 👉表头地址 + index << 2获取偏移地址(为负数)
-
- 根据偏移的地址执行对应
case逻辑。
⚠️注意:表中为什么不直接存地址? 1.地址过长 2.有ASLR的存在
Switch小结
switch语句的
分支< 3的时候没有必要使用表结构,相当于if。各个分支常量的差值较大的时候,编译器会在效率还是内存进行取舍,这个时候编译器还是会编译成类似于if-else的结构。比如:100、200、300、400这种case还是和
if-else相同,10、20、30、40会生成一张表。所以在写switch逻辑的时候最好使用连续的值。至于具体逻辑编译器会根据case和差值进行优化选择。case越多,差值越小,数值越连贯编译器会生成跳转表,否则还是if-else。在分支比较多的时候:在编译的时候会生成一个
表(跳转表每个地址四个字节)。跳转表中数量为
最大case - 最小case + 1为一共有多少种可能性。case分支的代码地址是连续的,使用的是用空间换时间的思想。
总结
- 状态(标志)寄存器 CPSR
- ARM64中cpsr寄存器(32位)为状态寄存器
- 最高4位(28,29,30,31)为标志位。NZ(执行结果) CV(无符号/有符号溢出)
- N标志(负标记位)
- 执行结果负数 N = 1,非负数 N = 0
- Z标志(0标记位)
- 结果为0 Z = 1,结果非0 Z = 0
- C标志(无符号数溢出)
- 加法:进位 C = 1,否则 C = 0
- 减法:借位 C = 0,否则 C = 1
- V标志(有符号数溢出)
- 正数 + 正数 = 负数 溢出 V = 1
- 负数+ 负数 = 正数 溢出 V = 1
- 正数 + 负数 不可能溢出 V = 0
- 溢出
V = 1,不溢出V = 0
- N标志(负标记位)
- 判断、选择和循环


