• 几个常用的字符串操作函数 - [assembly fasm ]2006-01-08

    下面一段代码实现了几个常用的字符串操作函数,可在汇编语言程序中调用。注释中已给出了C语言调用时的函数声明方式,传递参数时使用__stdcall调用较为快速。因考虑效率是主要因素,主要演示字符串操作指令的用法,能用到双字操作则用双字操作stosd,其次用字操作stosw,再其次用字节操作stosb,如用到产品中去还需对esi和edi进行保护。

    部分代码参考于Linux内核代码中这些串操作函数的实现。

    这些代码使用flat assembler汇编实现,因fasm能比masm生成更为优化的代码,如mov eax,8等是字节与字间传递,普通汇编器生成五字节代码,而fasm能自动优化生成二字节代码,ret 0在普通汇编器中生成两字节代码,而fasm能直接优化为1Byte(0xc3),本附件中包含了一个生成的dll文件,只有1K大小,里面用十六进制编辑器查看,还有许多填充的零字节(这是PE文件格式所限制的512Bytes对齐,同时PE头至少占用一个512Bytes,功能性代码需另占一个512Bytes所限制了最小PE文件为1K)。有人曾声称使用汇编做出的最小Windows程序是1.5K的,只有一个MessageBox,事实上用fasm来做这个MessageBox只需1K空间。更为重要一点是fasm是多平台的,目前支持Win,DOS,Linux三个平台,写程序时与操作系统API无关的代码都是可移植的。还有fasm的强大的macro定义功能,关于fasm胜于其他汇编器的优点太多了,这里无法一一道出,打算有时间另写一篇。

    下载地址:已更新见下文

    format PE GUI 4.0 DLL
    entry dllstart
    include 'win32a.inc'
    
    section	'.flat' code executable
    
    dllstart:
    	xor	eax,eax
    	inc	eax
    	ret
    
    
     ; unsigned __stdcall int mstrlen( void const *pSrc )
    mstrlen:
    	pop	edx		; pop return address
    	pop	esi
    	push	edx		; push return address for ret
    
    	xor	ecx,ecx
    
      .iter0:
    	lodsb
    	or	al,al
    	loopne	.iter0
      .out0:
    	mov	eax,ecx
    	neg	eax
    	ret
    
    
     ; void __stdcall mstrcpy( void *pDest, void const *pSrc )
    mstrcpy:
    	pop	edx		; pop return address
    	pop	edi
    	pop	esi
    	push	edx		; push return address for ret
    
      .iter0:
    	lodsb
    	or	al,al
    	jz	.out0
    	stosb
    	jmp	.iter0
      .out0:
    	ret
    
     ; void __stdcall mstrcat( void *pDest, void const *pSrc )
    mstrcat:
    	pop	edx		; pop return address
    	pop	edi
    	pop	esi
    	push	edx		; push return address for ret
    
    	xor	eax,eax		; get 0	in al
    	repne	scasb		; to find pDest	end null byte
      .iter0:
    	lodsb
    	or	al,al
    	jz	.out0
    	stosb
    	jmp	.iter0
      .out0:
    	ret
    
     ; void __stdcall mmemzero( void *pDest, unsigned int uCount )
    mmemzero:
    	pop	edx		; pop return address
    	pop	edi
    	pop	ecx
    	push	edx		; push return address for ret
    
    	xor	eax,eax
    
    	push	ecx
    	shr	ecx,2
    	rep	stosd
    	pop	ecx
    
    	test	ecx,2
    	jz	.next0
    	stosw
      .next0:
    	test	ecx,1
    	jz	.next1
    	stosb
      .next1:
    	ret
    
     ; void __stdcall mmemfill( void *pDest, unsigned char cChar, unsigned int uCount )
    mmemfill:
    	pop	edx		; pop return address
    	pop	edi
    	pop	eax
    	pop	ecx
    	push	edx		; push return address for ret
    
    	push	ecx
    	shr	ecx,2
    	rep	stosd
    	pop	ecx
    
    	test	ecx,2
    	jz	.next0
    	stosw
      .next0:
    	test	ecx,1
    	jz	.next1
    	stosb
      .next1:
    	ret
    
     ; void __stdcall mmemcopy( void *pDest, void const *pSrc, unsigned int uCount )
    mmemcopy:
    	pop	edx		; pop return address
    	pop	edi
    	pop	esi
    	pop	ecx
    	push	edx		; push return address for ret
    
    	push	ecx
    	shr	ecx,2
    	rep	movsd
    	pop	ecx
    
    	test	ecx,2
    	jz	.next0
    	movsw
      .next0:
    	test	ecx,1
    	jz	.next1
    	movsb
      .next1:
    	ret
    
    align 16
    
    data export
    
      export '_memop.dll',\
    	mstrlen,'mstrlen',\
    	mstrcpy,'mstrcpy',\
    	mstrcat,'mstrcat',\
    	mmemzero,'mmemzero',\
    	mmemfill,'mmemfill',\
    	mmemcopy,'mmemcopy'
    
    end data

    2005-12-07 18:00:03 Updated:

    实际上观看 mmemzero 和 mmemfill 代码中有大部分段落是相同的,只有 al 中到底为零还是为用户所传参数的不同,因此代码是可重用的,改写为如下:

     ; void __stdcall mmemzero( void *pDest, unsigned int uCount )
    mmemzero:
    	pop	edx		; pop return address
    	pop	edi
    	pop	ecx
    	push	edx		; push return address for ret
    
    	xor	eax,eax
    
      .reuse:
    	push	ecx
    	shr	ecx,2
    	rep	stosd
    	pop	ecx
    
    	test	ecx,2
    	jz	.next0
    	stosw
      .next0:
    	test	ecx,1
    	jz	.next1
    	stosb
      .next1:
    	ret
    
     ; void __stdcall mmemfill( void *pDest, unsigned char cChar, unsigned int uCount )
    mmemfill:
    	pop	edx		; pop return address
    	pop	edi
    	pop	eax
    	pop	ecx
    	push	edx		; push return address for ret
    
    	jmp	mmemzero.reuse

    如此实现的函数间代码共用是C语言所不可能实现的。

    如果看过 Linux 内核中少有的几个汇编语言程序,就能发现有些代码能令人为之拍案叫绝!
    如arch/i386/boot/setup.S中有一段如下:

    # Routine to print	asciiz string at ds:si
    prtstr:
    	lodsb
    	andb	%al, %al
    	jz	fin
    
    	call	prtchr
    	jmp	prtstr
    
    fin:	ret
    
    # Space	printing
    prtsp2:	call	prtspc		# Print	double space
    prtspc:	movb	$0x20, %al	# Print	single space (note: fall-thru)
    
    # Part of above	routine, this one just prints ascii al
    prtchr:	pushw	%ax
    	pushw	%cx
    	movw	$7,%bx
    	movw	$0x01, %cx
    	movb	$0x0e, %ah
    	int	$0x10
    	popw	%cx
    	popw	%ax
    	ret
    
    beep:	movb	$0x07, %al
    	jmp	prtchr

    在区区 35Bytes 中实现了实模式下打印字符串,打印双空格,打印空格,打印al中字符,打印响铃字符等多个实用函数,令人叹服。

    2006-01-08 13:26:03 Updated:

    经过测试,发现这些代码确实存在问题,已修正。

    • 对 scasb 等串指令工作方式理解有误:这些串指令如 repne scasb 搜索停止时 edi 停止于所搜索字节的的下一个地址单元,故 strlen 计算长度会多一个,而 strcat 中也增加了 dec edi 才能工作。
    • mmemfilll 中 pop eax 只得到 al 的值,以四字节方式填充是错误的,修改后的代码先将 al 填充到整个 eax 才解决问题。
    • 原来的mstrcpy 没有给目的字符串填充末尾零,添加了一个 stosb 指令刚刚好。

    更新的下载地址中包含 _memop.asm 和 _memoptest.asm 和两个已编译过的二进制文件,要说的是这些 dll 中提供的函数中使用了 eax,ecx,edx,esi,edi 故不能在C语言程序中调用了,C调用方式或 stdcall 调用方式都要求 callee 保护 ebx,ebp,esi,edi 而只能直接使用 eax,ecx,edx 三个寄存器,caller 自己保护 eax,ecx,edx ,故此段程序代码要使用于C程序中还要加上包裹函数,保护 esi,edi 或者另外设计代码才行。

    更新下载地址:http://crquan.blogbus.com/files/1136703788.tar





    Tags: assembly fasm

    非非 发表于 13:00:03 | 引用 0 | 编辑

评论

  • fff

    f (http://f) 发表于 2007-09-04 23:46:07   [回复]
  • http://dreamerate.blogbus.com/logs/2006/01/1789767.html



    看看我近更改过的,效率绝对是一级棒的。已经测试:)

    非非 回复 风里有梦 说:
    佩服啊,效率就是这样一步步趋向极限的!
    (2006-01-12 22:24:48)

    风里有梦 (http://dreamerate.blogbus.com) 发表于 2006-01-08 21:48:32   [回复]
  • 呵呵! 刚看了borland的RTL,发觉他们用的搜索字串长度都是用SCASB,不过没有检查参数,如果用户传一个NULL过来,就会崩溃了。这是我最新在使用中发觉的问题。



    并且他们传送数据都是先用SCASB计算源串的长度,然后再用



    MOV EDX, ECX

    SHR ECX, 2 ;除 4

    MOVSD

    MOV ECX, EDX

    AND ECX, 3 ;取 4 的模

    MOVSB



    好快啊!真是太棒了。



    :)



    RANK 你的MOVSW会出错的,因为在使用AND取模时,只能用是2的倍数,比如说,只用4:AND ECX, 3

    但是不能用3的:AND ECX, 2, 所以MOVSW是不能这样用的,最后,用AND ECX,3就行了

    风里有梦 (http://dreamerate.blogbus.com) 发表于 2006-01-08 21:29:02   [回复]
  • 把时间改到了今天,本是想把文章置顶,没想到后边 Latest Comments 里面的链接失效了。真是个惨剧啊。

    Rank Cheng () 发表于 2006-01-08 15:38:23   [回复]
  • rank:上面几个函数有BUG。昨天测试发现如下:



    1、mstrlen内的neg ecx后再加上dec ecx才能返回正确的字串大小。因为

    .iter0:lodsb

    or al,al

    loopne .iter0



    把最后的NULL也计算在内了。我利用repne scasb重写此函数,这个是否会更快呢?



    2、MStrCopy拷贝完后,不会在目的串后加NULL,我重写了这函数:



    ;//////////////////////////////////////////////////////////////////////////////////////

    MStrCopy proc uses esi edi pDest:DWORD, pSrc:DWORD



    mov edi, dword ptr [pDest]

    mov esi, dword ptr [pSrc]

    xor ecx, ecx

    cld



    @begin:

    lodsb

    stosb

    or al, al

    jz @final

    inc ecx

    jmp @begin



    @final:

    mov eax, ecx

    ret



    MStrCopy endp

    ;//////////////////////////////////////////////////////////////////////////////////////



    3、MStrCat执行结果也不正确,因为repne scasb后目的指针已加1,意思就是把NULL加进了,导致pSrc加在NULL后而无效。而repne scasb也会判别ECX是否为零,为了避免发生尚未检测完NULL由于ECX为零转换执行下一指令,我把ECX设为-1了,这样就不怕减一减到成零了:)看下面:



    MStrCat proc uses esi edi pDest:DWORD, pSrc:DWORD



    cld

    mov edi, dword ptr [pDest]

    mov esi, dword ptr [pSrc]



    ; 构造一个对于scasb操作ecx-=1不等于0的数

    xor ecx, ecx

    dec ecx



    xor eax, eax ; 搜索NULL字符

    repne scasb



    dec edi ; 指针减一

    xor ecx, ecx ; initialize cat bits



    @begin:

    lodsb

    stosb

    or al, al

    jz @final

    inc ecx

    jmp @begin



    @final:

    mov eax, ecx

    ret



    MStrCat endp



    4、MMemCopy及MMemFill我把MASM32库函数的拷过来了,你看看这俩函数是否会更快呢?



    :)


    非非 回复 风里有梦 说:
    当初只是看见用 mov 实现的这几个函数效率不高才写的,没想到有这么多朋友在关注啊,看来真要好好测一测!
    (2006-01-07 09:13:35)

    风里有梦 (http://dsdm.blogchina.com) 发表于 2006-01-05 12:19:34   [回复]
  • 偶至今尚未使用过LINUX。真是丢脸了……



    如上打印段代码中的指令:andb, movb, pushw, movw, popw应该是FASM的宏吧?

    非非 回复 风里有梦 说:
    哦,不是啊,最后一段代码是内核中的汇编语言用的是 GNU as 的格式,以 w 结尾的指令是 word 操作的意思,这是 GNU assembler 的风格,内核中所有的汇编语言都用这种风格,是 AT&T 的汇编语言格式,不是 Intel 的,所以很多指令的写法都与 Intel 公布的不一样,一般看懂还是可以的,自己写的话就有点难为了。 fasm 则与这无关的,主页为(http://flatassember.net/),不过现在好像直接不能访问了,要找个代理吧,(ping 它有IP返回,说明域名解析无错,但IP连不通,我想又是拜网警所赐乱封IP的结果吧)
    (2005-12-08 16:05:09)

    风里有梦 (http://dsdm.blogchina.com) 发表于 2005-12-08 02:53:44   [回复]
  • 确实不错啊!代码很好!LINUX就是强!有机会要试试FASM!^_^

    风里有梦 (http://dsdm.blogchina.com) 发表于 2005-12-06 01:17:06   [回复]

发表评论