当前位置: 首页 > article >正文

不碰内核源码也能“改“内核?聊聊 ftrace 函数挂钩那点事儿

一、先整明白这技术到底在干嘛咱们平时写的程序调个printf、发个网络请求底层其实都在麻烦操作系统内核帮忙干活。内核里有一大堆服务窗口——也就是系统调用比如clone创建进程、execve执行程序这些。那如果我想在内核处理这些请求的时候插一脚自己的逻辑——比如记录个日志、拦下可疑操作、或者改改参数——该咋办最笨的办法是改内核源码重新编译重启机器。这太折腾了而且生产环境谁让你随便重启这时候就轮到ftrace 函数挂钩登场了。简单说它就是一个动态拦截器你写一个内核模块塞进去不用改内核源码不用重启就能让内核在执行某个函数时先或后执行你写的代码。完事儿还能继续走原来的逻辑仿佛什么都没发生。听起来有点像黑客技术其实正经用途多得很安全审计、行为监控、动态调试、甚至内核热补丁底层都是这套思路。二、核心思路在函数门口改道要理解这玩意儿怎么工作的得先知道内核函数被调用时CPU 在干啥。在 x86_64 架构上内核编译时默认会在每个函数开头塞一段小逻辑跟-pg编译选项有关也就是mcount机制。这段逻辑原本是用来做性能分析的——ftrace 本身就是 Linux 里一个很强的跟踪框架。正常情况下ftrace 只是看看记录一下函数 A 被调用了耗时多少然后放行。但聪明的人们发现既然已经能在函数门口拦住 CPU 了那能不能直接告诉 CPU“你别去原来那个函数了去我指定的那个地址”答案是能。而且方法就是直接改 CPU 的指路牌——指令指针寄存器rip。当 ftrace 的回调被触发时内核会把当前的寄存器状态保存在struct pt_regs里传给你。你只需要把regs-ip也就是下一条要执行的指令地址改成你自己函数的地址CPU 就会乖乖转去执行你的代码。这就是整个技术的灵魂借 ftrace 的跟踪能力做劫持的事儿。三、代码里都在忙什么逐段拆解...structftrace_hook{constchar*name;void*function;void*original;unsignedlongaddress;structftrace_opsops;};staticintfh_resolve_hook_address(structftrace_hook*hook){hook-addresslookup_name(hook-name);if(!hook-address){pr_debug(unresolved symbol: %s\n,hook-name);return-ENOENT;}#ifUSE_FENTRY_OFFSET*((unsignedlong*)hook-original)hook-addressMCOUNT_INSN_SIZE;#else*((unsignedlong*)hook-original)hook-address;#endifreturn0;}staticvoidnotracefh_ftrace_thunk(unsignedlongip,unsignedlongparent_ip,structftrace_ops*ops,structftrace_regs*fregs){structpt_regs*regsftrace_get_regs(fregs);structftrace_hook*hookcontainer_of(ops,structftrace_hook,ops);#ifUSE_FENTRY_OFFSETregs-ip(unsignedlong)hook-function;#elseif(!within_module(parent_ip,THIS_MODULE))regs-ip(unsignedlong)hook-function;#endif}intfh_install_hook(structftrace_hook*hook){interr;errfh_resolve_hook_address(hook);if(err)returnerr;hook-ops.funcfh_ftrace_thunk;hook-ops.flagsFTRACE_OPS_FL_SAVE_REGS|FTRACE_OPS_FL_RECURSION|FTRACE_OPS_FL_IPMODIFY;errftrace_set_filter_ip(hook-ops,hook-address,0,0);if(err){pr_debug(ftrace_set_filter_ip() failed: %d\n,err);returnerr;}errregister_ftrace_function(hook-ops);if(err){pr_debug(register_ftrace_function() failed: %d\n,err);ftrace_set_filter_ip(hook-ops,hook-address,1,0);returnerr;}return0;}voidfh_remove_hook(structftrace_hook*hook){interr;errunregister_ftrace_function(hook-ops);if(err){pr_debug(unregister_ftrace_function() failed: %d\n,err);}errftrace_set_filter_ip(hook-ops,hook-address,1,0);if(err){pr_debug(ftrace_set_filter_ip() failed: %d\n,err);}}intfh_install_hooks(structftrace_hook*hooks,size_tcount){interr;size_ti;for(i0;icount;i){errfh_install_hook(hooks[i]);if(err)gotoerror;}return0;error:while(i!0){fh_remove_hook(hooks[--i]);}returnerr;}voidfh_remove_hooks(structftrace_hook*hooks,size_tcount){size_ti;for(i0;icount;i)fh_remove_hook(hooks[i]);}#ifndefCONFIG_X86_64#errorCurrently only x86_64 architecture is supported#endif#ifdefined(CONFIG_X86_64)(LINUX_VERSION_CODEKERNEL_VERSION(4,17,0))#definePTREGS_SYSCALL_STUBS1#endif#if!USE_FENTRY_OFFSET#pragmaGCCoptimize(-fno-optimize-sibling-calls)#endif#ifdefPTREGS_SYSCALL_STUBSstaticasmlinkagelong(*real_sys_clone)(structpt_regs*regs);staticasmlinkagelongfh_sys_clone(structpt_regs*regs){longret;pr_info(clone() before\n);retreal_sys_clone(regs);pr_info(clone() after: %ld\n,ret);returnret;}#elsestaticasmlinkagelong(*real_sys_clone)(unsignedlongclone_flags,unsignedlongnewsp,int__user*parent_tidptr,int__user*child_tidptr,unsignedlongtls);staticasmlinkagelongfh_sys_clone(unsignedlongclone_flags,unsignedlongnewsp,int__user*parent_tidptr,int__user*child_tidptr,unsignedlongtls){longret;pr_info(clone() before\n);retreal_sys_clone(clone_flags,newsp,parent_tidptr,child_tidptr,tls);pr_info(clone() after: %ld\n,ret);returnret;}#endifstaticchar*duplicate_filename(constchar__user*filename){char*kernel_filename;kernel_filenamekmalloc(4096,GFP_KERNEL);if(!kernel_filename)returnNULL;if(strncpy_from_user(kernel_filename,filename,4096)0){kfree(kernel_filename);returnNULL;}returnkernel_filename;}#ifdefPTREGS_SYSCALL_STUBSstaticasmlinkagelong(*real_sys_execve)(structpt_regs*regs);staticasmlinkagelongfh_sys_execve(structpt_regs*regs){longret;char*kernel_filename;kernel_filenameduplicate_filename((void*)regs-di);pr_info(execve() before: %s\n,kernel_filename);kfree(kernel_filename);retreal_sys_execve(regs);pr_info(execve() after: %ld\n,ret);returnret;}#elsestaticasmlinkagelong(*real_sys_execve)(constchar__user*filename,constchar__user*const__user*argv,constchar__user*const__user*envp);staticasmlinkagelongfh_sys_execve(constchar__user*filename,constchar__user*const__user*argv,constchar__user*const__user*envp){longret;char*kernel_filename;kernel_filenameduplicate_filename(filename);pr_info(execve() before: %s\n,kernel_filename);kfree(kernel_filename);retreal_sys_execve(filename,argv,envp);pr_info(execve() after: %ld\n,ret);returnret;}#endif#ifdefPTREGS_SYSCALL_STUBS#defineSYSCALL_NAME(name)(__x64_name)#else#defineSYSCALL_NAME(name)(name)#endif#defineHOOK(_name,_function,_original)\{\.nameSYSCALL_NAME(_name),\.function(_function),\.original(_original),\}staticstructftrace_hookdemo_hooks[]{HOOK(sys_clone,fh_sys_clone,real_sys_clone),HOOK(sys_execve,fh_sys_execve,real_sys_execve),};staticintfh_init(void){interr;errfh_install_hooks(demo_hooks,ARRAY_SIZE(demo_hooks));if(err)returnerr;pr_info(module loaded\n);return0;}module_init(fh_init);staticvoidfh_exit(void){fh_remove_hooks(demo_hooks,ARRAY_SIZE(demo_hooks));pr_info(module unloaded\n);}module_exit(fh_exit);If you need the complete source code, please add the WeChat number (c17865354792)咱们结合代码看看一个完整的挂钩模块是怎么搭起来的。整个过程可以分成四步找地址 → 填钩子 → 改道 → 防递归。第一步找到你要拦的函数在哪内核函数编译后都是二进制地址你得先知道目标函数的门牌号。代码里用了两种办法老内核 5.7直接调用kallsyms_lookup_name(sys_clone)查内核符号表。新内核≥ 5.7内核把这张表藏起来了那就临时注册一个kprobe让它帮忙查地址查完就注销。// 伪代码示意hook-addresslookup_name(sys_clone);拿到地址后顺手把原始函数的指针保存一份方便后面调用。第二步定义钩子结构代码里定义了一个ftrace_hook结构体把必要信息打包structftrace_hook{constchar*name;// 要挂钩的函数名void*function;// 你的替换函数void*original;// 原始函数指针的存放位置unsignedlongaddress;// 解析出来的地址structftrace_opsops;// ftrace 需要的状态};第三步写回调函数——这是真正改道的地方staticvoidnotracefh_ftrace_thunk(unsignedlongip,unsignedlongparent_ip,structftrace_ops*ops,structftrace_regs*fregs){structpt_regs*regsftrace_get_regs(fregs);structftrace_hook*hookcontainer_of(ops,structftrace_hook,ops);if(!within_module(parent_ip,THIS_MODULE))regs-ip(unsignedlong)hook-function;}这段是精华ftrace_get_regs拿到寄存器快照。container_of从ftrace_ops反推出我们自己的ftrace_hook结构。关键一行regs-ip hook-function直接把 CPU 的下一步指向了我们的函数。within_module(parent_ip, THIS_MODULE)是在检查调用者是不是我自己如果是就别再改了否则会死循环后面细说。第四步注册到 ftrace 框架hook-ops.funcfh_ftrace_thunk;hook-ops.flagsFTRACE_OPS_FL_SAVE_REGS// 要保存寄存器|FTRACE_OPS_FL_RECURSION// 关掉 ftrace 自带的递归保护我们自己管|FTRACE_OPS_FL_IPMODIFY;// 允许修改指令指针ftrace_set_filter_ip(hook-ops,hook-address,0,0);// 只盯这个地址register_ftrace_function(hook-ops);// 上线这里几个标志位很重要SAVE_REGS不保存寄存器你改啥IPMODIFY告诉 ftrace 我要改ip别拦我。RECURSION关掉 ftrace 自己的防递归因为我们要改ip它那个机制反而碍事。四、流程原理图咱们把一次完整的挂钩过程画清楚用户进程调用 clone() │ ▼ ┌─────────────────┐ │ 进入内核态 │ │ 走到 sys_clone │ │ 函数入口处 │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ mcount 触发 │ │ ftrace 回调 │ │ fh_ftrace_thunk │ └────────┬────────┘ │ ▼ [检查 parent_ip] │ ┌────┴────┐ ▼ ▼ 是模块内 不是模块内 自己人 别人调的 │ │ ▼ ▼ 直接放行 修改 regs-ip 不干预 指向 fh_sys_clone │ │ └────┬────┘ ▼ ┌─────────────────┐ │ 执行 fh_sys_clone │ │ 你的自定义逻辑 │ │ 打印日志等... │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ 调用 real_sys_clone│ │ 保存的原始函数 │ │ 真正干活的逻辑 │ └────────┬────────┘ │ ▼ 返回结果 │ ▼ 回到用户态五、那个防递归到底在防啥这是最容易踩坑的地方。假设你已经把sys_clone挂上了你的替换函数叫fh_sys_clone。fh_sys_clone里面为了完成工作又调用了real_sys_clone。但你想过没有real_sys_clone本质上就是sys_clone本身啊所以 CPU 走到real_sys_clone门口ftrace 又触发了又把regs-ip改成fh_sys_clone……于是fh_sys_clone又执行一遍又调用real_sys_clone……无限套娃内核直接栈溢出挂掉。代码里的解决办法很巧妙看parent_ip调用者的地址。如果调用者就在咱们这个内核模块内部within_module(parent_ip, THIS_MODULE)说明这次触发是自己人调用引起的直接放行不改道。这就打破了循环只有外部调用才会被拦截内部调用走原路。六、系统调用的变脸问题代码里有一大堆#ifdef PTREGS_SYSCALL_STUBS这是因为在 Linux 4.17 之后x86_64 的系统调用入口变了。以前sys_clone参数是散的clone_flags,newsp…后来统一改成只传一个struct pt_regs *regs寄存器里的参数自己拆。代码里做了兼容处理新内核从regs-di里拿第一个参数比如execve的文件名。老内核直接接参数。这也是为什么代码里duplicate_filename要专门从用户态拷字符串到内核态——内核里不能直接操作用户空间的指针得用strncpy_from_user安全地复制一份。七、测试运行加载模块sudoinsmod ftrace_hook.ko如果没有任何报错说明加载成功了看效果开两个终端终端 1 - 实时监控内核日志sudodmesg-w终端 2 - 随便执行点命令触发系统调用lspwdechohello然后回头看终端 1你会看到类似输出[ 123.456789] ftrace_hook: module loaded [ 145.123456] ftrace_hook: clone() before [ 145.123789] ftrace_hook: clone() after: 12345 [ 145.234567] ftrace_hook: execve() before: /usr/bin/ls [ 145.235012] ftrace_hook: execve() after: 0每执行一个命令就会触发一次execve每创建一个新进程比如ls本身可能 fork就会触发clone。卸载模块sudormmod ftrace_hook再看dmesg[ 200.987654] ftrace_hook: module unloaded总结ftrace 原本只是个看门的跟踪工具但借助修改寄存器的能力它成了一个守门的利器。整个方案最精妙的地方在于完全没有修改内核的任何代码只是借用了既有的跟踪基础设施就实现了对内核执行流的动态控制。当然玩内核模块有风险建议在虚拟机里折腾。而且内核版本迭代很快kallsyms藏起来了、ftrace_regs结构变了、系统调用传参方式改了……这些坑代码里都已经帮你踩了一遍读的时候多留意那些#if LINUX_VERSION_CODE的条件编译那就是一本活脱脱的内核 API 变迁史。搞懂了这套机制你对 Linux 内核的执行流程、中断处理、系统调用这些概念基本就打通任督二脉了。Welcome to follow WeChat official account【程序猿编码】

相关文章:

不碰内核源码也能“改“内核?聊聊 ftrace 函数挂钩那点事儿

一、先整明白:这技术到底在干嘛? 咱们平时写的程序,调个 printf、发个网络请求,底层其实都在麻烦操作系统内核帮忙干活。内核里有一大堆"服务窗口"——也就是系统调用,比如 clone(创建进程&#…...

大模型面试题:办公 Agent 的意图识别模块核心痛点是什么?怎么解决?

在办公 Agent 里,意图识别不是简单地判断用户“想干什么”,而是要判断:用户现在想做什么、缺什么信息、能不能直接调用工具、是否存在风险、要不要追问、当前任务和历史上下文是什么关系。很多候选人回答这类问题时,只会说“用 LL…...

激光三角法测距

激光三角测距原理详述 激光三角测距法作为低成本的激光雷达设计方案,可获得高精度、高性价比的应用效果,并成为室内服务机器人导航的首选方案,本文将对激光雷达核心组件进行介绍并重点阐述基于激光三角测距法的激光雷达原理。 激光雷达四大核…...

AzurLaneAutoScript:碧蓝航线全自动管理解决方案深度解析

AzurLaneAutoScript:碧蓝航线全自动管理解决方案深度解析 【免费下载链接】AzurLaneAutoScript Azur Lane bot (CN/EN/JP/TW) 碧蓝航线脚本 | 无缝委托科研,全自动大世界 项目地址: https://gitcode.com/gh_mirrors/az/AzurLaneAutoScript 在当今…...

如何构建你自己的Switch自定义固件:Atmosphere深度定制指南

如何构建你自己的Switch自定义固件:Atmosphere深度定制指南 【免费下载链接】Atmosphere-stable 大气层整合包系统稳定版 项目地址: https://gitcode.com/gh_mirrors/at/Atmosphere-stable Atmosphere不仅是一个现成的Switch自定义固件,更是一个完…...

Qt 工程瘦身工具:批量删除 build 目录与 IDE 配置文件

目录一、创作动机二、脚本的作用三、脚本做了什么(详细说明)3.1 环境准备3.2 扫描与统计3.3 清理当前目录3.4 遍历子目录清理3.5 统计报告四、脚本代码五、逻辑流程六、使用方法七、下载一、创作动机 作为一名 Qt/QML 开发者,你一定遇到过这…...

人该怎样活着呢?版本70.9

人该怎样活着呢?版本70.9 A思考现实问题并记录自己的灵感 。【生活的指南针】 (20250212) a1如何思考? 思考相似联想因果联想灵感(20251226)相似联想:比如看到苹果想到牛顿在树下被苹果砸…...

Arm Neoverse V3AE调试寄存器DBGWCR0_EL1与DBGBVR1_EL1详解

1. Arm Neoverse V3AE调试寄存器深度解析 在Arm架构的调试系统中,调试寄存器扮演着至关重要的角色。作为一位长期从事Arm架构底层开发的工程师,我经常需要与DBGWCR0_EL1和DBGBVR1_EL1这类调试寄存器打交道。这些寄存器不仅仅是简单的控制位集合&#xff…...

Xbox成就解锁器终极指南:免费工具3步解锁全成就

Xbox成就解锁器终极指南:免费工具3步解锁全成就 【免费下载链接】Xbox-Achievement-Unlocker Achievement unlocker for xbox games (barely works but it does) 项目地址: https://gitcode.com/gh_mirrors/xb/Xbox-Achievement-Unlocker 还在为Xbox游戏中那…...

你的代码仓库变成“毛线团”了?Monorepo 用 Turborepo 拆成“乐高积木”

你维护着五六个项目,每个都单独开一个 Git 仓库。改一个公共组件,要挨个进每个项目,复制粘贴,提交,发布。一上午就没了。今天我们来学 Monorepo——用 Turborepo 把多个项目放进同一个仓库,共享代码、统一构…...

终极免费Flash反编译工具:JPEXS Free Flash Decompiler全新指南

终极免费Flash反编译工具:JPEXS Free Flash Decompiler全新指南 【免费下载链接】jpexs-decompiler JPEXS Free Flash Decompiler 项目地址: https://gitcode.com/gh_mirrors/jp/jpexs-decompiler 你是否曾面对过这些困境?那些曾经精彩的Flash动画…...

基于FastAPI与MCP协议构建多服务AI工具集成平台

1. 项目概述与核心价值最近在折腾AI助手和代码编辑器集成的时候,发现一个挺有意思的需求:如何让Claude、Cursor或者Windsurf这类工具,能直接调用外部的天气、新闻、汇率这些实时数据?总不能每次都手动查了再复制粘贴吧。传统的做法…...

Clawtick CLI:统一命令行工具入口,提升开发运维效率

1. 项目概述:一个命令行里的“瑞士军刀”如果你和我一样,每天的工作都离不开终端,那肯定对命令行工具(CLI)又爱又恨。爱的是它的高效、直接和可编程性,恨的是不同工具之间五花八门的命令、参数和配置方式&a…...

终极指南:3分钟让你的PS4手柄在Windows上完美运行

终极指南:3分钟让你的PS4手柄在Windows上完美运行 【免费下载链接】DS4Windows Like those other ds4tools, but sexier 项目地址: https://gitcode.com/gh_mirrors/ds/DS4Windows 想让你的PS4手柄在Windows电脑上获得原生级的游戏体验吗?DS4Wind…...

IDEA卡在‘Resolving dependencies’?别急着重启,先试试这5个Maven/Gradle配置检查

IDEA卡在‘Resolving dependencies’?别急着重启,先试试这5个Maven/Gradle配置检查 每次看到IDEA底部进度条卡在"Resolving dependencies"时,那种焦躁感就像等快递显示"正在派送"却迟迟不到。大多数开发者会条件反射地点…...

除了上CDN,Unity微信小游戏包体优化还有这些“骚操作”:从插件源码到资源管理

突破20M限制:Unity微信小游戏深度包体优化实战指南 当Unity开发者将作品移植到微信小游戏平台时,20M的包体限制往往成为第一道技术门槛。这个看似简单的数字背后,实际上考验的是开发者对引擎机制、资源管理和平台特性的综合把控能力。本文将带…...

Cursor编辑器配置同步方案:基于Git与Shell脚本实现开发环境无缝漫游

1. 项目概述:一个为 Cursor 编辑器量身定制的配置同步方案如果你和我一样,是一个重度依赖 Cursor 这款“AI 原生”代码编辑器的开发者,那你一定遇到过这个痛点:辛辛苦苦在办公室的电脑上配置好了顺手的主题、快捷键、代码片段、AI…...

从实验室到工作站:手把手配置Ubuntu 20.04+CUDA 11.1开发环境,兼顾V100与3090混搭显卡

异构GPU集群实战:Ubuntu 20.04下V100与3090的CUDA 11.1协同配置指南 当实验室的计算节点同时搭载NVIDIA V100和RTX 3090显卡时,驱动安装会面临Volta与Ampere架构的版本兼容挑战。去年我们在部署某AI训练平台时,就遇到过驱动版本冲突导致3090无…...

打造高效心流体验:氛围感编码环境配置全攻略

1. 项目概述:一个为“氛围感编码”而生的资源宝库如果你和我一样,是个对工作环境、工具美学和流程仪式感有点“挑剔”的程序员,那么看到acvnace/awesome-vibe-coding-resources这个项目标题,大概率会会心一笑。这绝不是一个简单的…...

轻量化研究代理:基于Agent架构的自动化信息处理方案

1. 项目概述:轻量化研究代理的诞生背景与核心价值在信息爆炸的时代,无论是学术研究者、行业分析师,还是产品经理、内容创作者,都面临着一个共同的痛点:如何从海量的、碎片化的信息中,高效地筛选、整合、提炼…...

从《九章算术》到Python:手把手复现古人开方算法(附完整代码)

从《九章算术》到Python:手把手复现古人开方算法(附完整代码) 数学史与编程的碰撞总能擦出令人惊喜的火花。当我们在Python中敲下math.sqrt(2)时,很少有人会想到这个简单的函数背后,是两千多年来人类智慧的结晶。本文将…...

ViGEmBus内核级游戏控制器模拟:架构解析与高级故障排除方案

ViGEmBus内核级游戏控制器模拟:架构解析与高级故障排除方案 【免费下载链接】ViGEmBus Windows kernel-mode driver emulating well-known USB game controllers. 项目地址: https://gitcode.com/gh_mirrors/vi/ViGEmBus 在游戏开发、输入设备测试和远程游戏…...

终极视频加速工具:5大技巧让你每天多出2小时的高效观看体验

终极视频加速工具:5大技巧让你每天多出2小时的高效观看体验 【免费下载链接】videospeed HTML5 video speed controller (for Google Chrome) 项目地址: https://gitcode.com/gh_mirrors/vi/videospeed 你是否经常觉得视频内容太慢,但又不想错过关…...

GTA5线上小助手:免费开源的游戏增强工具,让你的洛圣都冒险更轻松

GTA5线上小助手:免费开源的游戏增强工具,让你的洛圣都冒险更轻松 【免费下载链接】GTA5OnlineTools GTA5线上小助手 项目地址: https://gitcode.com/gh_mirrors/gt/GTA5OnlineTools 想要在《侠盗猎车手5》线上模式中获得更流畅的游戏体验吗&#…...

DeepSeek-TUI 终端智能交互实战指南

在终端里敲命令是开发者的日常,但面对复杂的管道组合、记不住的参数选项,或是深夜排查故障时急需一条精准的查询语句,我们常常不得不中断思路去搜索文档。这种上下文切换不仅打断心流,更降低了效率。如果终端本身就能理解自然语言…...

Atom编辑器中文汉化实战指南:告别英文困扰,打造专属中文编程环境

Atom编辑器中文汉化实战指南:告别英文困扰,打造专属中文编程环境 【免费下载链接】atom-simplified-chinese-menu Atom 的简体中文汉化扩展,目前最全的汉化包。包含菜单汉化、右键菜单汉化以及设置汉化 项目地址: https://gitcode.com/gh_mirrors/at/a…...

抖音下载神器:douyin-downloader免费批量下载工具完整教程

抖音下载神器:douyin-downloader免费批量下载工具完整教程 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback s…...

别再折腾了!手把手教你搞定Fluent UDF编译环境(附VS安装避坑指南)

从零构建Fluent UDF编译环境:Visual Studio深度配置与避坑实战 当你在深夜第三次重装Visual Studio,盯着屏幕上那个顽固的"Error: The UDF library you are trying to load is not compiled"提示时,可能已经怀疑人生。这不是你的问…...

如何高效实现小说资源自动化采集:Rust开源方案深度解析

如何高效实现小说资源自动化采集:Rust开源方案深度解析 【免费下载链接】Tomato-Novel-Downloader 番茄小说下载器不精简版 项目地址: https://gitcode.com/gh_mirrors/to/Tomato-Novel-Downloader 番茄小说下载器(Tomato-Novel-Downloader&#…...

如何用JPEXS Free Flash Decompiler拯救你的Flash资源:5分钟快速上手指南

如何用JPEXS Free Flash Decompiler拯救你的Flash资源:5分钟快速上手指南 【免费下载链接】jpexs-decompiler JPEXS Free Flash Decompiler 项目地址: https://gitcode.com/gh_mirrors/jp/jpexs-decompiler 你是否曾经为找不到Flash动画中的素材而烦恼&#…...