news 2026/10/17 10:04:38

C程序逆向破解入门:从静态定位到修改跳转的实战流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C程序逆向破解入门:从静态定位到修改跳转的实战流程

如果你已经翻完了第一篇,把环境装好、随手反汇编过几个小Demo,那么这一篇我们直接开始动真家伙:拿一个真实的C程序,从头到尾走一遍逆向破解流程。说是“破解”,实际你全程看到的是代码分析、控制流判断、内存布局这些基本功。

这篇是“C程序逆向破解入门学习”的第二篇,面向已经知道什么是反汇编、什么是栈,但对“一个程序为什么能被破解”这件事还缺少直观体感的人。我会用最常见的密码校验场景做样本,带你用静态分析定位关键函数,再用动态调试精准修改跳转条件,把串流程跑通。整个过程不涉及高深内核知识,核心是建立“程序输入输出 → 逻辑判断 → 机器指令跳转 → 修改指令改变行为”这条完整链路。这个链路一旦打通,后面学脱壳、学协议逆向、学外挂分析,都能顺着这套思路往深里走。

咱们直接进正题。顺便提一句,本文里所有样本代码和操作仅限合法安全研究和个人学习,请勿用于破解他人商业产品。

1. 破解C程序前,先把底层逻辑摆明白

1.1 为什么C程序最好“破”?

很多新手一上来就找工具点按钮,这实际上跳过了最关键的认知。逆向破解C程序,本质是在操作“编译后留下来的机器码”。C语言编译后是一种接近机器指令的高级表达,它不像Java那样有虚拟机中间层,也不像Python那样自带解释器,所以你在反汇编窗口里看到的指令序列,几乎可以一一对应回源码里的语句。这就是C程序最容易被分析的原因。

举个例子,源码里一个简单的比较:

if (input == 1234) { printf("ok"); }

编译出来后,在汇编层面会变成cmp指令加上紧跟其后的jne/je条件跳转。你不需要理解全部指令,只要找到这个cmp的地方,尝试理解它为什么跳转,就能把程序的逻辑推断出来。破解的动作,本质上就是让原本不跳转的跳转、或者让原本跳转的不跳转。

用生活场景类比,程序就是一道门禁,门禁的规则是“刷卡且密码正确才开门”。你不需要重新装修整栋楼,只需要找到那个决定开门还是关门的继电器,把它短路或者断开,门就开了。逆向里的种种操作,就是去寻找、定位并且“短路”这个继电器。

1.2 函数调用、栈帧与寄存器:破解时盯住哪些东西

C程序执行的内在机制是函数调用。函数调用时会做几件固定的事情:把参数放进特定寄存器(x86-64)或压栈(x86),把返回地址压栈,进入新函数后分配一段内存作为函数的局部变量区,这块区域叫栈帧。

破解实践中,你至少要和下面这些问题打交道:

  • EBP/RBP:栈底指针,用来定位当前函数的参数和局部变量。虽然编译器优化后可能用RSP直接做位置参照,但有RBP的时候分析起来会更直观。
  • EIP/RIP:当前指令地址,单步执行时不断变化。
  • 条件跳转指令:jz/je(相等则跳转)、jnz/jne(不相等则跳转)、jg(大于则跳转)、jl(小于则跳转)等。几乎所有密码校验都会在调用字符串比较函数之后出现一个这样的跳转。

我见过太多新手一上来去找“加密算法”,其实入门阶段根本不用碰那些复杂算法。大部分C程序入门样本的校验逻辑都很直白:输入字符串,调用一个比较函数(比如strcmp),根据返回值决定跳转。你要做的就是在可疑的call之后下断点,观察返回值,然后修改紧接着的那个跳转指令。

重要提示:在x64系统上,参数依次放入RDI, RSI, RDX, RCX, R8, R9,返回值放在RAX。把这条记熟,动态调试时能省一半时间。

1.3 编译器与优化选项的影响

破解时你会遇到两个“性格”截然不同的程序:Debug版和Release版。Debug版编译时不开优化,变量基本都存在内存里,反汇编后结构清晰,每个变量都有明确的栈位置,新手用来学习最合适。Release版开了-O2甚至-O3优化,编译器会把变量尽可能塞进寄存器,很多函数会被内联展开,栈帧结构也变得非常规,静态分析时一眼看不清。

这不是说优化版不可破,而是在入门阶段没必要自己给自己上难度。我建议初学逆向时,自己编译样本就用gcc -g -O0,至少保存一份带符号的版本对照学习。等到对指令流敏感了,再尝试去分析开优化和strip过的二进制,那时候会顺畅得多。

还有一个关键点:动态链接和静态链接。动态链接的程序体积小,里面有大量对C库函数的调用(比如strcmp@plt),这些函数名称在动态符号表里可以查找到,给了你天然的“路标”。静态链接的程序体积大,函数全部揉在一起,新手很容易迷失。所以入门阶段也建议多用动态链接样本。

2. 工具与样本准备:搭一个最舒服的练手环境

2.1 工具链选型:静态、动态、系统监听三件套

老话重提,工具不求多,但三种类型都得有。静态分析工具我首选Ghidra或者某商业反汇编工具。Ghidra开源免费,反编译能力不错,适合长时间挂着分析。某商业反汇编工具的伪代码对新手更友好,交互手感也更丝滑,有条件用正版或者学习版都行。动态调试工具则是x64dbg(Windows下)或gdb(Linux下)。入门阶段我推荐x64dbg,因为它的界面把寄存器、栈、内存、反汇编直接平铺出来,比gdb的命令行交互直观得多。

系统监听工具指的是Process Monitor、Process Explorer这类辅助工具,用来观察程序读写了哪些文件、注册表、环境变量。虽然入门密码校验用不上它们,但一旦你开始接触“注册码依赖系统信息”的程序,它们就派上用场了。我不建议一开始就把IDA和调试器加一堆插件,维护成本高,反而分散注意力。

2.2 设计一个用于逆向练习的样本程序

这里咱们自己动手写一个样本,别去网上随便下载别人的程序,设计思路透明了,后面踩坑时才能判断是程序逻辑的问题还是自己操作的问题。

用C语言写一个控制台程序,功能是:提示用户输入密码,校验通过则输出Access Granted,否则输出Access Denied。源码如下:

#include <stdio.h> #include <string.h> int check(char *input) { char key[] = "secret_key"; return strcmp(input, key); } int main(int argc, char **argv) { char buff[64] = {0}; puts("Enter the password:"); gets(buff); if (check(buff) == 0) { puts("Access Granted"); } else { puts("Access Denied"); } return 0; }

注意我故意用了gets这种不安全函数,目的是让样本简单,没有任何栈溢出保护细节干扰。实际编译时,需要用-fno-stack-protector,否则编译器会插入__stack_chk_fail逻辑,初学者看到会困惑。编译命令:

gcc -m32 -g -O0 -fno-stack-protector -o crackme_crack.exe crackme_crack.c

如果你机器上没有32位运行环境,也可以去掉-m32直接编译64位,只是后面的汇编代码寄存器名会不同。本篇后面演示我以32位为例,因为EBP/ESP的栈帧经典结构更容易理解,学会32位后再看64位,无非是寄存器名和传参方式换了。

这里补充一点,为什么叫它“crackme_crack”而不是中文名?是因为国际上CTF和逆向游戏都有“crackme”说法,练手样本就叫crackme,你以后逛社区经常看到这个词。

2.3 环境避坑:关闭ASLR的注意事项

动态调试时你会反复重新加载程序。现代操作系统默认开启ALSR(地址空间布局随机化),每次运行时,程序基本地址都可能不一样。如果你在调试器里加载样本,调试器会为你固定基本地址,但直接双击运行和调试器运行的表现会不同。

为了避免分析地址和运行时地址对不上,建议在操作系统的调试设置里勾选“固定随机化基底地址”,或者直接用调试器加载运行,不要通过Windows资源管理器双击。在x64dbg中,用File -> Open加载exe,它默认就打着系统断点,加载完成后程序还没运行,内存布局已经固定,后续记录的地址在本次调试内直接可用。

另外我还建议:如果机器有杀毒软件,最好把被调试的样本目录加入可信区。很多保护软件会对“修改内存指令”的行为报毒,因为改指令确实符合恶意软件特征。不是你的程序有问题,是安全软件的合规判定逻辑比较敏感。搞逆向学习,自己准备一个干净的虚拟环境是最省心的方案。

3. 静态分析实战:在反汇编和反编译代码里找到关键校验位置

3.1 从“字符串”这个明显的路标入手

把编译好的crackme_crack.exe拖进反汇编工具,等自动分析完成。第一个值得注意的地方是字符串窗口或字符串列表。你会看到Enter the password:、Access Granted、Access Denied这些字符串。双击Access Granted,工具会直接跳到引用这个字符串的代码位置。这是整个分析过程最直观的“刀锋路径”。

顺着引用回溯,你会发现类似这样的逻辑:从某个地址读取字符串到寄存器,调用一个函数,然后比较返回值,再根据结果决定跳向打印Access Granted还是Access Denied的代码块。

这里我先说一种普遍经验:字符串引用是新手最强的路标,但真实世界的程序往往有加密的字符串,比如用异或打包过的提示信息,你在字符串窗口根本看不到。入门阶段用字符串引路只能算“热身课”,真正要掌握的是函数调用之间的关系。不过我们这个样本是标准的教学样例,字符串引路完全够用。

3.2 识别main函数和check函数

编译后的程序入口是start,它调用__libc_start_main,后者再去调用C的main。反汇编工具一般会把入口附近的main标出来,你可以直接跳到main。在Ghidra的反编译界面,基本能还原出原始流程:

int main() { char buff[64]; gets(buff); if (check(buff) == 0) { puts("Access Granted"); } else { puts("Access Denied"); } return 0; }

此时我们当然知道check函数内部是对静态密码做strcmp。你可以点进check函数的反汇编,会看到它把常量字符串地址加载到寄存器,然后调用strcmp。函数尾部通常就是:

call strcmp add esp, 0x10 test eax, eax sete al ...

这种模式非常经典。strcmp对两个字符串逐个字节比较,相等返回0,不等返回非0。随后test eax, eax把返回值与0进行“与”运算,设置标志位。接下来就会有jne或jz。

从这刻起,你其实已经知道破解方案了:不管输入什么字符串,只要让strcmp比较后“被当作相等”或者“跳转逻辑被反转”就行。但静态分析只是“纸上谈兵”,骨肉还需要在动态调试中确认。

3.3 理解反汇编中的“等价变形”

很多新手看反汇编喜欢一字一句对应源码,这没问题,但初学者要把重点放在“控制流汇聚点”上。什么是一个函数的控制流汇聚点?就是连续的几行高级逻辑可能被编译成非常规的汇编结构,比如用setne al加上test去打乱。

我讲到过check函数返回strcmp的结果等于0则通过。实际机器码更常见的是这样:

call sub_401050 ; 这里是被调用的strcmp add esp, 8 ; 平衡栈 mov [ebp-8], eax cmp dword [ebp-8], 0 ; 和0比较 jnz short loc_401045 ; 非零则跳到失败分支

当你看懂这个结构时,心里的“图景”应该是:程序读取用户输入、交给strcmp、再把返回值判断相等与否、通过跳转进入不同分支。后面一切操作就是从入口走到这个结点,把jnz改成jz,或者干脆把比较的两个操作数改成相同的值。

4. 动态调试实战:下断点、单步跟踪、修改跳转完成破解

4.1 把静态地址搬到动态调试器里

静态分析发现的地址在动态调试里往往不能直接用了,原因有PE加载机制、各类重定位、以及系统dll抢占内存区域。最稳妥的办法是“先加载,再定位”。用x64dbg打开样本,它会停在系统断点。我们先看静态分析中的main函数特征码,然后用“Ctrl+G”输入地址,前提是那个地址恰好是RVA地址。如果静态工具里显示的是文件偏移或者VA,你需要进行换算。

实际上,x64dbg里有一个更实用的功能:加载exe后,模块窗口会列出主模块的基址,比如0x00400000。静态分析工具里的地址如果是0x00401423,在x64dbg里一般也是同样地址,因为PE文件默认image base就是0x00400000。你可以直接跳到这个地址,看反汇编是否和静态工具里的一致。

经验提示:如果地址不一致,不要死磕地址换算,而是在x64dbg中搜索“Access Granted”字符串,双击到引用位置,再从引用位置向上翻一点,同样能定位到main区域。地址只是索引,代码特征才是真正的路标。

4.2 下断点、观察输入与strcmp返回值

进入x64dbg后,我们做以下四步:

  1. 在调用gets之后的下一行、或者在call check那行下断点(F2)。
  2. 按F9运行,程序停在断点处。在控制台输入任意内容,比如aaaaaa。
  3. 按F8单步执行,进入check函数内部,单步到call strcmp那一行。
  4. 当call strcmp执行完,观察EAX寄存器,它的值就是strcmp返回值。

如果输入是aaaaaa,预设密码是secret_key,EAX必然不等于0。此时来到下一个关键指令:对EAX进行判断的test eax, eax。再往下是jnz跳转到失败分支。这就是判决的“法官席”。

在同一次调试会话里,你可以再试一次输入secret_key,会看到EAX变成0,jnz不跳转,程序走向成功分支。这一对比,任何没接触过逆向的人都能理解:破解的本质就是把“非0也当成0”或“跳转逆转”。

4.3 修改指令的三种常见操作手法

动态调试里,最常用的三种修改方式:

  • 修改标志位寄存器:在jnz指令处,如果Z标志位为0,在x64dbg里可以通过右侧标志位面板单击ZF,把它从0改为1。改完单步会发现跳转被绕过了。
  • 修改机器码(patch字节):把jnz(机器码通常是0F 85或75)改成jz(0F 84或74)。在对应地址按空格键,输入jz short 目标地址,x64dbg自动帮你填好机器码。
  • 修改比较结果:直接改EAX为0,相当于欺骗判断条件。这种方式适合临时测试,不常做持久化修改。

持久化修改主要是patch字节。以本样本为例,如果关键跳转是短跳转指令75 0C,那么把75改成74即可。保存修改的方式是:x64dbg中选择需要修改的字节,右键-二进制-编辑,修改后右键-复制到可执行文件,再保存为一个新exe。

需要特别提醒:保存patch后的exe可能会引发文件校验或签名失效,但对学习样本无所谓。另外,如果样本里有校验自身hash的逻辑,单单改跳转是不够的,还得处理自校验。这个以后进阶再聊。

4.4 用一张表记录修改前后的对照

整理成对照表,方便以后调试时直接参考:

指令作用修改后效果常见机器码
jnz short addr不等于0则跳改为jz75→74
je short addr等于0则跳改为jne74→75
test eax, eax判断eax是否为0可把eax改为029 C0
cmp eax, 0比较eax与0可把第二个操作数改为083 F8 00
call strcmp字符串比较可改为xor eax, eax让返回值恒为0E8...

这张表里,call strcmp改为xor eax, eax是一种更恶趣味但直观的破解方式:根本不比较,直接让返回值为0,程序当然认为密码正确。这也是很多CTF里“一发入魂”的做法。

4.5 完整实操记录:从一个测试会话里看全过程

我简单记录一次真实调试过程,方便你对照。窗口右上角是寄存器面板,左边是反汇编。我在0x00401430处设断点,该地址指令是call check。按F9运行,控制台弹窗,输入whatever,回车,断点命中。此时栈顶返回地址、参数都在预期位置。按F8步入check函数,看到:

00401410 push ebp 00401411 mov ebp, esp 00401413 sub esp, 8 00401416 mov eax, [ebp+8] ; 输入字符串 00401419 mov [esp+4], eax ; 第二个参数 0040141D mov dword [esp], 00402030 ; 预设密码"secret_key" 00401424 call <strcmp> 00401429 add esp, 8 0040142C mov [ebp-4], eax 0040142F cmp dword [ebp-4], 0 00401433 jnz short 0040143F

我用F8单步到call <strcmp>之后,看到EAX=00000001,说明不相等。下一步执行cmp和jnz时,它会跳到失败分支。这时我直接右键jnz指令,选择“二进制->用NOP填充”,这样无论比较结果如何,程序都不会跳转,会顺着执行到成功分支。填充后按F9,控制台打出Access Granted。再看一眼代码,这个简单crackme就算被“破解”了。

5. 常见问题与排查技巧:这些坑我替你踩过

5.1 为什么下了断点却断不下来?

最常见的原因是地址不对。静态分析工具里看到的地址是文件映射地址,动态调试时程序可能因为ASLR换基址。排除方法是:在x64dbg加载exe后,看模块窗口里主模块的基址,如果模块基址是0x00400000,且静态工具里地址也是0x00400000 + offset,那就是一致的。如果模块基址变成0x003D0000,你就要对所有地址重定位。

第二个原因是符号问题。如果程序是带符号的(我们编译时用了-g),调试器里会能看到函数名;如果没带符号,只有地址。新手容易在“外部符号”列表里找main,却找不到,误以为程序有问题。实际上main是个符号,静态分析里你看到它,动态调试里可以通过字符串引用或者调用栈回溯来定位。

第三个原因是断点下在了正在执行的系统函数内部,比如ntdll或kernel32里,一按F9系统断点后系统dll也在跑,你如果随手F2会在系统函数里下了一堆杂乱断点,不停命中。初学者不用慌,点击调试菜单里的“删除所有断点”,重新在程序自己的代码区下断点。

5.2 为什么看不清逻辑?优化与strip后的应对策略

如果你没有按我建议的编译参数来,而是用了-O2并strip过,反汇编会变得很“滑”。典型表现是:没有push ebp; mov ebp, esp这种标准开场白,strcmp这行可能直接内联成了一段逐字节比较循环,check函数也可能直接被内联进main。

这时候再用“函数名定位”就失效了。你需要改成“行为定位”:找输入字符串在程序内部的使用位置。最笨也最有效的办法是动态调试:在gets或scanf对应的系统调用后,观察缓冲区地址,然后在内存窗口里跟随这个缓冲区,设置硬件访问断点,程序一旦读这个缓冲区就会断下来。硬件断点比普通INT3断点更难被检测,也是处理反调试时的一招。

另一种应对是在反汇编窗口里搜索字符串地址。即使strip了,程序中的字符串常量一般仍会存在.rdata段。通过字符串交叉引用,一样可以跳到使用该字符串的代码位置。这一点对任何二进制都适用。

5.3 遇到简单反调试怎么办?

入门样本通常没有反调试,但有些恶作剧程序会调用IsDebuggerPresent这样的API来检测自己是否被调试。如果你发现程序一用调试器运行就退出、或者运行逻辑异常,先用x64dbg的插件绕过并记录API调用,或者手动在调用IsDebuggerPresent后强制把返回值改为0。

更常规的办法是单步跟踪到可疑循环处,观察是否有奇偶数陷阱指令(比如int 3或rdtsc)。不过入门阶段不建议花太多时间在这里,先记住:在x64dbg中,如果遇到看起来像反调试的call,在调用之前看看可能的参数,或在调用后把EAX置0,大多能绕过去。深层的反调试,比如线程本地存储、时间校验、调试寄存器检查等,等后面脱壳专题再展开。

5.4 程序是64位,看汇编时要注意什么?

编译64位版本后,几点和32位差异很大:参数不是靠栈传递,而是按RCX, RDX, R8, R9传递(Windows x64调用约定)或RDI, RSI等(Linux/System V调用约定)。栈对齐也有要求。strcmp的返回值和判断思路不变,但反汇编里会少了很多push/pop,参数直接写寄存器,看着很“裸”。

有人问我刚入门是不是应该直接学64位?我的建议是:先用32位建立栈帧概念,再切到64位感受差异。如果你只学64位,很容易跳过堆栈参数传递的过程,后面理解缓冲区溢出时反而吃力。

以下是一个常见问题速查表:

现象可能原因排查方式
断点断不下来ASLR基址变化/符号缺失检查模块基址,用字符串引用定位
反汇编没有函数边界编译器优化/内联手动忽略边界,按行为定位
一调试就退出反调试检测搜索API调用,修改返回值
修改后程序无法运行字节修改错误/自校验复查checksum,观察修改范围

5.5 给新手的三个心态层面的建议

第一个建议是不要贪快。完整看一条cmp/jz指令的流向,比自己半小时“破”了十个程序更有价值。第二个建议是必须动笔记笔记。汇编指令又多又碎,把每条指令在具体程序里的作用记下来,一周后你会发现自己有了“指令直觉”。第三个建议是遇到卡壳就退回静态分析,不要死磕动态调试。动态调试就像在高速上打视频电话,随时可能爆,静态分析像看地图,先看地图再上路,效率才最高。

6. 破解学习之后的下一步:CTF、CrackMe与道德边界

6.1 到哪里去找更合适的学习样本?

学完这一篇,你可以去找两类练手资源。一类是CrackMe,这是逆向社区流传了二十多年的“破解小谜题”,通常体积小、逻辑简单,作者会故意设计各种密码校验或算法保护,非常适合练习。另一类是CTF(Capture The Flag)比赛中的Reverse题目。CTF反向题往往有时间限制和难度分级,可以从最低分题刷起。很多在线CTF平台都有历史题目和writeup,写完以后可以参考别人的思路,看自己有没有遗漏更好的解法。

我个人的经验是,每周至少做两道带crackme题的小程序。第一道用来做“快速热身”,第二道留出来做“深度分析”,分别锻炼速度和耐心。几个月下来,再到实际的软件逆向环境中,你看到汇编代码会像看母语一样自然。

6.2 一定要牢记的合法使用边界

逆向破解技术本身是双刃剑。我写这一篇的初衷,是希望帮你建立对程序底层执行逻辑的认知,方便日后做漏洞分析、恶意软件研究、历史软件维护或者CTF。但你手里这把“改跳转”的钥匙,绝应用在未经授权的软件上。

这里给出底线清单:只调试和分析自己写的程序、自己拥有权限的程序、明确授权做安全测试的程序;拿到软件先看用户协议和许可条款;不把破解器制作或传播给任何陌生人;不以“学习”名义分析盗版商业产品。在同行社区里分享逆向笔记时,也要脱敏,别直接公布某商业软件的具体漏洞和绕过方式,后果你可能担不起。

注意:我在文章中写的样本、地址、流程均为教学用途。如果你把同样的技术用在未经授权的地方,责任自负。这类话不是客套,而是这一行里真正重要的底线。

6.3 后续学习路径怎么走?

如果你已经把本文的样例完全吃透,接下来可以往四个方向延伸。

方向一是栈溢出缓冲区攻击。你会发现“修改程序执行流”不只是改跳转,还可以覆盖栈上的返回地址让程序跳转到任意位置。配合调试器,亲手复现一个溢出栈的例子,才算真正懂现代防御机制为什么存在。

方向二是壳与加壳。用加壳工具把程序包一层,再尝试脱壳后分析。脱壳的核心本质是研究程序自解密流程,和本文“找关键跳转”是一脉相承的。

方向三是汇编语言本身。花两周时间把常见指令过一遍,尤其要弄懂LEA、MOVZX、XOR的惯用法,它们频繁出现在反汇编代码里。你其实不需要背所有指令,但看到不认识的指令时,要学会通过调试器实验立即去验证它的行为。

方向四是其他架构。C程序不只在PC上跑,ARM、MIPS、RISC-V平台上的嵌入式设备逆向,近些年在物联网安全和工控安全领域需求巨大。如果你学有余力,找一个开源的小型RTOS编译出来的固件,尝试定位main和校验函数,会打开另一个世界。

我个人在实际学习中的体会是,逆向这门技能讲究“手感”。所谓的“手感”就是当你面对新程序时,能迅速猜到它哪里存在比较、哪里是关键的跳转、哪里值得下断点。这种手感没有捷径,只能靠一个个样本喂出来。希望这一篇能帮你迈出最扎实的第一步。下一篇如果想继续,我们可以讲讲如何用调试器写脚本自动化分析,或者专门拆解某个经典的CrackMe的算法层。你先拿这个小样本练手,练熟了我们再往下走。

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

从零配置深度学习环境:Anaconda、TensorFlow与PyCharm安装详解

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

作者头像 李华
网站建设 2026/10/17 10:03:31

蓝桥杯BASIC-12十六进制转八进制:大数进制转换的字符串分组解法

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

作者头像 李华
网站建设 2026/10/17 10:03:15

Markdown中LaTeX公式渲染问题排查与跨平台兼容写法

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

作者头像 李华
网站建设 2026/10/17 10:03:14

Zephyr内存池Memory Pools参数配置与踩坑实践:从k_mem_pool到sys_heap

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

作者头像 李华