首页
社区
课程
招聘
[原创]字符串函数的汇编实现
发表于: 2010-2-13 11:25 9989

[原创]字符串函数的汇编实现

2010-2-13 11:25
9989
在项目开发过程中,对字符串的处理是经常遇到的问题,稍有不慎,就会留下一些不易察觉的瑕疵;熟练使用字符串函数,理解其实现过程,对编程会大有裨益;

在开始之前,请思考一下,是否真的明白了这样几个函数的用法。
1. sizeof,这不是函数,而是一个运算符,在编译时就已经计算好了参数所占用的内存空间,并分配;
2. strlen,函数,运行时计算参数中的元素个数,不包括'\0'结束符
对应的版本有两个:strlenA窄字符版本,strlenW宽字符版本
3. strcpynA, strcpyA的区别,strcpynA中的 unsigned long len长度表示的是内存长度还是元素个数,从什么地方开始计算
4. 快速求出一个字符串的最后元素,可以用前面方法的组合,也可以自己实现一个函数strendA
5.在前面几个函数的基础上,就可以自己实现strcatA等一些扩展函数

6.如果上面的都十分清楚,那可以分析下内部实现,增强功力;汇编、数据结构、这些都是作为一个程序员所应具备的功底。

一点说明:WINDOWS 提供的大多库函数都是fastcall调用约定,顾名思义,就是为了提高传输参数的效率而使用的一个调用约定;我们知道,传递参数,要么用堆栈,要么用寄存器,而fastcall调用约定就是二者的结合;前两个参数用ecx,edx分别传递,之后的参数用堆栈从右向左依次传递。

现在,感兴趣的可以看下面的代码。代码是我经过严格测试的,注释也是为了方便大家阅读加上去的。


//
//	ANSI字符串求元素个数
//

__declspec(naked) unsigned long __fastcall strlenA(const char *s)
{
	__asm
	{
		mov edx, edi		//保存edi的值,到后面用 mov edi,edx恢复
		mov edi, ecx		//将参数ecx传递给edi,也就是说edi指向的内存空间存储字符串s
		or ecx, -1			//初始化ecx 为 0FFFFFFFFh
		xor eax, eax		//初始化 eax =0

		repne scasb			//scan byte,字符串搜索,在edi指向的内存空间中搜索与al相等的值,也就是结束符0			
		
		dec eax				// eax-1,这里,eax = -1,和repne scasb执行之前的ecx的值一样
		dec eax				// eax-1,相当于减去了'\0'的长度

		sub eax, ecx		//执行repne scasb的过程中,每循环一次,ecx--,那么sub eax,ecx 就是元素的个数了
							//这里比较巧妙,运算都是在负数范围内,元素的个数也就是eax与ecx相对与0的距离之差
		mov edi, edx		
		retn				//返回值在eax默认返回
	}
}

//
//	WIDE CHAR 字符串求元素个数
//
__declspec(naked) unsigned long __fastcall strlenW(const wchar_t *s)
{
	__asm
	{
		mov edx, edi
		mov edi, ecx
		or ecx, -1
		xor eax, eax
		repne scasw		//scan word,结构和思想与strlenA函数一样,只是这里,我们是每次比较word长度的单位(2字节)
					//每循环一次,ecx--,edi +=2
		dec eax
		dec eax
		sub eax, ecx
		mov edi, edx
		retn
	}
}



//
//	ANSI字符串的copy函数
//	参数三:unsigned long len:表示要copy的长度,即元素个数	
//

__declspec(naked) char* __fastcall strcpynA(char *dest, const char *src, unsigned long len)
{
	__asm
	{
		push edi
		push esi
		mov edi, ecx			//fast call调用约定,第一个参数用ecx传递,第二个参数用edx传递
		mov eax, ecx			//现在,edi 保存 dest 指向的内存地址

		mov esi, edx			//esi 保存 src 指向的内存地址

		mov edx, [esp + 12]		               //传递将要copy的长度len,第三个参数用堆栈传递,[esp+8] 就是返回地址		
		mov ecx, edx			//字符串的长度转存到ecx中

		shr ecx, 2				//长度缩小4倍,其实就是循环次数缩小4倍,每次传递4个字节

		repnz movsd			//mov dword

		mov ecx, edx			//余数部分,每次只传递1个字节
		and ecx, 3			//取余数部分,mod 4 的余数

		repnz movsb
		
		mov byte ptr [edi], 0	                //字符串结尾处,填充0
		
		pop esi				//按照进栈的顺序,弹栈,使堆栈平衡
		pop edi
		
		retn 4
	}
}

//
//	WIDE CHAR字符串的copy函数
//	参数三:unsigned long len:表示要copy的长度,即元素个数
//

__declspec(naked) wchar_t* __fastcall strcpynW(wchar_t *dest, const wchar_t *src, unsigned long len)
{
	__asm
	{
		push edi
		push esi
		mov edi, ecx
		mov eax, ecx			//让eax指向目的字符串的地址[ecx]
		mov esi, edx
		mov edx, [esp + 12]		               //保存要传递的元素个数

		mov ecx, edx
		shr ecx, 1				//循环次数缩小2倍,每次传输两个word长度,也就是4字节

		repnz movsd				//每次传递4个字节
		mov ecx, edx

		and ecx, 1				//取余数部分,mod 2的余数

		repnz movsw				//每次循环传递一个word长度,也是最小单元传输长度								
		
		mov word ptr [edi], 0	//字符串结尾,填充0
		
		pop esi
		pop edi
		
		retn 4					//返回的字符串保存于eax
	}
}



//
// 求ANSI字符串的最后一个元素
//

__declspec(naked) char* __fastcall strendA(const char *s)
{
	__asm
	{
		mov edx, edi
		mov edi, ecx
		or ecx, -1
		xor eax, eax
		repnz scasb
		lea eax, [edi - 1]	//此时,edi已经指向字符串的\0处,[edi-1]指向字符串的最后一个元素
		mov edi, edx
		retn
	}
}

//
//	求WIDE CHAR 字符串的最后一个元素
//
__declspec(naked) wchar_t* __fastcall strendW(const wchar_t *s)
{
	__asm
	{
		mov edx, edi
		mov edi, ecx
		or ecx, -1
		xor eax, eax
		repnz scasw
		lea eax, [edi - 2]
		mov edi, edx
		retn
	}
}

char* __fastcall strcatA(char *dest, const char *src)
{
	strcpyA(strendA(dest), src);
	return dest;
}

wchar_t* __fastcall strcatW(wchar_t *dest, const wchar_t *src)
{
	strcpyW(strendW(dest), src);
	return dest;
}



希望大家感兴趣。
别说我古董,我大学还没毕业,不过也快了。但是自己深知道,编程这东西,理解的越透彻,越深入,才会游刃有余;
现在大家知道,假如求一个字符串的长度,时间复杂度是多少,不用说,都是O(N)啊,不要因为我们平时见不到这些内部实现,就认为CPU会做的多智能...
顺祝大家春节快乐!

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 7
支持
分享
最新回复 (15)
雪    币: 392
活跃值: (89)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
2
自己回复一个
2010-2-13 15:16
0
雪    币: 209
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
只看了你那个求字符串长度的,有几个问题不太明白。第一,为什么保存寄存器的值不用堆栈,而用另一个寄存器呢?一个push 就可以搞定的,却要用一个mov,似乎不太合理。第二,你repne scasb之后,为什么不直接用:
neg ecx
dec ecx
mov eax,ecx
retn
还要费那么大周折呢?
小弟不才,提出自己的疑问而已,不对之处还请指正,下面的代码俺就没再看。
2010-2-13 16:16
0
雪    币: 392
活跃值: (89)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
4
1.对你的第一个问题,我不是保存寄存器值,而是指令需求,比如:repne scasb就要用到edi的值,而参数是用ecx传递进来的,当然首先要进行mov edi,ecx ,你可能把这样的代码理解成保存寄存器值了;另外,如果第一条mov edx,edi指令要是写成pushad,那又得浪费多少堆栈空间啊,我只是希望恢复edi而已

2.你说的repne scasb之后的代码写法,你的写法比较简约,不过少了一次dec ecx,你忘了前面初始化是-1;你四条指令,我三条指令,不过,在执行上,你的方法可能会好点
2010-2-13 16:33
0
雪    币: 203
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
我在application 编译出来 是 __cdecl  的,  驱动里编译出来 是优化到内联了, 都不是fastcall的
2010-2-13 23:02
0
雪    币: 1259
活跃值: (38)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
stu
6
repne scasb      //scan byte,字符串搜索,在edi指向的内存空间中搜索与eax相等的值,也就是结束符0
很明显的笔误.scasb应该是和al比较吧.
另外个人认为每次scasb前加个cld应该更严谨一些.
2010-2-14 00:50
0
雪    币: 392
活跃值: (89)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
7
笔误,呵呵,非常感谢;
不过,最高兴的是,还有朋友喜欢讨论讨论,喜欢指点指点
2010-2-14 01:25
0
雪    币: 209
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
假设当ecx=-3的时候,退出了循环,那么说明长度应该是2吧(不包括0)?那么neg ecx就应该是3,然后dec ecx就是2.应该没错吧?另外我这个是4条指令,你的是5条。。。(从repne scasb后到retn)。
另外,他这种方式肯定不能CLD的,否则整个就错了,不知道六楼的朋友明白不?
2010-2-14 12:53
0
雪    币: 392
活跃值: (89)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
9
首先,哈哈,很感谢你的解读;

有两点需要说明的:
1.ecx初始化为-1,那么,假设当ecx=-3的时候,退出循环,真正的字符串元素个数(也就是你说的长度)应该是1,而不是2;

2.从repne scasb这条指令开始,按照你的想法,应该是这样的:
repne scasb
neg  ecx              //如你所说,假设:ecx = -3
dec  ecx              // ecx = 2
dec  ecx              // ecx = 1
mov eax,ecx

mov edi,edx   //这一句,是恢复目的寄存器edi的,你我的写法都不能省去
retn
这下应该清楚了吧
2010-2-14 13:08
0
雪    币: 2067
活跃值: (82)
能力值: ( LV9,RANK:180 )
在线值:
发帖
回帖
粉丝
10
不明白. 愿闻其详.
2010-2-14 13:51
0
雪    币: 209
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
很简单啊,楼主所采用的方式是递减而不是递增的方式,CLD会将DF清0,变成递增,用在这里肯定不行的啊。
2010-2-14 18:14
0
雪    币: 2067
活跃值: (82)
能力值: ( LV9,RANK:180 )
在线值:
发帖
回帖
粉丝
12
听你在乱讲.
2010-2-14 18:37
0
雪    币: 209
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
汗。。。那还请你给我解释解释啊,楼上的大哥。我说的哪里有误请指出来啊,我好学习学习,我到这里来就是抱着学习的态度的,本人才疏学浅,您一句我在乱讲,就完事了?
2010-2-14 19:40
0
雪    币: 209
活跃值: (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
sorry,我重新看了下,是我说错了,抱歉,麻烦谁有权限的把我的那个回复删除了。的确是我说错了,我有点晕乎,把DF标志位给想成与ECX相关了,sorry了。sessiondiy说的对,不过您要是能直接给我指出来哪里错了就更好了。不过DF默认是0的,所以CLD不是必须的,当然加一个保险。现在更正下:
DF是方向标志位,默认为0,为0时EDI寄存器指针按照递增方式,为1时为递减。与ECX无关,sorry了。
2010-2-14 19:47
0
雪    币: 392
活跃值: (89)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
15
seizeme兄,讨论讨论也是好的,不必因为一点的小过失而自责
2010-2-14 20:10
0
雪    币: 65
活跃值: (12)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
路过学习了,膜拜下
2010-2-17 21:39
0
游客
登录 | 注册 方可回帖
返回
//