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

Linux 访问进程地址空间函数 access_process_vm

文章目录

  • 一、源码解析
  • 二、Linux内核 用途
    • 2.1 ptrace请求
    • 2.2 进程的命令行
  • 参考资料

一、源码解析

/*** get_task_mm - acquire a reference to the task's mm** Returns %NULL if the task has no mm.  Checks PF_KTHREAD (meaning* this kernel workthread has transiently adopted a user mm with use_mm,* to do its AIO) is not set and if so returns a reference to it, after* bumping up the use count.  User must release the mm via mmput()* after use.  Typically used by /proc and ptrace.*/
struct mm_struct *get_task_mm(struct task_struct *task)
{struct mm_struct *mm;task_lock(task);mm = task->mm;if (mm) {if (task->flags & PF_KTHREAD)mm = NULL;elseatomic_inc(&mm->mm_users);}task_unlock(task);return mm;
}
EXPORT_SYMBOL_GPL(get_task_mm);
/** Access another process' address space.* Source/target buffer must be kernel space,* Do not walk the page table directly, use get_user_pages*/
int access_process_vm(struct task_struct *tsk, unsigned long addr,void *buf, int len, int write)
{struct mm_struct *mm;int ret;mm = get_task_mm(tsk);if (!mm)return 0;ret = __access_remote_vm(tsk, mm, addr, buf, len, write);mmput(mm);return ret;
}

其功能主要在__access_remote_vm函数:

/** Access another process' address space as given in mm.  If non-NULL, use the* given task for page fault accounting.*/
static int __access_remote_vm(struct task_struct *tsk, struct mm_struct *mm,unsigned long addr, void *buf, int len, int write)
{struct vm_area_struct *vma;void *old_buf = buf;down_read(&mm->mmap_sem);/* ignore errors, just check how much was successfully transferred */while (len) {int bytes, ret, offset;void *maddr;struct page *page = NULL;ret = get_user_pages(tsk, mm, addr, 1,write, 1, &page, &vma);if (ret <= 0) {/** Check if this is a VM_IO | VM_PFNMAP VMA, which* we can access using slightly different code.*/
#ifdef CONFIG_HAVE_IOREMAP_PROTvma = find_vma(mm, addr);if (!vma || vma->vm_start > addr)break;if (vma->vm_ops && vma->vm_ops->access)ret = vma->vm_ops->access(vma, addr, buf,len, write);if (ret <= 0)
#endifbreak;bytes = ret;} else {bytes = len;offset = addr & (PAGE_SIZE-1);if (bytes > PAGE_SIZE-offset)bytes = PAGE_SIZE-offset;maddr = kmap(page);if (write) {copy_to_user_page(vma, page, addr,maddr + offset, buf, bytes);set_page_dirty_lock(page);} else {copy_from_user_page(vma, page, addr,buf, maddr + offset, bytes);}kunmap(page);page_cache_release(page);}len -= bytes;buf += bytes;addr += bytes;}up_read(&mm->mmap_sem);return buf - old_buf;
}

__access_remote_vm 函数用于访问另一个进程的地址空间。以下是对该函数的中文说明:
函数开始时使用 down_read(&mm->mmap_sem) 获取内存描述符的 mmap 信号量的读取锁。这确保了同步,并防止对内存映射的并发修改。

接着,函数进入一个循环,直到传输完整个长度 len 的数据或发生错误为止。

在循环内部,函数调用 get_user_pages 来从目标进程的地址空间中检索与给定地址 addr 对应的页面。get_user_pages 函数尝试锁定页面并检索它们。如果返回值 ret 小于等于零,则表示出现错误或地址空间的末尾。

如果 ret 小于等于零,则函数检查地址处的虚拟内存区域(VMA)是否是一种特殊类型(VM_IO | VM_PFNMAP),需要使用不同的访问方式。这个检查用于处理无法直接访问页面的特定情况。如果 VMA 是特殊类型,并且有关联的 access 函数,则调用该函数执行访问操作。如果 access 函数返回的值也是非正数,则终止循环。

如果页面检索成功(ret 是正数),函数继续执行数据传输操作。它根据剩余长度 len 和当前页面的偏移量计算要传输的字节数。然后使用 kmap 将页面映射到内核地址空间以进行直接访问。

如果设置了 write 标志,函数使用 copy_to_user_page 将数据从缓冲区 buf 复制到指定地址 addr 处的映射页面。它还使用 set_page_dirty_lock 将页面标记为脏页。

如果未设置 write 标志,函数使用 copy_from_user_page 将数据从映射页面复制到指定地址 addr 处的缓冲区 buf。

在完成数据传输后,函数使用 kunmap 解除页面在内核地址空间的映射,并使用 page_cache_release 释放页面。

然后,函数更新下一次循环迭代的剩余长度 len、缓冲区指针 buf 和地址 addr。

循环结束后,函数使用 up_read(&mm->mmap_sem) 释放对 mmap 信号量的读取锁。

最后,函数返回传输的字节数(buf - old_buf),表示成功传输的总大小。

__access_remote_vm 函数的作用是允许在内核中访问另一个进程的地址空间。通过该函数,内核可以直接读取或写入指定进程的内存数据,而无需通过用户空间或进程间通信来实现。

这种功能在某些情况下非常有用,例如:
(1)调试:允许调试器在内核级别访问目标进程的内存,以查看其状态、变量值和数据结构。

(2)进程间通信:某些进程间通信机制可能需要在内核中进行数据交换,例如通过共享内存或管道进行高效的数据传输。

(3)内核模块开发:内核模块可能需要读取或修改其他进程的内存数据,以实现特定的功能或扩展性。

(4)效率优化:在某些情况下,直接在内核中访问另一个进程的内存可以提高性能,避免了用户空间和内核空间之间的数据复制开销。

二、Linux内核 用途

2.1 ptrace请求

PTRACE_PEEKTEXT和PTRACE_PEEKDATA是用于在被跟踪进程的内存中读取数据的ptrace系统调用的请求选项。
PTRACE_POKETEXT和PTRACE_POKEDATA是用于向被跟踪进程的内存写入数据的ptrace系统调用的请求选项。

这两个选项父进程读取子进程内存地址空间数据或者写数据到子进程内存地址空间都是用到了access_process_vm函数。

SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,unsigned long, data)
{struct task_struct *child;child = ptrace_get_task_struct(pid);arch_ptrace(child, request, addr, data);	
}
long arch_ptrace(struct task_struct *child, long request,unsigned long addr, unsigned long data)
{ptrace_request(child, request, addr, data);
}
int ptrace_request(struct task_struct *child, long request,unsigned long addr, unsigned long data)
{switch (request) {case PTRACE_PEEKTEXT:case PTRACE_PEEKDATA:return generic_ptrace_peekdata(child, addr, data);case PTRACE_POKETEXT:case PTRACE_POKEDATA:return generic_ptrace_pokedata(child, addr, data);}
}

generic_ptrace_peekdata用来读取子进程的虚拟地址空间地址:

int generic_ptrace_peekdata(struct task_struct *tsk, unsigned long addr,unsigned long data)
{unsigned long tmp;int copied;copied = access_process_vm(tsk, addr, &tmp, sizeof(tmp), 0);if (copied != sizeof(tmp))return -EIO;return put_user(tmp, (unsigned long __user *)data);
}

对于读取子进程的虚拟地址空间,调用access_process_vm时,第五个参数 write = 0,代表是从该进程的虚拟地址空间读取内容。
函数的主要逻辑如下:
(1)声明一个局部变量 tmp,用于存储从目标进程内存中读取的数据。
(2)调用 access_process_vm 函数,传递目标进程的 task_struct 指针(tsk)、目标虚拟地址(addr)、存储数据的缓冲区指针(&tmp)、要读取的数据大小(sizeof(tmp))和读取标志(0)。
(3)access_process_vm 函数尝试访问目标进程的内存,并将读取的数据存储到 tmp 变量中。它返回实际复制的字节数。
(4)检查返回的 copied 是否等于 sizeof(tmp),即判断是否成功复制了全部数据。如果不等于,则表示读取失败,返回错误码 -EIO。
(5)调用 put_user 函数,将 tmp 变量的值复制到用户空间的 data 变量中。
(6)返回复制操作的结果。

这个函数的作用是从另一个进程的指定虚拟地址处读取数据,并将其存储到 data 变量中。它使用了 access_process_vm 函数来访问目标进程的内存,并使用 put_user 函数将读取的数据复制到用户空间。

generic_ptrace_pokedata用来写入数据到子进程的虚拟地址空间地址:

int generic_ptrace_pokedata(struct task_struct *tsk, unsigned long addr,unsigned long data)
{int copied;copied = access_process_vm(tsk, addr, &data, sizeof(data), 1);return (copied == sizeof(data)) ? 0 : -EIO;
}

对于写入数据到子进程的虚拟地址空间地址,调用access_process_vm时,第五个参数 write = 1,代表是向该进程的虚拟地址空间写入新内容。

函数的主要逻辑如下:
(1)声明一个局部变量 copied,用于存储复制的字节数。
(2)调用 access_process_vm 函数,传递目标进程的 task_struct 指针(tsk)、目标虚拟地址(addr)、源数据的指针(&data)、要复制的数据大小(sizeof(data))和写入标志(1)。
(3)access_process_vm 函数尝试访问目标进程的内存,并将源数据复制到目标进程的指定地址处。它返回实际复制的字节数。
(4)检查返回的 copied 是否等于 sizeof(data),即判断是否成功复制了全部数据。如果相等,则表示写入操作成功,返回0;否则,返回错误码 -EIO。

这个函数的作用是向另一个进程的指定虚拟地址处写入数据。它使用了 access_process_vm 函数来访问目标进程的内存,并尝试将源数据复制到目标进程中。函数返回0表示写入操作成功,返回非零错误码表示写入失败。

2.2 进程的命令行

该函数用来获取进程的命令行:

static int proc_pid_cmdline(struct task_struct *task, char * buffer)
{int res = 0;unsigned int len;struct mm_struct *mm = get_task_mm(task);if (!mm)goto out;if (!mm->arg_end)goto out_mm;	/* Shh! No looking before we're done */len = mm->arg_end - mm->arg_start;if (len > PAGE_SIZE)len = PAGE_SIZE;res = access_process_vm(task, mm->arg_start, buffer, len, 0);// If the nul at the end of args has been overwritten, then// assume application is using setproctitle(3).if (res > 0 && buffer[res-1] != '\0' && len < PAGE_SIZE) {len = strnlen(buffer, res);if (len < res) {res = len;} else {len = mm->env_end - mm->env_start;if (len > PAGE_SIZE - res)len = PAGE_SIZE - res;res += access_process_vm(task, mm->env_start, buffer+res, len, 0);res = strnlen(buffer, res);}}
out_mm:mmput(mm);
out:return res;
}

proc_pid_cmdline用于获取指定进程的命令行参数。

struct mm_struct {.....unsigned long arg_start, arg_end, env_start, env_end;.....	
}

struct mm_struct 用于描述进程的地址空间,该结构体包含了和进程地址空间有关的全部细信息。每个进程都有一个对应的 mm_struct 结构体,它存储了进程的地址空间布局、内存映射、页表等关键信息。

arg_start、arg_end、env_start 和 env_end 是用于描述进程的命令行参数和环境变量的起始地址和结束地址。以便访问和操作进程的命令行参数和环境变量。

函数的主要逻辑如下:
(1)调用 get_task_mm 函数获取目标进程的内存描述符(mm_struct)。
(2)计算命令行参数的长度,即 arg_end - arg_start。如果长度超过了页面大小(PAGE_SIZE),将长度截断为页面大小。
(3)调用 access_process_vm 函数,传递目标进程的 task_struct 指针(task)、命令行参数的起始地址(mm->arg_start)、存储命令行参数的缓冲区指针(buffer)、要复制的数据大小(len)和读取标志(0)。
(4)检查返回的 res 是否大于0,并且缓冲区中最后一个字节是否不等于空字符(‘\0’),以及长度是否小于页面大小。如果满足这些条件,说明可能发生了 setproctitle 的情况,需要进一步处理:
使用 strnlen 函数在缓冲区中查找第一个空字符(‘\0’),并将其位置存储在 len 变量中。
如果找到的空字符位置小于 res,说明命令行参数中存在空字符,将其作为有效长度。
否则,计算环境变量的长度(env_end - env_start)。
如果环境变量长度超过了剩余缓冲区的大小(PAGE_SIZE - res),将长度截断为剩余缓冲区的大小。
调用 access_process_vm 函数,从环境变量的起始地址(mm->env_start)开始,将环境变量的内容复制到缓冲区中(buffer+res),并返回实际复制的字节数。
使用 strnlen 函数在缓冲区中查找第一个空字符(‘\0’),并将其位置存储在 res 变量中。

这个函数的作用是获取指定进程的命令行参数,并将其存储在提供的缓冲区中。它首先获取进程的内存描述符,然后通过访问进程的内存来获取命令行参数。如果发现命令行参数中的空字符被覆盖,它还会尝试获取环境变量并将其添加到缓冲区中。最后,函数返回存储的命令行参数的长度。

参考资料

Linux 3.10.0

相关文章:

Linux 访问进程地址空间函数 access_process_vm

文章目录 一、源码解析二、Linux内核 用途2.1 ptrace请求2.2 进程的命令行 参考资料 一、源码解析 /*** get_task_mm - acquire a reference to the tasks mm** Returns %NULL if the task has no mm. Checks PF_KTHREAD (meaning* this kernel workthread has transiently a…...

selenium 动态爬取页面使用教程以及使用案例

Selenium 介绍 概述 Selenium是一款功能强大的自动化Web浏览器交互工具。它可以模拟真实用户在网页上的操作&#xff0c;例如点击、滚动、输入等等。Selenium可以爬取其他库难以爬取的网站&#xff0c;特别是那些需要登录或使用JavaScript的网站。Selenium可以自动地从Web页面…...

小程序中如何查看会员的积分和变更记录

​积分是会员卡的一个重要功能&#xff0c;可以用于激励会员消费和提升用户粘性。在小程序中&#xff0c;商家可以方便地查看会员卡的积分和变更记录&#xff0c;以便更好地了解会员的消费行为和积分变动情况。下面将介绍如何在小程序中查看会员卡的积分和变更记录。 1. 找到指…...

音视频 ffmpeg命令直播拉流推流

直播拉流 ffplay rtmp://server/live/streamName ffmpeg -i rtmp://server/live/streamName -c copy dump.flv对于不是rtmp的协议 -c copy要谨慎使用 直播推流 ffmpeg -re -i out.mp4 -c copy flvrtmp://server/live/streamName参数&#xff1a;-re,表示按时间戳读取文件 参…...

Python钢筋混凝土结构计算.pdf-T001-混凝土强度设计值

以下是使用Python求解上述问题的完整代码&#xff1a; # 输入参数 f_ck 35 # 混凝土的特征抗压强度&#xff08;单位&#xff1a;MPa&#xff09; f_cd 25 # 混凝土的强度设计值&#xff08;单位&#xff1a;MPa&#xff09; # 求解安全系数 gamma_c f_ck / f_cd # …...

长风破浪会有时,直挂云帆济沧海!(工作室年会总结)

前言 我也是有段时间没写过总结性的博客了。最近是很忙的&#xff0c;尤其是年会那两天&#xff0c;我甚至可以说这是我这辈子目前最忙的两天。但这段经历还是很值得我记录下来的&#xff0c;也是给后面有需要的人提供的一些建议。我个人也是第一次筹办这种大型些的活动&#x…...

(数字图像处理MATLAB+Python)第十一章图像描述与分析-第五、六节:边界描述和矩描述

文章目录 一&#xff1a;边界描述&#xff08;1&#xff09;边界链码A&#xff1a;概述B&#xff1a;边界链码改进C&#xff1a;程序 &#xff08;2&#xff09;傅里叶描绘子A&#xff1a;概述B&#xff1a;程序 二&#xff1a;矩描述&#xff08;1&#xff09;矩A&#xff1a;…...

Redis之bigkey问题解读

目录 什么是bigkey&#xff1f; bigkey引发的问题 如何查找bigkey redis-cli --bigkeys MEMORY USAGE bigKey如何删除 渐进式删除 unlink bigKey生产调优 什么是bigkey&#xff1f; bigkey简单来说就是存储本身的key值空间太大&#xff0c;或者hash&#xff0c;list&…...

ElementUI浅尝辄止27:Steps 步骤条

引导用户按照流程完成任务的分步导航条&#xff0c;可根据实际应用场景设定步骤&#xff0c;步骤不得少于 2 步。 1.如何使用&#xff1f; 设置active属性&#xff0c;接受一个Number&#xff0c;表明步骤的 index&#xff0c;从 0 开始。需要定宽的步骤条时&#xff0c;设置s…...

React 18 迁移状态逻辑至 Reducer 中

参考文章 迁移状态逻辑至 Reducer 中 对于拥有许多状态更新逻辑的组件来说&#xff0c;过于分散的事件处理程序可能会令人不知所措。对于这种情况&#xff0c;可以将组件的所有状态更新逻辑整合到一个外部函数中&#xff0c;这个函数叫作 reducer。 使用 reducer 整合状态逻…...

【SA8295P 源码分析】89 - QNX AIS Camera qcarcam_test 可执行程序 main() 函数 源代码流程分析

【SA8295P 源码分析】89 - QNX AIS Camera qcarcam_test 可执行程序 main 函数 源代码流程分析 一、qcarcam_test.cpp main() 函数源码分析二、qcarcam_test_setup_input_ctxt_thread( ) :初始化 gCtxt.inputs[input_idx] 上下文环境三、process_cb_event_thread( ) :负责处理…...

STM32屏幕计时器

目录 一、最终效果二、实现思想三、实现过程3.1 屏幕显示3.2 中断处理 一、最终效果 显示屏显示计时时间&#xff0c;格式为 00:00:00&#xff0c;依次为 时:分:秒&#xff0c;程序运行之后自动计时&#xff0c;当按下按键&#xff0c;计时清零&#xff0c;按下按键采用外部中…...

MRI多任务技术及应用

目录 一、定量心血管磁共振成像&#xff08;CMR&#xff09;的改进方法二、磁共振多任务三、磁共振多任务的成像框架四、磁共振多任务的图像模型和采样和重建策略五、利用MR多任务进行快速三维稳态CEST(ss-CEST)成像5.1 利用MR多任务进行快速三维稳态CEST(ss-CEST)成像介绍5.2 …...

app自动化测试(Android)

Capability 是一组键值对的集合&#xff08;比如&#xff1a;"platformName": "Android"&#xff09;。Capability 主要用于通知 Appium 服务端建立 Session 需要的信息。客户端使用特定语言生成 Capabilities&#xff0c;最终会以 JSON 对象的形式发送给 …...

【力扣每日一题】2023.9.3 消灭怪物的最大数量

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目比较长&#xff0c;我概括一下就是有一群怪物&#xff0c;每只怪物离城市的距离都不一样&#xff0c;并且靠近的速度也不一样&#x…...

Python入门教程 | Python3 列表(List)

Python3 列表 序列是 Python 中最基本的数据结构。 序列中的每个值都有对应的位置值&#xff0c;称之为索引&#xff0c;第一个索引是 0&#xff0c;第二个索引是 1&#xff0c;依此类推。 Python 有 6 个序列的内置类型&#xff0c;但最常见的是列表和元组。 列表都可以进…...

Java低代码开发:jvs-list(列表引擎)功能(一)配置说明

在低代码开发平台中&#xff0c;列表页是一个用于显示数据列表的页面。它通常用于展示数据库中的多条记录&#xff0c;并提供搜索、排序和筛选等功能&#xff0c;以方便用户对数据进行查找和浏览。 jvs-list是jvs快速开发平台的列表页的配置引擎&#xff0c;它和普通的crud 具…...

UI自动化之关键字驱动

关键字驱动框架&#xff1a;将每一条测试用例分成四个不同的部分 测试步骤&#xff08;Test Step&#xff09;&#xff1a;一个测试步骤的描述或者是测试对象的一个操作说明测试步骤中的对象&#xff08;Test Object&#xff09;&#xff1a;指页面的对象或者元素对象执行的动…...

前端高性能渲染 — 虚拟列表

虚拟列表&#xff0c;实际上就是在首屏加载的时候&#xff0c;只加载可视区域内需要的列表项&#xff0c;当滚动发生时&#xff0c;动态通过计算获得可视区域内的列表项&#xff0c;并将非可视区域内存在的列表项删除。该技术是解决渲染大量数据的一种解决方法。 实现虚拟列表&…...

防水出色的骨传导耳机,更适合户外运动,南卡Runner Pro 4S体验

已经接近尾声的夏季依然酷热&#xff0c;对于运动爱好者来说&#xff0c;这确实也是锻炼的好时机&#xff0c;无论是一会儿就能大汗淋漓的HIIT&#xff0c;还是是各种清凉的水上运动&#xff0c;在健身的同时&#xff0c;戴上一副耳机享受音乐&#xff0c;都会更加痛快一些。 相…...

docker快速安装-docker一键安装脚本

1.下载/配置安装脚本 touch install-docker.sh #!/bin/bash #mail:ratelcloudqq.com #system:centos7 #integration: docker-latestclear echo "######################################################" echo "# Auto Install Docker …...

1584 - Circular Sequence (UVA)

题目链接如下&#xff1a; Online Judge 我的代码如下&#xff1a; #include <cstdio> #include <string.h> const int maxN 101;int T, len, pivot; char a[maxN];int main(){scanf("%d", &T);for(int i 0; i < T; i){scanf("%s"…...

Revit SDK:Selections 选择

前言 Revit 作为一款成熟的商业软件&#xff0c;它将自己的UI选择功能也通过 API 暴露出来。通过 API 可以按照特定的过滤规则来选择相应的元素&#xff0c;能力和UI基本上是等价的。这个 SDK 用四个例子展示了 API 的能力&#xff0c;内容如下。 内容 PickforDeletion 核心…...

K8s中的RBAC(Role-Based Access Control)

摘要 RBAC&#xff08;基于角色的访问控制&#xff09;是一种在Kubernetes中用于控制用户对资源的访问权限的机制。以下是RBAC的设计实现说明&#xff1a; 角色&#xff08;Role&#xff09;和角色绑定&#xff08;RoleBinding&#xff09;&#xff1a;角色定义了一组权限&am…...

肖sir__设计测试用例方法之经验测试方法09_(黑盒测试)

设计测试用例方法之经验测试方法 一、经验的测试技术 &#xff08;1&#xff09;基于经验的测试技术之错误推测法 错误推测法也叫错误猜测法&#xff0c;就是根据经验猜想&#xff0c;已有的缺陷&#xff0c;测试经验和失败数据等可能有什么问题并依此设计测试用例 &#xff0…...

Python爬虫:下载小红书无水印图片、视频

该代码只提供学习使用&#xff0c;该项目是基于https://github.com/JoeanAmier/XHS_Downloader的小改动 1.下载项目 git clone https://github.com/zhouayi/XHS_Downloader.git2.找到需要下载的文章的ID 写入main.py中 3.下载 python main.py最近很火的莲花楼为例<嘿嘿…...

【小沐学Unity3d】3ds Max 多维子材质编辑(Multi/Sub-object)

文章目录 1、简介2、精简材质编辑器2.1 先创建多维子材质&#xff0c;后指定它2.2 先指定标准材质&#xff0c;后自动创建多维子材质 3、Slate材质编辑器3.1 编辑器简介3.2 编辑器使用 结语 1、简介 多维子材质&#xff08;Multi/Sub-object&#xff09;是为一个模形&#xff0…...

# Go学习-Day8

文章目录 Go学习-Day8单元测试Goroutine进程和线程并发和并行Go协程和主线程MPG模式CPU相关协程并行的资源竞争 Go学习-Day8 个人博客&#xff1a;CSDN博客 单元测试 testing框架会将xxx_test.go的文件引入&#xff0c;调用所有TestXxx的函数 在cal_test.go文件里面写这个 …...

Maven编译java及解决程序包org.apache.logging.log4j不存在问题

1、首先新建一个文件夹&#xff0c;比如hello Hello里新建pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi…...

【小吉测评】高效简洁的数据库管控平台—CloudQuery

文章目录 &#x1f384;CloudQuery是什么&#x1f6f8;CloudQuery支持的数据源类型&#x1f354;CloudQuery社区地址&#x1f33a;如何使用&#x1f6f8;参考官方文档&#x1f6f8;参考视频教程&#x1f388;点击免费下载&#x1f388;立即下载即可&#x1f388;使用服务器完成…...