匯編語言——更多功能


匯編語言——更多功能

轉移指令及其原理

可以修改IP,或同時修改cs和IP的指令統稱為轉移指令。概括地講,轉移指令就是可以控制CPU執行內存中某處代碼的指令。

8086CPU的轉移行為有以下幾類:

  • 只修改IP時,稱為段內轉移,比如:jmp ax

  • 同時修改cs和IP時,稱為段間轉移,比如:jmp 1000:0,由於轉移指令對IP的修改范圍不同,段內轉移又分為:短轉移和近轉移。

  • 短轉移IP的修改范圍為-128~127。

  • 近轉移IP的修改范圍為-32768~32767。

8086CPU的轉移指令分為以下幾類

  • 無條件轉移指令(如:jmp)
  • 條件轉移指令
  • 循環指令(如:loop)
  • 過程
  • 中斷

這些轉移指令轉移的前提條件可能不同,但轉移的基本原理是相同的。我們在這一章主要通過深入學習無條件轉移指令jmp來理解CPU執行轉移指令的基本原理。

操作符 offset和空操作指令nop

操作符offset在匯編語言中是由編譯器處理的符號 , 它的功能是取得標號的偏移地址。比如下面的程序:

assume cs:codesg
	codesg segment
		start: mov ax,offset start  ;等於mov ax,0
			s: mov ax,offset s      ;等於mov ax,3
	codesg ends
end start

空操作指令nop,只占用一個byte的空間,作為指令不會被執行。

jmp指令

jmp指令是無條件跳轉指令,可以只修改ip,也可以cs和ip都修改

jmp 指令要給出兩種信息:

  1. 轉移的目的地址
  2. 轉移的距離(段間轉移、段內短轉移、段內近轉移)

不同的給出目的地址的方法,和不同的轉移位置,對應有不同格式的jmp指令,下面分情況討論。


依據位移進行轉移的jmp指令

jmp short 標號(轉到標號處執行指令)

這種格式的jmp指令實現的是段內短轉移,它對IP的修改范圍為-128~127,也就是說,它向前轉移時可以最多越過128個字節,向后轉移可以最多越過127個字節。jmp指令中的"short"符號,說明指令進行的是短轉移。jmp指令中的“標號”是代碼段中的標號,指明了指令要轉移的目的地,轉移指令結束后,CS:IP應該指向標號處的指令。

assume cs:code
	code segment
		start:	mov ax,0
				jmp short s
				add ax,1
				
			  s:inc ax
				
				mov ax,4c00h
				int 21h
	code ends
end

上面的代碼執行完之后,ax中的值為1,因為jmp short指令跳過了add ax,1

在其他的用立即數操作的指令中,如mov ax,ds:[0123h],在debug中可以看到對應的機器碼為B82301,可以發現操作數就在機器碼中。
在debug中查看jmp short s發現對應代碼為jmp 0008,但機器碼確是EB03,可以發現沒有和0008直接有關。那么他是如何實現修改IP的呢?

;debug 機器碼
076C:0000	B80000  mov ax,0
076C:0003	EB03	jmp short s
076C:0005	050100  add ax,1
076C:0008	40		inc ax
076C:0009	B8004C  mov ax,4c00h
076C:000C	CD21    int 21h

可以很容易發現0005和0008之間隔了3,回想一下CPU如何處理指令,加載到緩沖區,IP+(指令長度),然后執行指令。不妨做個猜想,CPU讀取到jmp short s之后,IP=IP+3=5,IP指向了add ax,1,執行jmp short s后變為0008,實際上是通過對當前IP的加減操作,IP=IP+3。為了驗證,我們在原來代碼的基礎上,jmp short s后再加一條nop空指令來占據一個byte。再次debug可以發現jmp short s的機器碼為EB04,證明我們的猜想是對的。

實際上,jmp short 標號的功能為:(IP)=(IP)+8位位移。

  1. 8位位移=標號處的地址-jmp指令后的第一個字節的地址;
  2. short指明此處的位移為8位位移;
  3. 8位位移的范圍為-128~127,用補碼表示
  4. 8位位移由編譯程序在編譯時算出。

還有一種和jmp short 標號功能相近的指令格式,jmpnearptr標號,它實現的是段內近轉移。jmp near ptr 標號的功能為:(IP)=(IP)+16位位移。

  1. 16位位移=標號處的地址-jmp指令后的第一個字節的地址;
  2. nearptr指明此處的位移為16位位移,進行的是段內近轉移;
  3. 16位位移的范圍為-32768~32767,用補碼表示;
  4. 16位位移由編譯程序在編譯時算出。

轉移的目的地址在指令中的jmp 指令
前面說到的jmp指令對應的機器指令中並沒有轉移的目的地址,而是相對於其當前IP的轉移位移。jmp far ptr 標號實現的是段間轉移,又稱為遠轉移。

assume cs:code
	code segment
		start:	mov ax,0
				jmp far ptr s
				db 256 dup(0)
			  s:inc ax
				
				mov ax,4c00h
				int 21h
	code ends
end

上面這段代碼用debug查看機器碼可以看到jmp far ptr s的機器碼為EA08016C07,執行這條指令后IP為0108,CS為076C


轉移地址在寄存器中的jmp指令
可以在查看之前的講解


轉移地址在內存中的jmp指令

轉移地址在內存中的jmp指令有兩種格式:

  1. jmp word ptr 內存單元地址(段內轉移)

功能:從內存單元地址處開始存放着一個字,是轉移的目的偏移地址。內存單元地址可用尋址方式的任一格式給出。比如,下面的指令:

mov ax,0123H
mov ds:[0],ax
jmp word ptr ds:[0]

執行后,(IP)=0123H。

下面的指令也可以達到相同的效果:

mov ax,0123H
mov [bx],ax
jmp word ptr [bx]
  1. jmp dword ptr 內存單元地址(段間轉移)

功能:從內存單元地址處開始存放着兩個字,高地址處的字是轉移的目的段地址,低地址處是轉移的目的偏移地址。
(CS)=(內存單元地址+2)
(IP)=(內存單元地址)
內存單元地址可用尋址方式的任一格式給出。比如,下面的指令:

mov ax,0123H
mov ds:[0],ax
mov word ptr ds:[2],0
jmp dword ptr ds:[0]

執行后,(CS)=0,(IP)=0123H,CS:IP指向0000:0123

下面的指令也可以達到相同的效果:

mov ax,0123H
mov [bx],ax
mov word ptr [bx+2],0
jmp dword ptr [bx]

jcxz指令

jcxz指令為有條件轉移指令,所有的有條件轉移指令都是短轉移,在對應的機器碼中包含轉移的位移,而不是目的地址。對IP的修改范圍都為:-128~127。

指令格式:jcxz 標號(如果cx==0,轉移到標號處執行。操作:當(cx)=0時,(IP)=(IP)+8位位移;
8位位移=標號處的地址-jcxz指令后的第一個字節的地址;
8位位移的范圍為-128~127,用補碼表示;
8位位移由編譯程序在編譯時算出。
當cx!=0時,什么也不做(程序向下執行)。

loop指令

雖然在前面經常用,但還是說一下,loop指令也是短轉移指令,機器碼中的數字是位移,而不是目的地址。

編譯器對轉移位移超界的檢測

編譯器會對超界的位移報錯,程序無法通過編譯,例如下面這段程序在編譯時會報錯error A2053 jump out of range by 129 byte(s)。因為jmp short最多向后127個,但是卻有256的差距,所以越界了129byte。

assume cs:code
	code segment
		start:	mov ax,0
				jmp short ptr s
				db 256 dup(0)
			  s:inc ax
				
				mov ax,4c00h
				int 21h
	code ends
end

CALL和RET指令

call和ret指令都是轉移指令,這它們都修改IP或同時修改CS和IP。他們共同用來實現子程序的設計。

ret和retf

ret用棧中的數據,修改IP,實現近轉移
retf用棧中的數據,修改IP和CS,實現遠轉移

CPU執行ret指令時,進行下面兩步操作:

IP = ss*16+sp
sp=sp+2

CPU執行retf指令時,進行下面4步操作:

IP=((ss)*l6+(sp))
sp=(sp)+2
CS=((ss)*l6+(sp))
sp=(sp)+2

用不正確匯編指令表示出來就是這個

------RET-------
POP IP
------RETF-------
POP IP
POP CS

CALL指令

CPU執行CALL指令時,會把當前的IP或CS和IP壓入棧中,隨后轉移。

CALL指令除了不能短轉移(short 位移)之外和jmp指令很相似,不同的用法在下面分別講述


依據位移進行轉移的call指令

call 標號(將當前的IP壓棧后,轉到標號處執行指令)

CPU執行此種格式的call指令時,進行如下的操作:

sp=sp-2
ss*16+sp=IP
IP=IP+16位位移

16位位移=標號處的地址-call指令后的第一個字節的地址;
16位位移的范圍為-32768~32767,用補碼表示;
16位位移由編譯程序在編譯時算出。

從上面的描述中,可以看出,如果我們用匯編語法來解釋此種格式的call指令,則:
CPU執行"call標號”時,相當於進行:

push IP
jmp near ptr 標號

轉移的目的地址在指令中的call指令
上一個call指令的用法,對應的機器碼只有當前IP的偏移值,沒有指定的目的地址,CALL far ptr 標號實現的是段間轉移。

CPU執行此種格式的call指令時,進行如下的操作。

sp=sp-2
ss16+sp=CS
sp=sp-2
ss
16+sp=IP
CS=標號所在段的段地址
IP=標號在段中的偏移地址

從上面的描述中可以看出,如果我們用匯編語法來解釋此種格式的call指令,則:

CPU執行"callfarptr標號”時,相當於進行:

push CS
push IP
jmp far ptr 標號

轉移的目的地址在寄存器和內存中的call指令
大致用法與jmp指令並無不同。

CALL指令和RET指令配合使用

在開始的時候我們說到call和ret是用來實現子程序的,那么該如何使用呢?
call是用來保存當前即將執行到指令的偏移地址並且轉到標號處執行別的程序,ret可以用來恢復IP。通常我們用以下結構實現程序執行到一半去執行別的程序再回來接着執行剩余的指令

.
.
.
many instructions
.
.
.
call sub
	.
	.
	.
	many instructions
	.
	.
	.
sub:
	.
	.
	.
	many instructions
	.
	.
	.
	ret ;返回原程序繼續執行

參數和結果傳遞的問題

子程序一般都要根據提供的參數處理一定的事務,處理后,將結果(返回值)提供給調用者。其實,我們討論參數和返回值傳遞的問題,實際上就是在探討,應該如何存儲子程序需要的參數和產生的返回值。
比如,設計一個子程序,可以根據提供的N,來計算N的3次方。這里面就有兩個問題:
(1)將參數N存儲在什么地方?
(2)計算得到的數值,存儲在什么地方?
很顯然,可以用寄存器來存儲,可以將參數放到bx中;因為子程序中要計算
N X N X N,可以使用多個mul指令,為了方便,可將結果放到dx和ax中。子程序如下。
;說明:計算N的3次方
;參數:(bx)=N
;結果:(dx:ax)=N3

cube: mov ax,bx
	  mul bx
	  mul bx
	  ret

mul是乘法指令,用法與div指令相似,提供8位乘法和16位乘法,8位乘法是,一個乘數默認在AL,另一個可以在8位寄存器或者內存中,結果高位在AH,低位在AL,16位乘法是,AX中保存一個乘數,另一個可以在16位寄存器或者內存中,結果高位在DX,低位在AX。

批量數據的傳遞

在上面的例子中,只有一個參數,如果有很多的參數,寄存器跟不夠用,該怎么辦呢?
通常把數據存儲在內存中,然后將它們所在內存空間的首地址放在寄存器里,再把寄存器傳給子程序,返回是也是如此。

寄存器的沖突

在之前的實現雙重循環的時候,會出現cx寄存器的沖突,在構建子程序的時候,同樣也會遇到這樣的問題,會出現寄存器的沖突,解決方法是在子程序的開始,將使用到的寄存器保存到堆棧,在程序返回的時候再復原。

標志寄存器

CPU內部的寄存器中,有一種特殊的寄存器(對於不同的處理機,個數和結構都可能不同)具有以下3種作用。

  1. 用來存儲相關指令的某些執行結果;
  2. 用來為CPU執行相關指令提供行為依據;
  3. 用來控制CPU的相關工作方式。

這種特殊的寄存器在8086CPU中,被稱為標志寄存器。8086CPU的標志寄存器有16位,其中存儲的信息通常被稱為程序狀態字(PSW)。我們已經使用過8086CPU的ax、bx、ex、dx、si、di、bp、sp、IP、cs、ss、ds、es等13個寄存器了,本章中的標志寄存器(以下簡稱為flag)是我們要學習的最后一個寄存器。
flag和其他寄存器不一樣,其他寄存器是用來存放數據的,都是整個寄存器具有一個含義。而flag寄存器是按位起作用的,也就是說,它的每一位都有專門的含義,記錄特定的信息。
下面是8086flag的結構
|15|14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0|
|--|--|
| | | | |OF|DF|IF|TF|SF|ZF| |AF| |PF| |CF|

在這一章中,我們學習標志寄存器中的CF、PF、ZF、SF、OF、DF標志位,以及一些與其相關的典型指令。

ZF

flag的第6位是ZF,零標志位。它記錄相關指令執行后,其結果是否為0。如果結果為0,那么zf=l;如果結果不為0,那么zf=0。
如下面的程序,執行之后結果為0,zf為1:

mov ax,2
sub ax,2

注意,在8086CPU的指令集中,有的指令的執行是影響標志寄存器的,比如,add、sub、mul、div、inc、or、and等,它們大都是運算指令(進行邏輯或算術運算);有的指令的執行對標志寄存器沒有影響,比如,mov、push、pop等,它們大都是傳送指令。在使用一條指令的時候,要注意這條指令的全部功能,其中包括,執行結果對標志寄存器的哪些標志位造成影響。

PF

flag的第2位是PF,奇偶標志位。它記錄相關指令執行后,其結果的所有bit位中1
的個數是否為偶數。如果1的個數為偶數,pf=l,如果為奇數,那么pf=O。比如,指令:

mov al,1
add al,10

執行后,結果為00001011B,其中有3(奇數)個1,則pf=0;

mov al,1
or al,2

執行后,結果為00000011B,其中有2(偶數)個1,則pf=1;

SF

flag的第7位是SF,符號標志位。它記錄相關指令執行后,其結果是否為負。如果結果為負,sf=1;如果非負,sf=0。
計算機中通常用補碼來表示有符號數據。計算機中的一個數據可以看作是有符號數,也可以看成是無符號數。比如:
00000001B,可以看作為無符號數1,或有符號數+1;
10000001B,可以看作為無符號數129,也可以看作有符號數-127。

這也就是說,對於同一個二進制數據,計算機可以將它當作無符號數據來運算,也可以當作有符號數據來運算。比如:

mov al,10000001B
add al,1

結果:(al)=10000010B。

可以將add指令進行的運算當作無符號數的運算,那么add指令相當於計算129+1,結果為130(10000010B);也可以將add指令進行的運算當作有符號數的運算,那么
add指令相當於計算-127+1,結果為-126(10000010B)
不管我們如何看待,CPU在執行add等指令的時候,就已經包含了兩種含義,也將得到用同一種信息來記錄的兩種結果。關鍵在於我們的程序需要哪一種結果。
SF標志,就是CPU對有符號數運算結果的一種記錄,它記錄數據的正負。在我們將數據當作有符號數來運算的時候,可以通過它來得知結果的正負。如果我們將數據當作無符號數來運算,SF的值則沒有意義,雖然相關的指令影響了它的值。
這也就是說,CPU在執行add等指令時,是必然要影響到SF標志位的值的。至於我們需不需要這種影響,那就看我們如何看待指令所進行的運算了。

sf為什么值,代表了假如我們進行了有符號數的計算,結果是否為負數。

CF

flag的第0位是CF,進位標志位。一般情況下,在進行無符號數運算的時候,它記錄了運算結果的最高有效位向更高位的進位值,或從更高位的借位值。
對於位數為N的無符號數來說,其對應的二進制信息的最高位,即第N-1位,就是它的最高有效位,而假想存在的第N位,就是相對於最高有效位的更高位。

我們知道,當兩個數據相加的時候,有可能產生從最高有效位向更高位的進位。比如,兩個8位數據:98H+98H,將產生進位。由於這個進位值在8位數中無法保存,我們在前面的課程中,就只是簡單地說這個進位值丟失了。其實CPU在運算的時候,並不丟棄這個進位值,而是記錄在一個特殊的寄存器的某一位上。8086CPU就用flag的CF位來記錄這個進位值。比如,下面的指令:

mov al,98h
add al,al

執行后結果為30H,CF為1。

而當兩個數據做減法的時候,有可能向更高位借位。比如,兩個8位數據:97H-98H,將產生借位,借位后,相當於計算197H-98H。而falg的CF位也可以用來記錄這個借位值。

mov al,98h
sub al,98h

執行后結果為FFH,CF為1。

OF

我們先來談談溢出的間題。在進行有符號數運算的時候,如結果超過了機器所能表示的范圍稱為溢出。

那么,什么是機器所能表示的范圍呢?

比如說,指令運算的結果用8位寄存器或內存單元來存放,比如,addal,3,那么對於8位的有符號數據,機器所能表示的范圍就是—128127。同理,對於16位有符號數據,機器所能表示的范圍是-32768~32767,如果運算結果超出了機器所能表達的范圍,將產生溢出。

注意,這里所講的溢出,只是對有符號數運算而言。下面我們看兩個溢出的例子。

mov al,98
add al,99

執行后將產生溢出。因為add al,99進行的有符號數運算是:

(al)=(al)+99=98+99=197。

而結果197超出了機器所能表示的8位有符號數的范圍:-128~127。

add指令運算的結果是(al)=0C5H,因為進行的是有符號數運算,所以
有符號數,而0C5H是有符號數-59的補碼。指令進行的是有符號數運算,則98+99=-59這樣的結果讓人無法接受,造成這種情況的原因,就是實際的結果197,在8位寄存器al中存放不下。

由於在進行有符號數運算時,可能發生溢出而造成結果的錯誤。則CPU需要對指令執行后是否產生溢出進行記錄。
flag的第11位是OF,溢出標志位。一般情況下,OF記錄了有符號數運算的結果是否發生了溢出。如果發生溢出,OF=1,如果沒有,OF=0。
一定要注意CF和OF的區別:CF是對無符號數運算有意義的標志位,而OF是對有符號數運算有意義的標志位。比如:

mov al,98
add al,99

add指令執行后:CF=0,0F=1。前面我們講過,CPU在執行add等指令的時候,就包含了兩種含義:無符號數運算和有符號數運算。對於無符號數運算,CPU用CF位來記錄是否產生了進位;對於有符號數運算,CPU用0F位來記錄是否產生了溢出,當然,還要用SF位來記錄結果的符號。對於無符號數運算,98+99沒有進位,CF=0;對於有符號數運算,98+99發生溢出,0F=1。

mov al,0F0H
add al,88H

add指令執行后:CF=1,0F=1。對千無符號數運算,0F0H+88H有進位,CF=1;對於有符號數運算,0F0H+88H發生溢出,0F=1。

mov al,0F0H
add al,78H

add指令執行后:CF=1,0F=0。對於無符號運算,0F0H+78H有進位,CF=1;對於有符號數運算,0F0H+78H不發生溢出,0F=0。

我們可以看出,CF和0F所表示的進位和溢出,是分別對無符號數和有符號數運算而言的,它們之間沒有任何關系。

adc指令

adc指令是帶進位加法的指令,利用了CF中保存的進位信息。
例如:

mov ax,2
mov bx,1
sub bx,ax
adc ax,1

結果ax=4,因為ax=ax+bx+cf。

可以看出,adc指令比add指令多加了一個CF位的值。為什么要加上CF的值呢?

在進行大的數字計算的時候,可能無法直接相加,如32位數的相加,可以拆成2個16位相加,但是低位的相加,進位需要保存下來,在高位計算的時候用adc指令補進去。

sbb指令

sbb是帶借位減法指令,它利用了CF位上記錄的借位值。

指令格式:sbb操作對象1,操作對象2
功能:操作對象l=操作對象l—操作對象2—CF
比如指令sbb ax,bx實現的功能是:(ax)=(ax)-(bx)-CF

sbbadc是基於同樣的思想設計的兩條指令,在應用思路上和adc類似。在這里,我們就不再進行過多的討論。通過學習這兩條指令,我們可以進一步領會一下標志寄存器CF位的作用和意義。

cmp指令

cmp是比較指令,cmp的功能相當於減法指令,只是不保存結果。cmp指令執行后,將對標志寄存器產生影響。其他相關指令通過識別這些被影響的標志寄存器位來得知比較結果。
cmp指令格式:cmp 操作對象1,操作對象2
功能:計算操作對象1-操作對象2,但並不保存結果,僅僅根據計算結果對標志寄存器進行設置。
比如,指令cmp ax,ax,做(ax)-(ax)的運算,結果為0,但並不在ax中保存,僅影響
flag的相關各位。指令執行后:zf=1,pf=1,sf=0,cf=0,of=0。

對於無符號數比較,我們通過cmp指令執行后,相關標志位的值就可以看出比較的結果。

if ax =bx, then ZF=1
if ax!=bx, then ZF=0
if ax >bx, then ZF=0, CF=0
if ax <bx, then CF=1
if ax<=bx, then CF=1 or ZF=1
if ax>=bx, then CF=0

但是對有符號數這么比較是有漏洞的,對於有符號數,判斷是否相等可以直接用ZF標志位。對於有符號數計算,通過判斷SF標志符號,可以知道結果表示為有符號數的正負,但是這里同樣有個漏洞,例如:

mov al,22h
mov bl,0a0h
cmp al,bl

34-(-96)=130得到的結果是82H,-126的補碼是82H(10000010B),SF標志位的值為1,這樣的情況不能直接判斷是大於還是小於,思考一下,為什么出現上述的情況會無法只用SF判斷,原因是因為溢出了(130>127),因為溢出導致了無法正確判斷,在沒有發生溢出時(OF=0),可以直接判斷,所以:

if OF=1 and SF=1 ax>bx
if OF=0 and SF=1 ax<bx
if OF=1 and SF=0 ax<bx
if OF=0 and SF=0 ax>bx

檢測比較結果的條件轉移指令

在前面的時候,我們有說到關於條件的跳轉指令都是有關CX寄存器的,現在來說一下關於標志寄存器的,根據cmp指令修改標志位后,檢查指定標志位確定是否進行跳轉,兩者配合使用,類似於call和ret指令。

因為cmp分為無符號數字比較和有符號數字比較,所以,跳轉指令也分為對無符號數比較的跳轉指令和有符號數字比較的跳轉指令

;無符號跳轉
je  ;含義:相等跳轉
jne ;含義:不相等跳轉
jb  ;含義:小於跳轉
jnb ;含義:不小於跳轉
ja  ;含義:大於跳轉
jna ;含義:不大於跳轉

DF標志和串傳送指令

falg的第10位是DF,方向標志位。在串處理指令中,控制每次操作后si、中的增減。

df=0每次操作后si、di遞增;
df=1每次操作后si、di遞減。

我們來看下面的一個串傳送指令。

格式:movsb
功能:執行movsb指令相當於進行下面幾步操作。
(1)((es)X16+(di))=((ds)Xl6+(si))
(2)如果df=0則:(si)=(si)+1,(di)=(di)+1
(3)如果df=1則:(si)=(si)-1,(di)=(di)-1

該指令實現了內存中數據段中的數據復制到另一處位置,可以從一個指定位置開始,如果df為0則正向進行,否則反向進行。

當然也支持字傳輸:

格式:movsw
功能:執行movsw指令相當於進行下面幾步操作。
(1)((es)X16+(di))=((ds)Xl6+(si))
(2)如果df=0則:(si)=(si)+2,(di)=(di)+2
(3)如果df=1則:(si)=(si)-2,(di)=(di)-2

movsb和movsw進行的是串傳送操作中的一個步驟,一般來說,movsb和movsw都和rep配合使用,格式如下:

rep movsb

用匯編語法來描述rep movsb的功能就是:

s: movsb
   loop s

可見,rep的作用是根據cx的值,重復執行后面的串傳送指令。由於每執行一次
movsb指令si和小都會遞增或遞減指向后一個單元或前一個單,rep movsb就可以循環實現(cx)個字符的傳送。對於movsw也是同理。

對標志寄存器的保存和恢復

對於一半的寄存器可以直接 push 寄存器名實現把寄存器的值保存在堆棧中,對標志寄存器,使用pushf指令可以把標志寄存器壓入堆棧,popf從堆棧中彈出一個字給標志寄存器


免責聲明!

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



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