MetalLibraryArchive 是对 Apple 的 metallib 文件格式进行逆向工程的产物。
您可以使用 MetalLibraryArchive 从 metallib 文件中获取库类型、目标平台、Metal 函数等信息。
提取的 Metal 函数信息包括:
metallib 配置为包含源代码,则包含函数的源代码。可用地址:https://yuao.github.io/MetalLibraryExplorer
软件包中包含一个名为 "Explorer" 的可执行目标。"Explorer" 是一个 GUI 应用程序,可以打开、解包和反汇编(借助 llvm-dis)metallib 文件。
注意:不包含 llvm-dis,您可以在 https://github.com/llvm/llvm-project/releases 获取该二进制文件的副本。
使用应用程序中的 "Disassembler"(反汇编器)菜单来定位 llvm-dis 可执行文件。
您也可以将 MetalLibraryArchive 用作库。
import MetalLibraryArchive
let archive = try Archive(data: Data(contentsOf: metallibURL))
let libraryType = archive.libraryType
let functions = archive.functions
| Byte Range(字节范围) | Type(类型) | Content(内容) |
|---|---|---|
| 0...3 | FourCharCode | MTLB |
| 4...5 | UInt16 | Target platform(目标平台) |
| 6...9 | (UInt16, UInt16) | Version of the metallib file (major, minor)(metallib 文件的版本(主版本,次版本)) |
| 10 | UInt8 | Type of the metallib file(metallib 文件的类型) |
| 11 | UInt8 | Target OS(目标操作系统) |
| 12...15 | (UInt16, UInt16) | Version of the target OS (major, minor)(目标操作系统的版本(主版本,次版本)) |
| 16...23 | UInt64 | Size of the metallib file(metallib 文件的大小) |
| 24...39 | (UInt64, UInt64) | Offset and size of the function list(函数列表的偏移量和大小) |
| 40...55 | (UInt64, UInt64) | Offset and size of the public metadata section(公共元数据部分的偏移量和大小) |
| 56...71 | (UInt64, UInt64) | Offset and size of the private metadata section(私有元数据部分的偏移量和大小) |
| 72...87 | (UInt64, UInt64) | Offset and size of the bitcode section(Bitcode 部分的偏移量和大小) |
| Target Platform(目标平台) | Value(值) |
|---|---|
| macOS | 0x8001 (0x01,0x80) |
| iOS | 0x0001 (0x01,0x00) |
| metallib Type(metallib 类型) | Value(值) |
|---|---|
| Executable(可执行) | 0x00 |
| Core Image | 0x01 |
| Dynamic(动态) | 0x02 |
| Symbol Companion(符号伴侣) | 0x03 |
| Target OS(目标操作系统) | Value(值) |
|---|---|
| Unknown(未知) | 0x00 |
| macOS | 0x81 |
| iOS | 0x82 |
| tvOS | 0x83 |
| watchOS | 0x84 |
| bridgeOS (Probably)(可能) | 0x85 |
| macCatalyst | 0x86 |
| iOS Simulator(iOS 模拟器) | 0x87 |
| tvOS Simulator(tvOS 模拟器) | 0x88 |
| watchOS Simulator(watchOS 模拟器) | 0x89 |
| Byte Range(字节范围) | Type(类型) | Content(内容) |
|---|---|---|
| 0...3 | UInt32 | Entry count (the number of functions)(条目计数(函数数量)) |
| 4... | Tag Groups(标签组) | Each tag group holds some information about a Metal function(每个标签组保存有关 Metal 函数的一些信息) |
The number of tag groups equals the number of functions.(标签组的数量等于函数的数量。)
| Byte Range(字节范围) | Type(类型) | Content(内容) |
|---|---|---|
| 0...3 | UInt32 | Size of the tag group(标签组的大小) |
| 4... | Tags(标签) |
| Byte Range(字节范围) | Type(类型) | Content(内容) |
|---|---|---|
| 0...3 | FourCharCode | Name of the tag(标签的名称) |
| 4...5 | UInt16 | Size of the tag(标签的大小) |
| 6... | Bytes(字节) | Content of the tag(标签的内容) |
| Name(名称) | Content Data Type(内容数据类型) | Content(内容) |
|---|---|---|
| NAME | NULL-terminated C-style string(以 NULL 结尾的 C 风格字符串) | Name of the function(函数的名称) |
| MDSZ | UInt64 | Size of the bitcode(Bitcode 的大小) |
| TYPE | UInt8 | Type of the function(函数的类型) |
| HASH | SHA256 Digest(SHA256 摘要) | Hash of the bitcode data (SHA256)(Bitcode 数据的哈希值(SHA256)) |
| OFFT | (UInt64, UInt64, UInt64) | Offsets of the information about this function in the public metadata section, private metadata section, and bitcode section(此函数在公共元数据部分、私有元数据部分和 Bitcode 部分中的信息偏移量) |
| SOFF | UInt64 | Offset of the source code archive of the function in the embedded source code section(函数源代码存档在嵌入式源代码部分中的偏移量) |
| VERS | (UInt16, UInt16, UInt16, UInt16) | Bitcode and language versions (air.major, air.minor, language.major, language.minor)(Bitcode 和语言版本(air.major,air.minor,language.major,language.minor)) |
| LAYR | UInt8 | Metal type(Metal 类型) of the render_target_array_index (for layered rendering(分层渲染)) |
| TESS | UInt8 | Patch type and number of control points per-patch (for post-tessellation vertex function)(每个补丁的 Patch 类型和控制点数量(用于镶嵌后顶点函数)) |
| ENDT | End of the tag group(标签组的结束) |
| Function Type(函数类型) | Value(值) | Note(注意) |
|---|---|---|
| Vertex(顶点) | 0x00 | |
| Fragment(片段) | 0x01 | |
| Kernel(内核) | 0x02 | |
| Unqualified(无限定) | 0x03 | Functions in Metal dynamic library(Metal 动态库中的函数) |
| Visible(可见) | 0x04 | Functions with [[visible]] or [[stitchable]] attributes(具有 [[visible]] 或 [[stitchable]] 属性的函数) |
| Extern(外部) | 0x05 | Extern functions complied with -fcikernel option(使用 -fcikernel 选项编译的外部函数) |
| Intersection(相交) | 0x06 |
Content of the TESS tag(TESS 标签的内容)
// Patch types:
// - triangle: 1
// - quad: 2
let content: UInt8 = controlPointCount << 2 | patchType
Contains information about function constants, tessellation patches, return types, etc.(包含有关函数常量、镶嵌补丁、返回类型等的信息。)
Tags: CNST, VATT, VATY, RETR, ARGR, etc.
Contains paths to the shader source (DEBI tag) and .air (DEPF tag) files.(包含着色器源代码(DEBI 标签)和 .air(DEPF 标签)文件的路径。)
Only exists if FunctionListOffset + FunctionListSize + 4 != PublicMetadataOffset(仅当 FunctionListOffset + FunctionListSize + 4 != PublicMetadataOffset 时存在)
| Byte Range(字节范围) | Type(类型) | Content(内容) |
|---|---|---|
FunctionListOffset + FunctionListSize + 4... |
Tags(标签) | Header extension tags(头部扩展标签) |
| Name(名称) | Type(类型) | Content(内容) |
|---|---|---|
| HDYN | (UInt64, UInt64) | Offset and size of the dynamic header section(动态头部部分的偏移量和大小) |
| VLST | (UInt64, UInt64) | Offset and size of the exported variable list(导出的变量列表的偏移量和大小) |
| ILST | (UInt64, UInt64) | Offset and size of the imported symbol list(导入的符号列表的偏移量和大小) |
| HSRD/HSRC | (UInt64, UInt64) | Offset and size of the embedded source code section(嵌入式源代码部分的偏移量和大小) |
| UUID | UUID | UUID of the Metal library.(Metal 库的 UUID。) |
| ENDT | End of the header extension(头部扩展的结束) |
| Name(名称) | Content Data Type(内容数据类型) | Content(内容) |
|---|---|---|
| NAME | NULL-terminated C-style string(以 NULL 结尾的 C 风格字符串) | Install name of the library(库的安装名称) |
| DYNL | NULL-terminated C-style string(以 NULL 结尾的 C 风格字符串) | Linked dynamic library(链接的动态库) |
Variable list and imported symbol list have structures that are similar to that of the function list.(变量列表和导入的符号列表具有与函数列表类似的结构。)
Only exists if the metallib build process is configured to include source code(包含源代码)。
| Byte Range(字节范围) | Type(类型) | Content(内容) |
|---|---|---|
| 0...1 | UInt16 | Number of items in this section(此部分中的项目数) |
| 2...n | NULL-terminated C-style string(以 NULL 结尾的 C 风格字符串) | Link options of the metallib file(metallib 文件的链接选项) |
| n...m | NULL-terminated C-style string(以 NULL 结尾的 C 风格字符串) | Working directory(工作目录) |
| m... | Tag Group(标签组) | SARC tag |
Note: "Working directory" only exists in HSRD.(注意: "工作目录" 仅存在于 HSRD 中。)
Note: SARC tag uses 4-bytes (UInt32) content size.(注意: SARC 标签使用 4 字节 (UInt32) 内容大小。)
Content of the SARC tag(SARC 标签的内容)
| Byte Range(字节范围) | Type(类型) | Content(内容) |
|---|---|---|
| 0...n | NULL-terminated C-style string(以 NULL 结尾的 C 风格字符串) | ID of the source code archive(源代码存档的 ID) |
| n... | BZh | Bzip2 compressed source code archive(Bzip2 压缩的源代码存档) |
| Value(值) | Type(类型) | Value(值) | Type(类型) |
|---|---|---|---|
| 0x00 | None(无) | 0x01 | Struct(结构体) |
| 0x02 | Array(数组) | 0x03 | Float(浮点数) |
| 0x04 | Float2 | 0x05 | Float3 |
| 0x06 | Float4 | 0x07 | Float2x2 |
| 0x08 | Float2x3 | 0x09 | Float2x4 |
| 0x0A | Float3x2 | 0x0B | Float3x3 |
| 0x0C | Float3x4 | 0x0D | Float4x2 |
| 0x0E | Float4x3 | 0x0F | Float4x4 |
| 0x10 | Half(半精度浮点数) | 0x11 | Half2 |
| 0x12 | Half3 | 0x13 | Half4 |
| 0x14 | Half2x2 | 0x15 | Half2x3 |
| 0x16 | Half2x4 | 0x17 | Half3x2 |
| 0x18 | Half3x3 | 0x19 | Half3x4 |
| 0x1A | Half4x2 | 0x1B | Half4x3 |
| 0x1C | Half4x4 | 0x1D | Int(整数) |
| 0x1E | Int2 | 0x1F | Int3 |
| 0x20 | Int4 | 0x21 | UInt(无符号整数) |
| 0x22 | UInt2 | 0x23 | UInt3 |
| 0x24 | UInt4 | 0x25 | Short(短整数) |
| 0x26 | Short2 | 0x27 | Short3 |
| 0x28 | Short4 | 0x29 | UShort(无符号短整数) |
| 0x2A | UShort2 | 0x2B | UShort3 |
| 0x2C | UShort4 | 0x2D | Char(字符) |
| 0x2E | Char2 | 0x2F | Char3 |
| 0x30 | Char4 | 0x31 | UChar(无符号字符) |
| 0x32 | UChar2 | 0x33 | UChar3 |
| 0x34 | UChar4 | 0x35 | Bool(布尔值) |
| 0x36 | Bool2 | 0x37 | Bool3 |
| 0x38 | Bool4 | 0x3A | Texture(纹理) |
| 0x3B | Sampler(采样器) | 0x3C | Pointer(指针) |
| 0x3E | R8Unorm | 0x3F | R8Snorm |
| 0x40 | R16Unorm | 0x41 | R16Snorm |
| 0x42 | RG8Unorm | 0x43 | RG8Snorm |
| 0x44 | RG16Unorm | 0x45 | RG16Snorm |
| 0x46 | RGBA8Unorm | 0x47 | RGBA8Unorm_sRGB |
| 0x48 | RGBA8Snorm | 0x49 | RGBA16Unorm |
| 0x4A | RGBA16Snorm | 0x4B | RGB10A2Unorm |
| 0x4C | RG11B10Float | 0x4D | RGB9E5Float |
| 0x4E | RenderPipeline(渲染管线) | 0x4F | ComputePipeline(计算管线) |
| 0x50 | IndirectCommandBuffer(间接命令缓冲区) | 0x51 | Long(长整数) |
| 0x52 | Long2 | 0x53 | Long3 |
| 0x54 | Long4 | 0x55 | ULong(无符号长整数) |
| 0x56 | ULong2 | 0x57 | ULong3 |
| 0x58 | ULong4 | 0x59 | Double(双精度浮点数) |
| 0x5A | Double2 | 0x5B | Double3 |
| 0x5C | Double4 | 0x5D | Float8 |
| 0x5E | Float16 | 0x5F | Half8 |
| 0x60 | Half16 | 0x61 | Int8 |
| 0x62 | Int16 | 0x63 | UInt8 |
| 0x64 | UInt16 | 0x65 | Short8 |
| 0x66 | Short16 | 0x67 | UShort8 |
| 0x68 | UShort16 | 0x69 | Char8 |
| 0x6A | Char16 | 0x6B | UChar8 |
| 0x6C | UChar16 | 0x6D | Long8 |
| 0x6E | Long16 | 0x6F | ULong8 |
| 0x70 | ULong16 | 0x71 | Double8 |
| 0x72 | Double16 | 0x73 | VisibleFunctionTable(可见函数表) |
| 0x74 | IntersectionFunctionTable(相交函数表) | 0x75 | PrimitiveAccelerationStructure(图元加速结构) |
| 0x76 | InstanceAccelerationStructure(实例加速结构) | 0x77 | Bool8 |
| 0x78 | Bool16 |
如果您认为存在错误,请提出 issue。您也可以选择打开包含失败测试的 pull request。
如果没有 zhuowei 的研究,这个项目就不会启动。他的研究揭示了 metallib 文件的基本二进制布局、函数列表以及 Bitcode 部分。感谢 @zhuowei!
我尝试继续研究以获得 metallib 文件的完整结构,但发现仅凭猜测很难取得进展。因此,我将注意力转向 Metal.framework,希望找出该框架如何加载 metallib 文件。幸运的是,将 Metal.framework/Metal 拖到 Hopper Disassembler 后,并不太难。
Metal.framework 使用 MTLLibraryDataWithArchive::parseArchiveSync(...) 加载 metallib 文件。 MTLLibrary
文件以 0x424c544d (MTLB) 开头;文件大小记录在偏移量 0x10 处。
int __ZN25MTLLibraryDataWithArchive16parseArchiveSyncEPP7NSErrorb(void * * arg0, bool arg1) {
r12 = rdx;
r14 = arg1;
r13 = arg0;
(*(*arg0 + 0xb8))(arg0, 0x0); //LibraryWithFile::setPosition(...)
r15 = r13 + 0x78;
rbx = (*(*r13 + 0xc0))(r13, r15, 0x58); //LibraryWithFile::readBytes(...)
rax = *r13;
rax = (*(rax + 0xc8))(r13); //LibraryWithFile::getFileSize(...)
// 0x424c544d - MTLB
// File size field offset: 0x88 - 0x78 = 0x10
if (((rbx != 0x58) || (*(int32_t *)(r13 + 0x78) != 0x424c544d)) || (*(r13 + 0x88) != rax)) goto loc_6a65b;
...
loc_6a65b:
if (r14 == 0x0) goto loc_6a6c5;
loc_6a660:
rdx = @"Invalid library file";
...
}
偏移量 0x4 处的一个 Int16 值与目标平台相关。
loc_6a610:
// 0x7c - 0x78 = 0x4
rax = *(int16_t *)(r13 + 0x7c) & 0xffff;
if ((rax >= 0x0) || (r12 == 0x0)) goto loc_6a6ea;
loc_6a627:
if (r14 == 0x0) goto loc_6a6c5;
loc_6a630:
rdx = @"This library format is not supported on this platform (or was built with an old version of the tools)";
goto loc_6a689;
存在一个“Header Extension Section”(头部扩展节),其中包含有关“Dynamic Header Section”(动态头部节)、“Imported Symbol List”(导入符号列表)和“Variable List”(变量列表)的信息。
if (MTLLibraryDataWithArchive::parseHeaderExtension(r13, r13 + 0x100, r14) != 0x0) {
if (MTLLibraryDataWithArchive::parseDynamicHeaderSection(r13) != 0x0) {
if (MTLLibraryDataWithArchive::parseImportedSymbolListSection(r13) != 0x0) {
rax = MTLLibraryDataWithArchive::parseVariableListSection(r13);
} else {
rax = 0x0;
}
} else {
rax = 0x0;
}
} else {
rax = 0x0;
}
Bitcode 使用 SHA256 进行验证。
int ____ZN25MTLLibraryDataWithArchive15validateBitCodeEmmPK6NSDataRK12MTLUINT256_t_block_invoke(int arg0) {
...
CC_SHA256_Init(&var_B0);
CC_SHA256_Update(&var_B0, r14, *(int32_t *)(r15 + 0x38));
CC_SHA256_Final(&var_48, &var_B0);
...
}
许多 FourCC 代码。
// 0x454e4454 - ENDT
loc_6a8bc:
if (rax == 0x454e4454) goto loc_6a871;
...
// 0x54595045 - TYPE
loc_6a984:
if (rax == 0x54595045) goto loc_6a9dc;
...
// 0x44594e4c - DYNL
loc_6ae5b:
if (rax != 0x44594e4c) goto loc_6b002;
...
// 0x56455253 - VERS
loc_6b731:
if (rax == 0x56455253) goto loc_6b81c;
经过一番研究,我大致了解了 metallib 文件的结构。
该文件有一个 88 字节的头部,其中包含文件版本、目标平台、库类型、节索引等。
文件头中记录了 4 个节:
函数列表
公共元数据
私有元数据
Bitcode 模块
每个节都记录了偏移量和大小。 这意味着节可以是不连续的,这允许 Apple 在它们之间引入新的节而不会破坏兼容性。 Apple 确实为“header extension”节做了这一点 - 它位于函数列表和公共元数据节之间。
大多数节(bitcode 节除外)都类似于基于“标签”的结构。
FourCharCode 用作标签的名称/类型。
一个 UInt16(在大多数情况下)大小的值紧随标签名称之后。
源存档数据标签 SARC 不出所料地使用 UInt32 值表示其大小 - 源存档很容易超过 65KB。
标签被分组。
每个组代表一个项目的属性集。
标签组以 ENDT 标签结束。
接下来,我需要弄清楚每个标签/字段保存什么信息。 这很难从 Metal.framework 的汇编中获取,因为
某些字段可能纯粹是为工具或调试而设计的,因此 MTLLibraryDataWithArchive 可能只是忽略它们。
汇编是平台相关的。 例如,iOS 版本的 MTLLibraryDataWithArchive 可能只会检查 metallib 是否是为 iOS 构建的,而无法判断该库是否是为 macOS 构建的。
某些字段很难分析和跟踪。 例子:
函数的 OFFT 标签中有 3 个偏移量,它们指向哪里? 它们最终如何使用?
函数类型有哪些可能的值? 每个值的含义是什么?
似乎获得此信息的最快方法是通过实验。
我首先手动编译具有不同着色器、选项和 SDK 的 metal 文件,然后检查我感兴趣的每个字段。 我的桌面很快就被 metallib 文件和 HexFiend 窗口淹没了,但我没有找到太多有用的信息。 我需要一些可以自动构建 metallib 并只向我展示我感兴趣的字段的东西。
我想出了“Test Driven Guessing”(测试驱动猜测法)
基于手头的二进制结构概述编写一个 metallib 解析器。
在解析器中,记录当前未知的字段/标签(或一些相关字段)的值。
创建测试,使用不同类型的着色器和可能影响字段值的编译选项生成 metallib 文件,并使用解析器解析文件数据。
运行测试并分析日志以做出假设。
根据假设更新解析器。
再次运行测试进行验证。
经过几轮,我能够得到函数类型表、目标操作系统表以及 OFFT 标签中 3 个偏移量的含义。
我也在这个过程中发现了一些有趣的事情:
Metal 不支持 watchOS,但是,可以构建一个针对 watchOS 的 metallib。 Apple 确实在 watchOS SDK 中包含了一些 metallib。(例如:Xcode.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreImage.framework/ci_filters.metallib)
针对旧版本 iOS 的空 metallib 被 错误地标记为针对 macOS。
我无法构建目标操作系统值为 0x85 的 metallib。 起初我认为它可能被保留给隐藏的 realityOS,但后来发现它 更可能是为 bridgeOS。
2022 年 4 月 10 日
像 LAYR、VATY、CNST 等标签包含 Metal 数据类型的 UInt8 值。 可以使用 Metal.framework 中的一个私有类检索每个数据类型值的对应描述 - MTLTypeInternal
id value = [[NSClassFromString(@"MTLTypeInternal") alloc] initWithDataType:0x06];
NSLog(@"%@", value.description); // MTLDataTypeFloat4
我创建了一个命令行工具来生成 Metal 数据类型表。
cd Utilities
swift run metal-data-type-tools gen-markdown --columns 2 # generate a markdown table
swift run metal-data-type-tools gen-swift # generate a Swift enum for Metal data types.
2022 年 3 月 31 日
air-lld (Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/metal/ios/bin/air-lld) 也提供了很多关于 metallib 文件是如何构建的信息。 一些节名称和描述已更新。
int __ZN4llvm3air20MetalLibObjectWriter5writeEv() {
r14 = rdi;
rax = llvm::air::MetalLibObjectWriter::writeHeader();
if (rax != 0x0) goto loc_1000351b9;
loc_100035135:
rax = llvm::air::MetalLibObjectWriter::writeFunctionList();
if (rax != 0x0) goto loc_1000351b9;
loc_100035141:
rax = llvm::air::MetalLibObjectWriter::writeHeaderExtension();
if (rax != 0x0) goto loc_1000351b9;
loc_10003514d:
rax = llvm::air::MetalLibObjectWriter::writePublicMetadata();
if (rax != 0x0) goto loc_1000351b9;
loc_100035159:
rax = llvm::air::MetalLibObjectWriter::writePrivateMetadata();
if (rax != 0x0) goto loc_1000351b9;
loc_100035165:
rax = llvm::air::MetalLibObjectWriter::writeModuleList();
if (rax != 0x0) goto loc_1000351b9;
loc_100035171:
rax = llvm::air::MetalLibObjectWriter::writeSources();
if (rax != 0x0) goto loc_1000351b9;
loc_10003517d:
rax = llvm::air::MetalLibObjectWriter::writeDynamicHeader();
if (rax != 0x0) goto loc_1000351b9;
loc_100035189:
rax = llvm::air::MetalLibObjectWriter::writeVariableList();
if (rax != 0x0) goto loc_1000351b9;
loc_100035195:
rax = llvm::air::MetalLibObjectWriter::writeImportedSymbolList();
if (rax != 0x0) goto loc_1000351b9;
loc_1000351a1:
rax = llvm::air::MetalLibObjectWriter::computeUUID();
if (rax != 0x0) goto loc_1000351b9;
loc_1000351ad:
rax = llvm::air::MetalLibObjectWriter::backpatchAllLocations();
if (rax == 0x0) goto loc_1000351c2;
loc_1000351b9:
rbx = rax;
goto loc_1000351bb;
loc_1000351bb:
rax = rbx;
return rax;
loc_1000351c2:
rbx = 0x0;
std::__1::system_category();
goto loc_1000351bb;
}