c - 为什么编译器在初始化一个volatile数组的时候会生成这样的代码呢?

标签 c assembly compilation

我有以下程序启用 x86 处理器标志寄存器中的对齐检查 (AC) 位,以捕获未对齐的内存访问。然后程序声明了两个volatile变量:

#include <assert.h>

int main(void)
{
    #ifndef NOASM
    __asm__(
        "pushf\n"
        "orl $(1<<18),(%esp)\n"
        "popf\n"
    );
    #endif

    volatile unsigned char foo[] = { 1, 2, 3, 4, 5, 6 };
    volatile unsigned int bar = 0xaa;
    return 0;
}

如果我编译它,最初生成的代码会执行 显而易见的事情,例如设置堆栈和通过将值 1、2、3、4、5、6 移动到堆栈来创建字符数组:

/tmp ➤ gcc test3.c -m32
/tmp ➤ gdb ./a.out
(gdb) disassemble main
   0x0804843d <+0>: push   %ebp
   0x0804843e <+1>: mov    %esp,%ebp
   0x08048440 <+3>: and    $0xfffffff0,%esp
   0x08048443 <+6>: sub    $0x20,%esp
   0x08048446 <+9>: mov    %gs:0x14,%eax
   0x0804844c <+15>:    mov    %eax,0x1c(%esp)
   0x08048450 <+19>:    xor    %eax,%eax
   0x08048452 <+21>:    pushf
   0x08048453 <+22>:    orl    $0x40000,(%esp)
   0x0804845a <+29>:    popf
   0x0804845b <+30>:    movb   $0x1,0x16(%esp)
   0x08048460 <+35>:    movb   $0x2,0x17(%esp)
   0x08048465 <+40>:    movb   $0x3,0x18(%esp)
   0x0804846a <+45>:    movb   $0x4,0x19(%esp)
   0x0804846f <+50>:    movb   $0x5,0x1a(%esp)
   0x08048474 <+55>:    movb   $0x6,0x1b(%esp)
   0x08048479 <+60>:    mov    0x16(%esp),%eax
   0x0804847d <+64>:    mov    %eax,0x10(%esp)
   0x08048481 <+68>:    movzwl 0x1a(%esp),%eax
   0x08048486 <+73>:    mov    %ax,0x14(%esp)
   0x0804848b <+78>:    movl   $0xaa,0xc(%esp)
   0x08048493 <+86>:    mov    $0x0,%eax
   0x08048498 <+91>:    mov    0x1c(%esp),%edx
   0x0804849c <+95>:    xor    %gs:0x14,%edx
   0x080484a3 <+102>:   je     0x80484aa <main+109>
   0x080484a5 <+104>:   call   0x8048310 <__stack_chk_fail@plt>
   0x080484aa <+109>:   leave
   0x080484ab <+110>:   ret

但是在 main+60 处它做了一些奇怪的事情:它将 6 字节的数组移动到堆栈的另一部分:数据在寄存器中一次移动一个 4 字节的字。但字节从偏移量 0x16 开始,未对齐,因此程序在尝试执行 mov 时会崩溃。

所以我有两个问题:

  1. 为什么编译器发出代码将数组复制到堆栈的另一部分?我假设 volatile 会跳过所有优化并始终执行内存访问。也许 volatile vars 需要始终作为整个单词访问,因此编译器将始终使用临时寄存器来读/写整个单词?

  2. 如果编译器稍后打算执行这些 mov 调用,为什么它不将 char 数组放在对齐的地址?我知道 x86 对于未对齐的访问通常是安全的,并且在现代处理器上它甚至不会带来性能损失;但是在所有其他情况下,我看到编译器试图避免生成未对齐的访问,因为据我所知,它们被认为是 C 中的未指定行为。我的猜测是,因为后来它为堆栈上复制的数组提供了一个正确对齐的指针,它只是不关心仅用于以 C 程序不可见的方式进行初始化的数据的对齐方式?

如果我上面的假设是正确的,这意味着我不能期望 x86 编译器总是生成对齐访问,即使编译后的代码本身从不尝试执行未对齐访问,因此设置 Acflags不是一个实用的方法检测执行未对齐访问的代码部分。

编辑:经过进一步研究,我可以自己回答大部分问题。为了取得进展,我在 Redis 中添加了一个选项来设置 Acflags,否则可以正常运行。我发现这种方法不可行:进程立即在 libc: __mempcpy_sse2 () at ../sysdeps/x86_64/memcpy.S:83 内部崩溃。我假设整个 x86 软件栈根本不关心错位,因为这个架构处理得很好。因此,在设置 Acflags的情况下运行是不切实际的。

所以上面问题 2 的答案是,就像软件栈的其余部分一样,编译器可以随心所欲地做它想做的事情,并在不关心对齐的情况下重新定位栈上的东西,只要行为是正确的C程序的视角。

唯一需要回答的问题是,为什么使用 volatile 时,副本是在堆栈的不同部分制作的?我最好的猜测是,即使在初始化期间,编译器也试图访问声明为 volatile 的变量中的整个单词(假设该地址映射到 I/O 端口),但我不确定。

最佳答案

您在没有优化的情况下进行编译,因此编译器会生成直接的代码,而不必担心它的效率有多低。所以它首先创建初始化器 { 1, 2, 3, 4, 5, 6 }在堆栈的临时空间中,然后将其复制到为 foo 分配的空间中.

关于c - 为什么编译器在初始化一个volatile数组的时候会生成这样的代码呢?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42317329/

相关文章:

c++ - 从 C++ 调用 Fortran 子例程,链接时 undefined reference

c - 为什么编译C需要这么长时间?

c - fork 进程与套接字通信

c - 帮助此代码(给出 STATUS_ACCESS_VIOLATION )

assembly - x86组件: Why Do I Need Stack Frames?

algorithm - 检查两 block 瓷砖是否接触并在 NASM 组装中相邻

java - 我在 Netbeans 中的 java 项目变得很慢

c - 使用函数从文件中读取动态数组

无法打开输出文件,权限被拒绝错误 : Id returned 1 exit status

assembly - 难倒在 LC-3 组装的扩展乘法上