6

玩过循环、分支、表格和所有这些不错的运算符后,我几乎开始对这种语言感到满意,足以创建一些有用的东西,但有些逻辑我仍然不明白。请耐心等待,因为它会有点长。

问题:有人能解释一下翻译后的代码是如何工作的吗?我在下面进一步添加了具体问题。

首先是一些我一直在转换的琐碎的 c++ 代码:

class FirstClass {
  int prop1 = 111;
  int prop2 = 222;
  int prop3 = 333;

  public:
  FirstClass(int param1, int param2) {
    prop1 += param1 + param2;  

  }
};

class SecondClass {
  public:
  SecondClass() {

  }
};

int main() {
  FirstClass firstClass1(10, 5);
  FirstClass firstClass2(30, 15);
  FirstClass firstClass3(2, 4);
  FirstClass firstClass4(2, 4);
}

翻译成:

(module
  (table 0 anyfunc)
  (memory $0 1)
  (export "memory" (memory $0))
  (export "main" (func $main))
  (func $main (result i32)
    (local $0 i32)
    (i32.store offset=4
      (i32.const 0)
      (tee_local $0
        (i32.sub
          (i32.load offset=4
            (i32.const 0)
          )
          (i32.const 64)
        )
      )
    )
    (drop
      (call $_ZN10FirstClassC2Eii
        (i32.add
          (get_local $0)
          (i32.const 48)
        )
        (i32.const 10)
        (i32.const 5)
      )
    )
    (drop
      (call $_ZN10FirstClassC2Eii
        (i32.add
          (get_local $0)
          (i32.const 32)
        )
        (i32.const 30)
        (i32.const 15)
      )
    )
    (drop
      (call $_ZN10FirstClassC2Eii
        (i32.add
          (get_local $0)
          (i32.const 16)
        )
        (i32.const 2)
        (i32.const 4)
      )
    )
    (drop
      (call $_ZN10FirstClassC2Eii
        (get_local $0)
        (i32.const 2)
        (i32.const 4)
      )
    )
    (i32.store offset=4
      (i32.const 0)
      (i32.add
        (get_local $0)
        (i32.const 64)
      )
    )
    (i32.const 0)
  )
  (func $_ZN10FirstClassC2Eii (param $0 i32) (param $1 i32) (param $2 i32) (result i32)
    (i32.store offset=8
      (get_local $0)
      (i32.const 222)
    )
    (i32.store offset=4
      (get_local $0)
      (i32.const 222)
    )
    (i32.store
      (get_local $0)
      (i32.add
        (i32.add
          (get_local $1)
          (get_local $2)
        )
        (i32.const 111)
      )
    )
    (get_local $0)
  )
)

所以现在我对这里实际发生的事情有一些疑问。虽然我认为我理解了大部分内容,但仍有一些事情我不确定:

例如查看构造函数及其签名:

(func $_ZN10FirstClassC2Eii (param $0 i32) (param $1 i32) (param $2 i32) (result i32)

它有以下参数:(param $0 i32)我假设它是在主函数中定义的一些局部。让我们说一些记忆。但是,我们知道我们在 main 函数中有 4 个实例,这意味着所有这些实例都保存在同一个 (local $0 i32)但具有不同偏移量的内部,我是对还是错?

接下来让我们看一下对构造函数的调用:

(drop
  (call $_ZN10FirstClassC2Eii
    (i32.add
      (get_local $0)
      (i32.const 32)
    )
    (i32.const 30)
    (i32.const 15)
  )
)

我们调用构造函数并传入 3 个参数。究竟是为了什么?我们是否在本地添加空间?仔细观察,对于每个构造函数调用,这个数字都会减少 16(我从上到下阅读代码),大约是一个单词的大小。我不知道这意味着什么。

最后我们有:

(i32.store offset=4
  (i32.const 0)
  (tee_local $0
    (i32.sub
      (i32.load offset=4
        (i32.const 0)
      )
      (i32.const 64)
    )
  )
)

它甚至加载了什么以及为什么要减法?我的意思是它设置一个本地并返回它,以便我们可以将它存储在偏移量为 4 的线性内存中?偏移量 4 与什么有关?

4

1 回答 1

2

您注意到的很多内容都在 C++ 到某些编译器的 IR转换中。由于您使用的工具是基于 LLVM 的,因此如果您想进行探索,我建议您查看 LLVM 的 IR。这是您在 LLVM IR 中的示例,也未优化。这很有趣,因为 WebAssembly 发生在这个 LLVM IR 之后,所以你可以看到 C++ 的部分翻译。也许我们可以理解它!


与 C++ 中的所有非静态函数类成员一样,构造函数具有隐式*this参数。这就是第零个参数。为什么呢i32?因为 WebAssembly 中的所有指针都是i32.

在 LLVM IR 中,这是:

define linkonce_odr void @FirstClass::FirstClass(int, int)(%class.FirstClass*, i32, i32) unnamed_addr #2 comdat align 2 !dbg !29 {

指针%class.FirstClass*在哪里。*this稍后,当降低到 WebAssembly 时,它会变成一个i32.


对于您的以下问题......调用构造函数时添加了什么?我们必须创建*this,并且您将它们分配在堆栈上。LLVM 如此执行这些分配:

  %1 = alloca %class.FirstClass, align 4
  %2 = alloca %class.FirstClass, align 4
  %3 = alloca %class.FirstClass, align 4
  %4 = alloca %class.FirstClass, align 4

所以它的堆栈概念包含四个类型的变量FirstClass。当我们降低到 WebAssembly 时,堆栈必须去某个地方。在 WebAssembly 中有 3 个地方可以使用 C++ 堆栈:

  1. 在执行堆栈上(每个操作码都会推送和弹出值,所以add先弹出 2,然后再压入 1)。
  2. 作为当地人。
  3. Memory.

请注意,您不能获取 1. 和 2 的地址。构造函数传递*this给函数,因此编译器必须将该值放在Memory. 那个堆栈在Memory哪里?Emscripten 会为您服务!它决定将内存中的堆栈指针存储在地址 4 处,因此(i32.load offset=4 (i32.const 0)). 然后来自 LLVM的四个alloca位于该地址的偏移量处,因此(i32.add (get_local $0) (i32.const 48))它们正在获取堆栈位置(我们在 local 中加载$0)并获取其偏移量。这就是 的价值*this

请注意,经过优化后,绝大多数 C++ 堆栈上的变量都不会在内存中结束!大多数将被推送/弹出,或存储在 WebAssembly 本地(其中有无穷大)。这与 x86 或 ARM 等其他 ISA 类似:将本地变量放在寄存器中要好得多,但这些 ISA 只有少数几个。因为 WebAssembly 是一个虚拟 ISA,我们可以承受无限的本地人,因此 LLVM / Emscripten 必须实现到内存中的堆栈要小得多。唯一必须实现它们的时间是它们的地址被获取,或者它们通过引用传递(实际上是一个指针),或者一个函数有多个返回值(WebAssembly 将来可能会支持)。


您拥有的最后一段代码:

  1. 加载内存中的堆栈指针。
  2. 从中减去 64。
  3. 存储回堆栈指针。

那是你的功能序言。如果您查看函数的最后,您会发现匹配的结尾将 64 添加回指针。这为四人腾出空间alloca。它是(非官方)WebAssembly ABI 的一部分,每个函数都负责为其变量增加和缩小内存中的堆栈。

为什么是64?FirstClass那是 4 x 16,这对于这四个实例来说刚好够用:它们每个都包含 3i32个,在存储时每个都被舍入到 16 个字节,以便对齐。在 C++ 中尝试sizeof(FirstClass)(它是 12),然后尝试分配它们的数组(它们每个都被填充 4 个字节,因此每个条目都是对齐的)。这只是 C++ 通常实现的一部分,与 LLVM 或 WebAssembly 无关。

于 2017-04-23T16:11:20.367 回答