Commit 3ca3fe94f4

Luuk de Gram <luuk@degram.dev>
2022-12-31 16:58:49
wasm-linker: improve indirect function table
Rather than checking for function pointers during the writing phase, we now create a synethtic symbol when a new link job has started. This means the symbol can correctly be resolved during link time with the indirect function table from other object files, ensuring we are properly performing relocations and our binary writer is now unaware of any of its logic and simply emits the table according to the symbol such as any other symbols.
1 parent e4869ee
Changed files (1)
src
src/link/Wasm.zig
@@ -335,41 +335,64 @@ pub fn openPath(allocator: Allocator, sub_path: []const u8, options: link.Option
     wasm_bin.base.file = file;
     wasm_bin.name = sub_path;
 
-    // As sym_index '0' is reserved, we use it for our stack pointer symbol
-    const sym_name = try wasm_bin.string_table.put(allocator, "__stack_pointer");
-    const symbol = try wasm_bin.symbols.addOne(allocator);
-    symbol.* = .{
-        .name = sym_name,
-        .tag = .global,
-        .flags = 0,
-        .index = 0,
-    };
-    const loc: SymbolLoc = .{ .file = null, .index = 0 };
-    try wasm_bin.resolved_symbols.putNoClobber(allocator, loc, {});
-    try wasm_bin.globals.putNoClobber(allocator, sym_name, loc);
-
-    // For object files we will import the stack pointer symbol
-    if (options.output_mode == .Obj) {
-        symbol.setUndefined(true);
-        try wasm_bin.imports.putNoClobber(
-            allocator,
-            .{ .file = null, .index = 0 },
-            .{
-                .module_name = try wasm_bin.string_table.put(allocator, wasm_bin.host_name),
-                .name = sym_name,
-                .kind = .{ .global = .{ .valtype = .i32, .mutable = true } },
-            },
-        );
-    } else {
-        symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
-        const global = try wasm_bin.wasm_globals.addOne(allocator);
-        global.* = .{
-            .global_type = .{
-                .valtype = .i32,
-                .mutable = true,
-            },
-            .init = .{ .i32_const = 0 },
+    // create stack pointer symbol
+    {
+        const loc = try wasm_bin.createSyntheticSymbol("__stack_pointer", .global);
+        const symbol = loc.getSymbol(wasm_bin);
+        // For object files we will import the stack pointer symbol
+        if (options.output_mode == .Obj) {
+            symbol.setUndefined(true);
+            symbol.index = @intCast(u32, wasm_bin.imported_globals_count);
+            wasm_bin.imported_globals_count += 1;
+            try wasm_bin.imports.putNoClobber(
+                allocator,
+                loc,
+                .{
+                    .module_name = try wasm_bin.string_table.put(allocator, wasm_bin.host_name),
+                    .name = symbol.name,
+                    .kind = .{ .global = .{ .valtype = .i32, .mutable = true } },
+                },
+            );
+        } else {
+            symbol.index = @intCast(u32, wasm_bin.imported_globals_count + wasm_bin.wasm_globals.items.len);
+            symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
+            const global = try wasm_bin.wasm_globals.addOne(allocator);
+            global.* = .{
+                .global_type = .{
+                    .valtype = .i32,
+                    .mutable = true,
+                },
+                .init = .{ .i32_const = 0 },
+            };
+        }
+    }
+
+    // create indirect function pointer symbol
+    {
+        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`
+            .reftype = .funcref,
         };
+        if (options.output_mode == .Obj or options.import_table) {
+            symbol.setUndefined(true);
+            symbol.index = @intCast(u32, wasm_bin.imported_tables_count);
+            wasm_bin.imported_tables_count += 1;
+            try wasm_bin.imports.put(allocator, loc, .{
+                .module_name = try wasm_bin.string_table.put(allocator, wasm_bin.host_name),
+                .name = symbol.name,
+                .kind = .{ .table = table },
+            });
+        } else {
+            symbol.index = @intCast(u32, wasm_bin.imported_tables_count + wasm_bin.tables.items.len);
+            try wasm_bin.tables.append(allocator, table);
+            if (options.export_table) {
+                symbol.setFlag(.WASM_SYM_EXPORTED);
+            } else {
+                symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
+            }
+        }
     }
 
     if (!options.strip and options.module != null) {
@@ -400,6 +423,22 @@ pub fn createEmpty(gpa: Allocator, options: link.Options) !*Wasm {
     return wasm;
 }
 
+/// For a given name, creates a new global synthetic symbol.
+/// Leaves index undefined and the default flags (0).
+fn createSyntheticSymbol(wasm: *Wasm, name: []const u8, tag: Symbol.Tag) !SymbolLoc {
+    const name_offset = try wasm.string_table.put(wasm.base.allocator, name);
+    const sym_index = @intCast(u32, wasm.symbols.items.len);
+    const loc: SymbolLoc = .{ .index = sym_index, .file = null };
+    try wasm.symbols.append(wasm.base.allocator, .{
+        .name = name_offset,
+        .flags = 0,
+        .tag = tag,
+        .index = undefined,
+    });
+    try wasm.resolved_symbols.putNoClobber(wasm.base.allocator, loc, {});
+    try wasm.globals.putNoClobber(wasm.base.allocator, name_offset, loc);
+    return loc;
+}
 /// Initializes symbols and atoms for the debug sections
 /// Initialization is only done when compiling Zig code.
 /// When Zig is invoked as a linker instead, the atoms
@@ -1249,7 +1288,7 @@ pub fn updateDeclExports(
             const existing_sym: Symbol = existing_loc.getSymbol(wasm).*;
 
             const exp_is_weak = exp.options.linkage == .Internal or exp.options.linkage == .Weak;
-            // When both the to-bo-exported symbol and the already existing symbol
+            // When both the to-be-exported symbol and the already existing symbol
             // are strong symbols, we have a linker error.
             // In the other case we replace one with the other.
             if (!exp_is_weak and !existing_sym.isWeak()) {
@@ -1361,6 +1400,19 @@ fn mapFunctionTable(wasm: *Wasm) void {
     while (it.next()) |value_ptr| : (index += 1) {
         value_ptr.* = index;
     }
+
+    if (wasm.base.options.import_table or wasm.base.options.output_mode == .Obj) {
+        const sym_loc = wasm.globals.get(wasm.string_table.getOffset("__indirect_function_table").?).?;
+        const import = wasm.imports.getPtr(sym_loc).?;
+        import.kind.table.limits.min = index - 1; // we start at index 1.
+    } else if (index > 1) {
+        log.debug("Appending indirect function table", .{});
+        const offset = wasm.string_table.getOffset("__indirect_function_table").?;
+        const sym_with_loc = wasm.globals.get(offset).?;
+        const symbol = sym_with_loc.getSymbol(wasm);
+        const table = &wasm.tables.items[symbol.index - wasm.imported_tables_count];
+        table.limits = .{ .min = index, .max = index };
+    }
 }
 
 /// Either creates a new import, or updates one if existing.
@@ -1700,17 +1752,6 @@ fn setupImports(wasm: *Wasm) !void {
 /// Takes the global, function and table section from each linked object file
 /// and merges it into a single section for each.
 fn mergeSections(wasm: *Wasm) !void {
-    // append the indirect function table if initialized
-    if (wasm.string_table.getOffset("__indirect_function_table")) |offset| {
-        const sym_loc = wasm.globals.get(offset).?;
-        const table: std.wasm.Table = .{
-            .limits = .{ .min = @intCast(u32, wasm.function_table.count()), .max = null },
-            .reftype = .funcref,
-        };
-        sym_loc.getSymbol(wasm).index = @intCast(u32, wasm.tables.items.len) + wasm.imported_tables_count;
-        try wasm.tables.append(wasm.base.allocator, table);
-    }
-
     for (wasm.resolved_symbols.keys()) |sym_loc| {
         if (sym_loc.file == null) {
             // Zig code-generated symbols are already within the sections and do not
@@ -2613,28 +2654,9 @@ fn writeToFile(
 
     // Import section
     const import_memory = wasm.base.options.import_memory or is_obj;
-    const import_table = wasm.base.options.import_table or is_obj;
-    if (wasm.imports.count() != 0 or import_memory or import_table) {
+    if (wasm.imports.count() != 0 or import_memory) {
         const header_offset = try reserveVecSectionHeader(&binary_bytes);
 
-        // import table is always first table so emit that first
-        if (import_table) {
-            const table_imp: types.Import = .{
-                .module_name = try wasm.string_table.put(wasm.base.allocator, wasm.host_name),
-                .name = try wasm.string_table.put(wasm.base.allocator, "__indirect_function_table"),
-                .kind = .{
-                    .table = .{
-                        .limits = .{
-                            .min = @intCast(u32, wasm.function_table.count()),
-                            .max = null,
-                        },
-                        .reftype = .funcref,
-                    },
-                },
-            };
-            try wasm.emitImport(binary_writer, table_imp);
-        }
-
         var it = wasm.imports.iterator();
         while (it.next()) |entry| {
             assert(entry.key_ptr.*.getSymbol(wasm).isUndefined());
@@ -2657,7 +2679,7 @@ fn writeToFile(
             header_offset,
             .import,
             @intCast(u32, binary_bytes.items.len - header_offset - header_size),
-            @intCast(u32, wasm.imports.count() + @boolToInt(import_memory) + @boolToInt(import_table)),
+            @intCast(u32, wasm.imports.count() + @boolToInt(import_memory)),
         );
         section_count += 1;
     }
@@ -2680,22 +2702,20 @@ fn writeToFile(
     }
 
     // Table section
-    const export_table = wasm.base.options.export_table;
-    if (!import_table and wasm.function_table.count() != 0) {
+    if (wasm.tables.items.len > 0) {
         const header_offset = try reserveVecSectionHeader(&binary_bytes);
 
-        try leb.writeULEB128(binary_writer, std.wasm.reftype(.funcref));
-        try emitLimits(binary_writer, .{
-            .min = @intCast(u32, wasm.function_table.count()) + 1,
-            .max = null,
-        });
+        for (wasm.tables.items) |table| {
+            try leb.writeULEB128(binary_writer, std.wasm.reftype(table.reftype));
+            try emitLimits(binary_writer, table.limits);
+        }
 
         try writeVecSectionHeader(
             binary_bytes.items,
             header_offset,
             .table,
             @intCast(u32, binary_bytes.items.len - header_offset - header_size),
-            @as(u32, 1),
+            @intCast(u32, wasm.tables.items.len),
         );
         section_count += 1;
     }
@@ -2748,7 +2768,7 @@ fn writeToFile(
     }
 
     // Export section
-    if (wasm.exports.items.len != 0 or export_table or !import_memory) {
+    if (wasm.exports.items.len != 0 or !import_memory) {
         const header_offset = try reserveVecSectionHeader(&binary_bytes);
 
         for (wasm.exports.items) |exp| {
@@ -2759,13 +2779,6 @@ fn writeToFile(
             try leb.writeULEB128(binary_writer, exp.index);
         }
 
-        if (export_table) {
-            try leb.writeULEB128(binary_writer, @intCast(u32, "__indirect_function_table".len));
-            try binary_writer.writeAll("__indirect_function_table");
-            try binary_writer.writeByte(std.wasm.externalKind(.table));
-            try leb.writeULEB128(binary_writer, @as(u32, 0)); // function table is always the first table
-        }
-
         if (!import_memory) {
             try leb.writeULEB128(binary_writer, @intCast(u32, "memory".len));
             try binary_writer.writeAll("memory");
@@ -2778,7 +2791,7 @@ fn writeToFile(
             header_offset,
             .@"export",
             @intCast(u32, binary_bytes.items.len - header_offset - header_size),
-            @intCast(u32, wasm.exports.items.len) + @boolToInt(export_table) + @boolToInt(!import_memory),
+            @intCast(u32, wasm.exports.items.len) + @boolToInt(!import_memory),
         );
         section_count += 1;
     }
@@ -2787,11 +2800,18 @@ fn writeToFile(
     if (wasm.function_table.count() > 0) {
         const header_offset = try reserveVecSectionHeader(&binary_bytes);
 
-        var flags: u32 = 0x2; // Yes we have a table
+        const table_loc = wasm.globals.get(wasm.string_table.getOffset("__indirect_function_table").?).?;
+        const table_sym = table_loc.getSymbol(wasm);
+
+        var flags: u32 = if (table_sym.index == 0) 0x0 else 0x02; // passive with implicit 0-index table or set table index manually
         try leb.writeULEB128(binary_writer, flags);
-        try leb.writeULEB128(binary_writer, @as(u32, 0)); // index of that table. TODO: Store synthetic symbols
+        if (flags == 0x02) {
+            try leb.writeULEB128(binary_writer, table_sym.index);
+        }
         try emitInit(binary_writer, .{ .i32_const = 1 }); // We start at index 1, so unresolved function pointers are invalid
-        try leb.writeULEB128(binary_writer, @as(u8, 0));
+        if (flags == 0x02) {
+            try leb.writeULEB128(binary_writer, @as(u8, 0)); // represents funcref
+        }
         try leb.writeULEB128(binary_writer, @intCast(u32, wasm.function_table.count()));
         var symbol_it = wasm.function_table.keyIterator();
         while (symbol_it.next()) |symbol_loc_ptr| {