ARM程式設計高階之一 ARM彙編偽指令

2021-08-11 06:20:40 字數 4023 閱讀 6168

到目前為止,我們已經具備編寫較為複雜的arm匯程式設計序的能力,但要編寫較為複雜且實用的程式,我們就不得不掌握arm彙編的偽指令(pseudo-instruction)。千萬別把彙編偽操作(directive)與彙編偽指令(pseudo-instruction)弄混了,directive不會被編譯器編譯為機器指令,但pseudo-instruction會。而pseudo-instruction與指令(instruction)的區別在於,1條instruction與1條機器指令對應,而編譯器會把1條pseudo-instruction編譯為1條或多條機器指令。

arm彙編偽指令共4條:ldr、adr、adrl、nop

1、ldr

首先我們來回答「基本定址模式與基本指令」一文中提出的問題。「如果我們需要mov r0, #10000這樣的指令,應該怎麼辦?(常數10000不能在機器指令32bit中的低12bit中被表示出來)」。當你進行編譯的時候,「error:all70e」的錯誤就會出現,如下圖。

其實,這個問題很容易解決,只需要將mov r0, #10000換為ldr r0, =10000即可。為什麼這樣就可以了呢?因為,這裡的ldr r0, =10000並非我們已經學過的ldr指令,而是一條偽指令,編譯器會將這條偽指令替換為:

ldr r0, [pc, #-4]

dcd 10000

dcd所分配的記憶體空間中存放了整數10000,該記憶體空間被稱為literal pool,中文名稱「文字池」。由於整個程式都是由編譯器編譯的(包括文字池的分配),所以很顯然編譯器能夠知道ldr指令在記憶體中的位址與文字池在記憶體中的位置之間的偏移量,因此編譯器就可以正確地使用以pc為基址,採用相對定址的ldr指令將文字池中的數取出載入到暫存器r0中。由此可見,編譯器對於ldr r0, =10000這條偽指令的處理,其實質是:

在彙編源程式時,ldr偽指令被編譯器替換成一條合適的指令和存放常數的文字池。彙編器將常量放入文字池,並使用一條程式相對偏移的ldr指令從文字池讀出常量。

由於,4byte可以存放任何int型整數,這樣一來,常數就可以是任何int型整數,而不再受制於12bit的限制。當然此時的常數是存放在記憶體中的,而不是存放在機器指令的32bit編碼中的。

額外說明:

a)、ldr r0, [pc, #-4]中是-4,而不是+4,是由於流水線的原因(參見「流水線對pc值的影響」一文)。今後對於流水線的這種影響,我將不再予以特別說明。

b)、從指令位置到文字池的偏移量必須小於4kb

c)、從語法上來看,與arm指令的ldr相比,偽指令ldr的引數有「=」號,沒有「#」號

d)、如果常數能夠被12bit表示出來,例如:ldr r0, =0x100,那麼,編譯器對該偽指令的處理,是使用mov(或者mvn)指令代替該ldr偽指令,例如:mov r0, #0x100,而不會採用ldr指令+文字池的方式。

除了 ldr 暫存器, =常數 這種形式外,還有ldr 暫存器, =標號 這種形式也經常被使用,下面我就來講解這種形式的ldr偽指令。

由上圖可見:ldr pc, =initstack這條偽指令的作用是將標號initstack所代表的位址賦予pc。 這裡會使我們產生幾個疑問:

a)、為什麼不使用bl initstack,而要使用ldr pc, =initstack?

這是因為bl指令的跳轉範圍是正負32m,而initstack所代表的位置有可能距離ldr pc, =initstack超過32m,此時bl就無能為力了。竟然存在這麼大範圍跳轉的程式(這似乎意味著編譯出來的二進位制可執行檔案的大小會操作32m),這一點似乎令我們感到非常震驚。事實上是:大小超過32m的可執行程式的確幾乎不可能出現,但即使是很小的二進位制程式中也可能進行大範圍(超過32m)的跳轉,這一點在bootloader程式中幾乎是必然的。

b)、編譯器是如何得出initstack所代表的位址是0x64?

編譯器知道mov r0, lr這條指令相對於整個程式的第1條指令的偏移量為0x64;同時又知道這個程式將來在記憶體中的執行位址為0x0,所以編譯器在編譯的時候(不是程式執行的時候)就可以確定initstack所代表的位址為0x64+0x0=0x64。那麼編譯器又是如何知道「程式將來在記憶體中的執行位址」的呢?其實,這個「程式將來在記憶體中的執行位址」,我通常稱它為「程式的期望執行位址」,簡稱「執行位址」,以後我也將這樣稱呼它。它其實是在編譯程式前由程式設計師告知編譯器的。

c)、如果將來程式並沒有執行在它的執行位址處,很顯然這個程式就會出問題。如何解決?

出問題的原因,顯然是由於ldr偽指令使用的絕對位址。所以,解決的辦法就是使用相對位址,進行相對定址。這就要用到我們下面要介紹的另一條偽指令adr

2、adr

adr偽指令的作用與ldr偽指令的作用相同,都是將標號所代表的位址賦予暫存器,不過2者的實現機制是完全不同的:ldr採用絕對位址,adr採用相對位址。

adr偽指令將基於pc相對偏移的位址值讀取到暫存器中。在彙編源程式時,adr偽指令被編譯器替換成一條合適的指令。通常,編譯器用一條add指令或sub指令來實現該adr偽指令的功能。

很顯然,由於將adr r0, delay替換為add r0, pc, #0x3c,而它是以當前指令的位址(pc的值)進行相對位址計算的。所以即使將來程式沒有實際執行在執行位址處,也不會有問題。

當然,這裡還有2個問題:

a)、既然adr和ldr完成類似的功能,adr又能避免絕對位址的問題,還要ldr偽指令有何用?

這主要是因為,adr偽指令要求標號與adr偽指令必須在同乙個段中(段的概念參見「arm彙編偽操作」一文),而ldr偽指令則沒有這樣的要求。

b)、add r0, pc, #0x3c中的常數0x3c是放在機器指令12bit中的立即數,這個立即數有可能不能被12bit表示出來。此時編譯會產生錯誤。如果出現這樣的情況,又應該如何辦?

使用下面要講的偽指令adrl

3、adrl

在彙編源程式時,adrl偽指令被編譯器替換成兩條合適的指令。其本質是:將偏移量這個立即數(可能不能被12bit表示出來)拆分為2個可以被12bit表示的立即數,然後用2條add(或sub)指令來替換adrl偽指令。

當然你會問,如果那個立即數非常特殊,無論如何也拆分不成2個可以被12bit表示的立即數(也就是說需要拆分為3個甚至更多的數),那又應該如何辦?關於這個問題,我在這裡不予回答,不過你要記住一句話,如果機器智慧型到啥都能做的話,你作為程式設計師就失業了!哈哈!

4、nop

nop是no operation的意思,就是cpu不做任何事的意思。這裡千萬要明白,cpu一旦上電就將永不停歇地執行,絕不可能有一條指令能另cpu什麼都不做。所以,該偽指令在彙編時將會被代替成「mov r0,r0」指令。

nop主要用於短延時操作,關於這一點,這裡我要多說幾句。與應用程式不同,匯程式設計序通常要用於控制硬體,例如,你需要使用匯程式設計序要求某個硬體執行某個操作(例如:初始化nandflash),並要在該操作完成後才能進行後面的操作,而硬體從接到命令到完成該操作需要一段時間(該時間很短,但對cpu而言卻較長,通常需要幾個指令週期)。此時,程式設計師就必須在發出命令的指令和後續操作之間做延時,通常的做法就是加入幾個nop偽操作。

附:ldr指令與ldr偽指令的4種形式(這4種形式,極其容易讓初學者困惑,所以在此集中列出)

ldr r0, [r1]

指令,將記憶體存放的內容載入到r0中

ldr r0, label

指令,將標號label所代表的記憶體位址處存放的內容載入到r0中

ldr r0, =10000

偽指令,將常數10000賦予r0(採用ldr指令+文字池的方式實現)

ldr r0, =lable

偽指令,將標號label所代表的記憶體位址賦予r0

ARM程式設計高階之一 ARM彙編偽指令

到目前為止,我們已經具備編寫較為複雜的arm匯程式設計序的能力,但要編寫較為複雜且實用的程式,我們就不得不掌握arm彙編的偽指令 pseudo instruction 千萬別把彙編偽操作 directive 與彙編偽指令 pseudo instruction 弄混了,directive不會被編譯器編...

ARM彙編程式設計基礎之四 ARM彙編偽操作

掌握了基本的arm彙編指令後,要寫出簡單的arm匯程式設計序,還必須要掌握基本的arm彙編偽操作 directive 現在我們來看乙個簡單的匯程式設計序,該程式呼叫子程式完成了加法操作。1 檔名 test.s 2 功能 實現兩個暫存器相加 3 area example,code,readonly 宣...

ARM彙編程式設計基礎之四 ARM彙編偽操作

掌握了基本的arm彙編指令後,要寫出簡單的arm匯程式設計序,還必須要掌握基本的arm彙編偽操作 directive 現在我們來看乙個簡單的匯程式設計序,該程式呼叫子程式完成了加法操作。1 檔名 test.s 2 功能 實現兩個暫存器相加 3 area example,code,readonly 宣...