我们知道ARM CPU中有一条被广泛使用的指令LDR,它主要是用来从存储器(确切地说是地址空间)中装载数据到通用寄存器。但不论是ARMASM还是GNU ARM AS,都提供了一条与之同名的伪指令LDR,而在实际中使用该伪指令的情况也较多,那他们有什么不同呢?下面我谈谈我的理解。
由于我使用GNU工具链,所以以下的内容都以GNU AS的ARM语法为准。
LDR伪指令的语法形式如下:
LDR , =
这个常量表达式中可以包含Label(在ARM汇编中Label会在连接时解释为一个常数),且其中的常数前不加#符号。
范例demo.s:
.equ STACK_BASE, 0x0c002000
.equ STACK_SIZE, 0x00001000
.text
ldr sp, = STACK_BASE
ldr sl, = STACK_BASE - STACK_SIZE
ldr pc, = entry
这是一个合法的汇编文件,它把堆栈基址设为0x0c002000,栈限设为0x0c001000,然后跳到entry所标识的C程序中执行。
下面我们假设符号“entry”的地址为0x0c000000。
我们如果把上面代码写成:
.text
mov sp, #0x0c002000
mov sl, #0x0c001000
mov pc, #0x0c000000
汇编器会报错:
demo.s: Assembler messages:
demo.s:2: Error: invalid constant -- `mov sp,#0x0c002000'
demo.s:3: Error: invalid constant -- `mov sl,#0x0c001000'
说起这个错误的原因可就话长了,简而言之是因为RISC有一个重要的概念就是所有指令等长。在ARM指令集中,所有指令长度为4字节(Thumb指令是2字节)。那问题就来了,4字节是不可能同时存的下指令控制码和32位立即数的,那么我要把一个32位立即数(比如一个32位地址值)传送给寄存器该怎么办?
RISC CPU提供一个通用的方法就是把地址值作为数据而不是代码,从存储器中相应的位置读入到寄存器中,待会我们会看到这样的例子。
此外ARM还提供另一种方案。由于传送类指令的指令控制码部分(cond, opcode, S, Rd, Rn域)占去了20个字节,那能提供给立即数的就只剩12个位了。
ARM并未使用这12个位来直接存一个12位立即数,而是使用了类似有效数字一样的概念,只存8个字节的有效位和一个4位的位偏移量(偏移单位为2)。这个东西在ARM被叫做术语immed_8,有兴趣的人可以找资料了解一下,到处都有介绍。
可以看出ARM的这个方法能直接使用的立即数是相当有限的,像0xfffffff0这样的数显然无法支持。别着急,ARM的传送类指令中还有一个MVN指令可以解决该问题。显然0x0000000f是一个有效立即数,MVN会先将其取反再传送,这样有效立即数的范围又扩充了一倍。
可就算如此仍有大量的32位立即数是无效的,比如上面那个例子中的0x0c002000和0x0c001000。面对这种问题一是使用RISC的通用方法,二是分次载入。
比如可以这样载入0x0c002000:
.text
mov sp, #0x0c000000
add sp, sp, #0x00002000
或者:
.text
mov sp, #0x0c000000
orr sp, sp, #0x00002000
感觉很狡猾是吧,呵呵。但是要注意它和方法一的一大区别:需要多条指令。那么在一些对指令数目有限制的场合就无法使用它,比如异常向量表处要做长跳转(超过±32MB
)的话就只能用方法一;还有就是在同步事务中该操作不是原子的,因此可能需要加锁。
扯了这么多再回到LDR伪指令上来。显然上面的内容是复杂繁琐的,如果然程序员在写程序的时候还要考虑某个数是不是immed_8一定超级麻烦,因此为了减轻程序员的负担才引入了LDR伪指令。
你一定很好奇第一段代码demo.s被GNU AS变成了什么,好,让我们在Linux环境下执行下面的命令:
arm-elf-as -o demo.o demo.s
arm-elf-objdump -D demo.o
结果:
demo.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <.text>:
0: e59fd004 ldr sp, [pc, #4] ; c <.text+0xc>
4: e59fa004 ldr sl, [pc, #4] ; 10 <.text+0x10>
8: e59ff004 ldr pc, [pc, #4] ; 14 <.text+0x14>
c: 0c002000 stceq 0, cr2, [r0]
10: 0c001000 stceq 0, cr1, [r0]
14: 00000000 andeq r0, r0, r0
Disassembly of section .data:
由于entry还没连上目标地址,objdump反汇编会认为是0,我们先不管它。另外两条LDR伪指令变成了实际的LDR指令!但目标很奇怪,都是[pc, #4]。那好我们看看[pc, #4]是什么。
我们知道pc中存放的是当前指令的下下条指令的位置,也就是. + 8。那么上面的第一条指令ldr
sp, [pc, #4]中的pc就是0x8,pc + 4就是0xc,而[0xc]的内容正是0x0c002000;同理,第二条ldr指令也是如此。显然这里LDR伪指令采用的是RISC通用的方法。
另外要说的是,如果LDR的是一个immed_8或者immed_8的反码数,则会直接被解释成mov或mvn指令。如ldr pc, = 0x0c000000会被解释成mov pc, 0x0c000000。
最后一点补充,我发现arm-elf-gcc通常都用累加法。如C语句中的i = 0x100ffc04;会变成类似于以下的语句:
mov r0, #0x10000004
add r0, r0, #0x000ff000
add r0, r0, #0x00000c00
...
原因不详。
LDR指令详解(2009-10-27 11:26:31)
标签:
杂谈 分类:
ARM
ARM指令集中,LDR通常都是作加载指令的,但是它也可以作伪指令。
(1)LDR r0,=name,像这种带等号的是伪指令,而不是ARM指令,LDR 伪指令用于加载立即数或一个地址值到指定寄存器.
*如果name是立即数的话:LDR R0,=0X123;//将0X123存入R0
*如果name是个标识符:LDR R0,=NAME;//将NAME的地址存入R0
相当于:
LDR R0,LABEL;
LABEL DCB NAME;//分配内存并用NAME初始化(LABEL为内存的起始地址?)
(2)LDR R1,[R0] ;如果没有等号,LDR 指令用于从内存中读取数据放入寄存器中.该指令是 将R0 地址处的数据读出,保存到R1 中(零偏移)。
LDR/STR字和无符号字节加载/存储
1,LDR Rd,[Rn]
2, LDR Rd,[Rn,Flexoffset]
3, LDR Rd,[Rn],Flexoffset
4, LDR Rd,label
ldr只能在当前PC的4KB范围内跳转
B只能在当前PC的32M范围内跳转
label标号实际上就是个地址
eg:
合法:
ldr r1,[r2]
ldr r1,[r2,#0x4];不能超过0xfff,否侧编译不能通过或者linker时有错
ldr r1,[r2,#label];所以这个经常是编译不能通过,因为label的值一般都大于0xfff
ldr r1,[r2],#0x4
ldr r1,label ;把label这个地址里面的内容赋给r1
ldr伪指令
ldr r1,=0x2000014 ;将0x2000014付给r1.
ldr r1,=label ;把label这个地址值赋给r1
不合法:
ldr r1,#0x14
ldr r1,[#0x14]
ldr r1,[0x14]
ldr r1,#label
ldr r1,=#label
ldr r1,[label]
ldr r1,[#label]
ldr r1,[=label]
ldr r1,[r2,label]
ldr r1,[r2,=label]
ARM是RISC结构,数据从内存到CPU之间的移动只能通过L/S指令来完成,也就是ldr/str指令。
比如想把数据从内存中某处读取到寄存器中,只能使用ldr
比如:
ldr r0, 0x12345678
就是把0x12345678这个地址中的值存放到r0中。
而mov不能干这个活,mov只能在寄存器之间移动数据,或者把立即数移动到寄存器中,这个和x86这种CISC架构的芯片区别最大的地方。
x86中没有ldr这种指令,因为x86的mov指令可以将数据从内存中移动到寄存器中。
另外还有一个就是ldr伪指令,虽然ldr伪指令和ARM的ldr指令很像,但是作用不太一样。ldr伪指令可以在立即数前加上=,以表示把一个地址写到某寄存器中,比如:
ldr r0, =0x12345678
这样,就把0x12345678这个地址写到r0中了。所以,ldr伪指令和mov是比较相似的。只不过mov指令限制了立即数的长度为8位,也就是不能超过512。而ldr伪指令没有这个限制。如果使用ldr伪指令时,后面跟的立即数没有超过8位,那么在实际汇编的时候该ldr伪指令是被转换为mov指令的。
ldr的确是个复杂的指令,现总结一下:
首先要判断我们用的ldr 是arm指令还是伪指令。 当我们用的是arm指令时,它的作用不是向寄存器里加载立即数,而是将某个地址里的内容加载到寄存器。而伪指令ldr的作用就是向寄存器里加载立即数。
(1) ldr伪指令
ldr伪指令的格式是 ldr Rn, =expr
其中,expr是要加载到Rn中的内容,一般可以是立即数或者label。
如果expr可以用8bit数据向右移偶数位得到,那么这条伪指令就被编译器翻译成mov指令。具体的移位情况可以去查阅资料。反之如果立即数很大,超过了12bit的表示范畴,那么就不能用一条mov指令了,毕竟arm指令最大只有32bit的空间可用(RISC的arm所有的指令长度是一致的,效率较高,当然我们并不关心16bit的thumb指令)。如果不能用一条32bit的指令乘下来,那么就只能另辟蹊径了,新开一段缓冲,将立即数expr放到里面,然后将其地址(暂时标记为addr)拿来使用:
ldr Rn, addr
xxx (xxx就是expr)
xxx
由于编译器一般来说新安排的存储这个立即数expr的缓冲的位置是在相应代码的附近(这个应该可以使用.ltorg伪指令控制),然后从addr地址加载数据到Rn,就可以了。
(2)ldr arm 指令
就是将一个地址的内容加载到寄存器。不能用mov,因为arm里的mov只是在寄存器之间传输数据,不支持在寄出器和memory之间传递数据。因此就出现了ldr/str指令。如ldr Rn, addr,注意这里的addr的值也是有限制的。这个label应该距离当前指令的距离不超过4k。因为我们知道label在具体使用的时候应该是被翻译成了相对偏移,如果这个label长度不超过12bit,那么就不应超过4k,我们可以这样做:
ldr pc, _start_armboot
_start_armboot: .word arm_startboot
这样label _start_armboot就在指令下方,因此肯定是合法的。