要求写一个比较高效的文件比较程序,竟然发现memcmp比strcmp要快很多,于是跟踪调试,发现它们的实现原理:
intel/strcmp.asm:
mov edx, dword ptr [esp + 4] ;取第二个参数地址
mov ecx, dword ptr [esp + 8] ;取第一个参数地址
test edx, 3 ;edx是第二个参数的地址,这里即检验该地址是否是4的倍数。
;因为如果edx&3!=0,则其最低两位不为1,所以为4的倍数。这里有个内存地址对齐的问题。
jne dopartial ;如果地址不是4的倍数,就跳到dopartial去处理。
dodwords:
mov eax, dword ptr [edx]
cmp al, byte ptr [ecx]
jne donene
or al, al ;看看字符串是否结束,这就是strcmp之所以比memcmp慢的地方了。
je doneeq ;如果 al==0,则比较结束
cmp ah, byte ptr [ecx + 1]
jne donene
or ah, ah
je doneeq
shr eax, 10h ;右移16位
cmp al, byte ptr [ecx + 2]
jne donene
or al, al
je doneeq
cmp ah, byte ptr [ecx + 3]
jne donene
or ah, ah
je doneeq
add ecx, 4
add edx, 4
or ah, ah
jne dodwords
move edi, edi ;这里一直大惑不解,不明白为什么这里要多出这两个字节来
doneeq:
xor eax, eax ;比较结果是相等,返回值为0
ret
nop ;这里也一直大惑不解,不明白这里为什么要插入一条空指令,感觉和上面的mov edi, edi应该是同一个原因。
donene:
sbb eax, eax ;比较结果不相等,这里也很经典,使用带借位减法,eax = eax - eax -cf。
shl eax, 1 ;若不相等处是大于,则 cf == 0,eax == 0,下面加1后eax==1,返回。
inc eax ;若不相等处是小于,则 cf == 1,那么 sbb后eax==-1,补码为0xFFFFFFFF,左移再加1还是-1
ret
mov edi, edi ;这里又出来了,不知道为什么要这么做,痛苦。
dopartial:
test edx, 1
je doword ;同样,与1与如果为0,则地址是2的倍数,跳到doword去执行。
mov al, byte ptr [edx]
inc edx
cmp al, byte ptr [ecx]
jne donene
inc ecx
or al, al
je doneeq
test edx, 2
je dodwords
dowords:
mov ax, word ptr [edx]
add edx, 2
cmp al, byte ptr [ecx]
jne donene
or al, al
je doneeq
cmp ah, byte ptr[ecx + 1]
jne donene
or ah, ah
je doneeq
add ecx, 2
jmp dodwords
intel/memcmp.asm
memcmp函数代码:
参数堆栈:
offset str1 --- ebp - 4 ------------当前栈顶
offset str2 --- ebp - 8
eax (strlen(str1)的返回值)
memcmp:
mov eax, dword ptr [esp + 0ch] ;得到memcmp的第三个参数:要比较的个数
test eax, eax ; 看要比较的字节数是否为0
je retnull ;如果要比较的字节数为0则直接返回
mov edx, dword ptr [esp + 4] ;得到memcmp的第一个参数,即offset str1
push esi
push edi ;保存寄存器值
mov esi, edx ;源字符串地址,即offset str1
mov edi, dword ptr [esp + 10h] ;不知何意,
or edx, edi
and edx, 3 ;根据strcmp的分析,这里依然是判断edx地址是不是4的倍数
je dwords ;地址是4的倍数,则跳到dwords去处理
test eax, 1 ;eax中存的是字符串长度。如果地址不是4的倍数,那么看要比较的字节数是不是2的倍数。
je mainloop ;如果要比较的内存字节数是2的倍数,则转向mainloop。
mov cl, byte ptr [esi] ;否则eax==1,比较最后一个字节
cmp cl, byte ptr [edi]
jne not_equal
inc esi
inc edi
dec eax
je done
main_loop:
mov cl, byte ptr [esi]
mov dl, byte ptr [edi]
cmp cl, dl
jne not_equal
mov cl, byte ptr [esi+1]
mov dl, byte ptr [edi+1]
cmp cl, dl
jne not_equal
add edi, 2
add esi, 2
sub eax, 2
jne main_loop ;这里用了jne而不是jmp,太好了,一举两得
done:
pop edi
pop esi
retnull:
ret
dwords:
mov ecx, eax ;eax中保存着字符串长度
and eax, 3
shr ecx, 2 ;右移两位,等于除以字符串长度除以4, 现在ecx == 100(64h)
je tail_loop_start ;循环移位指令不影响除CF,OF以外的其它位,
;故这里是判断eax是否是4的倍数,若是(eax & 3 == 0, zf = 1)则跳
repe cmps dword ptr [esi], dword ptr [edi]
;这是一条经典代码,cmps为串比较指令
;repe/repz:计数相等重复串操作指令功能:
; <1>如果cx==0或zf==0(比较的两数不等),则退出repe/repz
; <2>cx = cx - 1
; <3>执行其后的串指令
; <4>重复<1>--<3>
; 对于cmpsr的功能:
; 代码中是用dword ptr修饰过,所以是双字比较
; 比较完后:edi = edi +/- 4, esi = esi +/- 4
; 到底是加还是减,看df位的设置
je tail_loop_start ;看repe是如何退出的,到底是全部比较完
;了都相等退出(则zf==1,je成功跳转),还是
;比较的中途遇到不相等的退出
mov ecx, dword ptr [esi-4] ;已知是不相等退出的了,现在看具体的大小关系
mov edx, dword ptr [edi-4] ;所以后退4个字节比较
cmp cl, dl
jne diffrence_in_tail
cmp ch, dh
jne diffrence_in_tail
shr ecx, 10h
shr edx, 10h
cmp cl, dl
jne diffrence_in_tail
cmp ch, dh
diffrence_in_tail:
mov eax, 0
not_equal:
sbb eax, eax
pop edi
sbb eax, 0fffffffh
pop esi
ret
tail_loop_start:
test eax, eax ;现在eax是原字符串长度模4后的零头
je done ;看看eax是否为0,是则说明比较完成,eax中是零,则返回值是0,相等
mov edx, dword ptr [esi]
mov ecx, dword ptr [edi]
cmp dl, cl
jne diffrence_in_tail
dec eax
je tail_done
cmp dh, ch
jne diffrence_in_tail
dec eax
je tail_done
and ecx, 0ff0000h
and edx, 0ff0000h
cmp edx, ecx
jne diffrence_in_tail
dec eax
tail_doen:
pop edi
pop esi
ret
至此,也明白了为什么这两个函数会有效率的差别,strcmp比较的字符串,而memcmp比较的是内存块,strcmp需要时刻检查是否遇到了字符串结束的 /0 字符,而memcmp则完全不用担心这个问题。另一个区别是
strcmp在比较四字节是逐字节比较,而memcmp是用了字符串比较指令,感觉用字符串比较指令比用逐字节比较好,不知道strcmp为什么比较四字节时不用。感觉memcmp倒是可以用来实现strncmp函数的功能。
遗留的问题有内存字节对齐的问题,以及两处mov edi,edi和一处nop指令的问题。交给以后吧。