原文鏈接 https://azeria-labs.com/memory-instructions-load-and-store-part-4/
ARM使用加載-存儲(chǔ)模式控制對(duì)內(nèi)存的訪(fǎng)問(wèn)西傀,這意味著只有加載/存儲(chǔ)(LDR或者STR)才能訪(fǎng)問(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)存地址的便宜訪(fǎng)問(wèn)形式彻亲。每個(gè)例子除了STR/LDR的偏移模式不同外孕锄,其余的都一樣吮廉。而且這個(gè)例子很簡(jiǎn)單,最佳的實(shí)踐方式是用GDB去調(diào)試這段匯編代碼畸肆。
第一種偏移形式:立即數(shù)作為偏移
- 地址模式:用作偏移
- 地址模式:前向索引
- 地址模式:后向索引
第二種偏移形式:寄存器作為偏移
- 地址模式:用作偏移
- 地址模式:前向索引
- 地址模式:后向索引
第三種偏移形式:寄存器縮放值作為偏移
- 地址模式:用作偏移
- 地址模式:前向索引
- 地址模式:后向索引
基礎(chǔ)樣例代碼
通常宦芦,LDR被用來(lái)從內(nèi)存中加載數(shù)據(jù)到寄存器,STR被用作將寄存器的值存放到內(nèi)存中恼除。
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à)吧:
同樣的再來(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)了與我們想訪(fǎng)問(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ū)別之所在淳玩。
第一種偏移形式:立即數(shù)作偏移
STR Ra, [Rb, imm]
LDR Ra, [Rc, imm]
在這段匯編代碼中直撤,我們使用立即數(shù)作為偏移量。這個(gè)立即數(shù)被用來(lái)與一個(gè)寄存器中存放的地址做加減操作(下面例子中的R1)蜕着,以訪(fǎng)問(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)示意圖庇配。
第二種偏移形式:寄存器作偏移
STR Ra, [Rb, Rc]
LDR Ra, [Rb, Rc]
在這個(gè)偏移模式中,寄存器的值被用作偏移绍些。下面的樣例代碼展示了當(dāng)試著訪(fǎ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)示意圖。
第三種偏移形式:寄存器縮放值作偏移
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)的位置了,如下圖所示十厢。
下一條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的三種偏移模式:
- 立即數(shù)作為偏移
ldr r3, [r1, #4]
- 寄存器作為偏移
ldr r3, [r1, r2]
- 寄存器縮放值作為偏移
ldr r3, [r1, r2, LSL#2]
如何區(qū)分取址模式:
- 如果有一個(gè)嘆號(hào)!,那就是索引前置取址模式娩嚼,即使用計(jì)算后的地址蘑险,之后更新基址寄存器。
ldr r3, [r1, #4]!
ldr r3, [r1, r2]!
ldr r3, [r1, r2, LSL#2]!
- 如果在[]外有一個(gè)寄存器岳悟,那就是索引后置取址模式佃迄,即使用原有基址寄存器重的地址,之后再更新基址寄存器
ldr r3, [r1], #4
ldr r3, [r1], r2
ldr r3, [r1], r2, LSL#2
- 除此之外贵少,就都是偏移取址模式了
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ù)在A(yíng)RM架構(gòu)下的處理宽气。
在A(yíng)RM中使用立即數(shù)的規(guī)律
是的随常,在A(yíng)RM中不能像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)題:
- 用小部分去組成更大的值玷禽。
- 比如對(duì)于指令 MOV r0, #511
- 將511分成兩部分:MOV r0, #256, and ADD r0, #255
- 用加載指令構(gòu)造‘ldr r1,=value’的形式魁袜,編譯器會(huì)幫你轉(zhuǎn)換成MOV的形式绑榴,如果失敗的話(huà)就轉(zhuǎn)換成從數(shù)據(jù)段中通過(guò)PC相對(duì)偏移加載怒医。
- 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í)踐抹锄,總歸是有好處的。