Agent 时代的 X-Y Problem
<p>最近这几个月,我一直在高强度折腾 Vibe Coding,我发现了一个特别反直觉的现象:<strong>当你面对越来越强大的 Agent 时,你给出的提示词越详细、规定的步骤越精确,它最后交付的结果反而越差。</strong></p> <p>然后我突然意识到,这其实是软件工程里臭名昭著的 X-Y Problem,正在 AI 时代以一种全新的形态大规模重演。</p>
<p>最近这几个月,我一直在高强度折腾 Vibe Coding,我发现了一个特别反直觉的现象:<strong>当你面对越来越强大的 Agent 时,你给出的提示词越详细、规定的步骤越精确,它最后交付的结果反而越差。</strong></p> <p>然后我突然意识到,这其实是软件工程里臭名昭著的 X-Y Problem,正在 AI 时代以一种全新的形态大规模重演。</p>
<blockquote> <p>【导语】<br>很多年前,折腾 KernelSU 让我饱受“为开挂提供温床”的批评。主流观念总是一刀切地认为:只要和“破坏规则”沾边,就是可耻的。<br>但后来我发现,无论是在技术发展的莽荒时代,还是在我们习以为常的现实社会中,创新往往源于对边界的试探;而那些被大众不假思索地捍卫的“公平”和“规则”,很多时候,只不过是系统为了维持运转而精心构建的一套控制代码。</p> </blockquote> <p>几年前,我<a href="https://mp.weixin.qq.com/s/CY-sTRV86A43-Iz2UTxyPQ">曾提到过</a>,KernelSU 某种程度上算是我早年折腾科技留下的副产品。那之后,我收到过不少批评,其中很多都围绕一个判断:只要和“开挂”沾边,就是可耻的,就是不正当的。</p> <p>现在回想起来,我探索写科技的那几年,我的人生也开挂了。这并非说我靠技术去盈利、改变了人生,而是因为我从中领悟到了一些关于技术、规则和社会运行的东西。</p>
<p>最近这一个月,我深度使用 Coding Agent 完成了好几个项目:</p> <ol> <li>KernelSU 的 metamodule 功能</li> <li>一个内核文件系统 (ksufs?)</li> <li>公司的全栈项目</li> </ol> <p>除了第二个项目我写了 MVP 原型外,其余项目我几乎没有动一行代码,全程指挥 AI 完成。从需求规划、架构讨论,到具体代码执行,以及后续的迭代重构,AI 都能极好地配合。</p> <p>经过这段时间的实战,我认为行业已经到了一个拐点:<strong>Coding Agent 的综合能力,已经超过了市面上人类工程师的平均水平。</strong></p> <p>当然,现在的 AI 并不完美。它会犯错,会“幻觉”,有时候需要反复纠正。但仔细想想,人类工程师不也一样吗?我们自己也会卡壳,也会写了前面忘了后面,也会因为项目迭代太快而堆砌“屎山”。</p> <p>如果我们不把 AI 视作完美的“神”,而是把它当做一个<strong>允许犯错、但极其勤奋的中级工程师</strong>,你会发现它完全能胜任当下的工作。</p> <p>真正让人“细思恐极”的是:这个 AI 工程师可以 7 x 24 小时待命,听话不抱怨,执行力超强且有问必答,而成本只有人类的一半甚至更低。</p> <p>面对这样的“竞争对手”,我们人类工程师的护城河到底在哪里?</p> <h3 id="品味"><a href="#品味" class="headerlink" title="品味"></a>品味</h3><p>品味,可能是 AI 时代最稀缺的能力。</p>
<p>最近,一款名为 Cursor 的 AI 编程工具火遍了编程圈。自从 ChatGPT 发布以来,AI 正以肉眼可见的速度进化,从最初的翻译和总结,到现在的语音、视觉和思考能力,极大地方便了我们的工作和生活。许多人担心,未来 AI 会不会取代人类?</p> <p>作为一个程序员,我的大部分工作都与代码打交道。在编程领域,AI 也展现了强大的能力。例如,Github Copilot 极大地提高了编程效率。Copilot 就像一个“副驾驶”,可以帮助你完成代码片段,但它无法自主完成整个项目,仍然需要你懂一些代码。</p> <p>但是 Cursor 不一样。你只需描述自己的想法,它就能自动生成代码,重复这个过程就能完成整个项目。它有多强大呢?有一个 8 岁的小女孩借助 Cursor AI 在45分钟内写出了一个聊天机器人,着实令人震惊。</p>
<p>自 2018 年我发布 VirtualXposed 以来,这 6 年里我做了不少开源或者免费的产品,用过我 App 的用户,没有千万也有百万;然而,所有这些东西都是完全免费的,我没有任何直接的经济收益,「用爱发电」说的大概就是这了。</p> <p>几天前我把小狐狸钱包的助记词搞丢了,当时我很着急,因为多年以前我就把这个钱包的地址放在我的很多捐赠页面,心想里面没准有一笔巨款呢;结果我去 etherscan 上去一查,没有任何交易记录,资产是零,总算有惊无险。</p> <p>这听起来似乎很滑稽,但却是绝大多数“用爱发电”项目的现状,靠用户自发捐赠,可能连一顿饭钱都挣不回来,更别提养家糊口了。</p> <p>不挣钱倒是没啥,毕竟我们都是手艺人,那两把刷子养活自己还是没啥问题的。更让人难受的是,我们还经常被各种网络喷子问候,那些无端的谩骂、诋毁甚至毁谤,会让人产生自我怀疑:“我到底是在图个啥?”。</p>
<p>前几天我发了一篇文章:<a href="https://mp.weixin.qq.com/s/F23K1HHvau0WqCfVBmHE9w">小米的存储扩容是拆承重墙吗</a>,文中站在非技术角度解读,其核心论点是“小米公司不可能背着巨大的风险去做收效甚微的事”,有粉丝表示赞同:“当对技术不了解的时候,还可以从基本逻辑来理解”;然而有一些童鞋嗤之以鼻,表示没有技术分析,纯属 YY。</p> <p>我观察了这些粉丝过往的留言,发现他们大部分都是懂技术的,其专业程度远超普通用户;我对此表示理解,他们就是因为我的技术关注我的,现在我在这说别的,当然属于是在扯淡。的确,我做技术已经十年了,在某些领域做得还不错,这自然会给人一种固有的刻板印象;五年前,我也因为技术而从大厂出来选择自主创业;不过,我对技术的理解并非你们想象中的那样。</p> <p><strong>技术,只是解决问题的一种方式</strong>。它是你日常工具箱里面的一种工具,就像水管松了用扳手拧,灯泡不亮用万用表测,衣服破了用缝纫机补一样。</p>
<p>若干年前,我还在做 Android 客户端性能优化的时候,读到了 OpenResty 作者章亦春老师的文章:<a href="https://blog.openresty.com.cn/cn/dynamic-tracing/">动态追踪技术漫谈</a>,当时被深深地震撼到了,原来通过使用各种高级的调试技术,解决问题竟然可以做到如此精准而优雅。</p> <p>然而当我真正要解决 Android 系统上应用程序的性能问题时,才发现理想很丰满现实很骨感——手头趁手的工具几乎没有。文章中提到的内核态追踪技术 SystemTap / DTrace 在 Android 系统上压根不存在,用户态的追踪技术开销大到可怕:TraceView 开启后程序性能直接下降十倍不止,Systrace 当时功能半残废,使用起来还需要自己插桩;simpleperf 能使,但就是有点 simple……到最后,真正要解决问题的时候,还是靠经验、二分法和 inline hook;为了定位 Android 虚拟机的性能问题,我甚至还自己造了个 <a href="https://github.com/tiann/epic">ART HOOK 的轮子</a>。</p> <p>然而,时间来到 2022 年,世界已焕然一新:eBPF 这种革命性的技术改变了一切。</p>
<p>去年春节之前,妈妈打电话给我说,我们老家那边有疫情,听说高速路口都封锁了;今年你在外面过年,一定要好好搞点吃的,过个好年。</p> <p>我听完之后一时间有点恍惚:长这么大,我还没有在外面过年过;每年春节,无论我身处何地,无论回家要转多少趟车,我都会回家陪爸妈过节。电话完之后我赶紧又给老家那边的疾控中心打电话确认了一遍,被告知疫情是隔壁县的,可以放心回家,心里一块石头落了地。终于,我在腊月廿七那一天踏上了回乡的旅途。</p> <h2 id="偷渡长江"><a href="#偷渡长江" class="headerlink" title="偷渡长江"></a>偷渡长江</h2><p>那一天我早早地起了床,在经过大半天的转车之后,终于在下午五点左右下了高速出口;结果出人意料的事情发生了:路上的一座大桥被人为堵住,回家的路被彻底堵死了;我辗转千里,却在离家不到两公里的地方被拒之门外。</p>
<blockquote> <p>你如今的气质里,藏着你走过的路、读过的书、爱过的人,以及学习过的编程语言。</p> </blockquote> <p>如果把编程类比作武侠或者修仙里面的打怪升级,那我在入行的头几年,一直在痴迷于各种各样的招式;那时候我学习和体验了各种各样的语言:C/C++/Java/Javascript/Kotlin/Python/Ruby/Scala/Clojure/Scheme/Erlang/Haskell,不过自从我学习了 Haskell 之后,这份列表就基本停止增长了;虽然后来也对 Elixer、Go 也有过简单的了解,但是也仅限于了解了。</p> <p>一方面随着从业时间的增长,要解决的问题逐渐变成了系统问题或者业务问题;另一方面也是体会到,语言只是工具,招式再花里胡哨也是徒劳,修炼内功才是王道。</p> <p>不过,在一次机缘巧合之下,我再一次接触到了 Rust。</p>
<p>对于搞机党或者开发人员来说,root 一定是一个不陌生的名词。在 [当我们谈论解锁 BootLoader 时,我们在谈论什么?] 一文中我们了解到,解锁 Bootloader 实际上能做到的是<strong>让手机可以运行第三方的操作系统</strong>,而通常来说,我们给手机解锁 Bootloader 就是为了获取 Root 权限。那么,<strong>何为 root?</strong>,解锁 Bootloader 和 root 到底有什么联系和区别?</p> <blockquote> <p>In Unix-like computer OSes (such as Linux), root is the conventional name of the user who has all rights or permissions (to all files and programs) in all modes (single- or multi-user).</p> </blockquote> <p>维基百科说,在类 Unix 系统中,root 是在<strong>所有模式</strong>(单用户或多用户)下对<strong>所有文件和程序</strong>拥有<strong>所有权利或许可</strong>的用户的名称。</p> <p>现代操作系统(本文主要讨论 Android 系统,下同)一般都是多用户的,那个名为 root 的用户所拥有的权限就是 root 权限;而 root 权限中有三个「所有」,可以简单这么理解:<strong>root 意味着最高权限</strong>;不过,这么描述不够具象,接下来就带大家了解一下 root 的方方面面。</p>
<p>有过刷机经验或者曾经尝试过刷机的童鞋,一定听说过「解锁」这个词。这里的「解锁」全称应该是「解锁 BootLoader」或者简称为「解 BL 锁」。与通过人脸识别或者指纹、数字图案解锁手机屏幕的那种「屏幕解锁」不同,这里的「解锁」完全是另外一个概念。直观来说,<strong>解 BL 锁是刷机的前提条件</strong>。通常情况下,一旦某个设备无法解锁 BL,基本上就无法在这个设备进行刷机了。</p> <p>那么,一定会有童鞋关心,解锁 BootLoader 到底意味着什么?为什么它会有限制?我们能绕过限制强制解锁吗?今天,我就尝试来回答一下这几个问题。</p> <p>在搞清楚解锁 BootLoader 之前,我们必须先搞清楚什么是 BootLoader:</p> <blockquote> <p>A bootloader is software that is responsible for booting a computer.</p> </blockquote> <p>维基百科上的介绍言简意赅:<strong>Bootloader 是负责启动计算机的软件</strong>。计算机开机的时候,会执行一个相对较小的程序来初始化内存、外设等启动后续操作系统必备的资源,并最终启动用户所使用的操作系统(如 Windows, Android 等);这个程序就是 BootLoader。</p> <p>我们知道,操作系统负责管理设备的硬件资源;而 BootLoader 是用来启动操作系统的,如果有人通过 BootLoader 来启动一个恶意的操作系统,那我们设备的安全性就无法得到保障了。因此,BootLoader 一个核心的功能就是,确保启动一个可信的操作系统。另外,当设备的操作系统出现问题时,BootLoader 还可以引导启动另外一个正常的可信系统来执行恢复;所以,BootLoader 另外一个功能重要功能就是恢复系统。</p>
<p>几个月前,我写了一篇<a href="http://weishu.me/2020/01/16/a-keep-alive-method-on-android/">Android 黑科技保活实现原理揭秘</a>,当时我们提到,现在的进程保活基本上分为两类,一种是想尽办法提升进程的优先级,保证进程不会轻易被系统杀死;另一种是确保进程被杀死之后能通过各种方式复活。</p> <p><a href="http://weishu.me/2020/01/16/a-keep-alive-method-on-android/">Android 黑科技保活实现原理揭秘</a> 中的进程永生术是第二种,它通过钻 Android 杀进程的空子实现了涅槃永生;不了解的同学可以参考一下 <a href="https://github.com/tiann/Leoric">PoC</a>。归根结底,所谓的黑科技就是利用系统漏洞。那么,既然我们可以利用漏洞逃过追杀,那何不更进一步,利用系统漏洞提权?</p>
<p>最近我在编写一个 Android 上的驱动程序,这个驱动程序的某些部分用到了 Unix domain socket,守护进程和客户端进程使用 C/S 模式进行通信。在调试程序的时候发现一个非常奇怪的问题:如果客户端开启若干个线程连上 socket,send/recv 若干消息之后立即退出进程,从日志上看,server 端有 10% 左右的概率无法正常回收资源。</p> <p>一开始我以为是我自己程序写的有问题,毕竟这个驱动是使用纯 C 语言实现的,并且用到了 epoll 的 <a href="https://linux.die.net/man/7/epoll">ET 模式</a>,这种非阻塞的编程模型的确有许多微妙的地方,一不小心就容易出错。我排查了很久都没有发现问题所在,更有趣的是,虽然看起来我的程序无法回收资源,但是在压力测试下他也能正常工作,完全没有资源泄漏的迹象;实在没办法,我就祭出了大杀器 <a href="https://linux.die.net/man/1/strace">strace</a>。不看不知道,一看就好笑:strace 显示,我的程序逻辑是正常的,它正确地调用了相关的资源释放函数!但是,logcat 中没有相关的日志,在客户端退出之后 server 端的日志就戛然而止了。看起来,好像不是我程序的问题,而是系统的 logcat 丢失了日志?</p>
<p>一直以来,App 进程保活都是各大厂商,特别是头部应用开发商永恒的追求。毕竟<strong>App 进程死了,就什么也干不了了</strong>;一旦 App 进程死亡,那就再也无法在用户的手机上开展任何业务,所有的商业模型在用户侧都没有立足之地了。</p> <p>早期的 Android 系统不完善,导致 App 侧有很多空子可以钻,因此它们有着有着各种各样的姿势进行保活。譬如说在 Android 5.0 以前,App 内部通过 native 方式 fork 出来的进程是不受系统管控的,系统在杀 App 进程的时候,只会去杀 App 启动的 Java 进程;因此诞生了一大批“毒瘤”,他们通过 fork native 进程,在 App 的 Java 进程被杀死的时候通过 <code>am</code>命令拉起自己从而实现永生。那时候的 Android 可谓是魑魅横行,群魔乱舞;系统根本管不住应用,因此长期以来被人诟病耗电、卡顿。同时,系统的软弱导致了 Xposed 框架、阻止运行、绿色守护、黑域、冰箱等一系列管制系统后台进程的框架和 App 出现。</p> <p>不过,随着 Android 系统的发展,这一切都在往好的方向演变。</p>
<p>去年发布的 Android P上引入了针对非公开API的限制,对开发者来说,这绝对是有史以来最重大的变化之一。前天 Google 发布了 Android Q 的 Beta 版,越来越多的 API 被加入了黑名单,而且 Google 要求下半年 APP 必须 target 28,这意味着现在的深灰名单也会生效;可以预见,在不久的将来,我们要跟大量的 API 说再见了。</p> <p>去年我给出了<a href="http://weishu.me/2018/06/07/free-reflection-above-android-p/">一种绕过Android P对非SDK接口限制的简单方法</a>,经验证,这办法在 Android Q 的 Beta 版上依然能正常使用。虽然这个方法需要进行内存搜索,理论上有可能失败,但实际上它曾在 VirtualXposed 和 太极 中得到了较为广泛的验证,从未收到过由于反射失败而导致问题的反馈。而且据我所知,有若干用户量不少的 APP 在线上使用了我提供的 <a href="https://github.com/tiann/FreeReflection/">FreeReflection</a> 库,想来应该也是没有问题的吧。</p> <p>不过今天,我打算给出另外一种绕过限制的办法。这个办法目前来说是最优方案,我个人使用了一个多月,不存在任何问题。</p>
<p>从我写下 <a href="http://weishu.me/2016/01/28/understand-plugin-framework-overview/">Android插件化原理解析</a> 系列第一篇文章至今,已经过去了两年时间。这期间,插件化技术也得到了长足的发展;与此同时,React Native,PWA,App Bundle,以及最近的Flutter也如火如荼。由于实现插件化需要太多的黑科技,它给项目的维护成本和稳定性增加了诸多不确定性;我个人认为,2017年手淘Atlas插件化项目的开源标志着插件化的落幕,2018年Android 9.0上私有API的限制几乎称得上是盖棺定论了——曾经波澜壮阔的插件化进程必将要退出历史主流。如今的插件化技术朝两个方向发展:其一,插件化的工程特性:模块化/解耦被抽离,逐渐演进为稳定、务实的的组件化方案;其二,插件化的黑科技特性被进一步发掘,inline hook/method hook大行其道,走向双开,虚拟环境等等。</p> <p>虽然插件化终将落幕,但是它背后的技术原理包罗万象,值得每一个希望深入Android的小伙伴们学习。</p> <p>很遗憾曾经的系列文章没有写完,现在已经没机会甚至可以说不可能去把它完结了;不过幸运的是,我的良师益友包老师(我习惯称呼他为包哥)写了一本关于插件化的书——《Android插件化开发指南》,书中讲述了过去数年浩浩荡荡的插件化历程以及插件技术的方方面面;有兴趣的小伙伴可以买一本看看。</p>
<p>众所周知,Android P 引入了<a href="https://developer.android.com/preview/restrictions-non-sdk-interfaces?hl=zh-cn">针对非 SDK 接口(俗称为隐藏API)的使用限制</a>。这是继 Android N上<a href="https://developer.android.com/about/versions/nougat/android-7.0-changes?hl=zh-cn">针对 NDK 中私有库的链接限制</a>之后的又一次重大调整。从今以后,不论是native层的NDK还是 Java层的SDK,我们只能使用Google提供的、公开的标准接口。这对开发者以及用户乃至整个Android生态,当然是一件好事。但这也同时意味着Android上的各种黑科技有可能会逐渐走向消亡。</p> <p>作为一个有追求的开发者,我们既要尊重并遵守规则,也要有能力在必要的时候突破规则的束缚,带着镣铐跳舞。恰好最近有人反馈 <a href="https://github.com/android-hacker/VirtualXposed/issues/115">VirtualXposed 在 Android P上无法运行</a>,那么今天就来探讨一下,如何突破Android P上针对非SDK接口调用的限制。</p>
<p>Xposed是Android系统上久负盛名的一个框架,它给了普通用户任意 DIY 系统的能力;比如典型的微信防撤回、自动抢红包、修改主题字体,以及模拟位置等等等等。不过,使用Xposed的前提条件之一就是需要Root。随着Android系统的演进,这一条件达成越来越难了;那么,<strong>能不能不用Root就可以享用Xposed的功能呢?</strong></p> <p>我们想一下,Xposed为什么需要Root?从现在的实现来看,因为Xposed需要修改系统文件,而这些文件只有root权限才能修改;但是这只是当前实现的特性(修改系统分区文件),而非根本原因。Xposed要实现的最终目的是在任意App进程启动之前能任意加载 <strong>特定Xposed模块</strong> 的代码;这些特定的Xposed模块中能在App进程启动之前有机会执行特定代码,从而控制任意进程的行为。归根结底,Xposed需要控制别的进程,而没有高级权限(Root),越俎代庖是不行的。</p> <p>有没有别的实现方式?</p>
<p>两年前阿里开源了<a href="https://github.com/alibaba/dexposed">Dexposed</a> 项目,它能够在Dalvik上无侵入地实现运行时方法拦截,正如其介绍「enable ‘god’ mode for single android application」所言,能在非root情况下掌控自己进程空间内的任意Java方法调用,给我们带来了很大的想象空间。比如能实现运行时AOP,在线热修复,做性能分析工具(拦截线程、IO等资源的创建和销毁)等等。然而,随着ART取代Dalvik成为Android的运行时,一切都似乎戛然而止。</p> <p>今天,我在ART上重新实现了Dexposed,在它能支持的平台(Android 5.0 ~ 7.1 Thumb2/ARM64)上,有着与Dexposed完全相同的能力和API;项目地址在这里 <a href="https://github.com/tiann/epic">epic</a>,感兴趣的可以先试用下:) 然后我们聊一聊ART上运行时Method AOP的故事。</p>
<p>Android上的热修复框架 AndFix 想必已经是耳熟能详,它的原理实际上很简单:方法替换——Java层的每一个方法在虚拟机实现里面都对应着一个ArtMethod的结构体,只要把原方法的结构体内容替换成新的结构体的内容,在调用原方法的时候,真正执行的指令会是新方法的指令;这样就能实现热修复,详细代码见 <a href="https://github.com/alibaba/AndFix">AndFix</a>。</p> <p>为什么可以这么做呢?那得从 Android 虚拟机的方法调用过程说起。作为一个系列的开篇,本文不打算展开讲虚拟机原理等内容,首先给大家一道开胃菜;后续我们再深入探索ART。</p> <p>众所周知,AndFix是一种 native 的hotfix方案,它的替换过程是用 c 在 native层完成的,但其实,我们也可以用纯Java实现它!而且,代码还非常精简,且看——</p>