news 2026/10/7 8:34:36

darwin-xnu libkdd 深度解析:从 KCDATA 内核分块数据格式到 kdd 命令行解析工具

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
darwin-xnu libkdd 深度解析:从 KCDATA 内核分块数据格式到 kdd 命令行解析工具
  • 操作系统
  • 驱动开发

【免费下载链接】darwin-xnu

Legacy mirror of Darwin Kernel. Replaced by https://github.com/apple-oss-distributions/xnu

项目地址:https://gitcode.com/gh_mirrors/da/darwin-xnu
点击查看免费下载

KCDATA(Kernel Chunked Data)是 darwin-xnu 内核中一套自描述(self-describing)的数据序列化格式,用于把内核动态数据(崩溃信息、stackshot 快照、进程退出原因等)安全传递给用户态工具,而无需让用户态工具绑定某个固定的 struct 版本。libkdd是这套格式的用户态解析库,提供从底层迭代器到高层字典化的完整解析 API;本文以 libkdd/README.md 为核心骨架,结合 kcdata.h、kdd.h、kcdata_core.m 与 kdd_main.m 等源码,完整讲解 KCDATA 的磁盘布局、四类核心特性(带描述数据、容器标记、按需自定义类型、流式压缩)以及端到端的解析与命令行使用,读完你就能读懂 kernel 导出的任何 KCDATA 缓冲,并自行编写或扩展解析工具。

一、KCDATA 是什么:为什么内核需要"分块数据"

内核态与用户态之间传递数据,传统做法是双方各自编译一份struct定义:内核按布局填充,用户态按同一布局读取。问题在于,一旦内核需要增加字段或调整结构,所有旧版用户态工具都必须重新编译,否则要么读错偏移,要么直接崩溃。

KCDATA 的设计目标正是为了解决这一痛点。它的核心思想记录在 kcdata.h 顶部注释的 "KCDATA MANIFESTO" 中,要点如下:

  • KCDATA 是自描述的序列化格式,用于"以最小代价把嵌套数据结构从 xnu 带出来",并且易于解析;
  • 它允许新增字段、演进格式而不破坏旧解析器;
  • KCDATA 是适合长期存储的持久格式(可以落盘保存),因此必须始终保持解析旧版本数据的能力。

围绕这一目标,kcdata.h明确列出了格式演进必须遵守的硬性不变量(invariants),任何改动都不许破坏它们:

不变量说明
魔数唯一性任何 magic number 永远不能是自身或其他魔数的字节交换(byteswap),以免大小端架构下产生歧义
类型不可删除永远不要移除任何已定义的类型
结构必须 packed所有 KCDATA struct 必须使用__attribute__((packed))打包,并且只能使用固定宽度类型
类型定义不可改永远不要修改已有类型的定义,只能在末尾追加新字段
追加字段需新增版本需要在末尾追加字段时,不要改动旧结构,而是定义带新字段的新结构(thread_snapshot_v3就是范例),同时保持旧读者源码兼容
修改需跑测试如果修改 libkdd 或 kcdata.py,必须运行libkdd下的单元测试
新增类型需加样本新增或扩展类型后,应在libkdd/tests添加样本测试,确保未来 libkdd 改动仍能正确解析你的结构

同时,解析方(用户态工具)也有两条必须遵守的准则:

  1. 检查每个 struct(含数组元素)的 length 字段:如果结构比预期长,必须忽略多余数据;
  2. 忽略不认识的类型:遇到无法识别的 type,直接跳过,不能当作错误。

再加上"尽量不新增取代旧类型的新版本,而是扩展长度或增加补充类型""除非明确要求,不要从已有格式中移除信息"等前向兼容建议,KCDATA 得以实现"旧工具尽量也能使用新数据"的目标。

二、KCDATA 格式布局:从魔数头到 END 标记

KCDATA 缓冲区采用统一的"头部 + 一系列 chunk + 结尾"结构。README 中的布局图逐字节说明了每一段的含义:

| 8 - bytes | |---------------------------| ------ offset = 00 | type = MAGIC | LENGTH | # BEGIN Header | 0 | |---------------------------| ------ offset = 16 | type | size | # chunk header | flags | |---------------------------| ------ offset = 32 | data | # arbitrary data (len=16) |___________data____________| |---------------------------| ------ offset = 48 | type | size | # chunk header | flags | |---------------------------| ------ offset = 64 | data | # arbitrary data (len=32) | data | | data | |___________data____________| |---------------------------| ------ offset = 96 | type = END | size=0 | # chunk header | 0 |

对应到 kcdata.h 中的结构定义,缓冲区最小单位是struct kcdata_item:

struct kcdata_item { uint32_t type; uint32_t size; /* len(data) */ uint64_t flags; char data[]; /* must be at the end */ };

其语义如下:

  • type:描述这段数据是什么。例如TASK_CRASHINFO_UUID表示其后跟着一个 UUID;这些类型需要定义在task_corpses.h(对应仓库中的崩溃信息头)中,供用户态检视工具直接消费。0 – 0x7ff 范围预留给基础类型(如 int、long 等)并在 kcdata.h 中定义;
  • size:data[]的长度(字节数);
  • flags:按 item 类别承载不同语义(详见下文"三类复合 item");
  • data[]:任意负载,长度由size决定,必须是结构末尾的柔性数组。

2.1 魔数与缓冲区分级

每个 KCDATA 缓冲区的第一个 item 是一个 magic number,用于标识"这一整块数据属于哪一类"。kcdata.h中定义的主要魔数如下(每个魔数还标注了所有者头文件与类型号段):

魔数宏值所有者 / 用途类型号段
KCDATA_BUFFER_BEGIN_CRASHINFO0xDEADF157崩溃信息(task_corpse.h体系)0x800 – 0x8ff
KCDATA_BUFFER_BEGIN_STACKSHOT0x59a25807stackshot 快照(sys/stackshot.h体系)0x900 – 0x93f
KCDATA_BUFFER_BEGIN_COMPRESSED0x434f4d50压缩后的 stackshot0x900 – 0x93f
KCDATA_BUFFER_BEGIN_DELTA_STACKSHOT0xDE17A59Adelta stackshot(增量快照)0x940 – 0x9ff
KCDATA_BUFFER_BEGIN_OS_REASON0x53A20900进程退出原因(sys/reason.h)0x1000 – 0x103f
KCDATA_BUFFER_BEGIN_XNUPOST_CONFIG0x1e21c09fxnupost 内核测试配置(osfmk/tests/kernel_tests.c)0x1040 – 0x105f

缓冲区最后以一个type = KCDATA_TYPE_BUFFER_END(0xF19158ED)、size = 0的 chunk 收尾。解析器正是依赖这个 END 标记判断"是否读完了整个合法缓冲区"(KCDATA_ITER_FOREACH_FAILED宏会检查这一点)。

2.2 三类复合 item:数组、容器与类型定义

除了基础类型(带描述类型等),flags字段承载着三类复合结构的元信息,这在struct kcdata_item的注释中定义得很清楚:

  • 结构(struct)类 item:padding = flags & 0xf,has_padding = (flags & 0x80) >> 7。has_padding用于消除歧义——例如旧内核发出的thread_snapshot_v2(0x68 字节)和现代thread_snapshot_v3(0x70 字节)都可能把 padding 记为 0,此时只有靠这个额外标志位区分。这也是迭代器函数中为STACKSHOT_KCTYPE_THREAD_SNAPSHOT硬编码特判的原因;
  • 容器(container)类 item:container_id = flags,用于把 BEGIN/END 标记配对;
  • 数组(array)类 item:element_count = flags & UINT32_MAX,element_type = (flags >> 32) & UINT32_MAX。

数组还有一套兼容旧内核的KCDATA_TYPE_ARRAY_PAD0–KCDATA_TYPE_ARRAY_PADf(0x20 – 0x2f)类型:旧内核的KCDATA_TYPE_ARRAY(0x11)在计算元素大小时因 16 字节对齐而存在歧义,新内核改用ARRAY_PADn明确告知实际补齐了多少字节。kcdata_iter_type()会把ARRAY_PAD*统一归一化为KCDATA_TYPE_ARRAY返回。

2.3 核心类型 ID 速查

kcdata.h同时给出了数量可观的标准类型定义,常用的包括:

  • 带描述的基础类型:KCDATA_TYPE_STRING_DESC(0x1)、KCDATA_TYPE_UINT32_DESC(0x2)、KCDATA_TYPE_UINT64_DESC(0x3)、KCDATA_TYPE_INT32_DESC(0x4)、KCDATA_TYPE_INT64_DESC(0x5)、KCDATA_TYPE_BINDATA_DESC(0x6)——它们的负载是"KCDATA_DESC_MAXLEN-1字节的字符串描述 + 其余字节的数据";
  • 复合类型:KCDATA_TYPE_ARRAY(0x11,已废弃勿用)、KCDATA_TYPE_TYPEDEFINTION(0x12,按需描述新类型的元类型)、KCDATA_TYPE_CONTAINER_BEGIN(0x13) /KCDATA_TYPE_CONTAINER_END(0x14);
  • 通用数据类型:如KCDATA_TYPE_LIBRARY_LOADINFO(0x30)、KCDATA_TYPE_LIBRARY_LOADINFO64(0x31)、KCDATA_TYPE_TIMEBASE(0x32)、KCDATA_TYPE_PID(0x36)、KCDATA_TYPE_PROCNAME(0x37)、KCDATA_TYPE_NESTED_KCDATA(0x38,内嵌 KCDATA 缓冲) 等;
  • stackshot 专属类型:STACKSHOT_KCTYPE_IOSTATS(0x901)、STACKSHOT_KCCONTAINER_TASK(0x903)、STACKSHOT_KCTYPE_TASK_SNAPSHOT(0x905)、STACKSHOT_KCTYPE_THREAD_SNAPSHOT(0x906)、STACKSHOT_KCTYPE_BOOTARGS(0x90e)、STACKSHOT_KCTYPE_OSVERSION(0x90f) 等,且kcdata.h明确要求:任何对STACKSHOT_KCTYPE_*的改动都必须同步更新kcdata/libkdd/kcdtypes.c;
  • 崩溃信息类型:TASK_CRASHINFO_BEGIN到TASK_CRASHINFO_END之间的一整套类型,如TASK_CRASHINFO_UUID(0x804)、TASK_CRASHINFO_PID(0x805)、TASK_CRASHINFO_PPID(0x806)、TASK_CRASHINFO_RUSAGE_INFO(0x808)、TASK_CRASHINFO_EXCEPTION_CODES(0x80e) 等;
  • 退出原因类型:EXIT_REASON_SNAPSHOT(0x1001)、EXIT_REASON_USER_DESC(0x1002)、EXIT_REASON_USER_PAYLOAD(0x1003) 等。

这些类型 ID 是"内核往缓冲区里写什么、用户态按什么解析"的共同契约。若某一类型未被预定义,libkdd 会把它当作裸uint8_t[]处理(见下文解析部分)。

三、特性一:带描述数据——用户态不"认识"也能打印

KCDATA 的第一大特性是通用数据带描述(generic data with description)。内核可以这样往缓冲区里添加一个带文本说明的 uint64:

kcdata_add_uint64_with_description(cdatainfo, 0x700, "NUM MACH PORTS"); // 以及更多 add_<type>_with_description() 系列函数

用户态工具只要读取这段描述字符串,即使没有预先编译对该字段的任何认识,也能把数据打印出来。因为这一机制的存在,像rusage这样的结构完全可以引入新版本而不会破坏已有工具——旧工具看到不认识的版本,只需忽略未知字段即可。

README 给出了一个真实的十六进制转储示例,直观展示了"描述 + 数据"在缓冲区中的样子(下面标注了关键字节含义):

0000 57 f1 ad de 00 00 00 00 00 00 00 00 00 00 00 00 W............... 0010 01 00 00 00 00 00 00 00 30 00 00 00 00 00 00 00 ........0....... 0020 50 49 44 00 00 00 00 00 00 00 00 00 00 00 00 00 PID............. 0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0040 9c 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0050 01 00 00 00 00 00 00 00 30 00 00 00 00 00 00 00 ........0....... 0060 50 41 52 45 4e 54 20 50 49 44 00 00 00 00 00 00 PARENT PID...... 0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0080 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0090 ed 58 91 f1

逐段解读:

  • 开头 8 字节是type = KCDATA_BUFFER_BEGIN_*魔数与长度(本例0xdeadf157是崩溃信息类缓冲区的头,注意小端序字节序显示为57 f1 ad de);
  • 第二个 chunk:type = 0x1(KCDATA_TYPE_STRING_DESC所属的带描述类型族)、size = 0x30(48 字节负载)、flags 为 0;负载前 31 字节是 ASCII 描述PID(50 49 44),随后是对齐填充,再后面是数据区0x9c(即十进制 156,一个 PID 值);
  • 第三个 chunk:同样布局,但描述为PARENT PID(50 41 52 45 4e 54 20 50 49 44),数据区为0x1;
  • 最后一个 chunk 的type = 0xF19158ED(KCDATA_TYPE_BUFFER_END)、size = 0,即缓冲区终止标记。

也就是说,PID = 156、PARENT PID = 1这两个键值对完全由缓冲区自带的描述字符串产生,用户态工具零预知也能正确输出。这正是该特性在崩溃信息(corpse)与 stackshot 场景中被大量使用的原因。

四、特性二:容器标记——打包"复合数据对象"

当某个内核数据类型非常复杂,需要在同一个容器对象里塞入多个可选字段供消费者理解任意数据时,就用**容器标记(container markers)**把数据打包。最典型的例子是stackshot 代码:它收集并描述某个 task 在众多子系统下的状态,包括 IO 统计、VM 计数器、进程名/标志、系统调用计数等,这些数据天然适合放进一个容器。

使用方式是在数据流中插入一对 BEGIN/END 标记:

kcdata_add_container_marker(kcdata_p, KCDATA_TYPE_CONTAINER_BEGIN, STACKSHOT_KCCONTAINER_TASK, task_uniqueid); // 在这里添加多个数据项,或 add_<type>_with_description() 系列调用 kcdata_add_container_marker(kcdata_p, KCDATA_TYPE_CONTAINER_END, STACKSHOT_KCCONTAINER_TASK, task_uniqueid);

这里的语义要点(来自kcdata.h的注释与struct kcdata_itemflags 定义):

  • KCDATA_TYPE_CONTAINER_BEGIN(0x13) 的data 段存放容器类型(container type),例如上面的STACKSHOT_KCCONTAINER_TASK(0x903);
  • BEGIN 与 END 两个 header 都携带一个(uint64_t)的容器 ID(存在flags字段里,即kcdata_iter_container_id()),用于把嵌套数据正确配对——同名的 container 可能出现多个,靠 ID 区分;
  • 容器可以任意嵌套:一个 task 容器里可以有多个 thread 容器,thread 容器里还可以再嵌套其他容器。

在解析侧,libkdd 的parseKCDataContainer()会从 BEGIN 开始迭代,遇到 END 时校验container_id是否与 BEGIN 一致,不一致即报 "container marker mismatch" 错误;若迭代到缓冲区末尾仍未遇到 END,则报 "missing container end" 错误。子容器会被单独收集后合并回父字典,重名容器 ID 会触发 "repeated container id" 错误。

五、特性三:按需自定义类型——把"struct 定义"也写进缓冲区

KCDATA 的自描述能力更进一步:内核可以在运行时把某个自定义数据类型的完整定义写进缓冲区,消费者无需事先了解该类型,只要解析类型信息就能获得解读数据所需的全部知识。这正是KCDATA_TYPE_TYPEDEFINTION(0x12) 元类型的用途。

README 给出的经典例子是自定义磁盘 IO 统计结构。先定义内核侧的结构:

struct sample_disk_io_stats { uint64_t disk_reads_count; uint64_t disk_reads_size; uint64_t io_priority_count[4]; uint64_t io_priority_size; } __attribute__ ((packed));

然后用kcdata_subtype_descriptor数组逐字段描述它的内存布局:

struct kcdata_subtype_descriptor disk_io_stats_def[] = { {KCS_SUBTYPE_FLAGS_NONE, KC_ST_UINT64, 0 * sizeof(uint64_t), sizeof(uint64_t), "disk_reads_count"}, {KCS_SUBTYPE_FLAGS_NONE, KC_ST_UINT64, 1 * sizeof(uint64_t), sizeof(uint64_t), "disk_reads_size"}, {KCS_SUBTYPE_FLAGS_ARRAY, KC_ST_UINT64, 2 * sizeof(uint64_t), KCS_SUBTYPE_PACK_SIZE(4, sizeof(uint64_t)), "io_priority_count"}, {KCS_SUBTYPE_FLAGS_ARRAY, KC_ST_UINT64, (2 + 4) * sizeof(uint64_t), sizeof(uint64_t), "io_priority_size"}, };

最后把类型定义本身压入缓冲区:

kcdata_add_type_definition(kcdata_p, KCTYPE_SAMPLE_DISK_IO_STATS, "sample_disk_io_stats", &disk_io_stats_def[0], sizeof(disk_io_stats_def)/sizeof(struct kcdata_subtype_descriptor));

此后内核就可以用KCTYPE_SAMPLE_DISK_IO_STATS这个数字作为 type 把实际的sample_disk_io_stats数据写进缓冲区;用户态只要之前收到了这份类型定义,就能把任意新数据解读出来。

5.1 子类型描述符的字段语义

struct kcdata_subtype_descriptor的每个字段(kcdata.h 中有完整注释)含义如下:

字段含义
kcs_flags标志位,见下方取值
kcs_elem_type元素基础类型,取值见enum KCDATA_SUBTYPE_TYPES(KC_ST_CHAR=1 到KC_ST_UINT64=9)
kcs_elem_offset该字段在结构体中的字节偏移
kcs_elem_size元素大小;对数组类型,是KCS_SUBTYPE_PACK_SIZE(count, size)打包后的值(count 占高 16 位、size 占低 16 位)
kcs_name[KCDATA_DESC_MAXLEN]字段名,最多 31 字节(含结尾 NUL)

kcs_flags的取值语义直接决定了 libkdd 如何呈现解析结果:

  • KCS_SUBTYPE_FLAGS_NONE(0x0):普通字段;
  • KCS_SUBTYPE_FLAGS_ARRAY(0x1):数组字段;
  • KCS_SUBTYPE_FLAGS_STRUCT(0x2):强制按结构体处理。正常情况下,只有一个 subtype 描述符的类型定义会被当作简单类型(libkdd 会把整数 42 直接表示成42);但设置了该标志后,即使只有一个字段也会表示为{"field_name": 42}这样的字典形式;若类型定义包含多个 subtype,则总是按结构体处理;
  • KCS_SUBTYPE_FLAGS_MERGE(0x4):合并标志,效果与 STRUCT 相反——即使有多个元素,也全部作为父字典的独立属性展开,而不是包成一个子结构。

对应地,kcdata.h提供三个内联辅助函数:kcs_get_elem_size()(数组返回count × elem_size,普通字段直接返回大小)、kcs_get_elem_count()(数组返回 count,否则返回 1)、kcs_set_elem_size()(写入时对 count>1 进行打包校验,超出 0xffff 返回 -1)。

六、特性四:流式压缩——让 panic stackshot 不再吃满内存

为了避免为 panic stackshot 预留超大内存,KCDATA 支持流式压缩:新推入缓冲区的数据会被自动压缩,压缩算法由 API 调用者选择(当前支持 pass-through 透传和 zlib 两种,WKDM 为规划中的未来方案)。

开启压缩的入口:

kcdata_init_compress(kcdata_p, hdr_tag, memcpy_f, comp_type);

参数含义:

  • kcdata_p:要使用的 kcdata 缓冲区;
  • hdr_tag:常规的头部 tag,标明这是哪一类 kcdata 缓冲区;
  • memcpy_f:复制数据用的 memcpy(3) 函数,可选;
  • comp_type:压缩类型,例如KCDCT_ZLIB。

压缩初始化之后的行为:

  1. 所有自描述 API(add_<type>系列)都会自动压缩;
  2. 新增了一组显式推送 API,只有调用过kcdata_init_compress()才会真正压缩:
API作用
kcdata_push_data(data, type, size, input_data)把[input_data, input_data+size)的type类型数据压入缓冲区,按需压缩
kcdata_push_array(data, type_of_element, size_of_element, count, input_data)压入元素类型为type_of_element、每个元素size_of_element字节、共count个元素的数组
kcdata_compression_window_open/close(data)当待压数据难以预测时,可打开一个"压缩窗口":open 与 close 之间不做压缩,close 时底层压缩算法会把窗口数据压缩进缓冲区并自动回卷当前 END 标记(kern_cdata.c内有 ASCII 图辅助理解)
kcdata_finish_compression(data)结束时必须调用,冲刷底层压缩缓冲,并向缓冲区写入压缩统计信息,供后续解压使用

整个缓冲区用完后再调用kcdata_deinit_compress()释放压缩算法内部申请的缓冲。

这一特性在用户态同样有对应:kdd_main.m中,如果输入文件解析失败且错误码为KERN_INVALID_VALUE,工具会尝试用 zlib(inflateInit2(&stream, 16+MAX_WBITS))对输入流做解压后再解析,从而支持直接读取压缩过的 stackshot 文件。这也是libkdd/tests中大量*.plist.gz样本存在的原因。

七、libkdd 用户态解析库:API 全景与解析原理

libkdd是这套格式在用户态的 Objective-C 解析库,公开 API 集中在 kdd.h,底层实现分布在 kcdata_core.m、KCDBasicTypeDescription.m、KCDStructTypeDescription.m、KCDEmbeddedBufferDescription.m 中。

7.1 高层 API:一调用把整个缓冲区变成字典

面向最终使用者的三个核心函数:

  • parseKCDataBuffer(void *dataBuffer, uint32_t size, NSError **error):解析完整 KCDATA 缓冲区,返回NSDictionary *。若缓冲区不以已知魔数开头,返回 NULL 且 error 的 code 为KERN_INVALID_VALUE;数组与容器会尽量递归归组;未知类型以"Type_0x123"为键返回NSData对象;
  • parseKCDataArray(kcdata_iter_t iter, NSError **error):把以KCDATA_TYPE_ARRAY开头的 item 解析为"类型名 → 元素数组"的字典;
  • parseKCDataContainer(kcdata_iter_t *iter, NSError **error):解析一个容器(迭代器会被推进到 END 标记处),容器内的每个子结构成为字典字段,嵌套容器递归并入同一字典。

7.2 类型对象体系:KCDataType抽象

KCDataType是所有类型描述对象的基类,接口为:

@interface KCDataType : NSObject - (NSDictionary *)parseData:(void *)dataBuffer ofLength:(uint32_t)length; - (NSString *)name; - (unsigned int)typeID; - (BOOL)shouldMergeData; @end

类型查找入口是getKCDataTypeForID(uint32_t typeID):先在缓存knownTypes中查;命中KCDATA_TYPE_NESTED_KCDATA时构造KCDEmbeddedBufferDescription;否则调用kcdata_get_typedescription()(实现在 kcdtypes.c,内含一张庞大的已知类型表)查询类型定义;查不到就退回KCDBasicTypeDescription createDefaultForType:,把数据当作uint8_t[]并命名Type_0x%x。KCDataTypeNameForID()则返回类型名,未知类型直接返回数值字符串。

7.3 基础类型与结构类型的解析细节

KCDBasicTypeDescription把单个kcdata_subtype_descriptor解析为字典:字段解析时先按kcs_elem_offset定位数据,再按kcs_elem_type用memcpy无对齐读取(read_unaligned宏),映射为NSNumber/NSString;char 数组按字符串处理(用strnlen校验 NUL 终止);数组按元素逐一解析成NSArray。它的shouldMergeData恒为YES,因为带描述类型携带自己的键名,需要直接并入父容器。

KCDStructTypeDescription聚合多个字段:把每个子字段解析结果合并进同一个字典;对KCDATA_TYPE_TYPEDEFINTION类型还会把每个kcdata_subtype_descriptor的文本描述收集到"fields"数组;对typeID在 0x1 – 0x6 之间的带描述类型(_needDescriptionAsKey = YES),会把desc字符串提升为字典键、data作为值,这正是"描述即键名"机制的落地。其shouldMergeData返回_needDescriptionAsKey || _flagsRequestedMerge,与KCS_SUBTYPE_FLAGS_MERGE语义一一对应。

7.4 安全迭代器:解析的基石

所有解析都建立在 kcdata.h 提供的安全迭代器上:

  • kcdata_iter(buffer, size)构造迭代器并记录缓冲区终点end;
  • kcdata_iter_valid(iter)校验item + sizeof(struct kcdata_item) + size不越界;
  • kcdata_iter_next(iter)按"item 头 + size"步进到下一 chunk;
  • kcdata_iter_size(iter)返回去 padding 后的有效负载长度(对STACKSHOT_KCTYPE_THREAD_SNAPSHOT、STACKSHOT_KCTYPE_SHAREDCACHE_LOADINFO有 legacy 特判,处理历史 padding 标志缺失问题);
  • KCDATA_ITER_FOREACH(iter)宏从头迭代到KCDATA_TYPE_BUFFER_END,KCDATA_ITER_FOREACH_FAILED(iter)判断是否"中途失效或缺失 END 标记";
  • kcdata_iter_array_valid/elem_type/elem_count/elem_size系列提供数组的严格校验与元素大小推断(旧KCDATA_TYPE_ARRAY通过kcdata_iter_array_size_switch()的一张固定大小表推算,不在表内的元素类型视为非法数据)。

八、实战:用 kdd 命令解析真实数据

libkdd附带的命令行工具入口是 kdd_main.m,用法为:

usage: kdd [-p] FILE

参数说明:

  • FILE:输入文件路径;传-时从标准输入读取数据(支持管道);
  • -p:把解析结果以XML plist 格式输出到 stdout;不带该选项则输出字典的文本描述;
  • 失败时进程返回 1,并在 stderr 输出错误信息。

读取文件时工具使用NSDataReadingMappedIfSafe(安全内存映射),并拒绝超过UINT32_MAX的大文件。整个解析流程体现了前文的容错设计:

  1. 直接调用parseKCDataBuffer()尝试解析;
  2. 若失败且错误码为KERN_INVALID_VALUE(即魔数不匹配),先用 zlib 流式解压(inflateInit2(&stream, 16+MAX_WBITS))再尝试解析——对应读取压缩过的 stackshot;
  3. 若仍失败且错误码为KERN_INVALID_VALUE,再尝试把输入当作 Base64 编码数据解码后解析;
  4. 最终仍失败则报错退出。

8.1 用仓库自带样本实测

libkdd/tests目录存放了大量真实内核产出的样本,既用于单元测试,也可以直接拿来体验 kdd 工具:

  • 崩溃信息类:corpse-sample、corpse-twr-sample、corpse-twr-sample-v2,以及对应的.plist.gz参考输出(可校验工具输出);
  • stackshot 类:stackshot-sample、stackshot-sample-asid、stackshot-sample-coalitions、stackshot-sample-duration、stackshot-sample-new-arrays、stackshot-sample-thread-groups、stackshot-sample-turnstileinfo等 30 余个变体;
  • delta stackshot 类:delta-stackshot-sample-new-arrays、delta-stackshot-sample-old-arrays;
  • 退出原因类:exitreason-sample、exitreason-codesigning;
  • 其他:nested-sample(嵌套容器)、test-twr-sample(thread waitinfo)、xnupost_testconfig-sample。

例如解析一个崩溃信息样本并输出 plist:

kdd -p libkdd/tests/corpse-sample

解析一个 stackshot 样本:

kdd libkdd/tests/stackshot-sample

若要验证压缩缓冲区路径,可以解压对应的.plist.gz参考文件比对输出结构。注意:这些样本是二进制数据文件,请用xxd/hexdump等工具查看原始字节,不要用文本编辑器打开。由于kdd是 Foundation 命令行程序,构建与运行依赖 macOS 工具链(kdd.xcodeproj提供 Xcode 工程配置)。

8.2 样本揭示的典型输出形态

以带描述数据与容器特性为例,解析后字典大致呈现如下层次(对应 README 中的十六进制示例):

kcdata_crashinfo (或 kcdata_stackshot / kcdata_reason 等根键) ├── PID : 156 ← 来自 "PID" 描述 ├── PARENT PID : 1 ← 来自 "PARENT PID" 描述 ├── <container 类型名> ← 容器 BEGIN 展开的字典 │ ├── <字段名> : <值> │ └── <嵌套容器名> : {...} └── Type_0x123 : <NSData> ← 未知类型原样返回

九、深入源码:类型定义表与测试保障

  • kcdtypes.c:内置"已知类型定义表"。kcdata_get_typedescription()用_SUBTYPE/_SUBTYPE_ARRAY/_STRINGTYPE宏快速生成每种类型的 subtype 描述(例如KCDATA_TYPE_UINT64_DESC= 31 字节descchar 数组 + 一个uint64_t的data)。仓库注释明确要求:改动STACKSHOT_KCTYPE_*等类型必须同步此文件;
  • KCDStructTypeDescription.m:结构类型解析与"描述即键"逻辑的实现;
  • tests/Tests.swift:单元测试入口,与corpse-*、stackshot-*、exitreason-*等样本一一对应,是"改动 libkdd 必须跑测试"这一项目不变量在代码层的落实;
  • xnu.libkdd.plist:库的产品信息配置(framework 名、版本等);
  • kdd.framework/module.modulemap:把kcdata.h等头文件以KernelChunkedData等模块名暴露给 Swift/Clang 导入。

从源码结构看,libkdd 的架构是一条清晰的管线:安全迭代器(kcdata.h)→ 类型对象解析(KCDBasic/Struct/Embedded)→ 缓冲区分发与合并(kcdata_core.m)→ 命令行输出(kdd_main.m)。内核侧对应的写端(kcdata_add_*、kcdata_init_compress等)位于 osfmk/kern/kern_cdata.c,写读两端共用同一份kcdata.h契约,这也是 KCDATA 能长期演进而不破坏兼容的根本原因。

十、小结:KCDATA 设计精髓一览

维度设计
序列化单元struct kcdata_item {type, size, flags, data[]},16 字节对齐,END 标记收尾
缓冲区分级魔数头区分 crashinfo / stackshot / delta-stackshot / os-reason / xnupost 五类,各自持有类型号段
前向兼容只追加不改写、新版本新结构、解析方忽略未知类型与多余长度
带描述数据负载内置 ≤31 字节描述字符串,用户态零预知可打印
容器标记BEGIN/END + 容器 ID 配对,支持任意嵌套复合对象
按需类型定义KCDATA_TYPE_TYPEDEFINTION携带完整 subtype 布局,消费者现场学习新类型
流式压缩pass-through / zlib,压缩窗口 + 结束冲刷 + 统计信息落缓冲
用户态落地parseKCDataBuffer三函数 + 安全迭代器 +kdd [-p] FILE命令行

对开发者而言,这套设计最值得借鉴的是它对演进与兼容的极端重视:格式规范(kcdata.h清单)、用户态解析(libkdd)、内核写端(kern_cdata.c)三方共享同一契约,并配以大量真实样本与单元测试锁住行为。无论你是要调试 stackshot 数据、分析崩溃 corpse,还是要为自己的内核特性设计新的数据导出格式,KCDATA + libkdd 都是可以直接复用与扩展的范本。

  • 操作系统
  • 驱动开发

【免费下载链接】darwin-xnu

Legacy mirror of Darwin Kernel. Replaced by https://github.com/apple-oss-distributions/xnu

项目地址:https://gitcode.com/gh_mirrors/da/darwin-xnu
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 8:34:32

30天从零开始学AI应用开发(Day 19):项目二完结:RAG 知识库问答系统,把自己攒的资料变成私人顾问

这是系列的第 19 篇。整个系列写给零基础、想入行 AI 的朋友&#xff0c;每天一篇&#xff0c;30 天后你会做出 3 个能写进简历的项目。这篇解决什么问题 Day 1 的时候我埋了一个包袱&#xff0c;说 Day 19 会做 RAG 知识库问答系统&#xff0c;让大家在评论区说说想用 AI 解决…

作者头像 李华
网站建设 2026/10/7 8:33:38

你的 AI 对话记录,存在谁家硬盘上?——聊聊 AI 编程的「上下文归属」

写代码的你&#xff0c;大概都遇到过这一幕。 你和 AI 编程助手磨合了三个月&#xff1a;它帮你理清了代码库架构、记住了你的命名习惯、踩过的坑、定过的规矩。然后你换了一把新工具——这一切&#xff0c;归零。 不是你的记忆出了问题。是这些上下文&#xff0c;从来就不在…

作者头像 李华
网站建设 2026/10/7 8:31:50

整装行业细分场景关键词布局方法:从奶油风到环保整装的内容组织

摘要整装行业的内容竞争&#xff0c;正在从“通用词争夺”转向“细分场景争夺”。用户不再只搜索“整装公司”&#xff0c;而是搜索“奶油风整装”“软装一体化装修”“老房改造整装”“环保整装”等带有明确场景指向的关键词。这些细分场景关键词&#xff0c;搜索量虽不及通用…

作者头像 李华