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