[爆卦]組合語言指令是什麼?優點缺點精華區懶人包

為什麼這篇組合語言指令鄉民發文收入到精華區:因為在組合語言指令這個討論話題中,有許多相關的文章在討論,這篇最有參考價值!作者purpose (purpose)看板ASM標題[心得] 個人的 x86 組合語言觀念筆記時間W...

組合語言指令 在 工具王 阿璋 Instagram 的最佳解答

2021-04-04 21:34:22

不務正業工程師的新單元:「#程式教學」來啦!  如果你想要入門學程式語言,但目前還沒有基礎,也沒有特定目標、 那這篇文章絕對要收、藏、起、來🤗! 如果你已經有特定目標,那可以期待之後的程式教學文章, 會帶你認識不同語言的優缺點👍👎。  程式語言百百種,C、C++、Python、Java...


※ [本文轉錄自 C_and_CPP 看板 #1Cis6A7c ]

作者: purpose (purpose) 看板: C_and_CPP
標題: Re: [問題] C/C++ 中的 asm 該如何學起?
時間: Tue Oct 12 03:12:07 2010


發篇筆記

一、[簡介] 機器語言與80x86
二、[觀念] 組合語言—Intel Style 與 AT&T Style、MASM 與 NASM
三、[教學] 簡單連結範例—NASM 組合語言 與 C/C++ (Windows 平台)
-------------------------------------------------------------

一、[簡介] 機器語言與80x86

大家家裡用的計算機器叫做個人電腦 (PC)。
可以拿來安裝 Windows、Linux,甚至 Mac OS X...等作業系統。

個人電腦的 CPU 演變歷史,可以說就是 Intel 的歷史。從最早的16位元CPU:
「8088/8086 -> 80286」,再演化到32位元的 80386、80486...
後來因為商標不能用數字註冊,Intel 不使用 80586 命名,從586開始,
改名為歷史上的 Pentium CPU。

AMD 也差不多是在 Pentium 時代開始慢慢成為 Intel 在個人電腦處理器上的
競爭者。

可以想見 Intel 就是個人電腦處理器的「唯一制定者」,Intel自己做新的 CPU
也要向後相容以前的東西,就像 Windows 7 也得要能執行 Windows XP 的程式一般。
你在 286 寫的程式,拿去給 486 的 CPU 也要能跑。

所以「個人電腦 CPU = x86 家族」...好啦,可能有人不認同這句話。

你跟美國人講話就要講英文、跟法國人講法文;
跟 x86 家族的處理器講話,就要講「x86 機器語言」;
跟 Intel 8051 單晶片處理器溝通,就講「8051 機器語言」;
在算盤本裡面介紹的處理器是「MIPS」家族,就用「MIPS 機器語言」跟其溝通。

所有處理器裡面,x86家族功能當然是最強,每一代都有增加新功能,又要向後相容,
所以其實該語言最複雜、不規則、不好學。但只用些基本的功能的話,還是過得去的。

二、[觀念] 組合語言—Intel Style 與 AT&T Style、MASM 與 NASM

機器語言因為電路關係,原始形式就是 010101 這種二進位形式,但你喜歡也可以
轉成十六進位寫出來給別人看。

下面這是一個 x86 機器語言指令 (instruction):

05 0A 00 00 00 (十六進位表示)

用人類說法就是你告訴某顆 x86 家族的 CPU:

「把你的 eax 暫存器內容取出,將其跟10相加,再把結果寫回 eax 暫存器」

用C語言表示法就是:

「eax += 10;」

機器語言形式顯然太麻煩了。



於是發明了「助憶符號」,比如用 add 代表「相加這個運算動作」;
減法動作,用符號 sub 標記;
將資料從A處複製過去B處的動作,就用 mov 助憶符號標記。

add, sub, mov...等是運算子,而 eax 暫存器跟 10 是運算參與單元 (運算元)。

如果綜合以上講的運算子跟運算元,想要寫出完整指令時,還會有一個問題!
若有 eax, ecx 兩個運算元,想要把 eax 的值取出,複製到 ecx 去

到底該寫 mov eax, ecx 還是 mov ecx, eax ?
哪邊來源?哪邊目的?

AT&T、Intel 各自有一套語法慣例。

詳細資料參考這裡:
http://www.ibm.com/developerworks/library/l-gas-nasm.html


C語言 Intel AT&T

指派運算子的 靠最左邊的運算元 靠最右邊的運算元
左邊是目的地 是目的地。 是運算結果放置處。

int eax = 4; mov eax, 4 movl $4, %eax

(暫存器名稱前,需加 % 符號;
而且4這個立即數值前,需加 $ 符號;
且用 movl 表示 move long 這麼長)

西瓜靠大邊,跟大家一起用 Intel 慣例的寫法就好。


像上面 mov eax, 4 這樣子的指令形式,都叫「組合語言」,說穿了只是把當初
的「x86 機器語言」寫成比較容易看懂的形式而已。


既然這樣,那 x86 機器語言就一套,助憶符號跟暫存器也固定那幾個。
為什麼最後卻搞出 MASM、TASM、NASM、FASM...這麼多種組合語言呢?

MASM,軟體界霸主微軟推行的組合語言 (雖然微軟最近變心去搞 MSIL 的樣子?)


NASM,在台灣是僅次於微軟的選擇方案,而且跨多個作業系統平台。文件完整、
有中文書籍在講它,而且狀態穩定。

※ FASM,較新,類似於NASM,聽說比較快?
※ TASM,老牌子,現在很少人用了,但是有 Turbo Debugger 很強大,可以
對 16 位元執行檔做偵錯,偶爾也值得一用。


上面提到的組合語言都是 Intel Style,而 AT&T 會看到的地方,就是使用「gcc -S」
功能時會出現。但是可以用 objdump 去看 Intel Style 的組語。
如果是 gdb 偵錯則直接就有選項 disassembly-flavor intel 可以切換到
Intel 風格組語。


分別用 VC 跟 GCC ,一樣寫個 C 語言動態函式庫,把某個函數輸出,
我用 VC 時,可以在函數前面加上 __declspec(dllexport) 告知 VC 將該函數輸出。
也可以寫個「模組定義檔」(*.def) 去記載哪些函數要輸出。

但是這兩個方法都是 VC 特有,不是 C 語言規定的。

同樣狀況在組合語言亦同,MASM 也有一些組譯器指令是其專有,而 NASM 沒有。
甚至在語法上,兩者也有差異。

詳細資料:http://www.nasm.us/doc/nasmdoc2.html#section-2.2


NASM 對於「LABEL 符號」是有分大小寫的,MASM 沒有分。

(這不包含暫存器名稱,沒必要非寫 eax 而不寫 EAX,
也不包含 NASM 的假指令,比如 SECTION 大小寫都可以。)



而且 MASM 有些遭詬病的語法規範,NASM 有對其改進之。

對於 MASM
「某符號」要拿來「當成記憶體位址」用時,需加上 offset 修飾。
對於 NASM
覺得微軟這樣太麻煩,直接寫就一定是當位址用,不用加 offset。

MASM 的毛病是
如果寫組譯器指令如下,去定義兩個符號: (MASM、NASM 都支援這兩個假指令)

foo equ 1
bar dw 2

equ 指令很類似 #define pi 3.141421356 (...偷用別人的梗)

dw 這個假指令,是告訴組譯器把現在這個地方,所對應的記憶體位址,
取別名叫 bar,並且寫入 double-word (4位元組) 到此處,
存放值為 0x 00 00 00 02。


此時,如果 MASM 有兩個指令如下:

mov eax, foo ; foo 因為是 equ 設定出來的值,所以 eax 會得到 1
mov eax, bar ; bar 因為是「組合語言變數」,故 eax 得到值是 2

上面兩個指令,語法格式看起來完全一致,
但如果沒去觀看 foo 跟 eax 的假指令定義,就不能判定機器碼該翻成哪個。


如果是用 NASM,因為他強制規定只要是「間接取值」者,一律需加上中括號 []。
這個間接取值,意思是第一次取到的值,不是我要的,第二次取到的值才是我要的。
可以想成「間接取值 = 二次取值」。


換言之
mov eax, foo
mov eax, bar
對於 NASM 來說,不需要去看假指令定義,
因為 bar 跟 foo 都沒有用中括號,所以兩個都做一次取值就好。
亦即 mov eax, bar 會得到 bar 對應的記憶體位址,而不是 bar 的存放內容 2。


可是 MASM 很沒規律,因為 foo 是 equ 所定義,所以 eax 得到 1 (沒間接取值);
因為 bar 是一個 dw 定義的「組合語言變數」,所以將會做「間接取值」,先取得
bar 所對應的記憶體位址後,再到該位址再取一次值。
抓4位元組得到 2 來傳給 eax。


三、[教學] 簡單連結範例—NASM 組合語言 與 C/C++ (Windows 平台)

存成檔案 xx.c
---------------------------------------
#include <stdio.h>
int plusTen(int val);

int plusEleven(int x) {
return x+11;
}

int main() {
printf("return = %d\n", plusEleven(1));
printf("return = %d\n", plusTen(0x00123456));
return 0;
}
---------------------------------------
用 VC 編譯器的話,執行指令 cl /c xx.c 可以獲得對應的目的檔「xx.obj」
從 Visual Studio 200X 命令提示字元,去下這個 cl 指令,以省略環境變數的設定



存成檔案 fun.asm
---------------------------------------
section .text

global _plusTen

_plusTen:
push ebp ;函數初始化工作
mov ebp, esp ;函數初始化工作

mov eax, 0
mov ax, word [ebp+8]
add eax, 10

pop ebp ;函數結尾工作
ret ;返回呼叫函數 (caller),eax 是存放返回值用
---------------------------------------

去下載 NASM
http://www.nasm.us/pub/nasm/releasebuilds/2.09.02/win32/nasm-2.09.02-win32.zip

解壓縮後,執行指令 nasm -f win32 D:\Desktop\fun.asm
就能獲得一樣是 COFF 格式的目的檔 fun.obj。

簡單來說就是這個目的檔跟 cl /c 得到的目的檔有一樣格式。
最後再去「Visual Studio 200X 命令提示字元」下指令 link xx.obj fun.obj
就能得到 xx.exe 完成 C/C++ 跟 NASM 函數的連結了。


※對原理有興趣可以參考《程式設計師的自我修養》一書。


section .text
是 NASM 假指令,表示從這行指令以下的內容,翻譯成機器碼後
都要放到 .text 區去。每個 *.obj、*.exe 內部都有 .text 區段。
global
是 NASM 假指令,在這裡表示要把 _function 標籤包括的機器碼
視為全域函數。

在 C/C++ 你預設寫個函數,像上面的 plusEleven() 就自然會是
這裡說的這種「全域函數」。

使用 dumpbinGUI 工具,跳去 xx.obj、fun.obj 查看符號表,就可
看到 external 字眼。表示 xx.obj 可以調用 fun.obj 裡面寫
external 的符號,反之則反。

mov ax, word [ebp+8]

這個 ebp+8 是代表 plusTen() 函數的「參數1」
查一下 stack frame 的觀念,再用偵錯軟體觀察「函數呼叫」
進入前後的堆疊、暫存器變化,應該就能理解。

要說明的是,[ebp+8] 是「二次取值」,當第一次取值得到
ebp+8 位址假設是 0x0012FF74,接著要在這個地方做二次取值,在
C/C++ 要取幾個位元組的值是看該指標的資料型態。

如果是在 MASM,則是在中括號前寫 word ptr [ebp+8] 代表 2位元組;
若寫 byte ptr [ebp+8] 則代表 1位元組。

而 NASM 跟 MASM 差不多,但必須拿掉 ptr 字眼,否則會組譯錯誤。


因C語言還沒公開前,就已經留下一堆目的檔、一堆函數,他們都用正常的命名,
一些好記的名字都被用過了,所以 C 語言在使用函數時,其實一律自動加 _ 來命名。

因此原本的 C 函數呼叫雖然是 plusTen,但在 fun.asm 裡輸出的全域函數要寫成
_plusTen 才能讓 xx.c 可以連結到。

------------------------------------

關於「目的檔」,參考這篇:

http://en.wikipedia.org/wiki/Object_file

都只講微軟平台

在 DOS 時代目的檔名稱也都是 *.obj;執行檔名稱也都是 *.exe,
但這裡的 *.obj 其實是 OMF 格式 (Relocatable Object Module Format)。

跟你在 Windows 用 VC 編譯出來的目的檔 *.obj 不同。

用 nasm -f obj fun.asm 應該就是產生 OMF 格式的目的檔?

用 nasm -f win32 fun.asm 則是產生 COFF 格式的目的檔。
更精確來說這個 COFF 格式是微軟修改過的變種 COFF 格式。
你也可以叫它 PE/COFF 格式,甚至叫他 PE 格式也行,要解讀 PE 格式,其
第一選擇當然也是微軟提供 dumpbin,而軟體 dumpbinGUI 是圖形介面的前端。

真要說的話 dumpbin 也是一個前端,其實是呼叫 VC 的 link.exe,給隱藏選項
link /DUMP /ALL xx.obj。

http://en.wikipedia.org/wiki/Portable_Executable
PE 格式不一定是 *.exe 執行檔,也可以是 *.dll 也可以是 *.obj...等。

因為 VC 編譯出的目的檔都是 PE 格式,而 VC 的 link.exe 不能處理古早的
目的檔 OMF 格式,所以上面需要叫 nasm 用 -f win32 選項去產生 PE 目的檔。

也許會有某個很強的連結器,可以把 OMF 跟 PE 目的檔連結成 PE 執行檔吧?
甚至把 gcc 編出來的目的檔跟 PE 目的檔 link 成 PE 執行檔?
看有沒有人知道囉

------------------------------------

MASM 組譯器指令清單 http://msdn.microsoft.com/en-us/library/8t163bt0.aspx
剛好看到,補充上來。

------------------------------------

(怕以後忘記,再寫篇記錄,上文不變動,新增內容於下)

16位元記憶體模型—Segment:Offset (分段記憶體模型)
32位元記憶體模型—Flat Memory Model 加 Paging

( http://en.wikipedia.org/wiki/X86_architecture )

個人電腦 x86 家族的 CPU,在 16 位元時代是 8088/8086/80286 這三位;
而 x86 家族第一個 32 位元始祖是劃時代的 80386。

8088跟8086的位址匯流排都有20條線,每條線都是一端連接記憶體,一端連接
處理器,在高低電位變化下 (0、1),總共可有 2^20 種控制變化。

換言之,依照每個記憶體位址對應一個位元組的慣例,可以定位 2^20 大小的
記憶體位址;
80286 則進化到 24 條位址線,定址能力達 2^24,即 16M 記憶體。
省麻煩,把它當成跟 8088/8086 一樣,只能定址到 1M 記憶體就好。

※ 在 x86 的術語中,記憶體位址可以分成三種:

邏輯位址、線性位址、真實位址(物理位址)

必須先「邏輯位址→線性位址」,然後接著才是「線性位址→真實位址」。

自從32位元 CPU 出現 (自 80386 後),記憶體 Model 變成 Flat Memory,
邏輯位址就已經等於線性位址了。

然後是因為有「分頁機制」武力介入,所以需要先透過分頁機制轉換,線性位址
才會變成真正的物理位址。
而分頁機制是從 80386 開始使用 (保護模式的完整版也是從 80386 開始)。

那為什麼 16 位元處理器,不使用 Flat Memory Model?為什麼當初的邏輯位址
要先經過轉換才會變成線性位址?

因為16位元CPU內部,參與運算的暫存器當時都還停留在16位元
(如:ax, bx, cx, dx),甚至最重要的指令暫存器 (IP) 也是 16 位元,
故只有定位到 0~65535 也就是 64K 記憶體的能力。

Intel 用額外提供的四個「分段暫存器」(CS、DS、ES、SS),搭配其他暫存器後
,使得每次記憶體定址方式其實是 Segment:Offset,此時這種位址表達法叫
邏輯位址。

CS 是 Code Segment、DS 是 Data Segment、SS 是 Stack Segment ...

邏輯位址→線性位址,公式是:「Segment Register * 0x10 + Offset」

假設我們有個程式,裡面的「全域變數」(不是放在堆疊的那種區域變數),
有個很大的整數陣列,總共有 128K。當程式執行時,這些資料區段假設放在
「線性位址=物理位址」的 0x0 ~ 0x1FFFF 這段連續的記憶體空間裡。

在邏輯位址(以寫程式的角度去觀看的位址),這 128K 資料會被分成兩段,
第一段是 DS=0 且 offset = 0x0000~0xFFFF,第二段是 DS=1 且 offset
= 0x0000~0xFFFF。

換言之,如果我要把某陣列元素移到 ax 暫存器,指令可能長這樣

mov ax, word ptr[0x1234]

如果執行這行時 DS=0,則會取到物理記憶體位址 0x01234 處 (第一段);
如果執行這行時 DS=1,則會取道物理記憶體位址 1*0x10 + 0x1234 = 0x11234。

若要讀取最後一個位元組到ax,只要執行以下指令即可:

mov ds, 1
mov al, byte ptr[0xFFFF]

因為不知道當時的 DS 值為多少,所以保險點,先設定 DS,然後因為 ax 是
16 位元,不必用到這麼大。所以用 al 存放即可。 al 就是 ax 暫存器
低 8 位元別名。
(ax 在 32 位元以上的 CPU 時,其實也是 eax 暫存器的低16位元處別名)


現今的執行檔,比如 PE 執行檔,往往內部都有分 .text (.code)、.data,
可能就是承襲當初的記憶體分段機制?


--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 124.8.140.240
JUSTLOVEAYU:大推~ 10/12 03:15
xam:gcc 吃 at&t 喔, intel 未必比較大邊.. 10/12 03:16
tropical72:大推大推..不過我想我需要一些時間吸收這篇文章吧.. 10/12 03:43
tropical72:看完後覺得想看的書似乎又更多了.. 10/12 03:43
king19880326:有啥好大不大邊的, 寫 assembly 第一件事就是看 10/12 03:44
king19880326:assembler 的 document. 10/12 03:44
tropical72:所以..要學組語的話,"程式設計師的自我修養"、算盤書 10/12 03:49
tropical72: 將會是我的第一步? 10/12 03:49
nand:讚啦!! 10/12 04:07
loveflames:原po如果發到ASM板的話,就能m起來了 10/12 04:16
xatier:大推!學了很多<(_ _)> 10/12 07:26
VictorTom:推一下:) 10/12 07:41
bill42362:推! 10/12 09:53
snoopy0907:推推推~~ 10/12 09:55
linjack:推薦這篇文章 10/12 10:04
purpose:學組語就買教組語的書來學。你可以多買幾本,交叉對照。 10/12 10:25
purpose:我對 AT&T 的偏見太深了...感謝指正 10/12 10:26
purpose:補充一下,我第一次修x86組語被當,第一本買的組語書完全 10/12 10:29
purpose:看不懂,不是每個人都是一次學懂,腦子累積的東西夠多,回 10/12 10:30
purpose:去看以前不會的組語,就開竅了,書多買多對照一樣有幫助。 10/12 10:30
stupid0319:我都用ollydbg學組語,很容易看數值怎麼跑 10/12 10:40
loveflames:但是ollydbg只能看ring 3 10/12 11:16
purpose:Win 三大偵錯軟體:IDA、OD、WinDbg (功能最強介面最囧) 10/12 11:34
stupid0319:有沒有可以在Nprotect下跑的debugger? 10/12 11:56
stupid0319:以一般的程式設計師需要看ring0嗎? 10/12 12:01
stupid0319:功能最強介面最囧是SoftIce吧 10/12 12:04
purpose:搞硬體、搞系統、搞XX的會需要ring0。Softice聽說不能在 10/12 12:17
purpose:新版 Windows 跑了,至於有沒有人修正就不知道囉 10/12 12:17
richliu:其實 MASM 很強, 當年寫 MASM 可以寫到像是 C 語言 10/12 12:55
loveflames:if跟while都有,else忘了有沒有 10/12 13:00
loveme00835:所以我不算錯啊 0.0 程式設計師的自我修養就是這樣寫 10/12 13:05
purpose:PE 其實大家都很隨便在講,廣義認定跟狹義認定的區別而已 10/12 13:16
purpose:有 if、while 喔...現在才知道 10/12 13:18
herman602:是有if跟 while, 可是好像是假指令吧(?) 10/12 17:54
herman602:組語上機考的時候老師說禁止使用xd 10/12 17:55
loveflames:我跟樓上相反,結果沒幾個人用jxx 10/12 18:04
tropical72:非常感謝purpose與各位的解釋,小弟我上網search tool, 10/12 19:26
tropical72:發現了這麼的東西..http://www.emu8086.com/ 10/12 19:27
tropical72:突然覺得很方便,不然p大所提供的tool info.我會再去學 10/12 19:28
tropical72:怎用的,謝謝各位的指教,感激不盡!! 10/12 19:28
tropical72:網址裡點進去還有Crystal FLOW for C(++),似乎很有趣.. 10/12 19:37
purpose:程式碼打好,或者貼上去,按下「emulate」就開始執行 10/12 19:42
purpose:第一行寫假指令 org 100h 是 *.COM 的格式,意思是當一個 10/12 19:43
purpose:*.COM 被執行,DOS會把他放置到 IP = 0x100 的地方 10/12 19:44
purpose:這個軟體只能模擬8086 CPU,我以前寫boot sector也用過 10/12 19:45
suhorng:推耶! 10/12 19:48
loveflames:*.com也沒分code段跟data段 10/12 19:50
purpose:沒有保護模式,沒有虛擬記憶體的...美好時代? 10/12 19:51
richliu:有 segment 的概念.. 還有 IO 其實 x86 指令很鳥.... 10/12 23:30
hpo14:我超弱,看得好頭痛 10/13 01:34
※ 編輯: purpose 來自: 124.8.128.171 (10/13 12:00)
tropical72:請問,89C51在設計時需要額外的ROM IC存放程式碼,到 10/13 12:08
tropical72:89S51時放置程式於同一IC中,至於x86所謂的code seg.與 10/13 12:11
tropical72:data seg.現行應都做於同一IC?? 另,8051系列似乎都可進 10/13 12:12
tropical72:行燒錄之動作,這些資料都算多,那x86市面上是否有提供燒 10/13 12:13
tropical72:錄器與可程式化IC可購? 10/13 12:13
purpose:code&data seg. 都是在主記憶體上。什麼燒錄器可程式化IC 10/13 12:29
purpose:我這輩子都沒碰過,不知道那什麼,你可以去電機版或組語版 10/13 12:29
tropical72:我在ASM發問,#1CjJU9xN,得到 WolfLord 以下解釋 10/13 13:59
tropical72:186-xxxx家族是SOC 然後也有一些廠商用X86CORE作SOC 10/13 13:59
tropical72:不過X86本身LICENSE就貴,作SOC其實很不合算,因此並 10/13 13:59
tropical72:不常見,而X86的SOC要ISP都要頗貴的特殊工具而且僅能 10/13 14:00
tropical72:洽原廠 #end#, 最後謝謝purpose解決我許多疑惑. 10/13 14:00
purpose:去看了看不懂。現在才發現loveflames大是ASM板板主,失敬 10/13 16:57

--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 124.8.128.171
purpose:錯誤的地方,還請各位專業的前輩不吝批評、指正,感謝 10/13 17:02
afai:感謝! 推~ 10/13 23:26
sorkayi:不常用x86 組語 還是推一個 10/17 09:31
Ross0916:mov ds, 1 是不行的 頂多是 pop ds 不過這是硬挑的XD 10/18 07:15
loveflames:要mov ds,ax才行 10/18 07:23
purpose:mov ds,1 沒驗證過就打了出來,謝謝樓上大大指正 10/18 11:50
purpose:回想起來,我之前也這樣寫過,被組譯器擋了下來改過就忘了 10/18 11:53
eva19452002:所有處理器裡面,x86家族功能當然是最強? 10/19 11:14
eva19452002:就算功能最強,也不表示執行效能最好 10/19 11:14
eva19452002:你是不是把複雜跟功能強畫上等號啊? 複雜不等於功能強 10/19 11:17
purpose:>10/19 11:14 小弟見識淺薄,沒見過什麼世面,僅能評論自 10/19 12:12
purpose:己見識過的處理器,讓前輩們看笑話了,感謝指正 10/19 12:13
purpose:>10/19 11:17 我覺得x86處理器跟Windows API狀況很像,因 10/19 12:14
purpose:為有舊有包袱,為了相容而把一些事情搞得很複雜,但是他們 10/19 12:14
purpose:每一代還是會推出些更好的新功能,比如新一代處理器的SIMD 10/19 12:15
purpose:指令集(如:SSE),一次可以處理多個資料,讓效率提昇很多 10/19 12:15
purpose:因為這些新功能的不斷提供,讓我覺得他們的功能很強 10/19 12:16
asususer: \⊙▽⊙/~by PTTNOW~ 04/14 01:51

你可能也想看看

搜尋相關網站