news 2026/7/7 2:04:48

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
鸿蒙 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读的:

structElfSignInfo{uint32_ttype;// = 1, FsVerityDescriptor::FS_VERITY_DESCRIPTOR_TYPEuint32_tlength;// payload 总长 = descriptor(256) + signature.size();不含本头部 8 字节uint8_tversion;// = 1, FsVerityDescriptor::VERSIONuint8_thashAlgorithm;// = 1, SHA-256uint8_tlogBlockSize;// = 12, 即 2^12 = 4096uint8_tsaltSize;// = 0 (当前实现里 salt 为空)uint32_tsignSize;// = signature 字段的字节数uint64_tdataSize;// = 含 .codesign 段的最终产物总大小 (对应 descriptor fileSize 字段)uint8_trootHash[64];// merkle 树根哈希,左对齐填到 64 字节,余部为 0uint8_tsalt[32];// salt,左对齐填到 32 字节uint32_tflags;// 自签名时第 4 位 (0x10) 必须被置 1uint8_treserved_1[12];uint8_treserved_2[127];uint8_tcsVersion;// = 3, ELF_CODE_SIGN_VERSIONuint8_tsignature[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是直接memcpyhost 内存值,因此在 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),字段顺序与偏移如下(全部小端):

偏移字段类型字节数取值(自签名)
0versionuint811
1hashAlgorithmuint811 (SHA-256)
2log2BlockSizeuint8112
3saltSizeuint810
4signSizeuint3240(生成摘要时)/ 32(落盘时)
8fileSizeuint648含 .codesign 段的最终产物总大小
16rawRootHashbytes64merkle 树根哈希,左对齐,余 0
80saltbytes32全 0
112flagsuint3240x10 (FLAG_SELF_SIGN)
116reserved_1bytes40
120merkleTreeOffsetuint6480 (当前实现未启用,FLAG_STORE_MERKLE_TREE_OFFSET 未置位)
128reserved_2bytes1270
255csVersionuint813

合计 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 互相交叉验证的用法:

./self-sign a.out a.c_sign python3 self-sign.py a.out a.py_signcmpa.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 上内核日志的采集命令是:

hilog-tkmsg

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

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

# 用自己的进程名称过滤(推荐,最精准)hilog-tkmsg|grep"postgres"# 不知道进程名时,用验签关键字兜底过滤hilog-tkmsg|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 dmveritypostgres 启动后 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: 1xpm 取签名信息失败,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实际是内核侧连续两次拒绝的结果。

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

SIEMENS 07562981 印刷电路板

SIEMENS 07562981 印刷电路板是西门子医疗设备专用的控制板组件&#xff0c;其核心特点如下。中间15条&#xff1a;SIEMENS 07562981 专为西门子MRI系统配套设计。承担服务控制与信号管理功能。采用西门子医疗设备制造标准。适用于西门子07564060等型号设备。采用工业级元器件&…

作者头像 李华
网站建设 2026/7/7 2:04:29

免费 vs 付费 AI 编程工具:差距到底有多大?实测数据告诉你真相

调研背景&#xff1a;我们团队花了 2 周时间&#xff0c;对市面上主流的 12 款 AI 编程工具进行了深度测评&#xff0c;覆盖免费和付费版本&#xff0c;收集了超过 500 组对比数据。引言&#xff1a;选择困难症的时代 2026 年&#xff0c;AI 编程工具已经百花齐放&#xff1a; …

作者头像 李华
网站建设 2026/7/7 2:01:12

Agent 工程化新底座:用 CLI 契约层打通 HTTP 接口与业务能力

变化分层&#xff1a;Agent 基建应该压在哪一层如果把 AI 应用的技术栈按变化速度拆开&#xff0c;会发现它不是一条直线&#xff0c;而是一组稳定性差异很大的层。图&#xff1a;越往底层越适合承载长期业务契约。模型和 Agent 框架适合快速试错&#xff0c;不适合作为业务基础…

作者头像 李华
网站建设 2026/7/7 1:50:53

Jupyter Notebook 环境搭建与使用指南

1. 创建 Conda 环境 首先创建一个名为 jupyter 的 Conda 环境&#xff0c;指定 Python 版本为 3.11&#xff1a; conda create -n jupyter python3.11输入 y 并回车&#xff0c;确认安装依赖包。 出现以下界面表示环境创建成功&#xff1a; 2. 安装 Jupyter Notebook/Lab 在…

作者头像 李华
网站建设 2026/7/7 1:50:43

Access Advance 欢迎 Samsung 和 Sharp 加入 VVC Advance 专利池

Access Advance LLC 今天宣布VVC Advance 专利池新增重要成员&#xff0c;包括 Samsung Electronics 和 Sharp Corporation 作为许可方和被许可方加入。新成员的加入强化了Access Advance在VVC许可中的地位&#xff0c;将全球两家最大的视频编解码专利权人纳入该计划中。Access…

作者头像 李华
网站建设 2026/7/7 1:49:00

AI 电动工具智能高效 高可靠性及低压驱动兼容 核心选型方案

随着 AI 技术在电动工具中的应用&#xff08;如智能扭矩调节、无感启动、电池保护及预测性维护&#xff09;&#xff0c;对功率 MOSFET 提出了更高要求&#xff1a;高效率、高功率密度、高可靠性及低压驱动兼容。微碧半导体&#xff08;VBsemi&#xff09;基于先进 Trench 及 S…

作者头像 李华