ARM的跳转及指令集切换
B
BL
BX
BLX
Thumb与ARM的切换
条件分支就是典型的跳转指令,这在编程中必不可少,arm
有2种方式支持指令跳转
- 使用
B
系列指令(B
有很多带后缀的其他指令) - 直接修改
pc
的值
跳转指令
B
B
,就是最直接最基础的跳转,没有副作用BL
,将BL
的下一条指令保存在lr
寄存器中,然后跳转,这种跳转方式通常需要在执行跳转任务后需要回到出发处的
除了这2个最基本的通用跳转外,还有与状态寄存器想配合的条件跳转
;因为arm
架构指令集本身就是带执行条件的架构, 与状态寄存器搭配使用的跳转有比如 beq,bge,blcc
等。需要注意的是跳转目的地并非直接编码入指令中的,而是目的地的偏移地址,也就是the_jump_target_addr = current_addr+offset_addr
中的那个offset_addr
;这样的意义可以大幅的减少编码位置的占用,因此thumb
跟arm
指令集占用的偏移长度位数是不一样的,arm
能访问到的内容空间是上下32M
,而thumb
访问的空间是上下16M
因为指令编码的原因,B
系指令跳转空间受限,而直接将一个地址值立即数写入pc
跳转是可行的,这样就不受限地址空间。不过这里比较棘手的问题在于arm
流水线问题:
大部分时候跳转后都需要调回来,这时候需要知道当前指令的地址值,那么当前指令的地址值就是此时pc
寄存器中的值吗?答案是否定的,因为arm
中fetch
、decode
excute
三级流水的结构,导致本条指令执行时,pc
的值已经是下下条指令的地址值了,这对于arm
指令集而言是current_code_addr = pc - 8
,而对于thumb
指令集而言current_code_addr = pc - 4
;可能有的cpu
设计了不止3级流水线,或者有5级流水线(取指、译码、执行、访存、回写
5级流水线),但是前三条结构是一致的,因此无论如何pc
寄存器与当前指令的地址值关系是确定的。
因此,这里的麻烦之处在于需要计算准确的pc
值,而这又取决于不同的指令集,当然,这是出现在读pc
寄存器值时候的问题。
写 pc
指针并不会出现读 pc
指针的问题,在 thumb
指令集中,add、 mov、pop
等指令可以写 pc
执行跳转,写入 PC
的值将会被强制对齐,对齐的字节数根据对应的指令集而定,thumb
下是半字(2字节
),arm
下是字(4字节
)。除了通用指令写 pc
,还有一些专门用于跳转的指令默认操作的就是 pc
指针,比如 B、BL、BX、BLX
等,这些是一些复合指令,也就是说这些指令包含的操作可能不仅仅是对 pc
的操作,可能还隐含其它操作(比如修改lr
寄存器和切换指令集)
指令集切换
armv7
支持thumb
和arm
两种指令集,分别用16bits
和32bits
指令长度,这两种长度各有优劣,比如thumb
指令集的指令密度是要大于arm
指令集的 ,而arm
指令集的性能则更为强大(因为更长的指令编码可以将多个步骤合一以及更多资源的访问能力),因此将两者结合起来使用做到指令切换是有其现实积极意义的(不过这给程序员造成了一些麻烦)
-
BX
BX Rm
,rm
有4bits
,因此支持r0~r15
,在arm
指令集下bx
的指令编码格式是:
显然,在arm
指令集下是支持cond
条件执行的,因此有bxz、bxne
等指令;使用bx
切换指令集的依据是根据跳转目标地址的最后1bits
决定,arm
指令集是定长4字节,因此其指令地址值不可能为奇数,这就决定了其最后1bit
不可能为1
,因此利用这个特性,bx
指令当发现跳转目的地址最后一位为0时,则切换或者保持在arm
指令集,否则,将切换或保持在thumb
指令集。
-
BLX
blx
是带返回的跳转,它的指令集切换分为2种情况:blx register
blx imm
当为
blx register
时,情况与bx
相同。当为
blx imm
时,则为无条件指令切换,也就是若当前为arm
指令集,则切换到thumb
指令集,若当前为thumb
指令集,则切换到arm
指令集。
当直接采用修改pc
寄存器的值来做跳转时,也涉及到指令集切换的问题,这一情况与bx
一致,也是根据跳转目标地址的最后1bit
来做决定。
main:
adr r0,back #获取 back 标号的地址
push {r0} #将 back 地址保存在栈上
adr r0,foo #获取 foo 标号的地址,当前处于 arm 指令集
add r0,r0,#1 #将 foo 地址最后一位加1,表示切换到 thumb 指令集
mov pc,r0 #跳转到 foo 地址
back:
blx _exit #退出
.thumb #指定代码编译为 thumb 指令集
foo:
pop {pc} #将栈上保存的 back 标号地址赋值给 pc,即实现跳转,同时指令集切换为 arm
上面有几处需要注意:
adr
伪指令的使用,arm
的跳转都是基于偏移而非绝对地址,因此使用pc
进行跳转时需要将偏移值赋值给pc
寄存器,而这又是一件比较麻烦的事,我们需要跳转到back
地址,不能直接将标签back
处的地址值赋值给pc
寄存器,应该是当前跳转执行指令地址值 - back
得到一个偏移值,然后赋值给pc
,那么,当前跳转执行指令地址值
是多少呢,显然就是当前这条指令的pc
值,但是,因为流水线的存在,实际上此时的pc
已经位于下下条指令处,因此此刻的真实pc
值应该是pc-4
或者pc-8
,具体是减多少,这取决于此刻是thumb
指令集还是arm
指令集,这就是棘手的地方!也就是我们必须首先判断此刻是什么指令集,然后再用pc
减去对应的大小,最终算得偏移赋值给pc
寄存器,一个简单的跳转,实在太难了...好在!adr
的出现帮我们解决了这个问题,我们完全不用操心此刻是什么指令集模式,也不用手动去做标签的减法,adr
伪指令编译器会帮我们转换为合适的真实硬件指令,得到最终的跳转偏移值,简直大救星!!!add r0,r0,#1
,将地址值加一,是的最后1bit
为1,这样目标地址就被切换为thumb
指令集(因为我们的跳转目标foo
为thumb
指令集模式)pop {pc}
,将栈中保持的立即数(之前push
的返回地址)pop
赋值给pc
寄存器,因为此时处于thumb
指令集模式下,因此直接切换成arm
指令集
需要注意的是,切换指令集是汇编指令运行时的事,而某指令是用何种指令集是在汇编编期就决定(通过伪指令.arm
和.thumb
决定),因此使用汇编指令跳转到某目标地址,当该地址处的指令集为arm
时,而你跳转时却切换为了thumb
,那么会运行失败,反之亦然。(因此在编写跳转指令时,需要明确跳转的目标地址处是什么指令集,该切换指令集时切换,不该切换时别瞎切~)
posted on 2021-12-28 00:02 shadow_fan 阅读(1221) 评论(0) 编辑 收藏 举报