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;
}