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.pswp.cn/bicheng/75312.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/75312.shtml
英文地址,請注明出處:http://en.pswp.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;確保注意力資源不被無效或無關信息占用。以下是對這一概念的系統性解…