鸿蒙 PC 底层开发技术详解(七):二进制自签名算法的实现

提示:本文创作过程中大量使用 AI 辅助研究,作者已验证代码结果的正确性,但无法保证所有结论一定严谨,切勿将文中结论用于严谨学术用途。

1 前言

本文是《鸿蒙 PC 底层开发技术详解(四):代码签名机制对我们的影响》的续写。

前文讲清楚了鸿蒙 PC HiShell 上"为什么要签名、用什么工具签名"------结论是:日常开发里绝大多数时候我们做的都是自签名,要么用 binary-sign-tool sign -selfSign 1,要么用 lld 的 --code-sign。讲解内容仅止步于"工具能签、产物能跑"这一层,并未深入细节。

本文将基于对 binary-sign-tool 开源代码的分析结果,进一步深入讲解代码签名的算法及其生成的数据结构,并使用 C 和 Python 两种语言各自实现一个代码签名工具。

本文所涉及的源码可在以下仓库找到:

  • ohos-bst-portable ------ 将 binary-sign-tool 从 OpenHarmony 项目里便携剥离出来,能够在脱离 GN 构建框架的情况下编出与 ohos-sdk 一致的 binary-sign-tool 工具。此项目可以让开发者排除无关组件干扰、专注研究 binary-sign-tool 的代码逻辑。
  • ohos-bst-light ------ binary-sign-tool 轻量重写版 ------ 基于官方源码逆向出 OpenHarmony 二进制自签名算法后,用 C 语言和 Python 各重写一份实现。

2 前文回顾

在前文中,我们用 llvm-readelf -S my_program 查看签名后的 ELF 文件,发现其末尾多了一个名为 .codesign 的段。

段内放的是什么内容呢?放的就是本文要讲的主角:ElfSignInfo

3 ElfSignInfo:段内 payload 的整体布局

一个自签名后的 ELF 文件,其末尾 .codesign 段的字节布局如下(从该段的起始偏移开始):

复制代码
.codesign section (size = 固定 4096 字节, 即 1 × 4KB)
┌──────────────────────────────────────────────────────────────┐
│  ElfSignInfo 头部  (type + length, 固定 8 字节)               │
├──────────────────────────────────────────────────────────────┤
│  ElfSignInfo 主体 = fs-verity descriptor (固定 256 字节)      │
├──────────────────────────────────────────────────────────────┤
│  signature    (自签名时 = 32 字节;证书签名时 = PKCS7 长度)    │
├──────────────────────────────────────────────────────────────┤
│  merkle tree bytes (段内 296B 之后直到段末,自签名下验签侧     │
│  允许全 0;但上游工具仍按实算结果写入,本实现亦写入实际内容)   │
└──────────────────────────────────────────────────────────────┘

段内 payload 部分合计:头部 8B + descriptor 256B + signature 32B = 296B(证书签名时 signature 长度不同,总计随之变化)。段总大小恒为 4096B------payload 仅 296B,余 ~3800B 即 merkle tree bytes 区,自签名下验签侧允许全 0,但上游工具与本实现都按实算结果写入。

ElfSignInfo 在上游 binary_sign_tool/hap/verify/include/verify_elf.h 里是一个 C 结构体,验签端就是按这个结构体直接 reinterpret_cast 读的:

c 复制代码
struct ElfSignInfo {
    uint32_t type;            // = 1, FsVerityDescriptor::FS_VERITY_DESCRIPTOR_TYPE
    uint32_t length;          // payload 总长 = descriptor(256) + signature.size();不含本头部 8 字节
    uint8_t  version;         // = 1, FsVerityDescriptor::VERSION
    uint8_t  hashAlgorithm;   // = 1, SHA-256
    uint8_t  logBlockSize;   // = 12, 即 2^12 = 4096
    uint8_t  saltSize;        // = 0 (当前实现里 salt 为空)
    uint32_t signSize;        // = signature 字段的字节数
    uint64_t dataSize;        // = 含 .codesign 段的最终产物总大小 (对应 descriptor fileSize 字段)
    uint8_t  rootHash[64];    // merkle 树根哈希,左对齐填到 64 字节,余部为 0
    uint8_t  salt[32];        // salt,左对齐填到 32 字节
    uint32_t flags;           // 自签名时第 4 位 (0x10) 必须被置 1
    uint8_t  reserved_1[12];
    uint8_t  reserved_2[127];
    uint8_t  csVersion;       // = 3, ELF_CODE_SIGN_VERSION
    uint8_t  signature[0];    // 柔性数组,紧跟着的就是 signature 字段
};
// 上述结构体 sizeof = 4+4 + 1+1+1+1 + 4 + 8 + 64+32 + 4 + 12+127+1 = 264 字节
//   (不含柔性数组 signature[0];验签端读完 264B 主体后,再按 length-256 读 signature)

这里有几个反直觉但必须讲清楚的点:

  • 头部的 length 字段到底量的是什么

    length 指的是 紧跟在头部 8 字节之后的 payload 总长 (descriptor + signature),而 不包括 头部自身的 type/length 这 8 字节。自签名下 length = 256 + 32 = 288。整块 ElfSignInfo(含头部)的字节总长是 8 + length = 296

    这一处很容易踩坑,因为字段叫 length、又紧挨在 type 后面,望文生义会以为它量的是"从 type 开始的总长"。但上游 code_signing.cpp 落盘时写的是 descriptor.size() + signature.size(),不含头部------所以本文和配套实现都按 288 写。

  • flags 字段:自签名的"身份证"

    复制代码
    FLAG_SELF_SIGN = 1 << 4 = 0x10

    自签名时 flags = 0x10。这一位是整个机制里最关键的开关:验签端 verify_elf.cpp 看到 (flags & 0x10) != 0 就直接判定为自签名、跳过 PKCS7 验证------自签名没有任何证书链可验,系统侧的实现实际是把"是否自签名"这一位作为白名单条件之一,由系统策略决定是否放行。

    也就是说:只要产物里 flags 的第 4 位是 1,工具链侧就认它是自签名。本文要讲的"没有私钥也能过验签",根因就在这一位上------它让系统跳过那个需要私钥才能完成的步骤。

  • csVersion 字段

    签名端写 ELF_CODE_SIGN_VERSION = 0x3。验签端目前没强校验这个值,但本算法保持写入 3

  • 字节序

    上游 ByteBuffer::PutInt32/PutInt64 是直接 memcpy host 内存值,因此在 aarch64/x86_64 Linux 上均为 小端(little-endian) 。验签端 reinterpret_cast 也是 host 序读,两端自洽。本文的实现也一律采用小端。

4 signature 字段在自签名下的含义

讲清这一节,本文就完成了一大半。

签名类型 signature 字段的内容 来源
证书签名 对 fs-verity descriptor 摘要做的 PKCS7 签名 (含证书链) GenerateSignature()BCSignedDataGenerator
自签名 fs-verity descriptor 自身的 SHA-256 摘要 (32 字节裸摘要,无任何 magic header) fsVerityGenerator->GetDescriptorDigest()

也就是说,自签名的"签名"就是被签名对象(descriptor)本身的 SHA-256 。这根本不是任何意义上的数字签名------它不抵抗篡改、不证明身份、不蕴含任何私钥。它只是把 SHA256(descriptor_with_signSize=0) 填到 descriptor 的 signature 字段,并把同一份 descriptor 的 signSize 字段从 0 改成 32 后再落盘。

如果你第一次看到这个机制觉得"这也算签名?"------反应是对的,它确实不是签名,它是个自洽占位 :让那些原本按"signature 字段必然存在"写死的字节布局能闭合一整套结构,同时用 flags 那一位告诉验签端"别去找证书链了,这块放过去"。

验签时若要核对,逻辑只需四步:

  1. 把读出来的 ElfSignInfo 里的 descriptor 部分(去掉头部 8 字节的 type/length)取出;
  2. 把其中的 signSize 字段临时改回 0,把 signature 字段当作不存在;
  3. 对这 256 字节做 SHA-256;
  4. 与 ElfSignInfo 后面紧跟着的 signature 字段比对,相等则通过。

这里有个非常 subtle 的点:"用来算摘要的 descriptor" 和"最终落盘的 descriptor"是同一份结构、但 signSize 字段不同。下一节就把这个差异摊开讲。

5 fs-verity descriptor 的 256 字节布局

descriptor 是一个定长 256 字节结构(FsVerityDescriptor::DESCRIPTOR_SIZE = 256),字段顺序与偏移如下(全部小端):

偏移 字段 类型 字节数 取值(自签名)
0 version uint8 1 1
1 hashAlgorithm uint8 1 1 (SHA-256)
2 log2BlockSize uint8 1 12
3 saltSize uint8 1 0
4 signSize uint32 4 0(生成摘要时)/ 32(落盘时)
8 fileSize uint64 8 含 .codesign 段的最终产物总大小
16 rawRootHash bytes 64 merkle 树根哈希,左对齐,余 0
80 salt bytes 32 全 0
112 flags uint32 4 0x10 (FLAG_SELF_SIGN)
116 reserved_1 bytes 4 0
120 merkleTreeOffset uint64 8 0 (当前实现未启用,FLAG_STORE_MERKLE_TREE_OFFSET 未置位)
128 reserved_2 bytes 127 0
255 csVersion uint8 1 3

合计 256 字节。这个布局里的"灵魂"是 signSize 字段,它在两个时刻取值不同:

  • 生成摘要时 (对应上游 GetByteForGenerateDigest)的 signSize0。这是为了让摘要可复算------摘要计算发生在 signature 还没诞生之前,signature 长度自然为 0。
  • 落盘时 (对应上游 ToByteArray)的 signSizesignature.size()(自签名下 = 32)。

这一改动是唯一让"落盘的 256 字节"与"被摘要的 256 字节"不同的地方。验签端重算摘要时要把 signSize 改回 0、把 signature 当不存在,才能算出和签名端一致的摘要------这也是上一节验签步骤 2 的依据。

descriptor 里其余字段的取值,自签名下都是固定的(如上表"取值"列):version=1hashAlgorithm=1log2BlockSize=12salt 全 0、csVersion=3。唯一需要在签名时现场算出来的,是 fileSize(含段产物总大小)和 rawRootHash(merkle 根哈希)。fileSize 好理解,根哈希怎么来------下一节。

6 merkle 树怎么构造:fs-verity 的跳段语义

descriptor 里要填的 rawRootHash 来自一棵 merkle 树,构造规则:

  1. 分页 :先注入 4KB 占位 .codesign 段得含段产物 tmp,对 tmp原偏移 分页(每页 4096B,末页补 0)。段所落在的那几页(csOffset/4096 .. ⌈(csOffset+段长)/4096⌉)的叶哈希全填 0;其余页正常 SHA-256。
  2. 叶层哈希 :对每一页做 SHA-256,得到 32 字节叶哈希,按页顺序排列。
  3. 逐层向上:把当前层每 4096 字节(即 4096/32 = 128 个哈希)当作一页,再做 SHA-256,得到上一层。最后会聚到一个 ≤ 4096 字节的根页。
  4. 根哈希 :若原始数据 < 4096 字节(只有一页),根哈希直接就是这页的哈希;否则,对最顶层的 4096 字节页再做一次 SHA-256,得到的 32 字节就是 rootHash
  5. merkle tree bytes :所有非叶层(即除最叶层外自顶向下各层)的哈希字节流会被顺次拼成 tree 字段,紧跟在 ElfSignInfo+signature 之后写入 .codesign 段的剩余空间。自签名下系统验签侧若不做 merkle 验证,这部分理论上可全填 0;但上游工具是老老实实按实算结果写进去的,本仓库两份实现亦按同一规则算出并写入实际 merkle tree bytes(实测段内 296B 之后确实非全 0)。

这里最坑、最容易写错的是第 1 步的 "跳段置 0" 语义 。直觉上你会以为:"既然段区间是要被覆盖写 payload 的,那算 merkle 时把段剥掉、对剥掉后的原文分页不就行了?"------不行 。上游真实做法(merkle_tree_builder.cpp::RunHashTask)不是"剥掉段区间再分页",而是:

  1. 先注入 4KB 占位 .codesign 段得含段产物 tmpcsOffset = 段在 tmp 中的文件偏移(4KB 对齐)。
  2. tmp原偏移 分页(每页 4096B,末页补 0),段所落在的那几页csOffset/4096 .. ⌈(csOffset+段长)/4096⌉)的叶哈希 全填 0(不做 SHA-256,直接置 0),其余页正常 SHA-256。
  3. 上推逐层照常。

区别看着小,但对得上对不上差很远:用"剥段后分页"算法算出的根哈希,喂给本仓库实现,与上游产物 descriptor 里写的根哈希 不一致 ;用"原偏移分页 + 段所在页置 0"算法算出的根哈希,与上游 一致 。本仓库两份实现的 merkle_root_hash 即采用后者。

fileSize 字段填的是 含段最终产物的总大小 (不是剥段后原文大小),因为上游 code_signing.cpp 的输入 inputstream 就是已含占位段的临时产物,fileSize = tellg() 读的是含段产物大小。

7 把整个流程串起来:完整算法伪代码

把上面几节拼到一起,整个自签名流程是这样的:

复制代码
# 输入:  inPath   = 待签名的 ELF 文件路径 (bin 或 .so)
# 输出:  outPath  = 签名后的产物文件路径 (通常与 inPath 相同)

function self_sign(inPath, outPath):
    raw = read_file(inPath)                      # 原始 ELF 字节

    # 1. 注入 4KB 占位 .codesign 段 → 含段产物 tmp, 段文件偏移 csOff (4KB 对齐)
    tmp, csOff = inject_codesign_section(raw, page=4096)
    fileSize = len(tmp)                          # fileSize 字段 = 含段产物总大小

    # 2. merkle 根哈希: 对 tmp 按原偏移分页, 段所在页叶哈希置0, 其余页正常 SHA-256
    rootHash = merkle_root_hash(tmp, csOff, csLen=4096, page=4096, algo=SHA256)
    # 3. flags置自签名位
    flags = 0x10
    # 4. 构造 descriptor 的 256 字节, 注意 signSize 此刻 = 0, fileSize = fileSize
    desc_for_digest = build_descriptor(
        version=1, hashAlgo=1, log2BlockSize=12, saltSize=0,
        signSize=0, fileSize=fileSize,
        rootHash=rootHash, salt=zeros(32),
        flags=flags, merkleTreeOffset=0, csVersion=3)
    # 5. signature = SHA256(descriptor_with_signSize_0)
    signature = SHA256(desc_for_digest)          # 32 字节裸摘要
    # 6. 重新构造落盘用的 descriptor, signSize = 32
    desc_on_disk = build_descriptor(
        ...同上..., signSize=32)
    # 7. merkle tree bytes (自签名验签侧不查; 实现里段内296B之后填全0即可)
    # 8. 拼 ElfSignInfo 头部 8 字节
    type  = 1
    length = 256 + len(signature)                # = 288 (不含头部8字节 type+length)
    head = pack_u32_le(type) + pack_u32_le(length)
    payload = head + desc_on_disk + signature    # = 296 字节
    # 9. 原地改写段内字节: tmp[csOff : csOff+len(payload)] = payload
    #    段已占 4KB, payload 仅 296B, 余部保持段占位0
    tmp[csOff:csOff+len(payload)] = payload
    # 10. 落盘
    write_file(outPath, tmp)

function merkle_root_hash(data, csOff, csLen, page, algo):
    """段所在页叶哈希全置0, 其余页正常SHA-256, 上推逐层照常."""
    npages = ceil(len(data) / page)
    hashes = []
    cs_page_begin = csOff // page
    cs_page_end   = (csOff + csLen + page - 1) // page
    for i in range(npages):
        if csLen > 0 and cs_page_begin <= i < cs_page_end:
            hashes.append(zeros(32))            # 段所在页: 叶哈希置0
            continue
        page_bytes = data[i*page : (i+1)*page]
        if len(page_bytes) < page:
            page_bytes += zeros(page - len(page_bytes))   # 末页补0
        hashes.append(SHA256(page_bytes))
    if len(hashes) == 1:
        return hashes[0]                        # 单页即根
    while len(hashes) > 1:
        packed = b''.join(hashes)
        if len(packed) <= page:
            return SHA256(packed + zeros(page - len(packed)))
        nextPageHashes = []
        for i in range(0, len(packed), page):
            chunk = packed[i:i+page]
            if len(chunk) < page:
                chunk = chunk + zeros(page - len(chunk))
            nextPageHashes.append(SHA256(chunk))
        hashes = nextPageHashes
    return hashes[0]

8 完整代码实现

我在 ohos-bst-light 仓库里提供了两份独立实现:

  • self-sign.c ------ C99 实现,自带 SHA-256(按 FIPS 180-4 公开规范自实现,不引自任何第三方代码),零第三方依赖。
  • self-sign.py ------ Python 3 实现,仅用标准库 hashlib,与 C 版产物整文件字节级一致。

两份实现都只依赖 SHA-256(C 版自实现,Python 版用标准库),不依赖 openssl / cJSON / elfio。

C/Python 互相交叉验证的用法:

sh 复制代码
./self-sign a.out a.c_sign
python3 self-sign.py a.out a.py_sign
cmp a.c_sign a.py_sign && echo "C 与 Python 产物整文件字节级一致"

功能约束:待签 ELF 的段布局须合规。如果用户不使用 ohos-sdk 而是使用其他非标工具链为鸿蒙系统编译程序,这两个签名工具可能会签名失败,该问题上游 binary-sign-tool 中同样存在。

9 验签失败时怎么排查

到此为止讲的都是"产物正常、跑得起来"的路径。但实务里更常踩的是反过来的坑:签名跑完了、产物落盘了,一执行终端就报 permission denied------这时到底签坏了哪一步、是哪个文件没过验签,光看终端这一行报错是判断不出来的。本节给一套可操作的排查手段。

HiShell 上验签失败时,用户态看到的就是一行 permission denied。单看这一行,完全无法分辨是验签失败了、还是缺乏 selinux 权限、或者是别的什么原因。要定位到具体是哪个文件、哪一步出问题,得去看 内核日志

内核日志里会有验签侧各组件(xpm / fs_security_verity 等)落下来的事件,明确写出"哪个 pid、哪个文件、为什么没过验签"。这是排查验签问题最硬的佐证。

HiShell 上内核日志的采集命令是:

sh 复制代码
hilog -t kmsg

-t kmsg 指定从内核环形缓冲(/dev/kmsg)取日志,而不是用户态日志。验签事件都是内核侧落的,必须带这个参数才看得到。

实际排查时一般会接一条 grep 把范围收窄到自己关心的进程:

sh 复制代码
# 用自己的进程名称过滤(推荐,最精准)
hilog -t kmsg | grep "postgres"

# 不知道进程名时,用验签关键字兜底过滤
hilog -t kmsg | grep -E "xpm|unsigned file"

三个常用过滤关键字:

  • 进程名称 ------ 最精准,只看自己进程触发的验签事件。
  • xpm ------ 验签主组件(eXecution Permission Manager)的事件标签,所有验签决策都会落一条 [xpm:...]
  • unsigned file ------ 验签失败的具体事件类型,凡是"未签名/签名损坏"的文件被加载时都会落一条 event_type: "unsigned file"

下面是 postgres 进程在 HiShell 上因依赖的 .so 未签名被拦截时,hilog -t kmsg | grep "postgres" 的真实输出:

复制代码
05-26 15:18:37 <3> [1897.008303]  -;[12] pid=35029 tid=35029 comm=zsh             [xpm:1010]postgres is not protected by dmverity, devid=28
05-26 15:18:37 <3> [1897.028672]  -;[11] pid=35029 tid=35029 comm=postgres        [xpm:1010]libicudata.so.78.3 is not protected by dmverity, devid=272629862
05-26 15:18:37 <3> [1897.080574]  -;[12] pid=35029 tid=35029 comm=postgres        [HKES:87][hkes][E]: event denied by client
05-26 15:18:37 <1> [1897.080589]  -;[12] pid=35029 tid=35029 comm=postgres        [fs_security_verity:74][hkes][I]: lib_no_signed event waken: -9(E_HM_PERM)
05-26 15:18:37 <3> [1897.080598]  -;[12] pid=35029 tid=35029 comm=postgres        [xpm:1671]xpm get signature info failed, etype: 1
05-26 15:18:37 <3> [1897.080607]  -;[12] pid=35029 tid=35029 comm=postgres        [xpm:1010]libicudata.so.78.3 is not protected by dmverity, devid=272629862
05-26 15:18:37 <3> [1897.080620]  -;[12] pid=35029 tid=35029 comm=postgres        [xpm:771]{ "event_type": "unsigned file", "code_type": "ABC", "pid": 35029, "filename": "/service/el2/100/hmdfs/account/files/Docs/.harmonybrew/Cellar/icu4c@78/78.3/lib/libicudata.so.78.3", "vm_prot": 1, "vm_pgoff": 0, "vm_size": 4096, "p_id_type": 34, "p_ownerid": 0, "f_id_type": 0, "f_ownerid": 0, "ssid": 0, "tsid": 0, "verified_datasize": 0, "code_segment": NA, "etype": 1, "timestamp": 1779779917}
05-26 15:18:37 <3> [1897.083661]  -;[12] pid=35029 tid=35029 comm=postgres        [xpm:1010]libicudata.so.78.3 is not protected by dmverity, devid=272629862
05-26 15:18:38 <3> [1897.137194]  -;[4] pid=35029 tid=35029 comm=postgres        [HKES:87][hkes][E]: event denied by client
05-26 15:18:38 <1> [1897.137204]  -;[4] pid=35029 tid=35029 comm=postgres        [fs_security_verity:74][hkes][I]: lib_no_signed event waken: -9(E_HM_PERM)
05-26 15:18:38 <3> [1897.137211]  -;[4] pid=35029 tid=35029 comm=postgres        [xpm:1671]xpm get signature info failed, etype: 1
05-26 15:18:38 <3> [1897.137218]  -;[4] pid=35029 tid=35029 comm=postgres        [xpm:1010]libicudata.so.78.3 is not protected by dmverity, devid=272629862
05-26 15:18:38 <3> [1897.137227]  -;[4] pid=35029 tid=35029 comm=postgres        [xpm:771]{ "event_type": "unsigned file", "code_type": "ABC", "pid": 35029, "filename": "/service/el2/100/hmdfs/account/files/Docs/.harmonybrew/Cellar/icu4c@78/78.3/lib/libicudata.so.78.3", "vm_prot": 1, "vm_pgoff": 0, "vm_size": 4096, "p_id_type": 34, "p_ownerid": 0, "f_id_type": 0, "f_ownerid": 0, "ssid": 0, "tsid": 0, "verified_datasize": 0, "code_segment": NA, "etype": 1, "timestamp": 1779779918}
05-26 15:18:38 <1> [1897.143176]  -;[4] pid=35029 tid=35029 comm=postgres        [procmgr_core:1722]process start exit, pid=35029
05-26 15:18:38 <1> [1897.143242]  -;[4] pid=35029 tid=35029 comm=postgres        [threadmgr_thread:1374]last thread has exited in user space, nr_threads=1

逐行拆解这条日志能告诉我们什么:

日志片段 含义 排查价值
comm=zsh 行的 postgres is not protected by dmverity 父进程 zsh 试图 exec postgres 主程序,但 postgres 没被 dmverity 保护 主程序没签名 ------ 走的是本文流程的话,先查主程序有没有 .codesign
comm=postgres 行的 libicudata.so.78.3 is not protected by dmverity postgres 启动后 dlopen 加载 libicudata.so.78.3,但这个 .so 没被 dmverity 保护 依赖的 .so 没签名
[fs_security_verity:74] lib_no_signed event waken: -9(E_HM_PERM) 内核 fs_security_verity 组件判定"无签名",唤醒拒绝事件,错误码 E_HM_PERM (-9) 错误码 -9permission denied 的内核侧真身
[xpm:1671] xpm get signature info failed, etype: 1 xpm 取签名信息失败,etype: 1 指签名类型 签名信息读不到------要么没段、要么段坏了
[xpm:771] { "event_type": "unsigned file", ..., "filename": ".../libicudata.so.78.3", ..., "verified_datasize": 0 } xpm 落的结构化事件,写明未签名文件的全路径、verified_datasize=0(验签数据量为 0) 最有价值的一条 ------直接告诉你"哪个文件、什么事件、验了多少"。verified_datasize: 0 意味着系统根本没找到可验的签名数据
[procmgr_core:1722] process start exit, pid=35029 + [threadmgr_thread:1374] last thread has exited 进程启动失败退出、最后一根线程也走了 进程被拒后清理收尾

把这几条串起来看,结论一目了然:postgres 主程序本身没签名(zsh exec 时被拒),就算主程序签了,它 dlopen 的 libicudata.so.78.3 也没签名(postgres 启动后被拒) 。两道关都过不去,所以终端看到的 permission denied 实际是内核侧连续两次拒绝的结果。

相关推荐
AI科技星1 小时前
超复数全域经济周期场与信息谱场——金融与密码学底层理论重构《0·1·∞三元一体全域超复数统一场论》系列全集(六一字不漏完整合订终版)
人工智能·算法·金融·密码学·拓扑学·乖乖数学·全域数学
AI科技星1 小时前
《01无穷全域信息场论:算子G与宇宙本体高维完备公理大典》
人工智能·python·算法·金融·乖乖数学·全域数学
Tim_102 小时前
【C++】013、空类占多少字节?
算法
Ricky_Theseus2 小时前
并查集:连通性问题
算法
Tairitsu_H2 小时前
[LC优选算法#18] 前缀和 | 除⾃⾝以外数组的乘积 | 和为K的⼦数组 | 和可被K整除的⼦数组
算法·前缀和·哈希算法
心中有国也有家2 小时前
鸿蒙 Flutter 本地存储实战:Hive CE 从入门到精讲
人工智能·hive·flutter·华为·harmonyos
绝世番茄2 小时前
鸿蒙原生ArkTS布局方式之Grid+scrollBar滚动条布局深度解析
华为·harmonyos·鸿蒙
凯瑟琳.奥古斯特3 小时前
力扣1008:前序重建BST
开发语言·c++·算法·leetcode·职场和发展
qq_383189623 小时前
07-运放运用电路-程控增益放大器
算法