Commit f1cc5f33e8

Luuk de Gram <luuk@degram.dev>
2022-02-14 21:50:55
wasm-linker: Implement section merging
This implements the merging of all sections, to generate a valid wasm binary where all symbols have been resolved and their respective sections have been merged into the final binary.
1 parent e7be0be
Changed files (4)
src/link/Wasm/Atom.zig
@@ -50,7 +50,7 @@ pub fn deinit(self: *Atom, gpa: Allocator) void {
     self.relocs.deinit(gpa);
     self.code.deinit(gpa);
 
-    while (self.locals.items) |*local| {
+    for (self.locals.items) |*local| {
         local.deinit(gpa);
     }
     self.locals.deinit(gpa);
@@ -93,7 +93,7 @@ pub fn symbolAtom(self: *Atom, symbol_index: u32) *Atom {
 /// Resolves the relocations within the atom, writing the new value
 /// at the calculated offset.
 pub fn resolveRelocs(self: *Atom, wasm_bin: *const Wasm) !void {
-    const symbol: Symbol = wasm_bin.symbols.items[self.sym_index];
+    const symbol: Symbol = wasm_bin.managed_symbols.items[self.sym_index];
     log.debug("Resolving relocs in atom '{s}' count({d})", .{
         symbol.name,
         self.relocs.items.len,
@@ -102,7 +102,7 @@ pub fn resolveRelocs(self: *Atom, wasm_bin: *const Wasm) !void {
     for (self.relocs.items) |reloc| {
         const value = try relocationValue(reloc, wasm_bin);
         log.debug("Relocating '{s}' referenced in '{s}' offset=0x{x:0>8} value={d}", .{
-            wasm_bin.symbols.items[reloc.index].name,
+            wasm_bin.managed_symbols.items[reloc.index].name,
             symbol.name,
             reloc.offset,
             value,
@@ -139,7 +139,7 @@ pub fn resolveRelocs(self: *Atom, wasm_bin: *const Wasm) !void {
 /// All values will be represented as a `u64` as all values can fit within it.
 /// The final value must be casted to the correct size.
 fn relocationValue(relocation: types.Relocation, wasm_bin: *const Wasm) !u64 {
-    const symbol: Symbol = wasm_bin.symbols.items[relocation.index];
+    const symbol: Symbol = wasm_bin.managed_symbols.items[relocation.index];
     return switch (relocation.relocation_type) {
         .R_WASM_FUNCTION_INDEX_LEB => symbol.index,
         .R_WASM_TABLE_NUMBER_LEB => symbol.index,
src/link/Wasm/Object.zig
@@ -6,14 +6,14 @@ const Object = @This();
 const Atom = @import("Atom.zig");
 const types = @import("types.zig");
 const std = @import("std");
-const Wasm = @import("Wasm.zig");
+const Wasm = @import("../Wasm.zig");
 const Symbol = @import("Symbol.zig");
 
 const Allocator = std.mem.Allocator;
 const leb = std.leb;
 const meta = std.meta;
 
-const log = std.log.scoped(.zwld);
+const log = std.log.scoped(.link);
 
 /// Wasm spec version used for this `Object`
 version: u32 = 0,
@@ -26,7 +26,7 @@ file: ?std.fs.File = null,
 /// Name (read path) of the object file.
 name: []const u8,
 /// Parsed type section
-types: []const std.wasm.Type = &.{},
+func_types: []const std.wasm.Type = &.{},
 /// A list of all imports for this module
 imports: []std.wasm.Import = &.{},
 /// Parsed function section
@@ -104,7 +104,8 @@ const RelocatableData = struct {
 pub const InitError = error{NotObjectFile} || ParseError || std.fs.File.ReadError;
 
 /// Initializes a new `Object` from a wasm object file.
-pub fn init(gpa: Allocator, file: std.fs.File, path: []const u8) InitError!Object {
+/// This also parses and verifies the object file.
+pub fn create(gpa: Allocator, file: std.fs.File, path: []const u8) InitError!Object {
     var object: Object = .{
         .file = file,
         .name = path,
@@ -161,7 +162,7 @@ pub fn getTable(self: *const Object, id: u32) *std.wasm.Table {
 /// we initialize a new table symbol that corresponds to that import and return that symbol.
 ///
 /// When the object file is *NOT* MVP, we return `null`.
-fn checkLegacyIndirectFunctionTable(self: *Object) !?Symbol {
+fn checkLegacyIndirectFunctionTable(self: *Object, gpa: Allocator) !?Symbol {
     var table_count: usize = 0;
     for (self.symtable) |sym| {
         if (sym.tag == .table) table_count += 1;
@@ -204,7 +205,7 @@ fn checkLegacyIndirectFunctionTable(self: *Object) !?Symbol {
 
     var table_symbol: Symbol = .{
         .flags = 0,
-        .name = table_import.name,
+        .name = try gpa.dupeZ(u8, table_import.name),
         .tag = .table,
         .index = 0,
     };
@@ -310,7 +311,7 @@ fn Parser(comptime ReaderType: type) type {
                         }
                     },
                     .type => {
-                        for (try readVec(&self.object.types, reader, gpa)) |*type_val| {
+                        for (try readVec(&self.object.func_types, reader, gpa)) |*type_val| {
                             if ((try reader.readByte()) != std.wasm.function_type) return error.ExpectedFuncType;
 
                             for (try readVec(&type_val.params, reader, gpa)) |*param| {
@@ -636,7 +637,7 @@ fn Parser(comptime ReaderType: type) type {
 
                     // we found all symbols, check for indirect function table
                     // in case of an MVP object file
-                    if (try self.object.checkLegacyIndirectFunctionTable()) |symbol| {
+                    if (try self.object.checkLegacyIndirectFunctionTable(gpa)) |symbol| {
                         try symbols.append(symbol);
                         log.debug("Found legacy indirect function table. Created symbol", .{});
                     }
@@ -662,7 +663,7 @@ fn Parser(comptime ReaderType: type) type {
             switch (tag) {
                 .data => {
                     const name_len = try leb.readULEB128(u32, reader);
-                    const name = try gpa.alloc(u8, name_len);
+                    const name = try gpa.allocSentinel(u8, name_len, 0);
                     try reader.readNoEof(name);
                     symbol.name = name;
 
@@ -684,16 +685,16 @@ fn Parser(comptime ReaderType: type) type {
 
                     const is_undefined = symbol.isUndefined();
                     if (is_undefined) {
-                        maybe_import = self.object.findImport(symbol.externalType(), symbol.index);
+                        maybe_import = self.object.findImport(symbol.tag.externalType(), symbol.index);
                     }
                     const explicit_name = symbol.hasFlag(.WASM_SYM_EXPLICIT_NAME);
                     if (!(is_undefined and !explicit_name)) {
                         const name_len = try leb.readULEB128(u32, reader);
-                        const name = try gpa.alloc(u8, name_len);
+                        const name = try gpa.allocSentinel(u8, name_len, 0);
                         try reader.readNoEof(name);
                         symbol.name = name;
                     } else {
-                        symbol.name = maybe_import.?.name;
+                        symbol.name = try gpa.dupeZ(u8, maybe_import.?.name);
                     }
                 },
             }
@@ -818,7 +819,6 @@ pub fn parseIntoAtoms(self: *Object, gpa: Allocator, object_index: u16, wasm_bin
             }
         }
 
-        // TODO: Replace `atom.code` from an existing slice to a pointer to the data
         try atom.code.appendSlice(gpa, relocatable_data.data[0..relocatable_data.size]);
 
         const segment: *Wasm.Segment = &wasm_bin.segments.items[final_index];
src/link/Wasm/Symbol.zig
@@ -38,6 +38,7 @@ pub const Tag = enum {
             .data => .memory,
             .section => unreachable, // Not an external type
             .event => unreachable, // Not an external type
+            .dead => unreachable, // Dead symbols should not be referenced
             .table => .table,
         };
     }
src/link/Wasm.zig
@@ -45,20 +45,29 @@ host_name: []const u8 = "env",
 /// This is ment for bookkeeping so we can safely cleanup all codegen memory
 /// when calling `deinit`
 decls: std.AutoHashMapUnmanaged(*Module.Decl, void) = .{},
-/// List of all symbols.
+/// List of all symbols generated by Zig code.
 symbols: std.ArrayListUnmanaged(Symbol) = .{},
 /// List of symbol indexes which are free to be used.
 symbols_free_list: std.ArrayListUnmanaged(u32) = .{},
 /// Maps atoms to their segment index
 atoms: std.AutoHashMapUnmanaged(u32, *Atom) = .{},
+/// Atoms managed and created by the linker. This contains atoms
+/// from object files, and not Atoms generated by a Decl.
+managed_atoms: std.ArrayListUnmanaged(*Atom) = .{},
 /// Represents the index into `segments` where the 'code' section
 /// lives.
 code_section_index: ?u32 = null,
 /// The count of imported functions. This number will be appended
 /// to the function indexes as their index starts at the lowest non-extern function.
 imported_functions_count: u32 = 0,
-/// Map of symbol indexes, represented by its `wasm.Import`
-imports: std.AutoHashMapUnmanaged(u32, wasm.Import) = .{},
+/// The count of imported wasm globals. This number will be appended
+/// to the global indexes when sections are merged.
+imported_globals_count: u32 = 0,
+/// The count of imported tables. This number will be appended
+/// to the table indexes when sections are merged.
+imported_tables_count: u32 = 0,
+/// Map of symbol locations, represented by its `wasm.Import`
+imports: std.AutoHashMapUnmanaged(SymbolLoc, wasm.Import) = .{},
 /// Represents non-synthetic section entries.
 /// Used for code, data and custom sections.
 segments: std.ArrayListUnmanaged(Segment) = .{},
@@ -77,6 +86,10 @@ functions: std.ArrayListUnmanaged(wasm.Func) = .{},
 wasm_globals: std.ArrayListUnmanaged(wasm.Global) = .{},
 /// Memory section
 memories: wasm.Memory = .{ .limits = .{ .min = 0, .max = null } },
+/// Output table section
+tables: std.ArrayListUnmanaged(wasm.Table) = .{},
+/// Output export section
+exports: std.ArrayListUnmanaged(wasm.Export) = .{},
 
 /// Indirect function table, used to call function pointers
 /// When this is non-zero, we must emit a table entry,
@@ -87,14 +100,14 @@ function_table: std.AutoHashMapUnmanaged(u32, u32) = .{},
 
 /// All object files and their data which are linked into the final binary
 objects: std.ArrayListUnmanaged(Object) = .{},
+/// A map of global names to their symbol location
+globals: std.StringHashMapUnmanaged(SymbolLoc) = .{},
 /// Maps discarded symbols and their positions to the location of the symbol
 /// it was resolved to
 discarded: std.AutoHashMapUnmanaged(SymbolLoc, SymbolLoc) = .{},
-/// Mapping between symbol names and their respective location.
-/// This map contains all symbols that will be written into the final binary
-/// and were either defined, or resolved.
-/// TODO: Use string interning and make the key an index, rather than a unique string.
-symbol_resolver: std.StringArrayHashMapUnmanaged(SymbolLoc) = .{},
+/// List of all symbol locations which have been resolved by the linker and will be emit
+/// into the final binary.
+resolved_symbols: std.ArrayListUnmanaged(SymbolLoc) = .{},
 
 pub const Segment = struct {
     alignment: u32,
@@ -116,6 +129,18 @@ pub const SymbolLoc = struct {
     /// The index of the object file where the symbol resides.
     /// When this is `null` the symbol comes from a non-object file.
     file: ?u16,
+
+    /// From a given location, returns the corresponding symbol in the wasm binary
+    pub fn getSymbol(self: SymbolLoc, wasm_bin: *const Wasm) *Symbol {
+        if (self.file) |object_index| {
+            if (wasm_bin.discarded.get(self)) |old_loc| {
+                return old_loc.getSymbol(wasm_bin);
+            }
+            const object = wasm_bin.objects.items[object_index];
+            return &object.symtable[self.index];
+        }
+        return &wasm_bin.symbols.items[self.index];
+    }
 };
 
 pub fn openPath(allocator: Allocator, sub_path: []const u8, options: link.Options) !*Wasm {
@@ -186,49 +211,118 @@ fn parseObjectFile(self: *Wasm, path: []const u8) !bool {
     const file = try fs.cwd().openFile(path, .{});
     errdefer file.close();
 
-    var object = Object.init(self.base.allocator, file, path) catch |err| {
-        if (err == error.InvalidMagicByte) {
+    var object = Object.create(self.base.allocator, file, path) catch |err| switch (err) {
+        error.InvalidMagicByte, error.NotObjectFile => {
             log.warn("Self hosted linker does not support non-object file parsing", .{});
             return false;
-        } else return err;
+        },
+        else => |e| return e,
     };
     errdefer object.deinit(self.base.allocator);
     try self.objects.append(self.base.allocator, object);
     return true;
 }
 
+fn resolveSymbolsInObject(self: *Wasm, object_index: u16) !void {
+    const object: Object = self.objects.items[object_index];
+    log.debug("Resolving symbols in object: '{s}'", .{object.name});
+
+    for (object.symtable) |symbol, i| {
+        const sym_index = @intCast(u32, i);
+        const location: SymbolLoc = .{
+            .file = object_index,
+            .index = sym_index,
+        };
+
+        if (symbol.isLocal()) {
+            if (symbol.isUndefined()) {
+                log.err("Local symbols are not allowed to reference imports", .{});
+                log.err("  symbol '{s}' defined in '{s}'", .{ symbol.name, object.name });
+                return error.undefinedLocal;
+            }
+            try self.resolved_symbols.append(self.base.allocator, location);
+            continue;
+        }
+
+        // TODO: locals are allowed to have duplicate symbol names
+        // TODO: Store undefined symbols so we can verify at the end if they've all been found
+        // if not, emit an error (unless --allow-undefined is enabled).
+        const maybe_existing = try self.globals.getOrPut(self.base.allocator, std.mem.sliceTo(symbol.name, 0));
+        if (!maybe_existing.found_existing) {
+            maybe_existing.value_ptr.* = location;
+
+            try self.globals.putNoClobber(self.base.allocator, std.mem.sliceTo(symbol.name, 0), location);
+            continue;
+        }
+
+        const existing_loc = maybe_existing.value_ptr.*;
+        const existing_sym: *Symbol = existing_loc.getSymbol(self);
+
+        if (!existing_sym.isUndefined()) {
+            if (!symbol.isUndefined()) {
+                log.err("symbol '{s}' defined multiple times", .{existing_sym.name});
+                log.err("  first definition in '{s}'", .{self.objects.items[existing_loc.file.?].name});
+                log.err("  next definition in '{s}'", .{object.name});
+                return error.SymbolCollision;
+            }
+
+            continue; // Do not overwrite defined symbols with undefined symbols
+        }
+
+        // simply overwrite with the new symbol
+        log.info("Overwriting symbol '{s}'", .{symbol.name});
+        log.info("  first definition in '{s}'", .{self.objects.items[existing_loc.file.?].name});
+        log.info("  next definition in '{s}'", .{object.name});
+        try self.discarded.putNoClobber(self.base.allocator, maybe_existing.value_ptr.*, location);
+        maybe_existing.value_ptr.* = location;
+        try self.globals.putNoClobber(self.base.allocator, std.mem.sliceTo(symbol.name, 0), location);
+    }
+}
+
 pub fn deinit(self: *Wasm) void {
+    const gpa = self.base.allocator;
     if (build_options.have_llvm) {
-        if (self.llvm_object) |llvm_object| llvm_object.destroy(self.base.allocator);
+        if (self.llvm_object) |llvm_object| llvm_object.destroy(gpa);
     }
 
     var decl_it = self.decls.keyIterator();
     while (decl_it.next()) |decl_ptr| {
         const decl = decl_ptr.*;
-        decl.link.wasm.deinit(self.base.allocator);
+        decl.link.wasm.deinit(gpa);
     }
 
     for (self.func_types.items) |*func_type| {
-        func_type.deinit(self.base.allocator);
+        func_type.deinit(gpa);
     }
     for (self.segment_info.items) |segment_info| {
-        self.base.allocator.free(segment_info.name);
+        gpa.free(segment_info.name);
+    }
+    for (self.objects.items) |*object| {
+        object.file.?.close();
+        object.deinit(gpa);
     }
 
-    self.decls.deinit(self.base.allocator);
-    self.symbols.deinit(self.base.allocator);
-    self.symbols_free_list.deinit(self.base.allocator);
-    self.atoms.deinit(self.base.allocator);
-    self.segments.deinit(self.base.allocator);
-    self.data_segments.deinit(self.base.allocator);
-    self.segment_info.deinit(self.base.allocator);
+    self.decls.deinit(gpa);
+    self.symbols.deinit(gpa);
+    self.symbols_free_list.deinit(gpa);
+    self.globals.deinit(gpa);
+    self.resolved_symbols.deinit(gpa);
+    self.discarded.deinit(gpa);
+    self.atoms.deinit(gpa);
+    self.managed_atoms.deinit(gpa);
+    self.segments.deinit(gpa);
+    self.data_segments.deinit(gpa);
+    self.segment_info.deinit(gpa);
+    self.objects.deinit(gpa);
 
     // free output sections
-    self.imports.deinit(self.base.allocator);
-    self.func_types.deinit(self.base.allocator);
-    self.functions.deinit(self.base.allocator);
-    self.wasm_globals.deinit(self.base.allocator);
-    self.function_table.deinit(self.base.allocator);
+    self.imports.deinit(gpa);
+    self.func_types.deinit(gpa);
+    self.functions.deinit(gpa);
+    self.wasm_globals.deinit(gpa);
+    self.function_table.deinit(gpa);
+    self.tables.deinit(gpa);
+    self.exports.deinit(gpa);
 }
 
 pub fn allocateDeclIndexes(self: *Wasm, decl: *Module.Decl) !void {
@@ -453,7 +547,7 @@ pub fn freeDecl(self: *Wasm, decl: *Module.Decl) void {
     }
 
     if (decl.isExtern()) {
-        const import = self.imports.fetchRemove(atom.sym_index).?.value;
+        const import = self.imports.fetchRemove(.{ .file = null, .index = atom.sym_index }).?.value;
         switch (import.kind) {
             .function => self.imported_functions_count -= 1,
             else => unreachable,
@@ -469,6 +563,9 @@ pub fn addTableFunction(self: *Wasm, symbol_index: u32) !void {
     try self.function_table.put(self.base.allocator, symbol_index, index);
 }
 
+/// Assigns indexes to all indirect functions.
+/// Starts at offset 1, where the value `0` represents an unresolved function pointer
+/// or null-pointer
 fn mapFunctionTable(self: *Wasm) void {
     var it = self.function_table.valueIterator();
     var index: u32 = 1;
@@ -484,7 +581,7 @@ fn addOrUpdateImport(self: *Wasm, decl: *Module.Decl) !void {
     symbol.setUndefined(true);
     switch (decl.ty.zigTypeTag()) {
         .Fn => {
-            const gop = try self.imports.getOrPut(self.base.allocator, symbol_index);
+            const gop = try self.imports.getOrPut(self.base.allocator, .{ .index = symbol_index, .file = null });
             const module_name = if (decl.getExternFn().?.lib_name) |lib_name| blk: {
                 break :blk std.mem.sliceTo(lib_name, 0);
             } else self.host_name;
@@ -599,21 +696,176 @@ fn allocateAtoms(self: *Wasm) !void {
 }
 
 fn setupImports(self: *Wasm) void {
+    for (self.resolved_symbols.items) |symbol_loc| {
+        if (symbol_loc.file == null) {
+            // imports generated by Zig code are already in the `import` section
+            continue;
+        }
+
+        const symbol = symbol_loc.getSymbol(self);
+        if (symbol.tag == .data or !symbol.requiresImport()) {
+            continue;
+        }
+
+        log.debug("Symbol '{s}' will be imported from the host", .{symbol.name});
+        const import = self.objects.items[symbol_loc.file.?].findImport(symbol.externalType(), symbol.index);
+        // TODO: De-duplicate imports
+        try self.imports.putNoClobber(self.base.allocator, symbol_loc, import);
+    }
+
+    // Assign all indexes of the imports to their representing symbols
     var function_index: u32 = 0;
+    var global_index: u32 = 0;
+    var table_index: u32 = 0;
     var it = self.imports.iterator();
     while (it.next()) |entry| {
-        const symbol = &self.symbols.items[entry.key_ptr.*];
+        const symbol = entry.key_ptr.*.getSymbol(self);
         const import: wasm.Import = entry.value_ptr.*;
         switch (import.kind) {
             .function => {
                 symbol.index = function_index;
                 function_index += 1;
             },
+            .global => {
+                symbol.index = global_index;
+                global_index += 1;
+            },
+            .table => {
+                symbol.index = table_index;
+                table_index += 1;
+            },
             else => unreachable,
         }
     }
 }
 
+/// Takes the global, function and table section from each linked object file
+/// and merges it into a single section for each.
+fn mergeSections(self: *Wasm) !void {
+    // append the indirect function table if initialized
+    if (self.globals.get("__indirect_function_table")) |sym_loc| {
+        const table: wasm.Table = .{
+            .limits = .{ .min = @intCast(u32, self.function_table.count()), .max = null },
+            .reftype = .funcref,
+        };
+        sym_loc.getSymbol(self).index = @intCast(u32, self.tables.items.len) + self.imported_tables_count;
+        try self.tables.append(self.base.allocator, table);
+    }
+
+    for (self.resolved_symbols.items) |sym_loc| {
+        if (sym_loc.file == null) {
+            // Zig code-generated symbols are already within the sections and do not
+            // require to be merged
+            continue;
+        }
+
+        const object = self.objects.items[sym_loc.file.?];
+        const symbol = &object.symtable[sym_loc.index];
+        if (symbol.isUndefined()) {
+            // Skip undefined symbols as they go in the `import` section
+            continue;
+        }
+
+        const offset = object.importedCountByKind(symbol.externalType());
+        const index = symbol.index - offset;
+        switch (symbol.tag) {
+            .function => {
+                const original_func = object.functions[index];
+                symbol.index = @intCast(u32, self.functions.items.len) + self.imported_functions_count;
+                try self.functions.append(self.base.allocator, original_func);
+            },
+            .global => {
+                const original_global = object.globals[index];
+                symbol.index = @intCast(u32, self.globals.items.len) + self.imported_globals_count;
+                try self.wasm_globals.append(self.base.allocator, original_global);
+            },
+            .table => {
+                const original_table = object.tables[index];
+                symbol.index = @intCast(u32, self.tables.items.len) + self.imported_tables_count;
+                try self.tables.append(self.base.allocator, original_table);
+            },
+            else => {},
+        }
+    }
+
+    log.debug("Merged ({d}) functions", .{self.functions.items.len});
+    log.debug("Merged ({d}) globals", .{self.wasm_globals.items.len});
+    log.debug("Merged ({d}) tables", .{self.tables.tems.len});
+}
+
+/// Merges function types of all object files into the final
+/// 'types' section, while assigning the type index to the representing
+/// section (import, export, function).
+fn mergeTypes(self: *Wasm) !void {
+    for (self.resolved_symbols.items) |sym_loc| {
+        if (sym_loc.file == null) {
+            // zig code-generated symbols are already present in final type section
+            continue;
+        }
+        const object = self.objects.items[sym_loc.file.?];
+        const symbol = object.symtable[sym_loc.index];
+        if (symbol.tag != .function) {
+            // Only functions have types
+            continue;
+        }
+
+        if (symbol.isUndefined()) {
+            log.debug("Adding type from extern function '{s}'", .{symbol.name});
+            const import: *wasm.Import = self.imports.getPtr(sym_loc);
+            const original_type = object.types[import.kind.function];
+            import.kind.function = try self.putOrGetFuncType(original_type);
+        } else {
+            log.debug("Adding type from function '{s}'", .{symbol.name});
+            const func = &self.functions.items[symbol.index - self.imported_functions_count];
+            func.type_index = try self.putOrGetFuncType(object.types[func.type_index]);
+        }
+    }
+    log.debug("Completed merging and deduplicating types. Total count: ({d})", .{self.func_types.items.len});
+}
+
+fn setupExports(self: *Wasm) !void {
+    log.debug("Building exports from symbols", .{});
+
+    // When importing memory option if false, we export it instead
+    if (!self.base.options.import_memory) {
+        try self.exports.append(self.base.allocator, .{ .name = "memory", .kind = .memory, .index = 0 });
+    }
+
+    for (self.resolved_symbols.items) |sym_loc| {
+        const symbol = sym_loc.getSymbol(self);
+        if (!symbol.isExported()) continue;
+
+        const exp: wasm.Export = .{ .name = symbol.name, .kind = symbol.externalType(), .index = symbol.index };
+        log.debug("Appending export for symbol '{s}' at index: ({d})", .{ exp.name, exp.index });
+        try self.exports.append(self.base.allocator, exp);
+    }
+
+    log.debug("Completed building exports. Total count: ({d})", .{self.exports.items.len});
+}
+
+fn setupStart(self: *Wasm) !void {
+    const entry_name = self.base.options.entry orelse "_start";
+
+    const symbol_loc = self.globals.get(entry_name) orelse {
+        if (self.base.options.output_mode == .Exe) {
+            if (self.base.options.wasi_exec_model == .reactor) return; // Not required for reactors
+        } else {
+            return; // No entry point needed for non-executable wasm files
+        }
+        log.err("Entry symbol '{s}' missing", .{entry_name});
+        return error.MissingSymbol;
+    };
+
+    const symbol = symbol_loc.getSymbol(self);
+    if (symbol.tag != .function) {
+        log.err("Entry symbol '{s}' is not a function", .{entry_name});
+        return error.InvalidEntryKind;
+    }
+
+    // Ensure the symbol is exported so host environment can access it
+    symbol.setFlag(.WASM_SYM_EXPORTED);
+}
+
 /// Sets up the memory section of the wasm module, as well as the stack.
 fn setupMemory(self: *Wasm) !void {
     log.debug("Setting up memory layout", .{});
@@ -754,6 +1006,12 @@ pub fn flushModule(self: *Wasm, comp: *Compilation) !void {
     // TODO: Also link with other objects such as compiler-rt
     try self.parseInputFiles(positionals.items);
 
+    var object_index: u16 = 0;
+    while (object_index < self.objects.items.len) : (object_index += 1) {
+        try self.resolveSymbolsInObject(object_index);
+        try self.objects.items[object_index].parseIntoAtoms(self.base.allocator, object_index, self);
+    }
+
     // When we finish/error we reset the state of the linker
     // So we can rebuild the binary file on each incremental update
     defer self.resetState();
@@ -777,6 +1035,9 @@ pub fn flushModule(self: *Wasm, comp: *Compilation) !void {
     try self.setupMemory();
     try self.allocateAtoms();
     self.mapFunctionTable();
+    try self.mergeSections();
+    try self.mergeTypes();
+    try self.setupExports();
 
     const file = self.base.file.?;
     const header_size = 5 + 1;