Linux中gcc的編譯、靜態庫和動態庫的製作

歡迎大家關注筆者,你的關注是我持續更博的最大動力


gcc是文本編譯器,就是編譯代碼的工具,下面介紹gcc編譯C語言(.c文件)的流程。

1 gcc的編譯過程

1.1 gcc的編譯過程

在這裏插入圖片描述

gcc的編譯分爲以下四個階段:

  • gcc預處理器:把.c文件編譯成預處理.i文件
  • gcc編譯器:把預處理.i文件編譯成.s的彙編文件
  • gcc彙編器:把.s彙編文件編譯成.o二進制文件
  • gcc鏈接器:把.o二進制文件鏈接成一個可執行文件

四個階段的編譯命令:

  • 預處理:gcc -E hello.c -o hello.i
  • 編譯:gcc -S hello.i -o hello.s
  • 彙編:gcc -c hello.s -o hello.o
  • 鏈接:gcc hello.o -o hello

上面的四個過程也可以用一個命令執行,直接生成可執行的文件:

gcc hello.c -o hello
# 或
gcc hello.c    # 沒有指定輸出問價名,默認是生成一個a.out可執行文件。

注意:

1、記憶參數可以用ESc-o參數是指定輸出文件的名字
2、在windows下,如果gcc hello.c,默認生成的可執行文件爲a.exe;如果gcc hello.c -o myapp,會直接生成可執行文件myapp.exe,自動添加後綴。
3、在第二階段把預處理.i文件編譯成.s彙編文件浪費時間
4、即使是直接生成可執行文件,但是也是經過了預處理編譯彙編鏈接這些過程,只是沒有生成中間的這些文件。


四個階段的具體功能:

  • 預處理:1)把.c文件中的頭文件展開添加到.i預處理文件的開頭;2)然後把.c文件代碼添加到.i的頭文件內容之後;3)把宏定義的量值替換爲具體的值,去掉原代碼中的註釋

  • 編譯:把c文件翻譯彙編文件,就是兩種程序語法的轉化。

  • 彙編:把彙編文件編程二進制文件,此時的文件已經看不出具體的內容。

  • 鏈接:將函數庫中相應的代碼組合到目標文件中。

1.2 gcc的常用參數

下面具體實例:

一、執行文件和頭文件同級目錄

1、創建一個sum.c文件,內容如下:

#include <stdio.h>
// 雙引號導入的頭文件是自己寫的
#include "head.h"
#define DEBUG

// main是入口函數
int main(void)
{
    int a = NUM1;
    int aa;
    int b = NUM2;
    int sum = a + b;
    // 這是一個加法運算
#ifdef DEBUG
    printf("The sum value is : %d + %d = %d\n", a, b, sum);
#endif
    return 0;   
}

2、在sum.c的同級創建head.h頭文件,內容如下:

#ifndef __HEAD_H_
#define __HEAD_H_

#define NUM1 10
#define NUM2 20
#endif

兩個文件的層級結構,同級目錄:

├── head.h
├── sum.c

3、預處理:gcc -E sum.c -o sum.i

執行完之後用vi sum.i查看預處理之後sum.i內容,如下:

在這裏插入圖片描述

從文件中可以看到,文件內容很長,之前的導入的頭文件,被替換爲具體的頭文件代碼內容,代碼中的宏定義量被替換爲具體的值,代碼中的註釋去掉。(相當於做菜食材的準備階段)

4、編譯:gcc -S sum.i -o sum.s

編譯就是把預處理的.i文件編譯成.s的彙編語言,編譯之後的sum.s內容,如下:

在這裏插入圖片描述

從文件中可以看出,這個文件顯示的已經不是C語言編寫的代碼,已經被轉換爲彙編語言的代碼,如果你對單片機瞭解,你可能也對彙編語言的語法有所瞭解。(編譯:就是把C語言翻譯成彙編語言

5、彙編:gcc -c sum.s -o sum.o

彙編就是把彙編文件變成二進制文件,彙編之後的sum.o內容,如下:

在這裏插入圖片描述

從文件中可以看出,彙編成二進制文件之後,裏面的內容已經看不出來了。

6、鏈接:gcc sum.o -o sum

使用gcc鏈接器二進制文件鏈接成一個可執行文件,將函數庫中相應的代碼組合到目標文件中。通過./sum即可執行該可執行文件,執行結果如下:

在這裏插入圖片描述

如果你打開可執行文件sum,顯示的內容和sum.o差不多。

二、執行文件和頭文件同級目錄

目錄層級結構:

├── include
│   └── head.h
├── sum.c

如果直接編譯(gcc sum.c -o sum),會提示找不到頭文件,如下:
在這裏插入圖片描述

找不到頭文件有兩種解決方法:

  • 直接在程序編寫的時候指定頭文件的位置
  • 在編譯的時候用-I參數,指定頭文件所在的文件夾位置

gcc sum.c -I ./include -o sum

三、gcc的其他參數使用

1、參數-D:指定一個宏定義

上面的程序中有printf()打印程序調試的log信息,但是程序發佈的時候,我們是不需要這些log信息的,當然我們可以通過加調試的#define DEBUG宏的聲明,但是,程序中需要調試輸出的log信息比較多的時候,這種方法顯然不合適。

現在我們把DEBUG的宏定義註釋掉

#include <stdio.h>
// 雙引號導入的頭文件是自己寫的
#include "head.h"
//#define DEBUG

// main是入口函數
int main(void)
{
    int a = NUM1;
    int aa;
    int b = NUM2;
    int sum = a + b;
    // 這是一個加法運算

// 程序有 DEBUG宏定義,程序纔會執行prinf()
#ifdef DEBUG
    printf("The sum value is : %d + %d = %d\n", a, b, sum);
#endif
    return 0;   
}

然後再執行:

>>>gcc sum.c -o sum
>>>./sum

結果:

並不會輸出print打印的信息了,如果再次打印出信息呢,此時可以通過參數-D,在執行命令的時候給程序指定一個宏,如下:

>>>gcc sum.c -o sum -D DEBUG
>>>./sum

此時就可以打印出printf()信息了。

總結:

-D參數的作用:不在程序中定義宏,在程序編譯的時候定義。不指定,在程序預處理的時候,printf()就會被刪掉了。

2、-O參數:程序預處理的時候對代碼優化

在程序預處理的時候對代碼進行優化,把冗餘的代碼去掉,有三個優化等級:

  • -O1:優化等級低
  • -O2:優化等級中
  • -O3:優化等級高

舉個例子:

int a = 10
int b = a
int c = b
int d = c

# 優化完之後就是
int d = 10  // 就是對d的一個賦值操作

3、-Wall參數:輸出程序中的警告信息

例如我們在程序中定義一個變量int aa;,但是沒有使用,此時就會輸出警告信息。

4、-g參數:在程序中添加一些調試信息

gcc sum.c -o sum -g

  • -g參數之後,輸出的可執行文件會比不加的大(因爲包含調試信息)
  • 程序發佈是不需要加-g參數
  • 調試需要加-g參數,否則沒有調試信息不可以調試。(gdb調試的時候必須加此參數

總結:
在這裏插入圖片描述
參數:-E-S,不是很重要,-c比較重要,後面我們在製作靜態庫和動態庫的時候需要用到生成的.o二進制值文件

2 gcc 靜態庫的製作

比如你和別人做項目合作,你不可能直接把源代碼給被人,那樣被人就可以自己開發,因爲源代碼就是你的核心技術。你不應該賣給他源代碼,而是應該是程序,這樣你就可以根據他有什麼需求進行改或添加什麼功能模塊等,就可以改一次就可以收費一次,這樣就可以有一個長期合作。

那應該給到客戶的是什麼呢?

  • 生成的庫
  • 頭文件

這樣把生成的庫頭文件給客戶也能夠使用,只是他不知道里面具體怎麼實現的。這樣二者才能維持一個長期的合作

頭文件對應的.c文件都被打包到了靜態庫動態庫裏面了。

2.1 靜態庫的製作流程

一、靜態庫的製作

1、命名規則

  • 1)lib + 庫的名字 + .a
  • 2)例如:libmytest.a

2、製作步驟:

  • 1)生成對應的.o二進制文件 .c --> .o eg:gcc sum.c -c sum.o
  • 2)將生成的.o文件打包,使用ar rcs + 靜態庫的名字(libMytest.a) + 生成的所有的.o
  • 3)發佈和使用靜態庫:
    • 發佈靜態庫
    • 頭文件

說明:

  • .c文件,也就是源代碼轉化成.o二進制文件之後,客戶就不知道到你的核心技術具體是怎麼實現的了。
  • ar是對.o的二進制文件進行打包rcs是打包參數,把所有.o二進制文件打包成一個.a文件,即:靜態庫。因此:靜態庫是一個打包了二進制文件的集合
  • 接口API是在頭文件中體現出來的。

實例:

目錄結構:

Calc
├── include
│   └── head.h
├── lib
├── main.c
└── src
    ├── add.c
    ├── div.c
    ├── mul.c
    └── sub.c

說明:

  • include文件夾:存放頭文件,提供給用戶調用的接口API
  • lib文件夾:存放庫文件,即:生成的靜態庫、動態庫
  • src文件夾:存放源文件
  • main.c程序:是用戶調用head.h頭文件裏面的接口,然後在調用靜態庫裏面我們實現的算法(只不過已經不是源碼,而是被編譯成二進制文件)

下面開始吧:

源代碼 src/add.c實現的是加法運算:

#include "head.h"

int add(int a, int b)
{
    int result = a + b;
    return result;
}

頭文件 include/head.h實現是對源代碼調用的接口API

#ifndef __HEAD_H_
#define __HEAD_H_
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int div(int a, int b);
#endif

main.c是對頭文件調用,然後調用靜態文件,對算法的使用,但是並不知道算法的具體實現源代碼

#include <stdio.h>
#include "head.h"

int main(void)
{
    int sum = add(2, 24);
    printf("sum = %d\n", sum);
    return 0;
}

用戶在main.c中引入頭文件#include "head.h",即在./include/head.h,就可以使用./include/head.h中定義的接口int add(int a, int b);,當main.c程序執行到add(int a, int b);接口時,就會到./src文件夾下找靜態文件(打包的二進制文件——即:加法算法的具體實現)


下面是具體的製作流程:
在這裏插入圖片描述

shliang@shliang-vm:~/shliang/gcc_learn/Calc$ tree
.
├── include
│   └── head.h
├── lib
├── main.c
└── src
    ├── add.c
    ├── div.c
    ├── mul.c
    └── sub.c

3 directories, 6 files
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ cd src
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  div.c  mul.c  sub.c

1、源代碼生成二進制文件(.o文件)
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ gcc *.c -c -I ../include
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  add.o  div.c  div.o  mul.c  mul.o  sub.c  sub.o

2、對生成的二進制文件(.o文件),打包成靜態文件(.a文件),並移動到lib目錄下
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ar rcs libMyCalc.a *.o
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  add.o  div.c  div.o  libMyCalc.a  mul.c  mul.o  sub.c  sub.o
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ mv libMyCalc.a ../lib
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ cd ..
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ls

3、調用include目錄下的頭文件(即:封裝的API接口)
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ gcc main.c lib/libMyCalc.a -I ./include -o sum
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ls
include  lib  main.c  src  sum
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ./sum
sum = 26
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ 

主要:

  • 製作好的靜態文件要放到lib目錄下
  • 調用頭文件中的接口API,然後用gcc編譯的自己調用的main.c文件,需要加上靜態文件(.a文件)
  • 程序發佈的時候只需要給用戶的文件:
    • 1)include目錄下的頭文件(head.h):封裝的是具體算法實現的接口API
    • 2)lib目錄下的靜態文件(.a文件):是源代碼編譯的之後的二進制文件(.o文件),然後被打包成靜態文件(.a文件)

用於另外一種調用靜態庫的方法爲:

gcc main.c -Iinclude -L lib -l MyCalc -o myapp

在這裏插入圖片描述

參數說明:

  • -I參數:指定頭文所在的文件夾名,文件夾名可以和參數貼着寫在一起
  • -L參數:指定靜態庫的文件夾名
  • -l參數:指定靜態庫的名字,但名字要掐頭去尾,eg:原靜態庫名字爲libMyCalc.a,在指定-l參數值的時候爲:-l MyCalc
  • -o參數:輸出編譯之後可執行文件的名字

注意:

之所以用-l指定靜態庫的名字,是因爲lib目錄下可能有多個靜態庫文件,但是我們只需要使用其中的某一個,此時可以用這種方法指定相應的靜態庫文件。

二、靜態庫相關文件查看

1、nm命令查看靜態庫

可以使用nm命令查看靜態庫文件中具體打包了哪些二進制文件.o文件

在這裏插入圖片描述

2、nm命令查看生成的可執行文件

在這裏插入圖片描述

T:代表的含義是把add代碼會被放到代碼區

2.2 靜態庫的優缺點

1、通過靜態庫生成可執行文件

在這裏插入圖片描述

  • 靜態庫中封裝了多個.o文件
  • main.c 中調用靜態庫中相應可執行文件(二進制文件)中的函數
  • 圖中只調用了add.o和sub.o中的函數,因此main.c在生成可執行文件的時候只會把靜態文件中的add.osub.o兩個文件打包到可執行文件中,靜態文件中的其他沒有用到的.o文件不會被打包進可執行文件中。
  • 在生成可執行文件的時候也是以.o可執行文件單位打包的,並不會把整個靜態文件.a都打包到可執行文件中。

靜態庫的優點:

  • 1)發佈程序的時候,不需要提供對應的庫了,因爲庫已經被打包到了可執行文件中去了。
  • 2)庫的加載速度比較快,因爲庫已經被打包到可執行文件中去了。

靜態庫的缺點:

  • 1) 庫被打包到應用程序(最後生成的可執行文件)中,如果庫很多的話就會導致應用程序的體積很大。
  • 2)庫發生了改變,需要重新編譯程序,如果源代碼比較多,可能編譯一遍一天就過去了。

3 gcc 動態庫 / 共享庫 的製作

動態庫也叫共享庫,在windows中對用.dll文件

3.1 動態庫 / 共享庫的製作流程

一、動態庫相關說明

1、命名規則:

  • 1)lib + 名字 + .so
  • 2)例如:libMyCalc.so

2、製作步驟:

  • 1)生成與位置無關的代碼 (生成與位置無關的.o)
  • 2)將.o打包成共享庫(動態庫)
  • 3)發佈和使用共享庫:

注意:

  • 靜態庫生成的.o文件是和位置有關的
  • gcc生成和位置無關的.o文件,需要使用參數-fPIC(常用) 或 -fpic

二、動態庫製作相關實例

在瞭解什麼叫生成和位置無關的.o文件,我們來先了解一下虛擬地址空間
在這裏插入圖片描述

linux上打開一個運行的程序進程),操作系統就會爲其分配一個(針對32位操作系統)0-4G的地址空間虛擬地址空間),虛擬地址空間不是在內存中,而是來自硬盤的存儲空間。

從下到上:

0-3G:是用戶區

  • .text 代碼段:存放的是代碼
  • .data :存放的是已初始化的變量
  • .bss:存放的是未初始化的變量
  • 堆空間:
  • 共享庫:動態庫的空間,每次程序運行的時候把動態庫加載到這個空間
  • 棧空間:我們定義的局部變量都是在棧空間分配的內存
  • 命令行參數
  • 環境變量

在往上 3-4G是內核區

  • 靜態庫生成與位置有關二進制文件(.o文件)

虛擬地址空間是從0開始的,生成的二進制文件(.o文件)會被放到代碼段,即.text代碼區。生成的.o代碼每次都被放到同一個位置,是因爲使用的是絕對地址

  • 動態庫生成與位置無關二進制文件(.o文件)

動態庫 / 共享庫 在程序打包的時候並不會把.o文件打包到可執行文件中,只是做了一個記錄,當程序運行之後纔去把動態庫加載到程序中,也就是加載到上圖中的共享庫空間,但是每次加載到共享庫空間的位置可能不同
還是和上面靜態庫製作同樣的目錄結構:


動態庫製作實例

Calc
├── include
│   └── head.h
├── lib
├── main.c
└── src
    ├── add.c
    ├── div.c
    ├── mul.c
    └── sub.c

在這裏插入圖片描述

shliang@shliang-vm:~/shliang/gcc_learn/Calc$ cd src/
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  div.c  mul.c  sub.c

1、把源碼生成和位置無關的二進制文件
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ gcc -fPIC -c *c -I ../include
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  add.o  div.c  div.o  mul.c  mul.o  sub.c  sub.o

2、使用gcc把生成的二進制文件(.o文件),打包成動態庫(.so文件)
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ gcc -shared -o libMyCalc.so *o -Iinclude
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ ls
add.c  add.o  div.c  div.o  libMyCalc.so  mul.c  mul.o  sub.c  sub.o

3、把生成的動態庫文件移動到lib目錄下
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$  mv libMyCalc.so ../lib
shliang@shliang-vm:~/shliang/gcc_learn/Calc/src$ cd ..
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ls
include  lib  main.c  src
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ 

參數說明:

  • -PIC:生成和位置無關的.o文件
  • -shared:共享,就是把.o文件,打包成動態庫 / 共享庫

上面就已經完成動態庫的製作,然後把下面的兩個文件發佈用戶即可調用

  • include/head.h: 頭文件,定義接口API
  • lib/libMyCalc.so:動態庫,封裝了編譯之後的源代碼二進制文件

用戶使用動態庫

用戶使用動態庫和靜態庫一樣有兩種方法:

  • 用戶使用動態庫方法一:

gcc main.c lib/libMyCalc.so -o app -Iinclude

在這裏插入圖片描述

  • 用戶使用動態庫方法二:

gcc main.c -Iinclude -L lib -l MyCalc -o myapp

在這裏插入圖片描述

3.2 動態庫查找不到解決方法

我們可以看到,第二種方法,至執行可執行程序的時候,提示找不到動態庫,這並不一定是動態庫文件不存在,可能是由於鏈接不到

是不是真的鏈接不到,我們可以通過一個命令ldd:查看可執行文件執行的時候,依賴的所有共享庫/動態庫(.so文件)

ldd命令使用:

ldd 可執行文件名

在這裏插入圖片描述

shliang@shliang-vm:~/shliang/gcc_learn/Calc$ ldd myapp 
	linux-vdso.so.1 =>  (0x00007fff59d26000)  # 後面的數字是庫的地址
	# 提示我們自己的動態庫 / 共享庫 libMyCalc.so沒有找到
	libMyCalc.so => not found
	# libc.so.6 是linux下的標準C庫 (寫C程序都會調用標準C庫裏的一些函數)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1e27462000)
	# 動態鏈接器,動態鏈接器的本質就是一個動態庫
	/lib64/ld-linux-x86-64.so.2 (0x00007f1e2782c000)
shliang@shliang-vm:~/shliang/gcc_learn/Calc$ 

在這裏插入圖片描述

如上圖:可執行程序./a.out在執行的時候,調用需要調用動態庫libmytest.so,但是實際上這個調用是通過動態鏈接器的來調用的。動態庫就是通過動態鏈接器--/lib64/ld-linux-x86-64.so.2加載到我們的可執行程序(應用程序)中的。

那麼動態鏈接器是-- /lib64/ld-linux-x86-64.so.2是通過什麼規則查找可執行文件在執行時,需要的動態文件的呢?

其實就是通過環境變量

在linux下查看環境變量:

echo $PATH

在這裏插入圖片描述

當然PATH下並不是存放動態庫的路徑,這裏只是做一個演示,如何查看環境變量。

一、動態庫查找不到解決方法一(不推薦——不允許使用

把自己製作的動態庫放到根目錄下的系統動態庫中,即/lib目錄下

sudo cp ./lib/libMyCalc.so /lib

在這裏插入圖片描述
從上面的結果可以看到,把自己製作的動態庫拷貝到系統動態庫中之後,動態鏈接器根據環境變量就可以找到這個動態庫,然後正確加載到可執行程序中。

注意:

這種方法一般不會使用的,因爲如果你的動態庫的名字和系統中某個動態庫的名字一樣,就可能會導致系統奔潰的!!!這裏只是做一個演示,證明動態鏈接器是根據環境變量去查找要加載的動態庫。

二、動態庫查找不到解決方法二(臨時測試設置

通過把動態庫添加到動態庫環境變量中,即:LD_LIBRARY_PATH

在這裏插入圖片描述

使用export添加環境變量,把當前動態庫所在的位置(文件夾位置)添加到LD_LIBRARY_PATH變量中,可執行程序在執行的時候會在默認的動態庫之前從LD_LIBARAY_PATH變量中查找有沒有所需動態庫。

# export
export LD_LIBARAY_PATH=./lib

注意:

但是,這種方法只是臨時的,當我們關閉終端,下次再執行程序又會提示找不到動態庫。因此,這鐘方法一般是再開發動態庫的過程中,用於臨時的測試

三、動態庫查找不到解決方法三(永久設置——不常用

當前用戶家目錄home)下的.bashrc文件中配置LD_LIBRARY_PAHT環境變量。

cd ~
vi .bashrc

# 然後再最後一行添加一個環境變量,如果沒有就創建(Shift+G跳到最後一行)
# 然後把動態庫的絕對路徑賦值給該變量
export LD_LIBARAY_PATH=/home/shliang/shliang/gcc_learn/Calc/lib

# 保存退出,用source激活配置,如果不激活需要重啓終端,因爲終端每次重啓都會從.bashrc中加載一次配置
source .bashrc

在這裏插入圖片描述

上面添加完環境變量之後就可以找到動態庫了。

在這裏插入圖片描述

四、動態庫查找不到解決方法四(永久設置

這種方法,相對與前三種複雜一些,一定要掌握,可能以後用作中用到的就是這種。做法如下:

1、需要找到動態連接器配置文件/etc/ld.so.conf
2、把我們自己製作的動態庫目錄的絕對路徑寫到配置文件中
3、更新配置文件:sudo ldconfig -v

  • ld:dynamic library 動態庫的縮寫
  • -v :是更細配置文件的時候輸出更新信息。

修改配置文件的路徑位置:/etc/ld.so.conf

/home/shliang/shliang/gcc_learn/Calc/lib添加到/etc/ld.so.conf配置文件中

之後就可以找到動態庫了,如下:

在這裏插入圖片描述

3.3 動態庫的優缺點

1、動態庫的優點

  • 執行程序的體積小:程序在執行的時候採取加載動態庫,並沒有和可執行程序打包在一起
  • 動態庫更新了,不需要重新編譯程序(不是絕對的,前提是函數的接口不變,內容便裏沒事)

2、動態庫的缺點

  • 程序發佈的時候,需要把動態庫提供給用戶
  • 動態庫沒有被打包到應用程序中,加載速度相對較慢

在這裏插入圖片描述


在這裏插入圖片描述


在這裏插入圖片描述
♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠ ⊕ ♠

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章