存储到内存和从内存加载是如何工作的;存储 32 位字时哪些地址会受到影响?

how does storing into and loading from memory work; which addresses are affected when you store a 32-bit word?

我正在从事一个二进制分析项目,我正在构建一个将程序集转换为 llvm 的提升器。我建立了一个内存模型,但对 str 和 ldr arm 汇编指令如何在内存上工作有点困惑。所以我的问题是。例如,给定一个内存地址 0000b8f0,我想在其中存储 64 位十进制值 20000000。str 指令是将整个 20000000 存储在地址 0000b8f0 中,还是将其分成字节并将第一个字节存储在 0000b8f0 中,将第二个字节存储在0000b8f1 和 0000b8f2 中的第 3 个字节等等...同样适用于从地址 (0000b8f0) 加载,ldr 指令是否仅采用存储在 0000b8f0 的字节或来自 0000b8f0-0000b8f4 的完整字节集。

抱歉,如果我的问题很基础,但我需要确保我正确地实现了 str 和 ldr 对我的记忆模型的影响。

逻辑上1,内存是一个8位字节的数组。

Word load/stores一次访问多个字节,就像C中的SIMD intrinsics,或者像((char*)my_int)[2]相反的加载第3个字节int.

的字节

C 的内存模型是围绕支持更广泛访问(如 PDP-11 或 ARM)的字节可寻址机器设计的,因此如果您了解 char* 在 C 中的工作原理,那么这就是您所习惯的访问其他对象的对象表示,例如为什么 memcpy 有效。

(我没有使用将 int* 指向 char 数组的 C 示例,因为 C 中的严格别名规则会导致未定义的行为。只允许 char* 为其他别名ISO C 中的类型。Asm 具有明确定义的行为,可以访问任何宽度的内存字节,与早期存储部分或完全重叠,GNU C 在使用 -fno-strict-aliasing 编译时也是如此,以禁用基于类型的别名分析 /优化。)


str是一个32位的字库;它一次写入所有 4 个字节。如果您要从 0000b8f1...2...3 加载,您将获得第 2、第 3 或第 4 个字节,因此 str 相当于 4 个单独的 strb 指令(通过移位提取正确的字节),除了明显缺乏原子性和性能。

str 始终存储 32 位寄存器中的 4 个字节。如果寄存器的值类似于 2,则表示高位字节全部为零。

ARM 可以是大端或小端。我认为现代 ARM 系统通常是小端字节序的,例如 x86,因此值的最低有效字节存储在最低地址。


0000b8f0 处的字节不能单独容纳 20000000;一个字节并没有那么大,如果这就是你要问的。

注意0000b8f4是下一个字的低字节;这是一个 4 字节对齐的地址。

此外,存储 int64_t20000000 需要 两个 32 位存储。例如两条 str 指令,或者一条 ARMv8 stp 来执行一对寄存器的 64 位存储,或者一条 stm 存储多条指令与两个寄存器。或八个 strb 字节存储指令。


脚注 1:这是来自软件 PoV,而不是内存控制器、数据总线或 DRAM 芯片的物理组织方式。甚至缓存,因此 比 ARM 上的整个单词,即使只是移动数据量的 1/4 或 1/8 作为 strstp

如果你问的是软件是如何工作的,从高层次的角度来看,如果你眯着眼睛,是的。地址 0x0000b8f0 是基地址,值 0x20000000 存储为

0xb8f0 0x00
0xb8f1 0x00
0xb8f2 0x00
0xb8f3 0x20

但硬件是完全不同的故事。首先,您有很多总线,在像 ARM 这样的内核中,您可能有一个内部 L1 缓存(您可能启用也可能未启用)。以及芯片供应商连接的 ahb/axi/etc 总线。这些通常是 32 或 64 位宽(在核心总线外部,在芯片内部)。因此,假设没有 mmu,这将是地址 0xb8f0 处的单个总线事务,数据为 0x20000000 或 0xXXXXXXXX20000000,其中 XX 可能是垃圾,通常是陈旧的,不假定为零,为什么要浪费门?内部或外部缓存实际上不会从字节宽的组件创建,它们可能是 32 位宽或 64 位加上奇偶校验或 ecc,所以 33 或 65 或 40 或 72 或其他。不假设内部 sram 是 8 位宽的倍数,单元库带有数百种大小和形状(宽度和深度)的 sram。

假设总线上的读取被假定为总线宽度,这很常见,因此如果您读取单个字节或认为您来自软件,则可能会导致完整的 32 位或 64 位或更宽,请按原样读取遍历总线,所有这些 bytes/bits 都会返回,处理器(核心)本身将隔离它感兴趣的字节,并用它执行指令想要的任何操作(例如 ldrb)。存储 另一方面,如果你想存储一个字节,那么硬件需要这样做,所以使用了一些方案,并且对于 axi/ahb,使用了一个字节掩码,所以如果是 32 位总线那么有4位字节mask/enable。每个字节通道一个。 64 位有 8 个。因此,将字节存储到地址 0x0123 本质上是写入 0x0120,字节掩码为 0b1000,以指示字节通道正在获取数据,该字节在正确的字节通道上(与另一个假定的字节数 garbage/stale,没有期望)。

假设你有一个缓存,哪一层都没有关系。如前所述,它们理想地是馈送它们的总线的倍数,因此如果是 32 位总线,则 32 位加上奇偶校验或 ecc 宽度(33、40,等等)。结果,单个字节的存储会导致读取-修改-写入,因为缓存 sram 本身在 32 位或 64 位宽的事务中只能是 read/written(地址本身就是您不 use/need 低地址位被剥离以进行基于字或基于双字的寻址),因此逻辑将读取整个字或双字,修改您想要写入的一个字节,然后将其写回 sram。这是一个性能损失,取决于整体架构,您是否可以轻松检测到它以及您损失了多少整体性能(您可能有很多其他开销,就像 x86 一样看不到它)。

硬件中的字大小的存储在任何方式、形状或形式上都等同于四个不同地址的四个字节大小的存储。我已经根据另一个回答这个问题的人的要求在这个网站上展示了这一点。从软件的角度来看,是的,如上所示(假设小结尾或 be-8 大端),这是一个功能等价物。如果你写一个字,你可以做字节读取以访问基于字节地址的那些字节,如果你做字节写,你可以做一个字读取,并在该读取中查看来自那些单独事务的字节。

还了解存储倍数 stm 不假定为单独的 32 位事务。无论是 32 位还是 64 位总线,每个事务都会有几个时钟的开销,如果不是更多的话,总线的处理器端对总线的 chip/bus 控制器端说,我想写,好吧,我准备好接受你的写作了。然后声明一个长度,即要发送的总线宽度项的数量。因此,32 位总线上的 stmia sp!,{r0,r1,r2,r3} 将是具有 4 个数据周期的单个事务,握手,然后是数据总线上的四个时钟。对于 64 位宽的总线,这不是假定的,这就是为什么 arm 现在需要在堆栈指针上进行 64 位对齐。如果地址是 64 位对齐的,那么它是一个长度为 2 的事务,因此数据总线上有两个时钟。但是,如果它不是 64 位对齐而是 32 位对齐,则它是三个事务,一个具有 32 位值(字节被屏蔽以将总线减半),一个 64 位事务,然后一个 32 位事务。性能下降。如果处理器支持未对齐(甚至不支持 32 位或 16 位),那么我自己还没有见过这样的内核,但我会假设它也不止一个事务。


所以纯粹是从软件编译器的角度来看。一个字大小的存储是一个字大小的存储,四个字节,一个基地址处的 32 位数字。功能上等同于基地址 +0、+1、+2、+3 处的四个字节大小的写入。但不等效于性能,也不等效于指令。负载也是如此。

许多编译器作者会在他们的设计中走得更远,尽可能避免字节大小的 load/store 指令。 byte/char based

你有两个选择
a = a + 1;


r0 = r0 + 1
r0 = r0 & 0xff;
or 
shift left 24
right shift arithmetic 24

r0 = r0 + 1

后者知道添加 r0 的高位已经根据变量类型(有符号或无符号)进行了填充,尽管它是 32 位寄存器和 8 位变量类型,但仍然是 32位表示。编译器确定何时 if/when 必须对其进行修改以表示实际宽度。即使

a = a + 1;
*bptr = a;

使用 8 位变量和 8 位指针,您仍然可以在 ARM 中使用 32 位寄存器来执行此操作。

 add r0,r0,#1
 strb r0,[r1]

由于存储将 ignore/mask 寄存器的高 24 位,因此无需准备。


更短。使用 str 指令将 0x20000000 写入 0xb8f0 是一条指令,也是处理器外(或内部到 L1 缓存)的单个事务。它不是四个单独的字节写入。如果您选择使用 strb 四次(并且还假设 isr 试图读取这个字大小的值时没有发生中断),则四字节写入是一个功能等价物。但是你必须自己明确地写四个字节。