news 2026/10/14 1:27:05

Linux x86_64汇编实现字符串比较:从CMPSB到REPE详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux x86_64汇编实现字符串比较:从CMPSB到REPE详解

上一讲把字符串的存储和寻址梳理完之后,字符串比较自然就成了下一个绕不开的话题。高级语言里一句strcmp就能搞定的事,落在 Linux x86 64 位汇编层面,本质是一连串标志位、指针移动和循环控制的选择。字符串比较在汇编里算“必修课”,因为它几乎覆盖了这个阶段最该吃透的机制:内存寻址、条件跳转、REP 类串指令,以及 SysV 调用约定下参数和返回值的传递。把这一篇啃下来,你再去看任何标准库的字符串实现都会顺畅很多。

这篇直接从 Linux x86 64 位 + GAS AT&T 语法出发,先拆解CMPSB、REPE前缀与标志位的底层逻辑,再一步一步实现自己的strcmp和memcmp,最后用 C 调用验证结果,再聊大小写不敏感比较、性能优化路径和典型调试场景。适合已经能写简单汇编函数、但还没系统接触过串指令的读者。

1. 字符串比较的技术全景与方案选型

1.1 为什么字符串比较在汇编里是“必修课”

很多人觉得字符串比较不过是“逐字节比对,遇到不同就返回”,但真实场景远比这层描述复杂。C 字符串没有显式长度信息,终止条件是\0,所以比较函数不能像memcmp那样拿到长度后一次性比完,必须边比较边检查是否到达结尾。单是“比较多长、什么时候停、停之后返回什么”,就足够把汇编的循环控制、标志位和内存访问逻辑全部串起来。

另一个原因是,字符串比较在底层项目里出现频率极高。解析命令行参数、查找配置文件中的键值、判断用户输入的命令,全都要做字符串比较。C 库帮你封装好了,但你手写汇编时没法调用库函数,或者说你理解了库的实现之后,才能知道它为什么快、为什么在某些极端情况下会有奇怪行为。这就像开车,你不需要会造发动机才能开,但懂原理之后遇到异响至少知道往哪个方向查。

1.2 从高级语言到汇编:三种常见比较函数的语义差异

动手之前,先把目标函数定义清楚。C 标准库有一套成体系的字符串比较接口,它们的语义差异决定了汇编实现逻辑完全不同:

函数比较终止条件返回值典型用途
strcmp(s1, s2)遇到\0或第一个不同字符小于 0 / 0 / 大于 0判断两个字符串是否相等、字典序排序
strncmp(s1, s2, n)比较到第 n 个字节,或遇到\0,或发现不同同上,最多比 n 字节前缀匹配、固定长度字段比较
memcmp(s1, s2, n)严格比较 n 个字节,不关心\0同上内存块比较、二进制数据比较

注意strcmp和memcmp的根本差别:前者以字符串语义为核心,\0是终止符;后者以内存语义为核心,\0只是一个普通字节。这直接导致汇编实现时循环条件不同。strncmp则是两者折中:既受长度限制,也要尊重字符串终止符。

标准库返回值不是简单的 0/1,而是第一个不同字符的差值。比如strcmp("abc", "abd")返回的是'c' - 'd',也就是负数。这一点很多新手会忽略,但如果你写排序函数、二分查找,返回值符号会直接影响比较逻辑。汇编实现里要做到这一点也不难,关键在于比较方向别搞反。

1.3 指令选型:逐字节循环、REPE 串指令还是 SIMD

方案选型决定了代码结构和性能上限。纯手写逐字节循环最简单直观,用movb+cmpb+je就能完成,适合讲清楚基础原理;REPE CMPSB是 x86 串指令体系的标准写法,一条指令隐式完成“比较 + 更新指针 + 计数”,代码量骤减,但理解门槛更高;SIMD 方案(SSE/AVX)是库函数真实性能的秘密,能一次比较 16 甚至 32 字节,代价是代码复杂度成倍上升。

我的建议是:初学阶段用逐字节循环把逻辑跑通,再改用REPE CMPSB做固定长度比较,等这两个版本都吃透了,再考虑性能优化。很多人一上来就追求 SSE 优化,结果连标志位都没摸清楚,调试时一头雾水。汇编学习最忌讳跳阶。

2. 核心指令与底层逻辑:CMPSB、REPE 与标志位

2.1 CMPSB 到底做了什么:隐含操作数与自动指针更新

CMPSB是“比较字符串字节”指令,它的精妙之处是没有显式操作数。这条指令隐式使用RSI和RDI两个寄存器:比较RSI指向的一个字节与RDI指向的一个字节,效果上等同于做了一次减法,并根据结果设置标志位。比较完成后,RSI与RDI自动递增或递减 1(CMPSW是 2,CMPSQ是 8)——递进方向由方向标志位DF决定。

生活里可以类比成两人对暗号:一个人负责从自己的纸条念一个字,另一个人负责从自己的纸条念一个字,CPU 当场判断两个字是否相同。相同就进入下一个字,不同就停下。CMPSB只负责“对比当前这一个字”,并不负责循环,循环交给前缀指令完成。

这里有个容易踩坑的语义细节:CMPSB比较方向是[RSI] - [RDI],也就是用RSI的值减去RDI的值。如果写成自定义strcmp,传参时s1放在RDI、s2放在RSI(这也是 SysV 调用约定的固定顺序),那减法方向和标准库想要的s1 - s2恰好相反。只判断相等时无所谓,但如果你要返回“差值”,就必须理解这个隐含方向,否则最终符号一定是反的。

2.2 REPE/REPNE 前缀与 ZF 标志的组合逻辑

REPE前缀能把单次CMPSB扩展成循环,全称是“Repeat While Equal”,也就是“只要相等就继续比较”。它和零标志位ZF直接相关:CMP类指令在两者相等时置ZF=1,不相等时置ZF=0。REPE的循环逻辑是:每次执行完CMPSB后检查ZF,如果ZF=1(相等)且RCX未减到 0,就继续下一次比较;一旦ZF=0(不相等)或RCX归零,循环立即终止。

与之配套的REPNE正好相反,它在ZF=0(不相等)时继续执行,用于“扫描直到找到某个不同字节”的场景,典型应用是REPNE SCASB查找字符串中的特定字符。理解这两个前缀的关键在于:前缀不是无脑重复,它是在“重复机制”上叠加了一个基于标志位的暂停条件。REPE关注“相等则继续”,所以用于验证两块数据是否完全相同;REPNE关注“不等则继续”,所以用于寻找差异点。

循环结束后,判断结果要分情况:如果RCX减到 0 结束,说明指定长度内所有字节都相等;如果ZF=0结束,说明比较过程发现了不同字节。很多初学者只检查是否相等,忽略了“为什么结束”对结果判断同样重要。

2.3 DF 方向标志与 CLD:最容易被忽略的经典坑

方向标志DF决定了串指令执行后指针是递增还是递减。执行CLD清除方向标志,指针向上增长(低地址到高地址);执行STD设置方向标志,指针向下递减。绝大对数代码期望的行为是从低地址向高地址依次比较,所以函数开头必须先写CLD。不写的后果很隐蔽:如果调用者之前执行过STD或者有其他代码意外设置了DF,你的比较顺序就是倒着的,轻则结果错乱,重则访问到错误内存直接崩溃。

实际上,按调用约定要求方向标志在函数进入和返回时都应该是清除状态,所以CLD不仅是“习惯”,更是合规代码的必要步骤。我在调试中见过不少次“明明逻辑没问题,字符串就是不相等”的场景,最后定位到就是缺了一句CLD,或者把CLD放在了循环之后而不是之前。

STD也不是完全没用。处理栈内存、从尾部向前扫描缓冲区、实现某些逆向遍历算法时,STD+ 递减串指令能把代码写得很紧凑。但日常字符串比较场景里,默认必须清除方向标志。

2.4 从 32 位到 64 位的演进注意点

很多系列教程早期都会讲 32 位汇编,到 64 位这儿习惯就得改。32 位时代参数字符串指针放在ESI/EDI,计数器放在ECX;64 位时代换成了RSI/RDI/RCX,地址宽度和计数范围都扩大到 64 位。串指令本身也多了 64 位版本,CMPSB比较 1 字节、CMPSW比较 2 字节、CMPSQ比较 8 字节,后者一次能比较一个 64 位寄存器宽度。

另外要注意 64 位模式下操作数默认宽度不是全都 64 位。像movb明确操作 1 字节、movl操作 4 字节、movq操作 8 字节,写错助记符会导致高位数据残留。拿movzbl扩展加载时,它会自动把高 32 位清零,这是 32 位指令在 64 位环境下的特殊行为。后面写代码时我会刻意利用这个特性,避免多余的清零指令。

3. 从零手写:完整的 strcmp 实现与逐行拆解

3.1 函数原型与 SysV 调用约定:参数进得来,结果出得去

先定接口。我们写一个自定义函数,在 C 里声明为:

int my_strcmp(const char *s1, const char *s2);

根据 Linux x86_64 的 SysV 调用约定,整数和指针参数按顺序放入RDI、RSI、RDX、RCX、R8、R9,返回值放入RAX。所以进入my_strcmp时,RDI就是s1的地址,RSI就是s2的地址。被调用者可以自由使用RAX、RCX、RDX、RSI、RDI等调用者保存寄存器,不需要恢复原值;而RBX、RBP、R12-R15属于被调用者保存寄存器,用了就得自己压栈保护。

这就是为什么很多入门汇编函数开头总写pushq %rbp和movq %rsp, %rbp——不是强制要求,而是为了建立栈帧,方便调试器回溯调用栈。你不用RBP也能写函数,但有帧指针能让 GDB 显示调用关系更清晰。后面的示例我会保留这种结构,但也会告诉你哪些部分可以省略。

3.2 逐字节循环版 strcmp:清晰但啰嗦的基线实现

先看完整代码,这是最直白、最适合跟读的版本:

# int my_strcmp(const char *rdi, const char *rsi) # 返回 0 表示两个字符串相同,返回 1 表示不相同 .text .globl my_strcmp .type my_strcmp, @function my_strcmp: pushq %rbp movq %rsp, %rbp cld xorl %eax, %eax xorl %ecx, %ecx # 用 ecx 作为偏移量 .Lloop: movzbl (%rdi,%rcx), %eax # 取 s1[i],同时清零高 32 位 movzbl (%rsi,%rcx), %edx # 取 s2[i] cmpb %dl, %al jne .Lnot_equal # 出现不同字符,直接判定不相等 testb %al, %al jz .Lequal # s1[i] 是 0,说明 s1 结束且之前没差异 incq %rcx jmp .Lloop .Lnot_equal: movl $1, %eax jmp .Ldone .Lequal: xorl %eax, %eax .Ldone: popq %rbp ret

逐行拆解一下关键点。xorl %ecx, %ecx把计数器清零,movzbl (%rdi,%rcx), %eax用RDI + RCX这种基址加变址的寻址方式取出s1[i]。这里特意用%eax而不是%al,是因为movzbl会把%eax的高位全部清零,避免上一轮留在寄存器里的脏数据影响比较。同理movzbl (%rsi,%rcx), %edx也扩展成 32 位。

cmpb %dl, %al本质上是%al - %dl,只关心结果是否为 0,所以jne就能判断两个字节是否相同。若相同且当前字节不是 0,继续递增偏移量循环;若%al为 0,说明s1走到了字符串结尾,并且一路下来所有字符都相同,由于前一步cmpb已经把s2[i]和 0 比较过了(不同会走.Lnot_equal),所以此时s2也必然在同位置结束,可以安全返回 0。

这个版本没有直接使用CMPSB,但它的逻辑是串指令的“白话版”。理解了这段,后面看REPE CMPSB就会觉得那不过是 CPU 把循环浓缩成了硬件操作。如果你希望返回标准库风格的差值,只需把cmpb之后的分支改成用movb+subb算出实际差值,再把符号扩展到 32 位返回即可。

3.3 固定长度 memcmp:REPE CMPSB 的极简写法

如果比较的不是以\0结尾的字符串,而是一段固定长度的二进制内存,写法就优雅多了:

# int my_memcmp(const void *rdi, const void *rsi, size_t rdx) # 比较 rdx 字节,返回 0 表示完全相同,返回 1 表示存在不同 .text .globl my_memcmp .type my_memcmp, @function my_memcmp: pushq %rbp movq %rsp, %rbp movq %rdx, %rcx # 把第三参数长度传给 rcx cld xorl %eax, %eax repe cmpsb # 只要相等就继续比较 je .Lsame movl $1, %eax jmp .Ldone .Lsame: xorl %eax, %eax .Ldone: popq %rbp ret

RDX是第三个参数,标准调用约定里长度就放在这里。把长度搬到RCX后,一句repe cmpsb就完成了整个循环。执行结束后,RCX是剩余未比较的字节数,RSI/RDI已经前进到停止位置,ZF标志则记录最后那次比较的结果。

判断相等的方式也简单:je .Lsame如果最后结果是相等则跳转。那如果是因为ZXC减到 0 结束的呢?RCX减到 0 意味着长度耗尽,此时最后一步比较的必然是最后一个字节,如果它不相等,ZF=0,je不成立;如果它相等,ZF=1,跳转成立。所以要判断“完全相等”,直接看ZF就够了,不依赖RCX。这正好印证了 2.2 里说的:结束原因不必手动区分,ZF状态就是最终结论。

这个版本比逐字节循环版简洁太多,但有个前提:你必须预先知道长度。对于 C 字符串,要先用strlen算出长度再比较,这就难免重复遍历字符串。所以REPE CMPSB最适合memcmp风格的场景,而不太适合直接做strcmp的替代品。

3.4 用 C 调用汇编函数:编译、链接与实测验证

代码写出来必须能跑,否则都是纸上谈兵。先写一个 C 测试文件:

#include <stdio.h> extern int my_strcmp(const char *s1, const char *s2); extern int my_memcmp(const void *p1, const void *p2, size_t len); int main(int argc, char **argv) { const char *a = "hello world"; const char *b = "hello world"; const char *c = "hello there"; printf("my_strcmp(a, b) = %d\n", my_strcmp(a, b)); printf("my_strcmp(a, c) = %d\n", my_strcmp(a, c)); printf("my_memcmp(a, b, 11) = %d\n", my_memcmp(a, b, 11)); printf("my_memcmp(a, c, 11) = %d\n", my_memcmp(a, c, 11)); return 0; }

编译命令:

gcc -c my_strcmp.s -o my_strcmp.o gcc -c test.c -o test.o gcc test.o my_strcmp.o -o test_prog ./test_prog

输出预期:

my_strcmp(a, b) = 0 my_strcmp(a, c) = 1 my_memcmp(a, b, 11) = 0 my_memcmp(a, c, 11) = 1

如果结果不对,先用gdb ./test_prog打断点,再stepi单步看寄存器状态。我会在第五章把典型的调试命令列出来。这里有个小提醒:当gcc编链接汇编.s文件时,默认把.s交给汇编器as,所以只要你写的语法没问题,它会直接生成目标文件,不需要手动as一遍。

4. 实战场景与性能优化路径

4.1 场景一:字符串数组去重与二分查找

学会strcmp不是终点,怎么在真实逻辑里用它才是重点。最简单的场景是实现字符串数组去重:两层循环遍历数组,外层固定当前元素,内层用my_strcmp检查是否已经在前面出现过。由于你的比较函数返回值是 0/1,判等逻辑非常自然:

const char *words[] = {"apple", "banana", "apple", "cherry"}; int seen[4] = {0}; for (int i = 0; i < 4; i++) { for (int j = 0; j < i; j++) { if (my_strcmp(words[i], words[j]) == 0) { seen[i] = 1; break; } } }

如果你坚持用汇编实现这些业务逻辑,my_strcmp就是你的基础组件。但如果只是想在 C 项目里用汇编优化某个瓶颈,直接拿自定义函数替换标准库strcmp并不明智——标准库往往针对你的 CPU 微架构做了充分优化。更合理的姿势是:先用标准库跑通业务,性能分析发现确实是字符串比较成为热点时,再考虑 SIMD 自研方案。

4.2 场景二:大小写不敏感比较的实现思路

现实需求里字符串比较经常需要忽略大小写,例如配置文件里的开关值、用户名匹配。大小写不敏感比较不能简单调用my_strcmp,你得先对两个字符分别做归一化处理再比较。

汇编里处理 ASCII 大小写转换有固定套路:判断字符是否落在'a'-'z'区间,如果是则减 32 得到大写字母(因为小写字母 ASCII 码比大写大 32)。用逐字节循环改造 3.2 的版本,在cmpb之前插入两小段转换逻辑即可。例如对%al:

subb $'a', %al cmpb $26, %al # 判断是否 a-z jae .Lnot_alpha # 超出字母范围 addb $'A', %al # 转成大写

当然这只覆盖了英文场景。真正的大小写不敏感比较还要考虑本地化字符集、UTF-8 编码等问题,复杂度远超汇编教程范畴。做业务系统时,优先用库函数;自己做协议解析、格式判断时,ASCII 归一化方案通常足够了。

4.3 从逐字节到 SIMD:一次比较 16 字节的秘密

普通逐字节循环一次只比较 1 字节,性能瓶颈明显。REPE CMPSB虽然代码简洁,但很多 CPU 微架构上它的吞吐率并不比展开的手写循环快,有时甚至更慢。库函数性能的真正来源是 SIMD 指令。

SSE2 里组合使用movdqu(非对齐 16 字节加载)和pcmpeqb(逐字节相等比较),一次就能比完 16 字节;再用pmovmskb把 16 个比较结果压缩成 16 位掩码,通过掩码是否为0xFFFF判断全部相等。核心逻辑伪代码如下:

movdqu (%rdi), %xmm0 movdqu (%rsi), %xmm1 pcmpeqb %xmm1, %xmm0 # 每个字节相等则置 0xFF pmovmskb %xmm0, %eax # 结果压缩成 16 位掩码 cmpl $0xFFFF, %eax je .Lnext_block

相比一次一字节,这相当于 16 路并行比较,而且不涉及分支跳转(除非发现差异)。对于长字符串,还能用movdqu+pcmpistri(SSE4.2 字符串指令)直接在硬件里做strcmp语义。不过 SIMD 对内存对齐敏感,movdqa要求 16 字节对齐,movdqu则容忍非对齐但可能略慢;比较长字符串时还要处理尾部不足 16 字节的剩余部分。这个优化路径很有价值,但建议把它放在下一篇单独展开,这里先知道“还有这条路”即可。

4.4 对齐、缓存行与分支预测:实际性能差异来源

很多人以为优化就是换指令集,忽略了内存访问模型。字符串比较是典型的内存密集型操作,主要开销往往不是 CPU 算术,而是把数据从内存搬进缓存的延迟。所以优化优先级是:能少读内存就少读,能顺序读就顺序读,能在比较前预取就预取。

逐字节循环最糟糕的情况是比较两个长字符串,前 99% 都相等,最后才发现不同。此时你浪费了大量内存带宽,而且每条cmpb背后的分支预测器很难学习到稳定模式——差异出现的位置不确定,分支模式几乎随机。这也是为什么库函数里常用“块比较”策略:先按 8 字节或 16 字节一组快速扫描,发现不相等再退回去按字节定位具体差异位置。这样既利用了宽位比较的吞吐,又避开了 SIMD 尾部处理的复杂度。

实际工程里还有另一个优化维度:先比长度再比内容。如果两个字符串长度不同,strcmp结果必然非 0,直接返回;长度相同再做逐块比较。很多二分查找和哈希表实现里还会缓存字符串哈希值,哈希不同直接判不相等,彻底省去内存访问。这一点对高频比较场景尤其有效。

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

5.1 问题一:忘掉 CLD 导致方向错乱

现象可能原因解决办法
两个明显相同的字符串比较结果不相等调用方或本函数执行过STD,DF=1,CMPSB指针递减函数开头强制cld,不要依赖“默认应该是清除状态”
程序访问到不期望的内存段导致崩溃DF=1时指针向低地址移动,可能越过缓冲区起始位置在每条串指令之前明确设置方向标志

调试这类问题时,用 GDB 观察RSI/RDI的变化方向是最直接的。如果发现它们在一个“向前比较”的逻辑里反而递减,那DF必然错了。

5.2 问题二:REPE CMPSB 后误判 RCX 的语义

REPE CMPSB结束后,RCX表示剩余未比较的字节数。如果你一开始给RCX赋了 100,比较在第 30 字节发现不同,那结束时RCX是 70。我见过有人据此写逻辑:if (rcx == 0)判断相等,这是错的。两个长度完全相等的字符串全相同,结束RCX确实为 0;但一个较短的字符串也可能因为\0提前让CMPSB在某字节发现不同,RCX同样不为 0,但结果不一定应该判“不相等”。在这种语义里你必须结合ZF判断:ZF=1才是最终相等。拿RCX判断相等只适用于提前给RCX精确设定为字符串长度的场景。

5.3 问题三:NULL 终止字符串的越界读取

REPE CMPSB不感知\0,它只知道比较你指定的字节数。如果拿它比较两个 C 字符串,却把RCX设置成一个很大的值(比如0xFFFFFFFF),两个完全相同的字符串会让RSI/RDI一路扫过整个字符串区域,直到RCX归零或触碰不可读内存页。这正是不能在strcmp里直接用REPE CMPSB的核心原因。

要避免越界,必须事先知道安全比较长度。要么先用REPE SCASB扫描出两个字符串的长度,取其中较小值作为比较长度;要么老老实实逐字节循环,在循环内检测\0。不要拿“反正汇编里啥都能读”的心态去处理这类问题。

5.4 问题四:寄存器保护违规

SysV 调用约定里RBX、RBP、R12-R15是被调用者保存寄存器。如果函数里用了它们却不压栈保护,返回后调用者手里这些寄存器的值就全部被破坏。典型的坑是拿RBX做循环计数器——因为RBX在调用者眼里是长期变量,你在函数里悄悄改掉,回到 C 层后可能立刻出现字段错乱。

解决办法有两个:优先使用RAX、RCX、RDX、RSI、RDI这些调用者保存寄存器;只有不够用才考虑RBX,并在函数入口pushq %rbx、出口popq %rbx。写汇编函数时养成一个习惯:动手前先列一份“本函数用到哪些寄存器”清单,对照调用约定分清哪些该保存。

5.5 调试技巧:用 GDB 单步观察指针与标志位

字符串比较的调试,核心是观察三样东西:RSI/RDI指向的字符串内容、RCX剩余计数、RFLAGS里的ZF。常用命令如下:

(gdb) break my_strcmp (gdb) run (gdb) info registers rdi rsi rcx (gdb) x/s $rdi (gdb) x/s $rsi (gdb) stepi (gdb) info registers eflags

stepi一条指令一条指令走,每步都看看RSI、RDI是否按预期递增,RFLAGS里的ZF是否符合当前字符是否相等的直觉判断。字符串比较这种逻辑,数据是可见的,逐步对照就能在几秒钟内发现问题。如果发现RSI/RDI没动,那多半是串指令没触发成功;如果动了但方向反了,就看方向标志;如果结果错了但寄存器和标志位都对,回头检查你的比较方向是不是真按标准库语义写对了。

我个人在实际调试中还有一个习惯:先用 C 标准库strcmp得到正确结果,再用自定义汇编函数跑同一组输入,两者结果不一致时才逐步缩小范围。这样能快速区分“是汇编实现本身错了”还是“外部调用时序有问题”。比较函数这种基础组件,一旦出错会污染上层所有逻辑,宁可多花两小时做对照测试,也不要在发现异常时靠猜。

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

这一篇的代码只是一个开始。理解了CMPSB和REPE机制之后,字符串处理相关的其他串指令都是一通百通:SCASB用于在字符串中查找特定字符,LODSB/STOSB用于字符串搬运和填充,把它们组合起来就能手写strchr、strstr、strlen等一整套基础库函数。性能要求高的场景,再把 SSE4.2 的pcmpistri加进来,体验一下“一条指令完成一个字符串函数”的威力。探索这些扩展之前,先把今天这篇里的标志位逻辑和寄存器约定彻底吃透,因为它们才是所有串指令的共同底座。

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

三路电流采样零点校准与电流换算的工程实操指南

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

作者头像 李华
网站建设 2026/10/14 1:26:30

Linux内核slub_debug内存调试原理与实战指南

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

作者头像 李华
网站建设 2026/10/14 1:26:25

基于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/14 1:26:17

RT-Thread自动初始化机制:链接段与函数指针的启动解耦实践

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

作者头像 李华
网站建设 2026/10/14 1:26:14

IBM DS8000电池故障处理:从告警定位到热插拔实操

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

作者头像 李华
网站建设 2026/10/14 1:25:51

ESP32固件安全:Secure Boot与Flash Encryption实战指南

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

作者头像 李华