ARM V7 伪指令

/*
 * 2012/6/9    16:39
 */


ARM伪指令不是真正的ARM指令或Thumb指令,伪指令在汇编编译器对源程序进行汇编处理时被替换成
对应的ARM或Thumb指令序列。


 LDR

        ldr <register> , = <expression>         相当于PC寄存器或其它寄存器的长转移.

        @对于ARM数据从内存到CPU之间的移动只能通过L/S指令,如:
        
            ldr r0, 0x12345678    // 把0x12345678内存中的数据写到r0中,
        
        还有一个就是ldr伪指令,如:
        
            ldr r0, =0x12345678 // 把0x12345678地址写到 r0 中,

        mov只能完成寄存器间数据的移动,而且立即数长度限制在8位

        当用

        LDR r, =imd  // r 为寄存器, imd为立即数

        LDR 是一条伪指令。编译器会根据 立即数的大小,决定用 ldr 指令或者是mov或mvn指令。

        当imd能用mov或者mvn操作时,就将它翻译成一条mov或mvn指令。当imd大于mov或mvn能够操作的数时,编译器
        会将imd存在一个内存单元中,然后再用一条ldr指令加载这个内存单元的的值到寄存器中。

        "LDR r, label  和 LDR r, =label的区别:"

            LDR r, =label    // 会把label表示的值加载到寄存器中,
            LDR r, label    // 会把label当做地址,把label指向的地址中的值加载到寄存器中。

            譬如 label的值是 0x8000,

            LDR r, =label    // 会将 0x8000加载到寄存器中,
            LDR r, label    // 则会将内存0x8000处的值加载到寄存器中。


        首先我们来回答“基本寻址模式与基本指令”一文中提出的问题。“如果我们需要mov r0, #10000这样的指令,应该怎么办?
        
        (常数10000不能在机器指令32bit中的低12bit中被表示出来)”。当你进行编译的时候,“Error:All70E”的错误就会出现.

        其实,这个问题很容易解决,只需要将mov r0, #10000换为ldr r0, =10000即可。为什么这样就可以了呢?因为,
        
        这里的ldr r0, =10000并非我们已经学过的ldr指令,而是一条伪指令,编译器会将这条伪指令替换为:

        ldr r0, [pc, #-4]
        DCD 10000

        DCD所分配的内存空间中存放了整数10000,该内存空间被称为literal pool,中文名称“文字池”。
        
        由于整个程序都是由编译器编译的(包括文字池的分配),所以很显然编译器能够知道ldr指令在内存中的地址与
        
        文字池在内存中的位置之间的偏移量,因此编译器就可以正确地使用以pc为基址,采用相对寻址的ldr指令将文字池
        
        中的数取出加载到寄存器r0中。由此可见,编译器对于ldr r0, =10000这条伪指令的处理,其实质是:

        在汇编源程序时,LDR伪指令被编译器替换成一条合适的指令和存放常数的文字池。汇编器将常量放入文字池,
        
        并使用一条程序相对偏移的LDR指令从文字池读出常量。  

        由于,4byte可以存放任何int型整数,这样一来,常数就可以是任何int型整数,而不再受制于12bit的限制。
        
        当然此时的常数是存放在内存中的,而不是存放在机器指令的32bit编码中的。


        额外说明:

        a)、ldr r0, [pc, #-4]中是-4,而不是+4,是由于流水线的原因(参见“流水线对PC值的影响”一文)。
        
            今后对于流水线的这种影响,我将不再予以特别说明。

        b)、从指令位置到文字池的偏移量必须小于4KB

        c)、从语法上来看,与ARM指令的LDR相比,伪指令LDR的参数有“=”号,没有“#”号

        d)、如果常数能够被12bit表示出来,例如:ldr r0, =0x100 ,那么,编译器对该伪指令的处理,

            是使用MOV(或者MVN)指令代替该LDR伪指令,例如:mov r0, #0x100 ,
            
            而不会采用ldr指令+文字池的方式。


        除了 ldr 寄存器, =常数 这种形式外,还有ldr 寄存器, =标号 这种形式也经常被使用,下面我就来讲解这种形式的ldr伪指令。

        ldr pc, =InitStack    // 这条伪指令的作用是将标号InitStack所代表的地址赋予pc。 这里会使我们产生几个疑问:

        a)、为什么不使用bl InitStack,而要使用ldr pc, =InitStack?

        这是因为bl指令的跳转范围是正负32M,而InitStack所代表的位置有可能距离 ldr pc, =InitStack 超过32M,
        
        此时bl就无能为力了。竟然存在这么大范围跳转的程序(这似乎意味着编译出来的二进制可执行文件的大小会操作32M),
        
        这一点似乎令我们感到非常震惊。事实上是:大小超过32M的可执行程序的确几乎不可能出现,但即使是很小的二进制
        
        程序中也可能进行大范围(超过32M)的跳转,这一点在bootloader程序中几乎是必然的。

        b)、编译器是如何得出InitStack所代表的地址是 0x64 ?

        编译器知道MOV R0, LR这条指令相对于整个程序的第1条指令的偏移量为 0x64 ;
        
        同时又知道这个程序将来在内存中的运行地址为 0x0 ,所以编译器在编译的时候(不是程序运行的时候)
        
        就可以确定InitStack所代表的地址为 0x64+0x0=0x64 。那么编译器又是如何知道“程序将来在内存中
        
        的运行地址”的呢?其实,这个“程序将来在内存中的运行地址”,我通常称它为“程序的期望运行地址”,
        
        简称“运行地址”,以后我也将这样称呼它。它其实是在编译程序前由程序员告知编译器的。
        
        c)、如果将来程序并没有运行在它的运行地址处,很显然这个程序就会出问题。如何解决?

        出问题的原因,显然是由于ldr伪指令使用的绝对地址。所以,解决的办法就是使用相对地址,进行相对寻址。
        
        这就要用到我们下面要介绍的另一条伪指令adr


        附:ldr指令与ldr伪指令的4种形式(这4种形式,极其容易让初学者困惑,所以在此集中列出)

        ldr r0, [r1]    // 指令,将内存存放的内容加载到r0中
        ldr r0, label    // 指令,将标号label所代表的内存地址处存放的内容加载到r0中
        ldr r0, =10000    // 伪指令,将常数10000赋予r0(采用ldr指令+文字池的方式实现)
        ldr r0, =lable    // 伪指令,将标号label所代表的内存地址赋予r0

adr

    将基于pc的地址值或基于寄存器的地址值读取到寄存器中

    adr{cond} register, expr    // 将基于pc或寄存器的地址表达式expr传给register;
    
    expr的取值范围为:

    如果地址值不是字对齐的,expr取值范围为-255~255;

    如果地址值是字对齐的,其取值范围为-1020~1020;

    如果地址值是16字节对齐的,取值范围更大

    start :    mov r0, #10        // 因为pc值为当前指令地址加8字节
               adr r4, start    // 本指令将被编译器替换成sub r4, pc, #0x0c

    "ADR伪指令的作用与LDR伪指令的作用相同,都是将标号所代表的地址赋予寄存器,"
    
    "不过2者的实现机制是完全不同的:ldr采用绝对地址,adr采用相对地址。 "

    ADR伪指令将基于PC相对偏移的地址值读取到寄存器中。在汇编源程序时,ADR伪指令被编译器替换成一条合适的指令。
    
    通常,编译器用一条ADD指令或SUB指令来实现该ADR伪指令的功能。

    很显然,由于将 ADR R0, Delay 替换为 ADD r0, pc, #0x3c,而它是以当前指令的地址(pc的值)进行相对地址计算的。
    
    所以即使将来程序没有实际运行在运行地址处,也不会有问题。


    当然,这里还有2个问题:

    a)、既然adr和ldr完成类似的功能,adr又能避免绝对地址的问题,还要ldr伪指令有何用?

        这主要是因为,adr伪指令要求标号与adr伪指令必须在同一个段中(段的概念参见“ARM汇编伪操作”一文),
        
        而ldr伪指令则没有这样的要求。

    b)、add r0, pc, #0x3c中的常数0x3c是放在机器指令12bit中的立即数,这个立即数有可能不能被12bit表示出来。
    
    此时编译会产生错误。如果出现这样的情况,又应该如何办?

    adr <register> <label>

    ADR为小范围的地址读取伪指令。ADR指令将基于PC相对偏移的地址值读取到寄存器中。在汇编编译源程序时,
    
    ADR伪指令被编译器替换在一条合适的指令,通常,编译器用一条ADD指令或SUB指令来实现该ADR伪指令的功能,
    
    若不能使用一条指令实现,则产生错误。

    其能加载的地址范围,当为字节对齐时,是-1020~1020,当为非字对齐时在-255~255之间。

    
    /* adr ldr 区别 */

    adr :相对寻址,与当前位置有关

    ldr  :绝对寻址,与当前位置无关

    在初始化SDRAM时就会用到adr,代码如下:

        /*   初始化SDRAM       */
           ldr r0,=BWSCON                // r0=SDRAM寄存器基地址,获取绝对地址
           adr r1,SDRAM_CONFIG           // 使用adr相对跳转, r1=SDRAM_CONFIG地址
           add r2,r0,#(13*4)              
    
    0:
           ldr r3,[r1],#4                      
           str r3,[r0],#4                      
           cmp r0,r2
           bne 0b                        
    
    SDRAM_CONFIG:
        .long 0x22011110;     //BWSCON
        .long 0x00000700;     //BANKCON0
        .long 0x00000700;     //BANKCON1
        .long 0x00000700;     //BANKCON2
        .long 0x00000700;     //BANKCON3
        .long 0x00000700;     //BANKCON4
        .long 0x00000700;     //BANKCON5
        .long 0x00018005;     //BANKCON6
        .long 0x00018005;     //BANKCON7
        .long 0x008C04F4;     //REFRESH
        .long 0x000000B1;     //BANKSIZE
        .long 0x00000030;     //MRSRB6
        .long 0x00000030;     //MRSRB7
     上面代码中第3行之所以用到了 " adr r1,SDRAM_CONFIG ",是因为SDRAM未初始化之前,不能使用绝对寻址,所以使用adr相对寻址.

    adr r1,SDRAM_CONFIG就相当于 ldr r1,=SDRAM_CONFIG

    通过以下实例:

            ldr r0, _start
            adr r0, _start
            ldr r0, =_start
            nop
            mov pc, lr
    
    _start:
            nop
    得出以下汇编(r0设为0C008000):

    0c008000 <_start-0x14>:
    c008000: e59f000c ldr r0, [pc, #12] ; c008014 <_start>            // ldr指令(不带=)  :  r0 = c008014里面内容= e1a00000
    c008004: e28f0008 add r0, pc, #8 ; 0x8                          // adr指令:           r0 =(当前PC值+8)+0x8=c008014
    c008008: e59f0008 ldr r0, [pc, #8] ; c008018 <_start+0x4>        // ldr指令(带=)  :    r0 =c008018 里面内容=0c008014
    c00800c: e1a00000 nop (mov r0,r0)
    c008010: e1a0f00e mov pc, lr

    0c008014 <_start>:
    c008014: e1a00000 nop (mov r0,r0)
    c008018: 0c008014 stceq 0, cr8, [r0], -#80


    分析出:

    ldr r0, _start
    将内存地址 _start (c008014)里的数据放入r0中。执行这个后,r0 = 0xe1a00000

    adr r0, _start
    将 _start 的地址值(c008014)放入 r0中,但是请看反编译的结果,它是与位置无关的。其实取得的时相对的位置。
    例如这段代码在 0x0c008000 运行,那么 adr r0, _start 得到 r0 = 0x0c008014;如果在地址 0 运行,就是 0x00000014 了。

    ldr r0, =_start
    这个取得标号 _start 的绝对地址值(c008014)。这个绝对地址是在 link 的时候确定的。看上去这只是一个指令,但是它要占用 2 个 32 bit 的空间,
    一条是指令,另一条是 _start 的数据(因为在编译的时候不能确定 _start 的值,而且也不能用 mov 指令来给 r0 赋一个 32bit 的常量,
    所以需要多出一个空间存放 _start 的真正数据,在这里就是 0x0c008014)。

    因此可以看出,这个是绝对的寻址,不管这段代码在什么地方运行,它的结果都是 r0 = 0x0c008014

    结论:    
        
        对于adr r0,XXX      就相当于         ldr r0,=XXX ,通常用于SDRAM 未初始化之前用

ADRL
    adrl <register> <label>

    相于PC寄存器或其寄存器的中范围转移.

    ADRL是中等范围的地址读取指令。会被编译器翻译成两条指令。如果不能用两条指令表示,则产生错误。

    ADRL能加载的地址范围当为非字节对齐时是-64K~64K之间;当为字节对齐时是-256K~256K之间。


    汇编编译器将把adrl指令替换成两条合适的指令,哪怕一条指令就可以完成它的功能。
    
    如果不能用两条指令来实现adrl指令的功能,编译器将报告错误
    
    adrl{cond} register, expr   // 与adr功能相同,但expr的取值范围更大。

    如果地址值不是字对齐的,expr取值范围为-64KB~64KB;

    如果地址值是字对齐的,其取值范围为-256KB~256KB;

    如果地址值是16字节对齐的,取值范围更大
    
    start :   mov r0, #10            // 因为pc值为当前指令地址加8字节

        adr r4, start + 60000            // 本指令将被编译器替换成以下两条指令:
        add  r4, pc, 0xe800
        add  r4, r4, 0x254

    在汇编源程序时,ADRL伪指令被编译器替换成两条合适的指令。
    
    其本质是:将偏移量这个立即数(可能不能被12bit表示出来)拆分为2个可以被12bit表示的立即数,
    
    然后用2条add(或sub)指令来替换adrl伪指令。

     当然你会问,如果那个立即数非常特殊,无论如何也拆分不成2个可以被12bit表示的立即数
    
    (也就是说需要拆分为3个甚至更多的数),那又应该如何办?

    关于这个问题,我在这里不予回答,不过你要记住一句话,如果机器智能到啥都能做的话,你作为程序员就失业了!哈哈!

nop
    空操作, 相当于MOV r0, r0

ARM GNU常用汇编伪指令介绍

1. abort
    .abort: 停止汇编

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

2. if...else...endif
    .if
    .else
    .endif: 支持条件预编译

3. include
    .include "file": 包含指定的头文件, 可以把一个汇编常量定义放在头文件中.

4. comm
    .comm  symbol, length:在bss段申请一段命名空间,该段空间的名称叫symbol,
    长度为length. Ld连接器在连接会为它留出空间.

5. data
    .data subsection: 说明接下来的定义归属于subsection数据段.
    
6. equ
    .equ symbol, expression: 把某一个符号(symbol)定义成某一个值(expression).该指令并不分配空间.

7. global
    .global symbol: 定义一个全局符号, 通常是为ld使用.

8. ascii
    .ascii "string": 定义一个字符串并为之分配空间.

9. byte
    .byte expressions: 定义一个字节, 并为之分配空间.

10. short
    .short expressions: 定义一个短整型, 并为之分配空间.

11. int
    .int expressions: 定义一个整型,并为之分配空间.

12 long
    .long expressions: 定义一个长整型, 并为之分配空间.

13 word
    .word expressions: 定义一个字,并为之分配空间, 4bytes.

14. macro/endm
    .macro: 定义一段宏代码, .macro表示代码的开始, .endm表示代码的结束.

15. req
    name .req register name: 为寄存器定义一个别名.

16. code
    .code [16|32]: 指定指令代码产生的长度, 16表示Thumb指令, 32表示ARM指令.

17. ltorg
    .ltorg: 表示当前往下的定义在归于当前段,并为之分配空间.


2 ARM GNU专有符号

    1. @
        表示注释从当前位置到行尾的字符.

    2. #
        注释掉一整行.

    3. ;
        新行分隔符.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值