ARM汇编

ARM架构介绍

内部寄存器

cortex-M3/M4/A7都有r0-r12通用寄存器、r13(SP)栈指针、r14(LR)链接寄存器、r15(PC)寄存器,如图所示:

../../_images/image-20210713083838833.png

../../_images/image-20210713083854409.png

运行模式

Cortex-A7架构的运行模式有9种,除了User属于非特权模式,其它8种都是特权模式。

模式 描述
User 用户模式,非特权模式,大部分程序运行的时候就处于此模式(不能访问系统全部资源)
Sys(System) 系统模式,用于运行特权级的操作系统任务
FIQ 快速中断模式,进入 FIQ 中断异常
IRQ 一般中断模式
ABT(Abort) 数据访问终止模式,用于虚拟存储以及存储保护
SVC(Supervisor) 超级管理员模式,供操作系统使用
UND(Undef) 未定义指令终止模式
MON(Monitor) 用于安全扩展模式
Hyp 用于虚拟化扩展

运行模式可以通过软件、中断或者异常来进行切换。用户模式下需要借助异常来完成模式切换,要切换模式的时候,应用程序可以产生异常,在异常的处理过程中完成处理器模式切换。

所谓“运行模式”,可以这样简单理解:

  1. 板子上电时,CPU处于SVC模式,它用的是SVC模式下的寄存器;

  2. 程序运行时发生了中断,CPU进入IRQ模式,它用的IRQ模式下的寄存器;

  3. CPU处理完中断,它切换回SVC模式,继续使用SVC模式下的寄存器

  4. CPU发生某种异常时,比如读取内存出错,它会进入ABT模式,使用ABT模式下的寄存器来处理错误。

在某种模式下,CPU执行时使用的是这种模式的资源,比如使用的是这组模式的寄存器。这样就可以免去保存上一个模式所使用的寄存器。

寄存器组

Cortex-A7 有 9 种运行模式,每一种运行模式都有一组与之对应的寄存器组,如下图:

../../_images/image-20210720111831851.png

每种模式下有其特定的寄存器,例如在FIQ运行模式下访问R13其实访问SP_fiq寄存器。总的来说可以分为4组:

  1. 未备份寄存器,即 R0~R7

在所有模式下R0~R7都是同一物理寄存器,所以从一种模式切换到另外一种模式,里面的数据肯定会被破坏。

  1. 备份寄存器,即 R8~R14

  • R8~R12 寄存器有两种物理寄存器,FIQ 是快速中断模式,这个中断模式要求快速执行!因为 FIQ 模式下的 R8~R12 是独立的,因此中断处理程序可以不用保存和恢复R8~R12,从而加速中断的执行过程

    • 在快速中断模式下(FIQ)它们对应着Rx_irq(x=8~12)物理寄存器,

    • 其他模式下对应着 Rx(8~12)物理寄存器。

  • R13(SP) :栈指针,有8个物理寄存器,User和Sys模式共用,其余七个在不同模式下使用不同的寄存器

  • R14(LR)来存放当前子程序的返回地址:如果使用 BL 或者 BLX来调用子函数的话,R14(LR)被设置成该子函数的返回地址,在子函数中,将 R14(LR)中的值赋给 R15(PC)即可完成子函数返回,如mov pc,lr

  1. 程序计数器 ,即 R15:总是指向“正在取指”的指令

    因为ARM处理器是三级流水线:取指->译码->执行,循环执行。比如当前正在执行第一条指令的同时也对第二条指令进行译码,第三条指令也同时被取出存放在 R15(PC)中,即 R15(PC)总是指向当前正在执行指令地址再加上 2 条指令的地址,如下图。

    ../../_images/image-20210720140259667.png 处理器处于ARM状态时,每条指令为4个字节, 处理器处于Thumb状态时,每条指令为2字节,所以

    1. ARM状态:R15(PC) = 当前执行指令地址 + 4*2字节

    2. Thumb状态:R15(PC) = 当前执行指令地址 + 2*2字节

  2. 程序状态寄存器

程序状态寄存器PSR可以分成当前程序状态寄存器CPSR与备份程序状态寄存器SPSR。

所有运行模式都共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问,但是所有运行模式都共用一个 CPSR 必然会导致冲突,因此除了 User 和 Sys 模式以外,其他 7 个模式都配备一个专用的物理状态寄存器,叫做 备份程序状态寄存器(SPSR),当特定异常中断发生时,SPSR用来保存CPSR的值,当异常退出以后可以用 SPSR 中保存的值来恢复 CPSR。

由于 SPSR 是 CPSR 的备份,因此 SPSR 和 CPSR 的寄存器结构相同,如下图:

../../_images/pu0s3qolqhn.png

  • N(bit31):当两个补码表示的有符号整数运算的时候, N=1 表示运算结果为负数, N=0表示结果为正数。

  • Z(bit30): Z=1 表示运算结果为零, Z=0 表示运算结果不为零,对于 CMP 指令, Z=1 表示进行比较的两个数大小相等。

  • C(bit29)

    • 在加法指令中,当结果产生了进位,则 C=1,表示无符号数运算发生上溢,其它情况下 C=0

    • 在减法指令中,当运算中发生借位,则 C=0,表示无符号数运算发生下溢,其它情况下 C=1

    • 对于包含移位操作的非加/减法运算指令, C 中包含最后一次溢出的位的数值

    • 对于其它非加/减运算指令, C 位的值通常不受影响。

  • V(bit28): 对于加/减法运算指令,当操作数和运算结果表示为二进制的补码表示的带符号数时, V=1 表示符号位溢出,通常其他位不影响 V 位。

  • Q(bit27): 仅 ARM v5TE_J 架构支持,表示饱和状态, Q=1 表示累积饱和, Q=0 表示累积不饱和。

  • IT1:0: 和 IT7:2一起组成 IT[7:0],作为 IF-THEN 指令执行状态。

  • J(bit24):控制指令执行状态,表明本指令是ARM指令还是Thumb指令,如表

J T 描述
0 0 ARM
0 1 Thumb
1 1 ThumbEE
1 0 Jazelle
  • GE[3:0](bit19:16): SIMD 指令有效,大于或等于。

  • IT[7:2](bit15:10): 参考 IT[1:0]。

  • E(bit9): 大小端控制位, E=1 表示大端模式, E=0 表示小端模式。

  • A(bit8): 禁止异步中断位, A=1 表示禁止异步中断。

  • I(bit7): I=1 禁止 IRQ, I=0 使能 IRQ。

  • F(bit6): F=1 禁止 FIQ, F=0 使能 FIQ。

  • T(bit5): 控制指令执行状态,表明本指令是 ARM 指令还是 Thumb 指令,通常和 J(bit24)一起表明指令类型,参考 J(bit24)位。

  • M[4:0]: 处理器模式控制位,如下表

M[4:0] 运行模式
10000 User 模式
10001 FIQ 模式
10010 IRQ 模式
10011 Supervisor(SVC)模式
10110 Monitor(MON)模式
10111 Abort(ABT)模式
11010 Hyp(HYP)模式
11011 Undef(UND)模式
11111 System(SYS)模式

汇编指令

指令集

  1. ARM指令集,这是32位的,每条指令占据4个字节,高效,但是太占空间

  2. Thumb指令集,这是16位的,每条指令占据2字节,节省空间要节省空间时用Thumb指令,要效率时用ARM指令

一个CPU既可以运行Thumb指令,也能运行ARM指令。程序状态寄存器中有一位T(bit5),它等于1时表示当前运行的是Thumb指令。

问题:假设函数A是使用Thumb指令写的,函数B是使用ARM指令写的,怎么让CPU在执行A函数是进入Thumb状态,在执行B函数时进入ARM状态?

答:调用函数A时,让PC寄存器的BIT0等于1,即:PC=函数A地址+(1<<0);调用函数B时,让PC寄存器的BIT0等于0:,即:PC=函数B地址,是不是如此麻烦。

所以引入Thumb2指令集,它支持16位指令、32位指令混合编程,这样自动实现切换。总结下就出现下面的三种指令集:

  • CODE32:表示使用ARM指令集

  • CODE16:表示使用Thumb指令集

  • THUMB:表示使用Thumb2指令集

汇编代码分析

代写……

汇编调用C函数

在arm中有个ATPCS规则(ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)。约定r0-r15寄存器的用途:

../../_images/image-20210723084017968.png

例如:

/* c 函数 */
int delay(unsigned int d)
{
	while (d--);
    return 0;
}

在汇编里调用delay函数的方式:

ldr  r0, =1000000   /* 给delay函数传参数,保存在r0里 */
bl delay
cmp r0, #0          /* 返回值保存在r0中 */

内敛汇编(C调用汇编)

内敛汇编代码清单1:

int add(int a, int b)
{
 int sum;
 __asm__ volatile (
     "add %0, %1, %2"    // 汇编代码:实现sum=a+b
     :"=r"(sum)          // 结果,存放在sum中,汇编代码中可以用%0引用
     :"r"(a), "r"(b)	 // 把a和b放入某些寄存器,汇编代码可以使用%1 %2引用
     :"cc"
 );
 return sum;
}

内敛汇编代码清单2

int add(int a, int b)
{
 int sum;
 __asm__ volatile (
     "add %[result], %[val1], %[val2]" // 汇编代码:实现sum=a+b
     :[result]"=r"(sum)                // 结果,存放在sum中,汇编代码中用%[result]引用
     :[val1]"r"(a), [val2]"r"(b)	   // 把a和b放入寄存器,汇编代码使用%[val1] %[val2]引用
     :"cc"
 );
 return sum;
}

内敛汇编语法

 asm [ asm-qualifiers ] (  
         assembler template                   /* 汇编代码,每行用双引号"",换行用\n */
         [ : output operands ]                /* 输出操作 */
         [ : input operands  ]                /* 输入操作 */
         [ : list of clobbered registers ]    /* 影响的寄存器列表 */
         );
  • asm:也可以写作__asm__,表示这是一段内联汇编;

  • asm-qualifiers:有三个取值

    • volatile:告诉编译器不要随便优化这段代码,例如使用mov r0, r0用来做短暂延时。

    • inline:不讲

    • goto:不讲

  • assembler template:具体汇编指令代码,双引号,\n分开;

  • output operands:输出操作数,格式如下:

    [ [asmSymbolicName] ] constraint (cvariablename)
    
    • asmSymbolicName:符号名,随便,也可以不写;

    • constraint:表示约束;

| constraint | 描述                                                         |
| ---------- | ------------------------------------------------------------ |
| m          | memory operand,表示要传入有效的地址,只要CPU能支持该地址,就可以传入 |
| r          | register operand,寄存器操作数,使用寄存器来保存这些操作数   |
| i          | immediate integer operand,表示可以传入一个立即数            |
`constraint`前还可以加上一些修饰字符,比如“=r”、“+r”、“=&r”,含义如下:
| constraint Modifier Characters | 描述                                         |
| ------------------------------ | -------------------------------------------- |
| =                              | 表示内联汇编会修改这个操作数,即:写         |
| +                              | 这个操作数即被读,也被写                     |
| &                              | 它是一个earlyclobber操作数  ,**后面重点讲** |
  • cvariablename:C语言的变量名:

    示例1:汇编代码中会通过某个寄存器把结果写入c语言的sum变量。在汇编代码中可以使用“%[result]”来引用它

    [result] "=r" (sum)
    

    示例2:变量a、b的值会放入某些寄存器。在汇编代码中可以使用%1、%2等使用它们。

    "r"(a), "r"(b)
    
  • InputOperands:输入操作符,接收输入参数。

    格式如下:

    [ [asmSymbolicName] ] constraint (cexpression)
    
    • asmSymbolicName:符号名,随便取,也可以不写;

    • constraint:表示约束,参考上一小节,跟OutputOperands类似;

    • cexpression:C语言的表达式。

    **示例1:**它的意思变量a、b的值会放入某些寄存器。在汇编代码中可以使用%[a_val]、%[b_val]使用它们

     [a_val]"r"(a), [b_val]"r"(b)
    

    **示例2:**变量a、b的值会放入某些寄存器。在汇编代码中可以使用%0、%1等使用它们

    "r"(a), "r"(b)
    
  • Clobbers:在汇编代码中,对于OutputOperands所涉及的寄存器、内存,肯定是做了修改。但是汇编代码中,也许要修改的寄存器、内存会更多。比如在计算过程中可能要用到r3保存临时结果,我们必须在Clobbers中声明r3会被修改。如下示例所示:

    : "r0", "r1", "r2", "r3", "r4", "r5", "memory"
    

    常用Clobbers如下所示:

Clobbers 描述
cc 表示汇编代码会修改flags register
memory 表示汇编代码中,除了InputOperandsOutputOperands中指定的之外, 还会会读、写更多的内存

earlyclobber

首先看下面的实例:

输出操作数%0对应的寄存器是r3,输入操作数%1对应的寄存器也是r3。第8行更新了%0的值后,第9行又修改%1的值,由于%0、%1是同一个寄存器r3,所以%0的值也被修改了。而我们的最终返回的累加值在%0中存取,所以导致返回值错误。

../../_images/image-20210207102931347.png

解决方案:在第11行加“&”就可以了,这是告诉编译器,对于%0操作数它是earlyclobber的,不能跟其他操作数共用寄存器

../../_images/image-20210207103226044.png