关闭

汇编中的字符串操作指令

1395人阅读 评论(0) 收藏 举报
分类:
一、字节操作指令:lodsb和stosb
1. lodsd需要寄存器esi配合使用。每执行一次lodsb,就将[esi]中的一个字节复制到al寄存器中。
    即:lodsd == [esi] --> al
 
2. stosb需要寄存器edi配合使用。每执行一次stosb,就将al中的内容复制到[edi]中。
    即:stosb == al --> [edi]
 
3. ecx和cld指令。以上两条指令,一次只能复制一个字节。要是有多个字节需要复制该怎么办呢?那就需要用到一个计数器,所以常用ecx保存需要复制的字节个数。
为了能够自动复制,esi和edi需要能够自动的递增或者递减。这就用到了DF标志位(Directory Flag)。DF=0,esi和edi自动增加;DF=1,esi和edi自动减少。通常用cld指令来设置DF标志位,使其置为0。
 
4. 举代码示例说明。下面使用代码说明,该代码的功能是实现strcpy。
定义需要拷贝的源字符变量:
szSource    db 'abcdefghijklmnopqrstuvwxyz', 0
 
4.1 首先看一下不使用lodsb和stosb的方法_StrCopy_1。
_StrCopy_1 proc
    local   @szBuffer[32]:byte
    ;字符串单个复制指令
    invoke lstrlen, offset szSource
    mov ecx,    eax                     ;要复制的字节个数
   mov esi,    offset szSource        ;源数据地址
    lea edi,    @szBuffer               ;目的数据地址
@@:
    mov bl, [esi]              ;一次复制一个字节
    mov [edi], bl              ;又将该字节复制到目的地
 
    inc edi                    ;目的地址和源数据地址都向上加一
    inc esi                   
    dec ecx                    ;复制个数减一
    ;cmp byte ptr [esi], 0       ;以null结尾的字符串
  cmp dec, 0
    jnz @B                     ;如果没有复制到结尾,则继续复制
    mov [edi], 0               ;将目的数据的最后一个字节置为null
   。。。。。                 ;其它代码
_StrCopy_1 endp
该段代码,没有使用任何特殊的指令。esi和edi也没有任何其它的意义,只是为了便于理解。用eax,edx代替esi,edi也是一样的。
 
4.2 使用lodsb和stosb的函数_StrCopy_2
_StrCopy_2 proc
    local   @szBuffer[32]:byte
   
    xor eax,    eax
    mov esi,    offset szSource    ;将要拷贝的源数据地址放入esi中
   test    esi,    esi             ;是否是有效地址?
    jz proc_end                   ;不是的话就停止运行
   
    lea edi,    @szBuffer           ;将要写入的目的地址放入edi中
    test    edi,    edi
    jz proc_end
 
    ;方向标志位。当方向标志位DF=0时,则esi自动增加;DF=1时,esi自动减小
    cld                ;设置DF=0
 
@@:
    ;代码的关键所在
   ;一次复制一个字节,将源地址的一个字节复制到al中。
    lodsb                           ;byte ptr [esi]-->al。
    ;本次传输一个字节,将al中的字节复制到目的地址中。
    stosb                           ;al-->byte ptr [edi]。
   test    al, al ;字符串是否已经到了结尾
    jnz @B      ;没有到结尾,继续传输下一个字符
    ;可以看到:从@@到此处的代码,就实现了字符串的自动复制。
;用到了DF标志位,esi和edi自动递增。
;用test al, al/jnz @B来判断是否到了结尾。也可以将要复制的长度放到ecx中,
;用代码dec ecx/test ecx, ecx/jnz @B来实现同样的功能。
;本函数比上个函数多使用了esi和edi寄存器。而且在循环里面,没有用指令递增地址(inc edi/inc esi),而是自动递增。
 
   ;加上这段代码,可以求得字符串的长度strlen,并放在ecx里面
    lea ecx,    @szBuffer
    sub edi,    ecx ;地址末尾减去地址头,等于该段地址的长度(字节数)
    mov ecx,    edi
    ;除了lodsb外,还有lodsw, lodsd,分别是一次传输一个字,双字到ax,eax里面。
    。。。。。。。。。 ;其它代码
_StrCopy_2 endp
 

 
二、字,双字操作指令
不是每次都要复制一个字节,有时候需要复制一个字或者双字怎么办呢?在_StrCopy_2的注释里面,也提到了,用lodsw/lodsd,相对应的指令是stosw/stosd。
概括说来:
 
2.1 从内存复制数据到寄存器
【lodsb指令】:从esi指向的源地址中逐一读取一个字符,送入AL中(然后,可以先判断这个字符是什么字符,如0dh,0ah之类等,再执行相应的操作)。
类似有【lodsw指令】:如果是lodsw,表明要处理的是字,而不是字符。则采用的相应指令是:lodsw;那么复制到的寄存器是AX,而不是AL了。
【lodsd指令】:如果是lodsd,表明要处理的是双字。则采用的相应指令是:lodsd;那么复制到的寄存器是EAX,而不是AL或AX了。
 
2.2 从寄存器复制数据到内存
【stosb指令】:将AL中的字符写入edi指向的目的地址。
类似有【stosw指令】:如果是stosw,表明要处理的是字,而不是字符。则采用的相应指令是:stosw;那么被复制的数据源寄存器是AX,而不是AL了。
【stosd指令】:如果是stosd,表明要处理的是双字。则采用的相应指令是:stosd;那么被复制的数据源寄存器是EAX,而不是AL或AX了。
 

 
三、结合rep指令
上面的介绍,都是一次复制一个字节、字或双字。比如一个双字数组长度位500个,那么就需要重复500次使用双字复制指令。有没有方法可以自动复制完所有数据?有,将要复制的长度500放到ecx中,然后在复制指令前加上rep指令即可(rep指令表示repeat)。
比如要在一个字数组的内存区域填充0xCC的代码如下:
    local   @wBuf[256]:word
 
   mov ax, 0cccch                    ;源数据
    lea edi,    @wBuf                 ;目的地址
  cld                 ;设置方向递增顺序
    mov ecx,    256                   ;将要复制的次数放在ecx中
    rep stosw                         ;复制
需要注意的是,如果是复制字符串的情况,要注意在后面加上0x00. 
 

四  如果要写一个memcpy函数,使用rep是不行的,因为rep只会将eax里面的东西搬运的到edi,而不会将源字符里面的东西搬运到eax。还是要使用_StrCopy_2里面的方法。
我所写的一个memcpy函数如下:
 
1 ;内存拷贝函数 2 ;_lpDest目的内存地址。本函数将把源内存的内容搬运到该内存地址上,搬运长度是_dwSize 3 ;_lpSource源内存地址 4 ;_dwSize,拷贝的长度。即使_lpSource里面包含了0x00的字符,只要小于_dwSize,仍然继续搬运。 5 ;例如: 6 ;_lpSource上的内容是:abcd(0x00)efg 7 ;_dwSize是8 8 ;函数运行的结果是:abcd(0x00)efg,虽然里面有字符串的结束符,但仍然搬运后面的剩下3个字符 9 _memcpy proc _lpDest, _lpSource, _dwSize10 mov edi, _lpDest11 mov esi, _lpSource12 mov ecx, _dwSize13 xor eax, eax14 cld15 @@:16 lodsb ; byte ptr [esi]-->al17 stosb ; al -----> byte ptr [edi]18 dec ecx19 test ecx, ecx20 jnz @B21 22 ret23 _memcpy endp
 
 可以使用以下代码作为测试:
1 local @szBuffer[256]:byte 2 3 invoke RtlZeroMemory, addr @szBuffer, 256 4 push 'abcd' 5 mov ebx, esp ;设置一个临时变量 6 7 invoke _memcpy, addr @szBuffer, ebx, 4 8 9 pop ebx ;平衡栈10 invoke MessageBox, NULL, addr @szBuff, NULL, MB_OK
 

repe   cmpsb   
 repe是一个串操作前缀,它重复串操作指令,每重复一次ECX的值就减一 
一直到CX为0或ZF为0时停止。 

cmpsb是字符串比较指令,把ESI指向的数据与EDI指向的数一个一个的进行比较。 

当repe   cmpsb配合使用时就是字符串比较啦,当相同时继续比较,不同时不比较 


mov   edi,[ebp+08]       将你输入的密码的地址付给EDI 
mov   esi,[ebp+0c]       真正的地址付给ESI 
mov   ecx,[ebp+10]       将他们的长度附给ECX 
repe   cmpsb                   进行比较 
jecxz   00401260           如果CX等于0,即密码正确跳 
mov   eax,00                   密码不正确时,会EAX为0 


再补充一下吧: 
    cmpsb是将ESI指向的字节与EDI指向的字节进行减操作 
    如果两个字符相等,即ZF为1,当不相等时ZF为0 


    而REPE停止重复的条件是ZF为0或CX为0,说明啦,当ZF为0时 
肯定就是字串不同啦,当CX为0时,表明字符串比对成功没有出现 
不相等的情况。 

  不过我觉得这样比不太好,如果你的密码打了8位 
  而真正的密码4位,如果ECX是4的话,他真会比对4位,正好你前4位 
  打对啦,那输入的密码也是正确的啦。如果他的ECX取得是你输入的密码 
  的个数,就没问题啦。不知道具体程序是什么样的,乱评一下,别介意。
 

 
MOVZX
汇编语言数据传送指令MOV的变体。无符号扩展,并传送。
  
movzx一般用于将较小值拷贝到较大值中。 这个指令是非常有用的,大家以后学程序设计的话,如果需要处理windows中的消息,例如WM_COMMAND消息,这个消息结构的wParam的高 16位是通知码,而低16位则是命令id。有时候需要判断命令id的话,则需要将这个wparam的低16位扩展成32位的,并且其余位用0填充。这就用 到了movzx。
movzx是将源操作数的内容拷贝到目的操作数,并将该值0扩展至16位或者32位。但是它只适用于无符号整数。 他大致下面的三种格式。
  movzx 32位通用寄存器, 8位通用寄存器/内存单元   
  movzx 32位通用寄存器, 16位通用寄存器/内存单元   
  movzx 16位通用寄存器, 8位通用寄存器/内存单元   
举个例子。
  例如   令eax=00304000h   若执行 movzx eax, ax后 eax = 00004000h 。   
  若执行 movzx eax, ah后 eax = 00000040h。   
  //windows内存00304000h存放在内存为   //00 40 30 00(这里请参见大端小端存储方式)所以ax = 4000h ah = 40h   //请注意不要搞混了   
又如:   MOV BL,80H   MOVZX AX,BL   
  运行完以上汇编语句之后,AX的值为0080H。由于BL为80H,最高位也即符号位为1,但在进行无符号扩展时,其扩展的高8位均为0,故赋值AX为0080H。   
 
总结:   movzx其实就是将我们的源操作数取出来,然后置于目的操作数,目的操作数其余位用0填充。
 

repne scasb 详解
SCAS是在检索目标字符串;REPNE/REPNZ是重复前缀(CX<>0 且ZF=0重复执行字符串指令),类似的还有REPE/REPZ、REP
  1. ax/al   搜索数据    
  2. es:di   目标串    
  3. cx         串长度    
  4. df         方向标志  


利用 REPNE SCAS 来检测代码中是否被下int3断点(CC):
  1. 00401241  /$  8D3D 00104000 LEA EDI,DWORD PTR DS:[<ModuleEntryPoint>>;  入口地址401000装入EDI  
  2. 00401247  |.  B9 97020000   MOV ECX,297                              ;  ECX赋值297  
  3. 0040124C  |.  E8 45000000   CALL 00401296                            ;  CC装入AL  
  4. 00401251  |.  F2:AE         REPNE SCAS BYTE PTR ES:[EDI]             ;  重复操作  
  5. 比如断点设在00401147处,入口出处为00401000,则F8后,ECX=297-(00401147-00401000+1)=14F,当然你也可以在原地不停的按住F4,直到ECX=1后再按F8  
  6. 00401253  |.  85C9          TEST ECX,ECX                             ;  ECX是否为0,如果下了断点,这里的ECX是非0值  
  7. 00401255  |.  75 3E         JNZ 00401295                             ;  既然你下了断点,这里一定不能跳  


利用 REPNE SCAS 来计算字符串长度:
  1. mov ecx, FFFFFFFF    设置循环次数-1  
  2. sub eax, eax             设置搜索内容0  
  3. repnz                           
  4. scasb                         一直重复搜索到EDI字符串末尾的0  
  5. not ecx                       得到搜索次数,也就是字符串的完整长度  
  6. dec ecx                       -1得到字符串不包含末尾0的长度  一、字节操作指令:lodsb和stosb
1. lodsd需要寄存器esi配合使用。每执行一次lodsb,就将[esi]中的一个字节复制到al寄存器中。
    即:lodsd == [esi] --> al
 
2. stosb需要寄存器edi配合使用。每执行一次stosb,就将al中的内容复制到[edi]中。
    即:stosb == al --> [edi]
 
3. ecx和cld指令。以上两条指令,一次只能复制一个字节。要是有多个字节需要复制该怎么办呢?那就需要用到一个计数器,所以常用ecx保存需要复制的字节个数。
为了能够自动复制,esi和edi需要能够自动的递增或者递减。这就用到了DF标志位(Directory Flag)。DF=0,esi和edi自动增加;DF=1,esi和edi自动减少。通常用cld指令来设置DF标志位,使其置为0。
 
4. 举代码示例说明。下面使用代码说明,该代码的功能是实现strcpy。
定义需要拷贝的源字符变量:
szSource    db 'abcdefghijklmnopqrstuvwxyz', 0
 
4.1 首先看一下不使用lodsb和stosb的方法_StrCopy_1。
_StrCopy_1 proc
    local   @szBuffer[32]:byte
    ;字符串单个复制指令
    invoke lstrlen, offset szSource
    mov ecx,    eax                     ;要复制的字节个数
   mov esi,    offset szSource        ;源数据地址
    lea edi,    @szBuffer               ;目的数据地址
@@:
    mov bl, [esi]              ;一次复制一个字节
    mov [edi], bl              ;又将该字节复制到目的地
 
    inc edi                    ;目的地址和源数据地址都向上加一
    inc esi                   
    dec ecx                    ;复制个数减一
    ;cmp byte ptr [esi], 0       ;以null结尾的字符串
  cmp dec, 0
    jnz @B                     ;如果没有复制到结尾,则继续复制
    mov [edi], 0               ;将目的数据的最后一个字节置为null
   。。。。。                 ;其它代码
_StrCopy_1 endp
该段代码,没有使用任何特殊的指令。esi和edi也没有任何其它的意义,只是为了便于理解。用eax,edx代替esi,edi也是一样的。
 
4.2 使用lodsb和stosb的函数_StrCopy_2
_StrCopy_2 proc
    local   @szBuffer[32]:byte
   
    xor eax,    eax
    mov esi,    offset szSource    ;将要拷贝的源数据地址放入esi中
   test    esi,    esi             ;是否是有效地址?
    jz proc_end                   ;不是的话就停止运行
   
    lea edi,    @szBuffer           ;将要写入的目的地址放入edi中
    test    edi,    edi
    jz proc_end
 
    ;方向标志位。当方向标志位DF=0时,则esi自动增加;DF=1时,esi自动减小
    cld                ;设置DF=0
 
@@:
    ;代码的关键所在
   ;一次复制一个字节,将源地址的一个字节复制到al中。
    lodsb                           ;byte ptr [esi]-->al。
    ;本次传输一个字节,将al中的字节复制到目的地址中。
    stosb                           ;al-->byte ptr [edi]。
   test    al, al ;字符串是否已经到了结尾
    jnz @B      ;没有到结尾,继续传输下一个字符
    ;可以看到:从@@到此处的代码,就实现了字符串的自动复制。
;用到了DF标志位,esi和edi自动递增。
;用test al, al/jnz @B来判断是否到了结尾。也可以将要复制的长度放到ecx中,
;用代码dec ecx/test ecx, ecx/jnz @B来实现同样的功能。
;本函数比上个函数多使用了esi和edi寄存器。而且在循环里面,没有用指令递增地址(inc edi/inc esi),而是自动递增。
 
   ;加上这段代码,可以求得字符串的长度strlen,并放在ecx里面
    lea ecx,    @szBuffer
    sub edi,    ecx ;地址末尾减去地址头,等于该段地址的长度(字节数)
    mov ecx,    edi
    ;除了lodsb外,还有lodsw, lodsd,分别是一次传输一个字,双字到ax,eax里面。
    。。。。。。。。。 ;其它代码
_StrCopy_2 endp
 

 
二、字,双字操作指令
不是每次都要复制一个字节,有时候需要复制一个字或者双字怎么办呢?在_StrCopy_2的注释里面,也提到了,用lodsw/lodsd,相对应的指令是stosw/stosd。
概括说来:
 
2.1 从内存复制数据到寄存器
【lodsb指令】:从esi指向的源地址中逐一读取一个字符,送入AL中(然后,可以先判断这个字符是什么字符,如0dh,0ah之类等,再执行相应的操作)。
类似有【lodsw指令】:如果是lodsw,表明要处理的是字,而不是字符。则采用的相应指令是:lodsw;那么复制到的寄存器是AX,而不是AL了。
【lodsd指令】:如果是lodsd,表明要处理的是双字。则采用的相应指令是:lodsd;那么复制到的寄存器是EAX,而不是AL或AX了。
 
2.2 从寄存器复制数据到内存
【stosb指令】:将AL中的字符写入edi指向的目的地址。
类似有【stosw指令】:如果是stosw,表明要处理的是字,而不是字符。则采用的相应指令是:stosw;那么被复制的数据源寄存器是AX,而不是AL了。
【stosd指令】:如果是stosd,表明要处理的是双字。则采用的相应指令是:stosd;那么被复制的数据源寄存器是EAX,而不是AL或AX了。
 

 
三、结合rep指令
上面的介绍,都是一次复制一个字节、字或双字。比如一个双字数组长度位500个,那么就需要重复500次使用双字复制指令。有没有方法可以自动复制完所有数据?有,将要复制的长度500放到ecx中,然后在复制指令前加上rep指令即可(rep指令表示repeat)。
比如要在一个字数组的内存区域填充0xCC的代码如下:
    local   @wBuf[256]:word
 
   mov ax, 0cccch                    ;源数据
    lea edi,    @wBuf                 ;目的地址
  cld                 ;设置方向递增顺序
    mov ecx,    256                   ;将要复制的次数放在ecx中
    rep stosw                         ;复制
需要注意的是,如果是复制字符串的情况,要注意在后面加上0x00. 
 

四  如果要写一个memcpy函数,使用rep是不行的,因为rep只会将eax里面的东西搬运的到edi,而不会将源字符里面的东西搬运到eax。还是要使用_StrCopy_2里面的方法。
我所写的一个memcpy函数如下:
1 ;内存拷贝函数 2 ;_lpDest目的内存地址。本函数将把源内存的内容搬运到该内存地址上,搬运长度是_dwSize 3 ;_lpSource源内存地址 4 ;_dwSize,拷贝的长度。即使_lpSource里面包含了0x00的字符,只要小于_dwSize,仍然继续搬运。 5 ;例如: 6 ;_lpSource上的内容是:abcd(0x00)efg 7 ;_dwSize是8 8 ;函数运行的结果是:abcd(0x00)efg,虽然里面有字符串的结束符,但仍然搬运后面的剩下3个字符 9 _memcpy proc _lpDest, _lpSource, _dwSize10 mov edi, _lpDest11 mov esi, _lpSource12 mov ecx, _dwSize13 xor eax, eax14 cld15 @@:16 lodsb ; byte ptr [esi]-->al17 stosb ; al -----> byte ptr [edi]18 dec ecx19 test ecx, ecx20 jnz @B21 22 ret23 _memcpy endp
 
 可以使用以下代码作为测试:
1 local @szBuffer[256]:byte 2 3 invoke RtlZeroMemory, addr @szBuffer, 256 4 push 'abcd' 5 mov ebx, esp ;设置一个临时变量 6 7 invoke _memcpy, addr @szBuffer, ebx, 4 8 9 pop ebx ;平衡栈10 invoke MessageBox, NULL, addr @szBuff, NULL, MB_OK 

repe   cmpsb   
 repe是一个串操作前缀,它重复串操作指令,每重复一次ECX的值就减一 
一直到CX为0或ZF为0时停止。 

cmpsb是字符串比较指令,把ESI指向的数据与EDI指向的数一个一个的进行比较。 

当repe   cmpsb配合使用时就是字符串比较啦,当相同时继续比较,不同时不比较 


mov   edi,[ebp+08]       将你输入的密码的地址付给EDI 
mov   esi,[ebp+0c]       真正的地址付给ESI 
mov   ecx,[ebp+10]       将他们的长度附给ECX 
repe   cmpsb                   进行比较 
jecxz   00401260           如果CX等于0,即密码正确跳 
mov   eax,00                   密码不正确时,会EAX为0 


再补充一下吧: 
    cmpsb是将ESI指向的字节与EDI指向的字节进行减操作 
    如果两个字符相等,即ZF为1,当不相等时ZF为0 


    而REPE停止重复的条件是ZF为0或CX为0,说明啦,当ZF为0时 
肯定就是字串不同啦,当CX为0时,表明字符串比对成功没有出现 
不相等的情况。 

  不过我觉得这样比不太好,如果你的密码打了8位 
  而真正的密码4位,如果ECX是4的话,他真会比对4位,正好你前4位 
  打对啦,那输入的密码也是正确的啦。如果他的ECX取得是你输入的密码 
  的个数,就没问题啦。不知道具体程序是什么样的,乱评一下,别介意。
 

 
MOVZX
汇编语言数据传送指令MOV的变体。无符号扩展,并传送。
  
movzx一般用于将较小值拷贝到较大值中。 这个指令是非常有用的,大家以后学程序设计的话,如果需要处理windows中的消息,例如WM_COMMAND消息,这个消息结构的wParam的高 16位是通知码,而低16位则是命令id。有时候需要判断命令id的话,则需要将这个wparam的低16位扩展成32位的,并且其余位用0填充。这就用 到了movzx。
movzx是将源操作数的内容拷贝到目的操作数,并将该值0扩展至16位或者32位。但是它只适用于无符号整数。 他大致下面的三种格式。
  movzx 32位通用寄存器, 8位通用寄存器/内存单元   
  movzx 32位通用寄存器, 16位通用寄存器/内存单元   
  movzx 16位通用寄存器, 8位通用寄存器/内存单元   
举个例子。
  例如   令eax=00304000h   若执行 movzx eax, ax后 eax = 00004000h 。   
  若执行 movzx eax, ah后 eax = 00000040h。   
  //windows内存00304000h存放在内存为   //00 40 30 00(这里请参见大端小端存储方式)所以ax = 4000h ah = 40h   //请注意不要搞混了   
又如:   MOV BL,80H   MOVZX AX,BL   
  运行完以上汇编语句之后,AX的值为0080H。由于BL为80H,最高位也即符号位为1,但在进行无符号扩展时,其扩展的高8位均为0,故赋值AX为0080H。   
 
总结:   movzx其实就是将我们的源操作数取出来,然后置于目的操作数,目的操作数其余位用0填充。
 

repne scasb 详解
SCAS是在检索目标字符串;REPNE/REPNZ是重复前缀(CX<>0 且ZF=0重复执行字符串指令),类似的还有REPE/REPZ、REP
  1. ax/al   搜索数据    
  2. es:di   目标串    
  3. cx         串长度    
  4. df         方向标志  


利用 REPNE SCAS 来检测代码中是否被下int3断点(CC):
  1. 00401241  /$  8D3D 00104000 LEA EDI,DWORD PTR DS:[<ModuleEntryPoint>>;  入口地址401000装入EDI  
  2. 00401247  |.  B9 97020000   MOV ECX,297                              ;  ECX赋值297  
  3. 0040124C  |.  E8 45000000   CALL 00401296                            ;  CC装入AL  
  4. 00401251  |.  F2:AE         REPNE SCAS BYTE PTR ES:[EDI]             ;  重复操作  
  5. 比如断点设在00401147处,入口出处为00401000,则F8后,ECX=297-(00401147-00401000+1)=14F,当然你也可以在原地不停的按住F4,直到ECX=1后再按F8  
  6. 00401253  |.  85C9          TEST ECX,ECX                             ;  ECX是否为0,如果下了断点,这里的ECX是非0值  
  7. 00401255  |.  75 3E         JNZ 00401295                             ;  既然你下了断点,这里一定不能跳  


利用 REPNE SCAS 来计算字符串长度:
  1. mov ecx, FFFFFFFF    设置循环次数-1  
  2. sub eax, eax             设置搜索内容0  
  3. repnz                           
  4. scasb                         一直重复搜索到EDI字符串末尾的0  
  5. not ecx                       得到搜索次数,也就是字符串的完整长度  
  6. dec ecx                       -1得到字符串不包含末尾0的长度  
3
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:388004次
    • 积分:4875
    • 等级:
    • 排名:第5838名
    • 原创:182篇
    • 转载:179篇
    • 译文:4篇
    • 评论:38条
    最新评论