鱼C论坛

 找回密码
 立即注册
查看: 1226|回复: 0

[技术交流] 别人家的 strlen

[复制链接]
发表于 2019-6-25 21:08:25 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
本帖最后由 RIXO 于 2019-6-25 21:20 编辑

今天看反汇编发现了一个奇怪的函数,然后百度后了解了一下原理,转载一下文章

原文参考链接
原文的帖子


    【原创性声明】在调试到strlen的汇编代码时,我发现其汇编代码略显“古怪”,当时并未有精力去关注,在研究过后重新查看了strlen的汇编代码。对strlen的原理解释并非原创,而是参考了《strlen源码剖析》一文的解释,在理解后感觉有必要写此文记之。



    首先我们创建一个测试项目,来比较以下我们自己用常规方法写的的strlen函数和<string.h> 中的strlen的时间消耗水平,这里我用 GetTickCount()做一个初步的估算(并不精确),测试代码如下:

  1. strlen时间消耗测试

  2. Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->#include "stdafx.h"
  3. #include <stdlib.h>
  4. #include <stdio.h>
  5. #include <string.h>
  6. #include <windows.h>

  7. //这是我们自己按常规算法实现的strlen,即逐个byte判断是否为0
  8. size_t mystrlen(char* s)
  9. {
  10.     char* p = s;
  11.     while(*p) ++p;
  12.     return (p - s);
  13. }

  14. int main(int argc, char* argv[])
  15. {
  16.     char* s;
  17.     size_t result, i;
  18.     DWORD tickcount;

  19.     s = (char*)malloc(4096);
  20.     memset(s, 'a', 4096);
  21.     s[4091] = 0;

  22.     //粗略测试两种strlen的时间
  23.     //标准库中的strlen:
  24.     tickcount = GetTickCount();
  25.     for(i=0; i<20000; i++)
  26.     {
  27.         result = strlen(s);
  28.     }
  29.     printf("strlen  :  %ld ms, result = %ld\n", GetTickCount() - tickcount, result);

  30.     //我们自己编写的strlen:
  31.     tickcount = GetTickCount();
  32.     for(i=0; i<20000; i++)
  33.     {
  34.         result = mystrlen(s);
  35.     }
  36.     printf("mystrlen:  %ld ms, result = %ld\n", GetTickCount() - tickcount, result);

  37.     free(s);
  38.     return 0;
  39. }
复制代码


    该测试的输出结果如下:

    strlen  :  16 ms, result = 4091
    mystrlen:  218 ms, result = 4091



    首先毫无疑问两种方法的时间复杂度相同(O(n)),区别在于常数系数不同,在这里我们自己写的函数的时间消耗大约是标准库版本的 13 倍之多。下面我们可以分别看下两个版本的汇编代码。



    首先是我们自己编写的mystrlen版本,编译器产生的汇编代码如下:
  1. 代码

  2. Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->
  3. .text:00401020 mystrlen        proc near
  4. .text:00401020
  5. .text:00401020 var_44          = dword ptr -44h
  6. .text:00401020 var_4           = dword ptr -4
  7. .text:00401020 arg_0           = dword ptr  8
  8. .text:00401020
  9. .text:00401020                 push    ebp
  10. .text:00401021                 mov     ebp, esp
  11. .text:00401023                 sub     esp, 44h
  12. .text:00401026                 push    ebx
  13. .text:00401027                 push    esi
  14. .text:00401028                 push    edi
  15. .text:00401029                 lea     edi, [ebp+var_44]
  16. .text:0040102C                 mov     ecx, 11h
  17. .text:00401031                 mov     eax, 0CCCCCCCCh
  18. .text:00401036                 rep stosd
  19. .text:00401038                 mov     eax, [ebp+arg_0]  ;
  20. .text:0040103B                 mov     [ebp+var_4], eax  ; char* p = s;
  21. .text:0040103E
  22. .text:0040103E loc_40103E:                             ;
  23. .text:0040103E                 mov     ecx, [ebp+var_4]
  24. .text:00401041                 movsx   edx, byte ptr [ecx] ; edx = *p;
  25. .text:00401044                 test    edx, edx            ; if ( edx == 0 )
  26. .text:00401046                 jz      short loc_401053    ;     break;
  27. .text:00401048                 mov     eax, [ebp+var_4]
  28. .text:0040104B                 add     eax, 1
  29. .text:0040104E                 mov     [ebp+var_4], eax    ; ++p;
  30. .text:00401051                 jmp     short loc_40103E    ;  
  31. .text:00401053 ;
  32. .text:00401053
  33. .text:00401053 loc_401053:                             ; CODE XREF: mystrlen+26j
  34. .text:00401053                 mov     eax, [ebp+var_4]     ; eax = p;
  35. .text:00401056                 sub     eax, [ebp+arg_0]     ; return (p - s);
  36. .text:00401059                 pop     edi
  37. .text:0040105A                 pop     esi
  38. .text:0040105B                 pop     ebx
  39. .text:0040105C                 mov     esp, ebp
  40. .text:0040105E                 pop     ebp
  41. .text:0040105F                 retn
  42. .text:0040105F mystrlen        endp
复制代码



    很明显,我们自己编写的函数 mystrlen 是逐个 byte 去比较的。

    下面我们看标准库中的strlen的汇编代码,位于VC6的如下路径:...\Microsoft Visual Studio\VC98\CRT\SRC\Intel\strlen.ASM

  1. Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->        CODESEG

  2.         public  strlen

  3. strlen  proc

  4.         .FPO    ( 0, 1, 0, 0, 0, 0 )

  5. string  equ     [esp + 4]

  6.         mov     ecx,string              ; ecx -> string
  7.         test    ecx,3                   ; test if string is aligned on 32 bits
  8.         je      short main_loop

  9. str_misaligned:
  10.         ; simple byte loop until string is aligned
  11.         mov     al,byte ptr [ecx]        ; 在DWORD未对齐部分逐个字节扫描
  12.         inc     ecx
  13.         test    al,al
  14.         je      short byte_3
  15.         test    ecx,3
  16.         jne     short str_misaligned

  17.         add     eax,dword ptr 0         ; 5 byte nop to align label below

  18.         align   16                      ; should be redundant

  19. main_loop:
  20.         mov     eax,dword ptr [ecx]     ; read 4 bytes, 复制四个char到eax
  21.         mov     edx,7efefeffh           ; 魔数 0x 7efefeff
  22.         add     edx,eax                 ; edx = 0x7efefeff + eax;
  23.         xor     eax,-1                  ; eax = ~eax (按位取反)
  24.         xor     eax,edx                 ; eax = ~eax ^ edx
  25.         add     ecx,4                   ; ecx 指向到下一个 DWORD
  26.         test    eax,81010100h           ; eax & (~0x 7efefeff), 含 0字节 吗?
  27.         je      short main_loop
  28.         ; found zero byte in the loop     如果当前的DWORD含0,找出其位于哪一个字节
  29.         mov     eax,[ecx - 4]
  30.         test    al,al                   ; is it byte 0
  31.         je      short byte_0
  32.         test    ah,ah                   ; is it byte 1
  33.         je      short byte_1
  34.         test    eax,00ff0000h           ; is it byte 2
  35.         je      short byte_2
  36.         test    eax,0ff000000h          ; is it byte 3
  37.         je      short byte_3
  38.         jmp     short main_loop         ; taken if bits 24-30 are clear and bit
  39.                                         ; 31 is set

  40.         ; 0 字节的地址减去字符串地址,即为strlen的结果。
  41. byte_3:
  42.         lea     eax,[ecx - 1]
  43.         mov     ecx,string
  44.         sub     eax,ecx
  45.         ret
  46. byte_2:
  47.         lea     eax,[ecx - 2]
  48.         mov     ecx,string
  49.         sub     eax,ecx
  50.         ret
  51. byte_1:
  52.         lea     eax,[ecx - 3]
  53.         mov     ecx,string
  54.         sub     eax,ecx
  55.         ret
  56. byte_0:
  57.         lea     eax,[ecx - 4]
  58.         mov     ecx,string
  59.         sub     eax,ecx
  60.         ret

  61. strlen  endp

  62.         end
复制代码


 请注意,strlen的主循环部分,是已4个char为一组作为一个DORD送到eax去检测的,因此要求必须读取DWORD时以 4 bytes 对齐,所以如果字符串地址并不对齐,则前面的1~3个char必须逐个字节检测。进入主循环的循环条件相当于以下代码:



    if ((((longword + magic_bits) ^ ~longword) & ~magic_bits) != 0 )

            {

        //则在这4个char中至少有一个char的值是0!

    }



    理解strlen代码的关键在于理解上面这个检测DWORD中含有 0 字节的条件。可以详细参考参考资料中的解释,这里我再少许重复以下:



    我们把 4 个 char 取到 eax 中,然后为了检测 eax 中是否含有 0 byte,注意不能逐个判断(否则效率就和普通算法无异),而是对eax加上一个魔数,该数字的取值是:7efefeff, 写成2进制是:

                                                             <-----------------------|    ...  ...     string

         |     byte [3]    |    byte [2]     |    byte [1]     |   byte [0]      |

         |       7E          |       FE          |      FE           |       FF          |

         | 0111 1110    | 1111 1110    | 1111 1110    | 1111 1111    |



    请注意上面这个数字的4个 0 的位置(holes),它们的位置是经过选取的,主要目的为了检测是否有进位。在eax和它相加后,如果eax的某个字节不为0,一定会在该字节上产生向“左侧”hole的进位,反之如果某个字节为0,则一定不会产生进位。这样相加后我们只要检测 4 个hole是否全部发生改变,如果有任何一个hole依旧为0不变,就知道当前的四个byte中必含有0,然后我们从byte[0]到byte[3]逐个探查哪一个byte为0即可。

    注意,当发现含有 0 的时候,是从低位byte到高位byte的顺序去检测的,这个顺序也很重要,因为低位的byte更靠近字符串的开始位置。

    上面的判断条件,是根据异或的性质,通过异或操作去找出 holes 是否发生改变,如果有任何hole在相加后未发生变化,会在计算结果中的相应位产生1,即不为0。如果所有hole都发生变化,这计算结果为0,即4个byte都不为0。



    代码中的" XOR EAX -1 "; 由于 -1 的补码是 0x FFFFFFFF,和1异或的结果相当于取反( 0^1 = 1, 1^1 = 0),即和(-1)异或相当于对该操作数取反。
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-5-27 13:38

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表