From b0024c48841b78a962918cd4ab20459ba6451050 Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sun, 12 Mar 2023 15:23:58 +0100 Subject: [PATCH 1/9] wasm-linker: basic TLS support Linker now parses segments with regards to TLS segments. If the name represents a TLS segment but does not contain the TLS flag, we set it manually as the object file is created using an older compiler (LLVM). For now we panic when we find a TLS relocation and implement those later. --- src/link/Wasm.zig | 19 +++++++++++++++++++ src/link/Wasm/Atom.zig | 7 +++++++ src/link/Wasm/Object.zig | 6 ++++++ src/link/Wasm/Symbol.zig | 4 ++++ src/link/Wasm/types.zig | 29 +++++++++++++++++++++-------- 5 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index e998a8d50e..287f880d92 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -894,6 +894,13 @@ fn resolveLazySymbols(wasm: *Wasm) !void { try wasm.discarded.putNoClobber(wasm.base.allocator, kv.value, loc); _ = wasm.resolved_symbols.swapRemove(loc); } + + if (!wasm.base.options.shared_memory) { + if (wasm.undefs.fetchSwapRemove("__tls_base")) |kv| { + const loc = try wasm.createSyntheticSymbol("__tls_base", .global); + try wasm.discarded.putNoClobber(wasm.base.allocator, kv.value, loc); + } + } } // Tries to find a global symbol by its name. Returns null when not found, @@ -2224,6 +2231,18 @@ fn setupMemory(wasm: *Wasm) !void { while (data_seg_it.next()) |entry| { const segment = &wasm.segments.items[entry.value_ptr.*]; memory_ptr = std.mem.alignForwardGeneric(u64, memory_ptr, segment.alignment); + + // set TLS-related symbols + if (mem.eql(u8, entry.key_ptr.*, ".tdata")) { + if (wasm.findGlobalSymbol("__tls_base")) |loc| { + const sym = loc.getSymbol(wasm); + sym.index = try wasm.globals.append(wasm.base.allocator, wasm.imports.globalCount, .{ + .global_type = .{ .valtype = .i32_const, .mutable = false }, + .init = .{ .i32_const = @intCast(i32, memory_ptr) }, + }); + } + } + memory_ptr += segment.size; segment.offset = offset; offset += segment.size; diff --git a/src/link/Wasm/Atom.zig b/src/link/Wasm/Atom.zig index 0c9d761f05..7d2f5a6696 100644 --- a/src/link/Wasm/Atom.zig +++ b/src/link/Wasm/Atom.zig @@ -126,10 +126,12 @@ pub fn resolveRelocs(atom: *Atom, wasm_bin: *const Wasm) void { .R_WASM_TABLE_INDEX_SLEB, .R_WASM_TABLE_NUMBER_LEB, .R_WASM_TYPE_INDEX_LEB, + .R_WASM_MEMORY_ADDR_TLS_SLEB, => leb.writeUnsignedFixed(5, atom.code.items[reloc.offset..][0..5], @intCast(u32, value)), .R_WASM_MEMORY_ADDR_LEB64, .R_WASM_MEMORY_ADDR_SLEB64, .R_WASM_TABLE_INDEX_SLEB64, + .R_WASM_MEMORY_ADDR_TLS_SLEB64, => leb.writeUnsignedFixed(10, atom.code.items[reloc.offset..][0..10], value), } } @@ -190,5 +192,10 @@ fn relocationValue(atom: Atom, relocation: types.Relocation, wasm_bin: *const Wa const rel_value = @intCast(i32, target_atom.offset + offset) + relocation.addend; return @intCast(u32, rel_value); }, + .R_WASM_MEMORY_ADDR_TLS_SLEB, + .R_WASM_MEMORY_ADDR_TLS_SLEB64, + => { + @panic("TODO: Implement TLS relocations"); + }, } } diff --git a/src/link/Wasm/Object.zig b/src/link/Wasm/Object.zig index 45c9464ec8..4b918064c1 100644 --- a/src/link/Wasm/Object.zig +++ b/src/link/Wasm/Object.zig @@ -674,6 +674,12 @@ fn Parser(comptime ReaderType: type) type { segment.alignment, segment.flags, }); + + // support legacy object files that specified being TLS by the name instead of the TLS flag. + if (!segment.isTLS() and (std.mem.startsWith(u8, segment.name, ".tdata") or std.mem.startsWith(u8, segment.name, ".tbss"))) { + // set the flag so we can simply check for the flag in the rest of the linker. + segment.flags |= @enumToInt(types.Segment.Flags.WASM_SEG_FLAG_TLS); + } } parser.object.segment_info = segments; }, diff --git a/src/link/Wasm/Symbol.zig b/src/link/Wasm/Symbol.zig index 156b507a32..8a1c4c5fdb 100644 --- a/src/link/Wasm/Symbol.zig +++ b/src/link/Wasm/Symbol.zig @@ -90,6 +90,10 @@ pub fn requiresImport(symbol: Symbol) bool { return true; } +pub fn isTLS(symbol: Symbol) bool { + return symbol.flags & @enumToInt(Flag.WASM_SYM_TLS) != 0; +} + pub fn hasFlag(symbol: Symbol, flag: Flag) bool { return symbol.flags & @enumToInt(flag) != 0; } diff --git a/src/link/Wasm/types.zig b/src/link/Wasm/types.zig index 964ba04ba0..a65e352c46 100644 --- a/src/link/Wasm/types.zig +++ b/src/link/Wasm/types.zig @@ -38,6 +38,8 @@ pub const Relocation = struct { R_WASM_TABLE_INDEX_SLEB64 = 18, R_WASM_TABLE_INDEX_I64 = 19, R_WASM_TABLE_NUMBER_LEB = 20, + R_WASM_MEMORY_ADDR_TLS_SLEB = 21, + R_WASM_MEMORY_ADDR_TLS_SLEB64 = 25, /// Returns true for relocation types where the `addend` field is present. pub fn addendIsPresent(self: RelocationType) bool { @@ -125,23 +127,34 @@ pub const Segment = struct { /// Bitfield containing flags for a segment flags: u32, + pub fn isTLS(segment: Segment) bool { + return segment.flags & @enumToInt(Flags.WASM_SEG_FLAG_TLS) != 0; + } + /// Returns the name as how it will be output into the final object /// file or binary. When `merge_segments` is true, this will return the /// short name. i.e. ".rodata". When false, it returns the entire name instead. - pub fn outputName(self: Segment, merge_segments: bool) []const u8 { - if (std.mem.startsWith(u8, self.name, ".synthetic")) return ".synthetic"; // always merge - if (!merge_segments) return self.name; - if (std.mem.startsWith(u8, self.name, ".rodata.")) { + pub fn outputName(segment: Segment, merge_segments: bool) []const u8 { + if (segment.isTLS()) { + return ".tdata"; + } else if (!merge_segments) { + return segment.name; + } else if (std.mem.startsWith(u8, segment.name, ".rodata.")) { return ".rodata"; - } else if (std.mem.startsWith(u8, self.name, ".text.")) { + } else if (std.mem.startsWith(u8, segment.name, ".text.")) { return ".text"; - } else if (std.mem.startsWith(u8, self.name, ".data.")) { + } else if (std.mem.startsWith(u8, segment.name, ".data.")) { return ".data"; - } else if (std.mem.startsWith(u8, self.name, ".bss.")) { + } else if (std.mem.startsWith(u8, segment.name, ".bss.")) { return ".bss"; } - return self.name; + return segment.name; } + + pub const Flags = enum(u32) { + WASM_SEG_FLAG_STRINGS = 0x1, + WASM_SEG_FLAG_TLS = 0x2, + }; }; pub const InitFunc = struct { From 09abd53da701a5ef4db4b81463e2535e192a5eee Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sun, 12 Mar 2023 16:02:02 +0100 Subject: [PATCH 2/9] wasm-linker: refactor Limits and add flags Rather than adding the flags "on-demand" during limits writing, we now properly parse them and store the flags within the limits itself. This also allows us to store whether we're using shared- memory or not. Only when the correct flag is set will we set the max within `Limits` or else we will leave it `undefined`. --- lib/std/wasm.zig | 16 +++++++++++++++- src/link/Wasm.zig | 25 +++++++++++++++++-------- src/link/Wasm/Object.zig | 11 ++++++++--- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/lib/std/wasm.zig b/lib/std/wasm.zig index 25a0bb7abf..35d0ba4866 100644 --- a/lib/std/wasm.zig +++ b/lib/std/wasm.zig @@ -551,8 +551,22 @@ test "Wasm - valtypes" { /// Limits classify the size range of resizeable storage associated with memory types and table types. pub const Limits = struct { + flags: u8, min: u32, - max: ?u32, + max: u32, + + pub const Flags = enum(u8) { + WASM_LIMITS_FLAG_HAS_MAX = 0x1, + WASM_LIMITS_FLAG_IS_SHARED = 0x2, + }; + + pub fn hasFlag(limits: Limits, flag: Flags) bool { + return limits.flags & @enumToInt(flag) != 0; + } + + pub fn setFlag(limits: *Limits, flag: Flags) void { + limits.flags |= @enumToInt(flag); + } }; /// Initialization expressions are used to set the initial value on an object diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 287f880d92..50846eecf1 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -111,7 +111,11 @@ functions: std.AutoArrayHashMapUnmanaged(struct { file: ?u16, index: u32 }, std. /// Output global section wasm_globals: std.ArrayListUnmanaged(std.wasm.Global) = .{}, /// Memory section -memories: std.wasm.Memory = .{ .limits = .{ .min = 0, .max = null } }, +memories: std.wasm.Memory = .{ .limits = .{ + .min = 0, + .max = undefined, + .flags = 0, +} }, /// Output table section tables: std.ArrayListUnmanaged(std.wasm.Table) = .{}, /// Output export section @@ -396,7 +400,7 @@ pub fn openPath(allocator: Allocator, sub_path: []const u8, options: link.Option const loc = try wasm_bin.createSyntheticSymbol("__indirect_function_table", .table); const symbol = loc.getSymbol(wasm_bin); const table: std.wasm.Table = .{ - .limits = .{ .min = 0, .max = null }, // will be overwritten during `mapFunctionTable` + .limits = .{ .flags = 0, .min = 0, .max = undefined }, // will be overwritten during `mapFunctionTable` .reftype = .funcref, }; if (options.output_mode == .Obj or options.import_table) { @@ -1524,7 +1528,7 @@ fn mapFunctionTable(wasm: *Wasm) void { const sym_loc = wasm.findGlobalSymbol("__indirect_function_table").?; const symbol = sym_loc.getSymbol(wasm); const table = &wasm.tables.items[symbol.index - wasm.imported_tables_count]; - table.limits = .{ .min = index, .max = index }; + table.limits = .{ .min = index, .max = index, .flags = 0x1 }; } } @@ -2236,8 +2240,9 @@ fn setupMemory(wasm: *Wasm) !void { if (mem.eql(u8, entry.key_ptr.*, ".tdata")) { if (wasm.findGlobalSymbol("__tls_base")) |loc| { const sym = loc.getSymbol(wasm); - sym.index = try wasm.globals.append(wasm.base.allocator, wasm.imports.globalCount, .{ - .global_type = .{ .valtype = .i32_const, .mutable = false }, + sym.index = @intCast(u32, wasm.wasm_globals.items.len) + wasm.imported_globals_count; + try wasm.wasm_globals.append(wasm.base.allocator, .{ + .global_type = .{ .valtype = .i32, .mutable = false }, .init = .{ .i32_const = @intCast(i32, memory_ptr) }, }); } @@ -2305,6 +2310,10 @@ fn setupMemory(wasm: *Wasm) !void { return error.MemoryTooBig; } wasm.memories.limits.max = @intCast(u32, max_memory / page_size); + wasm.memories.limits.setFlag(.WASM_LIMITS_FLAG_HAS_MAX); + if (wasm.base.options.shared_memory) { + wasm.memories.limits.setFlag(.WASM_LIMITS_FLAG_IS_SHARED); + } log.debug("Maximum memory pages: {?d}", .{wasm.memories.limits.max}); } } @@ -3517,10 +3526,10 @@ fn emitNameSubsection(wasm: *Wasm, section_id: std.wasm.NameSubsection, names: a } fn emitLimits(writer: anytype, limits: std.wasm.Limits) !void { - try leb.writeULEB128(writer, @boolToInt(limits.max != null)); + try writer.writeByte(limits.flags); try leb.writeULEB128(writer, limits.min); - if (limits.max) |max| { - try leb.writeULEB128(writer, max); + if (limits.hasFlag(.WASM_LIMITS_FLAG_HAS_MAX)) { + try leb.writeULEB128(writer, limits.max); } } diff --git a/src/link/Wasm/Object.zig b/src/link/Wasm/Object.zig index 4b918064c1..92336efbf2 100644 --- a/src/link/Wasm/Object.zig +++ b/src/link/Wasm/Object.zig @@ -852,12 +852,17 @@ fn readEnum(comptime T: type, reader: anytype) !T { } fn readLimits(reader: anytype) !std.wasm.Limits { - const flags = try readLeb(u1, reader); + const flags = try reader.readByte(); const min = try readLeb(u32, reader); - return std.wasm.Limits{ + var limits: std.wasm.Limits = .{ + .flags = flags, .min = min, - .max = if (flags == 0) null else try readLeb(u32, reader), + .max = undefined, }; + if (limits.hasFlag(.WASM_LIMITS_FLAG_HAS_MAX)) { + limits.max = try readLeb(u32, reader); + } + return limits; } fn readInit(reader: anytype) !std.wasm.InitExpression { From fb9d3cd50e9a6d112277d7de158ef857162c01d9 Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Mon, 13 Mar 2023 19:30:33 +0100 Subject: [PATCH 3/9] wasm-linker: feature verifiction for shared-mem When the user enables shared-memory, we must ensure the linked objects have the 'atomics' and 'bulk-memory' features allowed. --- src/link/Wasm.zig | 41 ++++++++++++++++++++++++++++++++++++---- src/link/Wasm/Object.zig | 4 ++-- src/link/Wasm/types.zig | 12 ++++++++---- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 50846eecf1..5792e57b89 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -795,6 +795,8 @@ fn validateFeatures( // when false, we fail linking. We only verify this after a loop to catch all invalid features. var valid_feature_set = true; + // will be set to true when there's any TLS segment found in any of the object files + var has_tls = false; // When the user has given an explicit list of features to enable, // we extract them and insert each into the 'allowed' list. @@ -825,6 +827,12 @@ fn validateFeatures( }, } } + + for (object.segment_info) |segment| { + if (segment.isTLS()) { + has_tls = true; + } + } } // when we infer the features, we allow each feature found in the 'used' set @@ -836,7 +844,7 @@ fn validateFeatures( allowed[used_index] = is_enabled; emit_features_count.* += @boolToInt(is_enabled); } else if (is_enabled and !allowed[used_index]) { - log.err("feature '{s}' not allowed, but used by linked object", .{(@intToEnum(types.Feature.Tag, used_index)).toString()}); + log.err("feature '{}' not allowed, but used by linked object", .{@intToEnum(types.Feature.Tag, used_index)}); log.err(" defined in '{s}'", .{wasm.objects.items[used_set >> 1].name}); valid_feature_set = false; } @@ -846,6 +854,30 @@ fn validateFeatures( return error.InvalidFeatureSet; } + if (wasm.base.options.shared_memory) { + const disallowed_feature = disallowed[@enumToInt(types.Feature.Tag.shared_mem)]; + if (@truncate(u1, disallowed_feature) != 0) { + log.err( + "shared-memory is disallowed by '{s}' because it wasn't compiled with 'atomics' and 'bulk-memory' features enabled", + .{wasm.objects.items[disallowed_feature >> 1].name}, + ); + valid_feature_set = false; + } + + for ([_]types.Feature.Tag{ .atomics, .bulk_memory }) |feature| { + if (!allowed[@enumToInt(feature)]) { + log.err("feature '{}' is not used but is required for shared-memory", .{feature}); + } + } + } + + if (has_tls) { + for ([_]types.Feature.Tag{ .atomics, .bulk_memory }) |feature| { + if (!allowed[@enumToInt(feature)]) { + log.err("feature '{}' is not used but is required for thread-local storage", .{feature}); + } + } + } // For each linked object, validate the required and disallowed features for (wasm.objects.items) |object| { var object_used_features = [_]bool{false} ** known_features_count; @@ -854,7 +886,7 @@ fn validateFeatures( // from here a feature is always used const disallowed_feature = disallowed[@enumToInt(feature.tag)]; if (@truncate(u1, disallowed_feature) != 0) { - log.err("feature '{s}' is disallowed, but used by linked object", .{feature.tag.toString()}); + log.err("feature '{}' is disallowed, but used by linked object", .{feature.tag}); log.err(" disallowed by '{s}'", .{wasm.objects.items[disallowed_feature >> 1].name}); log.err(" used in '{s}'", .{object.name}); valid_feature_set = false; @@ -867,7 +899,7 @@ fn validateFeatures( for (required, 0..) |required_feature, feature_index| { const is_required = @truncate(u1, required_feature) != 0; if (is_required and !object_used_features[feature_index]) { - log.err("feature '{s}' is required but not used in linked object", .{(@intToEnum(types.Feature.Tag, feature_index)).toString()}); + log.err("feature '{}' is required but not used in linked object", .{@intToEnum(types.Feature.Tag, feature_index)}); log.err(" required by '{s}'", .{wasm.objects.items[required_feature >> 1].name}); log.err(" missing in '{s}'", .{object.name}); valid_feature_set = false; @@ -3432,7 +3464,8 @@ fn emitFeaturesSection(binary_bytes: *std.ArrayList(u8), enabled_features: []con if (enabled) { const feature: types.Feature = .{ .prefix = .used, .tag = @intToEnum(types.Feature.Tag, feature_index) }; try leb.writeULEB128(writer, @enumToInt(feature.prefix)); - const string = feature.tag.toString(); + var buf: [100]u8 = undefined; + const string = try std.fmt.bufPrint(&buf, "{}", .{feature.tag}); try leb.writeULEB128(writer, @intCast(u32, string.len)); try writer.writeAll(string); } diff --git a/src/link/Wasm/Object.zig b/src/link/Wasm/Object.zig index 92336efbf2..c2243bdf10 100644 --- a/src/link/Wasm/Object.zig +++ b/src/link/Wasm/Object.zig @@ -601,8 +601,8 @@ fn Parser(comptime ReaderType: type) type { }); for (relocations) |*relocation| { - const rel_type = try leb.readULEB128(u8, reader); - const rel_type_enum = @intToEnum(types.Relocation.RelocationType, rel_type); + const rel_type = try reader.readByte(); + const rel_type_enum = std.meta.intToEnum(types.Relocation.RelocationType, rel_type) catch return error.MalformedSection; relocation.* = .{ .relocation_type = rel_type_enum, .offset = try leb.readULEB128(u32, reader), diff --git a/src/link/Wasm/types.zig b/src/link/Wasm/types.zig index a65e352c46..d19f32b862 100644 --- a/src/link/Wasm/types.zig +++ b/src/link/Wasm/types.zig @@ -50,6 +50,8 @@ pub const Relocation = struct { .R_WASM_MEMORY_ADDR_LEB64, .R_WASM_MEMORY_ADDR_SLEB64, .R_WASM_MEMORY_ADDR_I64, + .R_WASM_MEMORY_ADDR_TLS_SLEB, + .R_WASM_MEMORY_ADDR_TLS_SLEB64, .R_WASM_FUNCTION_OFFSET_I32, .R_WASM_SECTION_OFFSET_I32, => true, @@ -218,8 +220,10 @@ pub const Feature = struct { return @intToEnum(Tag, @enumToInt(feature)); } - pub fn toString(tag: Tag) []const u8 { - return switch (tag) { + pub fn format(tag: Tag, comptime fmt: []const u8, opt: std.fmt.FormatOptions, writer: anytype) !void { + _ = fmt; + _ = opt; + try writer.writeAll(switch (tag) { .atomics => "atomics", .bulk_memory => "bulk-memory", .exception_handling => "exception-handling", @@ -233,7 +237,7 @@ pub const Feature = struct { .simd128 => "simd128", .tail_call => "tail-call", .shared_mem => "shared-mem", - }; + }); } }; @@ -246,7 +250,7 @@ pub const Feature = struct { pub fn format(feature: Feature, comptime fmt: []const u8, opt: std.fmt.FormatOptions, writer: anytype) !void { _ = opt; _ = fmt; - try writer.print("{c} {s}", .{ feature.prefix, feature.tag.toString() }); + try writer.print("{c} {}", .{ feature.prefix, feature.tag }); } }; From 00af3a79aede098c60eeff38ad72fa399b9d3ccf Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Tue, 14 Mar 2023 19:31:45 +0100 Subject: [PATCH 4/9] wasm-linker: emit 'data count' & segment flags When linking with shared-memory enabled, we must ensure to emit the "data count" section as well as emit the correct segment flags to tell the runtime/loader that each segment is passive. This is required as we don't emit the offsets for such segments but instead initialize each segment (for each thread) during runtime. --- src/link/Wasm.zig | 54 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 5792e57b89..720e10fe03 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -180,6 +180,16 @@ pub const Segment = struct { alignment: u32, size: u32, offset: u32, + flags: u32, + + pub const Flag = enum(u32) { + WASM_DATA_SEGMENT_IS_PASSIVE = 0x01, + WASM_DATA_SEGMENT_HAS_MEMINDEX = 0x02, + }; + + pub fn isPassive(segment: Segment) bool { + return segment.flags & @enumToInt(Flag.WASM_DATA_SEGMENT_IS_PASSIVE) != 0; + } }; pub const Export = struct { @@ -1673,6 +1683,7 @@ fn parseAtom(wasm: *Wasm, atom_index: Atom.Index, kind: Kind) !void { .alignment = atom.alignment, .size = atom.size, .offset = 0, + .flags = 0, }); } @@ -1711,10 +1722,15 @@ fn parseAtom(wasm: *Wasm, atom_index: Atom.Index, kind: Kind) !void { break :result index; } else { const index = @intCast(u32, wasm.segments.items.len); + var flags: u32 = 0; + if (wasm.base.options.shared_memory) { + flags |= @enumToInt(Segment.Flag.WASM_DATA_SEGMENT_IS_PASSIVE); + } try wasm.segments.append(wasm.base.allocator, .{ .alignment = atom.alignment, .size = 0, .offset = 0, + .flags = flags, }); gop.value_ptr.* = index; @@ -2365,7 +2381,16 @@ pub fn getMatchingSegment(wasm: *Wasm, object_index: u16, relocatable_index: u32 const result = try wasm.data_segments.getOrPut(wasm.base.allocator, segment_info.outputName(merge_segment)); if (!result.found_existing) { result.value_ptr.* = index; - try wasm.appendDummySegment(); + var flags: u32 = 0; + if (wasm.base.options.shared_memory) { + flags |= @enumToInt(Segment.Flag.WASM_DATA_SEGMENT_IS_PASSIVE); + } + try wasm.segments.append(wasm.base.allocator, .{ + .alignment = 1, + .size = 0, + .offset = 0, + .flags = flags, + }); return index; } else return result.value_ptr.*; }, @@ -2439,6 +2464,7 @@ fn appendDummySegment(wasm: *Wasm) !void { .alignment = 1, .size = 0, .offset = 0, + .flags = 0, }); } @@ -3147,6 +3173,19 @@ fn writeToFile( section_count += 1; } + // When the shared-memory option is enabled, we *must* emit the 'data count' section. + const data_segments_count = wasm.data_segments.count() - @boolToInt(wasm.data_segments.contains(".bss") and import_memory); + if (data_segments_count != 0 and wasm.base.options.shared_memory) { + const header_offset = try reserveVecSectionHeader(&binary_bytes); + try writeVecSectionHeader( + binary_bytes.items, + header_offset, + .data_count, + @intCast(u32, binary_bytes.items.len - header_offset - header_size), + @intCast(u32, data_segments_count), + ); + } + // Code section var code_section_size: u32 = 0; if (wasm.code_section_index) |code_index| { @@ -3197,7 +3236,7 @@ fn writeToFile( } // Data section - if (wasm.data_segments.count() != 0) { + if (data_segments_count != 0) { const header_offset = try reserveVecSectionHeader(&binary_bytes); var it = wasm.data_segments.iterator(); @@ -3212,10 +3251,15 @@ fn writeToFile( segment_count += 1; var atom_index = wasm.atoms.get(segment_index).?; - // flag and index to memory section (currently, there can only be 1 memory section in wasm) - try leb.writeULEB128(binary_writer, @as(u32, 0)); + try leb.writeULEB128(binary_writer, segment.flags); + if (segment.flags & @enumToInt(Wasm.Segment.Flag.WASM_DATA_SEGMENT_HAS_MEMINDEX) != 0) { + try leb.writeULEB128(binary_writer, @as(u32, 0)); // memory is always index 0 as we only have 1 memory entry + } + // when a segment is passive, it's initialized during runtime. + if (!segment.isPassive()) { + try emitInit(binary_writer, .{ .i32_const = @bitCast(i32, segment.offset) }); + } // offset into data section - try emitInit(binary_writer, .{ .i32_const = @bitCast(i32, segment.offset) }); try leb.writeULEB128(binary_writer, segment.size); // fill in the offset table and the data segments From ff28c8b60080b24ae7d5e9d485b6aa47e8c8de9c Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Tue, 14 Mar 2023 19:51:30 +0100 Subject: [PATCH 5/9] wasm-linker: create TLS symbols Initialize TLS symbols when shared-memory is enabled. Those symbols will be called by synthetic functions created by the linker. (TODO). --- src/link/Wasm.zig | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 720e10fe03..d66facb912 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -443,6 +443,30 @@ pub fn openPath(allocator: Allocator, sub_path: []const u8, options: link.Option // at the end during `initializeCallCtorsFunction`. } + // shared-memory symbols for TLS support + if (wasm_bin.base.options.shared_memory) { + { + const loc = try wasm_bin.createSyntheticSymbol("__tls_base", .global); + const symbol = loc.getSymbol(wasm_bin); + symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN); + } + { + const loc = try wasm_bin.createSyntheticSymbol("__tls_size", .global); + const symbol = loc.getSymbol(wasm_bin); + symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN); + } + { + const loc = try wasm_bin.createSyntheticSymbol("__tls_align", .global); + const symbol = loc.getSymbol(wasm_bin); + symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN); + } + { + const loc = try wasm_bin.createSyntheticSymbol("__wasm_tls_init", .function); + const symbol = loc.getSymbol(wasm_bin); + symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN); + } + } + // if (!options.strip and options.module != null) { // wasm_bin.dwarf = Dwarf.init(allocator, &wasm_bin.base, options.target); // try wasm_bin.initDebugSections(); @@ -2286,12 +2310,28 @@ fn setupMemory(wasm: *Wasm) !void { // set TLS-related symbols if (mem.eql(u8, entry.key_ptr.*, ".tdata")) { - if (wasm.findGlobalSymbol("__tls_base")) |loc| { + if (wasm.findGlobalSymbol("__tls_size")) |loc| { const sym = loc.getSymbol(wasm); sym.index = @intCast(u32, wasm.wasm_globals.items.len) + wasm.imported_globals_count; try wasm.wasm_globals.append(wasm.base.allocator, .{ .global_type = .{ .valtype = .i32, .mutable = false }, - .init = .{ .i32_const = @intCast(i32, memory_ptr) }, + .init = .{ .i32_const = @intCast(i32, segment.size) }, + }); + } + if (wasm.findGlobalSymbol("__tls_align")) |loc| { + const sym = loc.getSymbol(wasm); + sym.index = @intCast(u32, wasm.wasm_globals.items.len) + wasm.imported_globals_count; + try wasm.wasm_globals.append(wasm.base.allocator, .{ + .global_type = .{ .valtype = .i32, .mutable = false }, + .init = .{ .i32_const = @intCast(i32, segment.alignment) }, + }); + } + if (wasm.findGlobalSymbol("__tls_base")) |loc| { + const sym = loc.getSymbol(wasm); + sym.index = @intCast(u32, wasm.wasm_globals.items.len) + wasm.imported_globals_count; + try wasm.wasm_globals.append(wasm.base.allocator, .{ + .global_type = .{ .valtype = .i32, .mutable = wasm.base.options.shared_memory }, + .init = .{ .i32_const = if (wasm.base.options.shared_memory) @as(u32, 0) else @intCast(i32, memory_ptr) }, }); } } From 9d13c2257dcae11d9bc69035e55c33a7dda14a2b Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Wed, 15 Mar 2023 19:27:55 +0100 Subject: [PATCH 6/9] wasm-linker: implement TLS initialization function Implements the TLS initialization function. This is a synthetic function created by the linker. This will only be created when shared-memory is enabled. This function will be called during thread creation, if there's any TLS symbols, which will initialize the TLS segment using the bulk-memory feature. --- src/link/Wasm.zig | 75 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index d66facb912..5d4ab2961b 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -1990,10 +1990,23 @@ fn initializeCallCtorsFunction(wasm: *Wasm) !void { try writer.writeByte(std.wasm.opcode(.end)); } - const loc = wasm.findGlobalSymbol("__wasm_call_ctors").?; + try wasm.createSyntheticFunction( + "__wasm_call_ctors", + std.wasm.Type{ .params = &.{}, .returns = &.{} }, + &function_body, + ); +} + +fn createSyntheticFunction( + wasm: *Wasm, + symbol_name: []const u8, + func_ty: std.wasm.Type, + function_body: *std.ArrayList(u8), +) !void { + const loc = wasm.findGlobalSymbol(symbol_name) orelse + try wasm.createSyntheticSymbol(symbol_name, .function); const symbol = loc.getSymbol(wasm); - // create type (() -> nil) as we do not have any parameters or return value. - const ty_index = try wasm.putOrGetFuncType(.{ .params = &[_]std.wasm.Valtype{}, .returns = &[_]std.wasm.Valtype{} }); + const ty_index = try wasm.putOrGetFuncType(func_ty); // create function with above type const func_index = wasm.imported_functions_count + @intCast(u32, wasm.functions.count()); try wasm.functions.putNoClobber( @@ -2025,6 +2038,60 @@ fn initializeCallCtorsFunction(wasm: *Wasm) !void { atom.offset = prev_atom.offset + prev_atom.size; } +fn initializeTLSFunction(wasm: *Wasm) !void { + if (!wasm.base.options.shared_memory) return; + + var function_body = std.ArrayList(u8).init(wasm.base.allocator); + defer function_body.deinit(); + const writer = function_body.writer(); + + // locals + try writer.writeByte(0); + + // If there's a TLS segment, initialize it during runtime using the bulk-memory feature + if (wasm.data_segments.getIndex(".tdata")) |data_index| { + const segment_index = wasm.data_segments.entries.items(.value)[data_index]; + const segment = wasm.segments.items[segment_index]; + + const param_local: u32 = 0; + + try writer.writeByte(std.wasm.opcode(.local_get)); + try leb.writeULEB128(writer, param_local); + + const tls_base_loc = wasm.findGlobalSymbol("__tls_base").?; + try writer.writeByte(std.wasm.opcode(.global_get)); + try leb.writeULEB128(writer, tls_base_loc.getSymbol(wasm).index); + + // load stack values for the bulk-memory operation + { + try writer.writeByte(std.wasm.opcode(.local_get)); + try leb.writeULEB128(writer, param_local); + + try writer.writeByte(std.wasm.opcode(.i32_const)); + try leb.writeULEB128(writer, @as(u32, 0)); //segment offset + + try writer.writeByte(std.wasm.opcode(.i32_const)); + try leb.writeULEB128(writer, @as(u32, segment.size)); //segment offset + } + + // perform the bulk-memory operation to initialize the data segment + try writer.writeByte(std.wasm.opcode(.prefixed)); + try leb.writeULEB128(writer, @enumToInt(std.wasm.PrefixedOpcode.memory_init)); + // segment immediate + try leb.writeULEB128(writer, @intCast(u32, data_index)); + // memory index immediate (always 0) + try leb.writeULEB128(writer, @as(u32, 0)); + } + + try writer.writeByte(std.wasm.opcode(.end)); + + try wasm.createSyntheticFunction( + "__wasm_init_tls", + std.wasm.Type{ .params = &.{.i32}, .returns = &.{} }, + &function_body, + ); +} + fn setupImports(wasm: *Wasm) !void { log.debug("Merging imports", .{}); var discarded_it = wasm.discarded.keyIterator(); @@ -2872,6 +2939,7 @@ fn linkWithZld(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) l try wasm.mergeSections(); try wasm.mergeTypes(); try wasm.initializeCallCtorsFunction(); + try wasm.initializeTLSFunction(); try wasm.setupExports(); try wasm.writeToFile(enabled_features, emit_features_count, arena); @@ -2991,6 +3059,7 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod try wasm.mergeSections(); try wasm.mergeTypes(); try wasm.initializeCallCtorsFunction(); + try wasm.initializeTLSFunction(); try wasm.setupExports(); try wasm.writeToFile(enabled_features, emit_features_count, arena); } From 9fce1df4cdab57951137c0da2b44fbe6da2442f2 Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Fri, 17 Mar 2023 06:32:37 +0100 Subject: [PATCH 7/9] wasm-linker: implement runtime TLS relocations --- src/link/Wasm.zig | 63 ++++++++++++++++++++++++++++++++++++++++ src/link/Wasm/Object.zig | 28 ++++++++++++++---- src/link/Wasm/types.zig | 12 -------- 3 files changed, 86 insertions(+), 17 deletions(-) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 5d4ab2961b..eaaabcc89a 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -139,6 +139,8 @@ archives: std.ArrayListUnmanaged(Archive) = .{}, /// A map of global names (read: offset into string table) to their symbol location globals: std.AutoHashMapUnmanaged(u32, SymbolLoc) = .{}, +/// The list of GOT symbols and their location +got_symbols: std.ArrayListUnmanaged(SymbolLoc) = .{}, /// Maps discarded symbols and their positions to the location of the symbol /// it was resolved to discarded: std.AutoHashMapUnmanaged(SymbolLoc, SymbolLoc) = .{}, @@ -635,6 +637,15 @@ fn parseArchive(wasm: *Wasm, path: []const u8, force_load: bool) !bool { return true; } +fn requiresTLSReloc(wasm: *const Wasm) bool { + for (wasm.got_symbols.items) |loc| { + if (loc.getSymbol(wasm).isTLS()) { + return true; + } + } + return false; +} + fn resolveSymbolsInObject(wasm: *Wasm, object_index: u16) !void { const object: Object = wasm.objects.items[object_index]; log.debug("Resolving symbols in object: '{s}'", .{object.name}); @@ -813,6 +824,48 @@ fn resolveSymbolsInArchives(wasm: *Wasm) !void { } } +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. + if (!wasm.base.options.shared_memory or !wasm.requiresTLSReloc()) { + return; + } + + // const loc = try wasm.createSyntheticSymbol("__wasm_apply_global_tls_relocs"); + var function_body = std.ArrayList(u8).init(wasm.base.allocator); + defer function_body.deinit(); + const writer = function_body.writer(); + + // locals (we have none) + try writer.writeByte(0); + for (wasm.got_symbols.items, 0..) |got_loc, got_index| { + const sym: *Symbol = got_loc.getSymbol(wasm); + if (!sym.isTLS()) continue; // only relocate TLS symbols + if (sym.tag == .data and sym.isDefined()) { + // get __tls_base + try writer.writeByte(std.wasm.opcode(.global_get)); + try leb.writeULEB128(writer, wasm.findGlobalSymbol("__tls_base").?.getSymbol(wasm).index); + + // add the virtual address of the symbol + try writer.writeByte(std.wasm.opcode(.i32_const)); + try leb.writeULEB128(writer, sym.virtual_address); + } else if (sym.tag == .function) { + @panic("TODO: relocate GOT entry of function"); + } else continue; + + try writer.writeByte(std.wasm.opcode(.i32_add)); + try writer.writeByte(std.wasm.opcode(.global_set)); + try leb.writeULEB128(writer, wasm.imported_globals_count + @intCast(u32, wasm.wasm_globals.items.len + got_index)); + } + try writer.writeByte(std.wasm.opcode(.end)); + + try wasm.createSyntheticFunction( + "__wasm_apply_global_tls_relocs", + std.wasm.Type{ .params = &.{}, .returns = &.{} }, + &function_body, + ); +} + fn validateFeatures( wasm: *const Wasm, to_emit: *[@typeInfo(types.Feature.Tag).Enum.fields.len]bool, @@ -2083,6 +2136,14 @@ fn initializeTLSFunction(wasm: *Wasm) !void { try leb.writeULEB128(writer, @as(u32, 0)); } + // If we have to perform any TLS relocations, call the corresponding function + // which performs all runtime TLS relocations. This is a synthetic function, + // generated by the linker. + if (wasm.findGlobalSymbol("__wasm_apply_global_tls_relocs")) |loc| { + try writer.writeByte(std.wasm.opcode(.call)); + try leb.writeULEB128(writer, loc.getSymbol(wasm).index); + } + try writer.writeByte(std.wasm.opcode(.end)); try wasm.createSyntheticFunction( @@ -2939,6 +3000,7 @@ fn linkWithZld(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Node) l try wasm.mergeSections(); try wasm.mergeTypes(); try wasm.initializeCallCtorsFunction(); + try wasm.setupTLSRelocationsFunction(); try wasm.initializeTLSFunction(); try wasm.setupExports(); try wasm.writeToFile(enabled_features, emit_features_count, arena); @@ -3059,6 +3121,7 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod try wasm.mergeSections(); try wasm.mergeTypes(); try wasm.initializeCallCtorsFunction(); + try wasm.setupTLSRelocationsFunction(); try wasm.initializeTLSFunction(); try wasm.setupExports(); try wasm.writeToFile(enabled_features, emit_features_count, arena); diff --git a/src/link/Wasm/Object.zig b/src/link/Wasm/Object.zig index c2243bdf10..363648971a 100644 --- a/src/link/Wasm/Object.zig +++ b/src/link/Wasm/Object.zig @@ -930,11 +930,29 @@ pub fn parseIntoAtoms(object: *Object, gpa: Allocator, object_index: u16, wasm_b reloc.offset -= relocatable_data.offset; try atom.relocs.append(gpa, reloc); - if (relocation.isTableIndex()) { - try wasm_bin.function_table.put(gpa, .{ - .file = object_index, - .index = relocation.index, - }, 0); + switch (relocation.relocation_type) { + .R_WASM_TABLE_INDEX_I32, + .R_WASM_TABLE_INDEX_I64, + .R_WASM_TABLE_INDEX_SLEB, + .R_WASM_TABLE_INDEX_SLEB64, + => { + try wasm_bin.function_table.put(gpa, .{ + .file = object_index, + .index = relocation.index, + }, 0); + }, + .R_WASM_GLOBAL_INDEX_I32, + .R_WASM_GLOBAL_INDEX_LEB, + => { + const sym = object.symtable[relocation.index]; + if (sym.tag != .global) { + try wasm_bin.got_symbols.append( + wasm_bin.base.allocator, + .{ .file = object_index, .index = relocation.index }, + ); + } + }, + else => {}, } } } diff --git a/src/link/Wasm/types.zig b/src/link/Wasm/types.zig index d19f32b862..801c25e9d9 100644 --- a/src/link/Wasm/types.zig +++ b/src/link/Wasm/types.zig @@ -71,18 +71,6 @@ pub const Relocation = struct { }; } - /// Returns true when the relocation represents a table index relocatable - pub fn isTableIndex(self: Relocation) bool { - return switch (self.relocation_type) { - .R_WASM_TABLE_INDEX_I32, - .R_WASM_TABLE_INDEX_I64, - .R_WASM_TABLE_INDEX_SLEB, - .R_WASM_TABLE_INDEX_SLEB64, - => true, - else => false, - }; - } - pub fn format(self: Relocation, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { _ = fmt; _ = options; From 09d6938df9246bac20e84f5512743e96bccdfa3d Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Fri, 17 Mar 2023 07:09:01 +0100 Subject: [PATCH 8/9] wasm: add atomics opcodes and refactoring This adds the atomic opcodes for the Threads proposal to the WebAssembly specification: https://github.com/WebAssembly/threads PrefixedOpcode has been renamed to MiscOpcode as there's multiple types of prefixed opcodes. This naming is similar to other tools such as LLVM. As we now use the 0xFE prefix, we moved the function_index MIR instruction as it was occupying the same value. This commit includes renaming all related opcodes. --- lib/std/wasm.zig | 92 ++++++++++++++++++++++++++++++++++++++- src/arch/wasm/CodeGen.zig | 14 +++--- src/arch/wasm/Emit.zig | 18 +++++--- src/arch/wasm/Mir.zig | 23 ++++++---- src/link/Wasm.zig | 4 +- 5 files changed, 126 insertions(+), 25 deletions(-) diff --git a/lib/std/wasm.zig b/lib/std/wasm.zig index 35d0ba4866..d54e998b67 100644 --- a/lib/std/wasm.zig +++ b/lib/std/wasm.zig @@ -189,7 +189,9 @@ pub const Opcode = enum(u8) { i64_extend16_s = 0xC3, i64_extend32_s = 0xC4, - prefixed = 0xFC, + misc_prefix = 0xFC, + simd_prefix = 0xFD, + atomics_prefix = 0xFE, _, }; @@ -217,7 +219,7 @@ test "Wasm - opcodes" { /// Opcodes that require a prefix `0xFC` /// Each opcode represents a varuint32, meaning /// they are encoded as leb128 in binary. -pub const PrefixedOpcode = enum(u32) { +pub const MiscOpcode = enum(u32) { i32_trunc_sat_f32_s = 0x00, i32_trunc_sat_f32_u = 0x01, i32_trunc_sat_f64_s = 0x02, @@ -239,6 +241,12 @@ pub const PrefixedOpcode = enum(u32) { _, }; +/// Returns the integer value of an `MiscOpcode`. Used by the Zig compiler +/// to write instructions to the wasm binary file +pub fn miscOpcode(op: MiscOpcode) u32 { + return @enumToInt(op); +} + /// Simd opcodes that require a prefix `0xFD`. /// Each opcode represents a varuint32, meaning /// they are encoded as leb128 in binary. @@ -510,6 +518,86 @@ pub fn simdOpcode(op: SimdOpcode) u32 { return @enumToInt(op); } +/// Simd opcodes that require a prefix `0xFE`. +/// Each opcode represents a varuint32, meaning +/// they are encoded as leb128 in binary. +pub const AtomicsOpcode = enum(u32) { + memory_atomic_notify = 0x00, + memory_atomic_wait32 = 0x01, + memory_atomic_wait64 = 0x02, + atomic_fence = 0x03, + i32_atomic_load = 0x10, + i64_atomic_load = 0x11, + i32_atomic_load8_u = 0x12, + i32_atomic_load16_u = 0x13, + i64_atomic_load8_u = 0x14, + i64_atomic_load16_u = 0x15, + i64_atomic_load32_u = 0x16, + i32_atomic_store = 0x17, + i64_atomic_store = 0x18, + i32_atomic_store8 = 0x19, + i32_atomic_store16 = 0x1A, + i64_atomic_store8 = 0x1B, + i64_atomic_store16 = 0x1C, + i64_atomic_store32 = 0x1D, + i32_atomic_rmw_add = 0x1E, + i64_atomic_rmw_add = 0x1F, + i32_atomic_rmw8_add_u = 0x20, + i32_atomic_rmw16_add_u = 0x21, + i64_atomic_rmw8_add_u = 0x22, + i64_atomic_rmw16_add_u = 0x23, + i64_atomic_rmw32_add_u = 0x24, + i32_atomic_rmw_sub = 0x25, + i64_atomic_rmw_sub = 0x26, + i32_atomic_rmw8_sub_u = 0x27A, + i32_atomic_rmw16_sub_u = 0x28A, + i64_atomic_rmw8_sub_u = 0x29A, + i64_atomic_rmw16_sub_u = 0x2A, + i64_atomic_rmw32_sub_u = 0x2B, + i32_atomic_rmw_and = 0x2C, + i64_atomic_rmw_and = 0x2D, + i32_atomic_rmw8_and_u = 0x2E, + i32_atomic_rmw16_and_u = 0x2F, + i64_atomic_rmw8_and_u = 0x30, + i64_atomic_rmw16_and_u = 0x31, + i64_atomic_rmw32_and_u = 0x32, + i32_atomic_rmw_or = 0x33, + i64_atomic_rmw_or = 0x34, + i32_atomic_rmw8_or_u = 0x35, + i32_atomic_rmw16_or_u = 0x36, + i64_atomic_rmw8_or_u = 0x37, + i64_atomic_rmw16_or_u = 0x38, + i64_atomic_rmw32_or_u = 0x39, + i32_atomic_rmw_xor = 0x3A, + i64_atomic_rmw_xor = 0x3B, + i32_atomic_rmw8_xor_u = 0x3C, + i32_atomic_rmw16_xor_u = 0x3D, + i64_atomic_rmw8_xor_u = 0x3E, + i64_atomic_rmw16_xor_u = 0x3F, + i64_atomic_rmw32_xor_u = 0x40, + i32_atomic_rmw_xchg = 0x41, + i64_atomic_rmw_xchg = 0x42, + i32_atomic_rmw8_xchg_u = 0x43, + i32_atomic_rmw16_xchg_u = 0x44, + i64_atomic_rmw8_xchg_u = 0x45, + i64_atomic_rmw16_xchg_u = 0x46, + i64_atomic_rmw32_xchg_u = 0x47, + + i32_atomic_rmw_cmpxchg = 0x48, + i64_atomic_rmw_cmpxchg = 0x49, + i32_atomic_rmw8_cmpxchg_u = 0x4A, + i32_atomic_rmw16_cmpxchg_u = 0x4B, + i64_atomic_rmw8_cmpxchg_u = 0x4C, + i64_atomic_rmw16_cmpxchg_u = 0x4D, + i64_atomic_rmw32_cmpxchg_u = 0x4E, +}; + +/// Returns the integer value of an `AtomicsOpcode`. Used by the Zig compiler +/// to write instructions to the wasm binary file +pub fn atomicsOpcode(op: AtomicsOpcode) u32 { + return @enumToInt(op); +} + /// Enum representing all Wasm value types as per spec: /// https://webassembly.github.io/spec/core/binary/types.html pub const Valtype = enum(u8) { diff --git a/src/arch/wasm/CodeGen.zig b/src/arch/wasm/CodeGen.zig index e79129ddb8..c05f07a602 100644 --- a/src/arch/wasm/CodeGen.zig +++ b/src/arch/wasm/CodeGen.zig @@ -895,10 +895,10 @@ fn addTag(func: *CodeGen, tag: Mir.Inst.Tag) error{OutOfMemory}!void { try func.addInst(.{ .tag = tag, .data = .{ .tag = {} } }); } -fn addExtended(func: *CodeGen, opcode: wasm.PrefixedOpcode) error{OutOfMemory}!void { +fn addExtended(func: *CodeGen, opcode: wasm.MiscOpcode) error{OutOfMemory}!void { const extra_index = @intCast(u32, func.mir_extra.items.len); try func.mir_extra.append(func.gpa, @enumToInt(opcode)); - try func.addInst(.{ .tag = .extended, .data = .{ .payload = extra_index } }); + try func.addInst(.{ .tag = .misc_prefix, .data = .{ .payload = extra_index } }); } fn addLabel(func: *CodeGen, tag: Mir.Inst.Tag, label: u32) error{OutOfMemory}!void { @@ -925,7 +925,7 @@ fn addImm128(func: *CodeGen, index: u32) error{OutOfMemory}!void { try func.mir_extra.ensureUnusedCapacity(func.gpa, 5); func.mir_extra.appendAssumeCapacity(std.wasm.simdOpcode(.v128_const)); func.mir_extra.appendSliceAssumeCapacity(@alignCast(4, mem.bytesAsSlice(u32, &simd_values))); - try func.addInst(.{ .tag = .simd, .data = .{ .payload = extra_index } }); + try func.addInst(.{ .tag = .simd_prefix, .data = .{ .payload = extra_index } }); } fn addFloat64(func: *CodeGen, float: f64) error{OutOfMemory}!void { @@ -2310,7 +2310,7 @@ fn store(func: *CodeGen, lhs: WValue, rhs: WValue, ty: Type, offset: u32) InnerE offset + lhs.offset(), ty.abiAlignment(func.target), }); - return func.addInst(.{ .tag = .simd, .data = .{ .payload = extra_index } }); + return func.addInst(.{ .tag = .simd_prefix, .data = .{ .payload = extra_index } }); }, }, .Pointer => { @@ -2420,7 +2420,7 @@ fn load(func: *CodeGen, operand: WValue, ty: Type, offset: u32) InnerError!WValu offset + operand.offset(), ty.abiAlignment(func.target), }); - try func.addInst(.{ .tag = .simd, .data = .{ .payload = extra_index } }); + try func.addInst(.{ .tag = .simd_prefix, .data = .{ .payload = extra_index } }); return WValue{ .stack = {} }; } @@ -4477,7 +4477,7 @@ fn airSplat(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { operand.offset(), elem_ty.abiAlignment(func.target), }); - try func.addInst(.{ .tag = .simd, .data = .{ .payload = extra_index } }); + try func.addInst(.{ .tag = .simd_prefix, .data = .{ .payload = extra_index } }); try func.addLabel(.local_set, result.local.value); return func.finishAir(inst, result, &.{ty_op.operand}); }, @@ -4493,7 +4493,7 @@ fn airSplat(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { try func.emitWValue(operand); const extra_index = @intCast(u32, func.mir_extra.items.len); try func.mir_extra.append(func.gpa, opcode); - try func.addInst(.{ .tag = .simd, .data = .{ .payload = extra_index } }); + try func.addInst(.{ .tag = .simd_prefix, .data = .{ .payload = extra_index } }); try func.addLabel(.local_set, result.local.value); return func.finishAir(inst, result, &.{ty_op.operand}); }, diff --git a/src/arch/wasm/Emit.zig b/src/arch/wasm/Emit.zig index 7d44d3622f..5982d3b48c 100644 --- a/src/arch/wasm/Emit.zig +++ b/src/arch/wasm/Emit.zig @@ -239,8 +239,9 @@ pub fn emitMir(emit: *Emit) InnerError!void { .i64_clz => try emit.emitTag(tag), .i64_ctz => try emit.emitTag(tag), - .extended => try emit.emitExtended(inst), - .simd => try emit.emitSimd(inst), + .misc_prefix => try emit.emitExtended(inst), + .simd_prefix => try emit.emitSimd(inst), + .atomics_prefix => try emit.emitAtomic(inst), } } } @@ -433,9 +434,9 @@ fn emitExtended(emit: *Emit, inst: Mir.Inst.Index) !void { const extra_index = emit.mir.instructions.items(.data)[inst].payload; const opcode = emit.mir.extra[extra_index]; const writer = emit.code.writer(); - try emit.code.append(0xFC); + try emit.code.append(std.wasm.opcode(.misc_prefix)); try leb128.writeULEB128(writer, opcode); - switch (@intToEnum(std.wasm.PrefixedOpcode, opcode)) { + switch (@intToEnum(std.wasm.MiscOpcode, opcode)) { // bulk-memory opcodes .data_drop => { const segment = emit.mir.extra[extra_index + 1]; @@ -472,7 +473,7 @@ fn emitSimd(emit: *Emit, inst: Mir.Inst.Index) !void { const extra_index = emit.mir.instructions.items(.data)[inst].payload; const opcode = emit.mir.extra[extra_index]; const writer = emit.code.writer(); - try emit.code.append(0xFD); + try emit.code.append(std.wasm.opcode(.simd_prefix)); try leb128.writeULEB128(writer, opcode); switch (@intToEnum(std.wasm.SimdOpcode, opcode)) { .v128_store, @@ -496,10 +497,15 @@ fn emitSimd(emit: *Emit, inst: Mir.Inst.Index) !void { .f32x4_splat, .f64x2_splat, => {}, // opcode already written - else => |tag| return emit.fail("TODO: Implement simd instruction: {s}\n", .{@tagName(tag)}), + else => |tag| return emit.fail("TODO: Implement simd instruction: {s}", .{@tagName(tag)}), } } +fn emitAtomic(emit: *Emit, inst: Mir.Inst.Index) !void { + _ = inst; + return emit.fail("TODO: Implement atomics instructions", .{}); +} + fn emitMemFill(emit: *Emit) !void { try emit.code.append(0xFC); try emit.code.append(0x0B); diff --git a/src/arch/wasm/Mir.zig b/src/arch/wasm/Mir.zig index 2d59c09e18..4c550d8637 100644 --- a/src/arch/wasm/Mir.zig +++ b/src/arch/wasm/Mir.zig @@ -87,6 +87,13 @@ pub const Inst = struct { /// /// Uses `label` call_indirect = 0x11, + /// Contains a symbol to a function pointer + /// uses `label` + /// + /// Note: This uses `0x16` as value which is reserved by the WebAssembly + /// specification but unused, meaning we must update this if the specification were to + /// use this value. + function_index = 0x16, /// Pops three values from the stack and pushes /// the first or second value dependent on the third value. /// Uses `tag` @@ -510,24 +517,24 @@ pub const Inst = struct { i64_extend16_s = 0xC3, /// Uses `tag` i64_extend32_s = 0xC4, - /// The instruction consists of an extension opcode. + /// The instruction consists of a prefixed opcode. /// The prefixed opcode can be found at payload's index. /// /// The `data` field depends on the extension instruction and /// may contain additional data. - extended = 0xFC, + misc_prefix = 0xFC, /// The instruction consists of a simd opcode. /// The actual simd-opcode is found at payload's index. /// /// The `data` field depends on the simd instruction and /// may contain additional data. - simd = 0xFD, - /// Contains a symbol to a function pointer - /// uses `label` + simd_prefix = 0xFD, + /// The instruction consists of an atomics opcode. + /// The actual atomics-opcode is found at payload's index. /// - /// Note: This uses `0xFE` as value as it is unused and not reserved - /// by the wasm specification, making it safe to use. - function_index = 0xFE, + /// The `data` field depends on the atomics instruction and + /// may contain additional data. + atomics_prefix = 0xFE, /// Contains a symbol to a memory address /// Uses `label` /// diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index eaaabcc89a..5175f760d1 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -2128,8 +2128,8 @@ fn initializeTLSFunction(wasm: *Wasm) !void { } // perform the bulk-memory operation to initialize the data segment - try writer.writeByte(std.wasm.opcode(.prefixed)); - try leb.writeULEB128(writer, @enumToInt(std.wasm.PrefixedOpcode.memory_init)); + try writer.writeByte(std.wasm.opcode(.misc_prefix)); + try leb.writeULEB128(writer, std.wasm.miscOpcode(.memory_init)); // segment immediate try leb.writeULEB128(writer, @intCast(u32, data_index)); // memory index immediate (always 0) From 4e0d7154b1a701b906f3d9c5401dc0109253971f Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sat, 18 Mar 2023 16:02:30 +0100 Subject: [PATCH 9/9] 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. --- src/link/Wasm.zig | 203 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 5175f760d1..31169b5de1 100644 --- a/src/link/Wasm.zig +++ b/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) {