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
,那么會運行失敗,反之亦然。(因此在編寫跳轉指令時,需要明確跳轉的目標地址處是什么指令集,該切換指令集時切換,不該切換時別瞎切~)