news 2026/9/18 18:24:58

从LLVM架构到自定义Pass:深入编译器基础设施

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从LLVM架构到自定义Pass:深入编译器基础设施

做编译器这些年,我朋友圈里的朋友总爱问一句:LLVM 到底是啥?有人说是编译器,有人说是 Clang 的底层,有人说是“造轮子神器”。其实都对,但都不完整。llvm-project绝不只是一款编译器,它是一整套编译基础设施生态:你能用它把 C/C++ 翻译成机器码,也能用它写一个代码分析工具,在程序里“找茬”;还能用它做 JIT 动态编译,让程序运行时现场生成机器指令;甚至能拿它做软件渲染器,在没有显卡的服务器上跑 OpenGL。这篇文章我想把它从底层架构到日常实操完整拆一遍,适合刚准备入门的读者,也适合那些想基于 LLVM 做点自己的分析工具、语言玩具、JIT 引擎,或者单纯想搞懂“编译器到底是怎么工作的”的同行。

1. llvm-project 在解决什么问题?

先聊一个很基础的疑问:编译器的世界已经有 GCC 这种老牌王者了,为什么 2000 年还有人要“重复造轮子”,搞出个 LLVM?

1.1 传统编译器的痛点:前后端强耦合

传统编译器(比如早期 GCC 就很有代表性)把“听代码语言”“做优化”“生成机器码”三件事直接串在一起。前端只认识某种特定语言,后端只服务于某类特定 CPU。今天你想支持一门新语言,几乎等于重新写一轮解析、优化、代码生成;明天你想适配一款新芯片,又得从前到后重新过一遍。这种设计在“语言少、硬件更少”的年代够用,但在移动芯片爆发、新语言不断冒出来的时代,太累了。

LLVM 给出的解法非常彻底:把编译过程切成三大块——前端(Frontend)中间表示(IR,Intermediate Representation)后端(Backend)。所有前端只负责把源码翻译成一份统一的“中间语言”;所有优化都在这份中间语言上做;所有后端只认这份中间语言,把它翻译成不同 CPU 的机器码。这样设计之后,新增一种语言,就只需要写一个新的前端;新增一种 CPU 架构,就只需要写一个新的后端。前后端之间的接口稳定下来,像乐高一样任意插拔。

1.2 “中间人”IR 为什么这么关键

这个拆分的核心,在于那份“统一中间表示” LLVM IR。你可以把它理解成一种低层但是带类型的、可读的汇编语言。它既保留了程序的真实控制流和数据流信息,又屏蔽了具体编程语言的语法差异和 CPU 的指令差异。正因为它“够低层又够结构”,优化器能在这个层面做各种花式变换,后端也能通过同一个 IR 生成不同指令。

我经常给朋友打一个比方:LLVM 这套架构像一个中央厨房的净菜配送中心。前端是各地风味的点菜员,后端是不同菜系的掌勺厨师,而 IR 是已经洗好、切好、按标准化规格打包的净菜。只要把净菜规格定好,不管客人点的是川菜还是粤菜,切配员只需处理原始食材一次,大厨拿到统一的净菜就能下锅。这样做的好处是切配经验可以复用,炒菜手艺也可以复用。

1.3 它到底“管”了哪些事

单看仓库名“llvm-project”,你可能会以为它只是一个代码库。实际它是一个monorepo(单仓多项目),里面同时维护了 Clang(C/C++ 前端)、LLVM 核心库、LLD 链接器、libc++、compiler-rt、LLVMPipe 软件渲染器等一大堆项目。以常见的 LLVM 15.0.7 版本为例,这个版本已经是非常成熟的生产版本:它支持 C/C++ 的新标准特性,也集成了新版 pass 管理器,在性能分析和优化方面表现稳定。很多团队至今还在用它做线上构建。

这个生态覆盖的链路是完整的:你写 C/C++ 代码,Clang 把它变成 IR;opt 优化器对 IR 做优化;llc 把 IR 变成汇编或目标文件;lld 再链接成最终可执行文件。同一套 IR 和优化器,还能服务 Rust 语言的 rustc(早期版本)以及各种领域特定语言(DSL)。所以“llvm-project”这个名字听着朴素,实际是一个能撑起整条编译链路的“全家桶”。

2. 核心组件拆解:从 Clang 到 LLVMPipe

既然要实操,先得把它拆开看懂。下面这几个组件是日常打交道最多的。

2.1 Clang:C/C++ 家族的锋线

Clang 是 LLVM 官方的 C/C++/Objective-C 前端。它不像 GCC 那样把前端和后端揉在一套代码里,而是以 LLVM 库为基础,专门负责“听”源码和报错。Clang 的报错信息很有名,报错会精确到某个符号、某个类型不匹配的地方,甚至给出修改建议。我自己在项目里体验下来,第三方库头文件被引入错误时,Clang 的诊断比很多编译器都定位更快。

Clang 在设计上也被做成了库:你想写一个代码重构插件、静态分析器,不必去碰复杂的语法树细节,可以直接调用 Clang 提供的 API,也可以基于它的 AST(抽象语法树)做自定义检查。实际写代码时,Clang 的很多子命令你大概率会用到,比如clang -S -emit-llvm可以把 C 代码翻译成 .ll 格式的 IR 文本,是学习 IR 最直接的入口。

2.2 LLVM IR:三种形态,一个内核

LLVM IR 有三种表现形式,底层是同一份内容:

  • 内存中的数据结构(C++ 对象图):编译器在运行时直接操作的形式;
  • Bitcode(.bc文件):二进制编码,适合快速加载、存储;
  • 文本形态(.ll文件):人类可读的汇编式代码。

三种形式可以互相转换,核心表达力完全一致。我建议新手直接读文本形式,它看起来长这样:

define i32 @add(i32 %a, i32 %b) { %r = add i32 %a, %b ret i32 %r }

这里的i32表示 32 位整数,%a%b是虚拟寄存器,add做加法后把结果赋给%r。熟悉汇编的读者会觉得很亲切,但比汇编多了类型系统,并且所有值都遵守“静态单赋值(SSA)”规则——每个变量只被赋值一次。这个 SSA 设计非常关键,它让优化器在分析数据流时获得了极大的确定性,比如做常量传播时,不需要追踪“这个变量在哪被改过”,因为根本不会被乱改。

2.3 opt 和优化 Pass:真正的价值所在

光有 IR 没什么用,重点在于能对 IR 做“手术”。每个“手术”在 LLVM 里叫Pass(优化过程)。LLVM 把数百种优化封装成独立 pass:有做内存提升的mem2reg,有做指令合并和常量折叠的instcombine,有做死代码消除的dce,还有做内联的inline

优化可以串联成一个个 pipeline。比如opt -O2不是一个 pass,而是一组 pass 的顺序执行。我自己在写新的分析 pass 时,最喜欢用新版 pass 管理器(New Pass Manager),它的迭代接口更清晰,写新分析时不必关心全局副作用。在 LLVM 15.0.7 里,新版 pass 管理器已经是默认选项了,不再需要额外的 flag 去开启。

注意一个细节:老的 pass 管理器里 pass 是“类”,会注册到全局注册表;新 pass 管理器里 pass 基本是按模块为单位跑的。写插件时要稍微留心新旧区分,不要把旧接口硬搬到新环境,否则很容易出现编译不过或运行时崩。

2.4 后端与 LLVMPipe:没有显卡也给你渲染

后端负责把 IR 翻译成具体目标架构的指令。LLVM 支持的后端非常多,x86、ARM、AArch64、RISC-V、PowerPC 都有。每个后端都有独立的目标描述文件、寄存器分配器、指令选择器、指令调度器。

这里必须专门说说LLVMPipe,因为它是 LLVM 基因里特别有意思的一个体现。LLVMPipe 是一个纯软件的光栅化渲染器(Software Rasterizer),它不依赖独立显卡,而是用 LLVM 的 JIT 能力,在运行时生成针对当前 CPU 优化过的汇编代码,然后执行像素处理。

LLVMPipe 可以支持 OpenGL 和 Vulkan,在云服务器、虚拟机、没有 GPU 的开发板上非常有用。比如你在一台云主机上跑一个需要图形界面的测试程序,显卡肯定不存在,但通过 Mesa 里的 LLVMPipe,程序依然能“渲染”出结果——只是计算全部由 CPU 完成,性能不如独立显卡。LLVMPipe 在 x86 平台上经常会用到 256 位 SIMD 指令(AVX/AVX2)来加速像素处理,这也是很多人在日志里看到“llvmpipe (LLVM 15.0.7, 256 bits)”这类信息的原因。

想确认当前系统图形后端是不是 LLVMPipe,可以跑:

glxinfo | grep "OpenGL renderer"

如果返回llvmpipe (LLVM 15.0.7, 256 bits),说明程序正在用 CPU 软渲染。对这个信息不要惊慌,很多 CI 环境、Docker 容器里没有 GPU,这是最正常的降级方案。软件渲染的兼容性是无敌的,只是性能上要有心理预期。

3. 实操:从零构建 llvm-project 15.0.7 并写一个自定义 Pass

接下来进入实战环节。我会带你把 llvm-project 15.0.7 完整构建一遍,再写一个简单的自定义 Pass,输出所有指令icmp(整数比较)的调用次数,并把条件恒真的比较替换成true。这个例子虽小,但涵盖了基于 LLVM 做工具开发的基本路径。

3.1 环境准备与源码拉取

我以 Ubuntu 22.04 为例,先安装基础依赖:

sudo apt update sudo apt install -y build-essential cmake ninja-build python3 git

这里用 Ninja 而不是传统的 Make,因为构建 LLVM 这种大型项目时,Ninja 的并行调度更快,而且增量编译更稳。磁盘空间要注意,完整构建 llvm-project 的 build 目录通常会消耗 30GB 以上,源码加工具链再加其他依赖最好准备 60GB 空余空间。

拉取指定版本时,建议直接切到 tag 上,不要用 master 分支,因为 master 处于快速迭代中,会有很多兼容性变动:

git clone --depth 1 --branch llvmorg-15.0.7 https://github.com/llvm/llvm-project.git cd llvm-project

如果你只需要默认后端,llvm-project/llvm目录就放在这里,后续 cmake 指向它即可。浅克隆可以省不少时间和流量,但如果之后想看 git 历史里的重要改动,就要完整克隆了,看你的实际需要。

3.2 CMake 配置与构建参数取舍

LLVM 把构建抽象成了 CMake,参数非常多,理解几个关键的能帮你少走弯路。我使用的配置如下:

cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_ENABLE_PROJECTS="clang;lld" \ -DLLVM_TARGETS_TO_BUILD="X86" \ -DLLVM_BUILD_LLVM_DYLIB=ON \ -DLLVM_ENABLE_RTTI=ON \ -DLLVM_ENABLE_EH=ON \ -DBUILD_SHARED_LIBS=OFF \ -DCMAKE_INSTALL_PREFIX=$HOME/llvm-15.0.7-install \ ../llvm

逐个解释:

  • CMAKE_BUILD_TYPE=Release:编译优化后的 LLVM,运行速度快,适合平常使用。如果要调试 LLVM 本身,就改成Debug,但库体积和编译时间都会暴涨。
  • LLVM_ENABLE_PROJECTS="clang;lld":除了 LLVM 核心库,还构建 Clang 和 LLD 链接器,两个都是日常非常常用的。暂时不需要 compiler-rt,因为它主要服务于 sanitizer、性能剖析和运行时支持,有需要再加不迟。
  • LLVM_TARGETS_TO_BUILD="X86":只生成 x86 后端的代码,能显著减少编译时间。如果你还需要 ARM 交叉编译,再加ARMAArch64
  • LLVM_BUILD_LLVM_DYLIB=ON:把 LLVM 核心库合成一个大的动态库。这样你写插件时链接更容易,加载也方便,缺点是没法对单个库做裁剪。
  • LLVM_ENABLE_RTTILLVM_ENABLE_EH:默认 LLVM 可能关掉 RTTI 和异常,但很多基于 LLVM 的第三方项目会需要它们,我默认打开,省得到时候别人代码编译不过。
  • BUILD_SHARED_LIBS=OFF:这里关闭所有组件单独打成动态库,避免几十个小 .so 文件搅乱部署。和上面并不冲突,核心大库仍然是动态的。

执行构建:

cmake --build . -j"$(nproc)"

第一次构建耗时比较长,16 核机器跑 x86-only + Release 大概需要 10~30 分钟;如果是全目标平台 + Debug,数小时都很正常。建议不要贪多,按需裁剪目标平台。

构建完成后安装:

cmake --install .

把安装路径加入 PATH:

export PATH=$HOME/llvm-15.0.7-install/bin:$PATH export LLVM_DIR=$HOME/llvm-15.0.7-install/lib/cmake/llvm

3.3 生成并阅读一份 .ll IR

先写一个最简单的 C 文件:

int gcd(int a, int b) { while (b != 0) { int t = b; b = a % b; a = t; } return a; }

用 Clang 生成 IR:

clang -S -emit-llvm gcd.c -o gcd.ll cat gcd.ll

你会看到类似这种结构:

define i32 @gcd(i32 %a, i32 %b) { entry: br label %while.cond while.cond: %b.addr.0 = phi i32 [ %b, %entry ], [ %rem, %while.body ] %a.addr.0 = phi i32 [ %a, %entry ], [ %t, %while.body ] %cmp = icmp ne i32 %b.addr.0, 0 br i1 %cmp, label %while.body, label %while.end while.body: %rem = srem i32 %a.addr.0, %b.addr.0 %t = getelementptr inbounds i32, i32* %a.addr.0, i32 0 store i32 %b.addr.0, i32* %t, align 4 br label %while.cond while.end: ret i32 %a.addr.0 }

看到phi节点别慌,这只是 SSA 形式里合并来自不同分支值的标准表示。守着“每个变量只赋值一次”这个规则去读,你会慢慢习惯。icmp ne就是在比较b.addr.00,这是优化器能做非常多后续工作的基础信息。

3.4 写一个真正能跑的 Custom Pass

为了少碰烦人的构建系统配置,我直接用一个独立的 pass 插件方式来做。新建目录和文件:

mkdir mypass && cd mypass touch CMakeLists.txt MyPass.cpp

CMakeLists.txt内容:

cmake_minimum_required(VERSION 3.20) project(MyPass) find_package(LLVM REQUIRED CONFIG) message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}") add_library(MyPass MODULE MyPass.cpp) target_link_libraries(MyPass PRIVATE LLVM)

find_package(LLVM REQUIRED CONFIG)会去读取我们设置的LLVM_DIR变量。如果它没找到,你可以检查一下echo $LLVM_DIR是否指向安装目录的lib/cmake/llvm

MyPass.cpp内容,新版 Pass 管理器写法:

#include "llvm/IR/Function.h" #include "llvm/IR/IRBuilder.h" #include "llvm/IR/InstIterator.h" #include "llvm/IR/InstrTypes.h" #include "llvm/IR/Instructions.h" #include "llvm/IR/LegacyPassManager.h" #include "llvm/Pass.h" #include "llvm/Passes/PassBuilder.h" #include "llvm/Passes/PassPlugin.h" #include "llvm/Transforms/IPO/PassManagerBuilder.h" using namespace llvm; namespace { class ReplaceICmpWithTruePass : public PassInfoMixin<ReplaceICmpWithTruePass> { public: PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) { int icmpCount = 0; for (auto &BB : F) { for (auto &I : BB) { if (auto *ICmp = dyn_cast<ICmpInst>(&I)) { ++icmpCount; ICmp->replaceAllUsesWith(ConstantInt::getTrue(ICmp->getType())); ICmp->eraseFromParent(); // 注意:因为边遍历边删,继续遍历会有风险。 // 这里简化处理,实际建议先收集所有比较指令再统一处理。 } } } errs() << "[MyPass] Replace const ICmp -> true in " << F.getName() << ", removed " << icmpCount << " icmp\n"; return PreservedAnalyses::none(); } }; } // namespace

这里我犯了一个“简化错误”:在遍历BasicBlock的同时删除指令,会导致迭代器失效,实际生产代码必须先把比较指令收集到SmallVector,遍历完再统一替换。写插件时,永远不要在迭代器活跃期间删除容器内的元素,这是新手最容易踩的坑。

修正后,把待删除的指令先放到 vector:

SmallVector<ICmpInst *, 4> ICmps; for (auto &BB : F) for (auto &I : BB) if (auto *ICmp = dyn_cast<ICmpInst>(&I)) ICmps.push_back(ICmp); for (auto *ICmp : ICmps) { ICmp->replaceAllUsesWith(ConstantInt::getTrue(ICmp->getType())); ICmp->eraseFromParent(); }

接着写插件入口:

extern "C" ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo() { return {LLVM_PLUGIN_API_VERSION, "ReplaceICmpWithTruePass", LLVM_VERSION_STRING, [](PassBuilder &PB) { PB.registerPipelineParsingCallback( [](StringRef Name, FunctionPassManager &FPM, ArrayRef<PassBuilder::PipelineElement>) { if (Name == "replace-icmp-with-true") { FPM.addPass(ReplaceICmpWithTruePass()); return true; } return false; }); }}; }

这样在 opt 命令行里就可以用--passes="replace-icmp-with-true"来运行它。构建插件:

cmake -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DLLVM_DIR=$HOME/llvm-15.0.7-install/lib/cmake/llvm \ -DCMAKE_CXX_STANDARD=17 \ . ninja

如果构建时出现“找不到 LLVMConfig.cmake”,说明LLVM_DIR没生效;如果出现一堆模板报错,大概率是 LLVM 版本和你用的 C++ 标准不匹配。每个 LLVM 版本编译所需的 C++ 标准不同,15 系列至少需要 C++17。

运行:

opt --load-pass-plugin=./MyPass.so --passes="replace-icmp-with-true" gcd.ll -o gcd_new.bc opt -S gcd_new.bc -o gcd_new.ll cat gcd_new.ll

如果一切正常,你会发现输出的 IR 里icmp ne被替换成了 true,br指令的条件变成了i1 true。这只是一个玩具 Pass,但它说明白了一件事:你可以用几十行 C++ 深度修改一段中间代码。很多静态分析工具就是这么搭起来的。

3.5 构建过程常见性能瓶颈

源码构建时最常卡住的是 ParallelLink,多个目标文件链接会让内存吃紧。建议链接大目标时用ninja -j2而不是-j$(nproc),或者用lld作为系统链接器,速度提升明显:

sudo update-alternatives --install /usr/bin/ld ld /usr/bin/ld.lld 50

使用 LLD 做系统链接器后,LLVM 链接阶段的速度可以快数倍,内存占用也更收敛。

4. 常见问题与排查技巧实录

这部分我直接按“踩坑记录”的方式整理,内容来自我自己在多个版本的 LLVM 上实操时遇到的问题。

4.1 问题速查表

问题现象可能原因解决思路
cmake找不到 LLVM 配置LLVM_DIR未设置或路径错误export LLVM_DIR=$HOME/llvm-15.0.7-install/lib/cmake/llvm,再删掉 build 缓存重新 cmake
构建中途 OOM并行任务太多、Debug 构建内存开销大降低-j、换成Release、用 LLD 链接、限制LLVM_TARGETS_TO_BUILD
插件opt加载报版本不匹配插件用不同 LLVM 版本编译所有 cmake、编译过程必须统一指向 15.0.7 的安装路径
IR 查看时不可读直接看了.bc二进制文件opt -S转换,或者用llvm-dis file.bc -o file.ll
链接时报大量未定义符号LLVM_BUILD_LLVM_DYLIB为 OFF 但插件依赖核心库链接到libLLVM.sotarget_link_libraries(MyPass PRIVATE LLVM)
运行 pass 后结果没变可能 pass 被跳过,或没使用新版 pass 管理器语法查看opt -debug-pass-manager确认 pass 是否进入 pipeline
llvmpipe渲染极慢没有 GPU 或驱动未装,CPU 软渲染性能有限排查时加大显存虚拟内存?实际不能。只能调整需求或换带 GPU 的机器

4.2 关于“llvmpipe 256 bits”的疑惑

老有人问,为什么日志里llvmpipe显示 “256 bits”,是不是说它只能用 256 位色彩?不是。这里的 256 bits 指 LLVMPipe 为当前 CPU 生成的 SIMD 向量宽度信息。LLVMPipe 会检测处理器的 AVX/AVX 2 支持情况,生成 256 位宽的向量指令来并行处理像素数据;如果 CPU 支持 512 位(比如部分服务器 CPU 启用 AVX-512),它也可能显示 512 bits。所以看到 “256 bits” 很正常,说明你的 CPU 支持 AVX2,LLVMPipe 正充分利用 SIMD 加速。

另外,在无 GPU 的容器里跑图形测试,出现 LLVMPipe 并不等于“程序坏了”,它只是一种软件后备方案。遇到和渲染相关的问题时,先确认是不是走了 LLVMPipe,再判断性能问题还是功能问题。

4.3 调试 Pass 的实用技巧

新版 pass 管理器提供了一些很棒的调试选项:

opt -S -debug-pass-manager --passes="default<O2>" gcd.ll -o /dev/null

这个命令会输出 O2 优化里实际执行的每个 pass 顺序。写自己的 pass 时,我会挂一个--debug-only=loop-vectorize之类的参数,让 pass 内部的调试信息打到终端,比自己打errs()快得多。如果 pass 崩溃,可以用-print-after-all带上崩溃前的 IR 快照,能精确定位是哪一步破坏了 IR 结构。

4.4 谨慎对待“内存占用”和“构建时间”

构建 llvm-project 最容易劝退新手的,就是编译时间。我见过有人在 2 核 4G 的云主机上跑全量 Debug + 全后端构建,结果跑了一整天还没完。建议条件有限时:

  • 只构建 X86 后端;
  • 只构建 Release;
  • 不启用LLVM_ENABLE_PROJECTS="all"
  • 用 ccache 缓存对象文件,二次构建会快很多。

我自己的习惯是先按最小配置跑通,再按需增量增加组件,这样即使出问题,定位半径也小得多。

5. 后续还能往哪个方向扩展

写完第一个 Pass 之后,整个世界的边界就已经打开了。你可以把 Pass 扩展成更强的分析器:统计每个函数的循环次数、识别重复 Load 模式、伪造一个“死循环”检测器,甚至把 LLVM IR 变换成你自己的 DSL 字节码。

我个人比较推荐的路线是:先读一些生产级的 Pass 源码(比如InstCombineGVN),学习它们如何处理边界指令、如何保留PreservedAnalyses;然后动手写一个基于FunctionAnalysisManager的自定义分析;之后再试着用PassBuilder把自定义 pass 接入 O2 优化管线,让它自动参与标准优化流程。

另外,Clang 的 LibTooling 也值得研究。它能帮你写跨文件的代码重构工具,可以批量改工程里所有函数签名,这在大型代码库迁移里非常实用。LLVM 的生态已经成熟到“你缺的不是能力,而是想象力的边界”这个程度了。

我在实际项目里最深的一个体会是:LLVM 把“编译器”这门高门槛手艺,硬生生做成了“可以拼装的工程项目”。它不会替你思考程序的语义,但给了你足够精确的“手术刀”。刚开始觉得 IR 复杂、Pass 接口繁琐,多写两三个工具之后,你会逐渐感受到这种架构的优雅——所有语言共享一套优化链路,所有后端共享一套分析结果,这种复用带来的力量,是可以重塑计算机软件生产方式的。

最后留一个小问题给你练手:如果你想把所有add指令都改成sub,并让程序依然能通过测试,需要在哪里拦截、怎么保证不改变数据流?动手试一试,比读十篇文档都管用。

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

verilog-ethernet:FPGA UDP以太网协议栈入门

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

作者头像 李华
网站建设 2026/9/18 18:23:51

Innovus CTS中clock_gen skew group自动分组的陷阱与处理

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

作者头像 李华
网站建设 2026/9/18 18:22:47

立即上手gh-stack的7个理由:GitHub官方Stacked PRs工具深度解析

立即上手gh-stack的7个理由&#xff1a;GitHub官方Stacked PRs工具深度解析 【免费下载链接】gh-stack GitHub Stacked PRs 项目地址: https://gitcode.com/GitHub_Trending/ghst/gh-stack gh-stack 是 GitHub 官方推出的 Stacked PRs&#xff08;堆叠 PR&#xff09;命…

作者头像 李华
网站建设 2026/9/18 18:21:48

ChatGPT 外链偷渡 AI 智能体?TaoToken 这样改 Codex 的 config.toml

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

作者头像 李华
网站建设 2026/9/18 18:21:30

基于 Jev 的决策服务,TaoToken 只提供 Key 入口

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

作者头像 李华