单片机
返回首页

ARM编程进阶之一 —— ARM汇编伪指令

2023-05-06 来源:elecfans

到目前为止,我们已经具备编写较为复杂的ARM汇编程序的能力,但要编写较为复杂且实用的程序,我们就不得不掌握ARM汇编的伪指令(pseudo-instruction)。千万别把汇编伪操作(directive)与汇编伪指令(pseudo-instruction)弄混了,directive不会被编译器编译为机器指令,但pseudo-instruction会。而pseudo-instruction与指令(instruction)的区别在于,1条instruction与1条机器指令对应,而编译器会把1条pseudo-instruction编译为1条或多条机器指令。

ARM汇编伪指令共4条:ldr、adr、adrl、nop

1、ldr

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

image

其实,这个问题很容易解决,只需要将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, =0×100,那么,编译器对该伪指令的处理,是使用MOV(或者MVN)指令代替该LDR伪指令,例如:mov r0, #0×100,而不会采用ldr指令+文字池的方式。

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

image

由上图可见: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所代表的地址是0×64?

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

未标题-1

c)、如果将来程序并没有运行在它的运行地址处,很显然这个程序就会出问题。如何解决?

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

2、adr

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

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

image

很显然,由于将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表示出来。此时编译会产生错误。如果出现这样的情况,又应该如何办?

使用下面要讲的伪指令adrl

3、adrl

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

image

当然你会问,如果那个立即数非常特殊,无论如何也拆分不成2个可以被12bit表示的立即数(也就是说需要拆分为3个甚至更多的数),那又应该如何办?关于这个问题,我在这里不予回答,不过你要记住一句话,如果机器智能到啥都能做的话,你作为程序员就失业了!哈哈!

4、nop

NOP是no operation的意思,就是CPU不做任何事的意思。这里千万要明白,CPU一旦上电就将永不停歇地运行,绝不可能有一条指令能另CPU什么都不做。所以,该伪指令在汇编时将会被代替成“MOV R0,R0”指令。

NOP主要用于短延时操作,关于这一点,这里我要多说几句。与应用程序不同,汇编程序通常要用于控制硬件,例如,你需要使用汇编程序要求某个硬件执行某个操作(例如:初始化nandflash),并要在该操作完成后才能进行后面的操作,而硬件从接到命令到完成该操作需要一段时间(该时间很短,但对CPU而言却较长,通常需要几个指令周期)。此时,程序员就必须在发出命令的指令和后续操作之间做延时,通常的做法就是加入几个NOP伪操作。

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

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


进入单片机查看更多内容>>
相关视频
  • RISC-V嵌入式系统开发

  • SOC系统级芯片设计实验

  • 云龙51单片机实训视频教程(王云,字幕版)

  • 2022 Digi-Key KOL 系列: 你见过1GHz主频的单片机吗?Teensy 4.1开发板介绍

  • TI 新一代 C2000™ 微控制器:全方位助力伺服及马达驱动应用

  • MSP430电容触摸技术 - 防水Demo演示

精选电路图
  • 家用电源无载自动断电装置的设计与制作

  • 短波AM发射器电路设计图

  • 带有短路保护系统的5V直流稳压电源电路图

  • 如何调制IC555振荡器

  • 基于ICL296的大电流开关稳压器电源电路

  • 基于TDA2003的简单低功耗汽车立体声放大器电路

    相关电子头条文章