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
591591 "${CMAKE_SOURCE_DIR}/src/link/MachO/bind.zig"
592592 "${CMAKE_SOURCE_DIR}/src/link/MachO/dead_strip.zig"
593593 "${CMAKE_SOURCE_DIR}/src/link/MachO/fat.zig"
594 "${CMAKE_SOURCE_DIR}/src/link/MachO/hasher.zig"
594595 "${CMAKE_SOURCE_DIR}/src/link/MachO/load_commands.zig"
595596 "${CMAKE_SOURCE_DIR}/src/link/MachO/thunks.zig"
596597 "${CMAKE_SOURCE_DIR}/src/link/MachO/uuid.zig"
src/link/MachO.zig+8-8
......@@ -99,10 +99,10 @@ page_size: u16,
9999/// fashion (default for LLVM backend).
100100mode: enum { incremental, one_shot },
101101
102uuid: macho.uuid_command = .{
103 .cmdsize = @sizeOf(macho.uuid_command),
104 .uuid = undefined,
105},
102uuid: struct {
103 buf: [16]u8 = undefined,
104 final: bool = false,
105} = .{},
106106
107107dylibs: std.ArrayListUnmanaged(Dylib) = .{},
108108dylibs_map: std.StringHashMapUnmanaged(u16) = .{},
......@@ -588,11 +588,11 @@ pub fn flushModule(self: *MachO, comp: *Compilation, prog_node: *std.Progress.No
588588
589589 try load_commands.writeBuildVersionLC(&self.base.options, &ncmds, lc_writer);
590590
591 {
592 std.crypto.random.bytes(&self.uuid.uuid);
593 try lc_writer.writeStruct(self.uuid);
594 ncmds += 1;
591 if (!self.uuid.final) {
592 std.crypto.random.bytes(&self.uuid.buf);
593 self.uuid.final = true;
595594 }
595 try load_commands.writeUuidLC(&self.uuid.buf, &ncmds, lc_writer);
596596
597597 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 @@
11const CodeSignature = @This();
2const Compilation = @import("../../Compilation.zig");
3const WaitGroup = @import("../../WaitGroup.zig");
42
53const std = @import("std");
64const assert = std.debug.assert;
......@@ -9,10 +7,13 @@ const log = std.log.scoped(.link);
97const macho = std.macho;
108const mem = std.mem;
119const testing = std.testing;
10
1211const Allocator = mem.Allocator;
12const Compilation = @import("../../Compilation.zig");
13const Hasher = @import("hasher.zig").ParallelHasher;
1314const Sha256 = std.crypto.hash.sha2.Sha256;
1415
15const hash_size: u8 = 32;
16const hash_size = Sha256.digest_length;
1617
1718const Blob = union(enum) {
1819 code_directory: *CodeDirectory,
......@@ -109,7 +110,7 @@ const CodeDirectory = struct {
109110 fn size(self: CodeDirectory) u32 {
110111 const code_slots = self.inner.nCodeSlots * hash_size;
111112 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);
113114 }
114115
115116 fn write(self: CodeDirectory, writer: anytype) !void {
......@@ -287,33 +288,11 @@ pub fn writeAdhocSignature(
287288 self.code_directory.inner.nCodeSlots = total_pages;
288289
289290 // Calculate hash for each page (in file) and write it to the buffer
290 var wg: WaitGroup = .{};
291 {
292 const buffer = try gpa.alloc(u8, self.page_size * total_pages);
293 defer gpa.free(buffer);
294
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 }
291 var hasher = Hasher(Sha256){};
292 try hasher.hash(gpa, comp.thread_pool, opts.file, self.code_directory.code_slots.items, .{
293 .chunk_size = self.page_size,
294 .max_file_size = opts.file_size,
295 });
317296
318297 try blobs.append(.{ .code_directory = &self.code_directory });
319298 header.length += @sizeOf(macho.BlobIndex);
......@@ -352,7 +331,7 @@ pub fn writeAdhocSignature(
352331 }
353332
354333 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);
356335 self.code_directory.inner.length = self.code_directory.size();
357336 header.length += self.code_directory.size();
358337
......@@ -372,19 +351,6 @@ pub fn writeAdhocSignature(
372351 }
373352}
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
388354pub fn size(self: CodeSignature) u32 {
389355 var ssize: u32 = @sizeOf(macho.SuperBlob) + @sizeOf(macho.BlobIndex) + self.code_directory.size();
390356 if (self.requirements) |req| {
src/link/MachO/DebugSymbols.zig+2-4
......@@ -5,6 +5,7 @@ const build_options = @import("build_options");
55const assert = std.debug.assert;
66const fs = std.fs;
77const link = @import("../../link.zig");
8const load_commands = @import("load_commands.zig");
89const log = std.log.scoped(.dsym);
910const macho = std.macho;
1011const makeStaticString = MachO.makeStaticString;
......@@ -303,10 +304,7 @@ pub fn flushModule(self: *DebugSymbols, macho_file: *MachO) !void {
303304 self.finalizeDwarfSegment(macho_file);
304305 try self.writeLinkeditSegmentData(macho_file, &ncmds, lc_writer);
305306
306 {
307 try lc_writer.writeStruct(macho_file.uuid);
308 ncmds += 1;
309 }
307 try load_commands.writeUuidLC(&macho_file.uuid.buf, &ncmds, lc_writer);
310308
311309 var headers_buf = std.ArrayList(u8).init(self.allocator);
312310 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
40374037 const uuid_offset_backpatch: ?usize = blk: {
40384038 const index = lc_buffer.items.len;
40394039 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
40404047 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;
40424049 };
40434050
40444051 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
40764083 const seg = zld.getLinkeditSegmentPtr();
40774084 const file_size = seg.fileoff + seg.filesize;
40784085 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);
40804087 const offset = @sizeOf(macho.mach_header_64) + headers_buf.items.len + backpatch + @sizeOf(macho.load_command);
40814088 try zld.file.pwriteAll(&uuid_buf, offset);
40824089 }