Skip to main content

NKM 内核模块二进制格式规范 V1.0

NKM 内核模块二进制格式规范

文档版本:V1.0(对应 Novus OS V1.6-FinalSpec) 适用对象:NKM 加载器开发者、驱动开发者、模块签名工具链开发者 平台:x86_64 UEFI 状态:格式冻结;哈希/签名规则已修复并固化,不得变更


1. 概述

NKM(Novus Kernel Module)是 Novus OS 的内核动态模块格式,扩展名 .nkm。 用途:驱动、文件系统扩展、Win32 兼容层、GPU DRM 驱动等可加载组件。

与 NUC(用户态程序)的本质区别:

维度 NUC(用户态) NKM(内核模块)
加载位置 用户虚拟地址空间 内核地址空间
权限 用户态页表,Ring3 内核页表,Ring0
签名 无要求 必须 Ed25519 签名
入口 entry_point 用户函数 nkm_init() + nkm_exit() 双入口
卸载 进程退出自动回收 需 nkm_unload 显式调用
访问范围 仅自己地址空间 可访问全部内核符号

2. 文件整体布局

偏移 0x0000 ┌──────────────────────────────┐
            │  NKM Header (nkm_hdr_t)      │   固定 208 字节
偏移 0x00D0 ├──────────────────────────────┤
            │  Module Dependencies         │   dep_count × nkm_dep_t (16B/条)
偏移 ...    ├──────────────────────────────┤
            │  Segment Table               │   seg_count × nkm_segment_t (32B/条)
偏移 ...    ├──────────────────────────────┤
            │  Symbol Table                │   sym_count × nkm_sym_t (24B/条)
偏移 ...    ├──────────────────────────────┤
            │  Relocation Table           │   rel_count × nkm_rel_t (16B/条)
偏移 ...    ├──────────────────────────────┤
            │  String Table               │
偏移 ...    ├──────────────────────────────┤
            │  段数据(.text/.data/.rodata)│   4KiB 对齐
            └──────────────────────────────┘

3. NKM Header(文件头)

固定长度 208 字节(0x00D0),位于文件偏移 0x0000,packed。

修订说明:本节原文写作「128 字节」,与 3.2 的字段偏移表矛盾——表中 reserved[32] 位于 0xB0,加 32 字节后头部实际结束于 0xD0;6.3 的 摘要规则(清零 0x50..0xB0)也只在 208 字节布局下成立。现按偏移表 统一为 208 字节,3.2 字段定义与 3.3 类型编号均未改动。

3.1 结构体定义

#define NKM_MAGIC 0x4E4B4D21   // "NKM!"

typedef struct __attribute__((packed)) {
    uint8_t  magic[4];           // 0x00  "NKM!" = 4E 4B 4D 21
    uint16_t abi_version;        // 0x04  bit[0..7]=minor, bit[8..15]=major
    uint16_t header_size;        // 0x06  = 208 (0xD0),便于扩展
    uint64_t min_kernel_ver;     // 0x08  最低兼容内核版本
    uint32_t module_type;        // 0x10  模块类型(驱动/FS/兼容层...)
    uint16_t seg_count;           // 0x14  段数量
    uint16_t dep_count;           // 0x16  依赖模块数量
    uint32_t symtab_count;        // 0x18  符号数量
    uint32_t rel_count;           // 0x1C  重定位数量
    uint32_t strtab_offset;       // 0x20  字符串表偏移
    uint32_t rel_offset;          // 0x24  重定位表偏移
    uint32_t init_offset;         // 0x28  nkm_init() 在 .text 内偏移
    uint32_t exit_offset;         // 0x2C  nkm_exit() 在 .text 内偏移
    uint8_t  module_name[32];     // 0x30  模块名('\0' 结尾)
    uint8_t  module_hash[32];     // 0x50  模块本体 SHA256(签名校验字段)
    uint8_t  module_sig[64];     // 0x70  Ed25519 签名
    uint8_t  reserved[0x20];      // 0xB0  预留 32 字节
} nkm_hdr_t;                        // 总大小 208 字节 (0xD0)

3.2 字段逐字节定义

偏移 大小 字段 取值规则
0x00 4 magic 固定 NKM!,不匹配拒绝加载
0x04 2 abi_version 高字节=major,低字节=minor。当前 0x0100
0x06 2 header_size 固定 208(0xD0),未来扩展用
0x08 8 min_kernel_ver 加载时校验 内核版本 >= min_kernel_ver
0x10 4 module_type 见 §3.3
0x14 2 seg_count 段数,1~16
0x16 2 dep_count 依赖模块数,0=无依赖
0x18 4 symtab_count 符号数量
0x1C 4 rel_count 重定位数量
0x20 4 strtab_offset 字符串表文件偏移
0x24 4 rel_offset 重定位表文件偏移
0x28 4 init_offset nkm_init() 在 .text 段内字节偏移
0x2C 4 exit_offset nkm_exit() 在 .text 段内字节偏移
0x30 32 module_name 模块名,\0 结尾,最长 31 字符
0x50 32 module_hash 模块本体 SHA256(见 §6)
0x70 64 module_sig Ed25519 签名(见 §6)
0xB0 32 reserved 必须全 0

3.3 module_type 取值

值 名称 说明
0 NKM_TYPE_DRIVER 硬件驱动(VGA/DP/存储/PCI)
1 NKM_TYPE_FS 文件系统驱动
2 NKM_TYPE_COMPAT 兼容层(Win32)
3 NKM_TYPE_CRYPTO 加密模块
4 NKM_TYPE_OTHER 其他扩展

4. 模块依赖表

紧接 Header 之后,共 dep_count 条,每条 16 字节。

typedef struct __attribute__((packed)) {
    uint32_t dep_name_off;      // 0x00  依赖模块名在 String Table 的偏移
    uint64_t min_dep_ver;       // 0x04  依赖模块最低版本
    uint32_t reserved;           // 0x0C  必须为 0
} nkm_dep_t;                     // 16 字节

加载规则:

  1. 加载器按依赖表顺序,先加载被依赖的模块;
  2. 依赖模块未加载 → 自动递归加载;
  3. 依赖模块版本不满足 min_dep_ver → 拒绝加载本模块;
  4. 依赖表出现循环依赖 → 拒绝加载,返回 NOVUS_EDEP。

5. 段表 / 符号表 / 重定位表

格式与 NUC 完全一致(见《NUC 二进制格式规范》第 4/5/7 节),差异点:

项 NKM 差异
段 vaddr 加载到内核地址空间(高地址区,非用户区)
段权限 全部 Ring0,不要求 VM_USER;TEXT 可执行,DATA 可写
外部符号 解析到内核导出符号表(nksym),而非系统 SDK
重定位类型 仅 R_X86_64_64(64 位绝对地址),内核模块不使用 PC32

内核导出符号示例(nksym 表,由内核 vmlinux 提供):

mm_alloc, mm_free, vm_map, vm_unmap
sched_spawn, sched_exit
vfs_register_fs, vfs_read, vfs_write
hal_display_probe, hal_display_flush
cap_check, log_print, err_report
pci_enumerate, blk_read, blk_write

6. Ed25519 签名与哈希(核心,已冻结)

6.1 密码套件

  • 哈希:SHA256
  • 签名:Ed25519(RFC 8032)
  • 公钥:32 字节
  • 签名:64 字节

6.2 内核公钥池

// 编译时固化在内核镜像中,运行时不可写、不可改
static const ed25519_pubkey_t kernel_trusted_keys[4] = {
    /* 公钥1 */, /* 公钥2 */, /* 公钥3 */, /* 公钥4 */
};
static const uint8_t trusted_key_count = 1; // 初始仅1把,后续版本追加
  • 最多支持 4 把公钥;
  • 仅编译时可更新(密钥轮换 = 发新版内核);
  • 运行时不接受用户导入公钥;
  • 验证时轮询所有公钥,任一通过即合法。

6.3 哈希计算范围(已修复冲突,冻结)

关键规则:SHA256 计算范围 = 整个模块文件,但跳过 module_hash[32] 和 module_sig[64] 两个字段。

具体操作(签名工具与加载器必须一致):

1. 拷贝整个模块文件到内存 temp_buf
2. 将 temp_buf 中偏移 0x50 起共 96 字节(32+64)置 0
   —— 即把 module_hash 和 module_sig 两个字段清零
3. computed_hash = SHA256(temp_buf, 文件总长度)
4. 比对 computed_hash 与头部原始 module_hash:
     不一致 → 哈希校验失败,拒绝加载

6.4 签名验证流程

1. 计算出 computed_hash(同 §6.3)
2. 比对 computed_hash == nkm_hdr.module_hash
3. 取头部 module_sig(64 字节)作为 Ed25519 签名
4. 取 nkm_hdr.module_hash(32 字节)作为签名原文
5. 遍历 kernel_trusted_keys[]:
     if ed25519_verify(module_hash, module_sig, pubkey[i]):
         → 校验通过,跳出
6. 全部公钥校验失败 → 拒绝加载

6.5 为什么跳过 hash+sig 字段

如果哈希包含自身签名,会造成"鸡生蛋"问题:签名依赖哈希,哈希依赖签名字节。 置零后计算哈希,使哈希与签名解耦:哈希覆盖"除自描述字段外的全部内容",签名再对这个哈希做签名。


7. 模块入口函数约定

每个 NKM 模块必须导出两个函数,通过 init_offset / exit_offset 指向:

// 模块初始化(加载时调用一次)
// 返回 0 成功,负数错误码
int nkm_init(void);

// 模块卸载(卸载时调用一次)
// 返回 0 成功
int nkm_exit(void);

调用约定:

  • 与系统调用 ABI 一致(x86_64 SysV);
  • nkm_init 返回非 0 → 加载失败,已分配资源自动回滚;
  • nkm_exit 必须释放所有申请的内核资源(内存、IRQ、设备注册),否则卸载后系统状态不一致。

8. 加载完整流程(冻结)

1. nkm_load(path) 被调用
2. 校验调用者 cap_check(mod.load),无权限 → NOVUS_EPERM
3. VFS 打开文件,读取前 208 字节(Header)
4. 校验 magic == "NKM!"
5. 校验 abi_version 主版本号
6. 校验 min_kernel_ver <= 当前内核版本
7. 拷贝整个文件到内核内存
8. 执行哈希校验:
   a. 临时副本中 0x50..0xB0 置 0
   b. 计算 SHA256
   c. 与 module_hash 比对
   失败 → NOVUS_ECORRUPT
9. 执行 Ed25519 签名校验(轮询公钥池)
   失败 → NOVUS_EMODULE
   (管理员单次临时放行:跳过第 9 步,打警告日志)
10. 按依赖表递归加载依赖模块
11. 映射段到内核地址空间
12. 解析重定位(外部符号查内核 nksym 表)
13. 调用 nkm_init()
14. init 返回 0 → 注册到模块列表,返回成功
15. init 失败 → 回滚段映射,返回错误

9. 卸载流程

1. nkm_unload(name) 被调用
2. 校验 cap_check(mod.unload)
3. 检查模块是否被其他模块依赖(dep_count 反向引用)
   被依赖 → 拒绝卸载,NOVUS_EBUSY
4. 调用 nkm_exit()
5. 解除段映射
6. 从模块列表移除
7. 释放模块内存

10. 管理员临时放行策略

  • 普通用户:未签名模块一律拒绝加载;
  • 管理员(拥有 mod.load CAP):可加载未签名模块,但:
    • 必须在加载时显式传入 NKM_FLAG_ALLOW_UNSIGNED;
    • 内核打印 WARNING 日志,记录模块名、PID、时间;
    • 仅本次会话有效,不持久保存信任;
    • 重启后该模块不会自动加载。

11. 错误码

错误码 含义
NOVUS_OK (0) 加载成功
NOVUS_EPERM (-4) 无 mod.load / mod.unload CAP
NOVUS_EINVAL (-1) 头部字段非法
NOVUS_ECORRUPT (-7) SHA256 哈希不匹配
NOVUS_EMODULE (-8) Ed25519 签名验证失败
NOVUS_EBUSY (-5) 模块被依赖,无法卸载
NOVUS_ENOTFOUND (-3) 依赖模块缺失
NOVUS_EDEP (-12) 循环依赖
NOVUS_EIO (-6) 文件读取失败
NOVUS_ENOMEM (-2) 内核内存不足

12. 完整示例:一个最小驱动模块

12.1 Header 关键字段

magic:          4E 4B 4D 21  ("NKM!")
abi_version:    00 01        (major=1, minor=0)
header_size:    D0 00        (208)
min_kernel_ver: 00 00 00 00 00 00 00 01   (要求内核 >= 1.0)
module_type:    00 00 00 00  (DRIVER)
seg_count:      02 00         (2段)
dep_count:      00 00         (无依赖)
symtab_count:   01 00 00 00
rel_count:      02 00 00 00
strtab_offset:  00 02 00 00   (0x200)
rel_offset:     C0 01 00 00   (0x1C0)
init_offset:    10 00 00 00   (.text+0x10)
exit_offset:    30 00 00 00   (.text+0x30)
module_name:    "nvme.ko" 00 00...
module_hash:    <32字节SHA256>
module_sig:     <64字节Ed25519签名>

12.2 模块源码骨架(C)

#include <novus/nkm.h>
#include <novus/mm.h>
#include <novus/log.h>

static void* buf = NULL;

int nkm_init(void)
{
    log_print(LOG_INFO, "nvme driver init\n");
    buf = mm_alloc(1);
    if (!buf) return NOVUS_ENOMEM;
    // 注册 NVMe 控制器
    return NOVUS_OK;
}

int nkm_exit(void)
{
    log_print(LOG_INFO, "nvme driver exit\n");
    if (buf) mm_free(buf, 1);
    return NOVUS_OK;
}

NUC 与 NKM 是不同格式:cppcc 只生成用户态 .nuc,不参与 NKM 目标文件编译或签名。 当前仓库的 tools/nkmgen 是主机端 NKM 测试镜像生成器,复用内核 Ed25519/SHA 实现生成并签名测试模块;它不是通用 C/C++ NKM 编译器。通用驱动目标文件到 NKM 的生产工具链不属于 cppcc。


13. 文档冻结声明

本规范为 NKM 内核模块格式唯一基准文档。约束:

  • Header 208 字节布局(0x00D0),含 reserved[32];
  • 哈希跳过 0x50..0xB0(hash+sig 字段)后计算 SHA256;
  • Ed25519 签名原文 = module_hash(32 字节);
  • 内核公钥池最多 4 把,编译时固化;
  • 入口固定 nkm_init / nkm_exit;
  • 依赖表递归加载,禁止循环依赖。

任何变更必须走 ABI 变更 RFC,升版本号。

文档状态: 可作为 M3(NKM 加载器)与 M10(驱动生态)的实现基准。