从数组0初始化奇怪的汇编

时间:2009-02-10 08:30:16

标签: c++ c compiler-construction assembly

受问题Difference in initalizing and zeroing an array in c/c++ ?的启发,我决定实际检查一下针对Windows Mobile Professional(ARM处理器,来自Microsoft Optimizing Compiler)的优化发布版本的程序集。我发现的有点令人惊讶,我想知道是否有人可以对我的问题有所了解。

检查了这两个例子:

byte a[10] = { 0 };

byte b[10];
memset(b, 0, sizeof(b));

它们在同一个函数中使用,因此堆栈如下所示:

[ ] // padding byte to reach DWORD boundary
[ ] // padding byte to reach DWORD boundary
[ ] // b[9] (last element of b)
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ] // b[0] = sp + 12 (stack pointer + 12 bytes)
[ ] // padding byte to reach DWORD boundary
[ ] // padding byte to reach DWORD boundary
[ ] // a[9] (last element of a)
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ]
[ ] // a[0] = sp (stack pointer, at bottom)

生成的程序集带有我的评论:

; byte a[10] = { 0 };

01: mov   r3, #0        // r3 = 0
02: mov   r2, #9        // 3rd arg to memset: 9 bytes, note that sizeof(a) = 10
03: mov   r1, #0        // 2nd arg to memset: 0-initializer
04: add   r0, sp, #1    // 1st arg to memset: &a[1] = a + 1, since only 9 bytes will be set
05: strb  r3, [sp]      // a[0] = r3 = 0, sets the first element of a
06: bl    memset        // continue in memset

; byte b[10];
; memset(b, 0, sizeof(b));

07: mov   r2, #0xA      // 3rd arg to memset: 10 bytes, sizeof(b)
08: mov   r1, #0        // 2nd arg to memset: 0-initializer
09: add   r0, sp, #0xC  // 1st arg to memset: sp + 12 bytes (the 10 elements
                        // of a + 2 padding bytes for alignment) = &b[0]
10: bl    memset        // continue in memset

现在,有两件事令我困惑:

  1. 第02和05行有什么意义?为什么不给memset一个[0]和10个字节?
  2. 为什么0初始化的填充字节不是?这只是结构中的填充吗?
  3. 编辑:我太好奇了,不想测试结构案例:

    struct Padded
    {
        DWORD x;
        byte y;
    };
    

    用于0初始化它的汇编程序:

    ; Padded p1 = { 0 };
    
    01: mov   r3, #0
    02: str   r3, [sp]
    03: mov   r3, #0
    04: str   r3, [sp, #4]
    
    ; Padded p2;
    ; memset(&p2, 0, sizeof(p2));
    
    05: mov   r3, #0
    06: str   r3, [sp]
    07: andcs r4, r0, #0xFF
    08: str   r3, [sp, #4]
    

    这里我们在第04行看到填充确实发生,因为使用str(而不是strb)。正确?

3 个答案:

答案 0 :(得分:14)

第2行和第5行的原因是因为您在数组初始值设定项中指定了0。编译器将初始化所有常量,然后使用memset填充其余常量。如果要在初始化程序中放置两个零,则会看到它是strw(字而不是字节)然后是memset 8字节。

对于填充,它仅用于对齐内存访问 - 在正常情况下不应使用数据,因此将其设置为浪费。

编辑:为了记录,我可能错误地认为上面的strw假设。 99%的ARM经验都是在iPhone上反转GCC / LLVM生成的代码,所以我的假设可能无法延续到MSVC。

答案 1 :(得分:11)

这两段代码都是无错误的。提到的两行并不聪明,但你只是证明这个编译器发出了次优代码。

填充字节通常只在初始化时才会简化程序集或加速代码。例如,如果在两个零填充成员之间填充,则通常也更容易对填充进行零填充。此外,如果最后有填充,并且memset()针对多字节写入进行了优化,则覆盖该填充也可能更快。

答案 2 :(得分:8)

一些快速测试表明,如果初始化程序列表为空,则Microsoft的x86编译器生成不同的程序集,而不是它包含零。也许他们的ARM编译器也是如此。如果你这样做会怎么样?

byte a[10] = { };

这是我得到的程序集列表(在Visual Studio 2008上使用选项/EHsc /FAs /O2)。请注意,在初始化列表中包含零会导致编译器使用未对齐的内存访问来初始化数组,而空的初始化列表版本和memset()版本都使用对齐的内存访问:

; unsigned char a[10] = { };

xor eax, eax
mov DWORD PTR _a$[esp+40], eax
mov DWORD PTR _a$[esp+44], eax
mov WORD PTR _a$[esp+48], ax

; unsigned char b[10] = { 0 };

mov BYTE PTR _b$[esp+40], al
mov DWORD PTR _b$[esp+41], eax
mov DWORD PTR _b$[esp+45], eax
mov BYTE PTR _b$[esp+49], al

; unsigned char c[10];
; memset(c, 0, sizeof(c));

mov DWORD PTR _c$[esp+40], eax
mov DWORD PTR _c$[esp+44], eax
mov WORD PTR _c$[esp+48], ax