深刻浅出WebAssembly(5) Memory

这系列主要是我对WASM研究的笔记,可能内容比较简略。总共包括:git

  1. 深刻浅出WebAssembly(1) Compilation
  2. 深刻浅出WebAssembly(2) Basic Api
  3. 深刻浅出WebAssembly(3) Instructions
  4. 深刻浅出WebAssembly(4) Validation
  5. 深刻浅出WebAssembly(5) Memory
  6. 深刻浅出WebAssembly(6) Binary Format
  7. 深刻浅出WebAssembly(7) Future
  8. 深刻浅出WebAssembly(8) Wasm in Rust(TODO)

内存寻址

Convertions

  1. 实模式: 逻辑地址 = 物理地址
  2. 保护模式: 分段 + 分页(option)
  3. 逻辑地址 :在进行C语言编程中,能读取变量地址值(&操做),实际上这个值就是逻辑地址,也能够是经过malloc或是new调用返回的地址。该地址是相对于当前进程数据段的地址,不和绝对物理地址相干。只有在Intel实模式下,逻辑地址才和物理地址相等(由于实模式没有分段或分页机制,CPU不进行自动地址转换)。应用程序员仅需和逻辑地址打交道,而分段和分页机制对通常程序员来讲是彻底透明的,仅由系统编程人员涉及。应用程序员虽然本身能直接操做内存,那也只能在操做系统给你分配的内存段操做。一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。
  4. 线性地址:是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。若是启用了分页机制,那么线性地址能再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。Intel 80386的线性地址空间容量为4G(2的32次方即32根地址总线寻址)。
  5. 物理地址(Physical Address) 是指出目前CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。若是启用了分页机制,那么线性地址会使用页目录和页表中的项变换成物理地址。若是没有启用分页机制,那么线性地址就直接成为物理地址了,好比在实模式下。

内存分段

现代的内存寻址机制,都引入了名为「分段」的概念:不一样级别的程序、程序的不一样数据类型,存放在不一样的「段」上面,而后再定义在「段」上的偏移量。也就是说,现代程序看到的地址都不是线性的,而是分段过的地址,形如:segmentSelector:offset。这些段和偏移量组成的空间,就是逻辑内存空间;这些二元组,就是逻辑地址。程序员

段标识符是由一个16位长的字段组成,又称为段选择符(Selector),由处理器提供段寄存器来存放段标识符,段寄存器有6种:github

  1. cs 代码段寄存器,指向包含程序指令的段;
  2. ss 栈寄存器,指向包含当前程序的段;
  3. ds 数据段寄存器,指向包含静态数据或者全局数据段;
  4. 其余三个寄存器es, fs, gs称为附加段寄存器,做通常用途,能够指向任意的数据段

段寄存器存放的并非段基地址也不是段描述符。段的详细信息须要经过选择器从描述符表(Descriptor Table,段表)中获得。 这样作能够加快查询速度,也能够进行权限控制,只有访问级别够的程序才能成功拿到段基地址和长度web

16位选择器具体组成:编程

  1. 第0-1位: 访问权限, 0最高(内核态),3最低(都能访问),低权限的请求不能访问高权限的内存
  2. 第2位:段表类型,0 - 全局段表(Global DT, GDT)对应寄存器gdtr; 1 - 局部段表(Local DT, LDT)对应寄存器 ldtr,DT相似于一个数组,每项占8个字节
  3. 第3-15位:段表描述符的索引信息,经过它能够得到段表描述符

段表描述符的结构比较复杂,不过最重要的是段的段基(BASE, 32bit)和段界(LIMIT, 20bit)数组

经过段描述符获得BASE 以后,再与逻辑地址偏移量offset相加,就获得了线性地址:缓存

内存分页

当段描述符中的G = 1时, 分页机制启用。markdown

分页就是人为地在逻辑上将连续的内存空间,按照固定大小切分红一段一段。对于线性内存来讲,这样切分出来的固定大小叫作页(Page);对于物理内存来讲,这样切分出来的固定大小叫作页帧(Page Frame)。 分页机制将线性内存分为若干页,将物理内存分为若干帧,并创建从页到帧的映射关系。这个映射关系,是一个「多对一」的映射。 线性地址的转换分两步完成,每一步都基于一种都基于一种转换表,第一种转换表称为页目录表转换,第二种转换称为页表转换。使用这种二级模式的目的在于减小每一个进程页表所需的RAM的数量。就像咱们看书有个书目录同样,方便快捷。具体转换以下图所示:架构

寻址过程(386):

  1. 读取段寄存器中的选择器;
  2. 验证访问权级(保护模式)——经过;
  3. 根据段表类型和段表位置索引,读取段表中的描述符;
  4. 检查访问权级位(保护模式)——经过;
  5. 检查 offset,看偏移量是否超过段界限;
  6. 检查 P 位,确保目标位置在物理内存中可用;
  7. 将段基址与 offset 拼接成线性地址;
  8. 检查是否命中高速缓存——未命中;
  9. 根据线性地址最高 10 位,读取一级页表;
  10. 一级页表检查访问权级——经过;
  11. 获得 20 位 + 12 位补 0 的二级页表位置;
  12. 根据二级页表位置访问二级页表;
  13. 根据线性地址中间 10 位,读取二级页表;
  14. 二级页表检查访问权级——经过;
  15. 获得 20 位帧基址;
  16. 与线性地址的低位 12 位拼接成 32 位的物理地址;
  17. 访存。

虚拟内存(VAS)的好处:

  1. 屏蔽底层:虚拟地址程序编写更加方便
  2. 权限控制:解决内存非法访问的问题
  3. 高效:虚拟内存寻址空间能够比物理内存大。能够灵活分配(高速缓存,LRU) 内核分段课程.PDF

内存分段与分页功能重合,所以不少新架构或OS倾向于使用Flat Segmentation,如x86-64和Linuxide

在Linux中细分了四种段:

全部的用户进程都是使用同一个用户代码段描述符和用户数据段描述符,它们是__USER_CS__USER_DS,也就是每一个进程处于用户态时,它们的CS寄存器和DS寄存器中的值是相同的。当任何进程或者中断异常进入内核后,都是使用相同的内核代码段描述符和内核数据段描述符,它们是__KERNEL_CS__KERNEL_DS。这里要明确记得,内核数据段实际上就是内核态堆栈段。 逻辑地址是由段选择符(16位) + 段内偏移量offset(32位)得来。以前也说到,只有处于用户态,CS和DS寄存器中的值都是__USER_CS__USER_DS。只要处于内核态,CS和DS寄存器中的值都是__KERNEL_CS__KERNEL_DS。在咱们编程过程当中,实际上提供的地址都是一个偏移量,系统会自动将这个偏移量与CS中的段选择符进行结合。也就是咱们使用的逻辑地址实际上只使用了offset这一段,段选择符都为空。以前也说了这四个段描述符的BASE都为0x00000000,也得出当逻辑地址经过这样的分段机制转为线性地址后,实际上并无变化,也就是逻辑地址=线性地址(其实这两个地址都是offset的值)。

WASM的虚拟内存管理

除了内存分段管理以外,应用程序也有段的概念,主要是描述的程序对数据的组织。通常Linux程序拥有下面几个段:

wasm的栈能够简化为:

\_\_data\_end 往低位增加而堆从\_\_heap\_base往高位增加,由于栈先放置因此须要在编译的时候给一个最大值。

stack\_size = \_\_heap\_base - \_\_data\_end

栈空间能够经过下面方式设置:

clang \
--target=wasm32 \
-O3 \
-flto \
-nostdlib \
-Wl,--no-entry \
-Wl,--export-all \
-Wl,--lto-O3 \
-Wl,-z,stack-size=$[8 * 1024 * 1024] \ # Set maximum stack size to 8MiB
-o add.wasm \
add.c
复制代码
// llvm 源码:
// <https://github.com/llvm-mirror/lld/blob/master/wasm/Driver.cpp#L355>
Config->InitialMemory = args::getInteger(Args, OPT_initial_memory, 0);
Config->GlobalBase = args::getInteger(Args, OPT_global_base, 1024);
Config->MaxMemory = args::getInteger(Args, OPT_max_memory, 0);
Config->ZStackSize =
      args::getZOptionValue(Args, OPT_z, "stack-size", WasmPageSize);
//...
复制代码

refer: dassur.ma/things/c-to…

内存对齐

If the effective address of a memory access is a multiple of the alignment attribute value of the memory access, the memory access is considered aligned, otherwise it is considered misaligned. Aligned and misaligned accesses have the same behavior

若是一个内存的访问有效地址(Effective address)是储存器访问的对齐属性的倍数,那么此次储存器访问就被称为是对齐的,不然是不对齐。对齐与不对齐的访问具备相同的行为,可是对齐会提升CPU的处理速度。

wasm32 的对齐属性是32,wasm64的native 对齐属性就是64

Effective Address

也便是当前访问的真实地址(相对于offset来讲)

effective\_adress = address\_operand + offset\_immediate

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/13/172ab8b78623cc76~tplv-t2oaga2asx-image.image

i32.const 3       ;; address_operand = 3
i64.const 1234    ;; value
i64.store16 1 3   ;; alignment=1, offset=3, effective_address = 3 + 3 = 6
复制代码

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/13/172ab8b79c937788~tplv-t2oaga2asx-image.image

上述是对齐的,可是若是是:

i32.const 3       ;; address_operand = 3
i64.const 1234    ;; value
i64.store16 2 3   ;; alignment=2
复制代码

那么将会对不齐:

https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/13/172ab8b7961d13d4~tplv-t2oaga2asx-image.image