// Copyright (c) 2026 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module arm64 import os import time fn C.open(charptr, int, int) int fn C.write(int, voidptr, int) int fn C.close(int) int fn C.chmod(charptr, int) int fn C.rename(charptr, charptr) int // Mach-O executable constants const mh_execute = 2 const lc_load_dylinker = 0xe const lc_load_dylib = 0xc const lc_main = u32(0x80000028) const lc_dyld_info_only = u32(0x80000022) const lc_dysymtab = 0xb const lc_uuid = 0x1b const lc_build_version = 0x32 const lc_source_version = 0x2a const lc_code_signature = 0x1d // Code signing constants (big-endian magic numbers) const csmagic_embedded_signature = u32(0xfade0cc0) const csmagic_codedirectory = u32(0xfade0c02) const csmagic_requirements = u32(0xfade0c01) const csmagic_blobwrapper = u32(0xfade0b01) const csslot_codedirectory = u32(0) const csslot_requirements = u32(2) const csslot_cms_signature = u32(0x10000) const cs_adhoc = u32(0x2) // Ad-hoc signing flag const cs_hashtype_sha256 = u8(2) const cs_hash_size = 32 // SHA256 = 32 bytes const cs_page_size_arm64 = 16384 // Code signing page size for ARM64 macOS const cs_page_shift_arm64 = 14 // log2(16384) const o_wronly_creat_trunc = 0x601 // O_WRONLY | O_CREAT | O_TRUNC on Darwin // ARM64 page size on macOS const page_size = 0x4000 // 16KB // Base address for executables const base_addr = u64(0x100000000) // Bind opcodes for dyld const bind_opcode_done = 0x00 const bind_opcode_set_dylib_ordinal_imm = 0x10 const bind_opcode_set_symbol_flags_imm = 0x40 const bind_opcode_set_type_imm = 0x50 const bind_opcode_set_segment_and_offset_uleb = 0x70 const bind_opcode_do_bind = 0x90 const bind_type_pointer = 1 const bind_symbol_flags_weak_import = 0x01 // vfmt off // Libc symbols that should ALWAYS resolve to the external system library, // never to local V wrappers. This prevents infinite recursion where // V's malloc() wrapper calls C.malloc() which would otherwise resolve // back to the V wrapper. const force_external_syms = ['_malloc', '_free', '_calloc', '_realloc', '_exit', '_abort', '_memcpy', '_memmove', '_memset', '_memcmp', '___stdoutp', '___stderrp', '_puts', '_printf', '_write', '_read', '_open', '_close', '_fwrite', '_fflush', '_fopen', '_fclose', '_putchar', '_sprintf', '_snprintf', '_fprintf', '_sscanf', '_mmap', '_munmap', '_getcwd', '_access', '_readlink', '_getenv', '_strlen', // Filesystem/directory operations '_opendir', '_readdir', '_closedir', '_mkdir', '_rmdir', '_unlink', '_rename', '_remove', '_stat', '_lstat', '_fstat', '_chmod', '_chdir', '_realpath', '_symlink', '_link', // Process/system '_getpid', '_getuid', '_geteuid', '_fork', '_execve', '_execvp', '_waitpid', '_kill', '_system', '_posix_spawn', '_signal', '_atexit', // I/O '_fgets', '_fputs', '_fread', '_fseek', '_ftell', '_rewind', '_fileno', '_popen', '_pclose', '_dup', '_dup2', '_pipe', '_isatty', '_freopen', '_dprintf', '_getc', // String/memory '_strdup', '_strcmp', '_strncmp', '_strchr', '_strrchr', '_strerror', '_strncasecmp', '_strcasecmp', '_atoi', '_atof', '_qsort', // Time '_time', '_localtime_r', '_gmtime_r', '_mktime', '_gettimeofday', '_clock', '_clock_gettime_nsec_np', '_mach_absolute_time', '_mach_timebase_info', '_nanosleep', '_sleep', '_usleep', '_strftime', '_task_info', '_mach_task_self_', // Other '_rand', '_srand', '_isdigit', '_isspace', '_tolower', '_toupper', '_setenv', '_unsetenv', '_sysconf', '_uname', '_gethostname', '_pthread_mutex_init', '_pthread_mutex_lock', '_pthread_mutex_unlock', '_pthread_mutex_destroy', '_pthread_self', '_pthread_create', '_pthread_join', '_pthread_attr_init', '_pthread_attr_setstacksize', '_pthread_attr_destroy', '_arc4random_buf', '_proc_pidpath', '_backtrace', '_backtrace_symbols', '_backtrace_symbols_fd', // macOS specific '_dispatch_semaphore_create', '_dispatch_semaphore_signal', '_dispatch_semaphore_wait', '_dispatch_time', '_dispatch_release', '_setvbuf', '_setbuf', '_memchr', '_getlogin_r', '_getppid', '_getgid', '_getegid', '_ftruncate', '_mkstemp', '_statvfs', '_chown', '_sigaction', '_sigemptyset', '_sigaddset', '_sigprocmask', '_select', '_kqueue', '_abs', // Terminal I/O '_tcgetattr', '_tcsetattr', '_ioctl', '_getchar', '_getline', // File I/O '_fdopen', '_feof', '_ferror', // Process '_setpgid', '_ptrace', '_wait', // Time '_timegm', '_clock_gettime', // Memory '_aligned_alloc', // System '_utime', '_getlogin', '_environ', // macOS errno: __error() returns int* '___error', // macOS stdin '___stdinp', // macOS dyld '__dyld_get_image_name', '__dyld_get_image_header', // Math '_cos', '_sin', '_tan', '_acos', '_asin', '_atan', '_atan2', '_cosh', '_sinh', '_tanh', '_acosh', '_asinh', '_atanh', '_exp', '_exp2', '_log', '_log2', '_log10', '_pow', '_sqrt', '_cbrt', '_ceil', '_floor', '_round', '_trunc', '_fmod', '_remainder', '_fabs', '_copysign', '_fmax', '_fmin', '_hypot', '_ldexp', '_frexp', '_modf', '_scalbn', '_ilogb', '_logb', '_erf', '_erfc', '_lgamma', '_tgamma', '_j0', '_j1', '_jn', '_y0', '_y1', '_yn', // Memory protection and cache (hot code reloading) '_mprotect', '_sys_icache_invalidate', // Objective-C runtime (from libobjc.A.dylib) '_objc_msgSend', '_objc_getClass', '_sel_registerName', '_objc_alloc_init', '_objc_autoreleasePoolPush', '_objc_autoreleasePoolPop', // Metal framework '_MTLCreateSystemDefaultDevice', // Dynamic loading '_dlopen', '_dlsym'] // vfmt on // Symbols that live in libobjc.A.dylib (not libSystem). const objc_syms = ['_objc_msgSend', '_objc_getClass', '_sel_registerName', '_objc_alloc_init', '_objc_autoreleasePoolPush', '_objc_autoreleasePoolPop'] // Symbols that live in Metal.framework. const metal_syms = ['_MTLCreateSystemDefaultDevice'] pub struct Linker { macho &MachOObject pub mut: // Frameworks to link (e.g. ['Metal', 'Cocoa', 'QuartzCore']) frameworks []string mut: // Output buffer buf []u8 // Segment/section info text_vmaddr u64 text_fileoff int text_size int data_vmaddr u64 data_fileoff int data_size int linkedit_off int linkedit_size int // External symbols needing binding extern_syms []string // GOT entries for external symbols got_offset int // Offset within __DATA segment got_size int // Stubs for external function calls stubs_offset int stubs_size int // Symbol to GOT index mapping sym_to_got map[string]int // Multi-dylib support: dylib paths and per-symbol ordinal mapping dylibs []string // ['/usr/lib/libSystem.B.dylib', '/usr/lib/libobjc.A.dylib', ...] sym_to_dylib map[string]int // symbol name → index into dylibs[] (ordinal = idx + 1) // Code start offset (after header + load commands) code_start int } pub fn Linker.new(macho &MachOObject) &Linker { return unsafe { &Linker{ macho: macho frameworks: []string{} buf: []u8{} extern_syms: []string{} sym_to_got: map[string]int{} dylibs: []string{} sym_to_dylib: map[string]int{} } } } pub fn (mut l Linker) link(output_path string, entry_name string) { // Pre-allocate buffer with estimated size to avoid reallocations estimated_size := l.macho.text_data.len + l.macho.str_data.len + l.macho.data_data.len + 0x10000 l.buf = []u8{cap: estimated_size} mut t := time.now() mut t_total := time.now() // First pass: collect all defined symbols (except external ones) mut defined_syms := map[string]bool{} for sym in l.macho.symbols { // N_SECT (0x0E) means symbol is defined in a section if (sym.type_ & 0x0E) == 0x0E { // Don't track external symbols as defined - they should come from libc if sym.name !in force_external_syms { defined_syms[sym.name] = true } } } // Second pass: collect truly external symbols. // force_external_syms should go through GOT/stubs. // All other undefined symbols are internal V functions or V-embedded C functions // (like wyhash) that resolve to local stubs. for sym in l.macho.symbols { if sym.name in force_external_syms && sym.name !in l.extern_syms { l.extern_syms << sym.name l.sym_to_got[sym.name] = l.extern_syms.len - 1 } } l.got_size = l.extern_syms.len * 8 l.stubs_size = l.extern_syms.len * 12 // Each stub is 12 bytes on ARM64 // Build dylib list: libSystem always first, then libobjc + frameworks as needed. l.dylibs = ['/usr/lib/libSystem.B.dylib'] // Map all existing symbols to libSystem (ordinal 0 = index into dylibs) for sym_name in l.extern_syms { l.sym_to_dylib[sym_name] = 0 // default: libSystem } // Check if any objc symbols are used → add libobjc mut need_objc := false for sym_name in l.extern_syms { if sym_name in objc_syms { need_objc = true break } } if need_objc { objc_idx := l.dylibs.len l.dylibs << '/usr/lib/libobjc.A.dylib' for sym_name in l.extern_syms { if sym_name in objc_syms { l.sym_to_dylib[sym_name] = objc_idx } } } // Check if any framework symbols are used → add framework dylibs for sym_name in l.extern_syms { if sym_name in metal_syms { if 'Metal' !in l.frameworks { l.frameworks << 'Metal' } } } for fw in l.frameworks { fw_idx := l.dylibs.len l.dylibs << '/System/Library/Frameworks/${fw}.framework/${fw}' // Map framework symbols to this dylib index if fw == 'Metal' { for sym_name in l.extern_syms { if sym_name in metal_syms { l.sym_to_dylib[sym_name] = fw_idx } } } } // Calculate layout // On macOS, __TEXT segment MUST start at fileoff 0 // The header and load commands are inside the __TEXT segment n_load_cmds := 13 + l.dylibs.len // 13 fixed commands + 1 LC_LOAD_DYLIB per dylib pagezero_cmd_size := 72 text_cmd_size := 72 + (80 * 2) // __text + __stubs data_cmd_size := 72 + (80 * 2) // __data + __got linkedit_cmd_size := 72 dyld_info_cmd_size := 48 symtab_cmd_size := 24 dysymtab_cmd_size := 80 dylinker_cmd_size := 32 // Each LC_LOAD_DYLIB: 24 bytes header + path padded to 8-byte alignment mut dylib_cmd_size := 0 for dylib_path in l.dylibs { path_len := dylib_path.len + 1 // +1 for null terminator padded_path := (path_len + 7) & ~7 dylib_cmd_size += 24 + padded_path } main_cmd_size := 24 uuid_cmd_size := 24 build_version_cmd_size := 24 source_version_cmd_size := 16 code_signature_cmd_size := 16 load_cmds_size := pagezero_cmd_size + text_cmd_size + data_cmd_size + linkedit_cmd_size + dyld_info_cmd_size + symtab_cmd_size + dysymtab_cmd_size + dylinker_cmd_size + dylib_cmd_size + main_cmd_size + uuid_cmd_size + build_version_cmd_size + source_version_cmd_size + code_signature_cmd_size // __TEXT starts at file offset 0 and vmaddr base_addr l.text_fileoff = 0 l.text_vmaddr = base_addr // Code starts after header + load commands, aligned to 16 bytes // Leave ~600 bytes extra for codesign to add LC_CODE_SIGNATURE // Header (32) + load_cmds (~700) + codesign reserve (600) ≈ 1332, align to 2048 header_size := 32 code_start_min := header_size + load_cmds_size + 600 // Reserve for codesign l.code_start = (code_start_min + 15) & ~15 // Align to 16 bytes // Calculate where stubs will be (after code and cstrings) l.stubs_offset = l.code_start + l.macho.text_data.len + l.macho.str_data.len // Align to 4 bytes for l.stubs_offset % 4 != 0 { l.stubs_offset++ } // Text segment size includes header, load commands, code, cstrings, stubs text_content_end := l.stubs_offset + l.stubs_size l.text_size = (text_content_end + page_size - 1) & ~(page_size - 1) // Data segment follows text l.data_fileoff = l.text_size l.data_vmaddr = base_addr + u64(l.text_size) // GOT offset within data section mut got_offset := l.macho.data_data.len // Align GOT to 8 bytes for got_offset % 8 != 0 { got_offset++ } l.got_offset = got_offset data_content_size := got_offset + l.got_size l.data_size = (data_content_size + page_size - 1) & ~(page_size - 1) if l.data_size == 0 { l.data_size = page_size } bind_info := l.generate_bind_info(got_offset) bind_size := bind_info.len // Write header l.write_header(n_load_cmds, load_cmds_size) // Write load commands l.write_pagezero_segment() l.write_text_segment() l.write_data_segment() linkedit_start := l.buf.len l.write_linkedit_segment() // Will patch later // Bind info position (in LINKEDIT) bind_off := l.data_fileoff + l.data_size // Build symbol table for internal function names (visible in objdump -d) mut symtab_data := []u8{} mut strtab_data := []u8{} strtab_data << 0 // First byte of string table must be null sym_code_vmaddr := l.text_vmaddr + u64(l.code_start) // Find data section base address (minimum symbol value in sect 3) mut sym_data_base := u64(0xFFFFFFFFFFFFFFFF) for sym in l.macho.symbols { if (sym.type_ & 0x0E) == 0x0E && sym.sect == 3 { if sym.value < sym_data_base { sym_data_base = sym.value } } } if sym_data_base == 0xFFFFFFFFFFFFFFFF { sym_data_base = u64(l.macho.text_data.len + l.macho.str_data.len) } for sym in l.macho.symbols { if (sym.type_ & 0x0E) != 0x0E { continue // Skip undefined symbols } if sym.name in force_external_syms { continue } mut vm_addr := u64(0) mut out_sect := u8(0) if sym.sect == 1 { // __text section vm_addr = sym_code_vmaddr + sym.value out_sect = 1 } else if sym.sect == 3 { // __data section vm_addr = l.data_vmaddr + (sym.value - sym_data_base) out_sect = 3 } else { continue } str_idx := strtab_data.len strtab_data << sym.name.bytes() strtab_data << 0 write_u32_le(mut symtab_data, u32(str_idx)) // n_strx symtab_data << sym.type_ // n_type symtab_data << out_sect // n_sect write_u16_le(mut symtab_data, sym.desc) // n_desc write_u64_le(mut symtab_data, vm_addr) // n_value } // Symbol table follows bind info and must be aligned in LINKEDIT. symtab_unaligned_off := bind_off + bind_size symtab_off := (symtab_unaligned_off + 7) & ~7 symtab_pad := symtab_off - symtab_unaligned_off n_syms := symtab_data.len / 16 strtab_off := symtab_off + symtab_data.len strtab_size := strtab_data.len // Code signature follows string table and should be aligned in LINKEDIT. code_limit_unaligned := strtab_off + strtab_size cs_off := (code_limit_unaligned + 15) & ~15 cs_pad := cs_off - code_limit_unaligned // code_limit is where the signature starts (everything before is hashed) code_limit := cs_off // Signature size: SuperBlob(12) + 2*BlobIndex(8) + CodeDirectory header + identifier + hashes + Requirements blob ident := output_path.all_after_last('/') // Use filename as identifier cs_size := l.estimate_signature_size(code_limit, ident) l.linkedit_off = bind_off l.linkedit_size = bind_size + symtab_pad + symtab_data.len + strtab_size + cs_pad + cs_size l.write_dyld_info(bind_off, bind_size) l.write_symtab(symtab_off, n_syms, strtab_off, strtab_size) l.write_dysymtab(n_syms) l.write_load_dylinker() l.write_load_dylibs() // Find entry point entry_off := l.find_entry_offset(entry_name) l.write_main_cmd(entry_off) l.write_uuid() l.write_build_version() l.write_source_version() // Write LC_CODE_SIGNATURE (will be at cs_off with size cs_size) codesig_cmd_start := l.buf.len l.write_code_signature_cmd(cs_off, cs_size) // Patch LINKEDIT segment with actual values (including signature) l.patch_linkedit(linkedit_start, bind_off, l.linkedit_size) println(' headers+cmds: ${time.since(t)}') t = time.now() // Pad to code start (after header + load commands) l.pad_to(l.code_start) // Write text section with relocations applied l.write_text_with_relocations() println(' text+relocs: ${time.since(t)}') t = time.now() // Write cstring section l.buf << l.macho.str_data // Pad and write stubs l.pad_to(l.stubs_offset) l.write_stubs() // Pad to data start l.pad_to(l.data_fileoff) // Write data section l.buf << l.macho.data_data // Pad to GOT offset and write GOT (initially zeros, dyld will fill) l.pad_to(l.data_fileoff + got_offset) l.write_zeros(l.extern_syms.len * 8) // Pad data segment l.pad_to(l.data_fileoff + l.data_size) // Write LINKEDIT content l.buf << bind_info l.write_zeros(symtab_pad) // Write symbol table nlist entries l.buf << symtab_data // Write string table l.buf << strtab_data // Align code signature start in LINKEDIT. l.write_zeros(cs_pad) println(' padding+data: ${time.since(t)}') t = time.now() // Generate and write code signature (ad-hoc signing) signature := l.generate_code_signature(ident) l.buf << signature // Patch LC_CODE_SIGNATURE if size differs from estimate actual_cs_size := signature.len if actual_cs_size != cs_size { // Patch datasize in LC_CODE_SIGNATURE command write_u32_le_at(mut l.buf, codesig_cmd_start + 12, u32(actual_cs_size)) } println(' codesign: ${time.since(t)}') t = time.now() tmp_output_path := '${output_path}.tmp.${os.getpid()}' if !write_file_array_raw(tmp_output_path, l.buf) { panic('failed to write output file') } if C.chmod(tmp_output_path.str, 0o755) != 0 { panic('failed to chmod output file') } if C.rename(tmp_output_path.str, output_path.str) != 0 { panic('failed to rename output file') } println(' file write: ${time.since(t)}') println(' TOTAL linker: ${time.since(t_total)}') } fn write_file_array_raw(path string, data []u8) bool { fd := C.open(path.str, o_wronly_creat_trunc, 0o755) if fd < 0 { return false } if data.len > 0 { written := C.write(fd, data.data, data.len) if written != data.len { C.close(fd) return false } } return C.close(fd) == 0 } fn (mut l Linker) write_header(ncmds int, cmdsize int) { write_mh_magic_64(mut l.buf) write_u32_le(mut l.buf, u32(cpu_type_arm64)) write_u32_le(mut l.buf, u32(cpu_subtype_arm64_all)) write_u32_le(mut l.buf, mh_execute) write_u32_le(mut l.buf, u32(ncmds)) write_u32_le(mut l.buf, u32(cmdsize)) write_u32_le(mut l.buf, 0x00200085) // MH_NOUNDEFS | MH_DYLDLINK | MH_TWOLEVEL | MH_PIE write_u32_le(mut l.buf, 0) // reserved } fn (mut l Linker) write_pagezero_segment() { write_u32_le(mut l.buf, u32(lc_segment_64)) write_u32_le(mut l.buf, 72) write_string_fixed(mut l.buf, '__PAGEZERO', 16) write_u64_le(mut l.buf, 0) // vmaddr write_u64_le(mut l.buf, base_addr) // vmsize write_u64_le(mut l.buf, 0) // fileoff write_u64_le(mut l.buf, 0) // filesize write_u32_le(mut l.buf, 0) // maxprot write_u32_le(mut l.buf, 0) // initprot write_u32_le(mut l.buf, 0) // nsects write_u32_le(mut l.buf, 0) // flags } fn (mut l Linker) write_text_segment() { write_u32_le(mut l.buf, u32(lc_segment_64)) write_u32_le(mut l.buf, 72 + 80 * 2) // cmd size with 2 sections write_string_fixed(mut l.buf, '__TEXT', 16) write_u64_le(mut l.buf, l.text_vmaddr) // vmaddr = base_addr write_u64_le(mut l.buf, u64(l.text_size)) // vmsize write_u64_le(mut l.buf, 0) // fileoff MUST be 0 write_u64_le(mut l.buf, u64(l.text_size)) // filesize write_u32_le(mut l.buf, 5) // maxprot (r-x) write_u32_le(mut l.buf, 5) // initprot (r-x) write_u32_le(mut l.buf, 2) // nsects write_u32_le(mut l.buf, 0) // flags // __text section (code starts at code_start offset) write_string_fixed(mut l.buf, '__text', 16) write_string_fixed(mut l.buf, '__TEXT', 16) write_u64_le(mut l.buf, l.text_vmaddr + u64(l.code_start)) // addr write_u64_le(mut l.buf, u64(l.macho.text_data.len)) // size write_u32_le(mut l.buf, u32(l.code_start)) // offset write_u32_le(mut l.buf, 4) // align (16 bytes = 2^4) write_u32_le(mut l.buf, 0) // reloff write_u32_le(mut l.buf, 0) // nreloc write_u32_le(mut l.buf, 0x80000400) // flags: S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS write_u32_le(mut l.buf, 0) // reserved1 write_u32_le(mut l.buf, 0) // reserved2 write_u32_le(mut l.buf, 0) // reserved3 // __stubs section - using regular code section flags since we use immediate binding write_string_fixed(mut l.buf, '__stubs', 16) write_string_fixed(mut l.buf, '__TEXT', 16) write_u64_le(mut l.buf, l.text_vmaddr + u64(l.stubs_offset)) // addr write_u64_le(mut l.buf, u64(l.stubs_size)) // size write_u32_le(mut l.buf, u32(l.stubs_offset)) // offset write_u32_le(mut l.buf, 2) // align write_u32_le(mut l.buf, 0) // reloff write_u32_le(mut l.buf, 0) // nreloc write_u32_le(mut l.buf, 0x80000400) // S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS (no S_SYMBOL_STUBS) write_u32_le(mut l.buf, 0) // reserved1 write_u32_le(mut l.buf, 0) // reserved2 write_u32_le(mut l.buf, 0) // reserved3 } fn (mut l Linker) write_data_segment() { write_u32_le(mut l.buf, u32(lc_segment_64)) write_u32_le(mut l.buf, 72 + 80 * 2) // cmd size with 2 sections write_string_fixed(mut l.buf, '__DATA', 16) write_u64_le(mut l.buf, l.data_vmaddr) // vmaddr write_u64_le(mut l.buf, u64(l.data_size)) // vmsize write_u64_le(mut l.buf, u64(l.data_fileoff)) // fileoff write_u64_le(mut l.buf, u64(l.data_size)) // filesize write_u32_le(mut l.buf, 3) // maxprot (rw-) write_u32_le(mut l.buf, 3) // initprot (rw-) write_u32_le(mut l.buf, 2) // nsects write_u32_le(mut l.buf, 0) // flags // __data section write_string_fixed(mut l.buf, '__data', 16) write_string_fixed(mut l.buf, '__DATA', 16) write_u64_le(mut l.buf, l.data_vmaddr) // addr write_u64_le(mut l.buf, u64(l.macho.data_data.len)) // size write_u32_le(mut l.buf, u32(l.data_fileoff)) // offset write_u32_le(mut l.buf, 3) // align (8 bytes = 2^3) write_u32_le(mut l.buf, 0) // reloff write_u32_le(mut l.buf, 0) // nreloc write_u32_le(mut l.buf, 0) // flags write_u32_le(mut l.buf, 0) // reserved1 write_u32_le(mut l.buf, 0) // reserved2 write_u32_le(mut l.buf, 0) // reserved3 // __got section - using regular data section since we use immediate binding via bind info write_string_fixed(mut l.buf, '__got', 16) write_string_fixed(mut l.buf, '__DATA', 16) write_u64_le(mut l.buf, l.data_vmaddr + u64(l.got_offset)) // addr write_u64_le(mut l.buf, u64(l.got_size)) // size write_u32_le(mut l.buf, u32(l.data_fileoff + l.got_offset)) // offset write_u32_le(mut l.buf, 3) // align write_u32_le(mut l.buf, 0) // reloff write_u32_le(mut l.buf, 0) // nreloc write_u32_le(mut l.buf, 0x00) // S_REGULAR (no special flags - dyld will fill via bind info) write_u32_le(mut l.buf, 0) // reserved1 write_u32_le(mut l.buf, 0) // reserved2 write_u32_le(mut l.buf, 0) // reserved3 } fn (mut l Linker) write_linkedit_segment() { write_u32_le(mut l.buf, u32(lc_segment_64)) write_u32_le(mut l.buf, 72) write_string_fixed(mut l.buf, '__LINKEDIT', 16) write_u64_le(mut l.buf, 0) // vmaddr - patched later write_u64_le(mut l.buf, 0) // vmsize - patched later write_u64_le(mut l.buf, 0) // fileoff - patched later write_u64_le(mut l.buf, 0) // filesize - patched later write_u32_le(mut l.buf, 1) // maxprot (r--) write_u32_le(mut l.buf, 1) // initprot (r--) write_u32_le(mut l.buf, 0) // nsects write_u32_le(mut l.buf, 0) // flags } fn (mut l Linker) patch_linkedit(cmd_start int, fileoff int, filesize int) { linkedit_vmaddr := l.data_vmaddr + u64(l.data_size) mut linkedit_vmsize := u64((filesize + page_size - 1) & ~(page_size - 1)) if linkedit_vmsize == 0 { linkedit_vmsize = u64(page_size) } // Patch vmaddr, vmsize, fileoff, filesize at known offsets within LINKEDIT cmd off := cmd_start + 8 + 16 // after cmd, cmdsize, segname write_u64_le_at(mut l.buf, off, linkedit_vmaddr) write_u64_le_at(mut l.buf, off + 8, linkedit_vmsize) write_u64_le_at(mut l.buf, off + 16, u64(fileoff)) write_u64_le_at(mut l.buf, off + 24, u64(filesize)) } fn (mut l Linker) write_dyld_info(bind_off int, bind_size int) { write_u32_le(mut l.buf, u32(lc_dyld_info_only)) write_u32_le(mut l.buf, 48) write_u32_le(mut l.buf, 0) // rebase_off write_u32_le(mut l.buf, 0) // rebase_size write_u32_le(mut l.buf, u32(bind_off)) // bind_off write_u32_le(mut l.buf, u32(bind_size)) // bind_size write_u32_le(mut l.buf, 0) // weak_bind_off write_u32_le(mut l.buf, 0) // weak_bind_size write_u32_le(mut l.buf, 0) // lazy_bind_off write_u32_le(mut l.buf, 0) // lazy_bind_size write_u32_le(mut l.buf, 0) // export_off write_u32_le(mut l.buf, 0) // export_size } fn (mut l Linker) write_symtab(symoff int, nsyms int, stroff int, strsize int) { write_u32_le(mut l.buf, u32(lc_symtab)) write_u32_le(mut l.buf, 24) write_u32_le(mut l.buf, u32(symoff)) write_u32_le(mut l.buf, u32(nsyms)) write_u32_le(mut l.buf, u32(stroff)) write_u32_le(mut l.buf, u32(strsize)) } fn (mut l Linker) write_dysymtab(_nsyms int) { write_u32_le(mut l.buf, u32(lc_dysymtab)) write_u32_le(mut l.buf, 80) write_u32_le(mut l.buf, 0) // ilocalsym write_u32_le(mut l.buf, 0) // nlocalsym write_u32_le(mut l.buf, 0) // iextdefsym write_u32_le(mut l.buf, 0) // nextdefsym write_u32_le(mut l.buf, 0) // iundefsym write_u32_le(mut l.buf, 0) // nundefsym write_u32_le(mut l.buf, 0) // tocoff write_u32_le(mut l.buf, 0) // ntoc write_u32_le(mut l.buf, 0) // modtaboff write_u32_le(mut l.buf, 0) // nmodtab write_u32_le(mut l.buf, 0) // extrefsymoff write_u32_le(mut l.buf, 0) // nextrefsyms write_u32_le(mut l.buf, 0) // indirectsymoff write_u32_le(mut l.buf, 0) // nindirectsyms write_u32_le(mut l.buf, 0) // extreloff write_u32_le(mut l.buf, 0) // nextrel write_u32_le(mut l.buf, 0) // locreloff write_u32_le(mut l.buf, 0) // nlocrel } fn (mut l Linker) write_load_dylinker() { write_u32_le(mut l.buf, u32(lc_load_dylinker)) write_u32_le(mut l.buf, 32) write_u32_le(mut l.buf, 12) // offset to string write_string_fixed(mut l.buf, '/usr/lib/dyld', 20) } fn (mut l Linker) write_load_dylibs() { for dylib_path in l.dylibs { path_len := dylib_path.len + 1 // +1 for null terminator padded_path := (path_len + 7) & ~7 cmd_size := 24 + padded_path // header (24) + padded path write_u32_le(mut l.buf, u32(lc_load_dylib)) write_u32_le(mut l.buf, u32(cmd_size)) write_u32_le(mut l.buf, 24) // offset to string (always 24 in header) write_u32_le(mut l.buf, 0) // timestamp write_u32_le(mut l.buf, 0x10000) // current version write_u32_le(mut l.buf, 0x10000) // compatibility version write_string_fixed(mut l.buf, dylib_path, padded_path) } } fn (mut l Linker) write_main_cmd(entry_off int) { write_u32_le(mut l.buf, u32(lc_main)) write_u32_le(mut l.buf, 24) write_u64_le(mut l.buf, u64(entry_off)) // entryoff (offset from __TEXT start) write_u64_le(mut l.buf, 0) // stacksize } fn (mut l Linker) write_uuid() { write_u32_le(mut l.buf, u32(lc_uuid)) write_u32_le(mut l.buf, 24) // Random UUID for _ in 0 .. 16 { l.buf << 0 } } fn (mut l Linker) write_build_version() { write_u32_le(mut l.buf, u32(lc_build_version)) write_u32_le(mut l.buf, 24) write_u32_le(mut l.buf, 1) // platform: MACOS write_u32_le(mut l.buf, 0x000b0000) // minos: 11.0.0 write_u32_le(mut l.buf, 0x000b0000) // sdk: 11.0.0 write_u32_le(mut l.buf, 0) // ntools } fn (mut l Linker) write_source_version() { write_u32_le(mut l.buf, u32(lc_source_version)) write_u32_le(mut l.buf, 16) write_u64_le(mut l.buf, 0) // version } fn (mut l Linker) write_code_signature_cmd(dataoff int, datasize int) { write_u32_le(mut l.buf, u32(lc_code_signature)) write_u32_le(mut l.buf, 16) // cmdsize write_u32_le(mut l.buf, u32(dataoff)) write_u32_le(mut l.buf, u32(datasize)) } fn (l &Linker) estimate_signature_size(code_limit int, ident string) int { // Calculate pages using ARM64 16KB page size n_pages := (code_limit + cs_page_size_arm64 - 1) / cs_page_size_arm64 // SuperBlob header (12) + 3 BlobIndex entries (24) // + CodeDirectory + Requirements blob + CMS blob ident_len := ident.len + 1 // null terminated n_special_slots := 2 special_hashes_size := n_special_slots * cs_hash_size // CodeDirectory: header (88 for version 0x20400) + ident + special hashes + code hashes cd_size := 88 + ident_len + special_hashes_size + (n_pages * cs_hash_size) // Round up to 4-byte alignment cd_size_aligned := (cd_size + 3) & ~3 // Requirements blob: minimal empty requirements (12 bytes) req_size := 12 // CMS blob: empty wrapper (8 bytes) cms_size := 8 // Total: SuperBlob(12) + 3*BlobIndex(8) + CodeDirectory + Requirements + CMS return 12 + 24 + cd_size_aligned + req_size + cms_size } fn (l &Linker) generate_code_signature(ident string) []u8 { mut sig := []u8{} // Calculate sizes using ARM64 16KB pages code_limit := l.buf.len // Current buffer is the code to hash n_pages := (code_limit + cs_page_size_arm64 - 1) / cs_page_size_arm64 ident_bytes := ident.bytes() ident_len := ident_bytes.len + 1 // null terminated // Special slots: we need at least slot for requirements (-2) n_special_slots := 2 // Slots -1 (info.plist) and -2 (requirements) special_hashes_size := n_special_slots * cs_hash_size // CodeDirectory layout for version 0x20400: // - Base header (44 bytes): magic, length, version, flags, hashOffset, identOffset, // nSpecialSlots, nCodeSlots, codeLimit, hashSize, hashType, platform, pageSize, spare2 // - scatterOffset (4 bytes) // - teamOffset (4 bytes) // - spare3 (4 bytes) // - codeLimit64 (8 bytes) // - execSegBase (8 bytes) // - execSegLimit (8 bytes) // - execSegFlags (8 bytes) // Total header: 88 bytes cd_header_size := 88 ident_offset := cd_header_size hash_offset := ident_offset + ident_len + special_hashes_size cd_size := hash_offset + (n_pages * cs_hash_size) cd_size_aligned := (cd_size + 3) & ~3 // Requirements blob (empty) req_size := 12 // CMS signature blob (empty wrapper for ad-hoc) cms_size := 8 // SuperBlob layout with 3 blobs blob_count := 3 // CodeDirectory + Requirements + CMS super_blob_header := 12 blob_index_size := blob_count * 8 cd_blob_offset := super_blob_header + blob_index_size req_blob_offset := cd_blob_offset + cd_size_aligned cms_blob_offset := req_blob_offset + req_size total_size := cms_blob_offset + cms_size // Write SuperBlob header (big-endian) write_u32_be(mut sig, csmagic_embedded_signature) write_u32_be(mut sig, u32(total_size)) write_u32_be(mut sig, u32(blob_count)) // BlobIndex for CodeDirectory (type = 0 = CSSLOT_CODEDIRECTORY) write_u32_be(mut sig, csslot_codedirectory) write_u32_be(mut sig, u32(cd_blob_offset)) // BlobIndex for Requirements (type = 2 = CSSLOT_REQUIREMENTS) write_u32_be(mut sig, csslot_requirements) write_u32_be(mut sig, u32(req_blob_offset)) // BlobIndex for CMS signature (type = 0x10000) write_u32_be(mut sig, csslot_cms_signature) write_u32_be(mut sig, u32(cms_blob_offset)) // Write CodeDirectory (big-endian) - version 0x20400 write_u32_be(mut sig, csmagic_codedirectory) write_u32_be(mut sig, u32(cd_size)) write_u32_be(mut sig, 0x20400) // version write_u32_be(mut sig, cs_adhoc) // flags (ad-hoc) write_u32_be(mut sig, u32(hash_offset)) // hashOffset write_u32_be(mut sig, u32(ident_offset)) // identOffset write_u32_be(mut sig, u32(n_special_slots)) // nSpecialSlots write_u32_be(mut sig, u32(n_pages)) // nCodeSlots write_u32_be(mut sig, u32(code_limit)) // codeLimit sig << cs_hash_size // hashSize sig << cs_hashtype_sha256 // hashType sig << 0 // platform sig << cs_page_shift_arm64 // pageSize (log2 of 16384 = 14) write_u32_be(mut sig, 0) // spare2 // Version 0x20400 additional fields: write_u32_be(mut sig, 0) // scatterOffset (0 = none) write_u32_be(mut sig, 0) // teamOffset (0 = none) write_u32_be(mut sig, 0) // spare3 write_u64_be(mut sig, 0) // codeLimit64 (0 = use codeLimit) write_u64_be(mut sig, 0) // execSegBase (0 = __TEXT starts at 0) write_u64_be(mut sig, u64(l.text_size)) // execSegLimit (size of __TEXT segment) write_u64_be(mut sig, 1) // execSegFlags (CS_EXECSEG_MAIN_BINARY = 1) // Write identifier (null-terminated) for i in 0 .. ident_bytes.len { sig << ident_bytes[i] } sig << 0 // Write special slot hashes (slots -2, -1 in that order) // Build the requirements blob first so we can hash it mut req_blob := []u8{} write_u32_be(mut req_blob, csmagic_requirements) write_u32_be(mut req_blob, u32(req_size)) write_u32_be(mut req_blob, 0) // count = 0 // Slot -2: Hash of requirements blob mut hash_buf := [32]u8{} sha256_hash(req_blob.data, req_blob.len, &hash_buf[0]) for i in 0 .. cs_hash_size { sig << hash_buf[i] } // Slot -1: Info.plist (zeros = no Info.plist) for _ in 0 .. cs_hash_size { sig << 0 } // Compute page hashes in parallel (16KB pages) mut all_hashes := []u8{len: n_pages * 32} data_ptr := unsafe { &u8(l.buf.data) } // Hash all pages sequentially. V's `spawn` is not supported on the native // ARM64 backend, so we avoid threads here for self-hosting compatibility. sha256_hash_pages(data_ptr, mut all_hashes, 0, n_pages, code_limit) for i in 0 .. all_hashes.len { sig << all_hashes[i] } // Pad CodeDirectory to alignment for sig.len < cd_blob_offset + cd_size_aligned { sig << 0 } // Write Requirements blob for i in 0 .. req_blob.len { sig << req_blob[i] } // Write empty CMS signature blob (for ad-hoc signing) write_u32_be(mut sig, csmagic_blobwrapper) write_u32_be(mut sig, u32(cms_size)) return sig } fn (mut l Linker) find_entry_offset(entry_name string) int { // Find the _main symbol // LC_MAIN entryoff is relative to __TEXT segment vmaddr // Code section starts at code_start within __TEXT for sym in l.macho.symbols { if sym.name == entry_name && sym.sect == 1 { return l.code_start + int(sym.value) } } return l.code_start // Default to start of code section } fn (mut l Linker) generate_bind_info(got_offset int) []u8 { mut info := []u8{} // Data segment index (segment 2: __PAGEZERO=0, __TEXT=1, __DATA=2) data_seg_idx := u8(2) mut got_entry_offset := got_offset for sym_name in l.extern_syms { // Internal runtime callback names can appear as unresolved function refs in // bootstrap builds. Bind them as weak imports so dyld does not abort load // when they are absent from libSystem; unresolved weak symbols become NULL. mut bind_flags := u8(0) if sym_name.contains('__') && sym_name !in force_external_syms { bind_flags = bind_symbol_flags_weak_import } // Set dylib ordinal (1-based: 1 = first dylib) mut ordinal_idx := 0 if idx := l.sym_to_dylib[sym_name] { ordinal_idx = idx } ordinal := u8(ordinal_idx + 1) info << (bind_opcode_set_dylib_ordinal_imm | ordinal) // Set symbol name info << (bind_opcode_set_symbol_flags_imm | bind_flags) info << sym_name.bytes() info << 0 // null terminator // Set type (pointer) info << (bind_opcode_set_type_imm | bind_type_pointer) // Set segment and offset info << (bind_opcode_set_segment_and_offset_uleb | data_seg_idx) info << l.encode_uleb128_int(got_entry_offset) // Do bind info << bind_opcode_do_bind got_entry_offset += 8 } // Done info << bind_opcode_done return info } fn (l Linker) encode_uleb128_int(val int) []u8 { mut result := []u8{} if val < 0x80 { result << u8(val) return result } if val < 0x4000 { result << (u8(val & 0x7f) | 0x80) result << u8((val >> 7) & 0x7f) return result } if val < 0x200000 { result << (u8(val & 0x7f) | 0x80) result << (u8((val >> 7) & 0x7f) | 0x80) result << u8((val >> 14) & 0x7f) return result } return l.encode_uleb128(u64(u32(val))) } fn (l Linker) encode_uleb128(val u64) []u8 { mut result := []u8{} mut v := val for { mut b := u8(v & 0x7f) v >>= 7 if v != 0 { b |= 0x80 } result << b if v == 0 { break } } return result } fn (mut l Linker) write_text_with_relocations() { // Copy text data mut text := l.macho.text_data.clone() // Build symbol address map // Note: code section vmaddr = text_vmaddr + code_start // Symbol values are offsets from segment start, so we use code_vmaddr for all __TEXT symbols code_vmaddr := l.text_vmaddr + u64(l.code_start) stubs_vmaddr := l.text_vmaddr + u64(l.stubs_offset) // In the object file, __data section starts at text_len + cstring_len + alignment_padding // We need to find the actual base address of data symbols (minimum symbol value in sect 3) // to correctly compute offsets within the data_data array mut data_base_addr := u64(0xFFFFFFFFFFFFFFFF) // Start with max, find minimum for sym in l.macho.symbols { if (sym.type_ & 0x0E) == 0x0E && sym.sect == 3 { if sym.value < data_base_addr { data_base_addr = sym.value } } } // If no data symbols, use section start if data_base_addr == 0xFFFFFFFFFFFFFFFF { data_base_addr = u64(l.macho.text_data.len + l.macho.str_data.len) } mut sym_addrs := map[int]u64{} // Map symbol names to their defined addresses (for resolving undefined references) mut sym_name_to_addr := map[string]u64{} // First pass: collect all defined symbol addresses (except external syms) for i, sym in l.macho.symbols { // N_SECT (0x0E) means symbol is defined in a section if (sym.type_ & 0x0E) == 0x0E { // Skip external symbols - they should always resolve to libc is_external := sym.name in force_external_syms if sym.sect == 1 { // Text section symbol (code) addr := code_vmaddr + sym.value sym_addrs[i] = addr if !is_external { sym_name_to_addr[sym.name] = addr } } else if sym.sect == 2 { // Cstring section symbol addr := code_vmaddr + sym.value sym_addrs[i] = addr if !is_external { sym_name_to_addr[sym.name] = addr } } else if sym.sect == 3 { // Data section symbol // Subtract data base address to get offset within data_data array addr := l.data_vmaddr + (sym.value - data_base_addr) sym_addrs[i] = addr if !is_external { sym_name_to_addr[sym.name] = addr } } } } // Second pass: handle external symbols and resolve undefined references to local symbols for i, sym in l.macho.symbols { if sym.type_ == 0x01 { // N_UNDF | N_EXT // Check if this symbol is defined locally if addr := sym_name_to_addr[sym.name] { // Resolve to local definition sym_addrs[i] = addr } else if sym.name in l.sym_to_got { // External symbol - address is in stub got_idx := l.sym_to_got[sym.name] sym_addrs[i] = stubs_vmaddr + u64(got_idx * 12) } } } // Apply relocations for r in l.macho.relocs { // Check if this relocation references an external symbol // If so, redirect it to use the stub instead of the local definition sym_name := l.macho.symbols[r.sym_idx].name mut sym_addr := sym_addrs[r.sym_idx] if sym_addr == 0 && r.sym_idx !in sym_addrs { eprintln('LINKER: unresolved symbol "${sym_name}" (idx=${r.sym_idx}) at text offset ${r.addr}') exit(1) } if sym_name in force_external_syms { // Use stub address for external symbols if sym_name in l.sym_to_got { got_idx := l.sym_to_got[sym_name] sym_addr = stubs_vmaddr + u64(got_idx * 12) } } pc := code_vmaddr + u64(r.addr) match r.type_ { arm64_reloc_branch26 { // BL instruction: PC-relative branch if sym_addr == 0 { eprintln('LINKER: unresolved BL to "${sym_name}" (idx=${r.sym_idx}) at text offset ${r.addr}') exit(1) } rel := i64(sym_addr) - i64(pc) imm26 := (rel >> 2) & 0x3FFFFFF instr := read_u32_le(text, r.addr) new_instr := (instr & 0xFC000000) | u32(imm26) write_u32_le_at_arr(mut text, r.addr, new_instr) } arm64_reloc_page21 { // ADRP instruction: PC-relative page address sym_page := i64(sym_addr) & ~0xFFF pc_page := i64(pc) & ~0xFFF page_off := (sym_page - pc_page) >> 12 immlo := u32(page_off & 0x3) << 29 immhi := u32((page_off >> 2) & 0x7FFFF) << 5 instr := read_u32_le(text, r.addr) new_instr := (instr & 0x9F00001F) | immlo | immhi write_u32_le_at_arr(mut text, r.addr, new_instr) } arm64_reloc_pageoff12 { // ADD/LDR instruction: page offset page_off := sym_addr & 0xFFF instr := read_u32_le(text, r.addr) // Check if this is ADD or LDR if (instr & 0xFF800000) == 0x91000000 { // ADD immediate new_instr := (instr & 0xFFC003FF) | (u32(page_off) << 10) write_u32_le_at_arr(mut text, r.addr, new_instr) } else { // LDR with scaled offset // Determine scale from instruction encoding scale := (instr >> 30) & 0x3 scaled_off := page_off >> scale new_instr := (instr & 0xFFC003FF) | (u32(scaled_off) << 10) write_u32_le_at_arr(mut text, r.addr, new_instr) } } arm64_reloc_got_load_page21 { // ADRP instruction: PC-relative page address to GOT entry mut got_idx1 := 0 if idx := l.sym_to_got[sym_name] { got_idx1 = idx } got_entry_addr1 := l.data_vmaddr + u64(l.got_offset) + u64(got_idx1 * 8) got_page := i64(got_entry_addr1) & ~0xFFF pc_page := i64(pc) & ~0xFFF page_off := (got_page - pc_page) >> 12 immlo := u32(page_off & 0x3) << 29 immhi := u32((page_off >> 2) & 0x7FFFF) << 5 instr := read_u32_le(text, r.addr) new_instr := (instr & 0x9F00001F) | immlo | immhi write_u32_le_at_arr(mut text, r.addr, new_instr) } arm64_reloc_got_load_pageoff12 { // LDR instruction: page offset to GOT entry mut got_idx2 := 0 if idx := l.sym_to_got[sym_name] { got_idx2 = idx } got_entry_addr2 := l.data_vmaddr + u64(l.got_offset) + u64(got_idx2 * 8) page_off := got_entry_addr2 & 0xFFF instr := read_u32_le(text, r.addr) // LDR with scaled offset (8-byte scale for 64-bit load) scaled_off := page_off >> 3 new_instr := (instr & 0xFFC003FF) | (u32(scaled_off) << 10) write_u32_le_at_arr(mut text, r.addr, new_instr) } else {} } } l.buf << text } fn (mut l Linker) write_stubs() { // Generate stub for each external symbol // Each stub: ADRP x16, GOT@PAGE; LDR x16, [x16, GOT@PAGEOFF]; BR x16 mut got_entry_offset := l.macho.data_data.len for got_entry_offset % 8 != 0 { got_entry_offset++ } mut stub_offset := l.stubs_offset for _ in l.extern_syms { got_entry_addr := l.data_vmaddr + u64(u32(got_entry_offset)) stub_addr := l.text_vmaddr + u64(stub_offset) // ADRP x16, got_entry@PAGE got_page := i64(got_entry_addr) & ~0xFFF stub_page := i64(stub_addr) & ~0xFFF page_off := (got_page - stub_page) >> 12 immlo := u32(page_off & 0x3) << 29 immhi := u32((page_off >> 2) & 0x7FFFF) << 5 adrp := u32(0x90000010) | immlo | immhi write_u32_le(mut l.buf, adrp) // LDR x16, [x16, got_entry@PAGEOFF] pageoff := (got_entry_addr & 0xFFF) >> 3 // Scale by 8 for 64-bit load ldr := u32(0xF9400210) | (u32(pageoff) << 10) write_u32_le(mut l.buf, ldr) // BR x16 write_u32_le(mut l.buf, 0xD61F0200) got_entry_offset += 8 stub_offset += 12 } } fn read_u32_le(data []u8, off int) u32 { b0 := u32(data[off]) & u32(0xff) b1 := (u32(data[off + 1]) & u32(0xff)) << 8 b2 := (u32(data[off + 2]) & u32(0xff)) << 16 b3 := (u32(data[off + 3]) & u32(0xff)) << 24 return b0 | b1 | b2 | b3 } fn write_u32_le_at_arr(mut data []u8, off int, v u32) { data[off] = u8(v) data[off + 1] = u8(v >> 8) data[off + 2] = u8(v >> 16) data[off + 3] = u8(v >> 24) } fn write_u32_le_at(mut data []u8, off int, v u32) { data[off] = u8(v) data[off + 1] = u8(v >> 8) data[off + 2] = u8(v >> 16) data[off + 3] = u8(v >> 24) } // Big-endian write for code signature (Mach-O signatures use big-endian) fn write_u32_be(mut b []u8, v u32) { b << u8(v >> 24) b << u8(v >> 16) b << u8(v >> 8) b << u8(v) } fn write_u64_be(mut b []u8, v u64) { b << u8(v >> 56) b << u8(v >> 48) b << u8(v >> 40) b << u8(v >> 32) b << u8(v >> 24) b << u8(v >> 16) b << u8(v >> 8) b << u8(v) } fn write_u64_le_at(mut b []u8, off int, v u64) { b[off] = u8(v) b[off + 1] = u8(v >> 8) b[off + 2] = u8(v >> 16) b[off + 3] = u8(v >> 24) b[off + 4] = u8(v >> 32) b[off + 5] = u8(v >> 40) b[off + 6] = u8(v >> 48) b[off + 7] = u8(v >> 56) } // Pad buffer to target size with zeros (efficient bulk write) fn (mut l Linker) pad_to(target int) { if l.buf.len >= target { return } count := target - l.buf.len for _ in 0 .. count { l.buf << u8(0) } } // Write n zero bytes (efficient) fn (mut l Linker) write_zeros(n int) { if n <= 0 { return } for _ in 0 .. n { l.buf << u8(0) } } // Self-contained SHA-256 implementation. Zero heap allocations — // uses fixed-size arrays and operates on raw pointers. const sha256_k = [ u32(0x428a2f98), 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, ]! @[inline] fn rotr32(x u32, n u32) u32 { return (x >> n) | (x << (32 - n)) } // sha256_hash_pages hashes a range of 16KB pages in a worker thread. // Each thread writes 32-byte hashes into disjoint slots of the pre-allocated output buffer. fn sha256_hash_pages(data &u8, mut hashes []u8, page_start int, page_end int, code_limit int) { mut hash_buf := [32]u8{} for page := page_start; page < page_end; page++ { start := page * cs_page_size_arm64 mut end := start + cs_page_size_arm64 if end > code_limit { end = code_limit } unsafe { sha256_hash(data + start, end - start, &hash_buf[0]) } hash_offset := page * 32 for i in 0 .. 32 { hashes[hash_offset + i] = hash_buf[i] } } } // sha256_hash computes SHA-256 of data[0..data_len] into out[0..32]. // No heap allocations — uses fixed-size arrays on the stack. @[direct_array_access] fn sha256_hash(data &u8, data_len int, out_ptr &u8) { mut state := [u32(0x6A09E667), 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]! mut w := [64]u32{} // Process complete 64-byte blocks directly from input n_full_blocks := data_len / 64 for blk := 0; blk < n_full_blocks; blk++ { off := blk * 64 for i in 0 .. 16 { j := off + i * 4 unsafe { w[i] = (u32(data[j]) << 24) | (u32(data[j + 1]) << 16) | (u32(data[j + 2]) << 8) | u32(data[ j + 3]) } } sha256_compress(&state[0], &w[0]) } // Build final padded block(s): remaining data + 0x80 + zeros + 64-bit big-endian length remaining := data_len - n_full_blocks * 64 mut pad := [128]u8{} // At most 2 final blocks for i in 0 .. remaining { unsafe { pad[i] = data[n_full_blocks * 64 + i] } } pad[remaining] = 0x80 // Need room for 8-byte length at end of last 64-byte block mut pad_blocks := 1 if remaining >= 56 { pad_blocks = 2 } // Write bit length (big-endian u64) at end of last padding block bit_len := u64(data_len) * 8 pad_end := pad_blocks * 64 pad[pad_end - 8] = u8(bit_len >> 56) pad[pad_end - 7] = u8(bit_len >> 48) pad[pad_end - 6] = u8(bit_len >> 40) pad[pad_end - 5] = u8(bit_len >> 32) pad[pad_end - 4] = u8(bit_len >> 24) pad[pad_end - 3] = u8(bit_len >> 16) pad[pad_end - 2] = u8(bit_len >> 8) pad[pad_end - 1] = u8(bit_len) for blk in 0 .. pad_blocks { off := blk * 64 for i in 0 .. 16 { j := off + i * 4 w[i] = (u32(pad[j]) << 24) | (u32(pad[j + 1]) << 16) | (u32(pad[j + 2]) << 8) | u32(pad[ j + 3]) } sha256_compress(&state[0], &w[0]) } // Write result big-endian mut out := unsafe { &u8(out_ptr) } for i in 0 .. 8 { unsafe { out[i * 4] = u8(state[i] >> 24) out[i * 4 + 1] = u8(state[i] >> 16) out[i * 4 + 2] = u8(state[i] >> 8) out[i * 4 + 3] = u8(state[i]) } } } @[direct_array_access] fn sha256_compress(state_ptr &u32, w_ptr &u32) { mut state := unsafe { &u32(state_ptr) } mut w := unsafe { &u32(w_ptr) } // Extend the first 16 words into the remaining 48 for i := 16; i < 64; i++ { unsafe { s0 := rotr32(w[i - 15], 7) ^ rotr32(w[i - 15], 18) ^ (w[i - 15] >> 3) s1 := rotr32(w[i - 2], 17) ^ rotr32(w[i - 2], 19) ^ (w[i - 2] >> 10) w[i] = w[i - 16] + s0 + w[i - 7] + s1 } } mut a := unsafe { state[0] } mut b := unsafe { state[1] } mut c := unsafe { state[2] } mut d := unsafe { state[3] } mut e := unsafe { state[4] } mut f := unsafe { state[5] } mut g := unsafe { state[6] } mut h := unsafe { state[7] } for i in 0 .. 64 { s1 := rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25) ch := (e & f) ^ (~e & g) t1 := h + s1 + ch + sha256_k[i] + unsafe { w[i] } s0 := rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22) maj := (a & b) ^ (a & c) ^ (b & c) t2 := s0 + maj h = g g = f f = e e = d + t1 d = c c = b b = a a = t1 + t2 } unsafe { state[0] += a state[1] += b state[2] += c state[3] += d state[4] += e state[5] += f state[6] += g state[7] += h } }