【ARM 匯編基礎(chǔ)速成4】ARM匯編內(nèi)存訪問(wèn)相關(guān)指令

原文鏈接 https://azeria-labs.com/memory-instructions-load-and-store-part-4/

ARM使用加載-存儲(chǔ)模式控制對(duì)內(nèi)存的訪問(wèn),這意味著只有加載/存儲(chǔ)(LDR或者STR)才能訪問(wèn)內(nèi)存。盡管X86中允許很多指令直接操作在內(nèi)存中的數(shù)據(jù),但ARM中依然要求在操作數(shù)據(jù)前,必須先從內(nèi)存中將數(shù)據(jù)取出來(lái)。這就意味著如果要增加一個(gè)32位的在內(nèi)存中的值,需要做三種類(lèi)型的操作(加載,加一,存儲(chǔ))將數(shù)據(jù)從內(nèi)存中取到寄存器,對(duì)寄存器中的值加一,再將結(jié)果放回到內(nèi)存中。

為了解釋ARM架構(gòu)中的加載和存儲(chǔ)機(jī)制,我們準(zhǔn)備了一個(gè)基礎(chǔ)的例子以及附加在這個(gè)基礎(chǔ)例子上的三種不同的對(duì)內(nèi)存地址的便宜訪問(wèn)形式。每個(gè)例子除了STR/LDR的偏移模式不同外,其余的都一樣。而且這個(gè)例子很簡(jiǎn)單,最佳的實(shí)踐方式是用GDB去調(diào)試這段匯編代碼。

第一種偏移形式:立即數(shù)作為偏移

  • 地址模式:用作偏移
  • 地址模式:前向索引
  • 地址模式:后向索引

第二種偏移形式:寄存器作為偏移

  • 地址模式:用作偏移
  • 地址模式:前向索引
  • 地址模式:后向索引

第三種偏移形式:寄存器縮放值作為偏移

  • 地址模式:用作偏移
  • 地址模式:前向索引
  • 地址模式:后向索引

基礎(chǔ)樣例代碼

通常,LDR被用來(lái)從內(nèi)存中加載數(shù)據(jù)到寄存器,STR被用作將寄存器的值存放到內(nèi)存中。

image
LDR R2, [R0]   @ [R0] - 數(shù)據(jù)源地址來(lái)自于R0指向的內(nèi)存地址
@ LDR操作:從R0指向的地址中取值放到R2中

STR R2, [R1]   @ [R1] - 目的地址來(lái)自于R1在內(nèi)存中指向的地址
@ STR操作:將R2中的值放到R1指向的地址中

樣例程序的匯編代碼及解釋如下:

.data          /* 數(shù)據(jù)段是在內(nèi)存中動(dòng)態(tài)創(chuàng)建的,所以它的在內(nèi)存中的地址不可預(yù)測(cè)*/
var1: .word 3  /* 內(nèi)存中的第一個(gè)變量 */
var2: .word 4  /* 內(nèi)存中的第二個(gè)變量 */

.text          /* 代碼段開(kāi)始 */ 
.global _start

_start:
    ldr r0, adr_var1  @ 將存放var1值的地址adr_var1加載到寄存器R0中 
    ldr r1, adr_var2  @ 將存放var2值的地址adr_var2加載到寄存器R1中 
    ldr r2, [r0]      @ 將R0所指向地址中存放的0x3加載到寄存器R2中  
    str r2, [r1]      @ 將R2中的值0x3存放到R1做指向的地址 
    bkpt             

adr_var1: .word var1  /* var1的地址助記符 */
adr_var2: .word var2  /* var2的地址助記符 */

在底部我們有我們的文字標(biāo)識(shí)池(在代碼段中用來(lái)存儲(chǔ)常量,字符串,或者偏移等的內(nèi)存,可以通過(guò)位置無(wú)關(guān)的方式引用),分別用adr_var1和adr_var2存儲(chǔ)著變量var1和var2的內(nèi)存地址(var1和var2的值在數(shù)據(jù)段定義)。第一條LDR指令將變量var1的地址加載到寄存器R0。第二條LDR指令同樣將var2的地址加載到寄存器R1。之后我們將存儲(chǔ)在R0指向的內(nèi)存地址中的值加載到R2,最后將R2中的值存儲(chǔ)到R1指向的內(nèi)存地址中。

當(dāng)我們加載數(shù)據(jù)到寄存器時(shí),方括號(hào)“[]”意味著:將其中的值當(dāng)做內(nèi)存地址,并取這個(gè)內(nèi)存地址中的值加載到對(duì)應(yīng)寄存器。

當(dāng)我們存儲(chǔ)數(shù)據(jù)到內(nèi)存時(shí),方括號(hào)“[]”意味著:將其中的值當(dāng)做內(nèi)存地址,并向這個(gè)內(nèi)存地址所指向的位置存入對(duì)應(yīng)的值。

聽(tīng)者好像有些抽象,所以再來(lái)看看這個(gè)動(dòng)畫(huà)吧:

image

同樣的再來(lái)看看的這段代碼在調(diào)試器中的樣子。

gef> disassemble _start
Dump of assembler code for function _start:
 0x00008074 <+0>:      ldr  r0, [pc, #12]   ; 0x8088 <adr_var1>
 0x00008078 <+4>:      ldr  r1, [pc, #12]   ; 0x808c <adr_var2>
 0x0000807c <+8>:      ldr  r2, [r0]
 0x00008080 <+12>:     str  r2, [r1]
 0x00008084 <+16>:     bx   lr
End of assembler dump.

可以看到此時(shí)的反匯編代碼和我們編寫(xiě)的匯編代碼有出入了。前兩個(gè)LDR操作的源寄存器被改成了[pc,#12]。這種操作叫做PC相對(duì)地址。因?yàn)槲覀冊(cè)趨R編代碼中使用的只是數(shù)據(jù)的標(biāo)簽,所以在編譯時(shí)候編譯器幫我們計(jì)算出來(lái)了與我們想訪問(wèn)的文字標(biāo)識(shí)池的相對(duì)便宜,即PC+12。你也可以看匯編代碼中手動(dòng)計(jì)算驗(yàn)證這個(gè)偏移是正確的,以adr_var1為例,執(zhí)行到8074時(shí),其當(dāng)前有效PC與數(shù)據(jù)段還有三個(gè)四字節(jié)的距離,所以要加12。關(guān)于PC相對(duì)取址我們接下來(lái)還會(huì)接著介紹。

PS:如果你對(duì)這里的PC的地址有疑問(wèn),可以看外面第二篇關(guān)于程序執(zhí)行時(shí)PC的值的說(shuō)明,PC是指向當(dāng)前執(zhí)行指令之后第二條指令所在位置的,在32位ARM模式下是當(dāng)前執(zhí)行位置加偏移值8,在Thumb模式下是加偏移值4。這也是與X86架構(gòu)PC的區(qū)別之所在。

image

第一種偏移形式:立即數(shù)作偏移

STR    Ra, [Rb, imm]
LDR    Ra, [Rc, imm]

在這段匯編代碼中,我們使用立即數(shù)作為偏移量。這個(gè)立即數(shù)被用來(lái)與一個(gè)寄存器中存放的地址做加減操作(下面例子中的R1),以訪問(wèn)對(duì)應(yīng)地址偏移處的數(shù)據(jù)。

.data
var1: .word 3
var2: .word 4

.text
.global _start

_start:
    ldr r0, adr_var1  @ 將存放var1值的地址adr_var1加載到寄存器R0中 
    ldr r1, adr_var2  @ 將存放var2值的地址adr_var2加載到寄存器R1中 
    ldr r2, [r0]      @ 將R0所指向地址中存放的0x3加載到寄存器R2中  
    str r2, [r1, #2]  @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加2所指向地址處。
    str r2, [r1, #4]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加4所指向地址處,之后R1寄存器中存儲(chǔ)的值加4,也就是R1=R1+4。
    ldr r3, [r1], #4  @ 取址模式:基于索引后置修改。R3寄存器中的值是從R1寄存器的值所指向的地址中加載的,加載之后R1寄存器中存儲(chǔ)的值加4,也就是R1=R1+4。
    bkpt

adr_var1: .word var1
adr_var2: .word var2

讓我們把上面的這段匯編代碼編譯一下,并用GDB調(diào)試起來(lái)看看真實(shí)情況。

$ as ldr.s -o ldr.o
$ ld ldr.o -o ldr
$ gdb ldr

在GDB(使用GEF插件)中,我們對(duì)_start下一個(gè)斷點(diǎn)并繼續(xù)運(yùn)行程序。

gef> break _start
gef> run
...
gef> nexti 3     /* 向后執(zhí)行三條指令 */

執(zhí)行完上述GDB指令后,在我的系統(tǒng)的寄存器的值現(xiàn)在是這個(gè)樣子(在你的系統(tǒng)里面可能不同):

$r0 : 0x00010098 -> 0x00000003
$r1 : 0x0001009c -> 0x00000004
$r2 : 0x00000003
$r3 : 0x00000000
$r4 : 0x00000000
$r5 : 0x00000000
$r6 : 0x00000000
$r7 : 0x00000000
$r8 : 0x00000000
$r9 : 0x00000000
$r10 : 0x00000000
$r11 : 0x00000000
$r12 : 0x00000000
$sp : 0xbefff7e0 -> 0x00000001
$lr : 0x00000000
$pc : 0x00010080 -> <_start+12> str r2, [r1]
$cpsr : 0x00000010

下面來(lái)分別調(diào)試這三條關(guān)鍵指令。首先執(zhí)行基于地址偏移的取址模式的STR操作了。就會(huì)將R2(0x00000003)中的值存放到R1(0x0001009c)所指向地址偏移2的位置0x1009e。下面一段是執(zhí)行完對(duì)應(yīng)STR操作后對(duì)應(yīng)內(nèi)存位置的值。

gef> nexti
gef> x/w 0x1009e 
0x1009e <var2+2>: 0x3

下一條STR操作使用了基于索引前置修改的取址模式。這種模式的識(shí)別特征是(!)。區(qū)別是在R2中的值被存放到對(duì)應(yīng)地址,R1的值也會(huì)被更新。這意味著,當(dāng)我們將R2中的值0x3存儲(chǔ)到R1(0x1009c)的偏移4之后的地址0x100A0后,R1的值也會(huì)被更新到為這個(gè)地址。下面一段是執(zhí)行完對(duì)應(yīng)STR操作后對(duì)應(yīng)內(nèi)存位置以及寄存器的值。

gef> nexti
gef> x/w 0x100A0
0x100a0: 0x3
gef> info register r1
r1     0x100a0     65696

最后一個(gè)LDR操作使用了基于索引后置的取址模式。這意味著基礎(chǔ)寄存器R1被用作加載的內(nèi)存地址,之后R1的值被更新為R1+4。換句話(huà)說(shuō),加載的是R1所指向的地址而不是R1+4所指向的地址,也就是0x100A0中的值被加載到R3寄存器,然后R1寄存器的值被更新為0x100A0+0x4也就是0x100A4。下面一段是執(zhí)行完對(duì)應(yīng)LDR操作后對(duì)應(yīng)內(nèi)存位置以及寄存器的值。

gef> info register r1
r1      0x100a4   65700
gef> info register r3
r3      0x3       3

下圖是這個(gè)操作發(fā)生的動(dòng)態(tài)示意圖。

image

第二種偏移形式:寄存器作偏移

STR    Ra, [Rb, Rc]
LDR    Ra, [Rb, Rc]

在這個(gè)偏移模式中,寄存器的值被用作偏移。下面的樣例代碼展示了當(dāng)試著訪問(wèn)數(shù)組的時(shí)候是如何計(jì)算索引值的。

.data
var1: .word 3
var2: .word 4

.text
.global _start

_start:
    ldr r0, adr_var1  @ 將存放var1值的地址adr_var1加載到寄存器R0中 
    ldr r1, adr_var2  @ 將存放var2值的地址adr_var2加載到寄存器R1中 
    ldr r2, [r0]      @ 將R0所指向地址中存放的0x3加載到寄存器R2中  
    str r2, [r1, r2]  @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加R2寄存器的值所指向地址處。R1寄存器不會(huì)被修改。 
    str r2, [r1, r2]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加R2寄存器的值所指向地址處,之后R1寄存器中的值被更新,也就是R1=R1+R2。
    ldr r3, [r1], r2  @ 取址模式:基于索引后置修改。R3寄存器中的值是從R1寄存器的值所指向的地址中加載的,加載之后R1寄存器中的值被更新也就是R1=R1+R2。
    bx lr

adr_var1: .word var1
adr_var2: .word var2

下面來(lái)分別調(diào)試這三條關(guān)鍵指令。在執(zhí)行完基于偏移量的取址模式的STR操作后,R2的值被存在了地址0x1009c + 0x3 = 0x1009F處。下面一段是執(zhí)行完對(duì)應(yīng)STR操作后對(duì)應(yīng)內(nèi)存位置的值。

gef> x/w 0x0001009F
 0x1009f <var2+3>: 0x00000003

下一條STR操作使用了基于索引前置修改的取址模式,R1的值被更新為R1+R2的值。下面一段是執(zhí)行完對(duì)應(yīng)STR操作后寄存器的值。

gef> info register r1
 r1     0x1009f      65695

最后一個(gè)LDR操作使用了基于索引后置的取址模式。將R1指向的值加載到R2之后,更新了R1寄存器的值(R1+R2 = 0x1009f + 0x3 = 0x100a2)。下面一段是執(zhí)行完對(duì)應(yīng)LDR操作后對(duì)應(yīng)內(nèi)存位置以及寄存器的值。

gef> info register r1
 r1      0x100a2     65698
gef> info register r3
 r3      0x3       3

下圖是這個(gè)操作發(fā)生的動(dòng)態(tài)示意圖。

image

第三種偏移形式:寄存器縮放值作偏移

LDR    Ra, [Rb, Rc, <shifter>]
STR    Ra, [Rb, Rc, <shifter>]

在這種偏移形式下,第三個(gè)偏移量還有一個(gè)寄存器做支持。Rb是基址寄存器,Rc中的值作為偏移量,或者是要被左移或右移的<shifter>次的值。這意味著移位器shifter被用來(lái)用作縮放Rc寄存器中存放的偏移量。下面的樣例代碼展示了對(duì)一個(gè)數(shù)組的循環(huán)操作。同樣的,我們也會(huì)用GDB調(diào)試這段代碼。

.data
var1: .word 3
var2: .word 4

.text
.global _start

_start:
    ldr r0, adr_var1         @ 將存放var1值的地址adr_var1加載到寄存器R0中 
    ldr r1, adr_var2         @ 將存放var2值的地址adr_var2加載到寄存器R1中 
    ldr r2, [r0]             @ 將R0所指向地址中存放的0x3加載到寄存器R2中  
    str r2, [r1, r2, LSL#2]  @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加(左移兩位后的R2寄存器的值)所指向地址處。R1寄存器不會(huì)被修改。
    str r2, [r1, r2, LSL#2]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加(左移兩位后的R2寄存器的值)所指向地址處,之后R1寄存器中的值被更新,也就R1 = R1 + R2<<2。
    ldr r3, [r1], r2, LSL#2  @ 取址模式:基于索引后置修改。R3寄存器中的值是從R1寄存器的值所指向的地址中加載的,加載之后R1寄存器中的值被更新也就是R1 = R1 + R2<<2。
    bkpt

adr_var1: .word var1
adr_var2: .word var2

下面來(lái)分別調(diào)試這三條關(guān)鍵指令。在執(zhí)行完基于偏移量的取址模式的STR操作后,R2被存儲(chǔ)到的位置是[r1,r2,LSL#2],也就是說(shuō)被存儲(chǔ)到R1+(R2<<2)的位置了,如下圖所示。

image

下一條STR操作使用了基于索引前置修改的取址模式,R1的值被更新為R1+(R2<<2)的值。下面一段是執(zhí)行完對(duì)應(yīng)STR操作后寄存器的值。

gef> info register r1
r1      0x100a8      65704

最后一個(gè)LDR操作使用了基于索引后置的取址模式。將R1指向的值加載到R2之后,更新了R1寄存器的值(R1+R2 = 0x100a8 + (0x3<<2) = 0x100b4)。下面一段是執(zhí)行完對(duì)應(yīng)LDR操作后寄存器的值。

gef> info register r1
r1      0x100b4      65716

小結(jié)

LDR/STR的三種偏移模式:

  1. 立即數(shù)作為偏移
ldr   r3, [r1, #4]
  1. 寄存器作為偏移
ldr   r3, [r1, r2]
  1. 寄存器縮放值作為偏移
ldr   r3, [r1, r2, LSL#2]

如何區(qū)分取址模式:

  1. 如果有一個(gè)嘆號(hào)!,那就是索引前置取址模式,即使用計(jì)算后的地址,之后更新基址寄存器。
ldr   r3, [r1, #4]!
ldr   r3, [r1, r2]!
ldr   r3, [r1, r2, LSL#2]!
  1. 如果在[]外有一個(gè)寄存器,那就是索引后置取址模式,即使用原有基址寄存器重的地址,之后再更新基址寄存器
ldr   r3, [r1], #4
ldr   r3, [r1], r2
ldr   r3, [r1], r2, LSL#2
  1. 除此之外,就都是偏移取址模式了
ldr   r3, [r1, #4]
ldr   r3, [r1, r2]
ldr   r3, [r1, r2, LSL#2]
  • 地址模式:用作偏移
  • 地址模式:前向索引
  • 地址模式:后向索引

關(guān)于PC相對(duì)取址的LDR指令

有時(shí)候LDR并不僅僅被用來(lái)從內(nèi)存中加載數(shù)據(jù)。還有如下這操作:

.section .text
.global _start

_start:
   ldr r0, =jump        /* 加載jump標(biāo)簽所在的內(nèi)存位置到R0 */
   ldr r1, =0x68DB00AD  /* 加載立即數(shù)0x68DB00AD到R1 */
jump:
   ldr r2, =511         /* 加載立即數(shù)511到R2 */ 
   bkpt

這些指令學(xué)術(shù)上被稱(chēng)作偽指令。但我們?cè)诰帉?xiě)ARM匯編時(shí)可以用這種格式的指令去引用我們文字標(biāo)識(shí)池中的數(shù)據(jù)。在上面的例子中我們用一條指令將一個(gè)32位的常量值放到了一個(gè)寄存器中。為什么我們會(huì)這么寫(xiě)是因?yàn)锳RM每次僅僅能加載8位的值,原因傾聽(tīng)我解釋立即數(shù)在ARM架構(gòu)下的處理。

在ARM中使用立即數(shù)的規(guī)律

是的,在ARM中不能像X86那樣直接將立即數(shù)加載到寄存器中。因?yàn)槟闶褂玫牧⒓磾?shù)是受限的。這些限制聽(tīng)上去有些無(wú)聊。但是聽(tīng)我說(shuō),這也是為了告訴你繞過(guò)這些限制的技巧(通過(guò)LDR)。

我們都知道每條ARM指令的寬度是32位,所有的指令都是可以條件執(zhí)行的。我們有16中條件可以使用而且每個(gè)條件在機(jī)器碼中的占位都是4位。之后我們需要2位來(lái)做為目的寄存器。2位作為第一操作寄存器,1位用作設(shè)置狀態(tài)的標(biāo)記位,再加上比如操作碼(opcode)這些的占位。最后每條指令留給我們存放立即數(shù)的空間只有12位寬。也就是4096個(gè)不同的值。

這也就意味著ARM在使用MOV指令時(shí)所能操作的立即數(shù)值范圍是有限的。那如果很大的話(huà),只能拆分成多個(gè)部分外加移位操作拼接了。

所以這剩下的12位可以再次劃分,8位用作加載0-255中的任意值,4位用作對(duì)這個(gè)值做0~30位的循環(huán)右移。這也就意味著這個(gè)立即數(shù)可以通過(guò)這個(gè)公式得到:v = n ror 2*r。換句話(huà)說(shuō),有效的立即數(shù)都可以通過(guò)循環(huán)右移來(lái)得到。這里有一個(gè)例子

有效值:
#256        // 1 循環(huán)右移 24位 --> 256
#384        // 6 循環(huán)右移 26位 --> 384
#484        // 121 循環(huán)右移 30位 --> 484
#16384      // 1 循環(huán)右移 18位 --> 16384
#2030043136 // 121 循環(huán)右移 8位 --> 2030043136
#0x06000000 // 6 循環(huán)右移 8位 --> 100663296 (十六進(jìn)制值0x06000000)

Invalid values:
#370        // 185 循環(huán)右移 31位 --> 31不在范圍內(nèi) (0 – 30)
#511        // 1 1111 1111 --> 比特模型不符合
#0x06010000 // 1 1000 0001.. --> 比特模型不符合

看上去這樣并不能一次性加載所有的32位值。不過(guò)我們可以通過(guò)以下的兩個(gè)選項(xiàng)來(lái)解決這個(gè)問(wèn)題:

  1. 用小部分去組成更大的值。
    1. 比如對(duì)于指令 MOV r0, #511
    2. 將511分成兩部分:MOV r0, #256, and ADD r0, #255
  2. 用加載指令構(gòu)造‘ldr r1,=value’的形式,編譯器會(huì)幫你轉(zhuǎn)換成MOV的形式,如果失敗的話(huà)就轉(zhuǎn)換成從數(shù)據(jù)段中通過(guò)PC相對(duì)偏移加載。
    1. LDR r1, =511

如果你嘗試加載一個(gè)非法的值,編譯器會(huì)報(bào)錯(cuò)并且告訴你:Error: invalid constant。如果在遇到這個(gè)問(wèn)題,你現(xiàn)在應(yīng)該知道該怎么解決了吧。唉還是舉個(gè)栗子,就比如你想把511加載到R0。

.section .text
.global _start

_start:
    mov     r0, #511
    bkpt

這樣做的結(jié)果就是編譯報(bào)錯(cuò):

azeria@labs:~$ as test.s -o test.o
test.s: Assembler messages:
test.s:5: Error: invalid constant (1ff) after fixup

你需要將511分成多部分,或者直接用LDR指令。

.section .text
.global _start

_start:
 mov r0, #256   /* 1 ror 24 = 256, so it's valid */
 add r0, #255   /* 255 ror 0 = 255, valid. r0 = 256 + 255 = 511 */
 ldr r1, =511   /* load 511 from the literal pool using LDR */
 bkpt

如果你想知道你能用的立即數(shù)的有效值,你不需要自己計(jì)算。我這有個(gè)小腳本,看你骨骼驚奇,傳給你呦 rotator.py。用法如下。

azeria@labs:~$ python rotator.py
Enter the value you want to check: 511

Sorry, 511 cannot be used as an immediate number and has to be split.

azeria@labs:~$ python rotator.py
Enter the value you want to check: 256

The number 256 can be used as a valid immediate number.
1 ror 24 --> 256

譯者注:這作者真的是用心良苦,我都看累了,但是怎么說(shuō),反復(fù)練習(xí)加實(shí)踐,總歸是有好處的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容