函数开始处的MOV EDI, EDI的作用

来源:互联网 发布:战地2游戏数据修改 编辑:程序博客网 时间:2024/06/18 08:14

调试程序调试到系统库函数的代码时,总会发现系统函数都是从一条MOV EDI, EDI指令开始的,紧接着这条指令下面才是标准的建立函数局部栈的代码。对系统DLL比如ntdll.dll进行反汇编,可以发现它的每个导出函数都是如此,并且每个导出函数开始处的MOV EDI, EDI上面紧接着5条NOP指令。比如在WinDbg中查看TextOutA周围的代码:

7573FDA0    E8 B8FDFFFF     call user32.MessageBoxTimeoutW
7573FDA5    5D              pop ebp      

7573FDA9    90              nop

7573FDAA    90              nop
7573FDAB    90              nop
7573FDAC    90              nop
7573FDAD    90              nop
7573FDAE >  8BFF            mov edi,edi
7573FDB0    55              push ebp
7573FDB1    8BEC            mov ebp,esp
7573FDB3    6A 00           push 0x0
7573FDB5    FF75 14         push dword ptr ss:[ebp+0x14]
7573FDB8    FF75 10         push dword ptr ss:[ebp+0x10]
7573FDBB    FF75 0C         push dword ptr ss:[ebp+0xC]
7573FDBE    FF75 08         push dword ptr ss:[ebp+0x8]
7573FDC1    E8 A0FFFFFF     call user32.MessageBoxExA
很明显,两个字节的MOV EDI,EDI指令什么事情也不做,那么,就有两个问题:第一,为什么不直接从函数体开始而要从这条什么都不做的指令开始呢?第二,即使需要在函数一开始空出两个字节,为什么不直接使用两条NOP指令,而要使用这条MOV指令呢?在网上查阅一些资料后,得到了答案: 
对于第一个问题,答案是为了实现hot-patching技术,即运行时修改一个函数的行为。修改过程如下:把MOV EDI, EDI修改为一条短跳转指令(一条短跳转指令恰好两个字节),把MOV EDI, EDI上面的五个NOP修改为一条长跳转指令(一条长跳转指令恰好五个字节),短跳转指令跳到长跳转指令上,长跳转指令跳到修改后的函数体上。 
对于第二个问题,答案是为了提高效率。执行一条MOV指令比执行两条NOP指令花费更少的时间。 
  
http://blog.csdn.net/zang141588761/article/details/70237455
在这篇日志这是一种实现hot-patching和hot-fix的技术,而且解释了为什么不使用detours技术来实现hot-patching。
  
// memory copy code benchmark// source code for Visual C++ 6.0 + Service Pack 4 + Processor Pack// copyright(C) 2001,2005 XELF. All rights reserved.// http://www.cyborg.ne.jp/~xelf/// You can use this source code for any purpose without permission.// Notes that this source code is not supported the processing of fraction bytes.#include <windows.h>#include <stdio.h>#include <malloc.h>#include <mmsystem.h>#pragma comment(lib,"winmm.lib")enum { size = 1024*1024*16, };void mem( LPBYTE d, const BYTE* s, int _size ) {memcpy(d,s,_size);}void memc( LPBYTE d, const BYTE* s, int _size ) {for(int i=_size>>2;--i>=0;) {*(LPDWORD)d=*(const DWORD*)s;d+=4;s+=4;}}void mem2( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,2;lx:mov eax,[esi];add esi,4;mov [edi],eax;add edi,4;dec ecx;jnz lx;}}void mem3( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,3;lx:mov eax,[esi];mov ebx,[esi+4];add esi,8;mov [edi],eax;mov [edi+4],ebx;add edi,8;dec ecx;jnz lx;}}void mem4( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,3;lx:movq mm0,[esi];add esi,8;movq [edi],mm0;add edi,8;dec ecx;jnz lx;}}void mem5( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,4;lx:movq mm0,[esi];movq mm1,[esi+8];lea esi,[esi+16];movq [edi],mm0;movq [edi+8],mm1;lea edi,[edi+16];dec ecx;jnz lx;}}void mem8( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,3;lx:movq mm0,[esi];lea esi,[esi+8];movntq [edi],mm0;lea edi,[edi+8];dec ecx;jnz lx;}}void mem9( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,4;lx:movq mm0,[esi];movq mm1,[esi+8];lea esi,[esi+16];movntq [edi],mm0;movntq [edi+8],mm1;lea edi,[edi+16];dec ecx;jnz lx;}}void mem10( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,4;lx:movq mm0,[esi];movq mm1,[esi+8];lea esi,[esi+16];movntq [edi],mm0;prefetcht0 [esi+768];movntq [edi+8],mm1;lea edi,[edi+16];dec ecx;jnz lx;}}void mem6( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,4;lx:movaps xmm0,[esi];lea esi,[esi+16];movaps [edi],xmm0;lea edi,[edi+16];dec ecx;jnz lx;}}void mem7( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,4;lx:movaps xmm0,[esi];lea esi,[esi+16];movntps [edi],xmm0;lea edi,[edi+16];dec ecx;jnz lx;}}void mem7pre( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,5;lx:movaps xmm0,[esi];movaps xmm1,[esi+16];lea esi,[esi+32];movntps [edi],xmm0;prefetcht0 [esi+1024];movntps [edi+16],xmm1;lea edi,[edi+32];dec ecx;jnz lx;}}void memfpu( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,3;lx:fld double ptr [esi];lea esi,[esi+8];fstp double ptr [edi];lea edi,[edi+8];dec ecx;jnz lx;}}void memrep( LPBYTE d, const BYTE* s, int _size ) {_asm {mov edi,d;mov esi,s;mov ecx,_size;shr ecx,2;rep movsd;}}void begin( LPBYTE mem1, LPBYTE mem2, int size, const char* text ) {memset(mem1,0x55,size);memset(mem2,0xAA,size);printf(text);}void end( LPBYTE mem1, LPBYTE mem2, int size, int time ) {_asm emms;printf("%d.%d [ms]",time/10,time%10);if (memcmp(mem1,mem2,size)) {printf("error!");}printf("\r\n");}enum {cpu_legacy=0, cpu_mmx=(1<<0),cpu_3dnow=(1<<1), cpu_e3dnow=(1<<2),cpu_sse=(1<<3), cpu_sse2=(1<<4),};int CPU() {int flags=0,type=0;__asm {xor eax,eax;cpuid;or eax,eax;jz quit;mov eax,1;cpuid;mov flags,edx;quit:}if (flags&(1<<31)) {type|=cpu_3dnow;if (flags&(1<<30)) {type|=cpu_e3dnow;}}if (flags&(1<<23)) {type|=cpu_mmx;if (flags&(1<<25)) {type|=cpu_sse;if (flags&(1<<26)) {type|=cpu_sse2;}}}return type;}void benchmark( LPBYTE a, LPBYTE b, int size ) {int cpu=CPU();{begin(a,b,size,"memcpy: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {mem(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}{begin(a,b,size,"rep movsd: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {memrep(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}{begin(a,b,size,"FPU 8bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {memfpu(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}if (cpu&cpu_sse) {begin(a,b,size,"MMX movntq pre 16bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {mem10(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}if (cpu&cpu_sse) {begin(a,b,size,"MMX movntq 16bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {mem9(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}if (cpu&cpu_sse) {begin(a,b,size,"MMX movntq 8bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {mem8(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}if (cpu&cpu_sse) {begin(a,b,size,"SSE movntps pre 32bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {mem7pre(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}if (cpu&cpu_sse) {begin(a,b,size,"SSE movntps 16bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {mem7(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}if (cpu&cpu_sse) {begin(a,b,size,"SSE 16bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {mem6(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}if (cpu&cpu_mmx) {begin(a,b,size,"MMX 16bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {mem5(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}if (cpu&cpu_mmx) {begin(a,b,size,"MMX 8bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {mem4(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}{begin(a,b,size,"asm 8bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {mem3(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}{begin(a,b,size,"asm 4bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {mem2(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}{begin(a,b,size,"C++ 4bytes: ");DWORD t0=timeGetTime();for(int i=0;i<10;i++) {memc(a,b,size);}DWORD t1=timeGetTime();end(a,b,size,t1-t0);}}int main() {timeBeginPeriod(1);printf("memory copy code benchmark VER.2005-09-01 by (C)2001,2005 XELF.\r\n");printf("copy size: %d [bytes]\r\n\r\n",size);LPBYTE a=(LPBYTE)_aligned_malloc(size,16);LPBYTE b=(LPBYTE)_aligned_malloc(size,16);benchmark(a,b,size);_aligned_free(a);_aligned_free(b);printf("completed.\r\n");timeEndPeriod(1);return 0;}
从效率和其它方面详细解释了为什么选择用这种技术来实现hot-patching以及为什么要这样实现(短跳转加长跳转而不是一次性长跳转)。 
  
http://xelf.info/knowledge/MemoryCopy.cpp 
这里给出了具体的memcpy的C语言源代码。如果在安装VC6的时候选择了安装CRT源代码,则在VC安装目录的SRC/INTEL/目录中有memcmp.asm等文件,它们就是对应的CRT函数的源代码,这些源代码中也都有详细的注释。 
  
要求写一个比较高效的文件比较程序,竟然发现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则完全不用担心这个问那条MOV EDI, EDI完全是为了内存四字节对齐的。源代码中写的是align 4,在运行时,如果需要一个填充字节,则会填充一条NOP指令,如果需要两个字节来填充,则会填充一条MOV EDI, EDI指令,之所以不用两条NOP,是出于效率的考虑。
1 0
原创粉丝点击