资讯

精准传达 • 有效沟通

从品牌网站建设到网络营销策划,从策略到执行的一站式服务

简单介绍主引导程序的扩展知识

 亿速云服务器与全球多个国家顶级机房直接合作,提供包括香港、美国、日本等国家和地区的服务器,需要的请联系创新互联官方客服! 优质的服务器租用!      

成都创新互联公司主要为客户提供服务项目涵盖了网页视觉设计、VI标志设计、成都全网营销推广、网站程序开发、HTML5响应式网站建设手机网站制作、微商城、网站托管及成都网站维护公司、WEB系统开发、域名注册、国内外服务器租用、视频、平面设计、SEO优化排名。设计、前端、后端三个建站步骤的完善服务体系。一人跟踪测试的建站服务标准。已经为地磅秤行业客户提供了网站改版服务。

我们在前面讲解了主引导程序的基础知识,今天我们就来讲讲主引导程序的扩展。 不过主引导程序有个限制,那便是主引导程序的代码量不能超过 512 字节!那么我们如何来突破这个限制呢?

        我们现在的基本思路是在主引导程序中做如下工作:

        1、完成最基本的初始化工作;

        2、从存储介质中加载程序到内存中;

        3、将控制权交由新加载的程序执行;

        4、......

        那么我们具体该如何做呢?思路如下图所示

简单介绍主引导程序的扩展知识

        那么主引导程序应如何来加载存储介质中的其他程序呢?通过一定的文件系统的格式。那么什么是文件系统呢?它是指存储介质上组织文件数据的方法(数据组织的方式)。我们在此处以 FAT12 文件格式为例来进行讲解,为什么选用它呢?因为它的文件格式最为简单,也最适用于用来学习。FAT12 文件格式如下图所示

简单介绍主引导程序的扩展知识

        FAT12 是 DOS 时代的早期文件系统;它的结构非常简单,一直沿用于软盘;它的基本组织单位有 3 个:字节(Byte)是基本的数据单位,扇区(Sector)是磁盘中的最小数据单元,簇(Cluster)是一个或多个扇区。我们的解决方案便是:1、使用 FAT12 对软盘(data.img)进行格式化;2、编写可执行程序(Loader),并将其拷贝到软盘中;3、主引导程序(boot)在文件系统中查找 Loader;4、将 Loader 复制到内存中,并跳转到入口地址处执行。

        我们下来要进行的实验是:往虚拟软盘中写入文件。原材料有 FreeDos,Bochs,bximage。步骤是:1、创建虚拟软盘(data.img);2、在 FreeDos 中进行格式化(FAT12);3、将 data.img 挂载到 Linux 中,并写入文件。

        我们先在 Linux 中创建一张虚拟软盘,然后将其挂载到 /mnt/hgfs/ 目录下(命令是 mount -o loop data.img /mnt/hgfs/),然后在 Linux 中创建两个文件 test.txt 和 loader.bin。test.txt 的内容是 This is test for virtual floopy ...,loader.bin 中的内容是很多行的 D.T.Software. ;然后将他们两个拷贝到虚拟软盘中(拷贝到 /mnt/hgfs/ 目录下),最后进行虚拟软盘的卸载(umount /mnt/hgfs/)。我们看看虚拟软盘中的文件

简单介绍主引导程序的扩展知识

        我们看到在虚拟软盘中已经有两个文件了,再来看看他们的内容

简单介绍主引导程序的扩展知识

        我们看到已经出现了我们所写的文件内容。那么我们现在已经有了一张虚拟软盘,里面包含有我们自己写的文件内容。那么我们现在已经做完了前两步了,下来我们的工作是 boot 查找目标文件(Loader),并读取文件的内容!下来我们来深入看看 FAT12 文件系统,FAT12 文件系统由引导区、FAT 表、根目录项表和文件数据组成。如下图所示

简单介绍主引导程序的扩展知识

        FAT12 的主引导区存储的比较重要的信息是文件系统的类型,文件系统逻辑扇区总数,每簇包含的扇区数等。主引导区最后以 0x55AA 拉你个字节作为结束,共占用一个扇区。它的信息如下图所示

简单介绍主引导程序的扩展知识

        下一步的是实验是读取 data.img 中的文件系统信息。步骤:1、创建 Fat12Header 结构体类型;2、使用文件流读取前 512 字节的内容;3、解析并打印相关的信息。下来我们进行相关的实验,我们以 C++ 语言来写出一个解析 Fat12Header 的程序,源码如下所示

#include 
#include 
#include 
#include 

#pragma pack(push)
#pragma pack(1)

struct Fat12Header
{
    char BS_OEMName[8];
    ushort BPB_BytsPerSec;
    uchar BPB_SecPerClus;
    ushort BPB_RsvdSecCnt;
    uchar BPB_NumFATs;
    ushort BPB_RootEntCnt;
    ushort BPB_TotSec16;
    uchar BPB_Media;
    ushort BPB_FATSz16;
    ushort BPB_SecPerTrk;
    ushort BPB_NumHeads;
    uint BPB_HiddSec;
    uint BPB_TotSec32;
    uchar BS_DrvNum;
    uchar BS_Reserved1;
    uchar BS_BootSig;
    uint BS_VolID;
    char BS_VolLab[11];
    char BS_FileSysType[8];
};

#pragma pack(pop)

void PrintHeader(Fat12Header& rf, QString p)
{
    QFile file(p);

    if( file.open(QIODevice::ReadOnly) )
    {
        QDataStream in(&file);

        file.seek(3);    // 偏移 3 个字节,因为与文件系统相关的东西都是从这开始的(上面表格中)

        in.readRawData(reinterpret_cast(&rf), sizeof(rf));

        rf.BS_OEMName[7] = 0;    // 将数组的最后一个成员赋 0 值,将他们在后边可以看做是字符串处理
        rf.BS_VolLab[10] = 0;
        rf.BS_FileSysType[7] = 0;

        qDebug() << "BS_OEMName: " << rf.BS_OEMName;
        qDebug() << "BPB_BytsPerSec: " << hex << rf.BPB_BytsPerSec;
        qDebug() << "BPB_SecPerClus: " << hex << rf.BPB_SecPerClus;
        qDebug() << "BPB_RsvdSecCnt: " << hex << rf.BPB_RsvdSecCnt;
        qDebug() << "BPB_NumFATs: " << hex << rf.BPB_NumFATs;
        qDebug() << "BPB_RootEntCnt: " << hex << rf.BPB_RootEntCnt;
        qDebug() << "BPB_TotSec16: " << hex << rf.BPB_TotSec16;
        qDebug() << "BPB_Media: " << hex << rf.BPB_Media;
        qDebug() << "BPB_FATSz16: " << hex << rf.BPB_FATSz16;
        qDebug() << "BPB_SecPerTrk: " << hex << rf.BPB_SecPerTrk;
        qDebug() << "BPB_NumHeads: " << hex << rf.BPB_NumHeads;
        qDebug() << "BPB_HiddSec: " << hex << rf.BPB_HiddSec;
        qDebug() << "BPB_TotSec32: " << hex << rf.BPB_TotSec32;
        qDebug() << "BS_DrvNum: " << hex << rf.BS_DrvNum;
        qDebug() << "BS_Reserved1: " << hex << rf.BS_Reserved1;
        qDebug() << "BS_BootSig: " << hex << rf.BS_BootSig;
        qDebug() << "BS_VolID: " << hex << rf.BS_VolID;
        qDebug() << "BS_VolLab: " << rf.BS_VolLab;
        qDebug() << "BS_FileSysType: " << rf.BS_FileSysType;
    }

    file.close();
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Fat12Header f12;

    PrintHeader(f12, "E:\\data.img");
    
    return a.exec();
}

        编译结果如下

简单介绍主引导程序的扩展知识

        我们看到输出信息和上面的表格是一一对应的,下来我们在 PrintHeader 函数中添加几行代码,如下

file.seek(510);

uchar b510 = 0;
uchar b511 = 0;

in.readRawData(reinterpret_cast(&b510), sizeof(b510));
in.readRawData(reinterpret_cast(&b511), sizeof(b511));

qDebug() << "Byte 510: " << hex << b510;
qDebug() << "Byte 511: " << hex << b511;

        我们定位到 510 字节处,看看 510 字节和 511 字节处是什么内容

简单介绍主引导程序的扩展知识

        是最后的两个字节 55 和 aa。我们再来看看在创建的虚拟软盘中启动的时候,它会打印出什么,如下

简单介绍主引导程序的扩展知识

        我们看到这个空的虚拟软盘在开机时输出的是这一行字符串,那么这行字符串是哪来的呢?通过上面的实验,我们可以得出下面的结论:1、在 FreeDos 中的 format 程序在格式化软盘的时候自动在第 0 扇区生成了一个主引导程序,这个主引导程序只打印一个字符串;2、文件格式和文件系统都是用于定义数据如何存放的规则,只要遵循这个规则就能成功读写目标数据。我们下来的问题就是如何在 FAT12 根目录中查找是否存在目标文件呢?我们首先来看看根目录区的大小和位置,如下

简单介绍主引导程序的扩展知识

        我们看到根目录区中的第19个扇区中存放的是目录文件项,它的大小为 7168 个字节,计算公式如上图所示。FAT12 文件系统中的根目录区是由目录项构成,每一个目录项代表根目录中的一个文件索引。一些数据成员如下图所示

简单介绍主引导程序的扩展知识

        下来我们就进行一个实验:读取 FAT12 文件系统的根目录信息。

        步骤如下:

        1、创建 RootEntry 结构体类型;

        2、使用文件流顺序读取每个项的内容;

        3、解析并打印相关的信息。

        我们在进行实验之前,还是先来介绍下目录项中的一些关键成员吧。a> DIR_Name 文件名(用于判断是否为目标文件);b> DIR_FstClus 文件数据起始存储位置(用于确定读取位置);c> DIR_FileSize 文件大小(用于确定读取的字节数)。

        我们还是基于 C++ 语言来写这个程序,源码如下

#include 
#include 
#include 
#include 
#include 
#include 

#pragma pack(push)
#pragma pack(1)

struct Fat12Header
{
    char BS_OEMName[8];
    ushort BPB_BytsPerSec;
    uchar BPB_SecPerClus;
    ushort BPB_RsvdSecCnt;
    uchar BPB_NumFATs;
    ushort BPB_RootEntCnt;
    ushort BPB_TotSec16;
    uchar BPB_Media;
    ushort BPB_FATSz16;
    ushort BPB_SecPerTrk;
    ushort BPB_NumHeads;
    uint BPB_HiddSec;
    uint BPB_TotSec32;
    uchar BS_DrvNum;
    uchar BS_Reserved1;
    uchar BS_BootSig;
    uint BS_VolID;
    char BS_VolLab[11];
    char BS_FileSysType[8];
};

struct RootEntry
{
    char DIR_Name[11];
    uchar DIR_Attr;
    uchar reserve[10];
    ushort DIR_WrtTime;
    ushort DIR_WrtDate;
    ushort DIR_FstClus;
    uint DIR_FileSize;
};

#pragma pack(pop)

void PrintHeader(Fat12Header& rf, QString p)
{
    QFile file(p);

    if( file.open(QIODevice::ReadOnly) )
    {
        QDataStream in(&file);

        file.seek(3);

        in.readRawData(reinterpret_cast(&rf), sizeof(rf));

        rf.BS_OEMName[7] = 0;
        rf.BS_VolLab[10] = 0;
        rf.BS_FileSysType[7] = 0;

        qDebug() << "BS_OEMName: " << rf.BS_OEMName;
        qDebug() << "BPB_BytsPerSec: " << hex << rf.BPB_BytsPerSec;
        qDebug() << "BPB_SecPerClus: " << hex << rf.BPB_SecPerClus;
        qDebug() << "BPB_RsvdSecCnt: " << hex << rf.BPB_RsvdSecCnt;
        qDebug() << "BPB_NumFATs: " << hex << rf.BPB_NumFATs;
        qDebug() << "BPB_RootEntCnt: " << hex << rf.BPB_RootEntCnt;
        qDebug() << "BPB_TotSec16: " << hex << rf.BPB_TotSec16;
        qDebug() << "BPB_Media: " << hex << rf.BPB_Media;
        qDebug() << "BPB_FATSz16: " << hex << rf.BPB_FATSz16;
        qDebug() << "BPB_SecPerTrk: " << hex << rf.BPB_SecPerTrk;
        qDebug() << "BPB_NumHeads: " << hex << rf.BPB_NumHeads;
        qDebug() << "BPB_HiddSec: " << hex << rf.BPB_HiddSec;
        qDebug() << "BPB_TotSec32: " << hex << rf.BPB_TotSec32;
        qDebug() << "BS_DrvNum: " << hex << rf.BS_DrvNum;
        qDebug() << "BS_Reserved1: " << hex << rf.BS_Reserved1;
        qDebug() << "BS_BootSig: " << hex << rf.BS_BootSig;
        qDebug() << "BS_VolID: " << hex << rf.BS_VolID;
        qDebug() << "BS_VolLab: " << rf.BS_VolLab;
        qDebug() << "BS_FileSysType: " << rf.BS_FileSysType;

        file.seek(510);

        uchar b510 = 0;
        uchar b511 = 0;

        in.readRawData(reinterpret_cast(&b510), sizeof(b510));
        in.readRawData(reinterpret_cast(&b511), sizeof(b511));

        qDebug() << "Byte 510: " << hex << b510;
        qDebug() << "Byte 511: " << hex << b511;
    }

    file.close();
}

RootEntry FindRootEntry(Fat12Header& rf, QString p, int i)
{
    RootEntry ret = {{0}};

    QFile file(p);

    if( file.open(QIODevice::ReadOnly) && (0 <= i) && (i < rf.BPB_RootEntCnt) )
    {
        QDataStream in(&file);

        file.seek(19 * rf.BPB_BytsPerSec + i * sizeof(RootEntry));

        in.readRawData(reinterpret_cast(&ret), sizeof(ret));
    }

    file.close();

    return ret;
}

void PrintRootEntry(Fat12Header& rf, QString p)
{
    for(int i=0; i= 0 )
            {
                QString n = fn.mid(0, d);
                QString p = fn.mid(d + 1);

                if( name.startsWith(n) && name.endsWith(p) )
                {
                    ret = re;
                    break;
                }
            }
            else
            {
                if( fn == name )
                {
                    ret = re;
                    break;
                }
            }
        }
    }

    return ret;
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QString img = "E:\\data.img";
    Fat12Header f12;

    qDebug() << "Read Header:";

    PrintHeader(f12, img);

    qDebug() << endl;

    qDebug() << "Print Root Entry:";

    PrintRootEntry(f12, img);

    return a.exec();
}

        我们来看看输出信息

简单介绍主引导程序的扩展知识

        我们看到在输出的信息中就有我们自己写的 test.txt 文件和 loader.bin 文件。下来我们来使用下 FindRootEntry 这个函数来进行文件的查找,在 main.cpp 中添加如下代码

    RootEntry re = FindRootEntry(f12, img, "LOADER.BIN");
    
    qDebug() << "DIR_Name: " << hex << re.DIR_Name;
    qDebug() << "DIR_Attr: " << hex << re.DIR_Attr;
    qDebug() << "DIR_WrtDate: " << hex << re.DIR_WrtDate;
    qDebug() << "DIR_WrtTime: " << hex << re.DIR_WrtTime;
    qDebug() << "DIR_FstClus: " << hex << re.DIR_FstClus;
    qDebug() << "DIR_FileSize: " << hex << re.DIR_FileSize;

        我们来看看会输出什么信息

简单介绍主引导程序的扩展知识

        我们看到输出的是 loader.bin 文件的相关信息,我们将其换成 test.txt 呢,看看结果

简单介绍主引导程序的扩展知识

        我们再来随便写个文件名试试呢

简单介绍主引导程序的扩展知识

        我们看到打印的全是 0。下来我们来看看 FAT 表中的先后关系,它是以簇(扇区)为单位存储文件数据,每个表项(vec[i])表示文件数据的实际位置(簇)。即 DIR_FstClus 表示文件第 0 簇(扇区)的位置,vec[DIR_FstClus] 表示文件第 1 簇(扇区)的位置;vec[vec[DIR_FstClus]] 表示文件第 2 簇(扇区)的位置。下面来看看 FAT12 数据物理组织示意图,如下如所示

简单介绍主引导程序的扩展知识

        即比如它的起始为 C,然后下一个表项为 O,O 指向的下一个表项又是 Z,以此类推直至最后的 S 指向的表项为 NULL。其实它的数据组织方式和我们之前所接触的单链表是有点类似的,它的逻辑组织示意图如下图所示

简单介绍主引导程序的扩展知识

        下来我们再来进行一个实验,加载 FAT12 中的文件数据。

        步骤如下:

        1、在根目录区查找目标文件对应的项;

        2、获取目标文件的起始簇号和文件大小;

        3、根据 FAT 表中记录的逻辑先后关系读取数据。

        值得注意的是:FAT 表中的每个表只占用 12 比特(1.5字节);FAT 表一共记录了 BPB_BytsPerSec * 9 * 2 / 3 个表项;可以使用一个 short 表示一个表项的值;如果表项值大于等于 0xFF8,这说明已经到达最后一个簇;如果表项值等于 0xFF7,则说明当前簇已经损坏;数据区起始簇(扇区)号为 33,地址为 0x4200;数据区起始地址所对应的标号为 2 (不为 0);因此,DIR_FstClus 对应的地址为:0x4200 + (DIR_FstClus - 2) * 512。

        我们基于前面编写的代码再次编写。源码如下

#include 
#include 
#include 
#include 
#include 
#include 

#pragma pack(push)
#pragma pack(1)

struct Fat12Header
{
    char BS_OEMName[8];
    ushort BPB_BytsPerSec;
    uchar BPB_SecPerClus;
    ushort BPB_RsvdSecCnt;
    uchar BPB_NumFATs;
    ushort BPB_RootEntCnt;
    ushort BPB_TotSec16;
    uchar BPB_Media;
    ushort BPB_FATSz16;
    ushort BPB_SecPerTrk;
    ushort BPB_NumHeads;
    uint BPB_HiddSec;
    uint BPB_TotSec32;
    uchar BS_DrvNum;
    uchar BS_Reserved1;
    uchar BS_BootSig;
    uint BS_VolID;
    char BS_VolLab[11];
    char BS_FileSysType[8];
};

struct RootEntry
{
    char DIR_Name[11];
    uchar DIR_Attr;
    uchar reserve[10];
    ushort DIR_WrtTime;
    ushort DIR_WrtDate;
    ushort DIR_FstClus;
    uint DIR_FileSize;
};

#pragma pack(pop)

void PrintHeader(Fat12Header& rf, QString p)
{
    QFile file(p);

    if( file.open(QIODevice::ReadOnly) )
    {
        QDataStream in(&file);

        file.seek(3);

        in.readRawData(reinterpret_cast(&rf), sizeof(rf));

        rf.BS_OEMName[7] = 0;
        rf.BS_VolLab[10] = 0;
        rf.BS_FileSysType[7] = 0;

        qDebug() << "BS_OEMName: " << rf.BS_OEMName;
        qDebug() << "BPB_BytsPerSec: " << hex << rf.BPB_BytsPerSec;
        qDebug() << "BPB_SecPerClus: " << hex << rf.BPB_SecPerClus;
        qDebug() << "BPB_RsvdSecCnt: " << hex << rf.BPB_RsvdSecCnt;
        qDebug() << "BPB_NumFATs: " << hex << rf.BPB_NumFATs;
        qDebug() << "BPB_RootEntCnt: " << hex << rf.BPB_RootEntCnt;
        qDebug() << "BPB_TotSec16: " << hex << rf.BPB_TotSec16;
        qDebug() << "BPB_Media: " << hex << rf.BPB_Media;
        qDebug() << "BPB_FATSz16: " << hex << rf.BPB_FATSz16;
        qDebug() << "BPB_SecPerTrk: " << hex << rf.BPB_SecPerTrk;
        qDebug() << "BPB_NumHeads: " << hex << rf.BPB_NumHeads;
        qDebug() << "BPB_HiddSec: " << hex << rf.BPB_HiddSec;
        qDebug() << "BPB_TotSec32: " << hex << rf.BPB_TotSec32;
        qDebug() << "BS_DrvNum: " << hex << rf.BS_DrvNum;
        qDebug() << "BS_Reserved1: " << hex << rf.BS_Reserved1;
        qDebug() << "BS_BootSig: " << hex << rf.BS_BootSig;
        qDebug() << "BS_VolID: " << hex << rf.BS_VolID;
        qDebug() << "BS_VolLab: " << rf.BS_VolLab;
        qDebug() << "BS_FileSysType: " << rf.BS_FileSysType;

        file.seek(510);

        uchar b510 = 0;
        uchar b511 = 0;

        in.readRawData(reinterpret_cast(&b510), sizeof(b510));
        in.readRawData(reinterpret_cast(&b511), sizeof(b511));

        qDebug() << "Byte 510: " << hex << b510;
        qDebug() << "Byte 511: " << hex << b511;
    }

    file.close();
}

RootEntry FindRootEntry(Fat12Header& rf, QString p, int i)
{
    RootEntry ret = {{0}};

    QFile file(p);

    if( file.open(QIODevice::ReadOnly) && (0 <= i) && (i < rf.BPB_RootEntCnt) )
    {
        QDataStream in(&file);

        file.seek(19 * rf.BPB_BytsPerSec + i * sizeof(RootEntry));

        in.readRawData(reinterpret_cast(&ret), sizeof(ret));
    }

    file.close();

    return ret;
}

void PrintRootEntry(Fat12Header& rf, QString p)
{
    for(int i=0; i= 0 )
            {
                QString n = fn.mid(0, d);
                QString p = fn.mid(d + 1);

                if( name.startsWith(n) && name.endsWith(p) )
                {
                    ret = re;
                    break;
                }
            }
            else
            {
                if( fn == name )
                {
                    ret = re;
                    break;
                }
            }
        }
    }

    return ret;
}

QVector ReadFat(Fat12Header& rf, QString p)
{
    QFile file(p);
    int size = rf.BPB_BytsPerSec * 9;
    uchar* fat = new uchar[size];
    QVector ret(size * 2 / 3, 0xFFFF);

    if( file.open(QIODevice::ReadOnly) )
  {
        QDataStream in(&file);

        file.seek(rf.BPB_BytsPerSec * 1);

        in.readRawData(reinterpret_cast(fat), size);

        for(int i=0, j=0; i((fat[i+1] & 0x0F) << 8) | fat[i];
            ret[j+1] = static_cast(fat[i+2] << 4) | ((fat[i+1] >> 4) & 0x0F);
        }
    }

    file.close();

    delete[] fat;

    return ret;
}

QByteArray ReadFileContent(Fat12Header& rf, QString p, QString fn)
{
    QByteArray ret;
    RootEntry re = FindRootEntry(rf, p, fn);

    if( re.DIR_Name[0] != '\0' )
    {
        QVector vec = ReadFat(rf, p);
        QFile file(p);

        if( file.open(QIODevice::ReadOnly) )
        {
            char buf[512] = {0};
            QDataStream in(&file);
            int count = 0;

            ret.resize(re.DIR_FileSize);

            for(int i=0, j=re.DIR_FstClus; j<0xFF7; i+=512, j=vec[j])
            {
                file.seek(rf.BPB_BytsPerSec * (33 + j - 2));

                in.readRawData(buf, sizeof(buf));

                for(uint k=0; k

        我们来看看打印信息

简单介绍主引导程序的扩展知识

        这便是我们之前自己写的内容,说明已经成功获取到了。我们再来随便写个文件的名字,看看会获取到什么

简单介绍主引导程序的扩展知识

        我们看到是空的,什么也没有。直到现在,我们已经对主引导程序中的文件成功的进行了加载并解析内容。通过今天对主引导程序扩展的学习,总结如下:1、主引导程序的代码量不超过 512 字节,可以通过主引导程序加载新程序的方式突破限制;2、加载新程序需要依赖于文件系统;3、FAT12 是一种早期用于软盘的简单文件系统,它的重要信息存储于 0 扇区;4、FAT12 根目录区记录了文件的起始簇号和长度;5、通过查找根目录区能够确定是否存在目标文件;6、FAT12 文件数据的组织使用了单链表的思想,文件数据离散的分布于存储介质中,文件数据通过 FAT 项进行关联。

创新互联的服务器不仅具有高稳定性,高速访问,而且易于管理,安全和轻松使用,以减少用户在服务器维护中的能量和时间成本,并专注于自己的业务的开发和推广。创新互联服务器,致力于为用户提供性价比最高的服务器!



网页题目:简单介绍主引导程序的扩展知识
网页链接:http://cdkjz.cn/article/gghdpp.html
多年建站经验

多一份参考,总有益处

联系快上网,免费获得专属《策划方案》及报价

咨询相关问题或预约面谈,可以通过以下方式与我们联系

大客户专线   成都:13518219792   座机:028-86922220