|
LDR 指令用于从内存种将一个 32 位的字读取到指令中的目标寄存器。一般情况下,这样的读取是字对齐的。比如,在一个应用程序中如下定义:
int a[4] = {0x00000001, 0x55226677, 0x68491247, 0x00000048} ;
int *p = a; 上面, p 是个整型指针,用来读取数组中的元素。假如数组 a 的首地址是 0xbffffe94 ,那么用 a 初始化 p 后,p 所代表的地址也是 0xbffffe94 。所以,当 p += 1; 后,p 的变为为 0xbffffe98;再 p+= 1 后,p 变为 0xbffffe9c ;以此类推。这样的地址能被 4 整除,所以是字对齐的。
在《ARM体系结构编程》一书中有说到:如果指令寻址方式确定的地址不是字对齐的,则从内存中读出的数值要进行循环右移操作,移位的位数为寻址方式确定的地址的 bits[1:0] 的 8 倍。这样对于 little-endian (小端模式),指令要读取的字节数句放在目标寄存器的低 8 位。
理解这句话,首先要弄明白“从内存中读出的数值要进行循环右移操作” 这话里的“从内存种读出到了什么样的数据” !
现在假设上面 p 指针是指向上面数组中的第二个元素 0x55226677 (其地址为 0xbffffe98),假如我强制令 p 所代表的地址变为 0xbffffe99 这里,那我读这个地址,那我到底读到的是哪 4 个连续地址处的数据?是 0xbffffe99 ~ 0xbffffe9c ?还是 0xbffffe98 ~ 0xbffffe9b?抑或是是别的数据。
当时我也一度不明白,在网上也没搜到具体的资料。那只有自己动手做一下实验。写一个测试用的小应用程序 test.c:
#include <stdio.h>
main()
{
int a[4] = {0x00000001, 0x55226677, 0x68491247, 0x00000048} ;
int *p = a;
int i;
p = p + 1;
i = 0xaa; /*方便观察所设语句*/
(char *)p = (char *)p + 1;
i = *p;
return (0);
} 上面的程序在实际中不会输出任何数据,没实际意义,但是对于弄明白上面的问题却有帮助。对程序编译成可执行文件后,上传到开发板里,然后用 gdbserver 和 主机的 arm-gdb 进行调试。关于如何建立 gdbserver 调试环境见:http://www.groad.net/bbs/read.php?tid-1365.html
在主机一端,启动 gdb ,然后给程序下个断点,直接运行到断点处,然后用 disas 命令反汇编整段程序(如果不运行进程序里,无法使用 disas 命令),输出结果如下:Breakpoint 1, main () at test.c:11
11 i = *p;
(gdb) disas
Dump of assembler code for function main:
0x8380 <main>: mov r12, sp
0x8384 <main+4>: stmdb sp!, {r11, r12, lr, pc}
0x8388 <main+8>: sub r11, r12, #4 ; 0x4
0x838c <main+12>: sub sp, sp, #24 ; 0x18
0x8390 <main+16>: ldr r3, [pc, #72] ; 0x83e0 <main+96>
0x8394 <main+20>: sub r12, r11, #28 ; 0x1c
0x8398 <main+24>: ldmia r3, {r0, r1, r2, r3}
0x839c <main+28>: stmia r12, {r0, r1, r2, r3}
0x83a0 <main+32>: sub r3, r11, #28 ; 0x1c
0x83a4 <main+36>: str r3, [r11, -#32]
0x83a8 <main+40>: ldr r3, [r11, -#32]
0x83ac <main+44>: add r3, r3, #4 ; 0x4
0x83b0 <main+48>: str r3, [r11, -#32]
0x83b4 <main+52>: mov r3, #170 ; 0xaa /*方便观察所设语句*/
0x83b8 <main+56>: str r3, [r11, -#36]
0x83bc <main+60>: ldr r3, [r11, -#32]
0x83c0 <main+64>: add r3, r3, #1 ; 0x1
0x83c4 <main+68>: str r3, [r11, -#32]
0x83c8 <main+72>: ldr r3, [r11, -#32]
0x83cc <main+76>: ldr r3, [r3]
0x83d0 <main+80>: str r3, [r11, -#36]
0x83d4 <main+84>: mov r3, #0 ; 0x0
---Type <return> to continue, or q <return> to quit---
0x83d8 <main+88>: mov r0, r3
0x83dc <main+92>: ldmdb r11, {r11, sp, pc}
0x83e0 <main+96>: andeq r8, r0, r4, lsr r4
End of assembler dump. 在 C 程序中,当程序运行到 p = p + 1; 这里时,可以观察到 p 所代表的地址为 0xbffffe98
(char *)p = (char *)p + 1; 语句是强制让 p 指向 0xbffffe99 这个地址,目的是故意造成 LDR 的寻址不对齐。
通过观察内存:(gdb) x/3wx 0xbffffe98
0xbffffe98: 0x55226677 0x68491247 0x00000048
(gdb) x/3wx 0xbffffe99
0xbffffe99: 0x47552266 0x48684912 0xc8000000 可以看出,目标板所采用的内存是 little-end 小端格式(低字节数据放在低地址,高字节数据放在高地址)。现在,单步执行 i = *p; 这条语句。i = *p; 这语句所对应的汇编是:0x83c8 <main+72>: ldr r3, [r11, -#32]
0x83cc <main+76>: ldr r3, [r3]
0x83d0 <main+80>: str r3, [r11, -#36] 上面,r11, -#32 对应的地址就是 p,[r11, -#32] 就是 *p ;而 [r11, -#36] 对应的变量自然就是 i 。
现在通过命令查看一下 r3 中的内容,也就是 *p 的内容:(gdb) info registers r3
r3 0x77552266 2002068070 由此可见,0x77552266 正是经过 0x55226677 循环右移 8 位得来!也就是说,原本设想的读取 0xbffffe99 会连续 0xbffffe99-0xbffffe9a-0xbffffe9b-0xbffffe9c 这 4 个内存单元内容是错误的。实际上仍然读取的是 0xbffffe98 ~ 0xbffffe9b 这 4 个连续内存单元的内容。由于你读取的地址不对齐,编译器会认为你的兴趣就是要读取那个不对齐的内存单元( 0xbffffe99 对应上的 0x66 ) 的内容,所以循环右移,将其送往低 8 位。
如果说,在 C 程序中,将 i = *p; 改为 (char)i = *((char *)p); ,那么汇编中 ldr r3, [r3] 会变成 ldrb r3, [r3] ,这样表示就只是读一个字节。
对于 big-end 大端内存存储格式同理分析。
至此,分析完毕。 |
|