跳轉指令
順序、選擇、循環是構建程序的基本結構,任何一個邏輯復雜的程序基本上都可以由這三種程序結構組合而成。而跳轉指令,則在子程序調用、選擇、循環程序結構中被大量使用。程序的跳轉是如何實現的呢?在了解這個機制之前,我們需要先了解一下程序計數器PC。
程序計數器PC,是CPU的寄存器列表中最重要的一個寄存器。它就像一桿槍,指哪打哪:你給PC指針賦值哪個地址,CPU就會到PC指針指向的這個地址去取指令、翻譯指令、執行指令。一般情況下,當你沒有給PC指針賦新地址時,CPU在PC指針指向的地址取完指令后,PC計數器會自動加一,指向下一條指令,程序可以自動執行下去。當我們需要跳轉時,可以直接給PC指針賦一個新地址,于是CPU就會跳轉到新地址去執行了。
在ARM中,常見的跳轉指令有B、BL、MOV、LDR等。不同的指令,它們的使用條件、使用場合是不同的,今天就給大家總結一下它們的區別及各自使用的場合。
1B跳轉指令
B指令是ARM中最基本的跳轉指令,它的使用方法如下:
B label
上面語句表示跳轉到label的標號處去執行。B跳轉指令是ARM中最簡單的指令,只是單純的跳轉,而且是相對跳轉。它可以跳到以當前位置PC為基址,前后32MB的地址空間范圍,所以B指令只是在臨近的代碼塊、標號之間跳轉。
B指令跳轉,大多數時候是單向的,跳過去就不再返回來了。但是我們可以通過添加一些標號來實現一些控制邏輯:比如循環、選擇程序結構:
;循環結構示例
LOOP
SUB R0,R0,#1
...
CMP R0,#0
BNE LOOP
;選擇結構示例
MOV R1,#10
MOV R2,#20
CMP R1,R2
BEQ HERE
...
B END
HERE
...
END
...
在上面的程序中,我們使用B跳轉指令實現了選擇、循環這兩種基本的程序結構。B指令像ARM的其它指令一樣,可以根據CPSR狀態寄存器的標志位,有條件的執行。這樣,可以減少指令數目、提高代碼密度和運行效率。如BNE、BEQ就是當結果相等、不相等時的條件跳轉。
當前程序狀態寄存器:
各種各樣的條件碼:
2BL指令
BL指令跟B不同:在跳轉之前,會先將當前指令的下一條指令地址保存到LR寄存器中,然后才跳轉到標號執行。這樣做的好處是:當我們想從標號地方返回時,可以直接將LR寄存器中的返回地址賦值給PC,程序就可以返回到原來的程序中繼續執行了。
BL跳轉指令一般用在子程序的調用中。無論是匯編語言子程序,還是C語言子程序,在跳轉到子程序之前,都要將返回地址保存起來。當子程序執行完畢,將LR寄存器保存的返回地址,重新賦值給PC,處理器就可以返回到主程序繼續執行了。
BEGIN
MOV R0,#SRC
MOV R1,#DST
MOV R2,#100
BL COPY
NOP
...
COPY
SUB R2,R2,#1
LDR R3,[R0],#1
STR R3,[R1],#1
CMP R2,#0
BNE COPY
MOV PC,LR
上面的匯編代碼段,我們定義了一個匯編子程序COPY,實現了數據拷貝的功能。當我們使用BL指令調用這個子程序COPY時,CPU會首先將當前指令的下一條指令:NOP 的地址保存到LR寄存器中,然后才跳轉到COPY子程序去執行。在COPY子程序中,處理完數據搬運后,通過
MOV PC,LR
這條語句,將保存在LR寄存器中的返回地址,重新賦值給PC,這樣我們就可以返回到原來的程序中繼續執行了。
在上面的匯編代碼中,LR,即R14,連接寄存器,常用來存放程序的返回地址;PC,即R15,程序計數器,表示當前指令地址。LR和PC都是ARM匯編器為了方便程序員編程,預定義的一些宏。你在程序中使用這些助記符其實就是相當于操作R14和R15寄存器。除此之外,ARM中常用的助記符有:
- FP:棧幀基址寄存器,即R12
- SP:棧指針寄存器,即R13
- LR:鏈接寄存器,即R14
- PC:程序計數器,即R15
同樣,在C語言調用子函數的過程中,在跳轉子函數執行之前,CPU也會將當前指令的下一條指令地址保存到LR寄存器中,然后再跳轉到子函數中執行。因為在子函數運行過程中,也有可能會用到ARM的一些寄存器,也有可能會調用其它的子函數,會覆蓋掉保存在LR寄存器中的返回地址,所以,我們一般在運行子函數之前,會首先將LR寄存器壓入子函數的棧幀,相當于將返回地址保存到了棧上。當子函數運行結束時,再通過出棧操作,將保存在棧中的返回地址彈出到PC指針中,這樣程序就成功從子程序中返回了,直接返回到原來的函數中繼續執行。
int main(void)
{
func();
printf("Hello!\\n");
return 0;
}
;對應的匯編代碼
main
BL func
BL printf
func
PUSH LR
...
pop pc
;func子函數返回
3MOV指令
通過上面的學習,我們可以看到,無論是B指令、還是BL指令,都是相對尋址。其本質都是以當前指令地址PC為基址,然后加上一個[0,32M]的偏移,達到修改PC的目的。
除此之外,我們也可以直接給PC指針賦值,達到跳轉的目的。如上面的 func 子程序返回,就是直接通過
MOV PC,LR
這條指令,將LR寄存器中的返回地址,直接賦值給PC,直返回到原來的主函數去執行。
MOV指令主要用來在寄存器之間傳輸數據,或者將一個立即數傳送到寄存器。但是MOV指令有一個硬傷,就是傳遞的立即數只能是8位數,有大小的限制。這是為什么呢?很簡單,ARM是RISC架構,在一個32位的ARM中,指令通常都是32位的。而一個指令中,通常要包括操作碼+操作數,如下圖:
一條指令,總共有32個bit空間,MOV這個操作碼要占幾位吧,Rd寄存器編碼要占據幾位吧,剩下的留給立即數的空間就不多了,所以這也就限定了MOV指令能傳遞的立即數的大小了。而一般的32位程序中,無論是變量還是函數,它們的地址一般都是32位的,如果使用MOV指令,將他們的地址傳送到PC,使用下面的形式:
MOV PC,#0x30008000
你會發現,立即數#0X30008000這個地址就已經32位了,在加上MOV指令這個操作碼,已經超過32位了,編譯器是無法翻譯這個指令的,所以說,當一個變量或函數地址為32位時,使用MOV指令給PC直接賦值,行不通,那怎么辦呢?
4LDR偽指令
辦法總是有的,比如,我們就可以通過偽指令LDR,直接將一個32位的立即數地址,傳送到PC:
LDR PC,=0x30008000
LDR偽指令的功能和MOV一樣,都可以將一個立即數傳送到寄存器。唯一區別的就是,MOV指令只能傳送8位的,而LDR可以傳送一個32位的立即數或地址。
這里需要注意一下,立即數 0X30008000 的前面有一個等于號“=”,這表示前面的LDR指令是一個偽指令。除此之外,在ARM中,LDR還有另外一個意思,用來將內存中的數據加載到寄存器。我們知道,ARM是RISC架構,使用LDR/STR架構,不能直接修改內存中的數據。如果我們要修改內存中的一個變量,要首先使用LDR指令將內存中的變量加載到寄存器中,接著對寄存器進行操作,最后再使用STR指令將寄存器中的變量回寫到內存中。所以,LDR可以看作是一個偽指令,也可以看做是普通的一個LDR指令,判定他們的區別就是看前面的等于號。
普通的LDR指令主要使用寄存器間接尋址,常用的使用方式如下:
LDR R0,[R1]
LDR R0,0x30008000
這里注意后面一句,是將地址0x30008000地址上的內容傳動到寄存器R0,而不是直接將這個地址傳送到R0,這里一定要注意其跟LDR偽指令的區別,這一點沒有注意到,你在分析程序時就可能誤入歧途了。
在《C語言嵌入式Linux高級編程》第二期中,我們已經探討了CPU、指令集、偽指令的基本概念,這里就不贅述了。簡單來說,偽指令并不是真正的ARM指令,并不屬于ARM指令集中的標準指令。它只是編譯器為了方便我們程序員開發程序,定義的一些助記符。在編譯時,這些偽指令還是會使用指令集中的標準指令來實現。
比如上面的LDR偽指令,程序在編譯時,看到這個偽指令,會使用ARM指令集中標準的指令實現。如果LDR偽指令中的立即數小于8位,它就會轉換為MOV指令來實現:
LDR R0,=200
MOV R0,#200
如果LDR偽指令中,立即數大于8bit表示的數據范圍,比如說是一個32位的立即數或地址,那就不能使用MOV指令來實現了,可以采用文字池的形式,先將這個地址常量單獨存放在存儲單元中,然后使用相對尋址,曲線救國,完成這個32位地址或立即數與寄存器之間的傳輸,這些細節在教程視頻中都有講到,就不再贅述了。
5小結
通過上面的學習,我們基本上理清了ARM系統中常見的幾種跳轉指令,以及它們的區別。只有徹底理解他們的底層機制及實現細節,才有可能在使用反匯編分析程序時,達到事半功倍的效果,從而大大提高我們的工作效率。否則,這些基本的細節和概念搞不清,將會永遠成為你學習和工作上的障礙。
-
計數器
+關注
關注
32文章
2261瀏覽量
94983 -
程序結構
+關注
關注
1文章
7瀏覽量
6946 -
程序調用
+關注
關注
0文章
3瀏覽量
837
發布評論請先 登錄
相關推薦
評論