419 lines
16 KiB
Zig
419 lines
16 KiB
Zig
const std = @import("std");
|
|
const fs = std.fs;
|
|
const io = std.io;
|
|
const mem = std.mem;
|
|
const meta = std.meta;
|
|
const macho = std.macho;
|
|
const testing = std.testing;
|
|
const assert = std.debug.assert;
|
|
|
|
const Allocator = std.mem.Allocator;
|
|
const MachO = @import("../MachO.zig");
|
|
const makeStaticString = MachO.makeStaticString;
|
|
const padToIdeal = MachO.padToIdeal;
|
|
|
|
pub const LoadCommand = union(enum) {
|
|
Segment: SegmentCommand,
|
|
DyldInfoOnly: macho.dyld_info_command,
|
|
Symtab: macho.symtab_command,
|
|
Dysymtab: macho.dysymtab_command,
|
|
Dylinker: GenericCommandWithData(macho.dylinker_command),
|
|
Dylib: GenericCommandWithData(macho.dylib_command),
|
|
Main: macho.entry_point_command,
|
|
VersionMin: macho.version_min_command,
|
|
SourceVersion: macho.source_version_command,
|
|
Uuid: macho.uuid_command,
|
|
LinkeditData: macho.linkedit_data_command,
|
|
Unknown: GenericCommandWithData(macho.load_command),
|
|
|
|
pub fn read(allocator: *Allocator, reader: anytype) !LoadCommand {
|
|
const header = try reader.readStruct(macho.load_command);
|
|
var buffer = try allocator.alloc(u8, header.cmdsize);
|
|
defer allocator.free(buffer);
|
|
mem.copy(u8, buffer, mem.asBytes(&header));
|
|
try reader.readNoEof(buffer[@sizeOf(macho.load_command)..]);
|
|
var stream = io.fixedBufferStream(buffer);
|
|
|
|
return switch (header.cmd) {
|
|
macho.LC_SEGMENT_64 => LoadCommand{
|
|
.Segment = try SegmentCommand.read(allocator, stream.reader()),
|
|
},
|
|
macho.LC_DYLD_INFO, macho.LC_DYLD_INFO_ONLY => LoadCommand{
|
|
.DyldInfoOnly = try stream.reader().readStruct(macho.dyld_info_command),
|
|
},
|
|
macho.LC_SYMTAB => LoadCommand{
|
|
.Symtab = try stream.reader().readStruct(macho.symtab_command),
|
|
},
|
|
macho.LC_DYSYMTAB => LoadCommand{
|
|
.Dysymtab = try stream.reader().readStruct(macho.dysymtab_command),
|
|
},
|
|
macho.LC_ID_DYLINKER, macho.LC_LOAD_DYLINKER, macho.LC_DYLD_ENVIRONMENT => LoadCommand{
|
|
.Dylinker = try GenericCommandWithData(macho.dylinker_command).read(allocator, stream.reader()),
|
|
},
|
|
macho.LC_ID_DYLIB, macho.LC_LOAD_WEAK_DYLIB, macho.LC_LOAD_DYLIB, macho.LC_REEXPORT_DYLIB => LoadCommand{
|
|
.Dylib = try GenericCommandWithData(macho.dylib_command).read(allocator, stream.reader()),
|
|
},
|
|
macho.LC_MAIN => LoadCommand{
|
|
.Main = try stream.reader().readStruct(macho.entry_point_command),
|
|
},
|
|
macho.LC_VERSION_MIN_MACOSX, macho.LC_VERSION_MIN_IPHONEOS, macho.LC_VERSION_MIN_WATCHOS, macho.LC_VERSION_MIN_TVOS => LoadCommand{
|
|
.VersionMin = try stream.reader().readStruct(macho.version_min_command),
|
|
},
|
|
macho.LC_SOURCE_VERSION => LoadCommand{
|
|
.SourceVersion = try stream.reader().readStruct(macho.source_version_command),
|
|
},
|
|
macho.LC_UUID => LoadCommand{
|
|
.Uuid = try stream.reader().readStruct(macho.uuid_command),
|
|
},
|
|
macho.LC_FUNCTION_STARTS, macho.LC_DATA_IN_CODE, macho.LC_CODE_SIGNATURE => LoadCommand{
|
|
.LinkeditData = try stream.reader().readStruct(macho.linkedit_data_command),
|
|
},
|
|
else => LoadCommand{
|
|
.Unknown = try GenericCommandWithData(macho.load_command).read(allocator, stream.reader()),
|
|
},
|
|
};
|
|
}
|
|
|
|
pub fn write(self: LoadCommand, writer: anytype) !void {
|
|
return switch (self) {
|
|
.DyldInfoOnly => |x| writeStruct(x, writer),
|
|
.Symtab => |x| writeStruct(x, writer),
|
|
.Dysymtab => |x| writeStruct(x, writer),
|
|
.Main => |x| writeStruct(x, writer),
|
|
.VersionMin => |x| writeStruct(x, writer),
|
|
.SourceVersion => |x| writeStruct(x, writer),
|
|
.Uuid => |x| writeStruct(x, writer),
|
|
.LinkeditData => |x| writeStruct(x, writer),
|
|
.Segment => |x| x.write(writer),
|
|
.Dylinker => |x| x.write(writer),
|
|
.Dylib => |x| x.write(writer),
|
|
.Unknown => |x| x.write(writer),
|
|
};
|
|
}
|
|
|
|
pub fn cmd(self: LoadCommand) u32 {
|
|
return switch (self) {
|
|
.DyldInfoOnly => |x| x.cmd,
|
|
.Symtab => |x| x.cmd,
|
|
.Dysymtab => |x| x.cmd,
|
|
.Main => |x| x.cmd,
|
|
.VersionMin => |x| x.cmd,
|
|
.SourceVersion => |x| x.cmd,
|
|
.Uuid => |x| x.cmd,
|
|
.LinkeditData => |x| x.cmd,
|
|
.Segment => |x| x.inner.cmd,
|
|
.Dylinker => |x| x.inner.cmd,
|
|
.Dylib => |x| x.inner.cmd,
|
|
.Unknown => |x| x.inner.cmd,
|
|
};
|
|
}
|
|
|
|
pub fn cmdsize(self: LoadCommand) u32 {
|
|
return switch (self) {
|
|
.DyldInfoOnly => |x| x.cmdsize,
|
|
.Symtab => |x| x.cmdsize,
|
|
.Dysymtab => |x| x.cmdsize,
|
|
.Main => |x| x.cmdsize,
|
|
.VersionMin => |x| x.cmdsize,
|
|
.SourceVersion => |x| x.cmdsize,
|
|
.LinkeditData => |x| x.cmdsize,
|
|
.Uuid => |x| x.cmdsize,
|
|
.Segment => |x| x.inner.cmdsize,
|
|
.Dylinker => |x| x.inner.cmdsize,
|
|
.Dylib => |x| x.inner.cmdsize,
|
|
.Unknown => |x| x.inner.cmdsize,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *LoadCommand, allocator: *Allocator) void {
|
|
return switch (self.*) {
|
|
.Segment => |*x| x.deinit(allocator),
|
|
.Dylinker => |*x| x.deinit(allocator),
|
|
.Dylib => |*x| x.deinit(allocator),
|
|
.Unknown => |*x| x.deinit(allocator),
|
|
else => {},
|
|
};
|
|
}
|
|
|
|
fn writeStruct(command: anytype, writer: anytype) !void {
|
|
return writer.writeAll(mem.asBytes(&command));
|
|
}
|
|
|
|
fn eql(self: LoadCommand, other: LoadCommand) bool {
|
|
if (@as(meta.Tag(LoadCommand), self) != @as(meta.Tag(LoadCommand), other)) return false;
|
|
return switch (self) {
|
|
.DyldInfoOnly => |x| meta.eql(x, other.DyldInfoOnly),
|
|
.Symtab => |x| meta.eql(x, other.Symtab),
|
|
.Dysymtab => |x| meta.eql(x, other.Dysymtab),
|
|
.Main => |x| meta.eql(x, other.Main),
|
|
.VersionMin => |x| meta.eql(x, other.VersionMin),
|
|
.SourceVersion => |x| meta.eql(x, other.SourceVersion),
|
|
.Uuid => |x| meta.eql(x, other.Uuid),
|
|
.LinkeditData => |x| meta.eql(x, other.LinkeditData),
|
|
.Segment => |x| x.eql(other.Segment),
|
|
.Dylinker => |x| x.eql(other.Dylinker),
|
|
.Dylib => |x| x.eql(other.Dylib),
|
|
.Unknown => |x| x.eql(other.Unknown),
|
|
};
|
|
}
|
|
};
|
|
|
|
pub const SegmentCommand = struct {
|
|
inner: macho.segment_command_64,
|
|
sections: std.ArrayListUnmanaged(macho.section_64) = .{},
|
|
|
|
pub fn empty(inner: macho.segment_command_64) SegmentCommand {
|
|
return .{ .inner = inner };
|
|
}
|
|
|
|
pub fn addSection(self: *SegmentCommand, alloc: *Allocator, section: macho.section_64) !void {
|
|
try self.sections.append(alloc, section);
|
|
self.inner.cmdsize += @sizeOf(macho.section_64);
|
|
self.inner.nsects += 1;
|
|
}
|
|
|
|
pub fn read(alloc: *Allocator, reader: anytype) !SegmentCommand {
|
|
const inner = try reader.readStruct(macho.segment_command_64);
|
|
var segment = SegmentCommand{
|
|
.inner = inner,
|
|
};
|
|
try segment.sections.ensureCapacity(alloc, inner.nsects);
|
|
|
|
var i: usize = 0;
|
|
while (i < inner.nsects) : (i += 1) {
|
|
const section = try reader.readStruct(macho.section_64);
|
|
segment.sections.appendAssumeCapacity(section);
|
|
}
|
|
|
|
return segment;
|
|
}
|
|
|
|
pub fn write(self: SegmentCommand, writer: anytype) !void {
|
|
try writer.writeAll(mem.asBytes(&self.inner));
|
|
for (self.sections.items) |sect| {
|
|
try writer.writeAll(mem.asBytes(§));
|
|
}
|
|
}
|
|
|
|
pub fn deinit(self: *SegmentCommand, alloc: *Allocator) void {
|
|
self.sections.deinit(alloc);
|
|
}
|
|
|
|
pub fn allocatedSize(self: SegmentCommand, start: u64) u64 {
|
|
assert(start > 0);
|
|
if (start == self.inner.fileoff)
|
|
return 0;
|
|
var min_pos: u64 = std.math.maxInt(u64);
|
|
for (self.sections.items) |section| {
|
|
if (section.offset <= start) continue;
|
|
if (section.offset < min_pos) min_pos = section.offset;
|
|
}
|
|
return min_pos - start;
|
|
}
|
|
|
|
fn detectAllocCollision(self: SegmentCommand, start: u64, size: u64) ?u64 {
|
|
const end = start + padToIdeal(size);
|
|
for (self.sections.items) |section| {
|
|
const increased_size = padToIdeal(section.size);
|
|
const test_end = section.offset + increased_size;
|
|
if (end > section.offset and start < test_end) {
|
|
return test_end;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
pub fn findFreeSpace(self: SegmentCommand, object_size: u64, min_alignment: u16, start: ?u64) u64 {
|
|
var st: u64 = if (start) |v| v else self.inner.fileoff;
|
|
while (self.detectAllocCollision(st, object_size)) |item_end| {
|
|
st = mem.alignForwardGeneric(u64, item_end, min_alignment);
|
|
}
|
|
return st;
|
|
}
|
|
|
|
fn eql(self: SegmentCommand, other: SegmentCommand) bool {
|
|
if (!meta.eql(self.inner, other.inner)) return false;
|
|
const lhs = self.sections.items;
|
|
const rhs = other.sections.items;
|
|
var i: usize = 0;
|
|
while (i < self.inner.nsects) : (i += 1) {
|
|
if (!meta.eql(lhs[i], rhs[i])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
pub fn emptyGenericCommandWithData(cmd: anytype) GenericCommandWithData(@TypeOf(cmd)) {
|
|
return .{ .inner = cmd };
|
|
}
|
|
|
|
pub fn GenericCommandWithData(comptime Cmd: type) type {
|
|
return struct {
|
|
inner: Cmd,
|
|
/// This field remains undefined until `read` is called.
|
|
data: []u8 = undefined,
|
|
|
|
const Self = @This();
|
|
|
|
pub fn read(allocator: *Allocator, reader: anytype) !Self {
|
|
const inner = try reader.readStruct(Cmd);
|
|
var data = try allocator.alloc(u8, inner.cmdsize - @sizeOf(Cmd));
|
|
errdefer allocator.free(data);
|
|
try reader.readNoEof(data);
|
|
return Self{
|
|
.inner = inner,
|
|
.data = data,
|
|
};
|
|
}
|
|
|
|
pub fn write(self: Self, writer: anytype) !void {
|
|
try writer.writeAll(mem.asBytes(&self.inner));
|
|
try writer.writeAll(self.data);
|
|
}
|
|
|
|
pub fn deinit(self: *Self, allocator: *Allocator) void {
|
|
allocator.free(self.data);
|
|
}
|
|
|
|
fn eql(self: Self, other: Self) bool {
|
|
if (!meta.eql(self.inner, other.inner)) return false;
|
|
return mem.eql(u8, self.data, other.data);
|
|
}
|
|
};
|
|
}
|
|
|
|
fn testRead(allocator: *Allocator, buffer: []const u8, expected: anytype) !void {
|
|
var stream = io.fixedBufferStream(buffer);
|
|
var given = try LoadCommand.read(allocator, stream.reader());
|
|
defer given.deinit(allocator);
|
|
testing.expect(expected.eql(given));
|
|
}
|
|
|
|
fn testWrite(buffer: []u8, cmd: LoadCommand, expected: []const u8) !void {
|
|
var stream = io.fixedBufferStream(buffer);
|
|
try cmd.write(stream.writer());
|
|
testing.expect(mem.eql(u8, expected, buffer[0..expected.len]));
|
|
}
|
|
|
|
test "read-write segment command" {
|
|
var gpa = testing.allocator;
|
|
const in_buffer = &[_]u8{
|
|
0x19, 0x00, 0x00, 0x00, // cmd
|
|
0x98, 0x00, 0x00, 0x00, // cmdsize
|
|
0x5f, 0x5f, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // segname
|
|
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // vmaddr
|
|
0x00, 0x80, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // vmsize
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // fileoff
|
|
0x00, 0x80, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, // filesize
|
|
0x07, 0x00, 0x00, 0x00, // maxprot
|
|
0x05, 0x00, 0x00, 0x00, // initprot
|
|
0x01, 0x00, 0x00, 0x00, // nsects
|
|
0x00, 0x00, 0x00, 0x00, // flags
|
|
0x5f, 0x5f, 0x74, 0x65, 0x78, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // sectname
|
|
0x5f, 0x5f, 0x54, 0x45, 0x58, 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // segname
|
|
0x00, 0x40, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, // address
|
|
0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // size
|
|
0x00, 0x40, 0x00, 0x00, // offset
|
|
0x02, 0x00, 0x00, 0x00, // alignment
|
|
0x00, 0x00, 0x00, 0x00, // reloff
|
|
0x00, 0x00, 0x00, 0x00, // nreloc
|
|
0x00, 0x04, 0x00, 0x80, // flags
|
|
0x00, 0x00, 0x00, 0x00, // reserved1
|
|
0x00, 0x00, 0x00, 0x00, // reserved2
|
|
0x00, 0x00, 0x00, 0x00, // reserved3
|
|
};
|
|
var cmd = SegmentCommand{
|
|
.inner = .{
|
|
.cmd = macho.LC_SEGMENT_64,
|
|
.cmdsize = 152,
|
|
.segname = makeStaticString("__TEXT"),
|
|
.vmaddr = 4294967296,
|
|
.vmsize = 294912,
|
|
.fileoff = 0,
|
|
.filesize = 294912,
|
|
.maxprot = macho.VM_PROT_READ | macho.VM_PROT_WRITE | macho.VM_PROT_EXECUTE,
|
|
.initprot = macho.VM_PROT_EXECUTE | macho.VM_PROT_READ,
|
|
.nsects = 1,
|
|
.flags = 0,
|
|
},
|
|
};
|
|
try cmd.sections.append(gpa, .{
|
|
.sectname = makeStaticString("__text"),
|
|
.segname = makeStaticString("__TEXT"),
|
|
.addr = 4294983680,
|
|
.size = 448,
|
|
.offset = 16384,
|
|
.@"align" = 2,
|
|
.reloff = 0,
|
|
.nreloc = 0,
|
|
.flags = macho.S_REGULAR | macho.S_ATTR_PURE_INSTRUCTIONS | macho.S_ATTR_SOME_INSTRUCTIONS,
|
|
.reserved1 = 0,
|
|
.reserved2 = 0,
|
|
.reserved3 = 0,
|
|
});
|
|
defer cmd.deinit(gpa);
|
|
try testRead(gpa, in_buffer, LoadCommand{ .Segment = cmd });
|
|
|
|
var out_buffer: [in_buffer.len]u8 = undefined;
|
|
try testWrite(&out_buffer, LoadCommand{ .Segment = cmd }, in_buffer);
|
|
}
|
|
|
|
test "read-write generic command with data" {
|
|
var gpa = testing.allocator;
|
|
const in_buffer = &[_]u8{
|
|
0x0c, 0x00, 0x00, 0x00, // cmd
|
|
0x20, 0x00, 0x00, 0x00, // cmdsize
|
|
0x18, 0x00, 0x00, 0x00, // name
|
|
0x02, 0x00, 0x00, 0x00, // timestamp
|
|
0x00, 0x00, 0x00, 0x00, // current_version
|
|
0x00, 0x00, 0x00, 0x00, // compatibility_version
|
|
0x2f, 0x75, 0x73, 0x72, 0x00, 0x00, 0x00, 0x00, // data
|
|
};
|
|
var cmd = GenericCommandWithData(macho.dylib_command){
|
|
.inner = .{
|
|
.cmd = macho.LC_LOAD_DYLIB,
|
|
.cmdsize = 32,
|
|
.dylib = .{
|
|
.name = 24,
|
|
.timestamp = 2,
|
|
.current_version = 0,
|
|
.compatibility_version = 0,
|
|
},
|
|
},
|
|
};
|
|
cmd.data = try gpa.alloc(u8, 8);
|
|
defer gpa.free(cmd.data);
|
|
cmd.data[0] = 0x2f;
|
|
cmd.data[1] = 0x75;
|
|
cmd.data[2] = 0x73;
|
|
cmd.data[3] = 0x72;
|
|
cmd.data[4] = 0x0;
|
|
cmd.data[5] = 0x0;
|
|
cmd.data[6] = 0x0;
|
|
cmd.data[7] = 0x0;
|
|
try testRead(gpa, in_buffer, LoadCommand{ .Dylib = cmd });
|
|
|
|
var out_buffer: [in_buffer.len]u8 = undefined;
|
|
try testWrite(&out_buffer, LoadCommand{ .Dylib = cmd }, in_buffer);
|
|
}
|
|
|
|
test "read-write C struct command" {
|
|
var gpa = testing.allocator;
|
|
const in_buffer = &[_]u8{
|
|
0x28, 0x00, 0x00, 0x80, // cmd
|
|
0x18, 0x00, 0x00, 0x00, // cmdsize
|
|
0x04, 0x41, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // entryoff
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // stacksize
|
|
};
|
|
const cmd = .{
|
|
.cmd = macho.LC_MAIN,
|
|
.cmdsize = 24,
|
|
.entryoff = 16644,
|
|
.stacksize = 0,
|
|
};
|
|
try testRead(gpa, in_buffer, LoadCommand{ .Main = cmd });
|
|
|
|
var out_buffer: [in_buffer.len]u8 = undefined;
|
|
try testWrite(&out_buffer, LoadCommand{ .Main = cmd }, in_buffer);
|
|
}
|