1. 先从一次设备自我介绍说起PCI解决了什么问题1.1 跳线时代为什么早期硬件工程师那么痛苦很多刚接触Linux内核的人会觉得PCI驱动框架是一大堆难啃的结构体和注册函数看不见摸不着。但如果你把它放到历史里看会发现这套框架解决的是一个非常朴素的问题如何让操作系统自动、可靠地识别一台硬件设备并给它分配好干活时需要的地址和中断资源。在PCI出现之前的年代ISA总线时代设备识别基本靠人工。你要在ISA卡上手动拨跳线开关来设置I/O端口地址、中断号每个设备还得分清楚谁用哪个地址、哪个中断稍微冲突一点机器就开不了机。我当时在实验室修过一台老工控机为了给两块采集卡避开中断冲突前前后后折腾了一个下午。那种痛苦现在的开发者很难体会。PCI的出现彻底改变了这个局面。它的核心理念概括成一句话设备插上总线后必须能够自我介绍并且允许软件来给它安排资源。这奠定了整个即插即用Plug and Play时代的基础也让后来的Linux内核能够用一种统一、公式化的方式去管理五花八门的硬件。1.2 配置空间一张挂在总线上的简历表PCI设备为什么能自我介绍因为每一块PCI设备内部都有一块独立的存储区域叫配置空间Configuration Space。标准PCI设备的配置空间是256字节PCIe设备扩展到了4096字节。包含的内容大概有字段位置/格式含义Vendor ID0x00~0x01厂商IDPCI-SIG统一分配比如0x8086是IntelDevice ID0x02~0x03设备型号ID由厂商自己定义Class Code0x09~0x0B设备类别比如网卡、显卡、存储控制器BAR0~BAR50x10~0x24六个基址寄存器描述设备需要的内存或I/O地址空间Interrupt Line/Pin0x3C~0x3D中断号和中断引脚的分配结果Command/Status0x04~0x06控制字控制I/O空间、内存空间使能等你可以把配置空间想象成一张贴在设备外面的简历表。内核不需要猜这个设备是什么、能干吗、需要多大空间只需要按固定偏移把这张表读出来。读到Vendor ID是0xFFFFFFFF说明这个位置不存在设备就跳过继续往下扫描。这个机制简单到令人发指但正是这种简单让软件枚举设备的行为变得完全可预测。1.3 BDF与扫描内核怎么知道设备在哪接下来是寻址问题。系统里可能挂着很多PCI设备内核该怎么描述某一块设备它沿用了一个延续到今天的规则BDF三元组即Bus总线号、Device设备号、Function功能号。标准PCI最多支持256条总线每条总线上最多32个设备一个设备又可以分出最多8个功能因此BDF就是domain:bus:slot.func这样的格式。比如0000:01:00.0表示域0、总线1、槽位0、功能0。你用lspci命令看到的所有设备都有一套这样的坐标。有了坐标扫描就有章法了。内核从总线0开始对每一个Device、每一个Function去读取配置空间头上的Vendor ID和Device ID读出来是有效值就创建一个struct pci_dev对象挂进设备模型。整个扫描过程本质上就是一次深度优先的探路——就像在一个大型仓库里打开一个个柜门清点里面的存货。这里想提醒一点配置空间的访问有两种方式早期x86用I/O端口0xCF8/0xCFC做配置读写PCIe时代更推荐通过内存映射的配置域ECAM访问但对于驱动开发者来说这两种方式通常已经被PCI核心层封装好了不需要亲自关心。你只需要理解读完配置空间设备的基本信息就已经进了内核。2. 内核里的三层分工PCI核心、设备模型和驱动代码各管什么2.1 三个角色和各自的边界很多初学者一上来就盯着struct pci_driver看却搞不清它跟struct pci_bus、struct pci_dev、总线模型之间的关系。其实这套框架在内核里是分层的每层只干自己的活。最底下那层是PCI核心层drivers/pci/。它负责的事情包括总线枚举、配置空间读写、资源分配、电源管理、热插拔、以及把设备和驱动撮合到一起。这一层的代码绝大多数情况下我们不会去改只是调用它提供的接口。中间层是内核的设备模型driver model。Linux用一套统一的device/bus/driver机制来管理所有设备PCI子系统只是这套机制的一个具体实现。pci_bus_type就是PCI系统在设备模型体系里的总线类型它记录了匹配规则、热插拔方法、电源管理回调等统一操作。换句话说PCI驱动的绑定、解绑最终依赖的其实是通用设备模型那套机制。最上一层才是我们写的具体驱动代码。一个PCI设备驱动要做的无非就是三件事向框架申报我能处理哪些设备、在设备到来时初始化它、在设备离开时清理现场。至于设备是怎么被发现的、匹配算法是怎么跑的那都是框架的事。这种分层带来的好处很明显内核里几百种PCI设备驱动代码风格高度一致。通用逻辑永远不会重复造轮子具体差异全部收敛在驱动自己那一亩三分地里。2.2 枚举时发生了什么核心动作拆解PCI核心层的启动枚举流程大体可以描述成这么几步系统启动早期PCI层通过平台相关代码x86上是pcibios_init拿到PCI域的根总线集合然后调用pci_scan_root_buses()对每条根总线做扫描。扫描过程中每发现一个新设备就分配并初始化一个struct pci_dev并为它申请资源。如果发现这个设备是PCI桥Bridge就会递归创建新的总线对象然后继续往桥下面扫描。扫描结束后内核进入资源分配阶段根据每个设备在BAR里声明的空间需求结合BIOS/固件已经分配好的资源做合入或重新分配。这就是后面说的pci resources问题的高发点。最后所有pci_dev都会被注册进设备模型触发与驱动进行匹配。你在dmesg里看到的pci 0000:01:00.0: [8086:153b] type 00 class 0x020000这样的日志就是枚举阶段留下来的足迹。所以我说看懂枚举日志比看一百遍框架图更解馋。2.3 资源谈判BAR、地址映射和中断协商枚举只是识别真正让设备能用起来的是下一件事资源分配。每块PCI设备的BARBase Address Register基址寄存器记录的是我想要一段多长的地址空间。驱动设备要去使用这段空间必须让内核完成两次动作一是分配/确认物理地址二是在CPU的虚拟地址空间里建立映射。物理地址这一层通常由BIOS或内核PCI核心在启动时做好驱动直接用pci_resource_start()就能拿到BAR对应的物理地址用pci_resource_len()拿到长度。但在某些自制主板上BIOS留给PCI的地址空间不足BAR拿到的地址是无效的这就会引发我们后面要讲的一个经典问题insufficient pci resources detected。虚拟映射这一层就需要驱动自己动手了。拿到物理地址后要用ioremap()或者PCI核心封装的pci_ioremap_bar()把它映射到内核虚拟地址空间之后才能像访问普通内存一样读写设备的寄存器。中断这一块PCI从传统的INTx共享中断线进化到MSI/MSI-X消息中断。对驱动来说打开MSI的方式很固定调用pci_alloc_irq_vectors()申请指定数量的中断向量。PCIe时代MSI基本是必选项因为它在性能和多队列场景下比传统INTx强太多。3. 驱动开发真正绕不开的两个结构体pci_driver和pci_dev3.1 pci_driver一张决定你被谁录用的应聘登记表struct pci_driver是每个PCI驱动的门面。它定义在include/linux/pci.h里核心字段包括struct pci_driver { const char *name; const struct pci_device_id *id_table; int (*probe)(struct pci_dev *dev, const struct pci_device_id *id); void (*remove)(struct pci_dev *dev); int (*suspend)(struct pci_dev *dev, pm_message_t state); int (*resume)(struct pci_dev *dev); // ... };你可以把它类比成一张应聘登记表name是你的姓名id_table是你声明自己会修哪些型号的设备probe是录用通知到达后的入职流程remove是离职流程。内核设备模型的匹配机制会拿系统里的每一个pci_dev去跟所有pci_driver的id_table比对一旦多个字段对上了就调用对应驱动的probe。id_table的写法有讲究。最常用的是PCI_DEVICE(vendor, device)宏精确匹配vendor和device两个ID。也有PCI_DEVICE_CLASS(class, mask)这种按设备类别匹配的写法适合一个驱动管一大类设备的场景。匹配时PCI核心会依次遍历表项直到最后一个全零表项结束。所以表项顺序不是随便排的越特殊的匹配要放越前面。3.2 pci_dev设备从被扫出来到停止服务的完整一生struct pci_dev是整个PCI框架里最关键的对象。每发现一个PCI设备内核就分配一个pci_dev。它记录的信息非常多vendor/device/subsystem_vendor/subsystem_device设备身份信息。bus/devfn它挂在哪条总线、哪个槽位和功能号。resource[]最多6个BAR对应的资源描述符即数组类型是struct resource。irq设备当前使用的中断号。msi_enabled/msix_enabledMSI/MSI-X启用状态。这个对象的生命周期是枚举时创建并注册匹配成功后调用probe驱动通过pci_set_drvdata()把它跟驱动私有数据关联起来设备移除时调用remove最后注销释放。这里有一个我特别想强调的细节设备模型驱动框架里设备和驱动是两套独立的对象probe是这两套对象建立绑定关系的仪式。所以你在probe里千万不要假设设备一定还在——设备随时可能因为热插拔、电源管理或错误被移除。做任何异步操作时想着remove随时会来。3.3 操作硬件资源的标准姿势BAR映射与地址访问驱动拿到pci_dev之后下一步就是把门打开。常规操作序列如下// 1. 启用PCI设备使能内存空间、I/O空间、总线主控等 ret pci_enable_device(dev); // 2. 获取BAR0的物理地址和长度 unsigned long addr pci_resource_start(dev, 0); unsigned long len pci_resource_len(dev, 0); // 3. 建立内核虚拟地址映射 void __iomem *base pci_ioremap_bar(dev, 0);第2步拿到的pci_resource_start()是物理地址绝对不能直接当成内核指针去访问。必须经过第3步的ioremap得到的是一个__iomem修饰的指针之后用readl/writel系列函数访问。为什么不能直接访问因为这是设备的寄存器空间不是普通RAM直接解引用轻则碰上缓存一致性问题重则直接触发总线错误系统死给你看。需要特别注意pci_resource_start(dev, 0)拿到的物理地址有可能是0。造成0的原因多半是设备的BAR没有被正确的分配地址空间也就是我们前面埋下的伏笔后面详细说。4. 手写一个最小PCI驱动骨架理解probe到remove的完整流程4.1 先贴完整代码再看每个部件的职责理论说了这么多我们来落地一套可用的最小骨架。假设设备厂商ID是0x1234设备ID是0x5678驱动代码如下#include linux/module.h #include linux/pci.h static int my_pci_probe(struct pci_dev *dev, const struct pci_device_id *id) { struct resource *res; void __iomem *bar0; int ret; dev_info(dev-dev, probe my device, vendor0x%04x device0x%04x\n, dev-vendor, dev-device); /* 第1步启用设备这一步会实际去设置PCI Command寄存器的相关位 */ ret pci_enable_device(dev); if (ret) { dev_err(dev-dev, failed to enable device, err%d\n, ret); return ret; } /* 第2步检查并使用BAR0资源 */ res dev-resource[0]; if (res-flags IORESOURCE_MEM) { bar0 pci_ioremap_bar(dev, 0); if (!bar0) { dev_err(dev-dev, failed to map BAR0\n); pci_disable_device(dev); return -ENOMEM; } dev_info(dev-dev, BAR0 mapped at %p, len%llu\n, bar0, (unsigned long long)resource_size(res)); /* 这里可以读一读设备版本寄存器、状态寄存器做初始化 */ } /* 第3步申请中断用MSI-X失败退化为MSI或INTx */ ret pci_alloc_irq_vectors(dev, 1, 1, PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_INTX); if (ret 0) { dev_err(dev-dev, failed to alloc irq vectors\n); goto err_unmap; } /* 第4步为这个设备保存私有上下文方便remove时取用 */ pci_set_drvdata(dev, bar0); dev_info(dev-dev, probe done\n); return 0; err_unmap: iounmap(bar0); pci_disable_device(dev); return ret; } static void my_pci_remove(struct pci_dev *dev) { void __iomem *bar0 pci_get_drvdata(dev); dev_info(dev-dev, remove called\n); /* 释放中断向量与pci_alloc_irq_vectors对应 */ pci_free_irq_vectors(dev); /* 解除虚拟地址映射 */ if (bar0) iounmap(bar0); /* 关闭设备 */ pci_disable_device(dev); } static const struct pci_device_id my_pci_id_table[] { { PCI_DEVICE(0x1234, 0x5678) }, { 0 } }; MODULE_DEVICE_TABLE(pci, my_pci_id_table); static struct pci_driver my_pci_driver { .name my_pci_dev, .id_table my_pci_id_table, .probe my_pci_probe, .remove my_pci_remove, }; module_pci_driver(my_pci_driver); MODULE_LICENSE(GPL);这里说一下module_pci_driver这个宏。它替你把pci_register_driver和pci_unregister_driver分别放进了模块初始化和退出函数里。如果你需要更精细的控制比如模块加载时要做一些PCI之外的事情也可以不用这个宏改成手动写module_init/module_exit函数在初始化函数里先做别的事最后再调用pci_register_driver()。4.2 probe到底做了什么四步走让我们拆开probe的四个动作解释每一个为什么必须做以及如果省掉的后果。启用设备。pci_enable_device()初始化PCI Command寄存器使能内存空间和I/O空间的访问。有些刚入行的朋友写驱动时没调用它结果读BAR0地址时要么读到全F要么直接总线错误。这一步并不是可选项它是设备允许你访问的握手信号。获取并映射资源。先检查BAR对应的resource是内存资源还是I/O资源判断方式看res-flags IORESOURCE_MEM。然后pci_ioremap_bar()把物理地址映射到内核虚拟地址空间。映射完成后你面对的bar0就是一个可以直接用readl/writel访问的地址了。申请中断向量。pci_alloc_irq_vectors()的设计比较现代你指定需要的向量个数系统自动在MSI-X、MSI、INTx之间选择可用方案。这在老代码里是看不到的老驱动通常直接request_irq(dev-irq, ...)靠dev-irq硬编码的中断号。新写法更灵活特别是支持多队列设备时可以申请多个中断向量分散到不同CPU核心处理实现纵向扩展。保存私有数据。pci_set_drvdata(dev, bar0)把一个指针挂到dev-driver_data上。这个指针通常是设备私有结构体、寄存器映射基址等。remove时再通过pci_get_drvdata(dev)取回来。这算得上驱动设计的记账技巧probe和remove不共享全局变量所有状态都跟着设备走天然支持多实例。4.3 remove和probe是倒影关系顺序绝不能乱remove执行时机有几种驱动模块被卸载、设备热拔、系统关闭。它的原则是把probe的每一步操作按完全相反的次序撤销。probe里申请了中断向量、建立了ioremap映射、启用了设备remove就必须先释放中断向量、解除映射、关闭设备。这个顺序为什么不能乱举个例子如果你在iounmap()之前先调用了pci_disable_device()而你对设备的寄存器映射还留在页表里理论上后面的读访问已经指向了一个被关停的设备在部分硬件上会触发非预期行为。更常见的问题发生在中断侧如果工作在remove里先关闭了设备电源但设备的中断处理函数还在跑设备马上难看地产生一次悬空中断导致处理器被无意义打断。还有一个早期容易漏的点probe失败分支里的清理。我在示例代码里写了err_unmap标签原因就是现实中probe经常会走到一半失败你必须保证失败时把已经申请的资源全部释放掉。否则模块加载失败后BAR映射残留、设备仍然处于enable状态下一次probe又是一团乱麻。这点对热插拔场景尤其致命我曾经见过一块PCIe卡拔插两次之后整个内核直接panic原因就是probe失败路径没有释放干净。5. 这套框架下我踩过的坑以及排查套路5.1 probe根本没被调用先查这三个地方这类问题几乎人人都会遇到一次。代码编译加载成功dmesg里却没有你的probe日志。排查思路按优先级排id_table匹配问题。用lspci -n查看设备真实的vendor/device ID跟你的表对比。常见错误包括宏参数顺序写反、id_table没有以全零项结尾。还有一个隐蔽点如果设备是PCIe桥下游的设备桥枚举正常但下游设备ID被报告的subsystem ID干扰需要用PCI_DEVICE_SUB做更精细匹配。驱动有没有注册成功。模块加载后可以先ls /sys/bus/pci/drivers/my_pci_dev/看不到目录说明pci_register_driver根本没执行成功或者模块没有真正装载。设备有没有被内核发现。输入lspci -tv看设备树如果设备压根不在列表里说明问题出在枚举阶段跟驱动无关。这种情况下先检查硬件、插槽、BIOS里的PCI配置不要折腾驱动代码。我做驱动时遇到probe不调用的情况八成是前两步但建议还是要严格按以上顺序排查别一上来就怀疑设备模型。5.2 ioremap没问题但一访问就崩溃核心症状是pci_ioremap_bar()返回了非空指针但readl(bar0)一执行要么读到全0或全1要么直接触发Oops。原因通常出在物理地址这一环。首先要看pci_resource_start(dev, 0)返回了什么东西。如果返回0大概率是设备枚举完成后BAR没有被合法分配。这种情况的典型背景是BIOS没有为设备预留地址窗口而内核在枚举时又因为种种原因没有重新分配。解决办法可以尝试给内核加启动参数pcirealloc强制重新分配资源同时在BIOS设置里打开PCI相关的地址窗选项特别是64位地址、Above 4G Decoding这类功能。其次检查是不是访问了错误的地址宽度。有些设备的BAR是64位的它需要占用连续的两个BAR槽位驱动里如果只映射了BAR0映射的只是低32位地址得用pci_ioremap_bar(dev, 0)同时处理dev-resource[0]和dev-resource[1]的长度取二者之和才是完整的BAR空间。踩过这个坑的人看资源长度时一定会多留个心眼。5.3 中断始终不触发中断问题在PCI驱动里非常经典一个设备的中断涉及到硬件是否拉起了中断、中断控制器是否正确路由、内核的IRQ子系统是否把它分发到了正确处理器。站在驱动角度高频元凶有这么几个没有启用设备的中断寄存器。很多PCI设备在probe后不会默认打开中断使能需要在初始化代码里写它自己的中断状态寄存器。很多新手以为pci_alloc_irq_vectors成功就万事大吉其实那只是让PCIe的消息能发出去设备本身的中断源还得单独打开。MSI使用条件没满足。有些老设备声称支持MSI实际固件有bug申请MSI向量之后中断乱套。遇到这种情况先用pci_alloc_irq_vectors(dev, 1, 1, PCI_IRQ_INTX)强制走INTx验证是否是MSI的锅。共享中断的IRQF_SHARED问题。传统INTx支持共享中断线一个中断线被多个设备共享。你申请中断时必须带IRQF_SHARED中断处理函数里还要先判断中断是不是我的设备发出的不是就返回IRQ_NONE。很多驱动编写者在处理共享中断时忘了读设备的中断状态寄存器来确认归属导致大量的无效中断处理甚至影响其他设备。我处理过一个实际案例一块网卡驱动加载后一个CPU核心的软中断占用率飙到100%排查下来就是共享中断处理函数里少了一次设备中断状态判断每次网卡来了中断系统其他设备的中断处理也跟着全跑一遍性能当然崩。5.4 资源不足insufficient pci resources与解决方向热搜词里出现的 error: insufficient pci resources detected!!! pci out of resources condition真的非常典型。这个信息通常出现在系统启动早期位置在PCI资源分配阶段含义直观明了总有设备在BAR里声明的地址空间BIOS和内核给不出足够大的有效地址窗口来装下它们。触发场景多数是主板BIOS地址分配策略保守、PCIe域过大挂了很多桥和端点设备、或者大家闺秀级的GPU/高速存储设备要的BAR空间动辄几GB甚至几十GB但BIOS默认没开Above 4G Decoding导致4GB以上的地址窗口不可用设备资源只能往4GB以下的窗口挤。排查和解决的方向我在实际工作中验证过几个有效的路子进BIOS打开Above 4G Decoding。很多消费级主板默认关这个选项但市面上大部分独立显卡、NVMe转接卡都需要64位BAR空间不开就等着资源冲突。加内核启动参数pcirealloc必要时配合pciassign-busses。让内核在BIOS分配的基础上重新梳理一遍资源能有效缓解一部分分配不上的问题。检查IOMMU和ACS相关配置。如果开启了IOMMU某些情况下IOMMU页表占用的地址空间会挤占PCI资源窗口这时候可以考虑调整IOMMU配置或者使用iommupt做透传测试。实在不行禁用掉不用的集成PCI设备。在内核里屏蔽未使用设备不能解决问题过滤器反而会让设备树变化导致资源重新分配路径改变一般不太推荐。这类问题严格来说不是驱动代码的错误但作为驱动开发者你要能看懂它因为如果你在probe里检查pci_resource_len()返回0而dmesg尾部躺着这么一句资源不足日志你就不会误判成驱动的问题而是直接去查BIOS和启动参数。5.5 模块卸载崩溃一切源于不对称最后一个高频事故出现在rmmod时系统直接Oops。原因几乎都是remove和probe不对称。典型表现释放了中断但没有释放中断处理函数中用到的数据区iounmap之后中断处理函数里还在访问那个地址全局变量里保存了旧设备的指针但所有设备都已经remove了指针变成悬空。这类问题的根源还是一个核心认知PCI驱动的绑定单位是设备不是模块。一个模块加载后可能对应多个同型号设备它们各自的probe/remove是交错发生的全局变量一旦保存设备上下文就无法正确处理多设备场景。正确做法是始终使用pci_set_drvdatapci_get_drvdata把上下文绑定到具体设备上这既是内核的推荐模式也是规避卸载崩溃最稳妥的方式。最后说点我个人的体会。做Linux PCI驱动真正的入门捷径不是捧着内核源码从头读到尾而是先用lspci -vvv把手上真实设备的配置空间全部读一遍再对照struct pci_dev的字段定义去理解每一段信息落在哪个结构体里。当你把配置空间里的一串十六进制和内核里一个结构体的某个字段对应起来之后整个PCI驱动框架对你来说就不再是抽象概念而是一张可以随手翻阅的地图了。后续再遇到中断控制器、IOMMU、热插拔这些外围话题你也会自然知道它们是在哪一层、跟谁打交道。希望这篇分析能帮你跨过第一道门槛系列后面的文章我们再继续往深处挖。
企业数字化 ERP 产品动态
相关推荐
Windows本地调试Spark/Hive必备:winutils配置指南 简介:针对Windows环境下调试Hadoop时的本机组件缺失问题,winutils-master.zip集中提供了2.6.0至3.0.0多个版本的winutils.exe与hadoop.dll等配套文件,适合需要在本地IDE中连接或调试Hadoop集群的开发者。包体共收录275个文件,按版… · 2026/9/25 14:45:31
Sigmoid激活函数深度解析:从数学推导到工程实践 1. 从一条公式说起:Sigmoid凭什么成为机器学习的“第一课”如果你翻过任何一本机器学习入门教材,不管是周志华的《机器学习》还是吴恩达的公开课讲义,Sigmoid函数几乎都是你遇到的第一个激活函数。它长得不复杂:f(x) 1 / (1 e^(… · 2026/9/25 14:45:24
免费降AI工具为什么用几次就收费?还有哪些工具可以免费改论文? 免费降AI工具为什么用几次就收费?还有哪些工具可以免费改论文?
第一次能免费改,后来却提示购买,未必是操作错了,可能只是试用字数已经用完。还想免费处理论文,可以把DeepSeek用于分析和人工修改࿰… · 2026/9/25 16:27:00
保研绩点规则盲区,家长提前摸清评定细则,别拖孩子保研脚步 更新时间2026年8月29本篇文章给你讲清楚四件事:
1、保研对绩点到底有什么要求2、大学哪些情况容易拉低绩点
3、哪几件事是家长具体可以做的
4、保研准备的常见误区有哪些
中间会用绩满满的公开数据作参考,文末附家长常问的问题一、保研对绩点到底有什么要… · 2026/9/25 16:27:00
Agent技能体系设计:从工具封装到工作流编排的实践指南 2. 核心技能设计思路2.1 技能的粒度与边界原子技能:解决单一问题的最小功能单元,比如"发送邮件""查询天气""计算表达式",典型特征是输入输出明确、无副作用、可以独立验证。原子技能是整个技能体系的基石&… · 2026/9/25 16:26:48
Shell变量与字符串操作从入门到避坑:赋值、引用、截取、替换与比较全解析 1. 为什么值得花时间把Shell变量和字符串吃透很多人第一次写Shell脚本,都是从一行echo "hello world"开始的。写完之后觉得“不过如此”,然后真正上手干活时,立刻被各种问题打脸:变量赋值多了空格报错、字符串里带空格被… · 2026/9/25 16:26:48
CTF 密碼學實戰:MD5 雜湊演算法的特徵識別、碰撞破解與安全評估(ctf-wiki) 文档网络安全教程 【免费下载链接】ctf-wiki Come and join us, we need you! 项目地址: https://gitcode.com/gh_mirrors/ct/ctf-wiki 点击查看 免费下载 MD5 是 CTF 密碼學賽題中最常出現的雜湊(Hash)演算法之一,本指南以 ctf-… · 2026/9/25 16:26:04
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37