【Android 内存优化】怎么理解Android PLT hook?

news2024/11/15 13:36:41

文章目录

  • 前言
  • 什么是hook?
  • PLT hook
    • 作用
    • 基本原理
    • PLT hook 总体步骤
  • 代码案例分析
  • 方案预研
    • 面临的问题
    • 怎么做?
    • ELF
      • ELF 文件头
      • SHT(section header table)
    • 链接视图(Linking View)和执行视图(Execution View)
    • 动态链接
  • 猜想-解决-验证
    • 第一次验证
    • 方案
    • 再次验证
  • 参考资料

前言

昨晚看完了爱奇艺出品的开源框架xhook中的《Android PLT hook 概述》,内容比较深。主要用到了ELF和动态链接这些Linux的知识点,但是总体上还算是理解了基本原理,这篇文章主要记录下现阶段对PLT hook的学习和理解。感觉写原文的作者很牛逼,内容看的我热血沸腾,需要看原文的读者可以翻到文章最后点开原文链接。
最后希望感谢你花时间阅读本文,下面开始学习下面的内容。

什么是hook?

“Hook”(钩子)是计算机编程中的一种技术,它允许开发者拦截、修改或扩展软件或系统的行为。通过使用钩子,开发者可以在特定事件发生时注入自定义代码,以便修改程序的行为或响应程序的特定事件。

钩子的使用场景大概有这些:

  • 键盘和鼠标事件:拦截键盘和鼠标输入,用于实现自定义的快捷键、鼠标手势或输入法。
  • 窗口消息:拦截和处理窗口消息,用于实现窗口管理、界面定制等功能。
  • 函数调用:拦截特定函数的调用,用于实现调试、性能分析、代码注入等功能。
  • 文件操作和网络请求:拦截文件操作和网络请求,用于实现文件监控、安全检测等功能。

钩子技术可以提供很大的灵活性和功能扩展性,但也需要谨慎使用,因为不正确的使用钩子可能会导致程序崩溃、安全漏洞或不稳定的行为。

当然,我们本文要讨论的场景属于函数调用场景。那么PLT hook又是什么意思?

PLT hook

作用

PLT (Procedure Linkage Table) hook 是一种在动态链接库(DLL)或共享对象(SO)中实现的技术,用于在运行时修改或拦截函数调用。这种技术通常用于在不修改原始代码的情况下,对函数的行为进行修改或监视。

基本原理

PLT hook 的基本原理是利用了动态链接库的符号解析机制。

在程序加载动态链接库时,系统会创建一个 PLT 表来保存对动态链接函数的调用。这个表中的每个条目实际上是一个跳转指令,将控制权转移到动态链接库中的实际函数实现。

PLT hook 就是通过修改 PLT 表中的条目,将原始函数调用指向自定义的函数或者跳转到其他代码段,从而实现对函数行为的修改或拦截。

PLT hook 总体步骤

  1. 定位目标函数:确定要 hook 的目标函数,获取其在动态链接库中的地址。

  2. 修改 PLT 表:通过修改 PLT 表中目标函数对应的条目,将其指向自定义的函数或者其他代码段。

  3. 处理原始函数调用:在自定义函数中可以执行一些额外的操作,然后再调用原始的目标函数,或者完全替换原始函数的行为。

  4. 恢复原始调用:有时候需要在自定义函数中调用原始的目标函数,以保持程序的正常行为。

有了上述的基本了解,再来看看给出的例子,就会容易理解很多。

代码案例分析

下面通过给出一个存在明显内存泄漏的代码,把它们编译为动态库libtest.so。看看最后能不能通过PLT hook把泄漏给解决了。

头文件 test.h

#ifndef TEST_H
#define TEST_H 1

#ifdef __cplusplus
extern "C" {
#endif

void say_hello();

#ifdef __cplusplus
}
#endif

#endif

源文件 test.c

#include <stdlib.h>
#include <stdio.h>

void say_hello()
{
    char *buf = malloc(1024);
    if(NULL != buf)
    {
        snprintf(buf, 1024, "%s", "hello\n");
        printf("%s", buf);
    }
}

源文件 main.c

#include <test.h>

int main()
{
    say_hello();
    return 0;
}

执行:

caikelun@debian:~$ adb push ./libtest.so ./main /data/local/tmp
caikelun@debian:~$ adb shell "chmod +x /data/local/tmp/main"
caikelun@debian:~$ adb shell "export LD_LIBRARY_PATH=/data/local/tmp; /data/local/tmp/main"
hello
caikelun@debian:~$

把编译后的libtest.so 推到Android设备中,给个可执行权限后,执行它。虽然泄漏了,但是还是可以执行的。这正是模拟真实情况的代码,泄漏了却不自知。

方案预研

面临的问题

假如我们发现了泄漏问题,要修复它并不难,问题在于怎么及时发现它们。
问题在于下面2个:
1.假如程序测试覆盖得不够的话,怎么及时发现和定位一些已经上线的APP呢?
2.如果libtest.so是第三方库,而且还是闭源的。我们可以修复它吗?能否监控它?

怎么做?

这正是hook可以做到的事情,比如 hook malloc,calloc,realloc 和 free,我们就能统计出各个动态库分配了多少内存,哪些内存一直被占用没有释放。
作者提到,hook自己的进程完全是可以的,hook其它的进程需要root权限。因为假如没有root权限,就无法修改其它进程的内存空间。
好消息是,我们只要hook自己的进程就够了。

下面本文的主角要出来了。
而要理解PLT hook,首先要了解ELF是什么。

ELF

ELF(Executable and Linkable Format)是一种行业标准的二进制数据封装格式,主要用于封装可执行文件、动态库、object 文件和 core dumps 文件。

在这里插入图片描述
ELF概述
ELF定义

对于PLT hook,最重要的是了解ELF 文件头、SHT(section header table)、PHT(program header table)。

ELF 文件头

ELF 文件的起始处,有一个固定格式的定长的文件头(32 位架构为 52 字节,64 位架构为 64 字节)。ELF 文件头以 magic number 0x7F 0x45 0x4C 0x46 开始(其中后 3 个字节分别对应可见字符 E L F)。

libtest.so 的 ELF 文件头信息:

caikelun@debian:~$ arm-linux-androideabi-readelf -h ./libtest.so
 
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          52 (bytes into file)
  Start of section headers:          12744 (bytes into file)
  Flags:                             0x5000200, Version5 EABI, soft-float ABI
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         8
  Size of section headers:           40 (bytes)
  Number of section headers:         25
  Section header string table index: 24

ELF 文件头中包含了 SHT 和 PHT 在当前 ELF 文件中的起始位置和长度。例如,libtest.so 的 SHT 起始位置为 12744,长度 40 字节;PHT 起始位置为 52,长度 32字节。

ELF header数据结构如图:

请添加图片描述

SHT(section header table)

ELF 以 section 为单位来组织和管理各种信息。ELF 使用 SHT 来记录所有 section 的基本信息。主要包括:section 的类型、在文件中的偏移量、大小、加载到内存后的虚拟内存相对地址、内存中字节的对齐方式等。

caikelun@debian:~$ arm-linux-androideabi-readelf -S ./libtest.so
 
There are 25 section headers, starting at offset 0x31c8:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .note.android.ide NOTE            00000134 000134 000098 00   A  0   0  4
  [ 2] .note.gnu.build-i NOTE            000001cc 0001cc 000024 00   A  0   0  4
  [ 3] .dynsym           DYNSYM          000001f0 0001f0 0003a0 10   A  4   1  4
  [ 4] .dynstr           STRTAB          00000590 000590 0004b1 00   A  0   0  1
  [ 5] .hash             HASH            00000a44 000a44 000184 04   A  3   0  4
  [ 6] .gnu.version      VERSYM          00000bc8 000bc8 000074 02   A  3   0  2
  [ 7] .gnu.version_d    VERDEF          00000c3c 000c3c 00001c 00   A  4   1  4
  [ 8] .gnu.version_r    VERNEED         00000c58 000c58 000020 00   A  4   1  4
  [ 9] .rel.dyn          REL             00000c78 000c78 000040 08   A  3   0  4
  [10] .rel.plt          REL             00000cb8 000cb8 0000f0 08  AI  3  18  4
  [11] .plt              PROGBITS        00000da8 000da8 00017c 00  AX  0   0  4
  [12] .text             PROGBITS        00000f24 000f24 0015a4 00  AX  0   0  4
  [13] .ARM.extab        PROGBITS        000024c8 0024c8 00003c 00   A  0   0  4
  [14] .ARM.exidx        ARM_EXIDX       00002504 002504 000100 08  AL 12   0  4
  [15] .fini_array       FINI_ARRAY      00003e3c 002e3c 000008 04  WA  0   0  4
  [16] .init_array       INIT_ARRAY      00003e44 002e44 000004 04  WA  0   0  1
  [17] .dynamic          DYNAMIC         00003e48 002e48 000118 08  WA  4   0  4
  [18] .got              PROGBITS        00003f60 002f60 0000a0 00  WA  0   0  4
  [19] .data             PROGBITS        00004000 003000 000004 00  WA  0   0  4
  [20] .bss              NOBITS          00004004 003004 000000 00  WA  0   0  1
  [21] .comment          PROGBITS        00000000 003004 000065 01  MS  0   0  1
  [22] .note.gnu.gold-ve NOTE            00000000 00306c 00001c 00      0   0  4
  [23] .ARM.attributes   ARM_ATTRIBUTES  00000000 003088 00003b 00      0   0  1
  [24] .shstrtab         STRTAB          00000000 0030c3 000102 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  y (noread), p (processor specific)
  • .dynamic:供动态链接器使用的各项信息,记录了当前 ELF 的外部依赖,以及其他各个重要 section 的起始位置等信息。
  • .got:Global Offset Table。用于记录外部调用的入口地址。动态链接器(linker)执行重定位(relocate)操作时,这里会被填入真实的外部调用的绝对地址。
  • .plt:Procedure Linkage Table。外部调用的跳板,主要用于支持 lazy binding 方式的外部调用重定位。(Android 目前只有 MIPS 架构支持 lazy binding)

下面用一张图描述相关的关系,这也是PLT hook的核心原理:

请添加图片描述

链接视图(Linking View)和执行视图(Execution View)

  • 连接视图:ELF 未被加载到内存执行前,以 section 为单位的数据组织形式。
  • 执行视图:ELF 被加载到内存后,以 segment 为单位的数据组织形式。

请添加图片描述

而hook操作关系的是动态形式的内存操作,所以关心的是执行视图,也就是上面的右图。

动态链接

动态链接的大体步骤如下:

  • 检查已经加载的ELF列表
  • 从libtest.so的.dynamic section 中读取 libtest.so中外部依赖的ELF列表,从列表中剔除已经加载的ELF,得到本次需要加载的ELF
  • 用mmap预留一块内存,用于后面映射ELF
  • 读取ELF的PHT,用mmap把所有PT_LOAD类型的segment映射到内存中
  • 从.dynamic section 中读取各个section的虚拟内存地址,计算&保存各个section的虚拟内存绝对地址。
  • 执行重定位操作(relocate),这是最关键的一步。重定位信息可能存在于下面的一个或多个 secion 中:.rel.plt, .rela.plt, .rel.dyn, .rela.dyn, .rel.android, .rela.android。动态链接器需要逐个处理这些 .relxxx section 中的重定位诉求。根据已加载的 ELF 的信息,动态链接器查找所需符号的地址(比如 libtest.so 的符号 malloc),找到后,将地址值填入 .relxxx 中指明的目标地址中,这些“目标地址”一般存在于.got 或 .data 中。
  • ELF引用计数加一
  • 逐个调用ELF的构造函数,先调用被依赖ELF的构造函数,再调用libtest.so自己的构造函数。

这里是全文最关键地地方,分析重定位操作可以推理出:
只要从这些 .relxxx 中获取到“目标地址”,然后在“目标地址”中重新填上一个新的函数地址,这样就完成 hook 了

.dynamic section
这是一个十分重要和特殊的 section,其中包含了 ELF 中其他各个 section 的内存位置等信息。在执行视图中,总是会存在一个类型为 PT_DYNAMIC 的 segment,这个 segment 就包含了 .dynamic section 的内容。
无论是执行 hook 操作时,还是动态链接器执行动态链接时,都需要通过 PT_DYNAMIC segment 来找到 .dynamic section 的内存位置,再进一步读取其他各项 section 的信息。

猜想-解决-验证

原文这部分就是通过读取libtest.so的汇编代码,通过NDK的objdump 查出反汇编输出。接着通过一些计算得出libtest.so中malloc的地址3f90 。

第一次验证

#include <test.h>

void *my_malloc(size_t size)
{
    printf("%zu bytes memory are allocated by libtest.so\n", size);
    return malloc(size);
}

int main()
{
    void **p = (void **)0x3f90;
    *p = (void *)my_malloc; // do hook
    
    say_hello();
    return 0;
}

运行结果:

caikelun@debian:~$ adb push ./main /data/local/tmp
caikelun@debian:~$ adb shell "chmod +x /data/local/tmp/main"
caikelun@debian:~$ adb shell "export LD_LIBRARY_PATH=/data/local/tmp; /data/local/tmp/main"
Segmentation fault
caikelun@debian:~$

例子验证了思路是正确的但是需要解决3个问题:

  1. 计算出来的地址是个先对内存地址,需要转换为绝对地址
  2. 地址很可能没有写入权限
  3. 新的函数地址即使可以赋值成功,my_malloc 也不会执行,因为处理器有指令缓存。

方案

上述第一个问题可以通过基地址解决。
第二个问题通过mprotect解决。
第三个问题通过__builtin___clear_cache函数调用解决。

再次验证

把main.c 修改为:

#include <inttypes.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
#include <test.h>

#define PAGE_START(addr) ((addr) & PAGE_MASK)
#define PAGE_END(addr)   (PAGE_START(addr) + PAGE_SIZE)

void *my_malloc(size_t size)
{
    printf("%zu bytes memory are allocated by libtest.so\n", size);
    return malloc(size);
}

void hook()
{
    char       line[512];
    FILE      *fp;
    uintptr_t  base_addr = 0;
    uintptr_t  addr;

    //find base address of libtest.so
    if(NULL == (fp = fopen("/proc/self/maps", "r"))) return;
    while(fgets(line, sizeof(line), fp))
    {
        if(NULL != strstr(line, "libtest.so") &&
           sscanf(line, "%"PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1)
            break;
    }
    fclose(fp);
    if(0 == base_addr) return;

    //the absolute address
    addr = base_addr + 0x3f90;
    
    //add write permission
    mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);

    //replace the function address
    *(void **)addr = my_malloc;

    //clear instruction cache
    __builtin___clear_cache((void *)PAGE_START(addr), (void *)PAGE_END(addr));
}

int main()
{
    hook();
    
    say_hello();
    return 0;
}
caikelun@debian:~$ adb push ./main /data/local/tmp
caikelun@debian:~$ adb shell "chmod +x /data/local/tmp/main"
caikelun@debian:~$ adb shell "export LD_LIBRARY_PATH=/data/local/tmp; /data/local/tmp/main"
1024 bytes memory are allocated by libtest.so
hello
caikelun@debian:~$

上述代码在没有重新编译libtest.so的前提下,libtest.so的函数say_hello的函数地址替换成了my_malloc的函数地址。从而完成了native层面的hook。

至此,PLT hook的整体原理介绍完毕。
原文更加详细和精彩,适合需要更深入理解和实操的朋友,链接在下面。

参考资料

参考原文:https://github.com/iqiyi/xHook/blob/master/docs/overview/android_plt_hook_overview.zh-CN.md
https://en.wikipedia.org/wiki/Hooking

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

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

相关文章

Linux系统宝塔面板搭建Typecho博客并实现公网访问本地网站【内网穿透】

文章目录 前言1. 安装环境2. 下载Typecho3. 创建站点4. 访问Typecho5. 安装cpolar6. 远程访问Typecho7. 固定远程访问地址8. 配置typecho 前言 Typecho是由type和echo两个词合成的&#xff0c;来自于开发团队的头脑风暴。Typecho基于PHP5开发&#xff0c;支持多种数据库&#…

【MGR】MySQL Group Replication 背景

目录 17.1 Group Replication Background 17.1.1 Replication Technologies 17.1.1.1 Primary-Secondary Replication 17.1.1.2 Group Replication 17.1.2 Group Replication Use Cases 17.1.2.1 Examples of Use Case Scenarios 17.1.3 Group Replication Details 17.1…

初学arp欺骗

首先准备一台靶机这里用虚拟机的win10 已知网关与ip地址&#xff08;怕误伤&#xff09; 现在返回kali从头开始 首先探测自己的网关 然后扫内网存活的ip 发现有3台 用nmap扫一下是哪几台 成功发现我们虚拟机的ip 现在虚拟机可以正常访问网络 接下来直接开梭 ip网关 返回虚拟机…

十秒学会Ubuntu命令行:从入门到进阶

一、引言 在使用Ubuntu操作系统时&#xff0c;命令行界面&#xff08;CLI&#xff09;是不可或缺的一部分。对于初学者来说&#xff0c;掌握基本的命令行操作可以帮助他们更高效地管理系统和软件。本文将介绍一些常见的Ubuntu命令以及如何解决与命令行相关的问题。 二、常用Ubu…

Java两周半速成之路(第十天)

一.String和StringBuffer的相互转换 1.适用场景&#xff1a; 场景1&#xff1a;可以需要用到另一种类中的方法&#xff0c;来方便处理我们的需求 场景2&#xff1a;方法传参 场景一演示&#xff1a; /*String和StringBuffer的相互转换A<-->B什么场景需要做转换操作&…

字节跳动发布SDXL-Lightning模型,支持WebUI与ComfyUI双平台,只需一步生成1024高清大图!

字节跳动发布SDXL-Lightning模型,WebUI与ComfyUI平台,只需一步生成1024高清大图,需要的步数比 LCM 更低了! 什么是SDXL-Lightning: SDXL-Lightning 是一种快速的文本到图像生成模型。SDXL-Lightning模型的核心优势在于其创新的蒸馏策略,它可以通过几个步骤生成高质量的 1…

【数据分享】1988~2020年中国月度1KM植被指数NDVI空间分布数据集

各位同学们好&#xff0c;今天和大伙儿分享的是1988~2020年中国月度1KM植被指数NDVI空间分布数据集。如果大家有下载处理数据等方面的问题&#xff0c;您可以私信或评论。 徐新良.中国月度植被指数&#xff08;NDVI&#xff09;空间分布数据集.中国科学院资源环境科学数据中心数…

SpringCloud-RabbitMQ消息模型

本文深入介绍了RabbitMQ消息模型&#xff0c;涵盖了基本消息队列、工作消息队列、广播、路由和主题等五种常见消息模型。每种模型都具有独特的特点和适用场景&#xff0c;为开发者提供了灵活而强大的消息传递工具。通过这些模型&#xff0c;RabbitMQ实现了解耦、异步通信以及高…

数据结构(一)——概述

一、绪论 1.1数据结构的基本概念 数据&#xff1a;用来描述客观事物的数、计算机中是字符及所有能输入并被程序识别和处理的符号的集合。 数据元素&#xff1a;数据的基本单位&#xff0c;一个数据元素可由若干数据项组成。 数据结构&#xff1a;指相互之间存在一种或多种特…

C++:Vector的使用

一、vector的介绍 vector的文档介绍 1. vector是表示可变大小数组的序列容器。 2. 就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。也就是意味着可以采用下标对vector的元素进行访问&#xff0c;和数组一样高效。但是又不像数组&#xff0c;它的大小是可以…

解决win10系统cmd命令无法使用ssh问题

目录 问题说明&#xff1a;在使用ssh命令连接虚拟机地址时&#xff0c;出现了以下报错&#xff1a;​编辑 解决方法如下&#xff1a; 1.打开Windows设置&#xff0c;搜索点击添加可选功能&#xff1a; 2.点击添加功能&#xff1a; 3.安装Open SSH客户端和Open SSH服务器: …

基于Python3的数据结构与算法 - 10 计数排序

一、问题 对列表进行排序&#xff0c;已知列表中的数范围都在0到100之间。设计时间复杂度为O(n)的算法。 二、解决思路 我们已知数字的范围&#xff0c;那么我们可以将数字的个数得到&#xff1a; 例如&#xff1a;有一个0~5的列表 [1,3,2,4,1,2,3,1,3,5] 则共有0个0&am…

Tomcat(二) 动静分离

一、(TomcatNginx)动静分离 1、单机反向代理 利用 nginx 反向代理实现全部转发至指定同一个虚拟主机 客户端curl www.a.com 访问nginx服务&#xff0c;nginx服务通过配置反向代理proxy_pass www.a.com:8080&#xff0c;最终客户端看到的是www.a.com 实验中&#xff1a;7-3 做客…

保姆级GeoWebCache矢量瓦片切片流程

1矢量切片解决方案 1.1Geoserver配置geowebcache插件 参考文章 (53条消息) 独立安装geoservergeowebcache发布arcgis切片服务_itouch_ok的专栏-CSDN博客 1.将下载好的geoserver 2.19.3安装部署 将下载好的geowebcache 2.19.3的war包解压到 GeoServer 安装目录下./usr/loc…

两天学会微服务网关Gateway-Gateway HelloWorld快速入门

锋哥原创的微服务网关Gateway视频教程&#xff1a; Gateway微服务网关视频教程&#xff08;无废话版&#xff09;_哔哩哔哩_bilibiliGateway微服务网关视频教程&#xff08;无废话版&#xff09;共计17条视频&#xff0c;包括&#xff1a;1_Gateway简介、2_Gateway工作原理、3…

SpringBoot 自定义映射规则resultMap collection一对多

介绍 collection是封装一对多关系的&#xff0c;通常情况下是一个列表&#xff0c;association是一对一&#xff0c;通常情况是一个对象。例如&#xff1a;查询班级下所有的学生&#xff0c;一个班级可以有多个学生&#xff0c;这就是一对多。 案例 有一个学生表&#xff0c…

重构笔记系统:Docker Compose在微服务架构中的应用与优化

虽然我的笔记系统的开发是基于微服务的思想&#xff0c;但是在服务的配置和编排上感觉还是不太合理&#xff0c;具体来说&#xff0c;在开发上的配置和在生产上的配置差别太大。现在规模小&#xff0c;后面规模变大&#xff0c;估计这一块会成为系统生长的瓶颈。 因此&#xff…

优先级队列(Java )

目录 一、 优先级队列1、概念 二、优先级队列的模拟实现1、堆的概念2、堆的存储方式 三、堆的创建1、堆向下调整2、堆的创建3、建堆的时间复杂度 四、堆的插入与删除1、堆的插入2、堆的删除 五、用堆模拟实现优先级队列 一、 优先级队列 1、概念 优先级队列&#xff08;Priori…

Windows Docker 部署 Jenkins

一、简介 今天介绍一下在 Windows Docker 中部署 Jenkins 软件。在 Windows Docker 中&#xff0c;分为两种情况 Linux 容器和 Windows 容器。Linux 容器是通常大多数使用的方式&#xff0c;Windows 容器用于 CI/CD 依赖 Windows 环境的情况。 二、Linux 容器 Linux 容器内部…

vue中使用echarts实现人体动态图

最近一直处于开发大屏的项目&#xff0c;在开发中遇到了一个小知识点&#xff0c;在大屏中如何实现人体动态图。然后看了下echarts官方文档&#xff0c;根据文档中的示例调整出来自己想要的效果。 根据文档上发现 series 中 type 类型设置为 象形柱形图&#xff0c;象形柱图是…