ARM指令集详解

ARM指令集详解ARM 指令集详解 1 nbsp nbsp nbsp nbsp nbsp nbsp 汇编 1 1 nbsp nbsp nbsp nbsp 通用寄存器 nbsp 通用寄存器 37 个寄存器 31 个通用寄存器 6 个状态寄存器 R13 堆栈指针 sp R14 返回指针 R15 为 PC 指针 nbsp cpsr c 代表的是这 32 位中的低 8 位 也就是控制位 CPSR 有 4 个 8 位区域 标志域 F 状态域 S 扩展域 X 控制域 C MSR Loadspecifie

ARM指令集详解

1.       汇编

1.1.    通用寄存器

ARM指令集详解

 

通用寄存器

37个寄存器,31个通用寄存器,6个状态寄存器,R13堆栈指针sp,R14返回指针,R15为PC指针, cpsr_c代表的是这32位中的低8位,也就是控制位

 

 

 ARM指令集详解

 

CPSR寄存器

FIQ和IRQ的区别?

 

MODE(以下为二进制)

10000

用户模式

PC,CPSR,R0~R14

10001

FIQ

PC,CPSR,SPSR_fiq,R14_fiq~R8_fiq,R7~R0

10010

IRQ

PC,CPSR,SPSR_irq,R14_irq~R13_irq,R12~R0

10011

管理模式(svc)

PC,CPSR,SPSR_svc,R14_svc~R13_svc,R12~R0

10111

终止模式

PC,CPSR,SPSR_abt,R14_abt~R13_abt,R12~R0

11011

未定义

PC,CPSR,SPSR_und,R14_und~R13_und,R2~R0

11111

系统模式(sys)

PC,CPSR,R14 ~R0

 

1.2.    指令格式

1) 基本格式

     

{

}{S}

,

{,

}




       其中,<>内的项是必须的,{}内的项是可选的,如

是指令助记符,是必须的,而{

}为指令执行条件,是可选的,如果不写则使用默认条件AL(无条件执行)。

       opcode  指令助记符,如LDR,STR 等

       cond  执行条件,如EQ,NE 等

       S  是否影响CPSR 寄存器的值,书写时影响CPSR,否则不影响

       Rd  目标寄存器

       Rn  第一个操作数的寄存器

       operand2  第二个操作数

       指令格式举例如下:

         LDREX–这条指令主要是从memory中取一个数,然后放到register中,但是相比普通的LDR指令,在于其内在的原子操作特性, 信号量和spin lock这些东西最核心的事情基本上就是load-update-store序列,为了防止并发,必须保证这个序列是原子的,所谓原子,即处理器在执行这个指令序列时,得绝对占有处理器而不能够被切换出去。在ARM上,从V6开始,指令LDREX和STREX就是用来干这事的

2) 第2个操作数

        在ARM 指令中,灵活的使用第2个操作数能提高代码效率,第2个操作数的形式如下:

        #immed_8r

       常数表达式:

         该常数必须对应8 位位图,即常数是由一个8 位的常数循环移位偶数位得到。

 

        合法常量:

       0x3FC、0、0xF0000000、200、0xF0000001等都是合法常量。

       非法常量:

       0x1FE、511、0xFFFF、0x1010、0xF0000010等都是非法常量。

       MOV R0,#1 ;R0=1

       AND R1,R2,#0x0F ;R2 与0x0F,结果保存在R1

       LDR R0,[R1],#-4 ;读取R1 地址上的存储器单元内容,且R1=R1-4

       Rm

        寄存器方式,在寄存器方式下操作数即为寄存器的数值。

        寄存器方式应用举例:

       SUB R1,R1,R2 ;R1-R2=>R1

       MOV PC,R0 ;PC=R0,程序跳转到指定地址

       LDR R0,[R1],-R2 ;读取R1 地址上的存储器单元内容并存入R0,且R1=R1-R2

       Rm, shift

        寄存器移位方式。将寄存器的移位结果作为操作数,但RM 值保存不变,移位方法如下:

       ASR #n  算术右移n 位(1≤n≤32)

       LSL #n  逻辑左移n 位(1≤n≤31)

       LSR #n  逻辑左移n 位(1≤n≤32)

       ROR #n  循环右移n 位(1≤n≤31)

       RRX  带扩展的循环右移1位

       ADD R1,R1,R1,LSL #3 ;R1=R1*9

       SUB R1,R1,R2,LSR#2 ;R1=R1-R2*4

表A-1  条件码表

 

        对于Thumb指令集,只有B 指令具有条件码执行功能,此指令条件码同表A-?,但如果为无条件执行时,条件码助记符“AL”不在指令中书写。

        条件码应用举例如下:

        If((a!=10)&&(b!=20))a=a+b;

1.3.    指令集

1.3.1.   ARM 存储器访问指令

        ARM 处理是加载/存储体系结构的典型的RISC处理器,对存储器的访问只能使用加载和存储指令实现。ARM 的加载/存储指令是可以实现字、半字、无符/有符字节操作;批量加载/存储指令可实现一条指令加载/存储多个寄存器的内容,大大提高效率;SWP指令是一条寄存器和存储器内容交换的指令,可用于信号量操作等。ARM 处理器是冯?诺依曼存储结构,程序空间、RAM 空间及IO 映射空间统一编址,除对对RAM 操作以外,对外围IO、程序数据的访问均要通过加载/存储指令进行。表A-2给出ARM存储访问指令表。

表A-2  ARM 存储访问指令表

ARM指令集详解

 

        加载/存储半字和带符号字节指令举例如下:

        数据是存储在基址寄存器的地址之上还是之下,地址是在存储第一个值之前还是之后增加还是减少。表A-3给出多寄存器传送指令映射示意表。

表A-3  多寄存器传送指令映射示意表

 

1.3.2.   ARM 数据处理指令

        数据处理指令大致可分为3 类:

(1)     数据传送指令(如MOV、MVN)

(2)     算术逻辑运算指令(如ADD,SUM,AND)

(3)     比较指令(如CMP、TST)。

数据处理指令只能对寄存器的内容进行操作。        所有ARM 数据处理指令均可选择使用S 后缀,以影响状态标志。比较指令CMP、CMN、TST和TEQ不需要后缀S,它们会直接影响状态标志。ARM数据处理指令列于表A-4中。

表A-4  ARM 数据处理指令

表A-5  全部的ARM 乘法指令

1.3.3.   ARM 跳转指令

     两种方式可以实现程序的跳转:

(1)     使用跳转指令直接跳转,跳转指令有跳转指令B,带链接的跳转指令BL ,带状态切换的跳转指令BX。

(2)   直接向PC 寄存器赋值实现跳转。

表A-6给出全部的ARM跳转指令。

表A-6  ARM跳转指令

        B    跳转指令,跳转到指定的地址执行程序。

   B{cond} label

   举例如下:

       B WAITA ;跳转到WAITA 标号处

       B 0x1234 ;跳转到绝对地址0x1234 处

        跳转到指令B 限制在当前指令的±32Mb 的范围内。

 

        BL       带链接的跳转指令。指令将下一条指令的地址拷贝到R14(即LR)链接寄存器中,然后跳转到指定地址运行程序。
        BL{cond} label

       举例如下:

       BL DELAY

       BX       带状态切换的跳转指令。跳转到Rm 指定的地址执行程序,若Rm 的位[0]为1,则跳转时自动将CPSR 中的标志T 置位,即把目标地址的代码解释为Thumb代码;若Rm 的位[0]为0,则跳转时自动将CPSR 中的标志T 复位,即把目标地址的代码解释为ARM代码。指令格式如下:
        BX{cond} Rm
       举例如下:
        ADRL R0,ThumbFun+1
        BX R0 ;跳转到R0 指定的地址,并根据R0 的最低位来切换处理器状态



         BLX

         BLX目标地址:跳转,改变状态及保存PC值

1.3.4.   ARM 协处理器指令

5              ARM 支持协处理器操作,协处理器的控制要通过协处理器命令实现。表A-7给出全部的ARM协处理器指令。

表A-7  ARM 协处理器指令

1.3.5.   ARM 杂项指令

        表A-8给出全部的ARM协处理器指令。

表A-8 ARM杂项指令

 

1.3.6.   ARM 伪指令

 

 

1.4.    寻址方式

1.4.1.   立即数寻址

立即数前面有“#”号,并且如果是十六进制数则在“#”后添加“0x”或“&”,二进制数“#”后面加“%”。

1.4.2.   寄存器寻址

1.4.3.   寄存器间接寻址

以寄存器中的值作为操作数的地址,而操作数本身放在存储器中。

例如:ADD R0,R1,[R2]

1.4.4.   基址变址寻址

将寄存器的内容与指令中给出的地址偏移量相加,从而得到一个操作数的有效地址。

例如:LDR R0,[R1,#4]  R0<-[R1+4]

1.4.5.   多寄存器寻址

一条指令可以完成多个寄存器值得传递,一条指令传送最多16个通用寄存器的值。

LDMIA  R0,{R1,R2,R3,R4}

1.4.6.   相对寻址

以程序计数器PC的值作为基地址,指令中的地址标号作为偏移量,将两者相加后得到的操作数的有效地址。

例如:BL NEXT;

1.4.7.   堆栈寻址

使用一个堆栈指针的专用寄存器指示当前操作位置

递增堆栈:向高地址方向生长

递减堆栈:向低地址方向生长

满堆栈:堆栈指针指向最后压入堆栈的有效数据

空堆栈:堆栈指针指向下一个要放入数据的空位置

 

2.       GNU ARM混编

         汇编源程序一般用于系统最基本的初始化:初始化堆栈指针、设置页表、操作 ARM的协处理器等。这些初始化工作完成后就可以跳转到C代码main函数中执行。

1.1.     GNU汇编语言语句格式

    任何Linux汇编行都是如下结构:[

l        instruction为指令

l        directive为伪操作

l        pseudo-instruction为伪指令

l        为标号, GNU汇编中,任何以冒号结尾的标识符都被认为是一个标号,而不一定非要在一行的开始。

l        comment为语句的注释

下面定义一个”add”的函数,最终返回两个参数的和:

.section.text, “x”

.globaladd      @ give the symbol “add” externallinkage

add:

    ADD r0, r0, r1 @ add input arguments

    MOV pc, lr  @ return from subroutine

@ endof program

注意:

l         ARM指令,伪指令,伪操作,寄存器名可以全部为大写字母,也可全部为小写字母,但不可大小写混用。

l         如果语句太长,可以将一条语句分几行来书写,在行末用“\”表示换行(即下一行与本行为同一语句)。“\”后不能有任何字符,包含空格和制表符(Tab)。

1.2.     GNU汇编程序中的标号symbol(或label)

    标号只能由a~z,A~Z,0~9,“.”,_等(由点、字母、数字、下划线等组成,除局部标号外,不能以数字开头)字符组成。

Symbol的本质:代表它所在的地址,因此也可以当作变量或者函数来使用。

l         段内标号的地址值在汇编时确定;

l         段外标号的地址值在连接时确定。

Symbol的分类:3类(依据标号的生成方式)。

<1>    基于PC的标号。基于PC的标号是位于目标指令前的标号或者程序中数据定义伪操作前的标号。这种标号在汇编时将被处理成PC值加上(或减去)一个数字常量,常用于表示跳转指令”b”等的目标地址,或者代码段中所嵌入的少量数据。

<2>    基于寄存器的标号。基于寄存器的标号常用MAP和FIELD来定义,也可以用EQU来定义。这种标号在汇编时将被处理成寄存器的值加上(或减去)一个数字常量,常用于访问数据段中的数据。

<3>    绝对地址。绝对地址是一个32位数据。它可以寻址的范围为[0,232-1]即可以直接寻址整个内存空间。

 

特别说明:局部标号Symbol

    局部标号主要在局部范围内使用,而且局部标号可以重复出现。它由两部组成:开头是一个0-99直接的数字,后面紧接一个通常表示该局部变量作用范围的符号。局部变量的作用范围通常为当前段,也可以用ROUT来定义局部变量的作用范围。

    局部变量定义的语法格式:N{routname}

l         N:为0~99之间的数字。

l         routname:当前局部范围的名称(为符号),通常为该变量作用范围的名称(用ROUT伪操作定义的)。

    局部变量引用的语法格式:%{F|B}{A|T}N{routname}

l         %:表示引用操作

l         N:为局部变量的数字号

l         routname:为当前作用范围的名称(用ROUT伪操作定义的)

l         F:指示编译器只向前搜索

l         B:指示编译器只向后搜索

l         A:指示编译器搜索宏的所有嵌套层次

l         T:指示编译器搜索宏的当前层次

例:使用局部符号的例子,一段循环程序

subs r0, r0, #1 @每次循环使r0=r0-1

bne 1F      @跳转到1标号去执行

 

注意:

l         如果F和B都没有指定,编译器先向前搜索,再向后搜索

l         如果A和T都没有指定,编译器搜索所有从当前层次到宏的最高层次,比当前层次低的层次不再搜索。

l         如果指定了routname,编译器向前搜索最近的ROUT伪操作,若routname与该ROUT伪操作定义的名称不匹配,编译器报告错误,汇编失败。

1.3.     GNU汇编程序中的分段

<1>    .section伪操作

.section
{,”

”}

Startsa new code or data section. Sections in GNU are called .text, a code section, .data, an initializeddata section, and .bss, an uninitialized data section.

Thesesections have default flags, and the linker understands the default names(similardirective to the armasm directive AREA).The following are allowable .section flags for ELF format files:


     Meaning

a       allowable section

w       writable section

x       executable section

 

中文解释:

用户可以通过.section伪操作来自定义一个段,格式如下:

.section section_name [,”flags”[, %type[,flag_specific_arguments]]]

  每一个段以段名为开始, 以下一个段名或者文件结尾为结束。这些段都有缺省的标志(flags),连接器可以识别这些标志。(与arm asm中的AREA相同)。下面是ELF格式允许的段标志flags:

<标志>
     含义

a          允许段

w          可写段

x          执行段

 

例:定义一个“段”

.section.mysection    @自定义数据段,段名为 “.mysection”

.align  2

strtemp:

     .ascii “Temp string \n\0” @对这一句的理解,我觉得应该是:将”Temp string \n\0″这个字符串存储在以标号strtemp为起始地址的一段内存空间里

<2>    汇编系统预定义的段名

l         .text     @代码段

l         .data    @初始化数据段.data Read-write initialized long data.

l         .bss     @未初始化数据段

l         .sdata   @ .sdata Read-write initialized short data.

l         .sbss    @

注意:源程序中.bss段应该在.text段之前。

1.4.     GNU汇编语言定义入口点

汇编程序的缺省入口是_start标号,用户也可以在连接脚本文件中用ENTRY标志指明其它入口点

例:定义入口点

.section .data

< initialized data here>

.section .bss

< uninitialized data here>

.section .text

.globl  _start

_start:

 

1.5.     GNU汇编程序中的宏定义

格式如下:

.macro 宏名参数名列表  @伪指令.macro定义一个宏

宏体

.endm                   @.endm表示宏结束

    如果宏使用参数,那么在宏体中使用该参数时添加前缀“\”。宏定义时的参数还可以使用默认值。可以使用.exitm伪指令来退出宏。

例:宏定义

.macroSHIFTLEFT a, b

.if \b< 0

MOV \a,\a, ASR #-\b

.exitm

.endif

MOV \a,\a, LSL #\b

.endm

 

 

1.6.     GNU汇编程序中的常数

<1>    十进制数以非0数字开头,如:123和9876;

<2>    二进制数以0b开头,其中字母也可以为大写;

<3>    八进制数以0开始,如:0456,0123;

<4>    十六进制数以0x开头,如:0xabcd,0X123f;

<5>    字符串常量需要用引号括起来,中间也可以使用转义字符,如: “You are welcome!\n”;

<6>    当前地址以“.”表示,在GNU汇编程序中可以使用这个符号代表当前指令的地址;

<7>    表达式:在汇编程序中的表达式可以使用常数或者数值, “-”表示取负数, “~”表示取补,“<>”表示不相等,其他的符号如:+、-、*、/、%、<、<<、>、>>、|、&、^、!、==、>=、<=、&&、|| 跟C语言中的用法相似。

 

1.7.     GNU ARM汇编的常用伪操作

    在前面已经提到过了一些为操作,还有下面一些为操作:

l         数据定义伪操作: .byte,.short,.long,.quad,.float,.string/.asciz/.ascii,重复定义伪操作.rept,赋值语句.equ/.set ;

l         函数的定义;

l         对齐方式伪操作 .align;

l         源文件结束伪操作.end;

l         .include伪操作;

l         if伪操作;

l         .global/ .globl 伪操作;

l         .type伪操作;

l         列表控制语句;

别于GNU AS汇编的通用伪操作,下面是ARM特有的伪操作:

.reg ,.unreq ,.code ,.thumb ,.thumb_func ,.thumb_set, .ltorg ,.pool

<1>    数据定义伪操作

l        .byte:单字节定义,如:.byte 1,2,0b01,0x34,072,’s’ ;

l        .short:定义双字节数据,如:.short 0x1234,60000 ;

l        .long:定义4字节数据,如:.long 0x,

l        .quad:定义8字节,如:.quad 0xabcd

l        .float:定义浮点数,如:.float 0f-\

    .E-40 @ – pi

l        .string/.asciz/.ascii:定义多个字符串,如:

.string “abcd”,”efgh”, “hello!”

.asciz “qwer”,”sun”, “world!”

.ascii “welcome\0”

     注意:ascii伪操作定义的字符串需要自行添加结尾字符’\0’。

l        .rept:重复定义伪操作, 格式如下:

 .rept 重复次数

  数据定义

 .endr @结束重复定义

  例:

 .rept 3

 .byte 0x23

 .endr

 

 

l        .equ/.set: 赋值语句, 格式如下:

  .equ(.set)变量名,表达式

  例:

 .equ abc, 3 @让abc=3

<2>    函数的定义伪操作

l         函数的定义,格式如下:

  函数名:

  函数体

  返回语句

    一般的,函数如果需要在其他文件中调用, 需要用到.global伪操作将函数声明为全局函数。为了不至于在其他程序在调用某个C函数时发生混乱,对寄存器的使用我们需要遵循APCS准则。函数编译器将处理函数代码为一段.global的汇编码。

l         函数的编写应当遵循如下规则:

a.         a1-a4寄存器(参数、结果或暂存寄存器,r0到r3 的同义字)以及浮点寄存器f0-f3(如果存在浮点协处理器)在函数中是不必保存的;

b.         如果函数返回一个不大于一个字大小的值,则在函数结束时应该把这个值送到 r0 中;

c.         如果函数返回一个浮点数,则在函数结束时把它放入浮点寄存器f0中;

d.         如果函数的过程改动了sp(堆栈指针,r13)、fp(框架指针,r11)、sl(堆栈限制,r10)、lr(连接寄存器,r14)、v1-v8(变量寄存器,r4 到 r11)和 f4-f7,那么函数结束时这些寄存器应当被恢复为包含在进入函数时它所持有的值。

 

<3>    .align .end .include .incbin伪操作

l        .align:用来指定数据的对齐方式,格式如下:

         .align [absexpr1, absexpr2]

      以某种对齐方式,在未使用的存储区域填充值第一个值表示对齐方式,4, 8,16或 32.第二个表达式值表示填充的值。

l        .end:表明源文件的结束。

l        .include:可以将指定的文件在使用.include 的地方展开,一般是头文件,例如:

         .include “myarmasm.h”

l        .incbin伪操作可以将原封不动的一个二进制文件编译到当前文件中,使用方法如下:

         .incbin”file”[,skip[,count]]

         skip表明是从文件开始跳过skip个字节开始读取文件,count是读取的字数.

<4>    ..if伪操作

    根据一个表达式的值来决定是否要编译下面的代码, 用.endif伪操作来表示条件判断的结束,中间可以使用.else来决定.if的条件不满足的情况下应该编译哪一部分代码。

.if有多个变种:

.ifdefsymbol           @判断symbol是否定义

.ifcstring1,string2      @字符串string1和string2是否相等,字符串可以用单引号括起来

.ifeqexpression        @判断expression的值是否为0

.ifeqsstring1,string2    @判断string1和string2是否相等,字符串必须用双引号括起来

.ifgeexpression        @判断expression的值是否大于等于0

.ifgtabsolute expression @判断expression的值是否大于0

.ifleexpression        @判断expression的值是否小于等于0

.ifltabsolute expression    @判断expression的值是否小于0

.ifncstring1,string2        @判断string1和string2是否不相等, 其用法跟.ifc恰好相反。

.ifndefsymbol, .ifnotdef symbol @判断是否没有定义symbol, 跟.ifdef恰好相反

.ifneexpression          @如果expression的值不是0, 那么编译器将编译下面的代码

.ifnesstring1,string2      @如果字符串string1和string2不相等, 那么编译器将编译下面的代码.

 

<5>    .global .type .title .list

l        .global/ .globl :用来定义一个全局的符号,格式如下:

          .global symbol 或者 .globl symbol

l         .type:用来指定一个符号的类型是函数类型或者是对象类型, 对象类型一般是数据, 格式如下:

          .type 符号, 类型描述

例:

.globla

.data

.align4

.typea, @object

.sizea, 4

a:

.long10

例:

.section.text

.typeasmfunc, @function

.globlasmfunc

asmfunc:

mov pc,lr

 

<6>    列表控制语句:

.title:用来指定汇编列表的标题,例如:

  .title “my program”

.list:用来输出列表文件.

 

<7>    ARM特有的伪操作

l         .reg: 用来给寄存器赋予别名,格式如下:

       别名 .req 寄存器名

l         .unreq: 用来取消一个寄存器的别名,格式如下:

.unreq 寄存器别名

    注意被取消的别名必须事先定义过,否则编译器就会报错,这个伪操作也可以用来取消系统预制的别名, 例如r0, 但如果没有必要的话不推荐那样做。

l         .code伪操作用来选择ARM或者Thumb指令集,格式如下:

.code 表达式

  如果表达式的值为16则表明下面的指令为Thumb指令,如果表达式的值为32则表明下面的指令为ARM指令.

l         .thumb伪操作等同于.code 16, 表明使用Thumb指令, 类似的.arm等同于.code 32

l         .force_thumb伪操作用来强制目标处理器选择thumb的指令集而不管处理器是否支持

l         .thumb_func伪操作用来指明一个函数是thumb指令集的函数

l         .thumb_set伪操作的作用类似于.set, 可以用来给一个标志起一个别名, 比.set功能增加的一点是可以把一个标志标记为thumb函数的入口, 这点功能等同于.thumb_func

l         .ltorg用于声明一个数据缓冲池(literal pool)的开始,它可以分配很大的空间。

l         .pool的作用等同.ltorg

l         .space

{,

}

    分配number_of_bytes字节的数据空间,并填充其值为fill_byte,若未指定该值,缺省填充0。(与armasm中的SPACE功能相同)

l         .word

{,

} …
插入一个32-bit的数据队列。(与armasm中的DCD功能相同)
可以使用.word把标识符作为常量使用。

例:

Start:

valueOfStart:

     .word Start

这样程序的开头Start便被存入了内存变量valueOfStart中。

l         .hword

{,

} …

   插入一个16-bit的数据队列。(与armasm中的DCW相同)

 

1.8.     GNU ARM汇编特殊字符和语法

<1>    代码行中的注释符号: ‘@’

<2>    整行注释符号: ‘#’

<3>    语句分离符号: ‘;’

<4>    立即数前缀: ‘#’ 或 ‘$’

 

3.       ARM GCC 内嵌汇编

对于基于ARM的RISC处理器,GNUC编译器提供了在C代码中内嵌汇编的功能。这种非常酷的特性提供了C代码没有的功能,比如手动优化软件关键部分的代码、使用相关的处理器指令。这里设想了读者是熟练编写ARM汇编程序读者,因为该片文档不是ARM汇编手册。同样也不是C语言手册。这篇文档假设使用的是GCC 4 的版本,但是对于早期的版本也有效。

GCCasm 声明

让我们以一个简单的例子开始。就像C中的声明一样,下面的声明代码可能出现在你的代码中。

/*NOP 例子 */

asm(“movr0,r0”);

该语句的作用是将r0移动到r0中。换句话讲他并不干任何事。典型的就是NOP指令,作用就是短时的延时。

请接着阅读和学习这篇文档,因为该声明并不像你想象的和其他的C语句一样。内嵌汇编使用汇编指令就像在纯汇编程序中使用的方法一样。可以在一个asm声明中写多个汇编指令。但是为了增加程序的可读性,最好将每一个汇编指令单独放一行。

asm(

“mov r0, r0\n\t”

“mov r0, r0\n\t”

“mov r0, r0\n\t”

“mov r0, r0”

);

换行符和制表符的使用可以使得指令列表看起来变得美观。你第一次看起来可能有点怪异,但是当C编译器编译C语句的是候,它就是按照上面(换行和制表)生成汇编的。到目前为止,汇编指令和你写的纯汇编程序中的代码没什么区别。但是对比其它的C声明,asm的常量和寄存器的处理是不一样的。通用的内嵌汇编模版是这样的。

asm(code : output operand list : input operand list : clobberlist);

汇编和C语句这间的联系是通过上面asm声明中可选的outputoperand list和input operand list。Clobber list后面再讲。

下面是将c语言的一个整型变量传递给汇编,逻辑左移一位后在传递给C语言的另外一个整型变量。

/* Rotating bits example */

asm(“mov %[result], %[value], ror #1” :[result] “=r” (y) : [value] “r” (x));

 

每一个asm语句被冒号(:)分成了四个部分。

汇编指令放在第一部分中的“”中间。

“mov %[result], %[value], ror #1”

接下来是冒号后的可选择的output operand list,每一个条目是由一对[](方括号)和被他包括的符号名组成,它后面跟着限制性字符串,再后面是圆括号和它括着的C变量。这个例子中只有一个条目。

 

[result] “=r” (y)

接着冒号后面是输入操作符列表,它的语法和输入操作列表一样

[value] “r” (x)

 

破坏符列表,在本例中没有使用

 

就像上面的NOP例子,asm声明的4个部分中,只要最尾部没有使用的部分都可以省略。但是有有一点要注意的是,上面的4个部分中只要后面的还要使用,前面的部分没有使用也不能省略,必须空但是保留冒号。下面的一个例子就是设置ARMSoc的CPSR寄存器,它有input但是没有output operand。

asm(“msr cpsr,%[ps]” : : [ps]”r”(status))

即使汇编代码没有使用,代码部分也要保留空字符串。下面的例子使用了一个特别的破坏符,目的就是告诉编译器内存被修改过了。这里的破坏符在下面的优化部分在讲解。

asm(“”:::”memory”);

为了增加代码的可读性,你可以使用换行,空格,还有C风格的注释

asm(“mov %[result], %[value], ror#1”

           : [result]”=r” (y) /*Rotation result. */

           : [value]”r” (x) /*Rotated value. */

           : /* No clobbers */

);

在代码部分%后面跟着的是后面两个部分方括号中的符号,它指的是相同符号操作列表中的一个条目

%[result]表示第二部分的C变量y,%[value]表示三部分的C变量x;

符号操作符的名字使用了独立的命名空间。这就意味着它使用的是其他的符号表。简单一点就是说你不必关心使用的符号名在C代码中已经使用了。在早期的C代码中,循环移位的例子必须要这么写:

asm(“mov %0, %1, ror #1″ :”=r” (result) : “r” (value))

在汇编代码中操作数的引用使用的是%后面跟一个数字,%1代表第一个操作数,%2代码第二个操作数,往后的类推。这个方法目前最新的编译器还是支持的。但是它不便于维护代码。试想一下,你写了大量的汇编指令的代码,要是你想插入一个操作数,那么你就不得不从新修改操作数编号。

优化C代码

有两种情况决定了你必须使用汇编。1st,C限制了你更加贴近底层操作硬件,比如,C中没有直接修改程序状态寄存器(PSR)的声明。2nd就是要写出更加优化的代码。毫无疑问GNUC代码优化器做的很好,但是他的结果和我们手工写的汇编代码相差很远。

这一部分有一点很重要,也是被别人忽视最多的就是:我们在C代码中通过内嵌汇编指令添加的汇编代码,也是要被C编译器的优化器处理的。让我们下面做个试验来看看吧。

下面是代码实例。

 

bigtree@just:~/embedded/basic-C$ arm-linux-gcc -c test.c

bigtree@just:~/embedded/basic-C$ arm-linux-objdump -D test.o

 

编译器选择r3作为循环移位使用。它也完全可以选择为每一个C变量分配寄存器。Load或者store一个值并不显式的进行。下面是其它编译器的编译结果。

E420A0E1 mov r2, r4, ror #1 @ y, x

编译器为每一个操作数选择一个相应的寄存器,将操作过的值cache到r4中,然后传递该值到r2中。这个过程你能理解不?

有的时候这个过程变得更加糟糕。有时候编译器甚至完全抛弃你嵌入的汇编代码。C编译器的这种行为,取决于代码优化器的策略和嵌入汇编所处的上下文。如果在内嵌汇编语句中不使用任何输出部分,那么C代码优化器很有可能将该内嵌语句完全删除。比如NOP例子,我们可以使用它作为延时操作,但是对于编译器认为这影响了程序的执行速速,认为它是没有任何意义的。

上面的解决方法还是有的。那就是使用volatile关键字。它的作用就是禁止优化器优化。将NOP例子修改过后如下:

/* NOP example, revised */

asm volatile(“movr0, r0”);

下面还有更多的烦恼等着我们。一个设计精细的优化器可能重新排列代码。看下面的代码:

i++;

if (j == 1)

x += 3;

i++;

优化器肯定是要从新组织代码的,两个i++并没有对if的条件产生影响。更进一步的来讲,i的值增加2,仅仅使用一条ARM汇编指令。因而代码要重新组织如下:

if (j == 1)

   x += 3;

i += 2;

这样节省了一条ARM指令。结果是:这些操作并没有得到许可。

这些将对你的代码产生很到的影响,这将在下面介绍。下面的代码是c乘b,其中c和b中的一个或者两个可能会被中断处理程序修改。进入该代码前先禁止中断,执行完该代码后再开启中断。

 

asm volatile(“mrs r12,cpsr\n\t”

   “orr r12, r12, #0xC0\n\t”

   “msr cpsr_c, r12\n\t” ::: “r12”, “cc”);

c *= b; /* This may fail. */

asm volatile(“mrs r12, cpsr\n”

   “bic r12, r12, #0xC0\n”

   “msr cpsr_c, r12” ::: “r12”, “cc”);

但是不幸的是针对上面的代码,优化器决定先执行乘法然后执行两个内嵌汇编,或相反。这样将会使得我们的代码变得毫无意义。

我们可以使用clobberlist帮忙。上面例子中的clobber list如下:

“r12″,”cc”

上面的clobber list将会将向编译器传达如下信息,修改了r12和程序状态寄存器的标志位。Btw,直接指明使用的寄存器,将有可能阻止了最好的优化结果。通常你只要传递一个变量,然后让编译器自己选择适合的寄存器。另外寄存器名,cc(condition registor 状态寄存器标志位),memory都是在clobber list上有效的关键字它用来向编译器指明,内嵌汇编指令改变了内存中的值。这将强迫编译器在执行汇编代码前存储所有缓存的值,然后在执行完汇编代码后重新加载该值。这将保留程序的执行顺序,因为在使用了带有memory clobber的asm声明后,所有变量的内容都是不可预测的。

asm volatile(“mrs r12,cpsr\n\t”

   “orr r12, r12, #0xC0\n\t”

   “msr cpsr_c, r12\n\t” :: : “r12”, “cc”,”memory”);

c *= b; /* This is safe. */

asm volatile(“mrs r12, cpsr\n”

   “bic r12, r12, #0xC0\n”

   “msr cpsr_c, r12” ::: “r12”, “cc”,”memory”);

使所有的缓存的值都无效,只是局部最优(suboptimal)。你可以有选择性的添加dummyoperand 来人工添加依赖。

asm volatile(“mrs r12,cpsr\n\t”

   “orr r12, r12, #0xC0\n\t”

   “msr cpsr_c, r12\n\t” : “=X” (b) :: “r12″,”cc”);

c *= b; /* This is safe. */

asm volatile(“mrs r12

上面的第一个asm试图修改变量先b,第二个asm试图修改c。这将保留三个语句的执行顺序,而不要使缓存的变量无效。

理解优化器对内嵌汇编的影响很重要。如果你读到这里还是云里雾里,最好是在看下个主题之前再把这段文章读几遍^_^。

 

Input and output operands

前面我们学到,每一个input和output operand,由被方括号[]中的符号名,限制字符串,圆括号中的C表达式构成。

这些限制性字符串有哪些,为什么我们需要他们?你应该知道每一条汇编指令只接受特定类型的操作数。例如:跳转指令期望的跳转目标地址。不是所有的内存地址都是有效的。因为最后的opcode只接受24位偏移。但矛盾的是跳转指令和数据交换指令都希望寄存器中存储的是32位的目标地址。在所有的例子中,C传给operand的可能是函数指针。所以面对传给内嵌汇编的常量、指针、变量,编译器必须要知道怎样组织到汇编代码中。

对于ARM核的处理器,GCC 4 提供了一下的限制。

Constraint

Usage in ARM state

Usage in Thumb state

f

Floating point registers f0 .. f7

Not available

G

Immediate floating point constant

Not available

H

Same a G, but negated

Not available

I

Immediate value in data processing instructions

e.g. ORR R0, R0, #operand

Constant in the range 0 .. 255

e.g. SWI operand

J

Indexing constants -4095 .. 4095

e.g. LDR R1, [PC, #operand]

Constant in the range -255 .. -1

e.g. SUB R0, R0, #operand

K

Same as I, but inverted

Same as I, but shifted

L

Same as I, but negated

 

Constant in the range -7 .. 7

e.g. SUB R0, R1, #operand

l

Same as r

Registers r0..r7

e.g. PUSH operand

M

Constant in the range of 0 .. 32 or a power of 2

e.g. MOV R2, R1, ROR #operand

Constant that is a multiple of 4 in the range of 0 .. 1020

e.g. ADD R0, SP, #operand

m

Any valid memory address

 

N

Not available

Constant in the range of 0 .. 31

e.g. LSL R0, R1, #operand

o

Not available

Constant that is a multiple of 4 in the range of -508 .. 508

e.g. ADD SP, #operand

r

General register r0 .. r15

e.g. SUB operand1, operand2, operand3

Not available

W

Vector floating point registers s0 .. s31

Not available

X

Any operand

 

 

 

 

= :Write-only operand, usually used for all output operands

+ :Read-write operand, must be listed as an output operand

& :A register that should be used for output only

 

Output operands必须为write-only,相应C表达式的值必须是左值。Input operands必须为read-only。C编译器是没有能力做这个检查。

比较严格的规则是:不要试图向input operand写。但是如果你想要使用相同的operand作为input和output。限制性modifier(+)可以达到效果。例子如下:

asm(“mov %[value], %[value], ror #1″ : [value]”+r” (y))

和上面例子不一样的是,最后的结果存储在input variable中。

可能modifier + 不支持早期的编译器版本。庆幸的是这里提供了其他解决办法,该方法在最新的编译器中依然有效。对于input operators有可能使用单一的数字n在限制字符串中。使用数字n可以告诉编译器使用的第n个operand,operand都是以0开始计数。下面是例子:

asm(“mov %0, %0, ror #1” : “=r” (value) :”0″ (value))

限制性字符串“0”告诉编译器,使用和第一个output operand使用同样input register。

请注意,在相反的情况下不会自动实现。如果我没告诉编译器那样做,编译器也有可能为input和output选择相同的寄存器。第一个例子中就为input和output选择了r3。

在多数情况下这没有什么,但是如果在input使用前output已经被修改过了,这将是致命的。在input和output使用不同寄存器的情况下,你必须使用&modifier来限制outputoperand。下面是代码示例:

asm volatile(“ldr %0, [%1]””\n\t”

             “str %2, [%1, #4]””\n\t”

             : “=&r” (rdv)

            : “r”(&table), “r” (wdv)

             : “memory”);

在以张表中读取一个值然后在写到该表的另一个位置。

其他

内嵌汇编作为预处理宏

要是经常使用使用部分汇编,最好的方法是将它以宏的形式定义在头文件中。使用该头文件在严格的ANSI模式下会出现警告。为了避免该类问题,可以使用__asm__代替asm,__volatile__代替volatile。这可以等同于别名。下面就是个例程:

#define BYTESWAP(val) \

   __asm__ __volatile__ ( \

       “eor r3, %1, %1, ror #16\n\t” \

       “bic r3, r3, #0x00FF0000\n\t” \

       “mov %0, %1, ror #8\n\t” \

       “eor %0, %0, r3, lsr #8” \

       : “=r” (val) \

       : “0”(val) \

       : “r3”, “cc” \

);

 

C 桩函数

宏定义包含的是相同的代码。这在大型routine中是不可以接受的。这种情况下最好定义个桩函数。

unsigned long ByteSwap(unsigned longval)

{

asm volatile (

       “eor r3, %1, %1, ror #16\n\t”

       “bic r3, r3, #0x00FF0000\n\t”

       “mov %0, %1, ror #8\n\t”

       “eor %0, %0, r3, lsr #8”

       : “=r” (val)

       : “0”(val)

       : “r3”

);

return val;

}

替换C变量的符号名

默认的情况下,GCC使用同函数或者变量相同的符号名。你可以使用asm声明,为汇编代码指定一个不同的符号名

unsigned long value asm(“clock”) =

这个声明告诉编译器使用了符号名clock代替了具体的值。

替换C函数的符号名

为了改变函数名,你需要一个原型声明,因为编译器不接受在函数定义中出现asm关键字。

extern long Calc(void) asm (“CALCULATE”)

调用函数calc()将会创建调用函数CALCULATE的汇编指令。

强制使用特定的寄存器

局部变量可能存储在一个寄存器中。你可以利用内嵌汇编为该变量指定一个特定的寄存器。

void Count(void) {

register unsigned char counterasm(“r3”);

… some code…

asm volatile(“eor r3, r3,r3”);

… more code…

}

汇编指令“eor r3, r3, r3”,会将r3清零。Waring:该例子在到多数情况下是有问题的,因为这将和优化器相冲突。因为GCC不会预留其它寄存器。要是优化器认为该变量在以后一段时间没有使用,那么该寄存器将会被再次使用。但是编译器并没有能力去检查是否和编译器预先定义的寄存器有冲突。如果你用这种方式指定了太多的寄存器,编译器将会在代码生成的时候耗尽寄存器的。

临时使用寄存器

如果你使用了寄存器,而你没有在input或output operand传递,那么你就必须向编译器指明这些。下面的例子中使用r3作为scratch 寄存器,通过在clobber list中写r3,来让编译器得知使用该寄存器。由于ands指令跟新了状态寄存器的标志位,使用cc在clobber list中指明。

asm volatile(

   “ands r3, %1, #3” “\n\t”

   “eor %0, %0, r3” “\n\t”

   “addne %0, #4”

   : “=r” (len)

   : “0” (len)

   : “cc”, “r3”

 );

最好的方法是使用桩函数并且使用局部临时变量

 

寄存器的用途

比较好的方法是分析编译后的汇编列表,并且学习C 编译器生成的代码。下面的列表是编译器将ARM核寄存器的典型用途,知道这些将有助于理解代码。

Register

Alt. Name

Usage

r0

a1

First function argument

Integer function result

Scratch register

r1

a2

Second function argument

Scratch register

 

r2

a3

Third function argument

Scratch register

 

r3

a4

Fourth function argument

Scratch register

 

r4

v1

Register variable

r5

v2

Register variable

r6

v3

Register variable

r7

v4

Register variable

r8

v5

Register variable=

r9

v6

rfp

Register variable

Real frame pointer

 

r10

sl

Stack limit

r11

fp

Argument pointer

r12

ip

Temporary workspace

r13

sp

Stack pointer

r14

lr

Link register Workspace

r15

pc

Program counter

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/201252.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月20日 上午9:44
下一篇 2026年3月20日 上午9:45


相关推荐

  • 什么是备兑开仓_期权如何备兑开仓

    什么是备兑开仓_期权如何备兑开仓小知识备兑开仓是指投资者在持有足额标的证券的基础上,卖出相应数量的认购期权合约。备兑开仓属于“抛补式”期权,也就是说投资者在合约到期时按行权价卖出标的证券的义务可以通过持有的标的证券予以履行。当认

    2022年8月6日
    8
  • xmind使用「建议收藏」

    基本简介XMINDXMind中文版是一款非常实用的商业思维导图软件,XMind应用先进的EclipseRCP软件架构,全力打造易用、高效的可视化思维软件,强调软件的可扩展、跨平台、稳定性和性能。XMIND能够协助用户快速捕捉创意与灵感,通过直观、友好的图形化操作界面,将思想、策略及商务信息转化为行动蓝图,全面提升企业办公效能。XMINDXMind基本功能头脑风暴XMIND头…

    2022年4月9日
    273
  • 端口timewait如何解决_如何检测端口状态

    端口timewait如何解决_如何检测端口状态网上查了一下端口状态的资料,我下面总结了一下,自己学习学习:TCP状态转移要点TCP协议规定,对于已经建立的连接,网络双方要进行四次握手才能成功断开连接,如果缺少了其中某个步骤,将会使连接处于假死状态,连接本身占用的资源不会被释放。网络服务器程序要同时管理大量连接,所以很有必要保证无用连接完全断开,否则大量僵死的连接会浪费许多服务器资源。在众多TCP状态中,最值得注意的状态有两个:C……

    2025年9月26日
    8
  • 分区表-理论

    分区表-理论目的分区表的主要目的是方便数据的维护,而不是提升MySQL数据库的性能。《高性能MySQL》中:分区的一个主要目的是将数据按照一个较粗的粒度分在不同的表中,这样做可以将相关的数据放在一起,另外

    2022年7月3日
    36
  • DHCP简单配置-CISCO

    DHCP简单配置-CISCO

    2021年8月1日
    60
  • oracle 常见函数_oracle有没有包含的函数

    oracle 常见函数_oracle有没有包含的函数oracle 数据库中主要使用两种类型的函数:1.  单行函数:操作一行数据,返回一个结果常用的单行函数有:字符串函数:对字符串操作。数字函数:对数字进行计算,返回一个数字。日期函数:对日期和时间进行处理。转换函数:可以将一种数据类型转换为另外一种数据类型。2.  聚合函数(多行函数、分组函数、组函数):操作多行数据,并返回一个结果。比如 SUM一、字符串函数字符函数接受字符参数,这些参数可以是表…

    2025年8月19日
    4

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号