tag 标签: arm汇编

相关博文
  • 热度 17
    2015-1-28 22:30
    936 次阅读|
    0 个评论
    1. LDR R1, =COUNT 意思是将 COUNT 变量的 地址 放到 R1中 LDR R1, COUNT 意思是将 COUNT 变量地址里面的 内容 赋给 R1 2. Load-Store 结构——这个应该是 RISC设计中比较有特点的一部分。 在 RISC 中,CPU 并不会对内存中的数据进行操作, 所有的计算都要求在寄存器中完成。 而寄存器和内存的通信则由单独的指令来完成。而在 CSIC中,CPU是可以直接对内存进行操作的 ,这也是一个比较特别的地方。所以, 在 ARM中,cpu只能通过寄存器来对内存的数据进行访问和更改。 LDR Rd,(地址) STR Rd, (地址) LDMIA Rn!, regist STMIA Rn!, regist 注意上面 LDR/STR 和 LDMIA/STMIA 的区别,LDR/STR 命令使用时,寄存器在前,地址在后。 而在 LDMIA/STMIA 使用时, 地址在前, 寄存器在后。 这就决定了 LDR 和 LDM 同为加载命令, 但操作顺序是不同的, 同理 STR/STM。 但有一点他们是相同的, 即加载 LDR/LDM的意思是把内存的数据 (即上面的地址) 加载到寄存器; 存储 STR/STM 的意思是把寄存器的内容存储到内存(即上面的地址) 。这样比较之后也就全明白了, 只需明白哪部分是寄存器,哪部分是地址(内存) ,然后区别是加载还是存储,就可以知道操作方向。 LDM/STM指令主要用于现场保护,数据复制,参数传送等 。 3. LDM/ STM IA / IB,DA,DB 数据块传输 FD /ED,EA/ FA 堆栈操作 LDMIA Rn!, regList STMIA Rn!, regList 其中 Rn 加载/存储的起始地址寄存器,Rn 必须为 R0~R7 RegList 加载/存储的起始寄存器列表,寄存器必须为 R0~R7 4. 在汇编程序中 !的使用,意思是回写,比如: ldr r1, ldr lr, ! 其中 ! 用来控制基址变址寻址的的最终新地址是否进行回写操作 此条语句的意思是 执行 ldr 之后 sp 被回 写成 sp+#S_PC 基址变址寻址的新地址。 5. ARM 堆栈的组织结构是 满栈降 的形式,满栈即 sp 是要停留在最后一个进栈元素 ;降,就是堆栈的增长方向是 从高地址向低地址 发展。 ARM 对于堆栈的操作一般采用 LDMFS(pop)和 STMFD(push)两个命令。 难点在于 STMFD 命令对于操作数是按照什么顺序压栈的。 比如:STMFD sp! {R0-R5,LR}进栈顺序是: 高地址 LR #先进栈 R5 R4 ........... R0 - SP 低地址 ARM 指令 多寄存器寻址: LDMIA R0!,{R1- R4} ;R1 ---- ;R2 ---- ;R3 ---- ;R4 ---- 堆栈寻址: STMFD 入栈指令,相当于 STMDB STMFD SP!,{R2- R4} ; --- R4 ; --- R3 ; ---R2 LDMFD 出栈指令,相当于 LDMIA LDMFD SP!,{R6- R8} ;R6 ---- ;R7 ---- ;R8 ---- 6. 汇编语句 LDMFD SP!, {R0-R12, LR, PC }^ 程序后面的^ ,表示什么意思? '^'是一个后缀标志,不能在 User 模式和 Sys 系统模式下使用该标志.该标志有两个存在目的: 1) 对于 LDM 操作,程序会自动的将 spsr 的值拷贝到 cpsr 中。 比如:在 IRQ 中断返回代码中 ldmfd sp!, {r4} // 读取 sp 中保存的的 spsr 值到 r4中 msr spsr_cxsf, r4 // 对 spsr 的所有控制为进行写操作,将 r4的值全部注入 spsr ldmfd sp! {r0 -r12,lr,pc}^ //当指令执行完毕,pc 跳转之前,将 spsr 的值自动拷贝到 cpsr 中 2)数据的送入,送出发生在 User 用户模式下的寄存器,而非当前模式寄存器 如 ldmdb sp, {r0-lr}^;表示 sp 栈中的数据回复到 User 分组寄存器 r0-lr 中,而不是恢复到当前模式寄存器 r0-lr, 当然对于 User, System, IRQ,SVC,Abort, Undefined这6种模式来说 r0-r12是共用的,只是 r13和 r14为分别独有,对于 FIQ 模式,仅仅 r0-r7是和前6种模式的 r0-r7共用,r8-r14都是 FIQ 模式下专有。 7. 关于 ldr/str 几条指令使用的区别 ldr ip, , # 4 将 sp 中内容存入ip,之后 sp=sp+4; ldr ip, 将 sp+4这个新地址下的内容存入ip,之后 sp 值保持不变 ldr ip, ! 将 sp+4这个新地址下的内容存入ip,之后 sp=sp+4将新地址值赋给 sp str ip, , # 4 将ip存入 sp 地址处,之后 sp=sp+4 str ip, 将ip存入 sp+4这个新地址,之后 sp 值保持不变 str ip, ! 将ip存入 sp+4这个新地址,之后 sp=sp+4将新地址值赋给sp 8. movs r1,#3; movs 将导致 ALU 被更改,因为 r1赋值非0,即操作结果 r1非0,所以 ALU 的 Z 标志清0 N,Z,C,V 称为 ALU(算术逻辑单元)状态标志。N:如果结果是负数则置位;Z:如果结果是零则置位;C:如果发生进位则置位;V:如果发生进位则置位。 9. teq r1,#0 //r1-0,将结果送入状态标志,如果 r1和0相减的结果为0,那么 ALU 的Z 置位,否则 Z 清0 bne reschedule//ne 表示 Z 非0,即:不等,那么执行 reschedule 函数 10。 .使用 tst 来检查是否设置了特定的位 tst r1,#0x80   //按位 and 操作,检测 r1的0x17,即第7位是否置1,按位与之后结果为0,那么 ALU 的 Z 置位 beq reset   //如果 Z 置位,即:以上按位与操作结果是0,那么跳转到 reset 标号执行 11.   PC 和 LR 寄存器中在异常发生时,或在系统运行时其 PC 和 LR 寄存器值为多少? 下图为用户模式下 ARM 处理器体系结构:   从图1中我们看到, 在 user 模式下, ARM CPU 有16个数据寄存器, 被命名为 r0~r15(这个要比 x86的多一些)。r13~r15有特殊用途,其中: ◆ r13 - 指向当前栈顶,相当于 x86的 esp,这个东西在汇编指令中要用 sp 表示 ◆ r14 - 称作链接寄存器,指向函数的返回地址。用 lr 表示,这和 x86将返回地址保存在栈中是不同的 ◆ r15 - 类似于 x86的 eip, 其值等于当前正在执行的指令的地址+8(因为在取址和执行之间多了一个译码的阶段),这个用 pc 表示。 另外, ARM 处理器还有一个名为 cspr 的寄存器, 用来监视和控制内部操作, 这点和x86 的状态寄存器是类似的。具体的内容就用到再说了。 总结:在系统正常运行时, PC 值等于当前正在执行的指令的地址+8 ,(因为在取址和执行之间多了一个译码的阶段)。 寄存器 R14(LR 寄存器)有两种特殊功能: 1)在任何一种处理器模式下,该模式对应的 R14寄存器用来保存子程序的返回地址 。 当执行 BL 或 BLX 指令进行子程序调用时,子程序的返回地址被放置在 R14中。这样,只要把 R14内容拷贝到 PC 中,就实现了子程序的返回。 2)当某异常发生时,相应异常模式下的 R14被设置成异常返回的地址(对于某些异常,可能是一个偏移量,一个较小的常量)。异常返回类似于子程序返回,但有小小的不同。 总结:   所谓的子程序的返回地址, 实际就是调用指令的下一条指令的地址, 也就是 BL 或 BLX指令的下一条指令的地址 。所谓的异常的返回的地址,就是异常发生前,CPU 执行的最后一条指令的下一条指令的地址。 例如:(子程序返回地址示例) 指令 指令所在地址 ADD R2,R1,R3 ; 0x300000 BL subC ; 0x300004 MOV R1,# 2 ; 0x300008 BL 指令执行后,R14中保存的子程序 subC 的返回地址是0x300008。 再例如:(异常返回地址示例) 指令 指令所在地址 ADD R2,R1,R3 ; 0x300000 SWI 0x98 ; 0x300004 MOV R1,# 2 ; 0x300008 SWI 指令执行后,进入 SWI 异常处理程序,此时 R14中保存的返回地址为0x300008。 总结:在系统正常运行时, PC 的值存储的是当前正在执行的指令地址的后两条地址(即 +8 ),而 LR 是在子程序返回或异常返回时才使用,其值为当前正在执行的指令的后一条指令地址(即 +4 )。 12.   由于上面 LR 和 PC 寄存器值的特点:我们可以解释软中断实现原理进行解释。   SWI,即 software interrupt 软件中断。该指令产生一个 SWI 异常。意思就是把处理器模式改变为超级用户模式,CPSR 寄存器保存到超级用户模式下的 SPSR 寄存器,并且跳到 SWI 向量。其 ARM 指令格式如下: SWI{cond} immed_24   Cond 域:是可选的条件码 (参见 ARM 汇编指令条件执行详解).   immed_24域:范围从 0 到 224-1 的表达式,(即0-16777215)。用户程序可以使用该常数来进入不同的处理流程。 一、方法1:获取 immed_24操作数。   为了能实现根据指令中 immed_24操作数的不同,跳转到不同的处理程序,所以我们往往需要在 SWI 异常处理子程序中去获得 immed_24操作数的实际内容。获得该操作数内容的方法是在异常处理函数中使用下面指令 LDR R0,   该指令将链接寄存器 LR 的内容减去4后所获得的值作为一个地址,然后把该地址的内容装载进 R0。此时再使用下面指令,immed_24操作数的内容就保存到了 R0: BIC R0,R0,#0x FF 000000 ; Rd, Rn, Oprand2 ; BIC(位清除)指令对 Rn 中的值 和 Operand2 值的反码按位进行逻辑“与”运算     该指令将 R0的高8位(绿色表示的)清零,并把结果保存到 R0,意思就是取 R0的低24位。   所以,在 SWI 异常处理子程序中执行 LDR R0, 语句,实际就是把产生本次 SWI异常的 SWI 指令的内容(如:SWI 0x98)装进 R0寄存器。又因为 SWI 指令的低24位保存了指令的操作数(如: 0x98), 所以再执行 BIC R0, R0, #0xFF000000语句, 就可以获得 immed_24操作数的实际内容。 二、方法2:使用参数寄存器。   实际上,在 SWI 异常处理子程序的实现时,还可以绕开 immed_24操作数的获取操作,这就是说,我们可以不去获取 immed_24操作数的实际内容,也能实现 SWI 异常的分支处理。这就需要使用 R0-R4寄存器,其中 R0-R4可任意选择其中一个,一般选择R0,遵从 ATPCS 原则。   具体方法就是, 在执行 SWI 指令之前, 给 R0赋予某个数值, 然后在 SWI 异常处理子程序中根据 R0值实现不同的分支处理。例如: 指令 指令所在地址 MOV R0,# 1 ; #1给 R0 SWI 0x98 ; 产生 SWI 中断,执行异常处理程序 SoftwareInterrupt ADD R2,R1,R3 ; ; SWI 异常处理子程序如下 SoftwareInterrupt CMP R0, # 6 ; if R0 6 LDRLO PC, ; if R0 6,PC = PC + R0*4,else next MOVS PC, LR SwiFunction DCD function0 ; 0 DCD function1 ; 1 DCD function2 ; 2 DCD function3 ; 3 DCD function4 ; 4 DCD function5 ; 5 Function0 异常处理分支0代码 Function1 异常处理分支1代码 function2 异常处理分支2代码 function3 异常处理分支3代码 function4 异常处理分支4代码 function5 异常处理分支5代码    在 ARM 体系结构中,当正确读取了 PC 的值时,该值为当前指令地址值加8字节,也就是说,对于 ARM 指令集来说,读出的 PC 值指向当前指令的下两条指令的地址,本例中就是指向SwiFunction 表头 DCD function0 这个地址,在该地址中保存了异常处理子分支 function0的入口地址。 所以, 当进入 SWI 异常处理子程序 SoftwareInterrupt 时, 如果 R0=0, 执行 LDRLO PC, 语句后, PC 的内容即为 function0的入口地址, 即程序跳转到了 function0执行。   在本例中, 因为 R0=1, 所以, 实际程序是跳转到了 function1执行。 R0左移2位 (LDRLO PC, ) ,即 R0*4, 是因为 ARM 指令是字(4个字节)对齐的 DCD function0等伪指令也是按4字节对齐的。   在本方法的实现中,实际指令中的24位立即数(immed_24域)被忽略了, 就是说immed_24域可以为任意合法的值。 如在本例中, 不一定使用 SWI 0x98, 还可以为 SWI 0x00或者 SWI 0x01等等,程序还是会进入 SWI 异常处理子程序 SoftwareInterrupt,然后根据 R0的内容跳转到相应的子分支。 13. 在 ARM 中栈底和栈顶的标识如下:满递减栈,栈底在上,栈顶在下是 SP。如下图所示: 14.   下面就两个具体的例子谈谈 ARM 汇编。第一个是使用跳转表解决分支转移问题的例程,源代码如下(保存的时候请将文件后缀名改为 s): AREA JumpTest,CODE,READONLY CODE32 num EQU 4 ENTRY start MOV r0, # 4 MOV r1, # 3 MOV r2, # 2 MOV r3, # 0 CMP r0, #num BHS stop ADR r4, JumpTable CMP r0, # 2 MOVEQ r3, # 0 LDREQ pc, CMP r0, # 3 MOVEQ r3, # 1 LDREQ pc, CMP r0, # 4 MOVEQ r3, # 2 LDREQ pc, CMP r0, # 1 MOVEQ r3, # 3 LDREQ pc, DEFAULT MOVEQ r0, # 0 SWITCHEND stop MOV r0, #0x18 LDR r1, =0x20026 SWI 0x123456 JumpTable DCD CASE1 DCD CASE2 DCD CASE3 DCD CASE4 DCD DEFAULT CASE1 ADD r0, r1, r2 B SWITCHEND CASE2 SUB r0, r1, r2 B SWITCHEND CASE3 ORR r0, r1, r2 B SWITCHEND CASE4 AND r0, r1, r2 B SWITCHEND END   程序其实很简单,可见我有多愚笨!还是 简要介绍一下这段代码吧。首先用 AREA 伪代码加上 CODE, 表明下面引出的将是一个代码段 (于此相对的还有数据段 DATA) , ENTRY 和 END成对出现,说明他们之间的代码是程序的主体。start 段给寄存器初始化。ADR r4, JumpTable一句是将相当于数组的 JumpTable 的地址付给 r4这个寄存器。   stop 一段是用来是程序退出的,第一个语句“MOV r0,#0x18”将 r0赋值为0x18,这个立即数对应于宏 angel_SWIreason_ReportException。表示 r1中存放的执行状态。语句“LDR r1,=0x20026”将 r1的值设置成 ADP_Stopped_ApplicationExit,该宏表示程序正常退出。然后使用SWI,语句“SWI 0x123456”结束程序,将 CPU 的控制权交回调试器手中。   在 JumpTable 表中, DCD 类型的数组包含四个字, 所以, 当实现 CASE 跳转的时候, 需要将给出的索引乘上4,才是真正前进的地址数。 在语句: CMP r0,#num BHS stop   书上意思是: 如果 r0寄存器中的值比 num 大的话, 程序就跳转到 stop 标记的行。 但是,实际测试的时候,我发现如果 r0和 num 相等也能跳转到 stop 标记的行,也就是说只要 r0小于num 才不会跳转。
  • 热度 17
    2015-1-24 23:25
    1387 次阅读|
    0 个评论
    今天看arm的汇编,发现很多有一个小点,但是借来的书上的语法却没有,问同学也不知道,于是在网上查了一番才发现我书上看到的是arm的标准汇编,而有小点的gnu的汇编,于是将收集到的资料整理后放到这里来。 GNU 汇编 语言结构                                                    主要包括三个常用的段:      data      数据段 声明带有初始值的元素      bss       数据段 声明使用0或者null初始化的元素      text      正文段 包含的指令, 每个 汇编 程序都必须包含此段           使用.section 指令定义段, 如:      .section .data      .section .bss      .section .text           起始点:      gnu 汇编 器使用_start标签表示默认的起始点, 此外如果想要 汇编 内部的标签能够被外部程序访问,      需要使用. globl 指令, 如:. globl _start                使用通用库函数时可以使用:      ld -dynamic-linker /lib/ld-linux.so.2 ################################################################################################ # 四, 数据传递                                              ################################################################################################   1, 数据段      使用 .data 声明数据段, 这个段中声明的任何数据元素都保留在内存中并可以被 汇编 程序的指令读取,      此外还可以使用.rodata声明只读的数据段, 在声明一个数据元素时, 需要使用标签和命令:           标签:用做引用数据元素所使用的标记, 它和c语言的变量很相似, 它对于处理器是没有意义的, 它           只是用做 汇编 器试图访问内存位置时用做引用指针的一个位置。           指令:这个名字指示 汇编 器为通过标签引用的数据元素保留特定数量的内存, 声明命令之后必须给出           一个或多个默认值。                声明指令:      .ascii   文本字符串      .asciz   以空字符结尾的字符串      .byte    字节值      .double 双精度浮点值      .float   单精度浮点值      .int     32位整数      .long    32位整数, 和int相同      .octa    16字节整数      .quad    8字节整数      .short   16位整数      .single 单精度浮点数(和float相同)                例子:      output:      .ascii "hello world."           pi:      .float 2.14           声明可以在一行中定义多个值, 如:      ages:      .int 20, 10, 30, 40                定义静态符号:      使用.equ命令把常量值定义为可以在文本段中使用的符号,如:      .section .data      .equ LINUX_SYS_CALL, 0x80      .section .text      movl $LINUX_SYS_CALL, %eax                2, bss段     和data段不同, 无需声明特定的数据类型, 只需声明为所需目的保留的原始内存部分即可。     GNU 汇编 器使用以下两个命令声明内存区域:     .comm           声明为未初始化的通用内存区域     .lcomm          声明为未初始化的本地内存区域         两种声明很相似, 但.lcomm是为不会从本地 汇编 代码之外进行访问的数据保留的, 格式 为:     .comm/.lcomm symbol, length         例子:     .section .bss     .lcomm buffer, 1000     该语句把1000字节的内存地址赋予标签buffer, 在声明本地通用内存区域的程序之外的函数是     不能访问他们的.(不能在. globl 命令中使用他们)             在bss段声明的好处是, 数据不包含在可执行文件中。在数据段中定义数据时, 它必须被包含在     可执行程序中, 因为必须使用特定值初始化它。 因为不使用数据初始化bss段中声明的数据区域,     所以内存区域被保留在运行时使用, 并且不必包含在最终的程序中                 3, 传送数据      move 指令:      格式 movex 源操作数, 目的操作数。 其中x为要传送数据的长度, 取值有:      l 用于32位的长字节      w 用于16位的字      b 用于8位的字节值                立即数前面要加一个$符号, 寄存器前面要加%符号。           8个通用的寄存器是用于保存数据的最常用的寄存器, 这些寄存器的内容可以传递      给其他的任何可用的寄存器。 和通用寄存器不同, 专用寄存器(控制, 调试, 段)      的内容只能传送给通用寄存器, 或者接收从通用寄存器传过来的内容。                在对标签进行引用时:      例:      .section .data      value:      .int 100      _start:      movl value, %eax      movl $value, %eax      movl %ebx, (%edi)      movl %ebx, 4(%edi)           其中:movl value, %eax     只是把标签value当前引用的内存值传递给eax           movl $value, %eax    把标签value当前引用的内存地址指针传递给eax           movl %ebx, (%edi)    如果edi外面没有括号那么这个指令只是把ebx中的                               值加载到edi中, 如果有了括号就表示把ebx中的内容                               传送给edi中包含的内存位置。           movl %ebx, 4(%edi) 表示把edi中的值放在edi指向的位置之后的4字节内存位置中           movl %ebx, -4(%edi) 表示把edi中的值放在edi指向的位置之前的4字节内存位置中                                   cmove 指令(条件转移):     cmovex 源操作数, 目的操作数. x的取值为:     无符号数:     a/nbe    大于/不小于或者等于     ae/nb    大于或者等于/不小于     nc       无进位     b/nae    小于/不大于等于     c        进位     be/na    小于或等于/不大于     e/z      等于/零     ne/nz    不等于/不为零     p/pe     奇偶校验/偶校验     np/po    非奇偶校验/奇校验         有符号数:     ge/nl    大于或者等于/不小于     l/nge    小于/不大于或者等于     le/ng    小于或者等于/不大于     o        溢出     no       未溢出     s        带符号(负)     ns       无符号(非负)                         交换数据:     xchg     在两个寄存器之间或者寄存器和内存间交换值     如:     xchg 操作数, 操作数, 要求两个操作数必须长度相同且不能同时都是内存位置     其中寄存器可以是32,16,8位的             bswap    反转一个32位寄存器的字节顺序         如: bswap %ebx             xadd 交换两个值 并把两个值只和存储在目标操作数中         如: xadd 源操作数,目标操作数     其中源操作数必须是寄存器, 目标操作数可以是内存位置也可以是寄存器     其中寄存器可以是32,16,8位的         cmpxchg     cmpxchg source, destination     其中source必须是寄存器, destination可以是内存或者寄存器, 用来比较     两者的值, 如果相等,就把源操作数的值加载到目标操作数中, 如果不等就把     目标操作数加载到源操作数中,其中寄存器可以是32,16,8位的, 其中源操作     数是EAX,AX或者AL寄存器中的值             cmpxchg8b 同cmpxchg, 但是它处理8字节值, 同时它只有一个操作数     cmpxchg8b destination     其中destination引用一个内存位置, 其中的8字节值会与EDX和EAX寄存器中     包含的值(EDX高位寄存器, EAX低位寄存器)进行比较, 如果目标值和EDX:EAX     对中的值相等, 就把EDX:EAX对中的64位值传递给内存位置, 如果不匹配就把     内存地址中的值加载到EDX:EAX对中             4, 堆栈     ESP 寄存器保存了当前堆栈的起始位置, 当一个数据压入栈时, 它就会自动递减,         反之其自动递增         压入堆栈操作:     pushx source, x取值为:     l 32位长字     w 16位字         弹出堆栈操作:     popx source     其中source必须是16或32位寄存器或者内存位置, 当pop最后一个元素时ESP值应该     和以前的相等         5,压入和弹出所有寄存器     pusha/popa     压入或者弹出所有16位通用寄存器     pushad/popad   压入或者弹出所有32位通用寄存器     pushf/popf     压入或者弹出EFLAGS寄存器的低16位     pushfd/popfd   压入或者弹出EFLAGS寄存器的全部32位       6,数据地址对齐     gas 汇编 器支持.align 命令, 它用于在特定的内存边界对准定义的数据元素, 在数据段     中.align命令紧贴在数据定义的前面 比较: cmp operend1, operend2 进位标志修改指令:     CLC          清空进位标志(设置为0)     CMC          对进位标志求反(把它改变为相反的值)     STC          设置进位标志(设置为1)         循环: loop            循环直到ECX寄存器为0 loope/loopz     循环直到ecx寄存器为0 或者没有设置ZF标志 loopne/loopnz   循环直到ecx为0或者设置了ZF标志 指令 格式 为: loopxx address    注意循环指令只支持8位偏移地址 以上转自 http://hi.baidu.com/walkingman520/blog/item/7296bbeec777012a2cf5344a.html 另有一个比较篇的如下: ARM汇编和Gnu汇编的转换 将ARM ADS下的汇编码移植到GCC for ARM编译器时,有如下规则: 1, 注释行以"@"或"/* ... */"代替";" 2, GET或INCLUDE = .INCLUDE 如:get option.a =     .include "option.a" 3, EQU = .equ TCLK2   EQU   PB25    =         .equ   TCLK2, PB25      SETA == .equ      SETL == .equ BUSWIDTH SETA 16   = .equ BUSWIDTH, 16 4, EXPORT = .global      IMPORT = .extern      GBLL = .global      GBLA = .global 5, DCD = .long 6, IF :DEF: = .IFDEF      ELSE = .ELSE      ENDIF = .ENDIF      :OR:    = |      :SHL:    =      7,   END    =.end NOTE:在被include的头文件中,如"option.a"中,不再需要.end,否则会导致主汇编程序结束。 8,   符号定义加":"号 Entry =    Entry: AREA Word, CODE, READONLY == .text AREA Block, DATA, READWRITE == .data CODE32     == .arm CODE16     == .thumb 9,   MACRO ==   .macro      MEND   == .endm