// Mach-O container parsing: magic numbers, fat (universal) binaries, the
// `mach_header` of a thin image and the generic `load_command` framing.
//
// Parsing happens in two stages. `parse_slice` validates the header and walks
// the load-command table, checking every offset and size; `commands.mbt` then
// interprets individual commands.
///|
/// The four byte orders and word sizes a Mach-O image can be wrapped in.
///
/// The magic number is read as a 32-bit little-endian word first; a value that
/// matches none of these four is not a Mach-O file at all.
pub(all) enum MachKind {
/// 64-bit little-endian image (`MH_MAGIC_64`).
Mach64
/// 32-bit little-endian image (`MH_MAGIC`).
Mach32
/// 64-bit big-endian image (`MH_CIGAM_64`), legacy PowerPC64.
Mach64Be
/// 32-bit big-endian image (`MH_CIGAM`), legacy PowerPC.
Mach32Be
}
///|
/// `MH_MAGIC_64`, read little-endian: bytes `cf fa ed fe`.
const MAGIC_64 : UInt32 = 0xFEED_FACF
///|
/// `MH_MAGIC`, read little-endian: bytes `ce fa ed fe`.
const MAGIC_32 : UInt32 = 0xFEED_FACE
///|
/// `FAT_MAGIC`, read big-endian: bytes `ca fe ba be`.
const FAT_MAGIC : UInt32 = 0xCAFE_BABE
///|
/// `FAT_MAGIC_64`, read big-endian: bytes `ca fe ba bf`.
const FAT_MAGIC_64 : UInt32 = 0xCAFE_BABF
///|
/// Renders the kind as its header identifier, which is the name `file(1)`
/// prints for the same image.
impl Show for MachKind with fn to_string(self) {
match self {
Mach64 => "MH_MAGIC_64"
Mach32 => "MH_MAGIC"
Mach64Be => "MH_CIGAM_64"
Mach32Be => "MH_CIGAM"
}
}
///|
/// Word size of a kind, in bytes.
pub fn MachKind::pointer_size(self : MachKind) -> Int32 {
match self {
Mach64 | Mach64Be => 8
Mach32 | Mach32Be => 4
}
}
///|
/// Byte order of a kind.
pub fn MachKind::endian(self : MachKind) -> Endian {
match self {
Mach64 | Mach32 => Little
Mach64Be | Mach32Be => Big
}
}
///|
/// Size of `mach_header` / `mach_header_64` in bytes.
pub fn MachKind::header_size(self : MachKind) -> Int64 {
match self {
Mach64 | Mach64Be => 32L
Mach32 | Mach32Be => 28L
}
}
///|
/// Resolves a magic word to a kind, or `None` if it is not a thin Mach-O image.
fn kind_of_magic(magic : UInt32) -> MachKind? {
match magic {
MAGIC_64 => Some(Mach64)
MAGIC_32 => Some(Mach32)
// Reversed on disk: a big-endian image read as little-endian.
0xCF_FA_ED_FE => Some(Mach64Be)
0xCE_FA_ED_FE => Some(Mach32Be)
_ => None
}
}
///|
/// The `mach_header` fields that are meaningful independent of word size.
///
/// Field names follow `` so that a reader can compare them
/// against the C definition without a mapping table.
pub(all) struct MachHeader {
/// Word size and byte order of the image.
kind : MachKind
/// `cputype`, signed because the loader keeps it signed.
cpu_type : Int64
/// `cpusubtype`, with the capability bits still attached.
cpu_subtype : Int64
/// `filetype`: 2 executable, 6 dylib, 8 bundle, 1 object, ...
file_type : UInt32
/// Number of load commands that follow the header.
ncmds : UInt32
/// Total byte size of the load-command table.
sizeofcmds : UInt32
/// `MH_*` flags bitfield.
flags : UInt32
/// Reserved word present only in the 64-bit header.
reserved : UInt32
}
///|
/// Mask selecting the capability bits of a `cpusubtype` field.
///
/// `` defines `CPU_SUBTYPE_MASK` as `0xff000000`: the low 24
/// bits identify the subtype, the high 8 bits carry capabilities such as
/// pointer authentication for arm64e. Both the thin header and the fat
/// architecture table store the combined value, so callers that want to compare
/// against constants like `CPU_SUBTYPE_ARM64E` must mask first.
pub const CPU_SUBTYPE_MASK : Int64 = 0xFF00_0000L
///|
/// Formats an integer as lowercase hexadecimal.
///
/// Lives in the library rather than the CLI because every caller that prints an
/// offset or a flags word needs it, and because it is the one conversion whose
/// output the tests can assert against `otool`.
pub fn hex(v : Int64) -> String {
if v == 0L {
return "0"
}
let digits = "0123456789abcdef"
let raw : Array[Char] = Array::make(16, '0')
let mut n = v
let mut i = 15
while i >= 0 {
let d = (n & 0xFL).to_int()
raw[i] = digits.code_unit_at(d).unsafe_to_char()
n = n >> 4
i = i - 1
}
let buf = StringBuilder()
let mut wrote = false
for j in 0..<16 {
let c = raw[j]
if !wrote && c == '0' {
continue
}
wrote = true
buf.write_char(c)
}
buf.to_string()
}
///|
/// The low 24 bits of a `cpusubtype`: the subtype without capability bits.
pub fn subtype_base(raw : Int64) -> Int64 {
raw & 0xFF_FFFFL
}
///|
/// The high 8 bits of a `cpusubtype`: pointer authentication and similar
/// capability flags.
pub fn subtype_capabilities(raw : Int64) -> Int64 {
(raw & CPU_SUBTYPE_MASK) >> 24
}
///|
/// The header's `cpusubtype` with capability bits removed.
pub fn MachHeader::cpu_subtype_base(self : MachHeader) -> Int64 {
subtype_base(self.cpu_subtype)
}
///|
/// The header's capability flags, from the high byte of `cpusubtype`.
pub fn MachHeader::cpu_capabilities(self : MachHeader) -> Int64 {
subtype_capabilities(self.cpu_subtype)
}
///|
/// A load command as framed by the parser, before its payload is interpreted.
pub(all) struct RawCommand {
/// `cmd` field.
cmd : UInt32
/// `cmdsize` field, as recorded in the file.
size : UInt32
/// Absolute file offset of the command's first byte.
offset : Int64
}
///|
/// A validated thin Mach-O image: the header, its load-command framing and the
/// byte range the slice occupies inside the file it came from.
pub(all) struct MachSlice {
/// Parsed header.
header : MachHeader
/// Load commands in file order, each already bounds-checked as a whole.
commands : Array[RawCommand]
/// Readable range for this slice; absolute offsets into the original file.
bounds : Bounds
}
///|
/// Result of opening a file: either one thin image or the slices of a fat one.
pub(all) enum MachFile {
/// A thin Mach-O image occupying the whole file.
Thin(MachSlice)
/// A universal binary and the architecture of each of its slices.
Fat(Array[FatSlice])
}
///|
/// One entry of a fat binary's architecture table, with its slice parsed.
///
/// `FatArch` fields keep the names used in ``.
pub(all) struct FatSlice {
/// `cputype` from the architecture table.
cpu_type : Int64
/// `cpusubtype` from the architecture table.
cpu_subtype : Int64
/// Absolute offset of the slice inside the file.
offset : Int64
/// Byte size of the slice.
size : Int64
/// `align` field: the slice is 2^align aligned.
align : UInt32
/// The parsed slice, with its bounds restricted to `[offset, offset+size)`.
image : MachSlice
}
///|
/// A one-line summary of the header, in the shape `file(1)` prints.
///
/// Used by the CLI's default output and by error reports, so that a user
/// looking at a failure knows which image the failure came from.
pub fn MachHeader::describe(self : MachHeader) -> String {
let arch = match self.cpu_type {
7L => "i386"
0x0100_0007L => "x86_64"
12L => "arm"
0x0100_000CL => "arm64"
18L => "ppc"
0x0100_0018L => "ppc64"
_ => "cpu=\{self.cpu_type}"
}
let kind = Show::to_string(self.kind)
"\{arch} \{kind} (\{Show::to_string(self.kind.endian())}), subtype \{self.cpu_subtype_base()}, filetype \{self.file_type}, \{self.ncmds} commands, \{self.sizeofcmds} bytes"
}
///|
/// The architecture table entry's `cpusubtype` with capability bits removed.
pub fn FatSlice::cpu_subtype_base(self : FatSlice) -> Int64 {
subtype_base(self.cpu_subtype)
}
///|
/// The architecture table entry's capability flags.
pub fn FatSlice::cpu_capabilities(self : FatSlice) -> Int64 {
subtype_capabilities(self.cpu_subtype)
}
///|
/// Parses a header and its load-command framing at `offset` inside `bounds`.
///
/// Every command is checked to start and end inside the slice and inside the
/// declared `sizeofcmds` region; a command whose `cmdsize` is smaller than the
/// 8-byte generic header, or not a multiple of 4, makes the whole image
/// unreadable because the table cannot be walked any further.
pub fn parse_slice(
bounds : Bounds,
offset : Int64,
) -> MachSlice raise MachscopeError {
let (magic, _) = bounds.read_u32(Cursor::{ pos: offset, endian: Little, })
let kind = match kind_of_magic(magic) {
Some(k) => k
None => raise BadMagic(magic~)
}
let endian = kind.endian()
let cur = Cursor::{ pos: offset, endian, }
let (_, cur) = bounds.read_u32(cur)
let (cpu_type, cur) = bounds.read_i32(cur)
let (cpu_subtype, cur) = bounds.read_i32(cur)
let (file_type, cur) = bounds.read_u32(cur)
let (ncmds, cur) = bounds.read_u32(cur)
let (sizeofcmds, cur) = bounds.read_u32(cur)
let (flags, cur) = bounds.read_u32(cur)
let (reserved, cur) = if kind.pointer_size() == 8 {
bounds.read_u32(cur)
} else {
(0U, cur)
}
let header = MachHeader::{
kind,
cpu_type,
cpu_subtype,
file_type,
ncmds,
sizeofcmds,
flags,
reserved,
}
// The table must fit, and a command table cannot hold more entries than
// there are 8-byte generic headers in it.
bounds.require(cur.pos, sizeofcmds.to_int64(), "load commands")
if ncmds.to_int64() * 8L > sizeofcmds.to_int64() {
raise Inconsistent(
what="load command table",
detail="ncmds cannot fit in sizeofcmds",
)
}
let table_end = cur.pos + sizeofcmds.to_int64()
let commands : Array[RawCommand] = []
let mut at = cur.pos
for _ in 0.. table_end {
raise Inconsistent(
what="load command table",
detail="command starts past sizeofcmds",
)
}
let (cmd, c1) = bounds.read_u32(Cursor::{ pos: at, endian, })
let (size, _) = bounds.read_u32(c1)
let size64 = size.to_int64()
if size64 < 8L {
raise BadValue(field="cmdsize", value=size64)
}
if size64 % 4L != 0L {
raise BadValue(field="cmdsize alignment", value=size64)
}
if at + size64 > table_end {
raise Inconsistent(
what="load command table",
detail="command extends past sizeofcmds",
)
}
commands.push(RawCommand::{ cmd, size, offset: at, })
at = at + size64
}
MachSlice::{ header, commands, bounds, }
}
///|
/// Opens a buffer as a Mach-O file, accepting both thin and fat images.
///
/// A fat image is parsed slice by slice; a slice that fails to parse makes the
/// whole call fail, because a universal binary with one unreadable
/// architecture cannot be reported as a usable universal binary.
pub fn parse(bytes : Bytes) -> MachFile raise MachscopeError {
let bounds = Bounds::of_bytes(bytes)
let (magic, _) = bounds.read_u32(Cursor::{ pos: 0L, endian: Big, })
match magic {
FAT_MAGIC => parse_fat(bounds, false)
FAT_MAGIC_64 => parse_fat(bounds, true)
_ => Thin(parse_slice(bounds, 0L))
}
}
///|
/// Parses a fat architecture table and every slice it names.
///
/// `wide` selects the `fat_arch_64` layout, whose offset and size fields are
/// 64-bit; a 32-bit table is otherwise identical.
fn parse_fat(bounds : Bounds, wide : Bool) -> MachFile raise MachscopeError {
let cur = Cursor::{ pos: 0L, endian: Big, }
let (_, cur) = bounds.read_u32(cur) // magic, already matched
let (nfat_arch, cur) = bounds.read_u32(cur)
let entry_size = if wide { 32L } else { 20L }
bounds.require(cur.pos, nfat_arch.to_int64() * entry_size, "fat arch table")
let slices : Array[FatSlice] = []
let mut at = cur.pos
for _ in 0..