TA的编译/入口函数/签名文章翻译

前言:

翻译:https://optee.readthedocs.io/en/latest/building/trusted_applications.html

上一篇翻译的文章:OP-TEE的安装。安装完之后,我可不想知道程序整个流程的具体细节。我现在想自己改写下hello world实现自己的TA和CA之间的交互。

简单的修改hello world实现一个example,可以参考这个视频:添加TA/CA的及简套路

而具体的细节,可以看下面对官网的翻译。文章按照我的理解/思路来翻译,以原文为准。同时我不明白的地方,我不翻译,直接给出原文。


可信应用

本文档介绍了如何使用OP-TEE的TA-devkit(TA的开发工具包)来构建(build)和签名(sign)Trusted Application二进制文件,实现Trusted Application。 本文档中,在OP-TEE os 中运行的可信应用程序,称为TA。 请注意,在默认设置中,由Linaro生成并与optee_os源一起分发的私钥用于对受信任的应用程序进行签名。 有关更多详细信息,请参见 TASign,包括TA的脱机签名。

1、TA必要组成文件

可信应用程序的Makefile编写,基于OP-TEE TA-devkit,以便成功构建目标应用程序。TA-devkit is built when one builds optee_os.

To build a TA, one must provide:

  • Makefile,一个make file 应该设置一些配置变量并包含TA-devkit make file。

  • sub.mk,一个make file,列出要构建的源(本地源文件,要解析的子目录,特定于源文件的构建指令)。

  • user_ta_header_defines.h ,一个特定的ANSI-C头文件,来定义大多数TA属性。

  • TA 入口点最少应该实现的外部函数:TA_CreateEntryPoint(), TA_DestroyEntryPoint(), TA_OpenSessionEntryPoint(), TA_CloseSessionEntryPoint(),TA_InvokeCommandEntryPoint()

2、TA文件分布实例

As an example, hello_world looks like this:

hello_world/
├── ...
└── ta
    ├── Makefile                  BINARY=<uuid>
    ├── Android.mk                Android way to invoke the Makefile
    ├── sub.mk                    srcs-y += hello_world_ta.c
    ├── include
    │   └── hello_world_ta.h      Header exported to non-secure: TA commands API
    ├── hello_world_ta.c          Implementation of TA entry points
    └── user_ta_header_defines.h  TA_UUID, TA_FLAGS, TA_DATA/STACK_SIZE, ...

2.1、TA Makefile 基础

我先把hello world中的makefile文件内容,放出来。看不懂可以从下面的翻译中理解出来。(我好多不晓得。刚开始先知道改啥哈。晓得50%先)

这个文件,目前只会改BINARY。

CFG_TEE_TA_LOG_LEVEL ?= 4
CPPFLAGS += -DCFG_TEE_TA_LOG_LEVEL=$(CFG_TEE_TA_LOG_LEVEL)

# The UUID for the Trusted Application
BINARY=8aaaf200-2450-11e4-abe2-0002a5d5c51b

-include $(TA_DEV_KIT_DIR)/mk/ta_dev_kit.mk

ifeq ($(wildcard $(TA_DEV_KIT_DIR)/mk/ta_dev_kit.mk), )
clean:
	@echo 'Note: $$(TA_DEV_KIT_DIR)/mk/ta_dev_kit.mk not found, cannot clean TA'
	@echo 'Note: TA_DEV_KIT_DIR=$(TA_DEV_KIT_DIR)'
endif

2.1.1 Required variables

TA-devkit 的make file 在 optee_os 中 。位置:optee/optee_os/ta/mk/ta_dev_kit.mk 。它包含allclean,可以 build a TA or a library and clean the built objects 。

这里,我们知道:某个TA,它的makefile文件,包含(include)ta_dev_kit.mk , 则可以执行 make all /make clean

  • TA_DEV_KIT_DIR

TA-devkit的基本目录。 由TA-devkit本身用来定位其工具。(我用的是vscode,目前不知道变量的定义位置。)

  • BINARY and LIBNAME

(此处没有全部翻译,注意参考原文)必须是唯一的。In native OP-TEE,BINARY变量保存的是UUID,用来区分不同的TA。 LIBNAME 干啥的,我不知道。

find -name 8aaaf200-2450-11e4-abe2-0002a5d5c51b.ta

原文部分:

These are exclusive, meaning that you cannot use both at the same time. If building a TA, BINARY shall provide the TA filename used to load the TA. The built and signed TA binary file will be named ${BINARY}.ta. In native OP-TEE, it is the TA UUID, used by tee-supplicant to identify TAs. If one is building a static library (that will be later linked by a TA), then LIBNAME shall provide the name of the library. The generated library binary file will be named lib${LIBNAME}.a

  • CROSS_COMPILE and CROSS_COMPILE32

用来交叉编译TA或者二进制源文件。CROSS_COMPILE32 is optional 。

Cross compiler for the TA or the library source files. CROSS_COMPILE32 is optional. It allows to target AArch32 builds on AArch64 capable systems. On AArch32 systems, CROSS_COMPILE32 defaults to CROSS_COMPILE.

  • Optional variables

一些可选的配置变量,比如O

Base directory for build objects filetree. If not set, TA-devkit defaults to ./out from the TA source tree base directory.

2.2、sub.mk directives

同样的,先展示hello world的sub.mk文件内容。带着问题去读官方文档。

global-incdirs-y += include
srcs-y += hello_world_ta.c

# To remove a certain compiler flag, add a line like this
#cflags-template_ta.c-y += -Wno-strict-prototypes

make file 期望当前目录有sub.mk文件。sub.mk列出要构建的源文件和其他特定构建指令。 以下是可以在sub.mk make文件中实现的指令的几个示例:

# Adds /hello_world_ta.c from current directory to the list of the source
# file to build and link.
srcs-y += hello_world_ta.c

# Includes path **./include/** from the current directory to the include
# path.
global-incdirs-y += include/

# Adds directive -Wno-strict-prototypes only to the file hello_world_ta.c
cflags-hello_world_ta.c-y += -Wno-strict-prototypes

# Removes directive -Wno-strict-prototypes from the build directives for
# hello_world_ta.c only.
cflags-remove-hello_world_ta.c-y += -Wno-strict-prototypes

# Adds the static library foo to the list of the linker directive -lfoo.
libnames += foo

# Adds the directory path to the libraries pathes list. Archive file
# libfoo.a is expected in this directory.
libdirs += path/to/libfoo/install/directory

# Adds the static library binary to the TA build dependencies.
libdeps += path/to/greatlib/libgreatlib.a

2.3、Android Build Environment

Android.mk 文件

不清楚。改动local_module是对的。

LOCAL_PATH := $(call my-dir)

local_module := 8aaaf200-2450-11e4-abe2-0002a5d5c51b.ta
include $(BUILD_OPTEE_MK)

OP-TEE的TA-devkit支持在Android构建环境中进行构建。 可以为TA编写一个Android.mk文件(与Makefile并存)。 Android的构建系统将解析TA的Android.mk文件,TA进而解析TA-devkit Android make文件来查找TA构建资源。 然后,Android构建将执行make命令以通过其通用Makefile文件构建TA。

2.4、TA Mandatory Entry Points

同样的,首先给出hello world中的hello_world_ta.c文件。有时候代码比较长,且。建议去gith一些常量定义在头文件中。这里没有给出头文件。建议去github看代码。

/*
 * Copyright (c) 2016, Linaro Limited
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <tee_internal_api.h>
#include <tee_internal_api_extensions.h>

#include <hello_world_ta.h>

/*
 * Called when the instance of the TA is created. This is the first call in
 * the TA.
 */
TEE_Result TA_CreateEntryPoint(void)
{
	DMSG("has been called");

	return TEE_SUCCESS;
}

/*
 * Called when the instance of the TA is destroyed if the TA has not
 * crashed or panicked. This is the last call in the TA.
 */
void TA_DestroyEntryPoint(void)
{
	DMSG("has been called");
}

/*
 * Called when a new session is opened to the TA. *sess_ctx can be updated
 * with a value to be able to identify this session in subsequent calls to the
 * TA. In this function you will normally do the global initialization for the
 * TA.
 */
TEE_Result TA_OpenSessionEntryPoint(uint32_t param_types,
		TEE_Param __maybe_unused params[4],
		void __maybe_unused **sess_ctx)
{
	uint32_t exp_param_types = TEE_PARAM_TYPES(TEE_PARAM_TYPE_NONE,
						   TEE_PARAM_TYPE_NONE,
						   TEE_PARAM_TYPE_NONE,
						   TEE_PARAM_TYPE_NONE);

	DMSG("has been called");

	if (param_types != exp_param_types)
		return TEE_ERROR_BAD_PARAMETERS;

	/* Unused parameters */
	(void)&params;
	(void)&sess_ctx;

	/*
	 * The DMSG() macro is non-standard, TEE Internal API doesn't
	 * specify any means to logging from a TA.
	 */
	IMSG("Hello World!\n");

	/* If return value != TEE_SUCCESS the session will not be created. */
	return TEE_SUCCESS;
}

/*
 * Called when a session is closed, sess_ctx hold the value that was
 * assigned by TA_OpenSessionEntryPoint().
 */
void TA_CloseSessionEntryPoint(void __maybe_unused *sess_ctx)
{
	(void)&sess_ctx; /* Unused parameter */
	IMSG("Goodbye!\n");
}

static TEE_Result inc_value(uint32_t param_types,
	TEE_Param params[4])
{
	uint32_t exp_param_types = TEE_PARAM_TYPES(TEE_PARAM_TYPE_VALUE_INOUT,
						   TEE_PARAM_TYPE_NONE,
						   TEE_PARAM_TYPE_NONE,
						   TEE_PARAM_TYPE_NONE);

	DMSG("has been called");

	if (param_types != exp_param_types)
		return TEE_ERROR_BAD_PARAMETERS;

	IMSG("Got value: %u from NW", params[0].value.a);
	params[0].value.a++;
	IMSG("Increase value to: %u", params[0].value.a);

	return TEE_SUCCESS;
}

static TEE_Result dec_value(uint32_t param_types,
	TEE_Param params[4])
{
	uint32_t exp_param_types = TEE_PARAM_TYPES(TEE_PARAM_TYPE_VALUE_INOUT,
						   TEE_PARAM_TYPE_NONE,
						   TEE_PARAM_TYPE_NONE,
						   TEE_PARAM_TYPE_NONE);

	DMSG("has been called");

	if (param_types != exp_param_types)
		return TEE_ERROR_BAD_PARAMETERS;

	IMSG("Got value: %u from NW", params[0].value.a);
	params[0].value.a--;
	IMSG("Decrease value to: %u", params[0].value.a);

	return TEE_SUCCESS;
}
/*
 * Called when a TA is invoked. sess_ctx hold that value that was
 * assigned by TA_OpenSessionEntryPoint(). The rest of the paramters
 * comes from normal world.
 */
TEE_Result TA_InvokeCommandEntryPoint(void __maybe_unused *sess_ctx,
			uint32_t cmd_id,
			uint32_t param_types, TEE_Param params[4])
{
	(void)&sess_ctx; /* Unused parameter */

	switch (cmd_id) {
	case TA_HELLO_WORLD_CMD_INC_VALUE:
		return inc_value(param_types, params);
	case TA_HELLO_WORLD_CMD_DEC_VALUE:
		return dec_value(param_types, params);
	default:
		return TEE_ERROR_BAD_PARAMETERS;
	}
}

A TA must implement a couple of mandatory entry points, these are:

TEE_Result TA_CreateEntryPoint(void)
{
    /* Allocate some resources, init something, ... */
    ...

    /* Return with a status */
    return TEE_SUCCESS;
}

void TA_DestroyEntryPoint(void)
{
    /* Release resources if required before TA destruction */
    ...
}

TEE_Result TA_OpenSessionEntryPoint(uint32_t ptype,
                                    TEE_Param param[4],
                                    void **session_id_ptr)
{
    /* Check client identity, and alloc/init some session resources if any */
    ...

    /* Return with a status */
    return TEE_SUCCESS;
}

void TA_CloseSessionEntryPoint(void *sess_ptr)
{
    /* check client and handle session resource release, if any */
    ...
}

TEE_Result TA_InvokeCommandEntryPoint(void *session_id,
                                      uint32_t command_id,
                                      uint32_t parameters_type,
                                      TEE_Param parameters[4])
{
    /* Decode the command and process execution of the target service */
    ...

    /* Return with a status */
    return TEE_SUCCESS;
}

2.5、TA Properties

/*
 * Copyright (c) 2016-2017, Linaro Limited
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * The name of this file must not be modified
 */

#ifndef USER_TA_HEADER_DEFINES_H
#define USER_TA_HEADER_DEFINES_H

/* To get the TA UUID definition */
#include <hello_world_ta.h>

#define TA_UUID				TA_HELLO_WORLD_UUID

/*
 * TA properties: multi-instance TA, no specific attribute
 * TA_FLAG_EXEC_DDR is meaningless but mandated.
 */
#define TA_FLAGS			TA_FLAG_EXEC_DDR

/* Provisioned stack size */
#define TA_STACK_SIZE			(2 * 1024)

/* Provisioned heap size for TEE_Malloc() and friends */
#define TA_DATA_SIZE			(32 * 1024)

/* The gpd.ta.version property */
#define TA_VERSION	"1.0"

/* The gpd.ta.description property */
#define TA_DESCRIPTION	"Example of OP-TEE Hello World Trusted Application"

/* Extra properties */
#define TA_CURRENT_TA_EXT_PROPERTIES \
    { "org.linaro.optee.examples.hello_world.property1", \
	USER_TA_PROP_TYPE_STRING, \
        "Some string" }, \
    { "org.linaro.optee.examples.hello_world.property2", \
	USER_TA_PROP_TYPE_U32, &(const uint32_t){ 0x0010 } }

#endif /* USER_TA_HEADER_DEFINES_H */

一个TA的属性应该定义在一个头文件中:user_ta_header_defines.h 包含

  • TA_UUID defines the TA uuid value
  • TA_FLAGS define some of the TA properties
  • TA_STACK_SIZE defines the RAM size to be reserved for TA stack
  • TA_DATA_SIZE defines the RAM size to be reserved for TA heap (TEE_Malloc() pool)

参考 TA Properties ,理解如何配置这些宏.

提示:两种方式来产生唯一的UUID

python -c 'import uuid; print(uuid.uuid4())'
cat /proc/sys/kernel/random/uuid # Linux only
uuidgen # available from the util-linux package in most distributions

2.6、Checking TA parameters

分析下hello world中类型检查。

我们期望传入的数据类型:uint32_t exp_param_types = 3;

uint32_t exp_param_types = TEE_PARAM_TYPES(TEE_PARAM_TYPE_VALUE_INOUT,
						   TEE_PARAM_TYPE_NONE,
						   TEE_PARAM_TYPE_NONE,
						   TEE_PARAM_TYPE_NONE);

if (param_types != exp_param_types)
		return TEE_ERROR_BAD_PARAMETERS;
#define TEE_PARAM_TYPES(t0,t1,t2,t3) ((t0) | ((t1) << 4) | ((t2) << 8) | ((t3) << 12))
The macro TEE_PARAM_TYPES can be used to construct a value that you can
compare against an incoming paramTypes to check the type of all the
parameters in one comparison, like in the following example:
if (paramTypes != TEE_PARAM_TYPES(TEE_PARAM_TYPE_MEMREF_INPUT,
TEE_PARAM_TYPE_MEMREF_OUPUT,
TEE_PARAM_TYPE_NONE, TEE_PARAM_TYPE_NONE)) {
return TEE_ERROR_BAD_PARAMETERS;
}

Expands to:

((3) | ((0) << 4) | ((0) << 8) | ((0) << 12))


#define TEE_PARAM_TYPE_VALUE_INOUT      3
#define TEE_PARAM_TYPE_NONE             0

param_types 是 TA_InvokeCommandEntryPoint 函数的第三个参数,而且是从normal world中传来。

我们可以推测,是来自TEEC_InvokeCommand的op。

且结构体op中的paramTypes,也被初始化成3。

TEE_Result TA_InvokeCommandEntryPoint(void *sess_ctx, uint32_t cmd_id, uint32_t param_types, TEE_Param *params)
Called when a TA is invoked. sess_ctx hold that value that was
assigned by TA_OpenSessionEntryPoint(). The rest of the paramters
comes from normal world.
res = TEEC_InvokeCommand(&sess, TA_HELLO_WORLD_CMD_INC_VALUE, &op,
				 &err_origin);

TEEC_Operation op;

typedef struct {
	uint32_t started;
	uint32_t paramTypes;
	TEEC_Parameter params[TEEC_CONFIG_PAYLOAD_REF_COUNT];
	/* Implementation-Defined */
	TEEC_Session *session;
} TEEC_Operation;

memset(&op, 0, sizeof(op));

	op.paramTypes = TEEC_PARAM_TYPES(TEEC_VALUE_INOUT, TEEC_NONE,
					 TEEC_NONE, TEEC_NONE);

#define TEEC_PARAM_TYPES(p0,p1,p2,p3) ((p0) | ((p1) << 4) | ((p2) << 8) | ((p3) << 12))
Encode the paramTypes according to the supplied types.
@param p0 The first param type.
@param p1 The second param type.
@param p2 The third param type.
@param p3 The fourth param type.
Expands to:
((0x00000003) | ((0x00000000) << 4) | ((0x00000000) << 8) | ((0x00000000) << 12))

#define TEEC_VALUE_INOUT            0x00000003
#define TEEC_NONE                   0x00000000

所以期望的类型和传入的类型相同。

“检查参数类型”,这一段我没有翻译。如果需要自行在官方文档处查看。(因为hello world中,没有采用文档中的检查方法)

3、TAs的签名

(签名这里,我不会。哪天操作过后,来补充)

All REE Filesystem Trusted Applications need to be signed.

在加载TA时,optee_os验证签名。

optee_os中,有个key目录。

All REE Filesystem Trusted Applications need to be signed. The signature is verified by optee_os upon loading of the TA. Within the optee_os source is a directory keys. The public part of keys/default_ta.pem will be compiled into the optee_os binary and the signature of each TA will be verified against this key upon loading. Currently keys/default_ta.pem must contain an RSA key.

警告:

optee_os 为了测试和开发的方便,默认含有私钥。所以在生产中,千万钥替换成自己的私钥/公钥。私钥别傻不拉几的上传到网上。

在optee_os中,使用sign.py脚本对TAs进行签名。而这个脚本参考ta/mk/ta_dev_kit.mk 其默认行为是对已编译的TA二进制文件进行签名,并附加签名以形成用于部署的完整TA。 对于脱机签名,需要三步过程:在第一步中,必须生成已编译二进制文件的摘要,在第二步中,使用私钥对该摘要进行脱机签名,最后在第三步中,对二进制文件及其摘要进行签名。 签名被缝合到完整的TA中。

3.1 Offline Signing of TAs

TA开发工具包确实在链接过程的最后一步对应用程序进行了签名。 例如,optee_os源树中的文件ta / arch / arm / link.mk包含以下语句

$(q)$(SIGN) --key $(TA_SIGN_KEY) --uuid $(user-ta-uuid) --version 0 \
                --in $$< --out $$@

为了避免在脱机签名时出现构建错误,需要采用此make脚本。 签名脚本可以在以下位置找到$(TA_DEV_KIT_DIR)/../scripts/sign.py

总体而言,脱机签名通过以下步骤序列完成:

  1. (准备工作)生成2048位RSA密钥,以在安全的脱机环境中签名。 解压缩公钥并将其复制到optee_os源树中的keys目录。 调整TA_SIGN_KEY以使用不同的文件/路径名称。 (复制并)修改link.mk文件以进行默认链接步骤,以生成TA二进制文件的摘要,而不是完整TA的摘要。

  2. 手动(或使用修改的链接脚本)使用以下命令生成TA二进制文件的摘要

sign.py digest --key $(TA_SIGN_KEY) --uuid $(user-ta-uuid)
  1. 离线签署此摘要,例如 使用OpenSSL
base64 --decode digestfile | \
openssl pkeyutl -sign -inkey $TA_SIGN_KEY \
  -pkeyopt digest:sha256 -pkeyopt rsa_padding_mode:pkcs1 | \
base64 > sigfile

or with pkcs11-tool using a Nitrokey HSM

echo "0000: 3031300D 06096086 48016503 04020105 000420" | \
  xxd -c 19 -r > /tmp/sighdr
cat /tmp/sighdr $(base64 --decode digestfile) > /tmp/hashtosign
pkcs11-tool --id $key_id -s --login -m RSA-PKCS \
  --input-file /tmp/hashtosign | \
  base64 > sigfile
  1. 手动(或与其他目标一起)将TA缝在一起
sign.py stitch --key $(TA_SIGN_KEY) --uuid $(user-ta-uuid)

默认情况下,UUID被用作所有文件的基本文件名。 可以通过sign.py的其他选项设置不同的文件名和路径。 有关选项和参数的完整列表,请查阅sign.py --help


译者话

TAs需要:编译的配置文件+入口函数的实现+签名

暂时缺:sign.py程序的分析。

这篇文章需改动结构:重点/目的不突出。


其他:

  • 科目三挂了。。。
  • 《精英律师》凑或。
  • 快过年,安逸挺好。
  • 大草 2020/1/22 阴
发布了104 篇原创文章 · 获赞 134 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/sinat_38816924/article/details/104068238
TA