【腦凍結】CRC我就拿下了


國人講CRC的沒有什么能講明白的文章,除了一篇《我學習 CRC32、CRC16、CRC 原理和算法的總結(與 WINRAR 結果一致)》,這里先感謝他,另,他也有一些沒有說明白的地方,怎么說呢,還是鄙人自己來吧。

我弄明白CRC這個原理和算法主要參考的是上面的國人的那篇和這個外國的《A PAINLESS GUIDE TO CRC ERROR DETECTION ALGORITHMS INDEX V3.00 (9/24/96)》,地址 http://www.repairfaq.org/filipg/LINK/F_crc_v3.html 這兩篇大作。

首先要明確的是CRC只是一種錯誤檢錯的碼,而沒有糾正的作用,那么糾錯碼可以通過對原數據的+/-/*/÷等進行,但是+/-的問題太大,比如都是對原數據每個bit進行累加運算,如果出錯了的話,比如有1bit的1變為0,1bit的0變為1,+/-運算完全識別不出這種錯誤,顯然這種錯誤除法的辨認概率是非常大的,於是不知道哪位大神就把CRC這種校驗方式給搞出來了。

這里我先介紹一下CRC的運算方式,其實就是一個數去除以生成多項式,余數就是CRC校驗碼,但是這里的除法和現實意義上面的除法有點出入。

首先先看一下原始的除法取余運算。正常除法,120÷9=13…3,這里的余數即是我們所要的校驗位的碼字。

               1 1 0 1
        ______________
1 0 0 1/ 1 1 1 1 0 0 0          被除數120是1111000,除數9是1001
         1 0 0 1
         -------------
           1 1 0 0              第一次減法后得到011
           1 0 0 1
           -------------
               1 1 0 0          第二次減法后得到0101
               1 0 0 1
           -------------
                   1 1   ->     余數是3

這里用的是真正的減法,不好用,要用減法真的,於是將所有減法運算都用XOR運算替換掉,CRC就橫空出世了,這里還用120這個數舉例子,CRC所用的除法里,120÷9=14…6

               1 1 1 0
        ______________
1 0 0 1/ 1 1 1 1 0 0 0          被除數120是1111000,除數9是1001
         1 0 0 1
         -------------
           1 1 0 0              第一次XOR后得到011
           1 0 0 1
           -------------
             1 0 1 0            第二次XOR后得到0101
             1 0 0 1
           -------------
                 1 1 0   ->     余數是6

這里的6就是真正的CRC校驗碼,那么他是誰的CRC呢?注意,他不是120的CRC校驗碼,而是15(0x0F)的CRC校驗碼,這里為啥是這樣,只能說是規定,因為CRC相當於是余數,如果說110b是1111b的CRC校驗碼,那么將1111 110b排成一排送入CRC模塊,剩余的余數就應該是0,規定就是這樣的。所以對於110b只能說他是1111b的CRC校驗碼,不能說他是1111000b的CRC校驗碼。

通過這個例子,可以說明CRC這種算法的結構,對1111b進行CRC校驗運算,校驗碼是110b,其中,1111b為數據,1001b(9)就是生成多項式,這里的CRC位數為3,記為W=3,因為是在1111b后面追加3bit的數據,所以是CRC3,生成多項式可以寫作g(x) = x3+1。以此為例來循序漸進的介紹CRC的3種計算方法。

一、直接計算法

直接計算法就是用寄存器去模擬上面的CRC除法過程,我們假設待測數據是11 0101 1011,生成項是10011(g(x) = x4+x+1),需要有一個4bit的寄存器(因為每次的最高位都必定被XOR為0或者是移位直到最高位為0再XOR),通過反復的移位和進行CRC的除法,最終該寄存器中的值就是我們所要求的余數。開始

1) 待測數據后擴展W=4個比特0,變成1101011011 0000(augmented message);
2) 寄存器初始化置0;
3) 先在寄存器中移入數據1101;
4) 寄存器左移一位,並且右邊移入下一位數據0。這樣最高位1移出,由於最高位是1,故本次的商是1,要用除數1001來進行XOR,最高位肯定XOR得0,故不管它,只要用低4位0011來進行XOR就可以,即0011對此時寄存器進行XOR,寄存器中得到1001,即第一次 XOR 后的結果(相當於是數據11010與生成項10011進行了一次XOR,並把最高位0消掉了)。 如果移出的最高位是0,則用0000來進行XOR(相當於只是進行了移位)。
5) 一直重復這個過程,就能得到最后余數了。

            3   2   1   0   Bits
          +---+---+---+---+
 Pop! <-- |   |   |   |   | <----- Augmented message(1101011011 0000)
          +---+---+---+---+

代碼描述如下

#define CRC_WIDTH   4
#define CRC_POLY    0x3     // 0011b

// load data
    data = 0x35B;           // 1101011011b
// append W zeros to the data
    data <<= CRC_WIDTH;
// initial the regs
    regs = 0;
// processing
    for(shift_bit=DATA_WIDTH+CRC_WIDTH; shift_bit>0; shift_bit--){
    // shift
        regs = (regs<<1) | ((data>>(shift_bit-1))&0x1);
    // xor
        if(regs>>CRC_WIDTH)     regs = regs ^ CRC_POLY;
    }

這種方法是最直觀的方法,先理解了這種方法才能理解后面的方法,從這里的C代碼到硬件實現就更簡單了,畫出電路圖

        +---+   +---+   +---+         +---+
    +<--|   |<--|   |<--|   |<--XOR<--|   |<--XOR<-- Augmented message
    |   +---+   +---+   +---+    ^    +---+    ^
    |                            |             |
    |                            |             |
    +----------------------------+-------------+

稍微體會一下就會明白,看清XOR的位置,對應除法的時候異或運算的處理,就好。

 

二、驅動表法(table drive)

故名思議,驅動表法就是查表,預先生成一個表,就省得每次都進行XOR費時費力,驅動表法的概念推導卻是全靠了直接計算法得來的。(但是其實驅動表法並不是我們常用的查表法,常用的查表法這里被稱為“直接查表法”,這是后話。)具體怎么來的呢,聽我慢慢道來

首先,舉個例子,待測數據是1011 0100b,生成多項式比如是1 0001 1100b,那么我們比如想做的是4bit的查找表,這里查找表的大小其實是無關緊要的,只不過一般都用的是8bit的查找表,因為一般的數據都是以byte為單位的,如果是4bit的查找表那么我們的表大小就是16,但是每個表單元還是根據CRC的寬度定的,話不多說,先來看看直接法計算的過程,

                                   1 0 1 1 1 0 0 0 
                  ________________________________
1 0 0 0 1 1 1 0 0/ 1 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0
                   1 0 0 0 1 1 1 0 0
                   -------------------------------
                       1 1 1 0 1 0 0 0 0
                       1 0 0 0 1 1 1 0 0
                   -------------------------------
                         1 1 0 0 1 1 0 0 0
                         1 0 0 0 1 1 1 0 0
                   -------------------------------
                           1 0 0 0 0 1 0 0 0
                           1 0 0 0 1 1 1 0 0
                   -------------------------------
                                   1 0 1 0 0 0 0 0

 首先,要知道的是A^B^C = A^(B^C),那么我們在使用直接計算法的時候,針對前4bit,我們需要計算4次,3次都是要計算與生成多項式的XOR結果,那么查表法應運而生,看下面的式子

                                   1 0 1 1 1 0 0 0 
                  ________________________________
1 0 0 0 1 1 1 0 0/ 1 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0
                   1 0 0 0 1 1 1 0 0              --->
                     0 0 0 0 0 0 0 0 0            --->
                       1 0 0 0 1 1 1 0 0          --->
                         1 0 0 0 1 1 1 0 0        --->
                   -------------------------------
                           1 0 0 0 0 1 0 0 0
                           1 0 0 0 1 1 1 0 0
                   -------------------------------
                                   1 0 1 0 0 0 0 0

我們用空間去換時間,計算4次,並且知道要計算的高4bit是1011,那么我們把根據生成多項式進行移位,得出的高4bit為index的table中查出后面的8bit就可以相當於原來的4次高4bit的XOR運算,就可以變成這樣

                                   1 0 1 1 1 0 0 0 
                  ________________________________
1 0 0 0 1 1 1 0 0/ 1 0 1 1 0 1 0 0 0 0 0 0 0 0 0 0
                  (1 0 1 1)1 1 0 0 0 1 0 0
                   -------------------------------
                           1 0 0 0 0 1 0 0 0
                           1 0 0 0 1 1 1 0 0
                   -------------------------------
                                   1 0 1 0 0 0 0 0

即是說,應用POLY(生成多項式)來搞出4bit的查找表,index是由生成多項式的最高bit進行移位和XOR運算得到,內容當然也就是上面這樣的XOR的后面8bit的結果,針對上面的例子,查找表的生成函數是這樣的

#define LUT_WIDTH   4
#define CRC_WIDTH   8
#define CRC_POLY    0x11C

for(index=0; index<(1<<LUT_WIDTH); index++){
    temp = 0;
    for(bit_cnt=LUT_WIDTH; bit_cnt>0; bit_cnt--){
        if((index>>(bit_cnt-1) ^ temp>>(CRC_WIDTH-1))&0x1)  temp = (temp<<1) ^ CRC_POLY;
        else                                                temp <<= 1;
    }
    table[index] = (unsigned char)temp;
}

這樣,表就制作完成了,好進入正題,接着就開始驅動表法的計算方法了,拿到表之后要怎么查呢?(4bit的沒人用,為了描述方便,以常用的8bit index的查找表為例敘述)

1)register左移一個字節,從原始數據中讀入一個新的字節.  
2)利用剛從register移出的字節作為index定位table中的一個值  
3)把這個值XOR到register中。
4)如果還有未處理的數據則回到第一步繼續執行。

              1    0   Bytes
           +----+----+
    +-----<|    |    | <----- Augmented message
    |      +----+----+
    |           ^
    |           |
    |          XOR
    |           |
    |     0+----+----+
    v      +----+----+
    |      +----+----+
    |      +----+----+
    |      +----+----+
    |      +----+----+
    |      +----+----+
    +----->+----+----+
           +----+----+
           +----+----+
           +----+----+
           +----+----+
        255+----+----+

這里舉個計算的例子,計算0x31, 0x32, 0x33, 0x34對CRC-CCITT(CRC16,POLY=0x1021)的計算。高亮的字體就是每一步的寄存器中的數據。

        __________________
  10 21/ 31 32 33 34 00 00
            26 72           --> index=31
        ------------------
            14 41 34 00 00
               52 B5        --> index=14
        ------------------
               13 81 00 00
                  22 52     --> index=13
        ------------------
                  A3 52 00
                     85 89  --> index=A3
        ------------------
                     D7 89

計算過程代碼描述

unsigned char   buff[] = {0x31, 0x32, 0x33, 0x34};
unsigned int    len  = sizeof(buff);
unsigned char   *pointer;
unsigned int    regs;

pointer = buff;
regs = 0;
while(len--){
    regs = ((regs<<8)|*pointer++) ^ table[(regs>>8)&0xFF];
}
// append zeros to finish the calculation
for(i=0; i<2; i++){
    regs = (regs<<8) ^ table[(regs>>8)&0xFF];
}

 

三、直驅表法(direct table)

這才是平常軟硬件正常使用的方法,先來看看他的廬山真面目(對比圖,CRC32),先看下CRC32的驅動表法的圖形

              3    2    1    0   Bytes
           +----+----+----+----+
    +-----<|    |    |    |    | <----- Augmented message
    |      +----+----+----+----+
    |                ^
    |                |
    |               XOR
    |                |
    |     0+----+----+----+----+
    v      +----+----+----+----+
    |      +----+----+----+----+
    |      +----+----+----+----+
    |      +----+----+----+----+
    |      +----+----+----+----+
    |      +----+----+----+----+
    +----->+----+----+----+----+
           +----+----+----+----+
           +----+----+----+----+
           +----+----+----+----+
           +----+----+----+----+
        255+----+----+----+----+

首先要明確的就是,驅動表法,寄存器的初始值必須是0,之后開始運算,如果不是0的話相當於是在原數據的最高位前面insert了數據,之后,就是后面加上去的0,其實沒有真正的作用觀察前面{31 32 33 34}計算的表就能得出這個結論,0的實際意義就是要為了把前面的數據都送進register里面進行運算。英文原文解釋如下(但是我沒看懂 后來自己琢磨明白的)

TAIL
    The W/4 augmented zero bytes that appear at the end of the message will be pushed into the register from the right as all the other bytes are, but their values (0) will have no effect whatsoever on the register because 1) XORing with zero does not change the target byte, and 2) the four bytes are never propagated out the left side of the register where their zeroness might have some sort of influence. Thus, the sole function of the W/4 augmented zero bytes is to drive the calculation for another W/4 byte cycles so that the end of the REAL data passes all the way through the register.
HEAD
    If the initial value of the register is zero, the first four iterations of the loop will have the sole effect of shifting in the first four bytes of the message from the right. This is because the first 32 control bits are all zero and so nothing is XORed into the register. Even if the initial value is not zero, the first 4 byte iterations of the algorithm will have the sole effect of shifting the first 4 bytes of the message into the register and then XORing them with some constant value (that is a function of the initial value of the register).

These facts, combined with the XOR property
(A xor B) xor C = A xor (B xor C)

 

         31 32 33 34 00 00  --> data(1)=31, data(2)=32, data(3)=33, data(4)=34
            26 72           --> index(1)=31, 31 = data(1) ^ 00
        ------------------
            14 41 34 00 00
               52 B5        --> index(2)=14, 14 = data(2) ^ table(index(1))[15:8]
        ------------------
               13 81 00 00
                  22 52     --> index(3)=13, 13 = data(3) ^ table(index(2))[15:8] ^ table(index(1))[7:0]
        ------------------
                  A3 52 00
                     85 89  --> index(4)=A3, A3 = data(4) ^ table(index(3))[15:8] ^ table(index(2))[7:0]
        ------------------
                     D7 89

觀察每一步的index,就會發現,如果是CRC-CCITT的計算,其實的話每一步的index和最終結果都可以這么表示

index(n) = data(n)[7:0] ^ table(index(n-1))[15:8] ^ table(index(n-2))[7:0];
result = index(last);

於是再看直驅表法的圖形

    +-----<Message (non augmented)
    |
    v         3    2    1    0   Bytes
    |      +----+----+----+----+
   XOR----<|    |    |    |    |
    |      +----+----+----+----+
    |                ^
    |                |
    |               XOR
    |                |
    |     0+----+----+----+----+
    v      +----+----+----+----+
    |      +----+----+----+----+
    |      +----+----+----+----+
    |      +----+----+----+----+
    |      +----+----+----+----+
    |      +----+----+----+----+
    +----->+----+----+----+----+
           +----+----+----+----+
           +----+----+----+----+
           +----+----+----+----+
           +----+----+----+----+
        255+----+----+----+----+

算法描述

1) Shift the register left by one byte(this byte is called top byte), reading in a new message byte.
2) XOR the top byte with the next message byte to yield an index into the table ([0,255]).
3) XOR the table value into the register.
4) Goto 1 if more message to be processed.

代碼這樣

// direct table crc calculation, CRC16
    pointer = buff;
    regs = CRC_INIT;
    for(i=0; i<len; i++){
        regs = (regs<<8) ^ table[(regs>>8)&0xFF ^ *pointer++];
    }

這樣的話如果使用這種方法,那么最后append zero的0,因為XOR運算,所以XOR0還是原來的數,於是最后兩步的0就省掉了(注:這個過程是首先有了驅動表法,根據計算過程推出了直驅表法,再接着由於推出的直驅表法而省略掉了最后面的augmented zeros)。

還舉例子為31323334,POLY還是0x1021的例子

31 --> 00 00
       31 26 72
      ------------------
32 -----> 26 72
          14 52 B5
      ------------------
33 --------> 20 B5
             13 22 52
      ------------------
34 -----------> 97 52
                A3 85 89
      ------------------
                   D7 89

 下划線的是每一步的index,高亮的是每一步寄存器中留下的值,可以看出,每一步留下的值相當於都是前面的數據計算所得的CRC校驗碼。而這里的00,是真正意義上的CRC_INIT,就是寄存器的初始值,其實我覺得反映的就是接着前面計算CRC值再繼續進行計算,因為寄存器中每次的值相當於都是前面輸入數據的CRC值,那么寄存器中有啥都是前面數據留下的真正的CRC值,所以寄存器的初始值就是這個意思,在什么樣的基礎上繼續進行CRC的計算。

四、CRC計算模型

CRC不止這點東西,還有一些其他的東西要處理,比如CRC其實都是有model的,不光是CRC的POLY和INIT

Name   : "CRC-32" 
Width  : 32
Poly   : 04C11DB7
Init   : FFFFFFFF
RefIn  : True
RefOut : True
XorOut : FFFFFFFF
Check  : CBF43926

這里面知道的不解釋,

RefIn  指的就是如果這個值是FALSE,表示待測數據的每個字節都不用“顛倒”,即BIT7仍是作為最高位,BIT0作為最低位。 如果這個值是TRUE,表示待測數據的每個字節都要先“顛倒”,即BIT7作為最低位,BIT0作為最高位。

RefOut  是說如果這個值是FALSE,表示計算結束后,寄存器中的值直接進入XOROUT處理即可。 如果這個值是TRUE,表示計算結束后,寄存器中的值要先“顛倒”,再進入XOROUT處理。注意,這是將整個寄存器的值顛倒,由最高bit到最低bit進行顛倒。

XorOut  這個值與經RefOut后的寄存器的值相XOR,得到的值就是最終正式的CRC值!

Check  就是字符串"123456789"(就是16進制的0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39)經過這個CRC運算的結果作為一個標准,來幫助你檢查你的算法是否正確的指標。

常用的CRC model還有

Name   : "CRC-16" 
Width  : 16
Poly   : 8005
Init   : 0000
RefIn  : True
RefOut : True
XorOut : 0000
Check  : BB3D

Name   : "CRC-16/CCITT"
Width  : 16
Poly   : 1021
Init   : FFFF
RefIn  : False
RefOut : False
XorOut : 0000
Check  : ?

Name   : "XMODEM"
Width  : 16
Poly   : 8408
Init   : 0000
RefIn  : True
RefOut : True
XorOut : 0000
Check  : ?

Name   : "ARC"
Width  : 16
Poly   : 8005
Init   : 0000
RefIn  : True
RefOut : True
XorOut : 0000
Check  : ?

 

好,至此就介紹完了,C的全程代碼后面附上,verilog的代碼稍候帶來。

注:我編譯的機器是64位的,如果是32位的話有的變量類型需要改變

 

直接計算法

#include <stdio.h>
#define     CRC_POLY    0x00011021
#define     CRC_WIDTH   16
#define     CRC_INIT    0x00000000
#define     DATA_POLY   0x31323334L
#define     DATA_WIDTH  32

void print_regs(int, int, int);
int main()
{
    unsigned long   data = DATA_POLY;
    unsigned long   regs = CRC_INIT;
    int shift_bit;
    int i;

// append zeros to the data
    data <<= CRC_WIDTH;
// print for debug
    printf("data -->\t0x%lx\t", data);
    for(i=DATA_WIDTH+CRC_WIDTH-1; i>=0; i--){
        printf("%d ", (data>>i)&0x1);
    }
    printf("\n");
// processing
    for(shift_bit=DATA_WIDTH+CRC_WIDTH; shift_bit>0; shift_bit--){
    // shift of 1 cycle
        regs = (regs<<1) | ((data>>(shift_bit-1))&0x1);
    // xor of 1 cycle
        if(regs>>CRC_WIDTH)     regs = regs ^ CRC_POLY;
    // result
        print_regs(DATA_WIDTH+CRC_WIDTH-shift_bit+1, CRC_WIDTH, regs);
    }

    return 0;
}

void print_regs(int crc_step, int crc_width, int crc_regs)
{
    int i;
    printf("Step %d\t\t", crc_step);
    for(i=crc_width; i>0; i--){
        printf("%d ", (crc_regs>>(i-1)&0x1));
    }
    printf("\n");
}

 

驅動表法

#include <stdio.h>
#define     BYTE_L  8
//#define     CRC32   // default CRC16, unless define CRC32

#ifdef  CRC32
    #define     CRC_WIDTH   32
    #define     CRC_POLY    0x04C11DB7
    #define     CRC_INIT    0x00000000
#else
    #define     CRC_WIDTH   16
    #define     CRC_POLY    0x1021
    #define     CRC_INIT    0x0000
#endif

void print_regs(unsigned int, unsigned int);
int main()
{
    unsigned char   bit_cnt     ;
    unsigned short  index       ;
    unsigned int    table[256]  ;
    unsigned int    temp        ;

    unsigned char   buff[] = {0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39};
    unsigned int    len  = sizeof(buff);
    unsigned char   *pointer    ;
    unsigned int    regs        ;
    unsigned int    i           ;

// crc LUT generation
    for(index=0; index<256; index++){
        temp = 0;
        for(bit_cnt=BYTE_L; bit_cnt>0; bit_cnt--){
            if((index>>(bit_cnt-1) ^ temp>>(CRC_WIDTH-1))&0x1)  temp = (temp<<1) ^ CRC_POLY;
            else                                                temp <<= 1;
        }
#ifdef  CRC32
        table[index] = (unsigned int)temp;
#else
        table[index] = (unsigned short)temp;
#endif
    }
// crc calculation
    pointer = buff;
    regs = CRC_INIT;
    while(len--){
        print_regs(CRC_WIDTH, regs);
        regs = ((regs<<BYTE_L)|*pointer++) ^ table[(regs>>(CRC_WIDTH-BYTE_L))&0xFF];
    }
    for(i=0; i<CRC_WIDTH/BYTE_L; i++){
        print_regs(CRC_WIDTH, regs);
        regs = (regs<<BYTE_L) ^ table[(regs>>(CRC_WIDTH-BYTE_L))&0xFF];
    }
#ifdef  CRC32
    printf("result --> 0x%Xh\n", regs);
#else
    printf("result --> 0x%Xh\n", (unsigned short)regs);
#endif

    return 0;
}

void print_regs(unsigned int crc_width, unsigned int crc_regs)
{
    int i;
    for(i=crc_width; i>0; i--){
        printf("%d ", (crc_regs>>(i-1)&0x1));
    }
    printf("\n");
}

 

直驅表法

#include <stdio.h>
#define     BYTE_L  8
//#define     CRC32   // default CRC16, unless define CRC32

#ifdef  CRC32
    #define     CRC_WIDTH   32
    #define     CRC_POLY    0x04C11DB7
    #define     CRC_INIT    0xFFFFFFFF
#else
    #define     CRC_WIDTH   16
    #define     CRC_POLY    0x1021
    #define     CRC_INIT    0x0000
#endif

void print_regs(unsigned int, unsigned int);
int main()
{
    unsigned char   bit_cnt     ;
    unsigned short  index       ;
    unsigned int    table[256]  ;
    unsigned int    temp        ;

    unsigned char   buff[] = {0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39};
    unsigned int    len  = sizeof(buff);
    unsigned char   *pointer    ;
    unsigned int    regs        ;
    unsigned int    i           ;

// crc LUT generation
    for(index=0; index<256; index++){
        temp = 0;
        for(bit_cnt=BYTE_L; bit_cnt>0; bit_cnt--){
            if((index>>(bit_cnt-1) ^ temp>>(CRC_WIDTH-1))&0x1)  temp = (temp<<1) ^ CRC_POLY;
            else                                                temp <<= 1;
        }
#ifdef  CRC32
        table[index] = (unsigned int)temp;
#else
        table[index] = (unsigned short)temp;
#endif
    }
// crc calculation
    pointer = buff;
    regs = CRC_INIT;
    for(i=0; i<len; i++){
        print_regs(CRC_WIDTH, regs);
        regs = (regs<<BYTE_L) ^ table[(regs>>(CRC_WIDTH-BYTE_L))&0xFF ^ *pointer++];
    }
#ifdef  CRC32
    printf("result --> 0x%Xh\n", regs);
#else
    printf("result --> 0x%Xh\n", (unsigned short)regs);
#endif

    return 0;
}

void print_regs(unsigned int crc_width, unsigned int crc_regs)
{
    int i;
    for(i=crc_width; i>0; i--){
        printf("%d ", (crc_regs>>(i-1)&0x1));
    }
    printf("\n");
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM