rep 和 stos 什么时候出现在编译的 C 中?

逆向工程 拆卸 C
2021-07-04 02:27:10

你能给我一些编译成repand 的示例 C 代码stos吗?

00401059  /$  55            PUSH EBP
0040105A  |.  8BEC          MOV EBP,ESP
0040105C  |.  83EC 5C       SUB ESP,5C
0040105F  |.  57            PUSH EDI
00401060  |.  66:A1 E068400>MOV AX,WORD PTR DS:[4068E0]
00401066  |.  66:8945 B0    MOV WORD PTR SS:[EBP-50],AX
0040106A  |.  B9 13000000   MOV ECX,13
0040106F  |.  33C0          XOR EAX,EAX
00401071  |.  8D7D B2       LEA EDI,DWORD PTR SS:[EBP-4E]
00401074  |.  F3:AB         REP STOS DWORD PTR ES:[EDI]
00401076  |.  66:AB         STOS WORD PTR ES:[EDI]
00401078  |.  8D4D B0       LEA ECX,DWORD PTR SS:[EBP-50]
0040107B  |.  51            PUSH ECX                                 ; /Arg2
0040107C  |.  8D55 A4       LEA EDX,DWORD PTR SS:[EBP-5C]            ; |
0040107F  |.  52            PUSH EDX                                 ; |Arg1
00401080  |.  E8 7BFFFFFF   CALL 00401000                            ;             

这只是我的例子。

2个回答

让我们首先解释每条指令的作用。REP OPD 的工作原理如下:

          for (; ecx > 0; ecx--) OPD

该指令将重复操作数,同时递减ECX直到达到0请注意,在您的代码中,ECX设置为 13(地址 0040106A)。

另一方面,STOS OPD 将ALAXEAX的值存储在给定的内存操作数中。寄存器大小由内存位置大小定义,因此是代码中DWORD

总的来说,这两条指令结合起来创建了一个将数据存储在内存中的循环。现在把东西放在C形式中,如果我们想将一个字节数组初始化为0(例如 memset),我们可以这样做:

    unsigned char t[MAX_CHAR];

    for (int i = 0; i < MAX_CHAR; i++)
         t[i] = 0;

此 C 代码可以转换为多个等价的汇编代码。它主要取决于编译器和指定的优化级别。基于 REP STOS 的一种变体可能是:

    mov ecx, MAX_CHAR  //Initialize ecx to the number of iterations desired
    xor eax, eax       //Initialize eax to 0  
    rep stos [t + ecx] //Store the value of eax in t[i] where i = ecx

另一个等效的汇编变体可能是:

    mov ecx, MAX_CHAR   //Initialize ecx to the number of iterations
    xor eax, eax        //Initialize eax to 0
    loop0:              //Define loop label
    mov [t + ecx], eax  //Copy eax into t[i] where i = ecx 
    dec ecx             //Decrement ecx ==> ecx = ecx - 1
    jnz loop0           //Jump only if previous result (ecx - 1) isn't 0

这两个汇编代码完全相似。唯一的区别是,在周期方面,一个会比另一个花费更多。换句话说,一个比另一个快。如何定义哪个是最好的?如果您查看第 162 页的Agner Fog 指令表,您会注意到在最坏的情况下REP STOS延迟为n(n 是迭代次数)。如果您查看指令表,您会发现XOR REG, SAME将花费我们 0.25 个周期,而MOV R, IMM将花费我们 0.5 个周期。总的来说,第一个汇编代码的性能可以评估为:latency = n + 0.75周期(对于 1000 次迭代 ~ 1000.75 个周期)。如果我们查看第二个汇编代码,我们会得到以下内容:

    mov ecx, MAX_CHAR   //0.50 cycles
    xor eax, eax        //0.25 cycles
    loop0:              
    mov [t + ecx], eax  //1 cycle 
    dec ecx             //2 cycles
    jnz loop0           //1 cycle

在这种情况下,我们得到延迟 = 4 * n + 0.75

现在,您可能认为第一个代码比第二个代码快,因为n < 4n不要忘记英特尔架构是流水线的,还有一些其他的奇怪之处。不过,我可以向您保证,第一个代码与第二个代码相比非常慢。为什么?代表斯托斯是微编码的(Fused µOps 列)。意思是,它不是真正的硬件指令。它本质上是通过将多个电路(此处为两个)组合而成的,这种方式在过去可以 - 使用 Pentium III 和 IV - 节省您的时间。这种方法的问题在于它在管道中并不适用。第二个汇编代码会快得多,因为所有指令都是硬编码的。这意味着,每个电路都有一个特殊的电路,不需要组合其他电路来执行所需的任务。因此,这些指令可以很容易地流水线化,因此计算出的4n总延迟可以远低于n(如果一切都被正确缓存)。对于所有三个指令,整个循环迭代将下降到大约 0.75 个周期。加上分支预测器正确命中时的奖励。

我必须提醒您,这些指令是在CPU难以有效运行循环构造的时候出现的。特别是对于字符串处理,因为分支太多,缓存太小。随着高效的分支预测器和更大/更复杂的缓存系统的出现,这些指令不知何故变得过时了。很少有编译器会根据REP STOSREP LODS生成代码,除非您在一些非常旧的CPU上运行代码或使用非常旧的编译器(例如Turbo C)。

我希望我的帖子有助于阐明这些说明以及如何使用它们。如果您需要更多详细信息或更多解释,请告诉我。

通常在memcpy()memset()等操作期间类似于下面的代码:

#define BUF_SIZE 1024

wchar _t foo[BUF_SIZE]  = "yyyyyyyyy";
wchar _t blah[BUF_SIZE] = {0};   
wchar _t bar[BUF_SIZE];

memcpy(blah, BUF_SIZE, foo);
memset(bar, BUF_SIZE, 0);

更新

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#pragma intrinsic (memset , memcpy)
void somefunc() { 
  char *foo = "yabba dabba choo";
  char *blah = (char *)malloc(0x20);
  if(blah) {
    memset(blah,0,0x20);  <<-- line no 9
    memcpy(blah,foo,0x20);
    printf("%s\n" , blah);
  }
}
void main (void) {
  somefunc();
}

编译/O1优化vc++

cl /nologo /Zi /W4 /O1 /analyze /EHsc /Ferepstos__opt.exe repstos.cpp  /link /RELEASE

拆卸

cdb -c "uf repstos__opt!somefunc;q" repstos__opt.exe | grep initial -A 40

0:000> cdb: Reading initial command 'uf repstos__opt!somefunc;q'
repstos__opt!somefunc:
010a1261 6a20            push    20h
010a1263 e8c69a0100      call    repstos__opt!malloc (010bad2e)
010a1268 8bd0            mov     edx,eax
010a126a 59              pop     ecx
010a126b 85d2            test    edx,edx
010a126d 7426            je      repstos__opt!somefunc+0x34 (010a1295)

repstos__opt!somefunc+0xe:
010a126f 56              push    esi
010a1270 57              push    edi
010a1271 6a08            push    8
010a1273 59              pop     ecx
010a1274 33c0            xor     eax,eax
010a1276 8bfa            mov     edi,edx
010a1278 f3ab            rep stos dword ptr es:[edi] <<----
010a127a 6a08            push    8
010a127c 59              pop     ecx
010a127d 8bfa            mov     edi,edx
010a127f beb0c10d01      mov     esi,offset repstos__opt!`string' (010dc1b0)
010a1284 52              push    edx
010a1285 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
010a1287 68c4c10d01      push    offset repstos__opt!`string' (010dc1c4)
010a128c e836000000      call    repstos__opt!printf (010a12c7)
010a1291 59              pop     ecx
010a1292 59              pop     ecx
010a1293 5f              pop     edi
010a1294 5e              pop     esi

repstos__opt!somefunc+0x34:
010a1295 c3              ret
quit:

9号线拆解

0:000> lsa .,4,2
     9:     memset(blah,0,0x20);
    10:     memcpy(blah,foo,0x20);
0:000> u `repstos__opt!repstos.cpp:9`
repstos__opt!somefunc+0xe [xxxx\repstos.cpp @ 9]:
002c126f 56              push    esi
002c1270 57              push    edi
002c1271 6a08            push    8
002c1273 59              pop     ecx
002c1274 33c0            xor     eax,eax
002c1276 8bfa            mov     edi,edx
002c1278 f3ab            rep stos dword ptr es:[edi] <<<---
002c127a 6a08            push    8
0:000>