vvizz 发表于 2018-9-15 17:24:01

【翻译】ARM汇编基础教程:4.内存指令:加载和存储

本帖最后由 vvizz 于 2018-9-15 17:27 编辑

vvizz 前言:翻译真不是啥好活。。。哈哈,累的腰疼,还要每句话都要考虑,别人能不能看得懂啊?哈哈,希望自己的工作对别人有那么一丢丢帮助,哪怕只是一个人 {:lol:}

原文链接:https://azeria-labs.com/memory-instructions-load-and-store-part-4/
翻译:vvizz
转载请注明出处:飘云阁https://www.chinapyg.com/thread-120888-1-1.html


      ARM平台使用“加载-存储”模式进行内存访问,也就意味着只有加载/存储指令(LDR和STR)可以访问内存。在X86平台大多数指令都允许直接操作内存数据,但是在ARM平台,必须将内存数据存放到寄存器才可以操作。也就是说,将一个特定内存地址的32位数值进行值增加操作,必须经过三种类型的指令(加载,增加,存储):首先将特定内存地址的数值加载到寄存器中,然后对寄存器中数值进行增加操作,最后将寄存器中的数值存放回内存中。
      在介绍ARM平台的加载和存储操作之前,我们通过一个基本示例开始教程,然后介绍三种不同寻址模式下的偏移形式(vvizz: 原文 offset form)。每一个例子,简单起见,我们都使用相同的汇编代码块,只不过是LDR/STR的偏移形式不同。学习本部分教程最好的方法就是在你的实验环境(搭建环境教程:https://azeria-labs.com/emulate-raspberry-pi-with-qemu/) 中通过GDB调试示例代码。
      1. 偏移形式:立即数作为偏移
      2. 偏移形式:寄存器作为偏移
      3. 偏移形式:比例寄存器(vvizz:原文 scaled register)作为偏移
      三种偏移形式均有如下寻址模式:Offset、Pre-indexed、Post-indexed。(vvizz:三种寻址不知道怎么称呼,第二和第三种区别是寻址时是否改变寻址地址的值,后边会有详细示例)


基本示例
      通常,LDR是从内存中加载数据到寄存器中,STR是将寄存器的值存储到内存地址中。


      LDR R2,    @ - R0中存放来源数值的地址
      STR R2,    @ - R1中存放目标地址


      LDR操作:R0中的数据作为地址,加载该内存地址处的数值到目标寄存器R2中。
      STR操作:R2中的数据存储到R1寄存器值为地址的内存中。


               


      下面就是函数化的汇编程序:


      .data          /* .data段是动态创建的,地址不容易预测 */
      var1: .word 3/* 内存变量1 */
      var2: .word 4/* 内存变量2 */

      .text          /* 代码段的起始位置 */
      .global _start

      _start:
                ldr r0, adr_var1@ 通过标签 adr_var1 将 var1的地址 加载到寄存器R0中
                ldr r1, adr_var2@ 通过标签 adr_var2 将 var2的地址 加载到寄存器R1中
                ldr r2,       @ 寄存器R0中存放的内存地址处的值(0x03),加载到R2寄存器中
                str r2,       @ 寄存器R2存放的值(0x03),存储到寄存器R1存放的内存地址处
                bkpt            

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


      在代码的底部,有一个文字池(在同一个代码段中的一个内存区域,存放可以以位置无关方式引用的常量、字符串或者偏移),我们用标签adr_var1 和 adr_var2 存储了 var1 和 var2(在数据段的顶部定义) 的地址。第一个LDR,将var1的地址加载到寄存器R0中。第二个LDR,同样将var2的地址加载到寄存器R1中。然后我们将R0中存放的内存地址处的值,加载到R2中,然后将R2的值存储到R1的数据对应的内存地址处。
      当我们往寄存器中加载数据时,括号([])含义:两个中括号中间的寄存器的值,是一个内存地址,是我们加载指令的来源地址。
      当我们往内存地址存储数据时,括号([])含义:两个中括号中间的寄存器的值,是一个内存地址,是我们存储指令的目标地址。
      这听起来好像很复杂,其实本身很简单,所以这里有一个可视化的展示,演示了上边内存和寄存器之间的交互的调试过程。



               


      来吧,让我们看看这些代码在调试器里啥样子来。
      gef> disassemble _start
      Dump of assembler code for function _start:
         0x00008074 <+0>:      ldrr0,    ; 0x8088 <adr_var1>
         0x00008078 <+4>:      ldrr1,    ; 0x808c <adr_var2>
         0x0000807c <+8>:      ldrr2,
         0x00008080 <+12>:   strr2,
         0x00008084 <+16>:   bx   lr
      End of assembler dump.

      我们定义的两个标签在LDR操作中都被改成了,这叫做PC相对寻址。因为我们使用了标签,所以编译器计算出了这些值在文字池的位置(PC+12)。你也可以用精确的方法计算出这些值的位置,当然也可以像上边那样直接使用标签,唯一的不同就是你需要精确计算这些值在文字池中的位置来代替标签。在这个例子中,取值位置距离有效的PC位置隔着3行(vvizz:原文hops),更多关于PC相对寻址的知识将在后续章节介绍。
      备注:也许你已经忘记为什么有效PC的位置比当前指令领先两个指令(vvizz: 就是在当前指令的下边第二条的意思),原因已经在第二章解释过了 [... 执行过程中,在ARM环境下,PC存储当前指令加8的地址(两个ARM指令),在Thumb环境下,存储的是当前指令加4(两个Thumb指令)。这与X86平台下,PC总是指向下一条即将执行指令的情况是不同的 ...]。
            

1. 偏移形式:立即数作为偏移
      STR    Ra,
      LDR    Ra,
      这里我们用一个立即数(整型)作为偏移。在编译的时候,这个数被用来与基寄存器(在下面例子中R1就是基寄存器)进行加或减运算,得到的结果作为被访问数据的偏移。
      .data
      var1: .word 3
      var2: .word 4

      .text
      .global _start

      _start:
                ldr r0, adr_var1@ 通过标签adr_var1获取var1的地址,存放到R0中
                ldr r1, adr_var2@ 通过标签adr_var2获取var2的地址,存放到R1中
                ldr r2,       @ 将R0对应地址处的数据(0x03)加载到寄存器R2中
                str r2, @ 寻址模式:offset. 将R2中的数值(0x03)存储到R1+2的内存地址处。基寄存器(R1)不修改。
                str r2, ! @ 寻址模式:pre-indexed. 将R2中的数值(0x03)存储到R1+4的内存地址处。基寄存器(R1)修改:R1 = R1 + 4
                ldr r3, , #4@ 寻址模式:post-indexed. 将R1中存放的内存地址处的值加载到寄存器R3中。基寄存器(R1)修改:R1 = R1 + 4
                bkpt

      adr_var1: .word var1
      adr_var2: .word var2

      将如上程序存储为文件ldr.s,编译并且用GDB进行调试运行,观察结果。
      $ as ldr.s -o ldr.o
      $ ld ldr.o -o ldr
      $ gdb ldr

      在GDB(已配置好gef功能)中_start处设置断点,然后运行程序。
      gef> break _start
      gef> run
      ...
      gef> nexti 3   /* to run the next 3 instructions */

      在我的系统上,寄存器的值被如下的值填充了。(友情提示,你的系统上可能地址与我列出的不同)
      $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,
      $cpsr : 0x00000010

      接下来,一条offset寻址模式的STR指令将被执行,将R2寄存器的值(0x00000003)存储到R1(0x0001009c)+偏移(#2) = 0x1009e的内存地址处。
      gef> nexti
      gef> x/w 0x1009e
      0x1009e <var2+2>: 0x3

      下一条STR操作使用的是pre-indexed寻址模式。你可以通过一个感叹号(!)来识别这个寻址模式。唯一的不同就是基寄存器将会在R2的值被存储完成之后更新。意思就是,将R2的值(0x3)存储到R1(0x1009C)+偏移(#4) = 0x100A0 内存地址处,然后将这个精确地址更新到R1中。(vvizz:R1 = R1 + 4)
      gef> nexti
      gef> x/w 0x100A0
      0x100a0: 0x3
      gef> info register r1
      r1   0x100a0   65696

      最后一个LDR操作使用post-indexed寻址模式。过程是,基寄存器(R1)被用做最后的地址,然后将R1更新为R1+4.也就是说,获取R1的值(而不是R1+4),这个值是0x100A0,然后加载到R3中,然后更新R1的值为:R1(0x100A0)+偏移(#4) = 0x100A4.
      gef> info register r1
      r1      0x100a4   65700
      gef> info register r3
      r3      0x3       3

      下面抽象说明了这三个指令的执行过程:
            


2. 偏移形式:寄存器作为偏移
      STR    Ra,
      LDR    Ra,

      这种偏移形式是把一个寄存器作为偏移的值。这里有一个这种偏移形式的示例,功能是访问数组的下标在运行时计算确定。
      .data
      var1: .word 3
      var2: .word 4

      .text
      .global _start

      _start:
                ldr r0, adr_var1@ 通过标签adr_var1获取var1的地址到R0
                ldr r1, adr_var2@ 通过标签adr_var2获取var2的地址到R1
                ldr r2,       @ 加载R0对应内存地址处的值(0x03)给R2
                str r2, @ 寻址模式:offset。R2的值(0x03)存储到R1结合偏移为R2(0x03)的内存地址处,基寄存器不变
                str r2, ! @ 寻址模式:pre-indexed。R2的值(0x03)存储到R1结合偏移为R2(0x03)的内存地址处,基寄存器修改:R1 = R1 + R2
                ldr r3, , r2@ 寻址模式:post-indexed。R1的内存地址处的值加载到R3中,然后修改基寄存器:R1 = R1 + R2
                bx lr

      adr_var1: .word var1
      adr_var2: .word var2

      执行完第一个offset寻址模式的STR操作之后,R2(0x00000003)被存储到如下地址处:0x0001009c + 0x00000003 = 0x0001009F.
      gef> x/w 0x0001009F
         0x1009f <var2+3>: 0x00000003

      第二个STR操作是pre-indexed寻址模式,基本操作相同,不同点是这条指令会修改基寄存器(R1)的值为R1+R2的结果。
      gef> info register r1
         r1   0x1009f      65695

      最后一个LDR操作是post-indexed寻址模式,将R1内存地址的值加载到R2寄存器中,然后更新基寄存器R1(R1+R2 = 0x1009f + 0x3 = 0x100a2)。
      gef> info register r1
         r1      0x100a2   65698
      gef> info register r3
         r3      0x3       3
            


3. 偏移形式:比例寄存器作为偏移
      LDR    Ra,
      STR    Ra,

      第三种偏移形式是比例寄存器作为偏移。这种形式下,Rb是基寄存器,Rc是立即数偏移(或者一个包含立即数的寄存器),可以通过左/右移位(<shifter>)来对立即数进行按比例修改,也就是说通过移位来按比例修改偏移。如下是这种偏移形式的一个示例,对一个数组进行迭代循环操作,你可以用GDB对这个简单的示例进行调试运行:
      .data
      var1: .word 3
      var2: .word 4

      .text
      .global _start

      _start:
                ldr r0, adr_var1         @ 通过标签adr_var1获取var1的地址到R0
                ldr r1, adr_var2         @ 通过标签adr_var2获取var2的地址到R1
                ldr r2,              @ 加载R0对应内存地址处的值(0x03)给R2
                str r2, @ 寻址模式:offset. R2的值(0x03)存储到R1结合 R2的值左移两位 作为偏移的内存地址处,基寄存器(R1)不变。
                str r2, ! @ 寻址模式:pre-indexed. R2的值(0x03)存储到R1结合 R2的值左移两位 作为偏移的内存地址处,基寄存器(R1)修改:R1 = R1 + R2<<2
                ldr r3, , r2, LSL#2@ 寻址模式:post-indexed. R1对应的内存地址的值加载到R3寄存器中,修改基寄存器:R1 = R1 + R2<<2
                bkpt

      adr_var1: .word var1
      adr_var2: .word var2

      第一个STR操作使用offset寻址模式,将R2的值存储到经过计算之后的内存地址处,就是获取R1的值作为基地址(这个例子中,R1包含var2的内存地址),然后获取R2的值(0x03)之后左右2位作为偏移。下面的图将尽力为你比较形象的演示的计算过程和内存变化过程。
            

      第二个STR操作使用pre-indexed寻址模式。执行的动作与前一个STR操作相同,不同点是它将会在指令执行之后修改基寄存器R1的值。也就是说,这条指令将会首先将R2寄存器的值存储到R1(0x1009C)+ 偏移左移两位(0x03 LSL#2 = 0xC) = 0x100a8 内存地址处,然后更新R1的值为0x100a8.
      gef> info register r1
      r1      0x100a8      65704

      最后一个LDR操作使用post-indexed寻址模式,就是将R1(0x100a8)内存地址的值加载到R3寄存器中,然后利用R2和LSL#2来更新基寄存器R1的值,也就是说,R1更新过程: R1 (0x100a8) + 偏移R2 (0x3) 左移两位(0xC) = 0x100b4.
      gef> info register r1
      r1      0x100b4      65716

总结
牢记 LDR/STR 的三种偏移模式:

      1. 立即数作为偏移
                ldr   r3,
      2. 寄存器作为偏移
                ldr   r3,
      3. 比例寄存器作为偏移
                ldr   r3,

如何记住LDR/STR中不同寻址模式的不同:

      如果存在符号 !, 就是 prefix address mode      (vvizz: 就是之前的pre-indexed寻址模式, 称作前缀寻址模式?O~~ 不敢随便命名,大家还是记住英文吧)
                ldr   r3, !
                ldr   r3, !
                ldr   r3, !
      如果基寄存器在一个只有他自己的中括号中, 那就是postfix address mode(vvizz:post-indexed寻址模式)
                ldr   r3, , #4
                ldr   r3, , r2
                ldr   r3, , r2, LSL#2
      其他情况都是 offset address mode.(vvizz:offset寻址模式)
                ldr   r3,
                ldr   r3,
                ldr   r3,


PC相对寻址中的LDR
      LDR指令不仅仅用于从内存中加载数据到寄存器,有时我们会看到如下语法:
      .section .text
      .global _start

      _start:
         ldr r0, =jump      /* 将函数标签 jump 的地址加载到R0中 */
         ldr r1, =0x68DB00AD/* 将地址值 0x68DB00AD 加载到寄存器R1中 */
      jump:
         ldr r2, =511         /* 将数值511加载到寄存器R2中 */
         bkpt

      这些指令被称为伪指令。我们可以使用这个语法从文字池中引用数据。文字池是同一区段的一个内存空间(因为文字池是代码的一部分),存储着常量、字符串或者偏移。在上例中,我们可以使用伪指令在一条指令中完成:引用一个函数的偏移,然后将一个32位常量存放到寄存器中。这就是为什么我们有时候需要用这种语法将32位常量存放到寄存器中,因为ARM每次只允许加载8位的值。啥玩意?为了搞懂为啥,你必须知道立即数在ARM平台是如何处理的。

ARM平台的立即数使用
      在ARM平台加载一个立即数到寄存器中,并不像X86平台一样直接。(vvizz:骚年们,感受X86的幸福吧,哈哈)在使用立即数时有很多限制,这些限制是什么、如何解决,并不是ARM汇编最令人兴奋的部分,但是别走,听我说,这是你理解并且可以绕过这些限制的诀窍。(疯狂暗示:LDR)
      -- 这里需要确认!!!!
      我们知道,每一个ARM指令都是32位长度,并且所有的指令都是有条件的,有16个条件码我们可以使用,每个条件码占指令的4位。我们需要2位用于目标寄存器,2位用于第一个操作寄存器,1位用于设置状态标志,加上其他用途的位数,例如实际操作码。关键点是,在将指令位数分配给指令类型、寄存器和其他字段之后,只留下12位用于立即数使用,也就是只能允许4096个不同的值。(vvizz:)
      这就意味着,ARM指令的MOVE直接操作的只能是一个限制区间内的立即数,如果一个数字不能被直接使用,就一定会被分割成多个部分,然后通过多个小的数字进行组合。
      还有,这12位不是一个独立的整数,而是被分成能够加载0-255范围内的任何8位值的8位数(n)和4位旋转字段(r),这4位用于在步骤2中进行0到30范围内的右转。也就是说一个完整的立即数v可以用公式获得:v = n ror 2*r.换句话说,有效的立即数都是可旋转的字节(这个值可以通过旋转一个偶数次数来变成一个字节)(vvizz:就是一个字节旋转偶数位可以得到有效立即数,一个有效立即数也可以旋转偶数位变成一个字节)

;-----------------------------------------------------------------------------------------------
vvizz: 不好意思,中间插一下子,这里我刚开始犯晕,后来看了这个图就明白了,如下图
;-----------------------------------------------------------------------------------------------

      这里有一些示例来演示什么是有效立即数:
      有效值:
      #256      // 1 ror 24 --> 256
      #384      // 6 ror 26 --> 384
      #484      // 121 ror 30 --> 484
      #16384      // 1 ror 18 --> 16384
      #2030043136 // 121 ror 8 --> 2030043136
      #0x06000000 // 6 ror 8 --> 100663296 (0x06000000 in hex)

      无效值:
      #370      // 185 ror 31 --> 31 不在范围 (0 – 30) 内
      #511      // 1 1111 1111 --> 字节模式不能匹配一个字节(vvizz:就是无论怎么旋转都不能压缩成一个字节)
      #0x06010000 // 1 1000 0001.. --> 字节模式不能匹配一个字节

      这里就可以得出一条指令不能加载一个完整32位地址的结论。我们可以通过使用如下的两种方式绕过这个限制:
      1. 利用多个合法的小部分构造大的数值
                1. 不使用 MOV R0, #511
                2. 将522分割成两部分:MOV r0, #256, and ADD r0, #255
      2. 使用 ‘ldr r1,=value’ 的加载结构,使用 MOV, or a PC-relative 轻松解决
                1. LDR r1, =511

      如果你试图加载一个无效的立即数,汇编器将会怼你并且报个错: Error: invalid constant.(vvizz:错误,无效的常量)如果你遇到了这个错误,你现在就知道啥意思了,也知道咋解决了。
      让咱们看看怎么把#511加载到R0中:
      .section .text
      .global _start

      _start:
                mov   r0, #511
                bkpt

      如果你尝试汇编这个代码(vvizz:这里写的是汇编代码,所以不能叫编译),汇编器将会抛出一个错误:
      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   /* 使用LDR 从文字池中加载511 */
         bkpt

         如果你需要计算判断一个数字是否是一个合法的立即数,不需要自己计算。你可以使用我编写的一个叫做 rotator.py(https://raw.githubusercontent.com/azeria-labs/rotator/master/rotator.py) 的python小脚本来完成,这个脚本把你需要判断的数字作为输入,然后判断这个数能不能作为有效立即数使用。
      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



vvizz:下面是 rotator.py 脚本的代码
from __future__ import print_function   # PEP 3105
import sys

# Rotate right: 0b1001 --> 0b1100
ror = lambda val, r_bits, max_bits: \
    ((val & (2**max_bits-1)) >> r_bits%max_bits) | \
    (val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))

max_bits = 32

input = int(raw_input("Enter the value you want to check: "))

print()
for n in xrange(1, 256):

    for i in xrange(0, 31, 2):

      rotated = ror(n, i, max_bits)

      if(rotated == input):
            print("The number %i can be used as a valid immediate number." % input)
            print("%i ror %x --> %s" % (n, int(str(i), 16), rotated))
            print()
            sys.exit()
else:
    print("Sorry, %i cannot be used as an immediate number and has to be split." % input)






;================================================


vvizz:文中如有错误,敬请勘正,谢谢!如有问题,留言交流~~







43060214 发表于 2018-10-7 11:09:01

谢谢分享,群主辛苦了

chunwei_2015 发表于 2019-2-28 16:54:01

感谢楼主分享!学习啦!

wsptr 发表于 2019-6-8 10:39:53

感谢楼主分享!学习啦!
页: [1]
查看完整版本: 【翻译】ARM汇编基础教程:4.内存指令:加载和存储