frankfan的胡思乱想

学海无涯,回头是岸

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;这样的意义可以大幅的减少编码位置的占用,因此thumbarm指令集占用的偏移长度位数是不一样的,arm能访问到的内容空间是上下32M,而thumb访问的空间是上下16M

image.png

arm与thumb的跳转空间

因为指令编码的原因,B系指令跳转空间受限,而直接将一个地址值立即数写入pc跳转是可行的,这样就不受限地址空间。不过这里比较棘手的问题在于arm流水线问题:

image.png
单周期最佳流水线

大部分时候跳转后都需要调回来,这时候需要知道当前指令的地址值,那么当前指令的地址值就是此时pc寄存器中的值吗?答案是否定的,因为armfetchdecode 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支持thumbarm两种指令集,分别用16bits32bits指令长度,这两种长度各有优劣,比如thumb指令集的指令密度是要大于arm指令集的 ,而arm指令集的性能则更为强大(因为更长的指令编码可以将多个步骤合一以及更多资源的访问能力),因此将两者结合起来使用做到指令切换是有其现实积极意义的(不过这给程序员造成了一些麻烦)

  • BX

    BX Rmrm4bits,因此支持r0~r15,在arm指令集下bx的指令编码格式是:

    image.png

bx指令的arm编码格式

显然,在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指令集(因为我们的跳转目标foothumb指令集模式)
  • pop {pc},将栈中保持的立即数(之前push的返回地址)pop赋值给pc寄存器,因为此时处于thumb指令集模式下,因此直接切换成arm指令集

需要注意的是,切换指令集是汇编指令运行时的事,而某指令是用何种指令集是在汇编编期就决定(通过伪指令.arm.thumb决定),因此使用汇编指令跳转到某目标地址,当该地址处的指令集为arm时,而你跳转时却切换为了thumb,那么会运行失败,反之亦然。(因此在编写跳转指令时,需要明确跳转的目标地址处是什么指令集,该切换指令集时切换,不该切换时别瞎切~)

posted on 2021-12-28 00:02  shadow_fan  阅读(1221)  评论(0编辑  收藏  举报

导航