汇编入门实战:从CPUID小程序理解寄存器与系统调用
1. 从一个获取CPUID的小程序说起1.1 为什么选CPUID作为汇编入门案例很多人学汇编一上来就被各种寄存器、寻址方式、指令集给劝退了。我当初也是抱着厚厚一本指令手册啃了三天结果连个Hello World都写不利索。后来一位做底层开发的朋友跟我说别急着啃手册先找一个能跑起来的小程序把它拆碎了理解比看一百页理论都管用。他当时给我的就是这个获取CPUID的程序。为什么选它因为CPUID这条指令本身就很有意思——它是CPU提供给软件的一扇窗口通过它你能直接问CPU你是谁你支持什么特性而实现这个问答过程只需要用到汇编里最核心的几个概念寄存器传参、指令调用、结果存储、系统调用输出。麻雀虽小五脏俱全。CPUID指令的基本用法是把功能号放进EAX寄存器执行CPUID指令然后CPU会把结果分别放到EAX、EBX、ECX、EDX四个寄存器里。比如EAX0时返回的是厂商ID字符串EAX1时返回的是处理器版本信息和特性标志位。整个过程不涉及内存分配、不涉及复杂算法就是纯粹的寄存器操作特别适合用来理解汇编的工作方式。这个程序能做什么简单说它能在Linux环境下运行打印出你的CPU厂商信息、型号、支持的特性标志。适合谁看如果你已经会一门高级语言C、Python都行想了解程序在底层到底怎么跑的或者你正在学操作系统、编译原理需要一点汇编基础来打通任督二脉那这个案例就是为你准备的。哪怕你之前完全没碰过汇编跟着走一遍也能有个直观感受。1.2 先搞清楚ATT汇编和Intel汇编的区别在动手写代码之前有个坑必须先填上ATT汇编和Intel汇编是两种不同的语法风格写错了编译器直接报错而且报错信息往往让人摸不着头脑。最直观的区别是操作数顺序。Intel风格是目标在前源在后比如mov eax, 1意思是把1放到eax里。ATT风格反过来是源在前目标在后同样的操作写成movl $1, %eax。我第一次切换的时候因为顺序搞反程序跑出来的结果完全不对排查了半天才发现是操作数顺序的问题。另一个显著区别是寄存器前面要加百分号%立即数前面要加美元符号$。Intel风格里直接写eax和1ATT风格必须写成%eax和$1。这个规则没有例外忘了加符号编译器会直接报错。还有指令后缀的问题。ATT汇编里指令后面要跟一个字母表示操作数大小b表示字节8位w表示字16位l表示双字32位q表示四字64位。比如movl表示移动32位数据movq表示移动64位数据。Intel汇编通常靠寄存器名来推断大小ATT则要求你显式写出来。对比项Intel风格ATT风格操作数顺序目标, 源源, 目标寄存器前缀无%立即数前缀无$指令后缀通常无b/w/l/q内存寻址[baseindex*scaledisp]disp(base,index,scale)我个人的建议是如果你主要看Linux内核源码或者GCC生成的汇编那就老老实实学ATT风格因为这是Unix世界的普通话。如果你更习惯Windows下的开发环境Intel风格可能更顺手。但不管选哪个先把一种搞熟另一种看懂就行别两种混着写那是自找麻烦。2. 程序整体设计与思路拆解2.1 程序要完成哪些事情这个获取CPUID的程序逻辑上分四步走第一步调用CPUID指令拿到厂商ID字符串第二步把拿到的数据保存到内存缓冲区第三步把缓冲区的内容打印到终端第四步正常退出程序。听起来简单但每一步都涉及汇编的核心概念。第一步考验的是对CPUID指令的理解——功能号怎么传、结果放在哪。第二步考验的是内存寻址——怎么把寄存器里的数据写到内存里。第三步考验的是系统调用——Linux下怎么输出字符串。第四步考验的是程序退出机制。我之所以强调这个四步拆解是因为很多初学者一上来就想写完整程序结果写到一半卡住了不知道哪出了问题。把步骤拆开每一步单独验证出错了也容易定位。比如你可以先写个只调用CPUID然后退出的程序用调试器看看寄存器里的值对不对确认无误后再加输出功能。2.2 为什么用系统调用而不是C库函数你可能会问为什么不用printf多方便啊。原因很简单用printf就需要链接C库程序的体积会变大而且你得理解调用约定、栈对齐这些额外概念。用系统调用syscall虽然原始一点但胜在直接——你直接告诉内核我要输出这段文字内核就帮你输出中间没有黑盒。Linux下输出字符串用的是write系统调用编号是1x86_64架构下。调用方式是rax放系统调用号rdi放文件描述符1表示标准输出rsi放字符串地址rdx放字符串长度然后执行syscall指令。退出程序用的是exit系统调用编号是60rdi放退出码。这种寄存器传参syscall指令的模式是Linux下汇编程序与内核交互的标准方式。理解了这一套你就能自己写各种系统调用的小工具了比如读文件、创建进程、发送信号等等。2.3 内存布局的规划程序需要一块内存来存放CPUID返回的厂商ID字符串。厂商ID是12个字节比如GenuineIntel就是12个字符加上换行符和可能的结束符我分配了16个字节的缓冲区。在汇编里分配内存有两种方式一种是用.bss段声明未初始化的空间一种是用.data段声明已初始化的数据。对于缓冲区用.bss段更合适因为不需要初始值。声明方式是.section .bss .lcomm buffer, 16.lcomm表示本地公共符号分配16字节的零初始化空间。这块内存在程序加载时由内核分配程序运行时可以直接读写。注意缓冲区大小一定要留足余量。CPUID返回的厂商ID是12字节但如果你后续想扩展程序比如加上型号字符串的输出缓冲区就要相应扩大。我见过有人只分配了12字节结果写入换行符的时候越界了程序直接崩溃。3. 核心细节解析与实操要点3.1 CPUID指令的详细用法CPUID指令的完整语义是这样的执行前EAX寄存器存放功能号也叫leaf号。执行后EAX、EBX、ECX、EDX四个寄存器分别存放返回结果。不同的功能号返回不同的信息。功能号0返回的是厂商ID字符串和最大功能号。厂商ID字符串的存放方式是EBX放前4个字节EDX放中间4个字节ECX放后4个字节。注意这个顺序不是按EBX、ECX、EDX排的而是EBX、EDX、ECX。我第一次写的时候按常规顺序排结果打印出来是乱码后来查手册才发现这个反直觉的顺序。功能号1返回的是处理器版本信息和特性标志位。EAX的高4位是家族号低4位是型号EBX的高8位是品牌索引低8位是CLFLUSH行大小ECX和EDX是各种特性标志位比如是否支持SSE、AVX等。功能号0x80000000返回扩展功能号的最大值功能号0x80000002到0x80000004返回处理器品牌字符串。如果你想获取完整的CPU型号名称需要依次调用这三个功能号每次拿16个字节拼起来就是48字节的品牌字符串。# 获取厂商ID的代码片段 movl $0, %eax # 功能号0 cpuid # 执行CPUID # 此时EBX、EDX、ECX中存放厂商ID实操心得CPUID指令会破坏EAX、EBX、ECX、EDX四个寄存器的值。如果你在调用CPUID之前有其他数据存在这些寄存器里一定要先保存到栈上或内存里。我当初写一个综合程序的时候忘了保存EBX结果后面用到的变量全乱了排查了很久才找到原因。3.2 寄存器使用与保存策略在x86_64架构下寄存器分为调用者保存caller-saved和被调用者保存callee-saved两类。调用者保存的寄存器有RAX、RCX、RDX、RSI、RDI、R8-R11被调用者保存的有RBX、RBP、R12-R15。CPUID指令会修改RAX、RBX、RCX、RDX。其中RBX是被调用者保存寄存器按照约定如果你在函数中使用了RBX应该先把它压栈保存函数返回前再恢复。虽然我们这个程序是直接写的没有函数调用的概念但养成好习惯很重要。我的做法是在程序开头把RBX压栈程序结束前弹栈恢复。这样即使后续扩展程序加入了函数调用也不会因为RBX被破坏而出问题。pushq %rbx # 保存RBX # ... 程序主体 ... popq %rbx # 恢复RBX另外CPUID返回的厂商ID分散在三个寄存器里需要分别写入内存。写入的时候要注意顺序EBX的值写到缓冲区偏移0处EDX的值写到偏移4处ECX的值写到偏移8处。这个顺序不能错错了打印出来就是乱码。3.3 系统调用的参数传递Linux x86_64的系统调用约定是这样的系统调用号放在RAX参数依次放在RDI、RSI、RDX、R10、R8、R9然后执行syscall指令。返回值放在RAX。write系统调用的参数是RDI文件描述符RSI缓冲区地址RDX要写入的字节数。exit系统调用的参数是RDI退出码。这里有个细节write系统调用返回的是实际写入的字节数如果出错返回负值。我们这个程序比较简单不检查返回值但实际项目中最好检查一下确认数据真的写出去了。# write系统调用示例 movq $1, %rax # 系统调用号1 write movq $1, %rdi # 文件描述符1 stdout movq $buffer, %rsi # 缓冲区地址 movq $13, %rdx # 写入13字节12字节厂商ID 1字节换行 syscall注意缓冲区地址要用$buffer这种形式表示取buffer的地址。如果写成buffer那就是取buffer里的内容结果完全不对。这个坑我踩过程序编译通过但运行没输出后来用调试器一看RSI里放的是0因为buffer初始值是0。3.4 数据输出格式的处理CPUID返回的厂商ID是12个字节的ASCII字符没有结束符。直接输出的话终端上会显示12个字符然后紧接着下一行内容看起来不整齐。所以我在缓冲区第12个字节处放了一个换行符ASCII码10输出的时候写13个字节这样打印完厂商ID后会自动换行。如果你想输出更友好的格式比如CPU Vendor: GenuineIntel那就需要先输出提示字符串再输出厂商ID。提示字符串可以放在.data段里用.ascii或.asciz声明。.section .data prompt: .ascii CPU Vendor: prompt_len . - prompt这里prompt_len . - prompt是汇编里常用的计算字符串长度的技巧。.表示当前地址减去字符串起始地址就是字符串长度。这种方法比手动数字符个数靠谱多了改字符串的时候不用同步改长度。4. 完整实操过程与核心环节实现4.1 开发环境准备我用的环境是Linux x86_64工具链是GNU asgas和ld。如果你用的是其他Unix-like系统工具链名字可能略有不同但基本用法一致。检查工具是否齐全as --version ld --version如果显示版本信息就说明装好了。没装的话在Debian系上用apt install binutils在Red Hat系上用yum install binutils。我建议再装一个调试器gdb或者lldb都行。汇编程序出错的时候光看代码很难定位问题用调试器单步执行、查看寄存器值效率会高很多。gdb --version实操心得写汇编的时候我习惯把编译和链接写成一个Makefile每次改完代码直接make就行不用手动敲两遍命令。虽然这个程序很小但养成自动化的习惯对后续写大程序有帮助。4.2 完整代码逐段解析下面是完整的程序代码我按段拆开讲解。.section .data # 这里暂时没有需要初始化的数据.data段存放已初始化的全局变量。这个程序暂时不需要但保留这个段声明是个好习惯后续扩展的时候直接在这里加数据就行。.section .bss .lcomm buffer, 16.bss段存放未初始化的数据。.lcomm buffer, 16分配16字节的本地公共空间标签是buffer。这块空间在程序加载时被清零。.section .text .global _start.text段存放代码。.global _start声明_start为全局符号链接器会把它作为程序入口点。_start: # 第一步调用CPUID获取厂商ID movl $0, %eax cpuid把0放入EAX执行CPUID。执行后EBX、EDX、ECX中存放厂商ID字符串。# 第二步把结果写入缓冲区 movl %ebx, buffer movl %edx, buffer4 movl %ecx, buffer8把EBX的值写到buffer偏移0处EDX的值写到偏移4处ECX的值写到偏移8处。注意这里用的是movl因为每个寄存器是32位正好4字节。# 第三步在末尾添加换行符 movb $10, buffer12把ASCII码10换行符写到buffer偏移12处。movb表示移动一个字节。# 第四步调用write系统调用输出 movq $1, %rax movq $1, %rdi movq $buffer, %rsi movq $13, %rdx syscall系统调用号1write文件描述符1stdout缓冲区地址buffer写入13字节12字节厂商ID 1字节换行。# 第五步调用exit系统调用退出 movq $60, %rax movq $0, %rdi syscall系统调用号60exit退出码0。4.3 编译、链接与运行把上面的代码保存为cpuid.s然后执行as -o cpuid.o cpuid.s ld -o cpuid cpuid.o ./cpuid如果一切正常你会看到类似这样的输出GenuineIntel或者AuthenticAMD这取决于你的CPU厂商。注意如果你在64位系统上编译但用了32位的指令可能会报错。比如movl %ebx, buffer在64位下buffer的地址是64位的但movl只移动32位数据地址会被截断。正确的做法是用movq或者确保buffer在低4GB地址空间内。我当初就因为这个原因程序编译通过但运行时段错误后来改成movl %ebx, buffer(%rip)才解决。4.4 用调试器验证每一步程序跑通了不代表你理解了每一步。我建议用gdb单步执行一遍看看每个寄存器在每个步骤后的值。gdb ./cpuid (gdb) break _start (gdb) run (gdb) stepi (gdb) info registersstepi是单步执行一条指令info registers查看所有寄存器的值。执行完CPUID后看看EBX、EDX、ECX里是不是ASCII字符。比如EBX里应该是Genu0x756e6547EDX里是ineI0x49656e69ECX里是ntel0x6c65746e。这种验证方式比盯着代码看有效得多。我当初就是通过调试器才发现厂商ID的存放顺序是EBX、EDX、ECX而不是我以为的EBX、ECX、EDX。5. 常见问题与排查技巧实录5.1 编译链接报错速查错误信息可能原因解决方法undefined symbol: _start没有声明.global _start在代码开头加.global _startinvalid instruction suffix指令后缀写错检查操作数大小补上b/w/l/qjunk at end of line注释符号用错ATT汇编用#注释不是//或;relocation truncated to fit地址超出范围用(%rip)相对寻址或改用64位指令segmentation fault内存访问越界检查缓冲区大小和写入偏移5.2 运行结果不对的排查思路如果程序能跑但输出是乱码按以下顺序排查第一确认CPUID的功能号是否正确。功能号0返回厂商ID功能号1返回版本信息。如果你把功能号搞错了拿到的数据自然不对。第二确认寄存器到缓冲区的写入顺序。EBX写偏移0EDX写偏移4ECX写偏移8。这个顺序是Intel手册规定的不能改。第三确认输出长度。厂商ID是12字节加上换行符是13字节。如果你写12字节换行符不会输出写14字节会多输出一个字节的垃圾数据。第四用调试器查看缓冲区内容。在write系统调用之前设断点用x/16xb buffer查看缓冲区里的原始字节确认是不是ASCII字符。实操心得我排查这类问题的时候习惯先把输出重定向到文件然后用xxd或od查看十六进制内容。终端有时候会把控制字符显示成乱码看不出问题。用十六进制查看器就一目了然了。5.3 跨平台兼容性注意事项这个程序是在Linux x86_64下写的换到其他平台需要调整。比如在32位Linux下系统调用号不同write是4exit是1寄存器名也不同EAX而不是RAX。在ARM架构下指令集完全不一样CPUID指令可能不存在需要用其他方式获取CPU信息。如果你想让程序在多个平台下都能编译可以用条件汇编#ifdef __x86_64__ # 64位代码 #else # 32位代码 #endif不过对于初学者来说我建议先把一个平台搞熟别一开始就追求跨平台。把x86_64下的CPUID程序吃透再去看其他架构的对应实现会容易很多。5.4 扩展练习获取更多CPU信息掌握了基本框架后可以尝试扩展程序获取更多CPU信息。比如调用功能号1获取处理器版本和特性标志位调用功能号0x80000002到0x80000004获取完整的品牌字符串解析特性标志位判断CPU是否支持SSE、AVX等指令集这些扩展练习能帮你巩固寄存器操作、内存寻址、位运算等汇编核心技能。我当初就是通过不断扩展这个程序慢慢把汇编的常用指令都摸熟了。最后分享一个小技巧写汇编的时候别怕代码长。高级语言里一行代码能搞定的事汇编里可能要写十行。这是正常的不是你的问题。关键是每一步都要清楚自己在干什么寄存器里放的是什么内存里存的是什么。想不清楚就画图把寄存器和内存的状态画出来比在脑子里空想靠谱得多。

相关新闻

数据库事务原理与实战:隔离级别、MVCC与底层日志全解析

数据库事务原理与实战:隔离级别、MVCC与底层日志全解析

写后端代码这些年,“事务”这两个字我听了不下几百遍,但真正把它从概念变成肌肉记忆,是从一次线上资损事故开始的。那次问题非常简单:两个账户之间做金额划拨,更新余额的 SQL 执行到一半,数据库连接断了&am…

2026/10/16 7:36:11 阅读更多 →
OpenClaw定时系统设计:让AI从被动响应走向时间感知与自主执行

OpenClaw定时系统设计:让AI从被动响应走向时间感知与自主执行

AI 不该等你说话才干活——OpenClaw 定时系统设计哲学几周前我在调试一套基于 OpenClaw 的智能体编排系统,突然意识到一个很别扭的事情:我的 AI 明明有很强的任务执行能力,但每次都得等我先开口,它才动一下。我说“查一下天气”&a…

2026/10/16 7:36:11 阅读更多 →
安卓模拟器抓包环境搭建:HTTPS证书与代理配置全攻略

安卓模拟器抓包环境搭建:HTTPS证书与代理配置全攻略

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

2026/10/16 7:35:10 阅读更多 →

最新新闻

2026 GEO服务商TOP5榜单:品牌如何成为AI答案中的首选信源

2026 GEO服务商TOP5榜单:品牌如何成为AI答案中的首选信源

前两年大家还在讨论"做SEO还有没有用",今年风向已经变了——用户开始直接问AI工具要答案。搜完不再翻十页链接,而是等AI给出一段综合回复、附带几个引用来源。品牌能不能出现在这段回复里,就是数字营销的下一个分水岭。GEO&#xf…

2026/10/16 8:14:22 阅读更多 →
哈夫曼编码与数据压缩:从最优二叉树到工程实践

哈夫曼编码与数据压缩:从最优二叉树到工程实践

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

2026/10/16 8:14:22 阅读更多 →
智能体是什么?从原理到搭建,一篇讲透AI Agent核心机制

智能体是什么?从原理到搭建,一篇讲透AI Agent核心机制

这段时间后台私信里问得最多的问题之一,就是“智能体到底是什么”。这个词最近出现频率高得离谱,好像所有AI产品一夜之间都改名叫“Agent”了。但真问起来,很多人自己也说不清智能体和聊天机器人有啥区别。有人说智能体就是能自己干活的AI&am…

2026/10/16 8:14:22 阅读更多 →
!医生转行AI产品经理/算法岗,高薪新机遇等你来探索!

!医生转行AI产品经理/算法岗,高薪新机遇等你来探索!

本文探讨了AI在医疗行业的应用,以及医生如何通过转行成为AI产品经理或算法工程师。文章分享了两位医生转行成功的案例,阐述了AI产品经理的工作内容与医院工作的区别,分析了薪资待遇与职业发展前景,并给出了适合尝试此类岗位的人群…

2026/10/16 8:14:22 阅读更多 →
智能体安全防线:OpenClaw提示注入与权限控制实战加固

智能体安全防线:OpenClaw提示注入与权限控制实战加固

1. 论文核心问题与现实背景1.1 一句话戳破的痛点:你的智能体,怎么会变成别人的资产第一次看到这篇论文标题的时候,我愣了一下。“Your Agent, Their Asset”,这个双关用得挺妙——你辛辛苦苦搭出来的智能体,在某个瞬间…

2026/10/16 8:14:22 阅读更多 →
Python列表批量删除全攻略:从遍历陷阱到性能优化

Python列表批量删除全攻略:从遍历陷阱到性能优化

说到 Python 里对列表做批量删除,很多人的第一反应是直接写一个 for 循环,在循环里调用 remove 或者 del。这个思路本身很自然,但一跑起来就会发现问题:被删除元素后面的数据悄悄被跳过,甚至整个程序直接抛出索引越界的…

2026/10/16 8:13:21 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

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

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/15 9:49:14 阅读更多 →