Android R adb remount 调用流程

目的:调查adb remount 与adb shell进去后执行remount的差异

调试方法:添加log编译adbd,替换system\apex\com.android.adbd\bin\adbd

一、调查adb remount实现

关键代码:system\core\adb\daemon\services.cpp

unique_fd daemon_service_to_fd(std::string_view name, atransport* transport) {ADB_LOG(Service) << "transport " << transport->serial_name() << " opening service " << name;#if defined(__ANDROID__) && !defined(__ANDROID_RECOVERY__)if (name.starts_with("abb:") || name.starts_with("abb_exec:")) {return execute_abb_command(name);}
#endif#if defined(__ANDROID__)if (name.starts_with("framebuffer:")) {return create_service_thread("fb", framebuffer_service);} else if (android::base::ConsumePrefix(&name, "remount:")) {std::string cmd = "/system/bin/remount ";cmd += name;return StartSubprocess(cmd, nullptr, SubprocessType::kRaw, SubprocessProtocol::kNone);} else if (android::base::ConsumePrefix(&name, "reboot:")) {return reboot_device(std::string(name));} else if (name.starts_with("root:")) {return create_service_thread("root", restart_root_service);} else if (name.starts_with("unroot:")) {return create_service_thread("unroot", restart_unroot_service);} else if (android::base::ConsumePrefix(&name, "backup:")) {std::string cmd = "/system/bin/bu backup ";cmd += name;return StartSubprocess(cmd, nullptr, SubprocessType::kRaw, SubprocessProtocol::kNone);} else if (name.starts_with("restore:")) {return StartSubprocess("/system/bin/bu restore", nullptr, SubprocessType::kRaw,SubprocessProtocol::kNone);} else if (name.starts_with("disable-verity:")) {return StartSubprocess("/system/bin/disable-verity", nullptr, SubprocessType::kRaw,SubprocessProtocol::kNone);} else if (name.starts_with("enable-verity:")) {return StartSubprocess("/system/bin/enable-verity", nullptr, SubprocessType::kRaw,SubprocessProtocol::kNone);} else if (android::base::ConsumePrefix(&name, "tcpip:")) {std::string str(name);int port;if (sscanf(str.c_str(), "%d", &port) != 1) {return unique_fd{};}return create_service_thread("tcp",std::bind(restart_tcp_service, std::placeholders::_1, port));} else if (name.starts_with("usb:")) {return create_service_thread("usb", restart_usb_service);}
#endifif (android::base::ConsumePrefix(&name, "dev:")) {return unique_fd{unix_open(name, O_RDWR | O_CLOEXEC)};} else if (android::base::ConsumePrefix(&name, "jdwp:")) {pid_t pid;if (!ParseUint(&pid, name)) {return unique_fd{};}return create_jdwp_connection_fd(pid);} else if (android::base::ConsumePrefix(&name, "shell")) {return ShellService(name, transport);} else if (android::base::ConsumePrefix(&name, "exec:")) {return StartSubprocess(std::string(name), nullptr, SubprocessType::kRaw,SubprocessProtocol::kNone);} else if (name.starts_with("sync:")) {return create_service_thread("sync", file_sync_service);} else if (android::base::ConsumePrefix(&name, "reverse:")) {return reverse_service(name, transport);} else if (name == "reconnect") {return create_service_thread("reconnect", std::bind(reconnect_service, std::placeholders::_1, transport));} else if (name == "spin") {return create_service_thread("spin", spin_service);}return unique_fd{};
}

根据之前调试adb方法可以得知adb remount会打印如下log

I adbd    : transport UsbFfs opening service shell,v2,TERM=xterm-256color,raw:remount

参考文档:Android adb自身调试log开关-CSDN博客

进而会执行上面daemon_service_to_fd函数中的如下else if

else if (android::base::ConsumePrefix(&name, "shell")) {return ShellService(name, transport);

在ShellService中会解析参数和cmd,再调用StartSubprocess

// Shell service string can look like:
//   shell[,arg1,arg2,...]:[command]
unique_fd ShellService(std::string_view args, const atransport* transport) {size_t delimiter_index = args.find(':');if (delimiter_index == std::string::npos) {LOG(ERROR) << "No ':' found in shell service arguments: " << args;return unique_fd{};}// TODO: android::base::Split(const std::string_view&, ...)std::string service_args(args.substr(0, delimiter_index));std::string command(args.substr(delimiter_index + 1));// Defaults://   PTY for interactive, raw for non-interactive.//   No protocol.//   $TERM set to "dumb".SubprocessType type(command.empty() ? SubprocessType::kPty : SubprocessType::kRaw);SubprocessProtocol protocol = SubprocessProtocol::kNone;std::string terminal_type = "dumb";for (const std::string& arg : android::base::Split(service_args, ",")) {if (arg == kShellServiceArgRaw) {type = SubprocessType::kRaw;} else if (arg == kShellServiceArgPty) {type = SubprocessType::kPty;} else if (arg == kShellServiceArgShellProtocol) {protocol = SubprocessProtocol::kShell;} else if (arg.starts_with("TERM=")) {terminal_type = arg.substr(strlen("TERM="));} else if (!arg.empty()) {// This is not an error to allow for future expansion.LOG(WARNING) << "Ignoring unknown shell service argument: " << arg;}}return StartSubprocess(command, terminal_type.c_str(), type, protocol);
}

继续跟踪也没发现adb remount有额外的参数或指令执行,看来只有一个remount,与在串口执行并无差异。

那为何adb remount后可以直接覆盖原文件,而串口执行remount必须先删除原文件才能cp成功呢?

adb remount
#cp system/apex/com.android.adbd/bin/adbd22 system/apex/com.android.adbd/bin/adbd
# sync串口remount
cp system/apex/com.android.adbd/bin/adbd22 system/apex/com.android.adbd/bin/adbd
cp: system/apex/com.android.adbd/bin/adbd: Text file busy

尝试使用adb shell remount,也可以直接覆盖,adb log和adb remount一样

尝试使用adb shell /system/bin/remount,无法覆盖,和串口remount现象一样

adb log如下

03-17 01:19:13.704  8511  8511 I adbd    : transport UsbFfs opening service shell,v2,TERM=xterm-256color,raw:/system/bin/remount
03-17 01:19:13.704  8511  8511 I adbd    : shell  opening service ,v2,TERM=xterm-256color,raw:/system/bin/remount
03-17 01:19:13.704  8511  8511 I adbd    : ShellService xterm-256color opening service /system/bin/remount
03-17 01:19:13.704  8511  8511 E adbd    : starting raw subprocess (protocol=shell, TERM=xterm-256color): '/system/bin/remount'

说明adb remount并不是调用的/system/bin/remount,而是一个系统集成的函数实现的,相当于调用了一个remount系统函数 ,这也就明白为何adb remount没有走daemon_service_to_fd中的单独remount else if(调用的/system/bin/remount)

走哪个remount是system\core\adb\client\commandline.cpp中如下代码实现的

else if (!strcmp(argv[0], "remount")) {FeatureSet features;std::string error;if (!adb_get_feature_set(&features, &error)) {fprintf(stderr, "error: %s\n", error.c_str());return 1;}if (CanUseFeature(features, kFeatureRemountShell)) {std::vector<const char*> args = {"shell"};args.insert(args.cend(), argv, argv + argc);return adb_shell_noinput(args.size(), args.data());} else if (argc > 1) {auto command = android::base::StringPrintf("%s:%s", argv[0], argv[1]);return adb_connect_command(command);} else {return adb_connect_command("remount:");}}

反转又来了。。。 

排查了system/core下remount实现只有一处system\core\fs_mgr\fs_mgr_remount.cpp

查看Android.bp,此文件是编译remount指令的源码,但实现方式的确也和init中的raw命令类似叫do_remount

system\core\init\builtins.cpp中没有remount的实现

// Builtin-function-map start
const BuiltinFunctionMap& GetBuiltinFunctionMap() {constexpr std::size_t kMax = std::numeric_limits<std::size_t>::max();// clang-format offstatic const BuiltinFunctionMap builtin_functions = {{"bootchart",               {1,     1,    {false,  do_bootchart}}},{"chmod",                   {2,     2,    {true,   do_chmod}}},{"chown",                   {2,     3,    {true,   do_chown}}},{"class_reset",             {1,     1,    {false,  do_class_reset}}},{"class_reset_post_data",   {1,     1,    {false,  do_class_reset_post_data}}},{"class_restart",           {1,     1,    {false,  do_class_restart}}},{"class_start",             {1,     1,    {false,  do_class_start}}},{"class_start_post_data",   {1,     1,    {false,  do_class_start_post_data}}},{"class_stop",              {1,     1,    {false,  do_class_stop}}},{"copy",                    {2,     2,    {true,   do_copy}}},{"domainname",              {1,     1,    {true,   do_domainname}}},{"enable",                  {1,     1,    {false,  do_enable}}},{"exec",                    {1,     kMax, {false,  do_exec}}},{"exec_background",         {1,     kMax, {false,  do_exec_background}}},{"exec_start",              {1,     1,    {false,  do_exec_start}}},{"export",                  {2,     2,    {false,  do_export}}},{"hostname",                {1,     1,    {true,   do_hostname}}},{"ifup",                    {1,     1,    {true,   do_ifup}}},{"init_user0",              {0,     0,    {false,  do_init_user0}}},{"insmod",                  {1,     kMax, {true,   do_insmod}}},{"installkey",              {1,     1,    {false,  do_installkey}}},{"interface_restart",       {1,     1,    {false,  do_interface_restart}}},{"interface_start",         {1,     1,    {false,  do_interface_start}}},{"interface_stop",          {1,     1,    {false,  do_interface_stop}}},{"load_persist_props",      {0,     0,    {false,  do_load_persist_props}}},{"load_system_props",       {0,     0,    {false,  do_load_system_props}}},{"loglevel",                {1,     1,    {false,  do_loglevel}}},{"mark_post_data",          {0,     0,    {false,  do_mark_post_data}}},{"mkdir",                   {1,     6,    {true,   do_mkdir}}},// TODO: Do mount operations in vendor_init.// mount_all is currently too complex to run in vendor_init as it queues action triggers,// imports rc scripts, etc.  It should be simplified and run in vendor_init context.// mount and umount are run in the same context as mount_all for symmetry.{"mount_all",               {0,     kMax, {false,  do_mount_all}}},{"mount",                   {3,     kMax, {false,  do_mount}}},{"perform_apex_config",     {0,     0,    {false,  do_perform_apex_config}}},{"umount",                  {1,     1,    {false,  do_umount}}},{"umount_all",              {0,     1,    {false,  do_umount_all}}},{"update_linker_config",    {0,     0,    {false,  do_update_linker_config}}},{"readahead",               {1,     2,    {true,   do_readahead}}},{"remount_userdata",        {0,     0,    {false,  do_remount_userdata}}},{"restart",                 {1,     1,    {false,  do_restart}}},{"restorecon",              {1,     kMax, {true,   do_restorecon}}},

在fs_mgr_remount.cpp添加log,编译remount二进制,发现执行串口remount和adb remount都会打印这个log,说明两条指令又走到一起了。那为何表现会不同?

又有新的发现,若先串口执行remount,再adb remount,CP覆盖也会报错。。。晕了

反复尝试,发现adb remount后也会覆盖报错,奇怪了,难道是我替换后remount后导致的?

先排查到这里,至少基本理清了adb remount的调用流程,也算是有收获。

真相来了。。。

前面adb remount后可以覆盖,串口remount不能覆盖是因为,adb remount后有push 过system/apex/com.android.adbd/bin/adbd这个文件,然后再手动cp就可以覆盖,而无法覆盖都是因为没有先adb push那个文件。所以导致能不能覆盖不是remount导致的,而是adb push 具备特异功能

二、新版本adb push 无需再手动改权限、selinux标签的原因

在老版本Android系统,若push文件到system/bin等路径下,需要手动改权限,若开启了selinux还需要手动改selinux标签,否则会导致系统启动失败或相应服务启动异常。而在新版本Android系统则无需担心,这些步骤由adb自动完成了。

文件:system\core\adb\daemon\file_sync_service.cpp

先看调试log:

03-17 02:27:30.294  7466  8560 E adbd    : sync id_name stat_v2 name:system/apex/com.android.adbd/bin/adbd
03-17 02:27:30.301  7466  8560 E adbd    : sync id_name send_v2 name:system/apex/com.android.adbd/bin/adbd

adb push会先查询目标文件的属性信息并记录,然后才发送文件。在发送文件过程还会记录原文件夹、文件的属性,并会修改 push后的文件的属性

大概的实现代码如下:

static bool handle_sync_command(int fd, std::vector<char>& buffer) {D("sync: waiting for request");SyncRequest request;if (!ReadFdExactly(fd, &request, sizeof(request))) {SendSyncFail(fd, "command read failure");return false;}size_t path_length = request.path_length;if (path_length > 1024) {SendSyncFail(fd, "path too long");return false;}char name[1025];if (!ReadFdExactly(fd, name, path_length)) {SendSyncFail(fd, "filename read failure");return false;}name[path_length] = 0;std::string id_name = sync_id_to_name(request.id);LOG(ERROR) << "sync id_name:" << id_name.c_str() << " name:" << name;switch (request.id) {case ID_LSTAT_V1:if (!do_lstat_v1(fd, name)) return false;break;case ID_LSTAT_V2:case ID_STAT_V2:if (!do_stat_v2(fd, request.id, name)) return false;break;case ID_LIST_V1:if (!do_list_v1(fd, name)) return false;break;case ID_LIST_V2:if (!do_list_v2(fd, name)) return false;break;case ID_SEND_V1:if (!do_send_v1(fd, name, buffer)) return false;break;case ID_SEND_V2:if (!do_send_v2(fd, name, buffer)) return false;break;case ID_RECV_V1:if (!do_recv_v1(fd, name, buffer)) return false;break;case ID_RECV_V2:if (!do_recv_v2(fd, name, buffer)) return false;break;case ID_QUIT:return false;default:SendSyncFail(fd, StringPrintf("unknown command %08x", request.id));return false;}return true;
}
static bool do_stat_v2(int s, uint32_t id, const char* path) {syncmsg msg = {};msg.stat_v2.id = id;decltype(&stat) stat_fn;if (id == ID_STAT_V2) {stat_fn = stat;} else {stat_fn = lstat;}struct stat st = {};int rc = stat_fn(path, &st);if (rc == -1) {msg.stat_v2.error = errno_to_wire(errno);} else {msg.stat_v2.dev = st.st_dev;msg.stat_v2.ino = st.st_ino;msg.stat_v2.mode = st.st_mode;msg.stat_v2.nlink = st.st_nlink;msg.stat_v2.uid = st.st_uid;msg.stat_v2.gid = st.st_gid;msg.stat_v2.size = st.st_size;msg.stat_v2.atime = st.st_atime;msg.stat_v2.mtime = st.st_mtime;msg.stat_v2.ctime = st.st_ctime;}return WriteFdExactly(s, &msg.stat_v2, sizeof(msg.stat_v2));
}
static bool handle_send_file(borrowed_fd s, const char* path, uint32_t* timestamp, uid_t uid,gid_t gid, uint64_t capabilities, mode_t mode, bool compressed,std::vector<char>& buffer, bool do_unlink) {int rc;syncmsg msg;__android_log_security_bswrite(SEC_TAG_ADB_SEND_FILE, path);unique_fd fd(adb_open_mode(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode));if (fd < 0 && errno == ENOENT) {if (!secure_mkdirs(Dirname(path))) {SendSyncFailErrno(s, "secure_mkdirs failed");goto fail;}fd.reset(adb_open_mode(path, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode));}if (fd < 0 && errno == EEXIST) {fd.reset(adb_open_mode(path, O_WRONLY | O_CLOEXEC, mode));}if (fd < 0) {SendSyncFailErrno(s, "couldn't create file");goto fail;} else {if (fchown(fd.get(), uid, gid) == -1) {SendSyncFailErrno(s, "fchown failed");goto fail;}#if defined(__ANDROID__)// Not all filesystems support setting SELinux labels. http://b/23530370.selinux_android_restorecon(path, 0);
#endif// fchown clears the setuid bit - restore it if present.// Ignore the result of calling fchmod. It's not supported// by all filesystems, so we don't check for success. b/12441485fchmod(fd.get(), mode);}{rc = posix_fadvise(fd.get(), 0, 0,POSIX_FADV_SEQUENTIAL | POSIX_FADV_NOREUSE | POSIX_FADV_WILLNEED);if (rc != 0) {D("[ Failed to fadvise: %s ]", strerror(rc));}bool result;if (compressed) {result = handle_send_file_compressed(s, std::move(fd), timestamp);} else {result = handle_send_file_uncompressed(s, std::move(fd), timestamp, buffer);}if (!result) {goto fail;}if (!update_capabilities(path, capabilities)) {SendSyncFailErrno(s, "update_capabilities failed");goto fail;}msg.status.id = ID_OKAY;msg.status.msglen = 0;return WriteFdExactly(s, &msg.status, sizeof(msg.status));}fail:....
}

调用fchmod改push后文件的属性

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/75312.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多模态大语言模型arxiv论文略读(二)

Identifying the Correlation Between Language Distance and Cross-Lingual Transfer in a Multilingual Representation Space ➡️ 论文标题&#xff1a;Identifying the Correlation Between Language Distance and Cross-Lingual Transfer in a Multilingual Representat…

【运维】负载均衡

老规矩&#xff0c;先占坑&#xff0c;后续更新。 开头先理解一下所谓的“均衡”&#xff0c;不能狭义地理解为分配给所有实际服务器一样多的工作量&#xff0c;因为多台服务器的承载能力各不相同&#xff0c;这可能体现在硬件配置、网络带宽的差异&#xff0c;也可能因为某台…

大型语言模型Claude的“思维模式”最近被公开解剖

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Ubuntu环境安装

1. 安装gcc、g和make sudo apt update sudo apt install build-essential 2. 安装cmake ubuntu安装cmake的三种方法&#xff08;超方便&#xff01;&#xff09;-CSDN博客 3. 安装ssh sudo apt-get install libssl-dev

【力扣hot100题】(028)删除链表的倒数第N个节点

链表题还是太简单了。 怕越界所以先定义了一个头结点的头结点&#xff0c;然后定义快慢指针&#xff0c;快指针先走n步&#xff0c;随后一起走&#xff0c;直到快指针走到头&#xff0c;删除慢指针后一个节点即可。 /*** Definition for singly-linked list.* struct ListNod…

C/C++回调函数实现与std::function和std::bind介绍

1 概述 回调函数是一种编程模式&#xff0c;指的是将一个函数作为参数传递给另一个函数&#xff0c;并在某个特定事件发生时或满足某些条件时由该函数调用。这种机制允许你定义在特定事件发生时应执行的代码&#xff0c;从而实现更灵活和模块化的程序设计。 2 传统C/C回调实现…

【蓝桥杯】单片机设计与开发,速成备赛

一、LED模块开看&#xff0c;到大模板 二、刷第零讲题目&#xff08;直接复制模板&#xff09; 三、空降芯片模板直接调用部分&#xff08;听完再敲代码&#xff09; 四、第十三讲开刷省赛题&#xff08;开始自己背敲模板&#xff09; 五、考前串讲刷一遍 b连接&#xff1…

Java 基础-28- 多态 — 多态下的类型转换问题

在 Java 中&#xff0c;多态&#xff08;Polymorphism&#xff09;是面向对象编程的核心概念之一。多态允许不同类型的对象通过相同的方法接口进行操作&#xff0c;而实际调用的行为取决于对象的实际类型。虽然多态提供了极大的灵活性&#xff0c;但在多态的使用过程中&#xf…

Epub转PDF软件Calibre电子书管理软件

Epub转PDF软件&#xff1a;Calibre电子书管理软件 https://download.csdn.net/download/hu5566798/90549599 一款好用的电子书管理软件&#xff0c;可快速导入电脑里的电子书并进行管理&#xff0c;支持多种格式&#xff0c;阅读起来非常方便。同时也有电子书格式转换功能。 …

在 Ubuntu 22.04 上安装 Docker Compose 的步骤

1. 确保已安装 Docker Docker Compose 需要 Docker 作为依赖&#xff0c;请先安装 Docker&#xff1a; sudo apt update sudo apt install docker.io sudo systemctl enable --now docker2. 下载 Docker Compose 二进制文件 推荐安装最新稳定版的 Docker Compose&#xff08…

Mysql-数据库、安装、登录

一. 数据库 1. 数据库&#xff1a;DataBase&#xff08;DB&#xff09;&#xff0c;是存储和管理数据的仓库。 2. 数据库管理系统&#xff1a;DataBase Management System&#xff08;DBMS&#xff09;,操纵管理数据库的大型软件 3. SQL&#xff1a;Structured Query Language&…

基于SpringAOP面向切面编程的一些实践(日志记录、权限控制、统一异常处理)

前言 Spring框架中的AOP&#xff08;面向切面编程&#xff09; 通过上面的文章我们了解到了AOP面向切面编程的思想&#xff0c;接下来通过一些实践&#xff0c;去更加深入的了解我们所学到的知识。 简单回顾一下AOP的常见应用场景 日志记录&#xff1a;记录方法入参、返回值、执…

Rust 语言语法糖深度解析:优雅背后的编译器魔法

之前介绍了语法糖的基本概念和在C/Python/JavaScript中的使用&#xff0c;今天和大家讨论语法糖在Rust中的表现形式。 程序语言中的语法糖&#xff1a;让代码更优雅的甜味剂 引言&#xff1a;语法糖的本质与价值 语法糖(Syntactic Sugar) 是编程语言中那些并不引入新功能&…

【56】数组指针:指针穿梭数组间

【56】数组指针&#xff1a;指针穿梭数组间 引言 在嵌入式系统开发中&#xff0c;指针操作是优化内存管理和数据交互的核心技术。本文以STC89C52单片机为平台&#xff0c;通过一维指针强制转换、二维指针结构化操作和**return返回指针**三种方法&#xff0c;系统讲解指针操作二…

C语言【指针二】

引言 介绍&#xff1a;const修饰指针&#xff0c;野指针 应用&#xff1a;指针的使用&#xff08;strlen的模拟实现&#xff09;&#xff0c;传值调用和传指调用 一、const修饰指针 1.const修饰变量 简单回顾一下前面学过的const修饰变量&#xff1a;在变量前面加上const&…

学习记录-软件测试基础

一、软件测试分类 1.按阶段&#xff1a;单元测试&#xff08;一般开发自测&#xff09;、集成测试、系统测试、验收测试 2.按代码可见度测试&#xff1a;黑盒测试、灰盒测试、白盒测试 3.其他&#xff1a;冒烟测试(冒烟测试主要是在开发提测后进行&#xff0c;主要是测试主流…

RAG系统实战:当检索为空时,如何实现生成模块的优雅降级(Fallback)?

目录 RAG系统实战&#xff1a;当检索为空时&#xff0c;如何实现生成模块的优雅降级&#xff08;Fallback&#xff09;&#xff1f; 一、为什么需要优雅降级&#xff08;Fallback&#xff09;&#xff1f; 二、常用的优雅降级策略 策略一&#xff1a;预设后备提示&#xff0…

spring boot前后端开发上传文件时报413(Request Entity Too Large)错误的可能原因及解决方案

可能原因及解决方案 1. Spring Boot默认文件大小限制 原因&#xff1a;Spring Boot默认单文件最大为1MB&#xff0c;总请求体限制为10MB。解决方案&#xff1a; 在application.properties中配置&#xff1a;spring.servlet.multipart.max-file-size10MB # 单文件最大 spring…

Qt - findChild

findChild 1. 函数原型2. 功能描述3. 使用场景4. 示例代码5. 注意事项6. 总结 在 Qt 中&#xff0c;每个 QObject 都可以拥有子对象&#xff0c;而 QObject 提供的模板函数 findChild 就是用来在对象树中查找满足特定条件的子对象的工具。下面我们详细介绍一下它的使用和注意事…

Sink Token

论文&#xff1a;ICLR 2025 MLLM视觉VAR方法Attention重分配 Sink Token 是一种在语言模型(LLM)和多模态模型(MLLM)中用于优化注意力分配的关键机制&#xff0c;通过吸收模型中冗余的注意力权重&#xff0c;确保注意力资源不被无效或无关信息占用。以下是对这一概念的系统性解…