Commit 4e0d7154b1

Luuk de Gram <luuk@degram.dev>
2023-03-18 16:02:30
wasm-linker: implement __wasm_init_memory & flag
Implements the __wasm_init_memory and __wasm_init_memory_flag synthetic function and symbol. The former will initialize all passive segments during runtime. For the bss section we will fill it with zeroes, whereas the other segments will simply be initialized only. The latter stores the offset into the linear data section, after all heap memory that is part of the Wasm module. Any memory initialized at runtime starts from this offset.
1 parent 09d6938
Changed files (1)
src
src/link/Wasm.zig
@@ -192,6 +192,14 @@ pub const Segment = struct {
     pub fn isPassive(segment: Segment) bool {
         return segment.flags & @enumToInt(Flag.WASM_DATA_SEGMENT_IS_PASSIVE) != 0;
     }
+
+    /// For a given segment, determines if it needs passive initialization
+    fn needsPassiveInitialization(segment: Segment, import_mem: bool, name: []const u8) bool {
+        if (import_mem and !std.mem.eql(u8, name, ".bss")) {
+            return true;
+        }
+        return segment.isPassive();
+    }
 };
 
 pub const Export = struct {
@@ -824,6 +832,178 @@ fn resolveSymbolsInArchives(wasm: *Wasm) !void {
     }
 }
 
+fn setupInitMemoryFunction(wasm: *Wasm) !void {
+    // Passive segments are used to avoid memory being reinitialized on each
+    // thread's instantiation. These passive segments are initialized and
+    // dropped in __wasm_init_memory, which is registered as the start function
+    // We also initialize bss segments (using memory.fill) as part of this
+    // function.
+    if (!wasm.hasPassiveInitializationSegments()) {
+        return;
+    }
+
+    const flag_address: u32 = if (wasm.base.options.shared_memory) address: {
+        // when we have passive initialization segments and shared memory
+        // `setupMemory` will create this symbol and set its virtual address.
+        const loc = wasm.findGlobalSymbol("__wasm_init_memory_flag").?;
+        break :address loc.getSymbol(wasm).virtual_address;
+    } else 0;
+
+    var function_body = std.ArrayList(u8).init(wasm.base.allocator);
+    defer function_body.deinit();
+    const writer = function_body.writer();
+
+    // we have 0 locals
+    try leb.writeULEB128(writer, @as(u32, 0));
+
+    if (wasm.base.options.shared_memory) {
+        // destination blocks
+        // based on values we jump to corresponding label
+        try writer.writeByte(std.wasm.opcode(.block)); // $drop
+        try writer.writeByte(std.wasm.block_empty); // block type
+
+        try writer.writeByte(std.wasm.opcode(.block)); // $wait
+        try writer.writeByte(std.wasm.block_empty); // block type
+
+        try writer.writeByte(std.wasm.opcode(.block)); // $init
+        try writer.writeByte(std.wasm.block_empty); // block type
+
+        // atomically check
+        try writer.writeByte(std.wasm.opcode(.i32_const));
+        try leb.writeULEB128(writer, flag_address);
+        try writer.writeByte(std.wasm.opcode(.i32_const));
+        try leb.writeULEB128(writer, @as(u32, 0));
+        try writer.writeByte(std.wasm.opcode(.i32_const));
+        try leb.writeULEB128(writer, @as(u32, 1));
+        try writer.writeByte(std.wasm.opcode(.atomics_prefix));
+        try leb.writeULEB128(writer, std.wasm.atomicsOpcode(.i32_atomic_rmw_cmpxchg));
+        try leb.writeULEB128(writer, @as(u32, 2)); // alignment
+        try leb.writeULEB128(writer, @as(u32, 0)); // offset
+
+        // based on the value from the atomic check, jump to the label.
+        try writer.writeByte(std.wasm.opcode(.br_table));
+        try leb.writeULEB128(writer, @as(u32, 2)); // length of the table (we have 3 blocks but because of the mandatory default the length is 2).
+        try leb.writeULEB128(writer, @as(u32, 0)); // $init
+        try leb.writeULEB128(writer, @as(u32, 1)); // $wait
+        try leb.writeULEB128(writer, @as(u32, 2)); // $drop
+        try writer.writeByte(std.wasm.opcode(.end));
+    }
+
+    var it = wasm.data_segments.iterator();
+    var segment_index: u32 = 0;
+    while (it.next()) |entry| : (segment_index += 1) {
+        const segment: Segment = wasm.segments.items[entry.value_ptr.*];
+        if (segment.needsPassiveInitialization(wasm.base.options.import_memory, entry.key_ptr.*)) {
+            // For passive BSS segments we can simple issue a memory.fill(0).
+            // For non-BSS segments we do a memory.init.  Both these
+            // instructions take as their first argument the destination
+            // address.
+            try writer.writeByte(std.wasm.opcode(.i32_const));
+            try leb.writeULEB128(writer, segment.offset);
+
+            if (wasm.base.options.shared_memory and std.mem.eql(u8, entry.key_ptr.*, ".tdata")) {
+                // When we initialize the TLS segment we also set the `__tls_base`
+                // global.  This allows the runtime to use this static copy of the
+                // TLS data for the first/main thread.
+                try writer.writeByte(std.wasm.opcode(.i32_const));
+                try leb.writeULEB128(writer, segment.offset);
+                try writer.writeByte(std.wasm.opcode(.global_set));
+                const loc = wasm.findGlobalSymbol("__tls_base").?;
+                try leb.writeULEB128(writer, loc.getSymbol(wasm).index);
+            }
+
+            try writer.writeByte(std.wasm.opcode(.i32_const));
+            try leb.writeULEB128(writer, @as(u32, 0));
+            try writer.writeByte(std.wasm.opcode(.i32_const));
+            try leb.writeULEB128(writer, segment.size);
+            try writer.writeByte(std.wasm.opcode(.misc_prefix));
+            if (std.mem.eql(u8, entry.key_ptr.*, ".bss")) {
+                // fill bss segment with zeroes
+                try leb.writeULEB128(writer, std.wasm.miscOpcode(.memory_fill));
+            } else {
+                // initialize the segment
+                try leb.writeULEB128(writer, std.wasm.miscOpcode(.memory_init));
+                try leb.writeULEB128(writer, segment_index);
+            }
+            try writer.writeByte(0); // memory index immediate
+        }
+    }
+
+    if (wasm.base.options.shared_memory) {
+        // we set the init memory flag to value '2'
+        try writer.writeByte(std.wasm.opcode(.i32_const));
+        try leb.writeULEB128(writer, flag_address);
+        try writer.writeByte(std.wasm.opcode(.i32_const));
+        try leb.writeULEB128(writer, @as(u32, 2));
+        try writer.writeByte(std.wasm.opcode(.atomics_prefix));
+        try leb.writeULEB128(writer, std.wasm.atomicsOpcode(.i32_atomic_store));
+        try leb.writeULEB128(writer, @as(u32, 2)); // alignment
+        try leb.writeULEB128(writer, @as(u32, 0)); // offset
+
+        // notify any waiters for segment initialization completion
+        try writer.writeByte(std.wasm.opcode(.i32_const));
+        try leb.writeULEB128(writer, flag_address);
+        try writer.writeByte(std.wasm.opcode(.i32_const));
+        try leb.writeILEB128(writer, @as(i32, -1)); // number of waiters
+        try writer.writeByte(std.wasm.opcode(.atomics_prefix));
+        try leb.writeULEB128(writer, std.wasm.atomicsOpcode(.memory_atomic_notify));
+        try leb.writeULEB128(writer, @as(u32, 2)); // alignment
+        try leb.writeULEB128(writer, @as(u32, 0)); // offset
+        try writer.writeByte(std.wasm.opcode(.drop));
+
+        // branch and drop segments
+        try writer.writeByte(std.wasm.opcode(.br));
+        try leb.writeULEB128(writer, @as(u32, 1));
+
+        // wait for thread to initialize memory segments
+        try writer.writeByte(std.wasm.opcode(.end)); // end $wait
+        try writer.writeByte(std.wasm.opcode(.i32_const));
+        try leb.writeULEB128(writer, flag_address);
+        try writer.writeByte(std.wasm.opcode(.i32_const));
+        try leb.writeULEB128(writer, @as(u32, 1)); // expected flag value
+        try writer.writeByte(std.wasm.opcode(.i32_const));
+        try leb.writeILEB128(writer, @as(i32, -1)); // timeout
+        try writer.writeByte(std.wasm.opcode(.atomics_prefix));
+        try leb.writeULEB128(writer, std.wasm.atomicsOpcode(.memory_atomic_wait32));
+        try leb.writeULEB128(writer, @as(u32, 2)); // alignment
+        try leb.writeULEB128(writer, @as(u32, 0)); // offset
+        try writer.writeByte(std.wasm.opcode(.drop));
+
+        try writer.writeByte(std.wasm.opcode(.end)); // end $drop
+    }
+
+    it.reset();
+    segment_index = 0;
+    while (it.next()) |entry| : (segment_index += 1) {
+        const name = entry.key_ptr.*;
+        const segment: Segment = wasm.segments.items[entry.value_ptr.*];
+        if (segment.needsPassiveInitialization(wasm.base.options.import_memory, name) and
+            !std.mem.eql(u8, name, ".bss"))
+        {
+            // The TLS region should not be dropped since its is needed
+            // during the initialization of each thread (__wasm_init_tls).
+            if (wasm.base.options.shared_memory and std.mem.eql(u8, name, ".tdata")) {
+                continue;
+            }
+
+            try writer.writeByte(std.wasm.opcode(.misc_prefix));
+            try leb.writeULEB128(writer, std.wasm.miscOpcode(.data_drop));
+            try leb.writeULEB128(writer, segment_index);
+        }
+    }
+
+    // End of the function body
+    try writer.writeByte(std.wasm.opcode(.end));
+
+    try wasm.createSyntheticFunction(
+        "__wasm_init_memory",
+        std.wasm.Type{ .params = &.{}, .returns = &.{} },
+        &function_body,
+    );
+}
+
+/// Constructs a synthetic function that performs runtime relocations for
+/// TLS symbols. This function is called by `__wasm_init_tls`.
 fn setupTLSRelocationsFunction(wasm: *Wasm) !void {
     // When we have TLS GOT entries and shared memory is enabled,
     // we must perform runtime relocations or else we don't create the function.
@@ -2469,6 +2649,16 @@ fn setupMemory(wasm: *Wasm) !void {
         offset += segment.size;
     }
 
+    // create the memory init flag which is used by the init memory function
+    if (wasm.base.options.shared_memory and wasm.hasPassiveInitializationSegments()) {
+        // align to pointer size
+        memory_ptr = mem.alignForwardGeneric(u64, memory_ptr, 4);
+        const loc = try wasm.createSyntheticSymbol("__wasm_init_memory_flag", .data);
+        const sym = loc.getSymbol(wasm);
+        sym.virtual_address = @intCast(u32, memory_ptr);
+        memory_ptr += 4;
+    }
+
     if (!place_stack_first and !is_obj) {
         memory_ptr = std.mem.alignForwardGeneric(u64, memory_ptr, stack_alignment);
         memory_ptr += stack_size;
@@ -3000,6 +3190,7 @@ fn linkWithZld(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) l
     try wasm.mergeSections();
     try wasm.mergeTypes();
     try wasm.initializeCallCtorsFunction();
+    try wasm.setupInitMemoryFunction();
     try wasm.setupTLSRelocationsFunction();
     try wasm.initializeTLSFunction();
     try wasm.setupExports();
@@ -3121,6 +3312,7 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod
     try wasm.mergeSections();
     try wasm.mergeTypes();
     try wasm.initializeCallCtorsFunction();
+    try wasm.setupInitMemoryFunction();
     try wasm.setupTLSRelocationsFunction();
     try wasm.initializeTLSFunction();
     try wasm.setupExports();
@@ -4473,6 +4665,17 @@ fn emitDataRelocations(
     try writeCustomSectionHeader(binary_bytes.items, header_offset, size);
 }
 
+fn hasPassiveInitializationSegments(wasm: *const Wasm) bool {
+    var it = wasm.data_segments.iterator();
+    while (it.next()) |entry| {
+        const segment: Segment = wasm.segments.items[entry.value_ptr.*];
+        if (segment.needsPassiveInitialization(wasm.base.options.import_memory, entry.key_ptr.*)) {
+            return true;
+        }
+    }
+    return false;
+}
+
 pub fn getTypeIndex(wasm: *const Wasm, func_type: std.wasm.Type) ?u32 {
     var index: u32 = 0;
     while (index < wasm.func_types.items.len) : (index += 1) {