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

安卓加固反调试核心机制:D-Bus监听与/proc/self/maps检测绕过实战

1. 这不是“绕过检测”,而是理解检测者如何思考

你打开一个加固过的金融类App,Frida一挂上去,进程秒退;换上repack后的so,刚调用Java.perform就抛出SecurityException;甚至只是加载了frida-gadget.so,应用在Application.attachBaseContext()里就直接System.exit(0)。这不是玄学,也不是“加固太强”,而是你还没看清对手的防御逻辑——它根本没在防Frida,它在防被调试、被注入、被枚举这三件基础事实。而D-Bus和/proc/self/maps,正是现代安卓反调试体系中两把最锋利、也最容易被忽视的“哨兵”。

这篇实战记录,讲的不是“怎么让Frida跑起来”,而是如何像防守方一样思考:他们靠什么发现你?信号链路在哪里中断?哪个环节的判断最脆弱?核心关键词是:D-Bus通信监听、maps内存映射扫描、JNI_OnLoad钩子劫持、ptrace检测规避、动态符号解析绕过。它适合两类人:一类是已经能用Frida hookToast.makeText,但面对加固App就束手无策的中级逆向者;另一类是正在做App安全加固方案,需要知道攻击面到底在哪的安全工程师。你不需要会写C++,但得懂/proc/self/maps里每行代表什么,得明白D-Bus不是Linux桌面才有的东西——在Android 8.0+,它早已是系统级IPC主干道。下面所有操作,均基于真实加固样本(某头部券商App v5.7.2)复现,不依赖任何第三方“免检测”插件,所有代码可直接粘贴进你的script.js运行。

2. D-Bus:那个你以为只在GNOME里跑的“地下交通网”

2.1 为什么加固App要监听D-Bus?

先破除一个常见误解:D-Bus不是安卓原生IPC机制(Binder才是),但它从Android 8.0(Oreo)起,被Google官方集成进system_serverzygote进程,用于协调hal_service_managervintfhwservicemanager等底层服务。更重要的是,大量商业加固SDK(如360、腾讯御安全、梆梆)利用D-Bus通道,主动向系统服务发送心跳探测请求,并监听特定D-Bus信号来确认环境完整性

具体到检测逻辑,典型路径是:

  1. App启动时,通过libdbus.so调用dbus_bus_get(DBUS_BUS_SYSTEM, &error)连接系统总线;
  2. org.freedesktop.DBus服务发送org.freedesktop.DBus.GetNameOwner请求,查询org.freedesktop.DBus自身是否由system_server持有;
  3. 同时监听org.freedesktop.DBus.NameOwnerChanged信号,一旦发现org.freedesktop.DBus的owner从1000(system_server PID)变成其他值(比如12345,某个Frida gadget进程PID),立即触发自毁流程。

提示:这个检测极难被静态分析发现。因为libdbus.so是系统库,调用链常藏在libxxx_security.so的JNI层深处,且dbus_bus_get参数DBUS_BUS_SYSTEM在编译期被宏定义为整数1,IDA里看到的只是call sub_XXXX; mov r0, #1,毫无语义。

2.2 Frida侧如何伪造D-Bus响应?

Frida本身不拦截D-Bus通信,但我们可以劫持其底层socket读写。关键在于定位D-Bus客户端使用的socket fd。观察/proc/self/fd/目录,D-Bus连接通常绑定在fd=3或fd=4(因进程启动顺序而异)。我们用以下脚本精准定位并篡改响应:

// D-Bus伪造核心:劫持dbus_connection_read_write() Java.perform(() => { const dbusLib = Module.findBaseAddress("libdbus.so"); if (!dbusLib) return; // 定位dbus_connection_read_write函数(Android 11+符号名) const readWriteAddr = Module.findExportByName("libdbus.so", "dbus_connection_read_write"); if (!readWriteAddr) { console.log("[D-Bus] 未找到dbus_connection_read_write,尝试旧版符号"); // Android 9-10常用符号 const altAddr = Module.findExportByName("libdbus.so", "dbus_connection_read_write_dispatch"); if (altAddr) { Interceptor.attach(altAddr, { onEnter: function(args) { this.conn = args[0]; }, onLeave: function(retval) { // 此处注入伪造逻辑 this.fakeDBusResponse(this.conn); } }); } return; } Interceptor.attach(readWriteAddr, { onEnter: function(args) { this.conn = args[0]; // 获取当前socket fd(通过conn结构体偏移) // D-Bus connection结构体中,fd存储在偏移0x18处(实测Android 11 AOSP) try { const fdPtr = this.conn.add(0x18).readU32(); if (fdPtr > 0 && fdPtr < 1024) { this.fd = fdPtr; console.log(`[D-Bus] 检测到D-Bus连接,fd=${this.fd}`); } } catch (e) { console.log(`[D-Bus] fd读取失败: ${e}`); } }, onLeave: function(retval) { if (this.fd && this.fd > 0) { this.fakeDBusResponse(this.fd); } } }); }); // 伪造响应:当检测到NameOwnerChanged信号时,返回system_server的PID function fakeDBusResponse(fd) { // 使用sendfile或writev向fd写入伪造的D-Bus消息 // 构造最小化合法D-Bus信号:NameOwnerChanged('org.freedesktop.DBus', '1000', '1000') // D-Bus消息头固定24字节,此处省略完整序列化,仅示意关键字段 const fakeSignal = [ 0x6c, 0x00, 0x00, 0x00, // LITTLE_ENDIAN, TYPE_SIGNAL 0x01, 0x00, 0x00, 0x00, // MAJOR_VERSION=1 0x00, 0x00, 0x00, 0x00, // BODY_LENGTH=0 0x00, 0x00, 0x00, 0x00, // SERIAL=0 0x00, 0x00, 0x00, 0x00, // FIELDS_ARRAY_LENGTH=0 // ... 后续为完整信号体,含"org.freedesktop.DBus"和"1000" ]; const buf = Memory.alloc(fakeSignal.length); buf.writeByteArray(fakeSignal); // 调用write(fd, buf, len) const writeAddr = Module.findExportByName(null, "write"); if (writeAddr) { const ret = new NativeCallback(function(fd, buf, count) { return 0; // 强制返回0,表示写入成功但实际丢弃 }, 'int', ['int', 'pointer', 'uint']); // 实际中需用Memory.patchCode注入,此处简化为日志 console.log(`[D-Bus] 已拦截并丢弃可疑信号,fd=${fd}`); } }

这段代码的核心思想是:不阻止D-Bus通信发生,而是让通信“看起来正常”,但关键信号内容被静默过滤。实测中,加固SDK发送的GetNameOwner请求仍能收到1000的响应,而它期待监听的NameOwnerChanged信号则永远无法抵达Java层——因为我们在socket层就把它吃掉了。

注意:dbus_connection_read_write的偏移量在不同Android版本差异极大。Android 9用dbus_connection_read_write_dispatch,Android 11用dbus_connection_read_write,而Android 12+可能用dbus_connection_read_write_with_timeout。务必用objdump -T libdbus.so | grep read_write确认符号名,再用readelf -s libdbus.so | grep connection查结构体布局。我踩过的最大坑是:在Pixel 4a(Android 12)上,conn结构体中fd偏移从0x18变成0x20,导致伪造失效,进程直接崩溃。

2.3 更隐蔽的方案:直接Hook D-Bus消息解析器

上述socket劫持仍有风险——如果加固SDK使用dbus_message_new_signal构造消息后,不走socket而走内存共享(如ashmem),就会绕过。此时需深入一层,Hook消息解析函数:

// Hook dbus_message_get_type 和 dbus_message_get_interface const msgGetType = Module.findExportByName("libdbus.so", "dbus_message_get_type"); if (msgGetType) { Interceptor.attach(msgGetType, { onEnter: function(args) { this.msg = args[0]; }, onLeave: function(retval) { // 如果是信号类型(retval == 4),且interface是"org.freedesktop.DBus" if (retval.toInt32() === 4) { const getInterface = Module.findExportByName("libdbus.so", "dbus_message_get_interface"); if (getInterface) { const ifacePtr = new NativeCallback(function(msg) { return ptr("org.freedesktop.DBus"); // 强制返回固定字符串指针 }, 'pointer', ['pointer']); // 此处需用Memory.patchCode重写get_interface函数逻辑 console.log("[D-Bus] 强制接口名返回 org.freedesktop.DBus"); } } } }); }

这个方案更底层,但代价是稳定性下降。dbus_message_get_interface在AOSP中是inline函数,部分厂商ROM会将其内联展开,导致Hook失效。我的经验是:优先用socket劫持(成功率90%),仅当加固SDK明确使用内存共享IPC时,才启用此方案

3. /proc/self/maps:那个被当成“内存快照”的致命证据

3.1 maps文件为什么是反调试的黄金线索?

/proc/self/maps是Linux内核为每个进程生成的虚拟内存映射表,每一行代表一段内存区域的权限(rwx)、偏移、设备号、inode及映射文件名。对加固SDK而言,它是一份无需权限、实时、不可伪造的进程环境快照。典型检测点有三个:

检测项正常App表现Frida注入后表现检测逻辑
frida-gadget.so不存在/data/local/tmp/frida-gadget.sostrstr(line, "frida") != NULL
libdobby.so不存在/data/app/xxx/lib/arm64/libdobby.so检查知名Hook框架so
内存页权限异常[anon:.bss][anon:frida][anon:hook]检查[anon:段的命名

但最狠的一招是:检查/system/lib64/libc.so的映射地址是否与/proc/sys/kernel/randomize_va_space开启时的预期范围一致。Frida默认使用dlopen加载gadget,会改变libc基址的随机化偏移,而加固SDK通过预计算libc在ASLR下的合法地址区间,一旦发现偏差超过±0x10000,立即判定为被注入。

提示:这个检测比字符串匹配更难绕过,因为它不依赖文件名,而是基于内存布局的数学特征。我在分析某银行App时,发现它用/proc/self/mapslibc.so的起始地址减去/system/lib64/ld-android.so的起始地址,得到一个固定差值(约0x3a0000),若实际差值偏离该值±0x5000,则触发abort()

3.2 如何让maps“看起来干净”?

核心思路是:不让Frida的so出现在maps里,或者让它出现得“合理”。有三种实操路径:

方案A:Preload注入(推荐,成功率最高)

不使用frida -U -f com.xxx -l script.js,而是将frida-gadget.so通过LD_PRELOAD注入。这样gadget会在libc加载前就被映射,其内存段在maps中显示为[vdso][vvar]的邻近区域,而非独立的frida-gadget.so行。

# 在root设备上执行 adb shell "export LD_PRELOAD=/data/local/tmp/frida-gadget.so; am start -n com.xxx/.MainActivity"

但此方案需App未设置android:sharedUserIdandroid:process隔离,否则preload会失败。实测某证券App因使用sharedUserId="android.uid.system",导致preload被SELinux策略拒绝。

方案B:内存马注入(高阶,需Root)

完全绕过so文件,将Frida gadget的二进制代码直接写入目标进程内存,并手动调用mmap分配可执行页,最后跳转到frida_gadget_main入口。这需要ptrace权限,但注入后maps中完全不会出现任何frida相关字符串

// C代码片段(需编译为arm64可执行文件) #include <sys/mman.h> #include <unistd.h> #include <fcntl.h> int inject_frida_gadget(pid_t pid) { int fd = open("/data/local/tmp/frida-gadget.so", O_RDONLY); void* code = mmap(NULL, 0x10000, PROT_READ, MAP_PRIVATE, fd, 0); // 用ptrace将code写入目标进程内存 // 调用mmap在目标进程分配EXEC内存 // 将code memcpy过去 // 修改PC寄存器跳转执行 return 0; }

此方案复杂度高,但效果极致。我在某政务App(深度加固)上实测,maps中libc.so地址与预设值完全吻合,加固SDK的check_maps_integrity()函数返回true

方案C:maps文件劫持(兼容性最强)

当无法Root或Preload失败时,直接Hookopenat系统调用,当检测到pathname包含"/proc/self/maps"时,返回一个伪造的maps文件描述符:

// Hook openat系统调用 const openatAddr = Module.findExportByName(null, "openat"); if (openatAddr) { Interceptor.attach(openatAddr, { onEnter: function(args) { const pathname = args[1].readCString(); if (pathname && pathname.includes("/proc/self/maps")) { this.isMapsOpen = true; console.log("[maps] 拦截openat for /proc/self/maps"); } }, onLeave: function(retval) { if (this.isMapsOpen && retval.toInt32() > 0) { // 替换fd对应的文件内容 const fakeMaps = generateCleanMaps(); // 生成不含frida的maps字符串 const mem = Memory.allocUtf8String(fakeMaps); // 此处需用dup2或重定向fd,简化为日志 console.log("[maps] 已返回伪造maps内容"); } } }); }

此方案的难点在于:openat之后还有readclose调用,需完整Hook整个IO链路。但优点是无需Root,适用于绝大多数场景。我建议作为保底方案——当Preload失败时,立刻启用此方案。

4. 双管齐下:D-Bus与maps协同防御的破解链路

4.1 为什么必须同时突破两者?

单独绕过D-Bus或maps,90%的加固App仍会崩溃。原因在于检测逻辑的冗余设计:D-Bus负责“主动探测外部环境”,maps负责“被动验证内部状态”,二者形成交叉验证闭环。例如:

  • 若只伪造D-Bus响应,但maps中仍存在frida-gadget.so,加固SDK会认为“外部欺骗成功,但内部已被污染”,触发kill(getpid(), SIGKILL)
  • 若只隐藏maps中的frida,但D-Bus监听到org.freedesktop.DBusowner变更,会判定“环境被接管”,执行System.exit(1)

真正的突破点,在于让两个检测源给出相互印证的“干净”结果。这就要求我们的Hook必须满足:D-Bus返回1000(system_server PID),同时maps中libc.so地址落在合法区间,且无可疑so。

4.2 完整实战步骤:从启动到稳定Hook

以某券商App(加固版本v5.7.2)为例,完整操作链如下:

步骤1:环境准备与初始探测
# 1. 确认设备架构与Android版本 adb shell getprop ro.product.cpu.abi # arm64-v8a adb shell getprop ro.build.version.release # 12 # 2. 提取加固so并静态分析 adb pull /data/app/com.xxx-*/lib/arm64/ lib_arm64/ strings lib_arm64/libsecurity.so | grep -i "dbus\|maps\|ptrace" # 输出: "checking dbus owner" "validate maps libc base" "ptrace check failed" # 3. 获取libc基址参考值(在未注入时) adb shell cat /proc/$(pidof com.xxx)/maps | grep "libc.so" # 输出: 7f8a123000-7f8a1a4000 r-xp 00000000 fd:00 1234567 /system/lib64/libc.so # 记录起始地址:0x7f8a123000
步骤2:Preload注入尝试(首选)
# 推送gadget(注意版本匹配!) adb push frida-gadget-15.1.17-android-arm64.so /data/local/tmp/frida-gadget.so # 设置SELinux策略(关键!) adb shell "su -c 'setenforce 0'" # Preload启动 adb shell "export LD_PRELOAD=/data/local/tmp/frida-gadget.so; am start -n com.xxx/.SplashActivity"

现象:App启动,但3秒后闪退。查看logcat:

E SecuritySDK: D-Bus owner mismatch: expected 1000, got 12345

说明D-Bus检测未过,Preload虽隐藏了so,但未解决D-Bus通信问题。

步骤3:注入D-Bus伪造脚本

将2.2节的fakeDBusResponse脚本保存为dbus-fix.js,用Frida附加:

frida -U -f com.xxx -l dbus-fix.js --no-pause

现象:App启动后卡在Splash页,logcat无报错。用adb shell ps | grep xxx发现进程仍在,但UI无响应。此时检查maps:

adb shell cat /proc/$(pidof com.xxx)/maps | grep "frida" # 输出:空(Preload成功隐藏so) adb shell cat /proc/$(pidof com.xxx)/maps | grep "libc.so" # 输出:7f8a123000-7f8a1a4000 ... /system/lib64/libc.so (地址未变)

D-Bus和maps均“干净”,但App仍卡死——说明存在第三重检测:JNI_OnLoad劫持

步骤4:定位并绕过JNI_OnLoad检测

加固SDK常在JNI_OnLoad中插入检测逻辑。用Frida搜索:

// 查找所有JNI_OnLoad Process.enumerateModules({ onMatch: function(module) { if (module.name.includes("lib") && module.name.includes(".so")) { const onLoad = module.findExportByName("JNI_OnLoad"); if (onLoad) { console.log(`[JNI] Found JNI_OnLoad in ${module.name} at ${onLoad}`); Interceptor.attach(onLoad, { onEnter: function(args) { console.log(`[JNI] ${module.name} JNI_OnLoad called`); } }); } } }, onComplete: function() {} });

输出发现libsecurity.soJNI_OnLoad被调用两次——第一次是系统加载,第二次是加固SDK主动dlopen后再次调用。第二次调用时,它会检查g_env->GetVm()->AttachCurrentThread返回的JNIEnv是否被篡改。

绕过方案:在libsecurity.soJNI_OnLoad第二次调用时,直接return 0,跳过所有检测代码:

// Hook libsecurity.so的JNI_OnLoad const secModule = Process.getModuleByName("libsecurity.so"); if (secModule) { const onLoad = secModule.findExportByName("JNI_OnLoad"); let callCount = 0; Interceptor.attach(onLoad, { onEnter: function(args) { callCount++; console.log(`[JNI] libsecurity.so JNI_OnLoad #${callCount}`); }, onLeave: function(retval) { if (callCount === 2) { console.log("[JNI] 第二次调用,强制返回JNI_VERSION_1_6"); // 修改返回值为0x00010006 (JNI_VERSION_1_6) retval.replace(ptr("0x00010006")); } } }); }
步骤5:最终验证与稳定Hook

完成以上三步后,App启动流畅。此时可安全加载业务脚本:

// final-hook.js Java.perform(() => { const cls = Java.use("com.xxx.security.SecurityManager"); cls.checkLogin.implementation = function() { console.log("[HOOK] checkLogin bypassed"); return true; }; });
frida -U -f com.xxx -l dbus-fix.js -l jni-bypass.js -l final-hook.js --no-pause

验证指标

  • App启动无闪退,功能正常使用;
  • logcat中无SecurityExceptionD-Bus owner mismatchmaps validation failed等关键字;
  • adb shell cat /proc/$(pidof com.xxx)/maps | grep frida返回空;
  • frida-ps -U能持续看到进程,且frida-trace可捕获任意Java方法。

5. 经验总结:那些文档里不会写的实战细节

5.1 版本陷阱:Frida、Android、加固SDK的三角兼容性

Frida 15.x在Android 12+上默认使用dlopen加载gadget,这会破坏libc的ASLR偏移,导致maps检测失败。解决方案不是降级Frida,而是编译自定义gadget:下载Frida源码,修改gum/gumdarwin.cgum_darwin_module_loaddlopen的替代实现(如mmap + memcpy),重新编译frida-gadget.so。我实测,自定义gadget在Android 12上maps地址偏差从±0x80000降至±0x2000,完美通过检测。

5.2 SELinux:那个总在最后关头跳出来的“守门员”

很多加固App在/system/etc/selinux/plat_sepolicy.cil中添加了自定义规则,禁止untrusted_app域执行ptraceopenat。Preload失败的真正原因,90%是SELinux拒绝。快速验证:

adb shell "su -c 'cat /proc/$(pidof com.xxx)/attr/current'" # 输出:u:r:untrusted_app:s0:c123,c456 adb shell "su -c 'sesearch -A -s untrusted_app -t untrusted_app -c capability -p ptrace'" # 若无输出,说明ptrace被禁

此时不能简单setenforce 0(部分厂商ROM会重启恢复),而应临时切换SELinux域

adb shell "su -c 'runcon u:r:shell:s0 cat /proc/$(pidof com.xxx)/maps'"

runcon以shell域运行,不受untrusted_app限制,可读取maps。此技巧在应急分析时极为高效。

5.3 日志对抗:如何让加固SDK“看不见”你的Hook

加固SDK常调用__android_log_print输出检测日志,这些日志会被Frida的console.log捕获,暴露Hook行为。终极方案是Hook__android_log_print本身

const logPrint = Module.findExportByName("liblog.so", "__android_log_print"); if (logPrint) { Interceptor.attach(logPrint, { onEnter: function(args) { const tag = args[1].readCString(); const msg = args[2].readCString(); // 屏蔽所有含"security"、"detect"、"frida"的日志 if (tag && (tag.includes("security") || tag.includes("detect")) || msg && (msg.includes("frida") || msg.includes("hook"))) { console.log(`[LOG] 屏蔽敏感日志: ${tag} - ${msg.substring(0,50)}...`); this.suppress = true; } }, onLeave: function(retval) { if (this.suppress) { // 不调用原函数,直接返回 retval.replace(ptr("0")); } } }); }

此方案让加固SDK的检测日志“石沉大海”,既避免暴露,又防止日志刷屏干扰分析。

5.4 最后一道防线:ptrace检测的绕过本质

所有加固SDK的ptrace检测,最终都归结为ptrace(PTRACE_TRACEME, 0, 0, 0)的返回值。但Frida的frida-gadget并不直接调用ptrace,而是通过libfridagum_interceptor_enable间接触发。绕过关键在于:ptrace系统调用进入内核前,篡改其参数或返回值

// Hook ptrace系统调用(需root) const ptraceAddr = Module.findExportByName(null, "ptrace"); if (ptraceAddr) { Interceptor.attach(ptraceAddr, { onEnter: function(args) { // 当request == PTRACE_TRACEME (0) 时,强制返回0(成功) if (args[0].toInt32() === 0) { console.log("[ptrace] 拦截PTRACE_TRACEME,强制返回0"); this.forceSuccess = true; } }, onLeave: function(retval) { if (this.forceSuccess) { retval.replace(ptr("0")); // 0表示成功 } } }); }

此方案是“最后一公里”,当所有上层检测都被绕过,却仍因ptrace失败而崩溃时,启用它即可。我在某政务App上,正是靠此方案让Frida稳定运行超2小时。

我在实际操作中发现,最有效的组合是:Preload注入 + D-Bus socket劫持 + JNI_OnLoad跳过。这三者覆盖了95%的加固场景,且无需Root,适配从Android 8到13的所有主流ROM。而maps文件劫持和ptrace Hook,应作为“特种作战”工具,仅在遇到深度定制加固时启用。记住,逆向不是堆砌技术,而是选择最轻量、最稳定、最不易被发现的那条路径——就像老猎人不会用加特林打鸟,而会用一把磨得发亮的匕首。

http://www.jsqmd.com/news/875032/

相关文章:

  • Debian挂载NFS远程硬盘踩坑实录:权限拒绝、连接超时问题一站式解决
  • 智慧医院边缘计算架构:QoS驱动的低延迟医疗物联网实践
  • C51嵌入式开发中的栈下溢检测与实现
  • 机器学习模型监控实战:KS检验与BC系数在大数据供应链预测中的应用
  • 【CC Switch】The All-in-One API Manager for AI Coding CLIs
  • CoQMoE:面向FPGA的MoE-ViT量化与硬件协同设计实践
  • AI加速器硬件安全防护技术与实践
  • 统信UOS/麒麟KYLINOS系统管理员必备:一键脚本批量清除所有用户的数科OFD阅读历史
  • 大数据供应链预测模型监控:KS检验与Bhattacharyya系数的工程实践
  • Arm Development Studio许可协议核心条款与合规指南
  • 图像翻译新思路:BBDM如何用‘布朗桥’在潜在空间里‘搭桥’,5分钟看懂原理与PyTorch实现
  • 基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年5月23日
  • CAD+MLIP:高效计算固体振动自由能与热力学性质的技术实践
  • Win11已加密?统信UOS 1060双系统安装后数据盘共享踩坑实录与解决方案
  • 机器学习赋能智能建筑:从能耗预测到个性化舒适度优化
  • Ubuntu 22.04 拔SD卡后二次插入报错?一招 `sudo systemctl restart udisks2` 快速解决
  • 移动3D打印的地形适应与智能控制技术解析
  • 从零到一:用 LangChain 搭建你的第一个 AI Agent,让 LLM 自己干活!
  • ARCADE:用AR任务驱动评估,弥合CV模型指标与真实感知的鸿沟
  • Arm调试中MEM-AP访问属性的配置与应用
  • Keil MDK网络调试中TCP序列号错误分析与优化
  • 机器学习势函数在氧化镓多晶型相变模拟中的应用与验证
  • 手把手教你用命令行管理BitLocker:快速解密‘等待激活’的C盘/D盘(附原理图解)
  • 科学计算中线性与非线性模型选择:从数据特性到应用场景的决策指南
  • 电池阻抗测量技术:伪随机序列与信号处理应用
  • WinPE + DiskGenius 实战:给单硬盘Windows系统加装ESP分区,实现Legacy到UEFI引导切换
  • 年轻人为何对AI成功学集体嘘声?
  • 用格拉姆矩阵特征值调整替代SVD,高效求解带正交约束的优化问题
  • AArch64架构下非缓存内存的指令缓存机制解析
  • 翻译工具:AI跨语言执行任务