问题描述
玩过循环、分支、表格和所有这些不错的操作符后,我几乎开始对这种语言感到满意,足以创造一些有用的东西,但有些逻辑我仍然不明白.请耐心等待,因为它会有点长.
问题:有人可以解释翻译后的代码是如何工作的吗?我在下面进一步添加了具体问题.
首先这里是一些我一直在转换的简单的 C++ 代码:
class FirstClass {int prop1 = 111;int prop2 = 222;int prop3 = 333;民众:FirstClass(int param1, int param2) {prop1 += param1 + param2;}};类二等{民众:第二类(){}};int main() {头等舱头等舱1(10, 5);头等舱头等舱2(30, 15);头等舱头等舱3(2, 4);FirstClass firstClass4(2, 4);}
翻译成:
(模块(表 0 anyfunc)(内存 $0 1)(导出内存"(内存 $0))(导出main"(func $main))(func $main (结果 i32)(本地 $0 i32)(i32.store 偏移=4(i32.const 0)(tee_local $0(i32.sub(i32.load offset=4(i32.const 0))(i32.const 64))))(降低(调用 $_ZN10FirstClassC2Eii(i32.add(get_local $0)(i32.const 48))(i32.const 10)(i32.const 5)))(降低(调用 $_ZN10FirstClassC2Eii(i32.add(get_local $0)(i32.const 32))(i32.const 30)(i32.const 15)))(降低(调用 $_ZN10FirstClassC2Eii(i32.add(get_local $0)(i32.const 16))(i32.const 2)(i32.const 4)))(降低(调用 $_ZN10FirstClassC2Eii(get_local $0)(i32.const 2)(i32.const 4)))(i32.store 偏移=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 偏移=8(get_local $0)(i32.const 222))(i32.store 偏移=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)
中,但是有不同的偏移量,我是对还是错?>
接下来让我们看一下对构造函数的调用:
(放下(调用 $_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 与什么有关?
您注意到的很多内容是在 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,对齐 4%2 = alloca %class.FirstClass,对齐 4%3 = alloca %class.FirstClass,对齐 4%4 = alloca %class.FirstClass,对齐 4
所以它的堆栈概念包含四个 FirstClass
类型的变量.当我们降低到 WebAssembly 时,堆栈必须去某个地方.在 WebAssembly 中有 3 个 C++ 堆栈可以去的地方:
- 在执行堆栈上(每个操作码都会压入和弹出值,所以
add
会先弹出 2,然后再压入 1). - 作为本地人.
- 在
内存
中.
请注意,您不能取 1. 和 2. 的地址.构造函数将 *this
传递给函数,因此编译器必须将该值放在内存
.Memory
中的那个堆栈在哪里?Emscripten 为您处理!它决定将内存中的堆栈指针存储在地址 4,因此 (i32.load offset=4 (i32.const 0))
.来自 LLVM 的四个 alloca
然后位于该地址的偏移量处,因此 (i32.add (get_local $0) (i32.const 48))
正在获取堆栈位置(我们在本地 $0
中加载)并获取其偏移量.这就是 *this
的价值.
请注意,优化后,绝大多数 C++ 堆栈上变量不会最终出现在内存中!大多数将被推送/弹出,或存储在 WebAssembly 本地(其中有无穷大).这类似于 x86 或 ARM 等其他 ISA:将本地变量放在寄存器中更好,但这些 ISA 只有少数几个.因为 WebAssembly 是一个虚拟 ISA,我们可以负担无限的局部变量,因此 LLVM/Emscripten 必须具体化到内存中的堆栈要小得多.它们必须被具体化的唯一时间是当它们的地址被获取时,或者它们通过引用传递(实际上是一个指针),或者一个函数有多个返回值(WebAssembly 将来可能会支持).
您拥有的最后一点代码:
- 加载内存中的堆栈指针.
- 从中减去 64.
- 存储回堆栈指针.
这就是你的函数序言.如果您查看函数的最后部分,您会发现匹配的结尾将 64 添加回指针.这为四个 alloca
腾出了空间.这是(非官方)WebAssembly ABI 的一部分,每个函数负责为其变量增加和收缩内存中的堆栈.
为什么是 64?那是 4 x 16,这对于这四个 FirstClass
实例来说刚好足够:它们每个都包含 3 个 i32
,每个存储时都会四舍五入到 16 个字节,以进行对齐.在 C++ 中尝试 sizeof(FirstClass)
(它是 12),然后尝试分配它们的数组(它们每个都填充 4 个字节,因此每个条目都对齐).这只是 C++ 通常实现的一部分,与 LLVM 或 WebAssembly 无关.
Having played around with loops, branches, tables and all those nice operators, I nearly start to feel comfortable with the language enough to create something useful but there is some logic that I still dont understand. Please bear with me as it will be a bit long.
Question: Could someone explain how the translated code works? I added concrete questions further below.
First here is some trivial c++ code which I have been converting:
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);
}
Which translates into:
(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)
)
)
So now I have some questions about what is actually going on here. While I think I understand most of it, there are still some things where im just not sure:
For example see the constructor and its signature:
(func $_ZN10FirstClassC2Eii (param $0 i32) (param $1 i32) (param $2 i32) (result i32)
It has the following parameter: (param $0 i32)
which I assume is some local defined in the main function. Lets say some memory. However, we know we have 4 instances inside the main function which means all those instances are saved inside the same (local $0 i32)
but with a different offset, am I right or am I wrong?
Next lets take a look at a call to the constructor:
(drop
(call $_ZN10FirstClassC2Eii
(i32.add
(get_local $0)
(i32.const 32)
)
(i32.const 30)
(i32.const 15)
)
)
We call the constructor and pass in 3 parameters. What exactly is the addition for though? Are we adding space inside our local? Looking at it closely, for every constructor call this number is decreasing by 16 (im reading the code from top to down) which is about the size of a word. I dont know what it means.
And finally we have:
(i32.store offset=4
(i32.const 0)
(tee_local $0
(i32.sub
(i32.load offset=4
(i32.const 0)
)
(i32.const 64)
)
)
)
What is it even loading and why the substraction? I mean its setting a local and returning it so that we can store it inside linear memory with an offset 4? offset 4 in relation to what?
A lot of what you notice is in the C++ to some compiler IR translation. Since the tool you're using is based on LLVM, I suggest you look at LLVM's IR if you want to go spelunking. Here's your example, also unoptimized, in LLVM IR. This is interesting because WebAssembly occurs after this LLVM IR, so you can see the translation from C++ part-way. And maybe we can make sense of it!
The constructor, like all non-static function class members in C++, has an implicit *this
parameter. That's what the zeroth parameter is. Why is it i32
? Because all pointers in WebAssembly are i32
.
In LLVM IR this is:
define linkonce_odr void @FirstClass::FirstClass(int, int)(%class.FirstClass*, i32, i32) unnamed_addr #2 comdat align 2 !dbg !29 {
Where %class.FirstClass*
is the *this
pointer. Later on, when lowering to WebAssembly, it'll become an i32
.
To your following question... What's the addition when calling the constructors? We have to create *this
, and you allocated them on the stack. LLVM performs these allocations thusly:
%1 = alloca %class.FirstClass, align 4
%2 = alloca %class.FirstClass, align 4
%3 = alloca %class.FirstClass, align 4
%4 = alloca %class.FirstClass, align 4
So its idea of the stack holds four variables of type FirstClass
. When we lower to WebAssembly the stack has to go somewhere. There are 3 places C++ stack can go in WebAssembly:
- On the execution stack (each opcode pushes and pops values, so
add
pops 2 and then pushes 1). - As a local.
- In the
Memory
.
Notice that you can't take the address of 1. and 2. The constructor passes *this
to a function, so the compiler must put that value on the Memory
. Where is that stack in Memory
? Emscripten takes care of it for you! It decided that it would store the in-memory stack pointer at address 4, hence the (i32.load offset=4 (i32.const 0))
. The four alloca
from LLVM are then located at offsets of that address, so the (i32.add (get_local $0) (i32.const 48))
are taking the stack location (which we loaded in local $0
) and getting its offset. That's the value of *this
.
Note that after optimization, the vast majority of C++ on-stack variables won't end up in the memory! Most will be pushed / popped, or stored in WebAssembly locals (of which there's an infinity). That's similar to other ISAs such as x86 or ARM: it's way better to put locals in registers, but these ISAs only have a handful of them. Because WebAssembly is a virtual ISA we can afford an infinity of locals, and so the stack that LLVM / Emscripten must materialize into memory is much smaller. The only times they must be materialized is when their address is taken, or they're passed by reference (effectively a pointer), or a function has multiple return values (which WebAssembly may support in the future).
The last bit of code you have:
- Loads the in-memory stack pointer.
- Subtracts 64 from it.
- Stores back the stack pointer.
That's your function prologue. If you look at the very end of your function you'll find the matching epilogue which adds 64 back to the pointer. That's making space for the four alloca
. It's part of the (unofficial) WebAssembly ABI that each function is responsible to grow and shrink the stack in-memory for its variables.
Why 64? That's 4 x 16, which is just enough space for those four FirstClass
instances: they each hold 3 i32
which get rounded up to 16 bytes each when stored, for alignment. Try out sizeof(FirstClass)
in C++ (it's 12), and then try allocating an array of them (they'll each be padded by 4 bytes so each entry is aligned). This is just part of C++'s usual implementation and has nothing to do with LLVM or WebAssembly.
这篇关于理解类结构和构造函数调用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!