您好,登錄后才能下訂單哦!
億速云服務器與全球多個國家頂級機房直接合作,提供包括香港、美國、日本等國家和地區的服務器,需要的請聯系億速云官方客服! 優質的服務器租用!
我們在前面講解了主引導程序的基礎知識,今天我們就來講講主引導程序的擴展。 不過主引導程序有個限制,那便是主引導程序的代碼量不能超過 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 <QtCore/QCoreApplication> #include <QFile> #include <QDataStream> #include <QDebug> #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<char*>(&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<char*>(&b510), sizeof(b510)); in.readRawData(reinterpret_cast<char*>(&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 <QtCore/QCoreApplication> #include <QFile> #include <QDataStream> #include <QDebug> #include <QVector> #include <QByteArray> #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<char*>(&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<char*>(&b510), sizeof(b510)); in.readRawData(reinterpret_cast<char*>(&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<char*>(&ret), sizeof(ret)); } file.close(); return ret; } void PrintRootEntry(Fat12Header& rf, QString p) { for(int i=0; i<rf.BPB_RootEntCnt; i++) { RootEntry re = FindRootEntry(rf, p, i); if( re.DIR_Name[0] != '\0' ) { qDebug() << i << ":"; 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; } } } RootEntry FindRootEntry(Fat12Header& rf, QString p, QString fn) { RootEntry ret = {{0}}; for(int i=0; i<rf.BPB_RootEntCnt; i++) { RootEntry re = FindRootEntry(rf, p, i); if( re.DIR_Name[0] != '\0' ) { int d = fn.lastIndexOf("."); QString name = QString(re.DIR_Name).trimmed(); if( d >= 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 <QtCore/QCoreApplication> #include <QFile> #include <QDataStream> #include <QDebug> #include <QVector> #include <QByteArray> #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<char*>(&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<char*>(&b510), sizeof(b510)); in.readRawData(reinterpret_cast<char*>(&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<char*>(&ret), sizeof(ret)); } file.close(); return ret; } void PrintRootEntry(Fat12Header& rf, QString p) { for(int i=0; i<rf.BPB_RootEntCnt; i++) { RootEntry re = FindRootEntry(rf, p, i); if( re.DIR_Name[0] != '\0' ) { qDebug() << i << ":"; 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; } } } RootEntry FindRootEntry(Fat12Header& rf, QString p, QString fn) { RootEntry ret = {{0}}; for(int i=0; i<rf.BPB_RootEntCnt; i++) { RootEntry re = FindRootEntry(rf, p, i); if( re.DIR_Name[0] != '\0' ) { int d = fn.lastIndexOf("."); QString name = QString(re.DIR_Name).trimmed(); if( d >= 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<ushort> ReadFat(Fat12Header& rf, QString p) { QFile file(p); int size = rf.BPB_BytsPerSec * 9; uchar* fat = new uchar[size]; QVector<ushort> ret(size * 2 / 3, 0xFFFF); if( file.open(QIODevice::ReadOnly) ) { QDataStream in(&file); file.seek(rf.BPB_BytsPerSec * 1); in.readRawData(reinterpret_cast<char*>(fat), size); for(int i=0, j=0; i<size; i+=3, j+=2) { ret[j] = static_cast<ushort>((fat[i+1] & 0x0F) << 8) | fat[i]; ret[j+1] = static_cast<ushort>(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<ushort> 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<sizeof(buf); k++) { if( count < ret.size() ) { ret[i+k] = buf[k]; count++; } } } } file.close(); } 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 File Content:"; QString content = QString(ReadFileContent(f12, img, "TEST.TXT")); qDebug() << content; return a.exec(); }
我們來看看打印信息
這便是我們之前自己寫的內容,說明已經成功獲取到了。我們再來隨便寫個文件的名字,看看會獲取到什么
我們看到是空的,什么也沒有。直到現在,我們已經對主引導程序中的文件成功的進行了加載并解析內容。通過今天對主引導程序擴展的學習,總結如下:1、主引導程序的代碼量不超過 512 字節,可以通過主引導程序加載新程序的方式突破限制;2、加載新程序需要依賴于文件系統;3、FAT12 是一種早期用于軟盤的簡單文件系統,它的重要信息存儲于 0 扇區;4、FAT12 根目錄區記錄了文件的起始簇號和長度;5、通過查找根目錄區能夠確定是否存在目標文件;6、FAT12 文件數據的組織使用了單鏈表的思想,文件數據離散的分布于存儲介質中,文件數據通過 FAT 項進行關聯。
億速云的服務器不僅具有高穩定性,高速訪問,而且易于管理,安全和輕松使用,以減少用戶在服務器維護中的能量和時間成本,并專注于自己的業務的開發和推廣。億速云服務器,致力于為用戶提供性價比最高的服務器!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。