Java VMTranslator Part II

用Java写一个翻译器,将Java的字节码翻译成汇编语言 

目录

程序控制流开发

基本思路

核心代码

实验结果,使用例子进行验证

函数调用

基本思路

核心代码

实验结果,使用例子进行验证

 Parser

CodeWriter

Main


程序控制流开发

基本思路

在project7的基础上将带有控制流的vm字节码翻译成asm汇编文件,既然是翻译,那就是字符串替换问题,在第一部分的程序控制流实现中,我们要做的就是用asm汇编语言实现goto、if-goto和label。

首先是label,这个非常简单,就直接换成(label)就完了。

然后是goto label,这个也简单,拿到label的值直接跳转就行。

最后是if-goto,这个我们首先要搞懂它是什么意思?从课件上可以知道,if-goto的效果是当栈顶元素不为0时发生跳转,并且弹栈。

因此我们首先将栈顶元素取出来,然后栈指针自减,当栈顶元素不为0时跳转。

核心代码

首先是parser类的修改,在代码调试的时候发现这次的vm字节码文件出现了有连续空格的情况,之前的parser没有对这种情况的处理,这次更新一下,增加将连续空格变成单个空格的处理。

然后是parser判断vm指令类型的函数需要增加对label、goto和if-goto指令类型的判断。

然后最主要的就是codeWriter的功能增加,增加了对label、goto和if-goto指令字符串的替换。

然后主函数就是增加对label、goto和if-goto情况的判断即可。

实验结果,使用例子进行验证

首先是BasicLoop的测试,如下图所示,成功通过测试。

然后是斐波那契的测试,如下图所示,成功通过测试,可见控制流的实现通过了。

函数调用

基本思路

首先解决call,这个比较复杂,这个主要是保存在调用函数之前当前程序的状态,主要是LCL、ARG、THIS和THAT,还有函数调用者的返回地址。

接下来我们分别一部分一部分的讲解每一个应该怎么操作。

Push retAddrLabel比较简单,就是将调用者当前的地址保存下来,将调用返回地址压入栈中,当然这个地址需要处理一下,因为我们要区分开每一次调用的label。

对于push LCL/ARG/THIS/THAT的操作是一样的,都是类似于project7中push pointer的操作,即拿到对应的字段值,把它压进栈即可。

对于ARG=SP-5-nArgs,直接翻译,让ARG的值为SP-5-nArgs。

LCL=SP这个也好翻译,而goto functionName就直接跳过去就行了,但是要记得把调用返回地址标号写在后面,因为调用完函数之后要回来。

对于函数定义function functionName nVars,根据课件可知我们需要进行nVars次push constant 0的操作。

因此只需要写上function名字的label后调用nVars次我们之前在project7写的push constant 0的翻译就行了。

对于return而言,基本上就是做的call的反操作,把call时期保存的函数调用者的状态给还原,其中主要的就是拿到函数返回地址以及把之前压入栈的字段值恢复。

因为这个return涉及到的操作很多,我们还是需要一个部分一个部分的讲解。

首先需要一个局部变量拿到我们函数调用之前写入LCL的栈指针的值。

然后根据这个调用前的栈指针的值我们可以拿到之前压入栈的函数返回地址。

然后把函数返回值写入ARG,这里是project7的pop argument 0的操作。

然后是恢复函数调用者时期SP的值。

恢复THAT/THIS/ARG/LCL字段的值。

最后回到函数调用者的地址。

然后是最后两个测试程序需要调用它们的启动函数。

启动函数就是初始化栈指针的值为256,然后调用它们写好的Sys.init函数。

还有根据最后一个测试StaticTest的调试结果可以知道不同vm文件的static字段应该是不一样的,难怪需要setFileName函数的存在,因为我没有这个函数就一直跑不对。

核心代码

parser判断vm指令类型的函数需要增加对call、function和return指令类型的判断。

然后最主要的就是codeWriter的功能增加,增加了对call、function和return指令字符串的替换,其中call和return就一一写入相应的字符串,call还需要区分开每一次调用的地址,因此调用label需要加入序号。

而function的处理,则重复调用project7写的push constant 0进行翻译。

然后主函数就是增加对label、goto和if-goto情况的判断即可。

还有调用启动函数的编写。

因为这里出现了多个vm文件,因此我们需要在主函数里对输入文件路径做一些处理,我们首先创建一个文件容器,先判断这个文件路径是否是单个文件还是一个文件夹,如果是单个文件,把这个文件装进容器。如果是一个文件夹,那么把这个文件夹目录下所有vm文件装进容器。

然后对于容器里的每一个文件都生成一个parser解析器就行解析翻译。

实验结果,使用例子进行验证

SimpleFunction先注释掉我们启动的初始化函数,因为它已经包含了启动函数,测试结果如下图所示,测试成功。

然后是这个非常非常长的斐波那契测试,这里包括了两个vm文件,同时我们启用写好的调用启动函数代码,测试结果如下图所示,成功通过。

然后是最后的考验StaticTest,第一次测试其实是失败的,为什么吗,因为一开始我没有搞懂为什么有个setFileName函数要写,于是我就没写,然后就在这里调试的时候发现,对于不同的vm文件需要不同的static字段,后来重写了project7的pop和push函数,加上了区分不同vm文件的static函数。

然后就终于搞定了。

 Parser

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.Objects;
import java.util.Scanner;public class Parser {private String command = null;private final Scanner scanner;private String cmd0 = null;private String cmd1 = null;private int cmd2;public Parser(File file) throws FileNotFoundException {scanner = new Scanner(new FileReader(file));}public boolean hasMoreCommands() {boolean hasMore = false;while (scanner.hasNextLine()) {command = scanner.nextLine();command = command.replaceAll("\\s+", " "); //将连续的空格替换成单个空格if (!Objects.equals(command, "") && command.charAt(0) != '/') { //去掉空白行和注释String[] pure = command.split("/");command = pure[0];hasMore = true;break;}}return hasMore;}public void advance() {String[] cmd = command.split(" ");cmd0 = cmd[0];if (cmd.length > 1) {cmd1 = cmd[1];if (cmd.length > 2) {cmd2 = Integer.parseInt(cmd[2]);}}}public String commandType() {if (Objects.equals(cmd0, "push")) {return "C_PUSH";} else if (Objects.equals(cmd0, "pop")) {return "C_POP";} else if (Objects.equals(cmd0, "label")) {return "C_LABEL";} else if (Objects.equals(cmd0, "goto")) {return "C_GOTO";} else if (Objects.equals(cmd0, "if-goto")) {return "C_IF";} else if (Objects.equals(cmd0, "call")) {return "C_CALL";} else if (Objects.equals(cmd0, "function")) {return "C_FUNCTION";} else if (Objects.equals(cmd0, "return")) {return "C_RETURN";} else {cmd1 = cmd0;return "C_ARITHMETIC";}}public String arg1() {return cmd1;}public int arg2() {return cmd2;}public void close() {scanner.close();}}

CodeWriter

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Objects;public class CodeWriter {private final FileWriter asm;private String asmCommand;private String fileName="";private final HashMap<String, String> vmToAsm = new HashMap<>();private int jump = 0;public CodeWriter(File file) throws IOException {asm = new FileWriter(file);String fetch = "@SP\nM=M-1\nA=M\nD=M\nA=A-1\n";vmToAsm.put("add", fetch + "M=M+D\n");vmToAsm.put("sub", fetch + "M=M-D\n");vmToAsm.put("and", fetch + "M=M&D\n");vmToAsm.put("or", fetch + "M=M|D\n");vmToAsm.put("gt", fetch + "D=M-D\n@TRUE\nD;JGT\n@SP\nA=M-1\nM=0\n@END\n0;JMP\n(TRUE)\n@SP\nA=M-1\nM=-1\n(END)\n");vmToAsm.put("eq", fetch + "D=M-D\n@TRUE\nD;JEQ\n@SP\nA=M-1\nM=0\n@END\n0;JMP\n(TRUE)\n@SP\nA=M-1\nM=-1\n(END)\n");vmToAsm.put("lt", fetch + "D=M-D\n@TRUE\nD;JLT\n@SP\nA=M-1\nM=0\n@END\n0;JMP\n(TRUE)\n@SP\nA=M-1\nM=-1\n(END)\n");vmToAsm.put("neg", "D=0\n@SP\nA=M-1\nM=D-M\n");vmToAsm.put("not", "@SP\nA=M-1\nM=!M\n");}public void writeArithmetic(String vmCommand) throws IOException {asmCommand = vmToAsm.get(vmCommand);if (Objects.equals(vmCommand, "gt") || Objects.equals(vmCommand, "eq") || Objects.equals(vmCommand, "lt")) {asmCommand = asmCommand.replaceAll("TRUE", "TRUE" + jump);asmCommand = asmCommand.replaceAll("END", "END" + jump);jump++;}asm.write(asmCommand);}public void writePushPop(String cmd, String segment, int index) throws IOException {if (Objects.equals(cmd, "C_PUSH")) {if (Objects.equals(segment, "constant")) {asmCommand = "@" + index + "\nD=A\n@SP\nA=M\nM=D\n@SP\nM=M+1\n";} else if (Objects.equals(segment, "local")) {asmCommand = "@LCL\nD=M\n@" + index + "\nA=D+A\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n";} else if (Objects.equals(segment, "argument")) {asmCommand = "@ARG\nD=M\n@" + index + "\nA=D+A\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n";} else if (Objects.equals(segment, "this")) {asmCommand = "@THIS\nD=M\n@" + index + "\nA=D+A\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n";} else if (Objects.equals(segment, "that")) {asmCommand = "@THAT\nD=M\n@" + index + "\nA=D+A\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n";} else if (Objects.equals(segment, "temp")) {asmCommand = "@" + (5 + index) + "\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n";} else if (Objects.equals(segment, "pointer")) {if (index == 0) {asmCommand = "@THIS\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n";} else {asmCommand = "@THAT\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n";}} else if (Objects.equals(segment, "static")) {asmCommand = "@" + fileName+ index + "\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n";}} else {if (Objects.equals(segment, "local")) {asmCommand = "@LCL\nD=M\n@" + index + "\nD=D+A\n@255\nM=D\n@SP\nM=M-1\nA=M\nD=M\n@255\nA=M\nM=D\n";} else if (Objects.equals(segment, "argument")) {asmCommand = "@ARG\nD=M\n@" + index + "\nD=D+A\n@255\nM=D\n@SP\nM=M-1\nA=M\nD=M\n@255\nA=M\nM=D\n";} else if (Objects.equals(segment, "this")) {asmCommand = "@THIS\nD=M\n@" + index + "\nD=D+A\n@255\nM=D\n@SP\nM=M-1\nA=M\nD=M\n@255\nA=M\nM=D\n";} else if (Objects.equals(segment, "that")) {asmCommand = "@THAT\nD=M\n@" + index + "\nD=D+A\n@255\nM=D\n@SP\nM=M-1\nA=M\nD=M\n@255\nA=M\nM=D\n";} else if (Objects.equals(segment, "temp")) {asmCommand = "@SP\nM=M-1\nA=M\nD=M\n@" + (5 + index) + "\nM=D\n";} else if (Objects.equals(segment, "pointer")) {if (index == 0) {asmCommand = "@SP\nM=M-1\nA=M\nD=M\n@THIS\nM=D\n";} else {asmCommand = "@SP\nM=M-1\nA=M\nD=M\n@THAT\nM=D\n";}} else if (Objects.equals(segment, "static")) {asmCommand = "@SP\nM=M-1\nA=M\nD=M\n@" + fileName+ index + "\nM=D\n";}}asm.write(asmCommand);}public void writeLabel(String label) throws IOException {asm.write("(" + label + ")\n");}public void writeGoto(String label) throws IOException {asm.write("@" + label + "\n0;JMP\n");}public void writeIf(String label) throws IOException {asm.write("@SP\nM=M-1\nA=M\nD=M\n@" + label + "\nD;JNE\n");}public void writeCall(String functionName, int nArgs) throws IOException {asm.write("@Caller" + jump + "\nD=A\n@SP\nA=M\nM=D\n@SP\nM=M+1\n");asm.write("@LCL\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n");asm.write("@ARG\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n");asm.write("@THIS\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n");asm.write("@THAT\nD=M\n@SP\nA=M\nM=D\n@SP\nM=M+1\n");asm.write("@SP\nD=M\n@5\nD=D-A\n@" + nArgs + "\nD=D-A\n@ARG\nM=D\n");asm.write("@SP\nD=M\n@LCL\nM=D\n");asm.write("@" + functionName + "\n0;JMP\n(Caller" + jump + ")\n");jump++;}public void writeReturn() throws IOException {asm.write("@LCL\nD=M\n@FRAME\nM=D\n");asm.write("@5\nA=D-A\nD=M\n@RET\nM=D\n");asm.write("@ARG\nD=M\n@0\nD=D+A\n@255\nM=D\n@SP\nM=M-1\nA=M\nD=M\n@255\nA=M\nM=D\n");asm.write("@ARG\nD=M\n@SP\nM=D+1\n");asm.write("@FRAME\nD=M-1\nAM=D\nD=M\n@THAT\nM=D\n");asm.write("@FRAME\nD=M-1\nAM=D\nD=M\n@THIS\nM=D\n");asm.write("@FRAME\nD=M-1\nAM=D\nD=M\n@ARG\nM=D\n");asm.write("@FRAME\nD=M-1\nAM=D\nD=M\n@LCL\nM=D\n");asm.write("@RET\nA=M\n0;JMP\n");}public void writeFunction(String functionName, int nArgs) throws IOException {asm.write("(" + functionName + ")\n");for (int i = 0; i < nArgs; i++) {writePushPop("C_PUSH", "constant", 0);}}public void writeInit() throws IOException {asm.write("@256\nD=A\n@SP\nM=D\n");writeCall("Sys.init", 0);}public void close() throws IOException {asm.close();}public void setFileName(String fileName) {this.fileName = fileName;}
}

Main

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Objects;public class Main {public static void main(String[] args) throws IOException {File file = new File("C:\\Users\\Yezi\\Desktop\\Java程序设计\\nand2tetris\\projects\\08\\FunctionCalls\\StaticsTest");ArrayList<File> vm = new ArrayList<>();CodeWriter codeWriter = new CodeWriter(new File("C:\\Users\\Yezi\\Desktop\\Java程序设计\\nand2tetris\\projects\\08\\FunctionCalls\\StaticsTest\\StaticsTest.asm"));codeWriter.writeInit();if (file.isFile()) {vm.add(file);} else {for (File one : Objects.requireNonNull(file.listFiles())) {if (one.getName().endsWith(".vm")) {vm.add(one);}}}for (File one : vm) {Parser parser = new Parser(one);codeWriter.setFileName(one.getName());while (parser.hasMoreCommands()) {parser.advance();if (Objects.equals(parser.commandType(), "C_ARITHMETIC")) {codeWriter.writeArithmetic(parser.arg1());} else if (Objects.equals(parser.commandType(), "C_LABEL")) {codeWriter.writeLabel(parser.arg1());} else if (Objects.equals(parser.commandType(), "C_GOTO")) {codeWriter.writeGoto(parser.arg1());} else if (Objects.equals(parser.commandType(), "C_IF")) {codeWriter.writeIf(parser.arg1());} else if (Objects.equals(parser.commandType(), "C_FUNCTION")) {codeWriter.writeFunction(parser.arg1(), parser.arg2());} else if (Objects.equals(parser.commandType(), "C_RETURN")) {codeWriter.writeReturn();} else if (Objects.equals(parser.commandType(), "C_CALL")) {codeWriter.writeCall(parser.arg1(), parser.arg2());} else {codeWriter.writePushPop(parser.commandType(), parser.arg1(), parser.arg2());}}parser.close();}codeWriter.close();}
}

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

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

相关文章

2.JMeter压测接口

概述 今日目标&#xff1a; JMeter使用 配置线程组配置 HTTP 接口配置断言 配置响应断言配置断言响应时间 配置结果监听压测报告 接口准备聚合报告察看结果树其它 线程组配置详解 线程数Ramp-Up bug结束 JMeter使用 双击 ApacheJMeter.jar 启动&#xff0c;然后修改名称&a…

C 练习实例10 打印楼梯,同时在楼梯上方打印两个笑脸。

题目&#xff1a;打印楼梯&#xff0c;同时在楼梯上方打印两个笑脸。 程序分析&#xff1a;用 ASCII 1 来输出笑脸&#xff1b;用i控制行&#xff0c;j来控制列&#xff0c;j根据i的变化来控制输出黑方格的个数。 如果出现乱码情况请参考【C 练习实例7】的解决方法。 实例 …

第四届辽宁省大学生程序设计竞赛(正式赛)(12/13)

AC情况 赛中通过赛后通过暂未通过A√B√C√D○E○F√G√H√I○J√K—L√M√ 整体体验 easy&#xff1a;ABFHL mid&#xff1a;MJGC hard&#xff1a;IDKE 心得 感觉出了一堆典题&#xff0c;少数题还有些意思&#xff0c;E题确实神仙 题解 A. 欢迎来到辽宁省赛&#x…

iphone15 nplayer播放本地电影投屏天猫魔盒(电视)卡顿解决方案

文章目录 投屏环境现象写在前面 解决方案所需投屏app安装方法试用结果如果文章对您有用&#xff0c;欢迎收藏或关注&#xff01; iphone15 nplayer播放本地电影投屏天猫魔盒(电视)卡顿解决方案 投屏环境 全千兆wifi6局域网 1000兆电信宽带 天锚魔盒4Pro 8G&#xff08;M19&…

《研发效能(DevOps)工程师》课程简介(四)丨IDCF

由国家工业和信息化部教育与考试中心颁发的职业技术证书&#xff0c;也是国内首个研发效能&#xff08;DevOps&#xff09;职业技术认证&#xff0c;内涵1000页学习教材2000分钟的课程内容讲解460多个技术知识点300多道练习题。涵盖【组织与协作】、【产品设计与运营】、【开发…

YOLOv8-Seg改进:动态蛇形卷积(Dynamic Snake Convolution) | ICCV2023

🚀🚀🚀本文改进:动态蛇形卷积(Dynamic Snake Convolution),增强微小特征提取能力,引入到YOLOv8-Seg,与C2f结合实现二次创新 🚀🚀🚀Dynamic Snake Convolution亲测在番薯破损分割任务中,mask mAP@0.5 从原始的0.625提升至0.645 🚀🚀🚀YOLOv8-seg创新专…

namespace

1.namespace技术 namespace是Linux内核的一组特性&#xff0c;支持对内核资源进行分区隔离&#xff0c;让一组进程只能看到一组资源&#xff0c;而另一组进程只能看到另一组不同的资源。换句话说&#xff0c;namespace的关键特性是进程隔离。在运行许多不同服务的服务器上&…

2023.11.4 Idea 配置国内 Maven 源

目录 配置国内 Maven 源 重新下载 jar 包 配置国内 Maven 源 <mirror><id>alimaven</id><name>aliyun maven</name><url>http://maven.aliyun.com/nexus/content/groups/public/</url><mirrorOf>central</mirrorOf> …

chrome 扩展 popup 弹窗的使用

popup的基本使用方法 popup介绍 popup 是点击 browser_action 或者 page_action图标时打开的一个小窗口网页&#xff0c;焦点离开网页就立即关闭&#xff0c;一般用来做一些临时性的交互。 popup配置 V3版本中&#xff08;V2版本是在 browser_action 中 &#xff09;&#x…

台灯选用什么类型好?双十一值得入手的护眼台灯推荐

如何给孩子挑选一盏能够护眼的台灯一直是许多家长都为之头痛的一大难题&#xff0c;主要是如今市面上的台灯实在太多了&#xff0c;而且迭代速度非常快&#xff0c;再加上这些产品中还混杂了许多不专业品牌、网红产品和低价劣质产品等等&#xff0c;想要挑选到一款好的台灯确实…

python把Word题库转成Excle题库

又到了一年一度的背题时刻&#xff0c;但是收到的题库是Word版的&#xff0c;页数特别多 话不多说&#xff0c;上代码&#xff0c;有图有真相&#xff0c;代码里面备注的很详细 # 导入所需库 import csv import os import refrom docx import Document from win32com import c…

电子敲木鱼小程序源码系统 支持广告视频流量主 带完整搭建教程

大家好啊&#xff01;好久不见。今天罗峰来给大家分享一款电子敲木鱼小程序源码系统。相信大家都听说这个电子敲木鱼小程序&#xff0c;是当代年轻人缓解压力的一款小程序。今天罗峰就来给大家介绍一下他的功能&#xff0c;这款小程序自带广告视频流量&#xff0c;帮你轻松赚钱…

皮肤病辅助诊断软件,基于Android编写

1.系统介绍 编写的皮肤病辅助诊断软件&#xff0c;包括皮肤病识别、皮肤病区域分割、皮肤病信息介绍、识别历史记录查询、简单图像处理操作以及本机信息查询等功能 2.登录界面 运行之后首先显示登录界面 3.注册界面 注册一个账号 4.主界面 输入用户名密码点击登录按钮…

基于SpringAOP实现自定义接口权限控制

文章目录 一、接口鉴权方案分析1、接口鉴权方案2、角色分配权限树 二、编码实战1、定义权限树与常用方法2、自定义AOP注解3、AOP切面类&#xff08;也可以用拦截器实现&#xff09;4、测试一下 一、接口鉴权方案分析 1、接口鉴权方案 目前大部分接口鉴权方案&#xff0c;一般…

中文sd:SkyPaint-AI-Diffusion

https://huggingface.co/SkyworkAIGC/SkyPainthttps://huggingface.co/SkyworkAIGC/SkyPainthttps://github.com/SkyWorkAIGC/SkyPaint-AI-Diffusionhttps://github.com/SkyWorkAIGC/SkyPaint-AI-Diffusion从model_index.json看&#xff0c;应该算是标准的sd1.5架构了。 {&quo…

【Head First 设计模式】-- 观察者模式

背景 客户有一个WeatherData对象&#xff0c;负责追踪温度、湿度和气压等数据。现在客户给我们提了个需求&#xff0c;让我们利用WeatherData对象取得数据&#xff0c;并更新三个布告板&#xff1a;目前状况、气象统计和天气预报。 WeatherData对象提供了4个接口&#xff1a; …

linux系统SQL server数据库定时收缩

问题现象 出现下图问题&#xff0c;导致连接该数据库的程序不能正常启动 解决办法 定时收缩数据库 数据库定时收缩脚本 需要三个脚本文件 linux_sqlcmd_timing_task_shrink.sh&#xff1a;主脚本文件 # 设置数据库名称、用户名、密码等信息 # db_name"volador"…

Elasticsearch:使用你的 RAG 来进行聊天

什么是人工智能中的检索增强生成&#xff08;RAG&#xff09;&#xff1f; 检索增强生成 (RAG)&#xff0c;与你的文档聊天的超级英雄&#xff0c;架起信息检索和文本生成世界的桥梁&#xff01; 这就像福尔摩斯和莎士比亚联手解决需要大量知识的复杂任务。 RAG 突然介入&…

使用Python自动修改电脑的静态IP地址

目录 一、引言 二、实现思路 三、详细步骤 四、Python代码 五、注意事项 六、适用性和局限性 七、总结 一、引言 在网络应用中&#xff0c;有时我们需要频繁更改电脑的静态IP地址。例如&#xff0c;当我们在不同网络环境&#xff08;家庭、办公室&#xff09;中使用电脑…

洗衣洗鞋柜洗衣洗鞋小程序

支持&#xff1a;一键投递、上门取衣、自主送店、多种支付方式 TEL: 17638103951(同V) -----------------用户下单-------------- -------------------------多种支付和投递方式------------------------- -----------------商家取鞋--------------