简介围绕某东webpack方式h5st逆向的实战代码包面向具备一定前端与爬虫基础、希望理解h5st参数生成与加密链路的开发者也可作为安全测试与逆向分析的学习案例。资源共2个文件包含1个Python脚本和1个JavaScript脚本压缩包约159KB前者便于串联请求与结果验证后者侧重前端逻辑复现与webpack模块定位两者可配合对照阅读。已有860人学习。读者可借助完整代码分析h5st模板的加载方式、模块组织与关键参数生成过程梳理从页面调试、网络请求分析到Python侧复现的排错思路并理解webpack打包代码的阅读与拆解方法对定位签名参数、还原调用链和验证请求结果有参考价值。内容强调技术研究与合规边界适合用于学习逆向工程基本方法、提升前端代码分析能力不建议用于非法用途。1. 为什么这类 webpack 签名参数值得单独拆一遍前阵子帮朋友看一个抓取任务页面数据在浏览器里一切正常把请求原样搬到 Node 里跑服务端回的内容却是空的——问题就出在请求头里那个几十位长的签名字段。这个项目是围绕某商城 H5 页面做的签名参数逆向分析实践用 webpack 打包产物作为切入点把混淆过的 JS 还原成可以在本地调用的函数。它适合两类人一类是把爬虫当正经工程做、迟早要面对参数签名问题的开发者另一类是前端同学想搞清楚自己每天打包出来的 bundle 到底能被人看到什么。这篇笔记按「产物长什么样 → 怎么定位 → 怎么补环境跑起来 → 怎么防止下次改版翻车」的顺序把这套流程完整走一遍。所有代码只针对学习场景和自有系统的调试分析。2. 打包产物解剖从__webpack_require__里找到签名入口打开页面加载的那个几百 KB 的 JS 文件第一反应通常是懵的一层套一层的立即执行函数变量名全是a、b、c字符串散落成数组。但只要确认它是 webpack 打的结构其实是固定的认出来之后剩下的就是体力活。2.1 runtime 那几十行到底在干什么webpack 打包后的文件头部通常是一小段「运行时」它的作用是定义模块注册和加载机制。压缩之后长这样// webpack 5 生产环境 runtime 的典型形态已简化 (() { use strict; var e {}; // 模块缓存key 是模块 id function r(t) { // 这就是 __webpack_require__ var n e[t]; if (void 0 ! n) return n.exports; var o e[t] { exports: {} }; // 先占位防止循环依赖死递归 return modules[t].call(o.exports, o, o.exports, r), o.exports; } r.m modules; // 模块定义表通常是数组或对象 r.d (t, n) { // 定义 getter 形式的导出 for (var o in n) r.o(n, o) !r.o(t, o) Object.defineProperty(t, o, { enumerable: !0, get: n[o] }); }; r.o (t, n) Object.prototype.hasOwnProperty.call(t, n); r(r.s 0); // 从入口模块 id 开始执行 })();这段代码里只有三个东西需要记住。r就是__webpack_require__负责按 id 取模块e是缓存对象同一个模块只会被执行一次r.m是模块定义表里面每一项都是一个函数参数是(module, exports, require)跟 Node 的 CommonJS 包装函数一个思路。参数t是模块 id压缩后一般是数字未压缩可能是./src/utils/sign.js这样的路径。认结构的意义在于你不需要读懂全部业务代码只要拿到r这个引用就能像调库一样把任意模块的导出取出来。这是后面所有工作的地基。2.2 模块 id 与依赖链顺着 require 往上爬知道了r的作用下一步是找到「哪个模块负责算签名」。压缩后的文件里搜不到函数名但模块之间的调用关系还在。常见的做法是从某个已知的入口往下追比如页面初始化时调用的主模块r(r.s 0)或者从 XHR 断点拿到调用栈看栈顶函数属于哪个模块。模块 id 有两种常见形态。数字 id 下模块表是个数组r.m[12]就是第 12 个模块字符串 id 下模块表是对象键名通常保留了一点路径信息像./src/request/index.js。后者对分析友好得多很多团队为了排查线上问题会开启optimization.moduleIds: named这算是个意外之喜。依赖链的读法是模块 A 的包装函数体里出现r(12)说明 A 依赖 12 号模块。你可以写个小脚本扫描整个文件用正则抓出所有r(\d)或者r(...)的调用建一张依赖图然后看哪几个模块被 HTTP 请求模块依赖、同时又依赖了加密相关的模块。我一般会先把「被请求模块依赖的模块」列出来范围一下就能缩到个位数。提示不要指望一次就定位准。签名逻辑经常被拆成三四个模块——参数排序一个、时间戳一个、加密一个、拼装一个追的时候要有耐心。2.3 用 hook 抓模块导出比肉眼找快十倍肉眼读代码容易漏更省事的办法是在运行时劫持r把所有模块的导出记录下来。因为r在 runtime 闭包里外部拿不到所以要在文件加载前动手把 JS 文件下载到本地在 runtime 末尾r(r.s 0)之前插入一段代码把r挂到全局上。// 插入到 runtime 中 r(r.s0) 之前把 require 暴露到全局 window.__wp_require__ r; // 再包一层记录每个模块的导出情况 const rawRequire window.__wp_require__; const cacheLog new Map(); window.__wp_require__ function (id) { const exported rawRequire(id); if (!cacheLog.has(id)) { cacheLog.set(id, exported); // 只打有函数导出的模块减少噪音 const keys Object.keys(exported || {}); const fnKeys keys.filter(k typeof exported[k] function); if (fnKeys.length) { console.log([module ${id}], fnKeys.join(, )); } } return exported; };这段代码做了两件事先把原始r存到全局变量再用一个包装函数替换它。包装函数调用原始r拿到导出结果首次遇到某个模块时打印它的函数型属性名——Object.keys会把r.d定义的 getter 也列出来所以能直接看到sign、encrypt、getToken这类语义化的名字。参数id就是模块 idcacheLog用来去重避免同一个模块刷屏。实际跑起来之后控制台会哗哗输出一串模块名。这时候再去触发一次页面上的数据请求看最后几行输出大概率就能锁定目标模块。拿到模块 id 之后window.__wp_require__(目标id)就能直接把签名函数取出来调用不需要再管它内部怎么组织的。注意hook 要放在业务代码执行之前。如果你的页面用了懒加载分包还要考虑在 chunk 加载时重复这个动作否则会漏掉后加载的模块。3. 定位与还原关键字、AST 与断点三条路拿到模块只是开始里面的代码往往还被字符串数组、控制流平坦化包了几层。这一步的目标是把「人能读的代码」还原出来方便确认输入输出格式。3.1 关键字定位法与 XHR 断点最快的路子是搜关键字。签名参数最终要拼进请求头或 URL所以搜请求头的字段名、搜setRequestHeader、搜sign、搜时间戳格式化函数的名字都能命中。压缩代码里字符串常常被抽到数组里直接搜可能搜不到明文这时候改搜 Unicode 转义或者数组下标也行但更靠谱的还是断点。XHR 断点的操作是开发者工具 Source 面板里勾上XHR/fetch Breakpoints填上请求 URL 的一个片段页面发请求时就会停在send那一行。然后看 Call Stack从send往上翻两三层一般就能看到签名函数被调用的位置。如果栈里全是压缩后的短名字点开每一层看它引用了哪些模块 id再回模块表里对就能定位。我习惯两条路一起走先搜关键字缩小范围再用断点确认。关键字负责「猜」断点负责「验」单靠哪一条都容易走偏。3.2 字符串数组还原一个能直接抄的 babel 脚本webpack 本身不会做字符串混淆但很多项目会叠加一层javascript-obfuscator之类的工具把所有字符串抽到一个大数组里通过一个偏移函数取值// 混淆后的典型形态 var _0x1a2b [sign, timestamp, abcdef, getTime]; function _0x3c4d(i) { i i - 0x0; // 偏移量混淆器随机生成 return _0x1a2b[i]; } // 业务代码里变成 _0x3c4d(0x0) _0x3c4d(0x1)还原思路是找到那个数组和取值函数用 Babel 遍历 AST把所有_0x3c4d(数字)形式的调用替换成字符串字面量。脚本大概长这样// deobfuscate.js —— 基于 babel/parser 和 babel/traverse const fs require(fs); const parser require(babel/parser); const traverse require(babel/traverse).default; const generate require(babel/generator).default; const code fs.readFileSync(./bundle.js, utf-8); const ast parser.parse(code, { sourceType: script }); // 第一遍收集字符串数组和取值函数名 const stringArrays new Map(); let decoderName null; traverse(ast, { VariableDeclarator(path) { const { id, init } path.node; // 匹配 var _0x1a2b [..., ...]; if ( init init.type ArrayExpression init.elements.length 10 init.elements.every(e e.type StringLiteral) ) { stringArrays.set(id.name, init.elements.map(e e.value)); } }, FunctionDeclaration(path) { const name path.node.id path.node.id.name; const body path.node.body.body; // 匹配 return _0x1a2b[i - 0x0] 这种单语句返回 if ( name body.length 1 body[0].type ReturnStatement body[0].argument.type MemberExpression ) { decoderName name; } } }); // 第二遍把解码函数调用替换成字面量 if (decoderName stringArrays.size) { const arr [...stringArrays.values()][0]; traverse(ast, { CallExpression(path) { if (path.node.callee.name ! decoderName) return; const arg path.node.arguments[0]; if (arg.type ! NumericLiteral) return; const value arr[arg.value]; // 偏移量需要按实际情况修正 if (typeof value string) { path.replaceWith({ type: StringLiteral, value }); } } }); } fs.writeFileSync(./bundle.decoded.js, generate(ast).code);脚本分两遍遍历第一遍扫变量声明收集字符串数组同时找「函数体只有一条 return 语句」的解码函数第二遍把所有对解码函数的调用替换成对应的字符串字面量。参数上sourceType: script是因为打包产物是普通脚本不是 ES modulearr[arg.value]里的下标需要根据实际的偏移表达式调整如果原代码是i - 0x10就要改成arr[arg.value - 16]。跑完之后代码可读性会明显上一个台阶至少函数名和字段名都能看见了。控制流平坦化那层更麻烦常见做法是用 Babel 把switch-case的分发逻辑展开成顺序语句但这一块每个混淆器生成的结构都不一样没有通用脚本只能看情况手写规则。3.3 控制流平坦化先跑通再还原面对平坦化的代码我的建议是别急着还原。平坦化的目的是让你读不懂但不影响执行——只要环境补对了这段代码在 Node 里一样能跑。所以顺序应该是先让它跑起来验证输入输出正确如果需要长期维护再回头做 AST 还原。判断要不要还原的标准很实际如果签名逻辑只有百来行还原后维护成本低于每次改版重新分析的成本那就值得做如果代码量上千行、还依赖大量浏览器 API那不如直接保留原始 bundle用补环境的方式调用改版时只重新定位入口即可。4. 补环境让签名函数在 Node 里跑起来把 bundle 拿到 Node 里执行第一件事通常是报错window is not defined、document is not defined、navigator is not defined。签名函数里多多少少会读一些浏览器环境信息这一步要做的事就是把这些空补上。4.1 最小环境清单与执行顺序先给一个最小可用的骨架把常见的全局对象挂上去// env.js —— 在 require bundle 之前执行 const vm require(vm); const fs require(fs); // 1. 造一个沙箱上下文 const sandbox { console, setTimeout, setInterval, clearTimeout, Math, Date, JSON, parseInt, parseFloat, encodeURIComponent, decodeURIComponent, atob: s Buffer.from(s, base64).toString(binary), btoa: s Buffer.from(s, binary).toString(base64), }; // 2. 浏览器对象 sandbox.window sandbox; sandbox.self sandbox; sandbox.globalThis sandbox; sandbox.navigator { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, platform: Win32, language: zh-CN, languages: [zh-CN, zh], hardwareConcurrency: 8, }; sandbox.location { href: https://example.com/list, protocol: https:, host: example.com, hostname: example.com, pathname: /list, search: , }; sandbox.document { cookie: , referrer: https://example.com/, createElement: () ({ getContext: () null, style: {} }), documentElement: { style: {} }, }; sandbox.screen { width: 1920, height: 1080, availWidth: 1920, availHeight: 1040 }; sandbox.localStorage { getItem: () null, setItem: () {}, removeItem: () {} }; sandbox.sessionStorage sandbox.localStorage; // 3. 执行 bundle const code fs.readFileSync(./bundle.js, utf-8); const context vm.createContext(sandbox); vm.runInContext(code, context); // 4. 从上下文里取签名函数 const sign context.window.__sign_entry__; console.log(sign({ a: 1, b: 2 }));这段代码的重点在顺序所有全局对象必须在runInContext之前挂到sandbox上因为 bundle 顶层的立即执行函数一进来就会读环境sandbox.window sandbox是为了让window.xxx和直接xxx指向同一个对象很多代码两种写法混用globalThis也要指过去否则用globalThis判断运行时的库会走错分支。参数说明vm.createContext创建的上下文与主进程隔离bundle 里的var声明不会污染 Node 全局atob/btoa在 Node 16 之后虽然内置了但行为细节和浏览器有差异手动包一层 Buffer 更可控。真实场景里navigator.userAgent不能随便填很多签名会把 UA 参与计算必须和服务端校验时用的一致。4.2 用 Proxy 拦截把没补的变量揪出来手写环境最常见的失败是「漏了一个变量跑到某一行才报错」。用 Proxy 包一层window能在访问未定义属性时立即报警比等运行时报错快得多// strict-env.js —— 用 Proxy 拦截未定义属性访问 const realWindow sandbox.window; const handler { get(target, prop) { // 内置符号和已知属性直接放行 if (typeof prop symbol) return target[prop]; if (prop in target) return target[prop]; // 未定义属性打印调用栈方便定位是哪一行读的 console.warn([env miss] window.${String(prop)}); console.trace(); return undefined; }, set(target, prop, value) { target[prop] value; return true; } }; sandbox.window new Proxy(realWindow, handler); sandbox.self sandbox.window;拦截器只做了两件事访问已知属性原样返回访问未知属性先打一条[env miss]警告再返回undefined。typeof prop symbol的判断不能省因为有些库会遍历Symbol.iterator不排除会触发大量误报。console.trace()会打出完整调用栈直接告诉你这个属性是在 bundle 的哪个位置被读的对着栈去补对应的对象就行。这个办法在补document、navigator时同样适用。拦截器本身会拖慢执行但它只在分析阶段启用定位完成之后关掉即可。提示如果你发现某个属性被访问了几百次还是未定义先别急着补。有可能是它在检测环境真实性比如读navigator.webdriver硬补反而会露馅这种情况要看业务上是否真的需要。4.3 crypto 与时间两个最容易被忽略的依赖点签名函数里有两类调用最容易出问题。一类是加密浏览器里走window.crypto.subtle异步或者CryptoJS纯 JS 实现另一类是时间Date.now()返回毫秒有些算法还要拼上时区偏移。window.crypto.subtle在 Node 里可以用require(crypto).webcrypto顶替但它是异步的签名函数如果写成了同步调用就会崩。判断方式是看源码里有没有await或 Promise 链没有的话八成用的是纯 JS 实现环境里不需要补crypto直接把 bundle 里那个加密库模块取出来用就行。时间这块签名往往包含一个有效期服务端会校验请求时间和服务端时间的差值。本地复现时如果时间不准签名是对的也会被拒。解决办法是在沙箱里把Date换成一个受控版本// 用真实时间但允许手动偏移 const RealDate Date; let offset 0; sandbox.Date class extends RealDate { constructor(...args) { if (args.length 0) super(RealDate.now() offset); else super(...args); } static now() { return RealDate.now() offset; } }; // 需要校准时调 sandbox.__setOffset(-2000) sandbox.__setOffset ms { offset ms; };继承原生Date而不是完全重写是为了保留getTimezoneOffset、toISOString这些方法很多签名会用到它们。offset允许你在发现本地时间与服务端有偏差时手动补偿补偿值通过__setOffset传入单位是毫秒。补完这一层签名函数基本就能在 Node 里返回正常结果了。剩下的工作是把调用封装起来接到自己的请求流程里。5. 避坑与排查几段让人加班的真实记录5.1 症状签名能算出来但服务端一直说无效原因通常是参数顺序或编码不一致。签名算法对参数的排序和序列化方式极其敏感——你用JSON.stringify拼的字符串和页面用拼的字符串算出来完全是两个值。解决办法是把页面发出的原始请求参数抓下来打印你本地参与签名的字符串逐字符对比。重点看三处空值参数是保留还是丢弃、中文是否做了encodeURIComponent、数字类型的1和字符串1有没有被区别对待。我遇到过一次是 URL 参数里的空格页面编成了%20我这边编成了差一个字符全盘皆输。5.2 症状代码在浏览器里跑得好好的Node 里报xxx is not a function原因一般是环境判断走了不同分支。很多库会检测typeof window ! undefined来选择浏览器实现还是 Node 实现你在沙箱里塞了window它走了浏览器分支但对应的 API 你只补了个空对象。解决方式是顺着报错往上找那个xxx是在哪赋值的看它所在的函数有没有环境判断。如果确实是分支走错要么补齐对应的 API要么把判断条件改掉——但改代码会让后续版本更新时的 diff 变大我的习惯是优先补环境实在补不了再动代码动过的地方单独记一份 patch。5.3 症状签名有效期只有几十秒批量请求中途全挂原因是签名里带了时间戳且服务端做了窗口校验。页面本身是一次请求算一次签名你如果把签名缓存下来复用很快就过期。解决办法是在每次请求前重新调用签名函数不做缓存如果一次任务要发几百个请求控制并发数别让后面的请求排队排到过期。另外注意本地时钟——机器时间漂移几分钟签名的有效期窗口就永远对不上定期校时是必须的。5.4 症状hook 打出来的模块导出是空的原因是模块用了r.d定义的 getter属性是懒执行的Object.keys虽然能列出键名但值只有在真正访问时才会计算。如果你的记录代码只存了引用后面模块被回收或者状态变化值就对不上了。解决办法是在 hook 里直接读一次值并深拷贝或者至少把键名和类型记下来。另一种可能是目标模块还没执行——依赖它的模块没被加载r(id)会惰性执行这时候要主动调一次r(目标id)触发初始化。5.5 症状版本更新后所有定位全部失效原因是打包配置变了模块 id 从数字变成了带哈希的字符串或者字符串数组的取值函数改成了带三元判断的版本。解决办法是把「定位」这一步脚本化。与其每次手动翻文件不如写一个扫描脚本正则抓模块表、抓请求头字段名、抓加密库的特征字符串比如某个常量表的开头几位输出候选模块 id 列表。这样改版后跑一遍脚本几分钟就能重新定位比重新读一遍 bundle 强太多。6. 二次开发把一次性分析变成能维护的工具一次分析结束之后真正的成本在「下次改版」。我的经验是把整个流程拆成四层每层独立一层坏了不影响其他层。层级职责改版影响抓取层下载 bundle、比对 hash必变定位层找模块 id、找导出名大概率变环境层补 window/navigator/Date偶尔变调用层封装签名函数、拼请求基本不变分离之后改版时只需要处理前两层。定位层我会写成一个配置文件驱动的扫描器// locate.js —— 配置驱动的模块定位 const fs require(fs); const rules [ // 特征字符串 → 用于在模块表里找候选模块 { name: sign_module, patterns: [setRequestHeader, timestamp] }, { name: crypto_module, patterns: [ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef, 0123456789/] }, ]; const code fs.readFileSync(./bundle.js, utf-8); const report {}; for (const rule of rules) { const hit rule.patterns.filter(p code.includes(p)); report[rule.name] { matched: hit.length, // 记录命中的位置方便人工二次确认 positions: hit.map(p code.indexOf(p)) }; } console.log(JSON.stringify(report, null, 2));配置文件里每条规则包含一个名字和一组特征字符串脚本统计每组命中数量并记录位置。改版之后跑一遍如果sign_module的命中数从 2 变成 0说明请求头字段名或者某个关键字变了对着位置附近翻一翻就能重新锁定。这套东西看起来土但比每次凭记忆翻代码可靠得多。调用层要防的是「签名对了但请求还是失败」。我的习惯是在封装里加两道校验第一道是格式校验检查签名长度、字符集是否符合预期不符合直接抛错而不是发出去第二道是失败重试服务端返回无效签名时重新算一次再发最多重试两次避免死循环。这两道加起来不到二十行能省掉大量「到底是网络问题还是签名问题」的排查时间。还有一点值得做把每次签名时的输入参数、输出结果、本地时间写进日志按天切分。签名失效这类问题事后复盘时最需要的就是当时的输入输出没有日志基本靠猜。日志里别存敏感字段参数做个摘要就行。最后说个习惯。我早期做这类分析定位到函数就直接跑跑通就完事从来不记录环境补了哪些项。结果某次改版后环境层报错我花了整整一下午才想起来当初navigator里还塞过hardwareConcurrency漏了它就走进另一个分支。从那以后我每次补环境都强制写一份env.md列清楚补了哪些对象、每个字段的值、从哪来的改版时先对着清单过一遍。这套看起来笨的办法反而让我在几个项目的版本迭代里省下了最多时间。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
算法与数据结构可视化工具评测与学习指南 1. 为什么需要算法与数据结构可视化工具? 作为一名从业十年的全栈工程师,我深刻理解算法和数据结构学习的痛点。初学者常陷入两个极端:要么死记硬背代码模板却不懂核心思想,要么被数学推导吓退失去兴趣。直到2016年我接触了第一个… · 2026/11/6 7:59:34
云运维工程师进阶指南:负载均衡与高可用架构实战 1. 云运维工程师成长指南:从入门到精通的完整知识体系 作为一名在云计算领域摸爬滚打多年的老运维,我深知这个行业的知识体系有多么庞杂。今天我想分享一份经过实战检验的云运维学习路径,这不仅仅是一份脑图,而是我带着团队完成数… · 2026/11/6 7:59:34
OpenStack Cinder备份可靠性增强:三重校验与状态机治理 简介:这是一份面向OpenStack运维工程师与云平台开发者的Python工具脚本,专为简化Cinder卷备份/还原流程而设计,有效应对Cinder Backup Service在多租户支持、备份可见性控制及自动轮换等方面的原生限制。资源包共4个文件(11KB&… · 2026/11/6 7:58:33
AI如何重构软件研发流程与团队角色 1. AI时代下的研发流程重构:效率革命与角色重塑 最近几年,AI编程工具的快速发展正在深刻改变软件研发的每个环节。作为一名从业十余年的全栈工程师,我亲眼见证了从传统瀑布开发到敏捷开发,再到如今AI辅助开发的演进过程。这次变革… · 2026/11/6 9:05:29
macOS文件删不掉:unlocker与chflags/ACL解锁指南 简介:这份 Unlocker 3.0.3 工具包面向在 macOS 下遇到文件或文件夹被占用、无法删除或重命名的用户,尤其适合需要处理 VMware 虚拟机文件锁定的开发与运维人员。压缩包共 21 个文件,约 15.62MB,包含 cmd 批处理、py 脚本、exe 可执… · 2026/11/6 9:05:29
SQL注入实战:五种万能密码攻击与防御方案 ## 1. 项目背景与核心挑战SQL注入作为Web安全领域的经典漏洞类型,在CISP-PTE认证考试中占据重要地位。这个实验环境模拟了真实场景下的认证绕过场景,要求通过五种不同技术手段实现"万能密码"攻击。我在实际渗透测试中发现,很多防御… · 2026/11/6 9:05:29
医疗局域网大文件分片上传与断点续传优化方案 1. 医疗局域网病历PDF上传优化方案概述 在医疗信息化系统中,病历PDF文件的上传与存储是核心功能之一。传统方案采用WebUploader等现成组件,但在实际医疗局域网环境中面临诸多挑战:大体积病历(通常50-200MB)上传失败率高… · 2026/11/6 9:05:29
Spring Boot微信支付APIv3商家转账到零钱:签名、回调与幂等 简介:面向使用 Java 开发微信企业付款到零钱功能的后台开发者,这份压缩包提供可参考的接口调用与签名实现,聚焦企业向员工发放工资、奖金或处理退款等转账场景。包内共 2 个 Java 文件,压缩包约 3KB,主要包含转账控制器… · 2026/11/6 9:05:29
Java Swing 手搓森林冰火人:游戏循环与 AABB 碰撞实战 简介:面向Java初学者、课程设计实践者与游戏开发入门读者的森林冰火人单人版工程,围绕角色行走跳跃、水晶收集、积分累计和倒计时失败等玩法,涉及Swing图形界面、键盘事件监听、游戏循环、重力模拟与碰撞检测等知识点,可用于项目练… · 2026/11/6 9:04:27
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07
三菱PLC数据采集到数据库的时序设计:Python+MC协议实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
ASIC主模式RTL设计:状态机与三态驱动的流片级实现 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
STM32嵌入式C++开发:GDB调试与Renode仿真实战指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 1:02:11
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07