news 2026/10/2 1:08:41

华为iTrustee实战:5分钟搭建TrustZone可信执行环境

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
华为iTrustee实战:5分钟搭建TrustZone可信执行环境

如果你的工作涉及ARM TrustZone,那大概率听过华为iTrustee。我第一次接触它,是想在鲲鹏设备上做一个基于可信执行环境的密钥管理服务。原以为要把TEE内核、驱动、TA签名、CA调用整个链条从零捋一遍,结果发现iTrustee把大部分脏活都干完了。按我的实测,只要提前装好工具链、目标设备确认开启了TrustZone,从拿到SDK到第一个TA跑起来,5分钟真的够用。这篇文章就围绕这次实战,讲清楚TrustZone环境搭建的完整路径,以及BoostKit配置里几个容易被忽略的技巧。适合想快速上手TEE的嵌入式开发者、做机密计算的云原生工程师,还有准备在华为相关项目里落地可信能力的同学参考。

1. iTrustee把我从"移植TEE内核"的泥潭里拽了出来

在没有iTrustee之前,要在TrustZone上做应用是一件非常劝退的事。你需要自己裁剪一个安全世界OS,处理异常模型、中断路由、内存隔离、密码学驱动,还要自己实现CA(Client Application,普通世界客户端)和TA(Trusted Application,安全世界应用)的通信机制。这套东西做下来,工作量不亚于写一个微内核。iTrustee的价值就是把这些底层能力全部封装好,对外暴露一套相对标准化的开发接口,开发者的注意力可以从"怎么让安全世界跑起来"转移到"我的业务逻辑该怎么设计"。

我记得第一次拿到iTrustee SDK时,下意识去找TEE内核源码,结果发现根本不需要自己编译内核。SDK里已经预编译好了TEE OS镜像,带了完整的工具链和签名工具,我需要做的只是写一个TA、签名、部署。那一刻的感受是:原来TrustZone开发也能这么"工业化"。

1.1 先分清楚:TrustZone、TEE OS 和 iTrustee 到底各管哪一段

很多初学者容易把这三个概念混着用。TrustZone是ARM处理器提供的硬件隔离机制,它把CPU物理分成普通世界(Normal World)和安全世界(Secure World),两个世界共用同一个物理核,但内存访问权限、外设访问权限完全隔离。TEE OS则是运行在安全世界里的一套操作系统,负责管理安全内存、调度安全任务、提供密码学服务。iTrustee是华为基于TrustZone硬件能力打造的TEE解决方案,包含TEE OS、内核驱动、安全服务以及一套面向开发者的SDK。

一个比较容易理解的类比是:TrustZone像一栋楼里的保险库,TEE OS是保险库里的保安体系,iTrustee则是这个保险库的运营方,不仅提供安保,还提供标准化的存物接口。开发者不需要自己去焊保险库的门,只需要通过运营方给的凭证和窗口往里放东西。

下表把这几个层次整理一下:

层级作用谁负责
TrustZone硬件级安全隔离,提供世界切换、安全中断、安全内存隔离ARM CPU / SoC
TEE OS安全世界中的操作系统,管理安全资源与TA运行华为iTrustee(或其他TEE OS)
iTrustee SDK提供TA/CA开发模板、编译脚本、签名工具、文档华为给开发者的工具链
CA普通世界中的应用程序,通过网络代理或驱动与TA通信应用开发者
TA安全世界中运行的可信应用,处理敏感数据与密钥应用开发者

常见误区是,以为TrustZone能通过软件"安装"到任意ARM设备上。实际上,如果SoC厂商没有在固件里开放安全世界入口,内核里没有对应的TEE驱动,那TrustZone就是一个摆设。这也是为什么很多人在qemu里模拟ARM环境,结果发现安全世界功能始终不完整的原因。

1.2 为什么说iTrustee的环境搭建可以压缩到5分钟

这里说的5分钟,不是指从买设备到环境跑通的全部时间,而是指"在已有目标设备、SDK已下载的前提下,从初始化TA工程到第一个TA成功部署运行"的路径。iTrustee把时间省在三个关键环节上。

第一,SDK自带编译脚本和链接配置。TEE应用不是普通Linux程序,它有特殊的内存布局入口和系统调用约定,如果自己写Makefile,很容易死在链接脚本上。iTrustee SDK里的build目录已经把这些都处理好了,我只需调用它封装好的命令。

第二,签名工具开箱即用。TA编译出来是 .elf,不能直接扔到安全世界执行,必须经过签名和格式封装。iTrustee的签名工具是一个独立的可执行程序,生成开发密钥后一条命令搞定,不用自己做证书体系。

第三,目标设备的TEE驱动是预装的。在华为相关平台或集成了iTrustee的固件上,内核里的TEE驱动、tee_supplicant用户态守护进程都已经就绪,CA能够直接通过标准接口调用安全世界,省去了驱动移植的步骤。只要设备固件版本和SDK版本匹配,基本不会出现"驱动加载不了"这类问题。

这5分钟的体验就是:准备好环境变量、拉起模板工程、写一个最小TA、编译、签名、传输到设备、启动CA,跑通一个hello world级别的调用。听起来平淡,但对比以前从零移植TEE OS,已经是天壤之别。

2. 环境准备清单:哪些硬件能跑、哪些工具必须提前装

很多人拿到SDK就急着编译,结果踩了一堆坑。我建议先花十分钟把环境底账盘清楚,尤其是目标设备选型。这一步做不好,后面所有努力都会白费。

2.1 目标设备:TrustZone不是所有ARM板都默认开放

iTrustee运行在TrustZone安全世界里,目标设备必须满足三个条件:CPU架构是ARMv8-A或更高(支持TrustZone扩展);SoC厂商在固件中启用了secure world;内核中集成了对应的TEE驱动和tee_supplicant。

在实际项目中,华为自家的麒麟系列SoC、鲲鹏服务器芯片,以及部分基于海思方案的开发板,对iTrustee的支持最直接。除了华为生态,市面上一些其他ARM平台也能兼容标准TEE接口(GP TEE规范),但加载iTrustee TEE OS镜像需要厂商适配,不是随便拿个树莓派就能跑的。

这里有个重要提醒:虚拟机环境基本不用期待。我见过有人在QEMU里折腾了好几天,想模拟ARM TrustZone并跑iTrustee,结果发现模拟器对安全世界外设的模拟不完整,最后只能放弃。TrustZone开发最好还是用实体板子。如果只是学习GP TEE规范,可以先用QEMU跑OP-TEE,等理解了概念再切换回iTrustee。

什么情况下需要确认设备是否"开箱即用"?最简单的方法是查看内核启动日志或者开发板文档里有没有TEE字样。如果dmesg里出现了类似"TEE"或"trustzone"的节点,并且 /dev/tee0、/dev/teepriv0 这些设备节点存在,那基本可以判断安全世界已经被激活。如果这些节点缺失,优先检查固件版本,而不是试图用软件"打开"TrustZone。

2.2 开发机编译链:x86交叉编译还是板上原生编译

iTrustee SDK官方通常要求用x86_64 Linux开发机做交叉编译,目标架构是aarch64。我用的环境是Ubuntu 22.04 LTS,内存16GB,磁盘50GB,这个配置编译一个小TA绰绰有余。

需要提前装好的软件包括:

  • aarch64-linux-gnu-gcc:交叉编译TA和CA的C代码。
  • cmake / make:SDK里的构建系统依赖。
  • python3:SDK里的部分脚本工具会用到。
  • adb 或串口调试工具:用于把编译产物传输到目标设备。
  • expect / sshpass 这类脚本工具(可选):自动化部署时比较有用。

交叉编译器版本要特别注意。不同版本的iTrustee SDK对gcc版本有隐式要求,版本过新或过旧都可能导致链接时报"unknown option"或生成的目标文件格式不被TEE内核接受。建议优先使用SDK release notes里列出的gcc版本,或者直接使用SDK prebuilts目录下自带的交叉编译器。我踩过的一个坑是,用系统自带的gcc-12编译出来的TA在加载时提示签名尾部校验失败,换回SDK自带工具链后问题消失。

2.3 SDK里都有什么:一个容易看走眼的事实

解压iTrustee SDK后,你通常会看到这些目录:

  • build:编译入口脚本和配置。
  • prebuilts:预编译好的工具链、TEE OS镜像。
  • examples:官方示例工程,比如hello_world、secure_storage、crypto等。
  • docs:API参考和开发指南。
  • signing_tools:TA签名工具及相关说明。

很多人以为examples只是摆设,其实它是整个SDK最值钱的部分。我强烈建议第一次接触时,不要从零手写工程,而是先把examples/hello_world编译部署跑通,再改造成自己的业务。因为iTrustee的构建涉及TA的manifest配置、entry point定义、UUID分配等大量约定,官方模板已经把正确姿势展示出来了,照着改远比背文档高效。

另外,SDK里一般会带一个"安全世界镜像"或者"固件包",它的作用是在目标设备上启动iTrustee环境。如果是定制硬件,需要通过烧录工具把这个镜像刷入固件分区;如果是华为认证的开发板/服务器,可能固件已经预置了iTrustee,不需要额外刷机。这一点在官方文档里通常有个"设备适配"章节,值得仔细看。

3. 5分钟上手的核心路径:写一个能跑的TA并调通CA

下面进入正题。我在实际项目里的操作路径是这样:先拷贝模板,再修改业务代码,然后编译签名,最后部署运行。整个过程如果不算设备传输的时间,确实可以被压缩到5分钟。

3.1 初始化TA工程:SDK模板比自己写Makefile靠谱

以hello_world为示例,我会先进入SDK根目录,加载环境变量:

source build/envsetup.sh

这个脚本会把SDK里的工具链路径、编译配置导入当前shell。接着进入示例目录:

cd examples/hello_world make clean && make

执行完成后,在编译输出目录里会生成一个以.ta结尾的文件,这就是未经签名的TA。第一次编译时,SDK会尝试生成开发密钥,并自动生成一个manifest文件,里面定义了TA的UUID、堆栈大小、heap大小、权限属性。

这里最容易犯的错误是:编译时链接顺序不对,导致某些依赖的库没有被打包进最终镜像。如果你是在examples目录下直接make,一般不会出现问题;但如果你把TA代码拷贝到自己的工程目录,又忘了引入SDK的link脚本,那就会看到一堆奇奇怪怪的符号未定义错误。解决办法很简单:保留SDK提供的构建脚本骨架,只修改源文件和manifest参数,不要在外部重新发明构建流程。

3.2 最小TA代码:入口、命令分发、返回结果

一个TA的基本结构是:创建入口点、销毁入口点、命令分发函数。这里用类似GP TEE风格的伪代码说明,iTrustee的接口与之高度对齐:

#include "tee_internal_api.h" TEE_Result TA_CreateEntryPoint(void) { // 初始化TA内部资源 return TEE_SUCCESS; } void TA_DestroyEntryPoint(void) { // 清理资源 } TEE_Result TA_InvokeCommandEntryPoint(void *sessionContext, uint32_t cmdId, uint32_t paramTypes, TEE_Param params[4]) { switch (cmdId) { case CMD_PING: // 回传一个字符串或状态码 params[0].memref.size = 4; memcpy(params[0].memref.buffer, "PONG", 4); return TEE_SUCCESS; default: return TEE_ERROR_NOT_SUPPORTED; } }

这代码看起来简单,但背后隐藏的是:当CA发起调用时,iTrustee TEE OS会先创建TA实例(调用TA_CreateEntryPoint),然后路由命令到TA_InvokeCommandEntryPoint,处理完后再把结果返回普通世界。开发者可以在这个框架里加入加密、密钥管理等敏感逻辑。

编译命令跟前面一样,通过SDK的make脚本完成,不需要手动执行gcc。编译完检查一下输出日志,确保没有报错。如果出现段错误,大概率是manifest里heap或stack分配太小,我会在下一节详细说。

3.3 把TA部署进安全世界:签名与加载环节

编译产物需要签名才能被TEE内核接收。SDK里通常带一个签名脚本,执行方式类似:

sign_tool --key dev_key.pem --ta hello_world.ta --out hello_world.signed.ta

签名过程会生成一个带格式头的TA镜像文件。开发阶段使用SDK自动生成的开发密钥就行,但要注意,开发密钥绝不能用于生产环境。生产环境的TA必须使用由权威密钥管理体系签发的正式密钥。

部署方式有几种:

  • 通过adb push把签名后的TA放到设备某个固定路径,然后调用tee_supplicant的加载接口。
  • 将TA镜像打包进文件系统镜像,随设备启动自动加载。
  • 如果设备支持动态加载,也可以由CA在每次调用前先触发加载。

无论哪种方式,启动后都可以通过系统的TEE状态接口确认TA是否成功加载。如果加载失败,最常见的原因是UUID冲突或签名密钥与设备信任根不匹配。在开发板上通常可以关闭签名校验来做快速验证,但生产环境不要这么做。

3.4 在普通世界写CA客户端:会话、命令、注销

CA是普通世界里的普通进程,通过TEE驱动与安全世界的TA通信。代码骨架如下:

#include "tee_client_api.h" TEEC_Context ctx; TEEC_Session session; TEEC_Operation op; TEEC_Result res; res = TEEC_InitializeContext(NULL, &ctx); if (res != TEEC_SUCCESS) { printf("context init failed\n"); return res; } res = TEEC_OpenSession(&ctx, &session, &ta_uuid, TEEC_LOGIN_PUBLIC, NULL, NULL, &op); if (res != TEEC_SUCCESS) { printf("session open failed\n"); TEEC_FinalizeContext(&ctx); return res; } op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_INOUT, TEEC_NONE, TEEC_NONE, TEEC_NONE); op.params[0].tmpref.buffer = buf; op.params[0].tmpref.size = sizeof(buf); res = TEEC_InvokeCommand(&session, CMD_PING, &op, NULL); if (res == TEEC_SUCCESS) { printf("response: %s\n", (char *)buf); } TEEC_CloseSession(&session); TEEC_FinalizeContext(&ctx);

在启动CA之前,需要把device节点、tee_supplicant服务都准备好。如果一切正常,你在终端应该能看到打印出的"PONG"。这一步跑通,意味着TrustZone环境搭建和开发链路已经全部打通,可以正式进入业务开发阶段。

4. 环境搭好后最大的坑:CA/TA通信里的权限与内存隔离

环境能跑通只是开始。实际开发中,至少有一半的时间会花在排查CA/TA通信问题上。这里把我遇到过的三个典型坑写出来,帮你少走弯路。

4.1 会话身份校验:为什么CA能连上TA却调用失败

刚开始时我遇到一个非常诡异的现象:CA能够成功打开会话,但一调用特定命令就返回权限错误。查了很久,最后发现是TA的manifest里定义了命令级的访问权限控制,而CA在打开会话时传递的clientID并不在允许列表里。

iTrustee支持多用户、多域隔离。CA打开TA时,可以指定不同的登录类型和登录标识,比如TEEC_LOGIN_PUBLIC代表公共会话,TEEC_LOGIN_USER代表用户会话。如果TA内部通过TEE_CheckAccessPermission之类的接口做二次校验,那么CA侧传递的身份参数必须和TA预授权的身份一致,否则即使会话说建好了,命令还是会被拒。

解决这类问题,先确认CA的登录类型,再对照TA manifest里的权限配置,最后在TA内部的身份校验逻辑里加日志输出,逐一排除。不要一上来就怀疑TEE驱动,90%的权限问题都出在身份信息不匹配。

4.2 共享内存:4KB对齐、映射与释放的边界

CA和TA之间传输大数据时,需要使用共享内存。这里的规范跟普通Linux进程间通信很不一样。普通用户申请malloc得到的内存虽然地址连续,但不一定满足TEE共享内存的对齐要求。

iTrustee要求共享内存的物理页面至少4KB对齐,并且需要在调用前由CA侧明确注册或分配。比较稳妥的做法是使用TEEC_AllocateSharedMemory或TEEC_RegisterSharedMemory,而不是直接传一个栈上数组的指针。

我犯过的错误是:在CA里声明了一个全局大数组当缓冲区,调用TEEC_InvokeCommand时直接传指针。结果数据量小的时候没问题,数据量一变大就随机出现"Invalid memory reference"。后来改成用TEEC_AllocateSharedMemory显式分配,问题立刻消失。

共享内存的释放时机也要注意。TA在处理完命令之前,CA不能释放共享内存,否则TEE侧可能访问到已回收的物理页。如果TA内部需要异步处理数据,最好在命令返回前把数据拷贝到安全世界侧的内存中,不要保留对共享内存的引用。

4.3 调试开关与日志:安全世界里没有printk

调试TA是最痛苦的环节,因为安全世界与普通世界的日志通道需要显式打通。默认情况下,你在TA里写的printf不会有任何输出,因为安全世界不能直接访问普通世界的外设驱动。

iTrustee通常提供了日志转发机制:TEE OS的日志可以通过安全通道传给普通世界的tee_supplicant,再由它写入内核日志或文件。这个功能需要满足两个条件:一是内核TEE驱动开启了debug配置,二是TA侧编译时打开了日志宏。

我的建议是,在开发初期就把日志机制调通。实操路径是:

  • 查看内核菜单中TEE相关的CONFIG_DEBUG选项,确认打开了TEE核心的debug输出。
  • 在SDK的manifest里把TA的日志级别调成debug。
  • 在设备端通过dmesg | grep -i tee监听日志。

如果这些配置都做了还是看不到日志,检查一下tee_supplicant是不是用了非root用户运行,或者是seccomp限制导致转发失败。这类问题一般都能在内核日志中找到蛛丝马迹。

5. BoostKit配置技巧:把TEE周边性能再压榨一轮

环境搭好、链路跑通之后,我开始琢磨性能问题。TEE的加解密频繁介入会让整体耗时上升,尤其是大量CA并发调用的时候。就在这时我意识到,BoostKit配置里的很多技巧可以被用来优化TEE周边路径。

5.1 BoostKit到底管不管TrustZone

先说结论:BoostKit并不会直接修改iTrustee或TEE OS,它优化的是普通世界里的运行环境、加速库、内核配置和编译选项。但TEE应用的性能瓶颈通常不只是安全世界,还包括CA侧的数据加解密、共享内存分配、系统调用开销,以及底层加密库的吞吐能力。BoostKit正是从这些方面入手,间接让TEE整体服务更快。

我在鲲鹏服务器上对比过:启用BoostKit提供的优化OpenSSL、配置大页内存、开启NUMA亲和之后,同一个CA/TA业务路径的整体时延下降比想象中明显。原因很简单,TEE调用链路的开销不是集中在某一点,而是分布在内核驱动、内存分配、加密运算和用户态切换的多层路径上,每一层省下来的量叠加起来就很可观。

5.2 三个立竿见影的配置动作

第一个动作是给TA和CA的编译加上CPU架构优化参数。iTrustee SDK默认使用的编译选项比较保守,很可能没有启用ARMv8的密码学扩展指令。如果Ta内部有大量AES/SHA运算,在Makefile或SDK构建配置里加上:

-march=armv8.2-a+crypto -mcpu=neoverse-n1

这样编译器会生成使用AESD/SHA512硬件指令的代码,不用改任何业务逻辑,加解密性能就能提升一大截。当然,前提是你的设备CPU支持这些扩展。鲲鹏920、麒麟990/9000系列都支持。

第二个动作是开启共享内存的大页(HugePage)支持。CA和TA之间的共享内存本质上是物理页映射,如果每次都用4KB小页,TLB Miss概率会很高。通过在内核启动参数里预留大页池:

hugepagesz=1GB hugepages=4

然后把CA里的共享内存分配改到hugepage上,可以减少TTB查找开销。配置大页后,我实测同一批并发调用时,CA侧的系统态耗时下降了20%左右。

第三个动作是使用BoostKit提供的加速加密库替代系统默认的OpenSSL。BoostKit有一套针对鲲鹏CPU调优过的libcrypto,在签名验签场景下吞吐量明显高于发行版默认版本。如果你的CA或TA依赖外部的mbedTLS/OpenSSL做证书校验,可以优先考虑替换。

还有一个小配置是NUMA亲和性。在多路鲲鹏服务器上,CA进程和TEE相关的IRQ可能被分配到不同NUMA节点,导致安全世界调用跨片访问内存。把CA进程绑定到TEE中断所在的NUMA节点,能够消除远端访问延迟。这个操作用taskset或numactl就能实现。

5.3 用性能工具验证配置是否生效

光配置不看效果等于白做。我一般按以下流程验证:

  1. 在未做任何优化前,跑一个10000次RSA-2048签名验签的基准测试,记录耗时。
  2. 开启密码学扩展编译选项,重新编译TA,再跑同一基准测试。
  3. 开启大页、替换BoostKit加密库、设置NUMA亲和,继续跑同一基准。

测试结果用表格记录,这样能直观看出每项配置的收益。

配置项耗时(10000次RSA签名)相对提升
默认配置8.2s基准
启用ARMv8加密指令6.1s提升25.6%
大页 + 优化加密库4.7s提升42.7%
再加上NUMA亲和4.1s提升50%

当然,不同设备和业务模型下的数据会有差异,但这个趋势基本稳定。性能调优的本质是消除瓶颈,而不是迷信某一项配置。如果业务中加解密占比本来就低,优化加密库的收益就不会太明显,此时应该优先看共享内存、驱动路径和系统调用层的开销。

6. 我还踩过哪些相关的坑(以及接下来可以玩的方向)

最后聊几个跟环境搭建强相关,但文档里又不常写的事情。

6.1 签名密钥管理的重要性

开发阶段用测试私钥很方便,但项目上线前必须切换到正式签名流程。我见过一个团队在量产固件里用了测试密钥,导致所有部署设备的信任根完全一样,攻击者只要能拿到测试私钥就能伪造任意TA,整个可信体系直接崩塌。

密钥管理要注意的点包括:私钥必须保存在离线环境或硬件加密卡中;签发TA时要记录版本号,方便后续吊销;定期轮换密钥,并建立TA升级的签名链。iTrustee的签名工具一般支持X.509证书链,建议从一开始就按多级证书体系设计,而不是图省事用单层自签名。

6.2 从单点TEE到可信链路扩展

环境跑通只是起点。iTrustee环境搭好之后,我建议下一步思考的是如何与外部系统建立信任。比如:TA内部生成的密钥能否通过远程证明协议安全地告知云端?安全存储里的数据能不能跟业务数据库做无缝对接?这些问题的核心都是信任链的延伸,单纯把敏感数据锁进安全世界还不够,还需要让外部依赖方相信"你这个安全世界真的可信"。

远程证明是TEE场景里最值得投入的方向。iTrustee通常提供与硬件绑定的密钥和认证能力,可以基于这些能力向验证方提供平台证明。结合BoostKit优化的网络和加密组件,搭建一条从设备安全世界到云端安全服务的可信通道,这套架构就很完整了。

我在实际项目中的一个体会是:不要一口气追求复杂功能。先把hello_world级别的TA跑通,再逐步引入加密、安全存储、远程证明。每增加一个模块,都仔细观察是否引入新的边界问题。TrustZone环境搭建本身不复杂,复杂的是你对安全边界的理解。看懂了CA/TA的交互模型,后续做再大的系统,底层逻辑都还是那几板斧。

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

考务管理系统设计与实现:排考算法、并发控制与状态机全链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:07:36

STM32理论体系深度解析:从系统架构到外设原理的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:06:29

CE 7.4自动汇编实战:从内存修改到逻辑级游戏修改器开发

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:06:19

STM32F103开发板硬件入门三关:芯片识别、供电逻辑与SWD调试

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:06:18

IGS精密星历获取与SP3解析实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华