常用指令
cmp(Compare)比较指令 把一个寄存器的内容和另一个寄存器的内容或立即数进行比较。但不存储结果,只是正确的更改标志。一般CMP做完判断后会进行跳转,后面通常会跟上B指令!
BL 标号:跳转到标号处执行
B.LE 标号:比较结果是小于等于(less than or equal),执行标号,否则不跳转
B.GT 标号:比较结果是大于(greater than),执行标号,否则不跳转
B.GE 标号:比较结果是大于等于(greater than or equal to),执行标号,否则不跳转
B.EQ 标号:比较结果是等于,执行标号,否则不跳转
B.HI 标号:比较结果是无符号大于,执行标号,否则不跳转
ldrsw:[X8,X9,LSL#2]` 表示以 X8 为基地址,X9的二进制左移2位,再相加得到一个新的地址值。
例如:A表示(X8 为基地址),B 表示(X9的二进制左移2位)
那么 X10 = [A B];
if
void func(int a, int b){
if(a > b){
printf("a大于b");
}else{
printf("a不大于b");
}
}
上面代码的汇编如下:
demo1`func:
0x100f926a0 <+0>: sub sp, sp, #0x20 ; =0x20 //拉升栈空间
0x100f926a4 <+4>: stp x29, x30, [sp, #0x10]
0x100f926a8 <+8>: add x29, sp, #0x10 ; =0x10
0x100f926ac <+12>: stur w0, [x29, #-0x4]
0x100f926b0 <+16>: str w1, [sp, #0x8]
0x100f926b4 <+20>: ldur w0, [x29, #-0x4]
0x100f926b8 <+24>: ldr w1, [sp, #0x8]
//上面这些代码不用管,全是函数对局部变量的操作
//比较w0和w1的值
0x100f926bc <+28>: cmp w0, w1
//如果w0<=w1 跳转到地址为0x100f926d8的指令
0x100f926c0 <+32>: b.le 0x100f926d8 ; <+56> at main.m:17
//下面两句代码得到的x0对应的常量值为0x100f9365c
0x100f926c4 <+36>: adrp x0, 1
0x100f926c8 <+40>: add x0, x0, #0x65c ; =0x65c
//打印
0x100f926cc <+44>: bl 0x100f92a68 ; symbol stub for: printf
0x100f926d0 <+48>: str w0, [sp, #0x4]
//跳转地址为0x100f926e8指令-->相当于高级语音中的goto
0x100f926d4 <+52>: b 0x100f926e8 ; <+72> at main.m:19
//下面两句代码得到的x0对应的常量值为0x100f93665
0x100f926d8 <+56>: adrp x0, 1
0x100f926dc <+60>: add x0, x0, #0x665 ; =0x665
//打印
0x100f926e0 <+64>: bl 0x100f92a68 ; symbol stub for: printf
0x100f926e4 <+68>: str w0, [sp]
//栈平衡
0x100f926e8 <+72>: ldp x29, x30, [sp, #0x10]
0x100f926ec <+76>: add sp, sp, #0x20 ; =0x20
//返回
0x100f926f0 <+80>: ret
while
先补充两个指令
Zero Register: 在大多数情况下,作为源寄存器使用时, r31读出来的值 是0; 作为目标寄存器使用时, 丢弃结果。 WZR(word zero rigiser)或者XZR(64位)
Stack Register: 当 用作load/store 的base register时, 或者 一些算术指令中, r31提供当前的stack pointer WSP或者 SP
void func(int a, int b){
while(a < b){
printf("a大于b");
}
}
上面代码的汇编如下:
0x1009ca700 <+0>: sub sp, sp, #0x10 ; =0x10
//将0寄存器的值写进内存
0x1009ca704 <+4>: str wzr, [sp, #0xc]
//将刚刚存的值取出来并放在w8寄存器中
0x1009ca708 <+8>: ldr w8, [sp, #0xc]
//比较w8 和 10的大小
0x1009ca70c <+12>: cmp w8, #0xa ; =0xa
如果w8 >= 10,跳转0x1009ca724
0x1009ca710 <+16>: b.ge 0x1009ca724 ; <+36> at main.m:18
//从内存拿出值赋值w8
0x1009ca714 <+20>: ldr w8, [sp, #0xc]
//w8 += 1
0x1009ca718 <+24>: add w8, w8, #0x1 ; =0x1
//将w8值写进内存
0x1009ca71c <+28>: str w8, [sp, #0xc]
//跳转到0x1009ca708
0x1009ca720 <+32>: b 0x1009ca708 ; <+8> at main.m:15
//栈平衡
0x1009ca724 <+36>: add sp, sp, #0x10 ; =0x10
0x1009ca728 <+40>: ret
for / do-while基本和while类似,此处就不再赘述
Switch
- 1、假设switch语句的分支比较少的时候(例如3,少于4的时候没有意义)没有必要使用此结构,相当于if。
- 2、各个分支常量的差值较大的时候,编译器会在效率还是内存进行取舍,这个时候编译器还是会编译成类似于if,else的结构。
- 3、在分支比较多的时候:在编译的时候会生成一个表(跳转表每个地址四个字节)。
CBZ
比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
void func(int a){
switch (a) {
case 0:
{
printf("0");
}
break;
case 1:
{
printf("1");
}
break;
default:
break;
}
}
上面代码的汇编代码如下
demo1`func:
0x1005ae6a0 <+0>: sub sp, sp, #0x30 ; =0x30
0x1005ae6a4 <+4>: stp x29, x30, [sp, #0x20]
0x1005ae6a8 <+8>: add x29, sp, #0x20 ; =0x20
0x1005ae6ac <+12>: stur w0, [x29, #-0x4]
0x1005ae6b0 <+16>: ldur w0, [x29, #-0x4]
0x1005ae6b4 <+20>: mov x8, x0
0x1005ae6b8 <+24>: stur w8, [x29, #-0x8]
//上面全是栈操作和其他无关紧要操作
//如果传进来的w0的值为0,就跳转地址0x1005ae6d8的指令,否则继续往下执行
0x1005ae6bc <+28>: cbz w0, 0x1005ae6d8 ; <+56> at main.m:17
//跳转地址为0x1005ae6c4的指令
0x1005ae6c0 <+32>: b 0x1005ae6c4 ; <+36> at main.m
//拿到w8=x0的值
0x1005ae6c4 <+36>: ldur w8, [x29, #-0x8]
//w9=w8 - 1
0x1005ae6c8 <+40>: subs w9, w8, #0x1 ; =0x1
//将w9的值存入内存
0x1005ae6cc <+44>: stur w9, [x29, #-0xc]
//w9=0,就跳转地址为0x1005ae6ec的指令,否则不跳转
0x1005ae6d0 <+48>: b.eq 0x1005ae6ec ; <+76> at main.m:22
//跳转地址为0x1005ae700指令
0x1005ae6d4 <+52>: b 0x1005ae700 ; <+96> at main.m:28
//下面三句代码是一个打印,printf("0");
0x1005ae6d8 <+56>: adrp x0, 1
0x1005ae6dc <+60>: add x0, x0, #0x670 ; =0x670
0x1005ae6e0 <+64>: bl 0x1005aea7c ; symbol stub for: printf
0x1005ae6e4 <+68>: str w0, [sp, #0x10]
//跳转汇编为0x1005ae704的指令
0x1005ae6e8 <+72>: b 0x1005ae704 ; <+100> at main.m:31
//下面三句是打印
0x1005ae6ec <+76>: adrp x0, 1
0x1005ae6f0 <+80>: add x0, x0, #0x672 ; =0x672
0x1005ae6f4 <+84>: bl 0x1005aea7c ; symbol stub for: printf
0x1005ae6f8 <+88>: str w0, [sp, #0xc]
//跳转地址为0x1005ae704指令
0x1005ae6fc <+92>: b 0x1005ae704 ; <+100> at main.m:31
0x1005ae700 <+96>: b 0x1005ae704 ; <+100> at main.m:31
0x1005ae704 <+100>: ldp x29, x30, [sp, #0x20]
0x1005ae708 <+104>: add sp, sp, #0x30 ; =0x30
0x1005ae70c <+108>: ret
void func(int a){
switch (a) {
case 0:
{
printf("0");
}
break;
case 1:
{
printf("1");
}
break;
case 2:
{
printf("2");
}
break;
case 3:
{
printf("3");
}
break;
default:
break;
}
}
上面代码的汇编如下
0x1002e2658 <+0>: sub sp, sp, #0x40 ; =0x40
0x1002e265c <+4>: stp x29, x30, [sp, #0x30]
0x1002e2660 <+8>: add x29, sp, #0x30 ; =0x30
0x1002e2664 <+12>: stur w0, [x29, #-0x4]
0x1002e2668 <+16>: ldur w0, [x29, #-0x4]
0x1002e266c <+20>: mov x8, x0
0x1002e2670 <+24>: mov x0, x8
//以上代码全是栈操作这里就不做分析了
//w0 = w0 - 3;
0x1002e2674 <+28>: subs w0, w0, #0x3 ; =0x3
//将x8的值写入内存
0x1002e2678 <+32>: stur x8, [x29, #-0x10]
//将w0写入内存
0x1002e267c <+36>: stur w0, [x29, #-0x14]
//如果w0>0就跳转到地址为0x1002e26ec的指令,否则不跳转
0x1002e2680 <+40>: b.hi 0x1002e26ec ; <+148> at main.m:37
//将常量0的地址赋值给x8 x8 = 0
0x1002e2684 <+44>: adrp x8, 0
0x1002e2688 <+48>: add x8, x8, #0x6fc ; =0x6fc
//将上面0x1002e2678存入内存的值取出来
0x1002e268c <+52>: ldur x9, [x29, #-0x10]
//这个是x10 = x8 + x9<<2,得出系统给我们存的switch的那个表值得地址
0x1002e2690 <+56>: ldrsw x10, [x8, x9, lsl #2]
//x8 = x10 + x8
0x1002e2694 <+60>: add x8, x10, x8
//计算得到x8的值后,跳转到x8指向的地址值
0x1002e2698 <+64>: br x8
//下面3句打印
0x1002e269c <+68>: adrp x0, 1
0x1002e26a0 <+72>: add x0, x0, #0x66c ; =0x66c
0x1002e26a4 <+76>: bl 0x1002e2a78 ; symbol stub for: printf
//保护w0
0x1002e26a8 <+80>: str w0, [sp, #0x18]
//跳转地址为0x1002e26f0的指令
0x1002e26ac <+84>: b 0x1002e26f0 ; <+152> at main.m:40
//下面3句打印
0x1002e26b0 <+88>: adrp x0, 1
0x1002e26b4 <+92>: add x0, x0, #0x66e ; =0x66e
0x1002e26b8 <+96>: bl 0x1002e2a78 ; symbol stub for: printf
0x1002e26bc <+100>: str w0, [sp, #0x14]
0x1002e26c0 <+104>: b 0x1002e26f0 ; <+152> at main.m:40
//下面3句打印
0x1002e26c4 <+108>: adrp x0, 1
0x1002e26c8 <+112>: add x0, x0, #0x670 ; =0x670
0x1002e26cc <+116>: bl 0x1002e2a78 ; symbol stub for: printf
0x1002e26d0 <+120>: str w0, [sp, #0x10]
0x1002e26d4 <+124>: b 0x1002e26f0 ; <+152> at main.m:40
//下面3句打印
0x1002e26d8 <+128>: adrp x0, 1
0x1002e26dc <+132>: add x0, x0, #0x672 ; =0x672
0x1002e26e0 <+136>: bl 0x1002e2a78 ; symbol stub for: printf
0x1002e26e4 <+140>: str w0, [sp, #0xc]
0x1002e26e8 <+144>: b 0x1002e26f0 ; <+152> at main.m:40
0x1002e26ec <+148>: b 0x1002e26f0 ; <+152> at main.m:40
//栈平衡
0x1002e26f0 <+152>: ldp x29, x30, [sp, #0x30]
0x1002e26f4 <+156>: add sp, sp, #0x40 ; =0x40
0x1002e26f8 <+160>: ret
执行过程
- switch 内分支比较多的时候(超过4个),在编译的时候会生成一个表(跳转表每个地址四个字节);
- 先判断是否是 default 分支;
- 根据相应的操作计算出要跳转的地址;
这个是x10 = x8 + x9<<2,得出系统给我们存的switch的那个表值得地址
0x1002e2690 <+56>: ldrsw x10, [x8, x9, lsl #2]
通过计算得到跳转的指令的地址
0x1002e2694 <+60>: add x8, x10, x8
- 跳转到X8的地址;
0x1002e2698 <+64>: br x8
注意
虽然说正常分支超过4个,编译器就会生成一张表;但是也有不生成表的,比如你一定要写出case1,case 1000,case 200,编译器存储是连续的地址,会根据1,2,3...1000,如果还按照表的形式会浪费大量的空间,编译器会在效率和存储空间做出最合理的选择,将他们做成if,else的形式表现出来。
总结
switch语句 和 if...else语句执行效率问题,通过上面汇编代码得出结论:
当 switch 分支和 if...else的条件判断小于4的时候,执行效率是一样的;
当 switch 分支和 if...else的条件判断大于等于4的时候,switch 执行效率更高。(你的case是连续常量)