authorgravatar for kubkon@jakubkonka.comJakub Konka <kubkon@jakubkonka.com> 2022-12-14 15:15:20+01:00
committergravatar for kubkon@jakubkonka.comJakub Konka <kubkon@jakubkonka.com> 2022-12-16 00:01:04+01:00
log79457fc76a61695560e6314246b0a8c21a7e2d2c
tree7a35a25f880c057e27098b240f8fac7e42b842eb
parentec40c6b28fb1612f401db3c43b68aba670327e2e

macho: generalize parallel hasher; impl parallel MD5-like hash

By pulling out the parallel hashing setup from `CodeSignature.zig`, we can now reuse it different places across MachO linker (for now; I can totally see its usefulness beyond MachO, eg. in COFF or ELF too). The parallel hasher is generic over actual hasher such as Sha256 or MD5. The implementation is kept as it was. For UUID calculation, depending on the linking mode: * incremental - since it only supports debug mode, we don't bother with MD5 hashing of the contents, and populate it with random data but only once per a sequence of in-place binary patches * traditional - in debug, we use random string (for speed); in release, we calculate the hash, however we use LLVM/LLD's trick in that we calculate a series of MD5 hashes in parallel and then one an MD5 of MD5 final hash to generate digest.

7 files changed, 160 insertions(+), 59 deletions(-)

CMakeLists.txt+1
...@@ -591,6 +591,7 @@ set(ZIG_STAGE2_SOURCES...@@ -591,6 +591,7 @@ set(ZIG_STAGE2_SOURCES
591 "${CMAKE_SOURCE_DIR}/src/link/MachO/bind.zig"591 "${CMAKE_SOURCE_DIR}/src/link/MachO/bind.zig"
592 "${CMAKE_SOURCE_DIR}/src/link/MachO/dead_strip.zig"592 "${CMAKE_SOURCE_DIR}/src/link/MachO/dead_strip.zig"
593 "${CMAKE_SOURCE_DIR}/src/link/MachO/fat.zig"593 "${CMAKE_SOURCE_DIR}/src/link/MachO/fat.zig"
594 "${CMAKE_SOURCE_DIR}/src/link/MachO/hasher.zig"
594 "${CMAKE_SOURCE_DIR}/src/link/MachO/load_commands.zig"595 "${CMAKE_SOURCE_DIR}/src/link/MachO/load_commands.zig"
595 "${CMAKE_SOURCE_DIR}/src/link/MachO/thunks.zig"596 "${CMAKE_SOURCE_DIR}/src/link/MachO/thunks.zig"
596 "${CMAKE_SOURCE_DIR}/src/link/MachO/uuid.zig"597 "${CMAKE_SOURCE_DIR}/src/link/MachO/uuid.zig"
src/link/MachO.zig+8-8
...@@ -99,10 +99,10 @@ page_size: u16,...@@ -99,10 +99,10 @@ page_size: u16,
99/// fashion (default for LLVM backend).99/// fashion (default for LLVM backend).
100mode: enum { incremental, one_shot },100mode: enum { incremental, one_shot },
101101
102uuid: macho.uuid_command = .{102uuid: struct {
103 .cmdsize = @sizeOf(macho.uuid_command),103 buf: [16]u8 = undefined,
104 .uuid = undefined,104 final: bool = false,
105},105} = .{},
106106
107dylibs: std.ArrayListUnmanaged(Dylib) = .{},107dylibs: std.ArrayListUnmanaged(Dylib) = .{},
108dylibs_map: std.StringHashMapUnmanaged(u16) = .{},108dylibs_map: std.StringHashMapUnmanaged(u16) = .{},
...@@ -588,11 +588,11 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No...@@ -588,11 +588,11 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
588588
589 try load_commands.writeBuildVersionLC(&self.base.options, &ncmds, lc_writer);589 try load_commands.writeBuildVersionLC(&self.base.options, &ncmds, lc_writer);
590590
591 {591 if (!self.uuid.final) {
592 std.crypto.random.bytes(&self.uuid.uuid);592 std.crypto.random.bytes(&self.uuid.buf);
593 try lc_writer.writeStruct(self.uuid);593 self.uuid.final = true;
594 ncmds += 1;
595 }594 }
595 try load_commands.writeUuidLC(&self.uuid.buf, &ncmds, lc_writer);
596596
597 try load_commands.writeLoadDylibLCs(self.dylibs.items, self.referenced_dylibs.keys(), &ncmds, lc_writer);597 try load_commands.writeLoadDylibLCs(self.dylibs.items, self.referenced_dylibs.keys(), &ncmds, lc_writer);
598598
src/link/MachO/CodeSignature.zig+11-45
...@@ -1,6 +1,4 @@...@@ -1,6 +1,4 @@
1const CodeSignature = @This();1const CodeSignature = @This();
2const Compilation = @import("../../Compilation.zig");
3const WaitGroup = @import("../../WaitGroup.zig");
42
5const std = @import("std");3const std = @import("std");
6const assert = std.debug.assert;4const assert = std.debug.assert;
...@@ -9,10 +7,13 @@ const log = std.log.scoped(.link);...@@ -9,10 +7,13 @@ const log = std.log.scoped(.link);
9const macho = std.macho;7const macho = std.macho;
10const mem = std.mem;8const mem = std.mem;
11const testing = std.testing;9const testing = std.testing;
10
12const Allocator = mem.Allocator;11const Allocator = mem.Allocator;
12const Compilation = @import("../../Compilation.zig");
13const Hasher = @import("hasher.zig").ParallelHasher;
13const Sha256 = std.crypto.hash.sha2.Sha256;14const Sha256 = std.crypto.hash.sha2.Sha256;
1415
15const hash_size: u8 = 32;16const hash_size = Sha256.digest_length;
1617
17const Blob = union(enum) {18const Blob = union(enum) {
18 code_directory: *CodeDirectory,19 code_directory: *CodeDirectory,
...@@ -109,7 +110,7 @@ const CodeDirectory = struct {...@@ -109,7 +110,7 @@ const CodeDirectory = struct {
109 fn size(self: CodeDirectory) u32 {110 fn size(self: CodeDirectory) u32 {
110 const code_slots = self.inner.nCodeSlots * hash_size;111 const code_slots = self.inner.nCodeSlots * hash_size;
111 const special_slots = self.inner.nSpecialSlots * hash_size;112 const special_slots = self.inner.nSpecialSlots * hash_size;
112 return @sizeOf(macho.CodeDirectory) + @intCast(u32, self.ident.len + 1) + special_slots + code_slots;113 return @sizeOf(macho.CodeDirectory) + @intCast(u32, self.ident.len + 1 + special_slots + code_slots);
113 }114 }
114115
115 fn write(self: CodeDirectory, writer: anytype) !void {116 fn write(self: CodeDirectory, writer: anytype) !void {
...@@ -287,33 +288,11 @@ pub fn writeAdhocSignature(...@@ -287,33 +288,11 @@ pub fn writeAdhocSignature(
287 self.code_directory.inner.nCodeSlots = total_pages;288 self.code_directory.inner.nCodeSlots = total_pages;
288289
289 // Calculate hash for each page (in file) and write it to the buffer290 // Calculate hash for each page (in file) and write it to the buffer
290 var wg: WaitGroup = .{};291 var hasher = Hasher(Sha256){};
291 {292 try hasher.hash(gpa, comp.thread_pool, opts.file, self.code_directory.code_slots.items, .{
292 const buffer = try gpa.alloc(u8, self.page_size * total_pages);293 .chunk_size = self.page_size,
293 defer gpa.free(buffer);294 .max_file_size = opts.file_size,
294295 });
295 const results = try gpa.alloc(fs.File.PReadError!usize, total_pages);
296 defer gpa.free(results);
297 {
298 wg.reset();
299 defer wg.wait();
300
301 var i: usize = 0;
302 while (i < total_pages) : (i += 1) {
303 const fstart = i * self.page_size;
304 const fsize = if (fstart + self.page_size > opts.file_size)
305 opts.file_size - fstart
306 else
307 self.page_size;
308 const out_hash = &self.code_directory.code_slots.items[i];
309 wg.start();
310 try comp.thread_pool.spawn(workerSha256Hash, .{
311 opts.file, fstart, buffer[fstart..][0..fsize], out_hash, &results[i], &wg,
312 });
313 }
314 }
315 for (results) |result| _ = try result;
316 }
317296
318 try blobs.append(.{ .code_directory = &self.code_directory });297 try blobs.append(.{ .code_directory = &self.code_directory });
319 header.length += @sizeOf(macho.BlobIndex);298 header.length += @sizeOf(macho.BlobIndex);
...@@ -352,7 +331,7 @@ pub fn writeAdhocSignature(...@@ -352,7 +331,7 @@ pub fn writeAdhocSignature(
352 }331 }
353332
354 self.code_directory.inner.hashOffset =333 self.code_directory.inner.hashOffset =
355 @sizeOf(macho.CodeDirectory) + @intCast(u32, self.code_directory.ident.len + 1) + self.code_directory.inner.nSpecialSlots * hash_size;334 @sizeOf(macho.CodeDirectory) + @intCast(u32, self.code_directory.ident.len + 1 + self.code_directory.inner.nSpecialSlots * hash_size);
356 self.code_directory.inner.length = self.code_directory.size();335 self.code_directory.inner.length = self.code_directory.size();
357 header.length += self.code_directory.size();336 header.length += self.code_directory.size();
358337
...@@ -372,19 +351,6 @@ pub fn writeAdhocSignature(...@@ -372,19 +351,6 @@ pub fn writeAdhocSignature(
372 }351 }
373}352}
374353
375fn workerSha256Hash(
376 file: fs.File,
377 fstart: usize,
378 buffer: []u8,
379 hash: *[hash_size]u8,
380 err: *fs.File.PReadError!usize,
381 wg: *WaitGroup,
382) void {
383 defer wg.finish();
384 err.* = file.preadAll(buffer, fstart);
385 Sha256.hash(buffer, hash, .{});
386}
387
388pub fn size(self: CodeSignature) u32 {354pub fn size(self: CodeSignature) u32 {
389 var ssize: u32 = @sizeOf(macho.SuperBlob) + @sizeOf(macho.BlobIndex) + self.code_directory.size();355 var ssize: u32 = @sizeOf(macho.SuperBlob) + @sizeOf(macho.BlobIndex) + self.code_directory.size();
390 if (self.requirements) |req| {356 if (self.requirements) |req| {
src/link/MachO/DebugSymbols.zig+2-4
...@@ -5,6 +5,7 @@ const build_options = @import("build_options");...@@ -5,6 +5,7 @@ const build_options = @import("build_options");
5const assert = std.debug.assert;5const assert = std.debug.assert;
6const fs = std.fs;6const fs = std.fs;
7const link = @import("../../link.zig");7const link = @import("../../link.zig");
8const load_commands = @import("load_commands.zig");
8const log = std.log.scoped(.dsym);9const log = std.log.scoped(.dsym);
9const macho = std.macho;10const macho = std.macho;
10const makeStaticString = MachO.makeStaticString;11const makeStaticString = MachO.makeStaticString;
...@@ -303,10 +304,7 @@ pub fn flushModule(self: *DebugSymbols, macho_file: *MachO) !void {...@@ -303,10 +304,7 @@ pub fn flushModule(self: *DebugSymbols, macho_file: *MachO) !void {
303 self.finalizeDwarfSegment(macho_file);304 self.finalizeDwarfSegment(macho_file);
304 try self.writeLinkeditSegmentData(macho_file, &ncmds, lc_writer);305 try self.writeLinkeditSegmentData(macho_file, &ncmds, lc_writer);
305306
306 {307 try load_commands.writeUuidLC(&macho_file.uuid.buf, &ncmds, lc_writer);
307 try lc_writer.writeStruct(macho_file.uuid);
308 ncmds += 1;
309 }
310308
311 var headers_buf = std.ArrayList(u8).init(self.allocator);309 var headers_buf = std.ArrayList(u8).init(self.allocator);
312 defer headers_buf.deinit();310 defer headers_buf.deinit();
src/link/MachO/hasher.zig created+60
...@@ -0,0 +1,60 @@
1const std = @import("std");
2const assert = std.debug.assert;
3const fs = std.fs;
4const mem = std.mem;
5
6const Allocator = mem.Allocator;
7const ThreadPool = @import("../../ThreadPool.zig");
8const WaitGroup = @import("../../WaitGroup.zig");
9
10pub fn ParallelHasher(comptime Hasher: type) type {
11 const hash_size = Hasher.digest_length;
12
13 return struct {
14 pub fn hash(self: @This(), gpa: Allocator, pool: *ThreadPool, file: fs.File, out: [][hash_size]u8, opts: struct {
15 chunk_size: u16 = 0x4000,
16 max_file_size: ?u64 = null,
17 }) !void {
18 _ = self;
19
20 var wg: WaitGroup = .{};
21
22 const file_size = opts.max_file_size orelse try file.getEndPos();
23 const total_num_chunks = mem.alignForward(file_size, opts.chunk_size) / opts.chunk_size;
24 assert(out.len >= total_num_chunks);
25
26 const buffer = try gpa.alloc(u8, opts.chunk_size * total_num_chunks);
27 defer gpa.free(buffer);
28
29 const results = try gpa.alloc(fs.File.PReadError!usize, total_num_chunks);
30 defer gpa.free(results);
31
32 {
33 wg.reset();
34 defer wg.wait();
35
36 var i: usize = 0;
37 while (i < total_num_chunks) : (i += 1) {
38 const fstart = i * opts.chunk_size;
39 const fsize = if (fstart + opts.chunk_size > file_size) file_size - fstart else opts.chunk_size;
40 wg.start();
41 try pool.spawn(worker, .{ file, fstart, buffer[fstart..][0..fsize], &out[i], &results[i], &wg });
42 }
43 }
44 for (results) |result| _ = try result;
45 }
46
47 fn worker(
48 file: fs.File,
49 fstart: usize,
50 buffer: []u8,
51 out: *[hash_size]u8,
52 err: *fs.File.PReadError!usize,
53 wg: *WaitGroup,
54 ) void {
55 defer wg.finish();
56 err.* = file.preadAll(buffer, fstart);
57 Hasher.hash(buffer, out, .{});
58 }
59 };
60}
src/link/MachO/uuid.zig created+69
...@@ -0,0 +1,69 @@
1const std = @import("std");
2const fs = std.fs;
3const mem = std.mem;
4
5const Allocator = mem.Allocator;
6const Compilation = @import("../../Compilation.zig");
7const Md5 = std.crypto.hash.Md5;
8const Hasher = @import("hasher.zig").ParallelHasher;
9
10/// Somewhat random chunk size for MD5 hash calculation.
11pub const chunk_size = 0x4000;
12
13/// Calculates Md5 hash of the file contents.
14/// Hash is calculated in a streaming manner which may be slow.
15pub fn calcUuidStreaming(file: fs.File, file_size: u64, out: *[Md5.digest_length]u8) !void {
16 const total_num_chunks = mem.alignForward(file_size, chunk_size) / chunk_size;
17
18 var hasher = Md5.init(.{});
19 var buffer: [chunk_size]u8 = undefined;
20
21 var i: usize = 0;
22 while (i < total_num_chunks) : (i += 1) {
23 const start = i * chunk_size;
24 const size = if (start + chunk_size > file_size)
25 file_size - start
26 else
27 chunk_size;
28 const amt = try file.preadAll(&buffer, start);
29 if (amt != size) return error.InputOutput;
30
31 hasher.update(buffer[0..size]);
32 }
33
34 hasher.final(out);
35 conform(out);
36}
37
38/// Calculates Md5 hash of each chunk in parallel and then hashes all Md5 hashes to produce
39/// the final digest.
40/// While this is NOT a correct MD5 hash of the contents, this methodology is used by LLVM/LLD
41/// and we will use it too as it seems accepted by Apple OSes.
42pub fn calcUuidParallel(comp: *const Compilation, file: fs.File, file_size: u64, out: *[Md5.digest_length]u8) !void {
43 const total_hashes = mem.alignForward(file_size, chunk_size) / chunk_size;
44
45 const hashes = try comp.gpa.alloc([Md5.digest_length]u8, total_hashes);
46 defer comp.gpa.free(hashes);
47
48 var hasher = Hasher(Md5){};
49 try hasher.hash(comp.gpa, comp.thread_pool, file, hashes, .{
50 .chunk_size = chunk_size,
51 .max_file_size = file_size,
52 });
53
54 const final_buffer = try comp.gpa.alloc(u8, total_hashes * Md5.digest_length);
55 defer comp.gpa.free(final_buffer);
56
57 for (hashes) |hash, i| {
58 mem.copy(u8, final_buffer[i * Md5.digest_length ..][0..Md5.digest_length], &hash);
59 }
60
61 Md5.hash(final_buffer, out, .{});
62 conform(out);
63}
64
65inline fn conform(out: *[Md5.digest_length]u8) void {
66 // LC_UUID uuids should conform to RFC 4122 UUID version 4 & UUID version 5 formats
67 out[6] = (out[6] & 0x0F) | (3 << 4);
68 out[8] = (out[8] & 0x3F) | 0x80;
69}
src/link/MachO/zld.zig+9-2
...@@ -4037,8 +4037,15 @@ pub fn linkWithZld(macho_file: *MachO, comp: *Compilation, prog_node: *std.Progr...@@ -4037,8 +4037,15 @@ pub fn linkWithZld(macho_file: *MachO, comp: *Compilation, prog_node: *std.Progr
4037 const uuid_offset_backpatch: ?usize = blk: {4037 const uuid_offset_backpatch: ?usize = blk: {
4038 const index = lc_buffer.items.len;4038 const index = lc_buffer.items.len;
4039 var uuid_buf: [16]u8 = [_]u8{0} ** 16;4039 var uuid_buf: [16]u8 = [_]u8{0} ** 16;
4040
4041 if (zld.options.optimize_mode == .Debug) {
4042 // In Debug we don't really care about reproducibility, so put in a random value
4043 // and be done with it.
4044 std.crypto.random.bytes(&uuid_buf);
4045 }
4046
4040 try load_commands.writeUuidLC(&uuid_buf, &ncmds, lc_writer);4047 try load_commands.writeUuidLC(&uuid_buf, &ncmds, lc_writer);
4041 break :blk index;4048 break :blk if (zld.options.optimize_mode == .Debug) null else index;
4042 };4049 };
40434050
4044 try load_commands.writeLoadDylibLCs(zld.dylibs.items, zld.referenced_dylibs.keys(), &ncmds, lc_writer);4051 try load_commands.writeLoadDylibLCs(zld.dylibs.items, zld.referenced_dylibs.keys(), &ncmds, lc_writer);
...@@ -4076,7 +4083,7 @@ pub fn linkWithZld(macho_file: *MachO, comp: *Compilation, prog_node: *std.Progr...@@ -4076,7 +4083,7 @@ pub fn linkWithZld(macho_file: *MachO, comp: *Compilation, prog_node: *std.Progr
4076 const seg = zld.getLinkeditSegmentPtr();4083 const seg = zld.getLinkeditSegmentPtr();
4077 const file_size = seg.fileoff + seg.filesize;4084 const file_size = seg.fileoff + seg.filesize;
4078 var uuid_buf: [16]u8 = undefined;4085 var uuid_buf: [16]u8 = undefined;
4079 try uuid.calcMd5Hash(zld.gpa, zld.file, file_size, &uuid_buf);4086 try uuid.calcUuidParallel(comp, zld.file, file_size, &uuid_buf);
4080 const offset = @sizeOf(macho.mach_header_64) + headers_buf.items.len + backpatch + @sizeOf(macho.load_command);4087 const offset = @sizeOf(macho.mach_header_64) + headers_buf.items.len + backpatch + @sizeOf(macho.load_command);
4081 try zld.file.pwriteAll(&uuid_buf, offset);4088 try zld.file.pwriteAll(&uuid_buf, offset);
4082 }4089 }