汇编版正则表达式
相关的例子:下载>>> 作者:AoGo 于2008-1-17上传 

    我们今天要学习的这个正则表达式库,是我使用汇编语言编写的一个表达式库,它是以 EditPlus的表达式功能为参照并增强一些符号而编写的,目前已经开源,如果有需要可以参考一下代码改进一下。
    首先,你要下载这个库:http://www.aogosoft.com/download/explib.rar
    或者,源代码包含例子:http://www.aogosoft.com/bbs/upfile/20071217184145_Express.rar

    如果你是使用MASMPlus,那就不需要再下载, MASMPlus中已经带有这个库,不过因为没有例子程序,还是推荐从上面的链接中下载完整的包含例子的版本。
与通用而强大的正则表达式库相比,这个库优点:
    1.     体积小,以库文件方式发布,引用到程序中只占用1 - 2K的大小;
    2.     功能强,因为没有预处理与编译,不需要占用内存;
    3.     把查找目标当成一行处理因而使用简单。
缺点是:
    1.     复杂的表达式查找性能比其它表达式库低;
    2.     个别特定的复杂表达式查找无法实现,如查找指定数目的某个表达式

    另外,目前的这个表达式库中有一个Bug,符号*在搜索时,如果有结束条件,但是第一个字符就是结束条件时,*会首先匹配前缀,如:
    在字符串”abc”中搜索”%*[a]

    正确的结果应该是没有匹配,上面的表达式意思是:在目标中查找单词字符,直到碰到字符a,并不把a放入匹配范围。因为bug,上面的表达式首先判断了a,再去判断下一个字符的结束条件。结果是这个表达式与”%+[a]”的结果是一样的了。这种情况只发生在第一个字符就是表达式结束条件的时候。
    同时,为了能够让使用者熟悉与方便使用,这个库中加了很多影响性能的代码,如果表达式有错,库函数会返回详细出错的位置,这就意味着在分析表达式并查找时,需要对可能出错的表达式符号进行检查,从而影响性能,下一版本的表达式库,会包含两个版本的搜索函数,一个是学习用,一个是去掉了检查代码的实用函数。同时会修正上面所说的Bug。

    这个库中只有一个函数,就是表达式搜索:

ExpressSearch:
    lpCharTable    ;字符表,一个字符是单词还是字词分隔符,由这个表来定义
                ;0表示使用默认值
    lpStart        ;查找目标的开始地址
    lpEnd        ;目标的结束地址
    lpExpress        ;表达式的地址
    lpOut        ;输出结果的数组字址
    lpiCount        ;数组大小与返回结果的数量
    EF_*        ;查找的方法标志

lpCharTable,指向一个字符表,内部默认的字符表已经输出为Public,可以在程序中直接使用: Extrn DefCharTable :DWORD来引用,它是一个256个DWORD的数组,直接修改即可,或复制一份,再使用ExpressSearch时,把地址传给这个参数,修改时,首先参照以下标志:

CF_WORD equ 1h        ;单词
CF_NUMBER equ 2h        ;数字
CF_HEX    equ 4h        ;十六进制
CF_BIN equ 8h        ;二进制
CF_UPCHAR equ 10h        ;大写字符
CF_LOWCHAR equ 20h        ;小写字符
CF_COMMENT equ 40h        ;注释字符
CF_COMMENTEND equ 80h        ;注释第二个字符
CF_TAB equ 100h        ;语法线对齐符
CF_SPACE equ 200h        ;空格
CF_ENTER equ 400h        ;回车
CF_WARP equ 800h        ;换行符
CF_MATCH equ 1000h        ;包含符,如双引号
CF_SELFMATCH    equ 2000h        ;包含符,如单引号
CF_INVALID    equ 40000000h        ;无效/无意义字符字符
CF_DOUBLEBYTE equ 80000000h        ;中文字符首


    比如,你要把符号&当成一个字符,可以这样:
    or dword ptr DefCharTable[‘&’*4],CF_WORD

    如果要把符号@当成字词分隔符,这样:
   and dword ptr DefCharTable[‘&’*4],not CF_WORD

    修改之后,表达式搜索会立即使用新的标志进行判断。如果复制了多份,可以针对不同的查找定义不同的字符表,而不需要苦恼,复制可直接申请内存并复制:

invoke LocalAlloc,LMEM_FIXED,256
mov lpCharTable,eax
invoke RtlMoveMemory,eax,offset DefCharTable,256

    同时,表达式只使用到了很少的这些标志,其它的标志,是提供给用户使用的,比如,搜索返回一个结果,如果要判断它们是否是数字,只需依次判断:

lea esi,结果字符串
movzx eax,byte ptr [esi]
.if dword ptr DefCharTable[eax*4] & CF_ NUMBER
    ;是数字字符
.endif

参数EF_*
    是指定表达式搜索行为的一些标志控制位,如下:

EF_DEC 表示从后往前搜索,此时,lpStart与lpEnd是相反的,同时,向前搜索时,中文字符将无法正确识别,请一定注意。
EF_USEEXPRESS 使用表达式,如果没有些标志,将使用普通的搜索方法,这样速度是很快的,因为这个表达式搜索库中有专门针对普通搜索的函数,速度当然比表达式快,所以如果只是普通搜索,请不要包含此标志。
EF_WORDCASE 默认不区分大小写,包含此标志,将区分大小写。
EF_MATCHWORD 匹配整个表达式,也就是结果范围的字串,最前一个字符与最后一个字符与相邻的字符必须不是相同字符表定义的字符。
EF_BEGINNOTLINEBEGIN 开始位置不是行首,如果是从一个结果中再搜索,目标首是紧跟着上一次搜索内存地址,可包含此标志,表示第一个字符位置不是行首。否则会当成行首对待。另外,表达式搜索函数库,行首行尾是以0D0A定义的,如果搜索目标中包含0D或0A,会当成行尾与行首对待。
EF_ENDNOTLINEEND 结束位置不是行尾,同上。
EF_ENDISSIZE 表示lpEnd不是地址,而是查找目标的长度。
EF_FINDONCE 只查找一次,如在“abc”中查找”b”,包含此标志时失败,因为只比较了一次,也就是a与b,就结束了。通常用于在结果中获得特定位置的字符。
EF_MODIFLINESTARTEND 查找结束后,如果是以^开头或$结束,将选择0D与0A字符,包含此标志,将自动去掉这两个字符。
EF_DOUBLEISWORD 中文字符也是单词,包含此标志,使用%号时,会自动匹配。
EF_RANGENOTCASE 表达式[]中总是区分大小写,有些时候,整个表达式不需要区分大小写,而[]中却需要区分时,包含此标志。

表达式查找时有可能发生的错误:

EFERR_NOTCONST 没有常数定义
EFERR_EXPNOTSUPPORT 不能这样使用符号
EFERR_NOTCLOSEBRACKET 没有关闭括号
EFERR_EMPTYEXPRESS 空的表达式,()/[]/{}/<>
EFERR_NOTEXISTEXP 多余的表达式结束符,请使用\],\),\>等等
EFERR_NOTBYONSELF 表达式符号不能单独使用
EFERR_NOTLOOPEND 对于?*/?+必须设置结束条件,同时,*/+对于?来说是一样的
EFERR_DEADLOOP 在使用*/+的表达式里面的整个表达式均是可忽略的,如(.*)+,因为.*总是成功
后面的+要求成功时继续,这是一个死循环.
EFERR_INVALIDEXP 无效表达式,如整个表达式就是 %*,因为*是可忽略符号,则上述表达式总是成功

下面,我们开始使用表达式搜索函数,表达式符号如下:

? 一个任意字符
. 一个空格或制表符
, 一个字词分隔符(包含.)
% 一个单词字符,字符是否是一个单词以字符表来定义
*<>|[] 0或更多,如果有结束条件,是直到[结束条件],如:?*<=>,直到碰到=号
+<>|[] 1或更多,如果有结束条件,是直到[结束条件],如:?+<=>,直到碰到=号
# 转义符,后面跟十六进制数,如#0D表达回车符号。#?表示中文字符
\ 字符,中间的字符是实际符号 如.表示空格制表符\.表示一个"."
[-] 在范围内 [a-b],或者清单,[abcdef],表达式符号仍然使用\来转换,如()/<>/{}/[]
" " " "内包含的必须完全一样,不区分大小写,不会理会是否包含EF_WORDCASE
' ' ' '与" " 的区别在于' '区分大小写,不会理会是否包含EF_WORDCASE
( ) 表达式,可嵌套,嵌套层次数无限制
{ } 标记并返回{}所包含的区域,可用在任何表达式内.
| 或者,支持任意表达式成员组合,如:(.)|(a)|((.*<%>)|(%+))
!() 条件取反,取反条件必须使用表达式包含
^ 行首
$ 行尾

我们来从例子进行分析:

;MASMPlus 代码模板

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc
include express.inc             ;MASMPlus已经自带了这个表达式库。可以直接使用

includelib express.lib        ;引用库文件
includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
include macro.asm
   
.DATA
    lpszString    db 'link.exe /SUBSYSTEM:WINDOWS /nologo /OUT:"Main.exe" "Main.obj" "Main.RES"'
    lpEnd db 0
   
.DATA?
    hInstance    dd ?
    lpRet             POINT 20 dup(<?>)                 ;最多20个
    iCount        dd ?
    buffer        db MAX_PATH*2 dup(?)
.CODE
GetRetString proc uses edx,lpPOINT,lpRetString,iSize
    mov edx,lpPOINT
    mov eax,[edx].POINT.y
    sub eax,[edx].POINT.x
    .if eax>iSize
        mov eax,iSize
    .endif
    push eax
    invoke RtlMoveMemory,lpRetString,[edx].POINT.x,eax
    pop eax
    mov edx,lpRetString
    mov BYTE ptr [edx+eax],0
    ret
GetRetString endp
START:

    invoke GetModuleHandle,NULL
    mov hInstance,eax
.data
        lp2 db '/{OUT}:{(\")+>}',0    ;将返回"Main.exe"包含引号
        lp3 db '/{OUT}:(\")+<{?+[\"]}\">',0 ;将返回 main.exe 无引号
        lp4 db '/{OUT}(:(\")+<{?+[\"]}\">)*',0
                            ;与lp3相同,但是如果/OUT之后没有值,也返回成功
        ;接着,我们进行不定个数的查找测试,查找所有单独使用引号包含的字符串.
        ;目标必须是类似组合的字串,并且相连续, "Main.obj" "Main.RES ,分开后就是:
        ; "Main.obj"
        ; "Main.RES"
        ;分析得到表达式如下:
        lp5 db '( +{\"?+<\">})+',0

.code
    ;在每次使用ExpressSearch前都要赋值.因为ExpressSearch会修改它
    mov iCount,20       
invoke ExpressSearch,0,                             ;使用默认的字符表
offset lpszString,            ;目标地址
offset lpEnd,                 ;结束地址
offset lp5, ;查找/OUT:"Main.exe",并分离出参数与值
offset lpRet,                 ;返回结果地址
offset iCount,                 ;个数
EF_USEEXPRESS                 ;使用表达式,区分大小写
    .if SDWORD ptr eax>0
        ;lpRet.x与lpRet.y,分别是整个匹配字符串的开始地址与结束地址.
        ;这个是不会变的。
        ;如果表达式中没有符号{}引用,则iCount永远为1
        ;这里有两个{},分别引出参数与值,一共是3个。
        .if iCount==3
            invoke GetRetString,addr lpRet[sizeof POINT],
                    addr buffer,sizeof buffer-1
            invoke MessageBox,0,addr buffer,CTXT("参数"),0 ;显示 OUT
            invoke GetRetString,addr lpRet[sizeof POINT*2],
                    addr buffer,sizeof buffer-1
            invoke MessageBox,0,addr buffer,CTXT("值"),0        
                    ;显示 "Main.exe" 或 Main.exe
        .elseif iCount==2    ;在使用lp4时才会有这个判断分支.
            invoke GetRetString,addr lpRet,addr buffer,sizeof buffer-1
            invoke MessageBox,0,addr buffer,CTXT("无值"),0   
                ;显示整个匹配
            ;/OUT之后无参数
        .endif
    .else
;        ;错误处理
;        .if eax==-1
;           
;        .elseif eax==-2
;            ... ...
;        .endif
    .endif
    invoke ExitProcess,0
END START

    代码中都有说明,使用其实是很简单的,这个表达式库最强的地方,在于可以选择性地获得结果,并一次性得到别的表达式需要多次查找才能得到的效果。是专门为程序员使用而定制的,使用这个表达式库,最重要的在于了解它的运作原理,使用起来才会得心应手。

我们就表达式来进行分析:

源字符串:
link.exe /SUBSYSTEM:WINDOWS /nologo /OUT:"Main.exe" "Main.obj" "Main.RES"
使用lp2 db '/{OUT}:{(\")+>}',0 时,拆分来分析:
/
{
    OUT
}
:
{
    (\")+<
        ?+<
            \"
        >
    >
}

    找到字符/,然后匹配OUT,如果有,返回位置(使用了符号{}),接着,后面必须是:号,然后返回两个”号之间的位置,?+<\”>,表示,任意字符直到碰到”号,合起来,就是/OUT:”*”,引号之间的可以是任意字符。
    lp3 db '/{OUT}:(\")+<{?+[\"]}\">',0

    这个与lp2的区别在于,返回的值不包含引号, {?+[\"]}\",?+[\”]表示任意字符直到碰到”号,但不把”号放入这次查找范围之内,也就是指针移动到了”号之前,这样后面需要再次用\”来匹配。如果已经是表达式的最后,可以不填。
   lp4 db '/{OUT}(:(\")+<{?+[\"]}\">)*',0

    这个表达式后面的部分用()*来包含,也就是表示,包含的这整个表达式都可以不要。这样,当只有前面的/OUT匹配时,仍然会返回成功,但返回的位置只有前面的。
   lp5 db '( +{\"?+<\">})+',0

    这个表达式查找并返回所有引号包含的字符串,但必须是空格开头,这样/OUT:”Main.exe”这一项就不会查找到。\"?+<\">就是返回引号之间的所有字符,()+表示,循 环地用这个表达式匹配成功之后的字符串,直到失败。这种时候,返回的位置{}个数是不确定的。这里使用了()+,也就是表示,至少要有1个,如果是使用()*,则一个都没有,都会返回成功。这就看使用者如果取舍了。

    上述的例子一次性得到/SUBSYSTEM:WINDOWS与/OUT:”Main.exe”,看看吧:
    (?+</{%+}:{(%+)|((\")+

    要注意,这个表达式查找后,iCount是大于3的喔。修改显示的代码为:

    .if iCount>1
        Xor ebx,ebx
        @@:
        invoke GetRetString,addr lpRet[ebx*sizeof POINT],addr buffer,sizeof buffer-1
        invoke MessageBox,0,addr buffer,CTXT("显示"),0

        inc ebx
        cmp ebx,iCount
        jl @B
    .endif


    这样可以依次显示所有抓到的字串。看很简单吧。

    如果要确定一个字串中是否包含某个字串,如在 Welcome中查找com,直接查找com即可,表达式搜索函数,是先从最开始进行搜索,一个一个向后来匹配,直到完成或失败,才结束的。

    如果是确定不包含com呢?使用!(com)吧,如果包含这个,则表达式失败。

    详细的,请大家多做一下测试吧,总之,这个表达式库,是专门为程序员使用而写的,强调查找与处理的方便性,而不在于功能如何复杂,所以,能够用在程序开发中的地方是非常之多的,别的表达式库,使用者总会认为太复杂,如果用得少还不如自己开发专用的函数来完成,事实上,我的这个库已经够复杂的了T_T,多分析与组合表达式,是可以实现极为快速的查找的,尤其是需要处理某些特殊的查找时,很多时候,使用这个函数和好的表达式,一次性就可以得到需要的所有字符与结果。当然必须以目标查找字串的格式而定制表达式。同一个表达式,在不同的字串查找中是不同的。

    因为表达式的复杂性,是没办法详细举例的,大家可以利用上述的例子,详细地进行测试,在下载的表达式库中,还包含一个测试程序,是专门用来测试表达式是否正确的,根据返回的错误,可以慢慢地熟悉,有任何的问题,你可以在我们的论坛提出。



<<<上一篇
欢迎访问AoGo汇编小站:http://www.aogosoft.com
下一篇>>>