Понимание базовых указателей и указателей стека: в контексте с выходом gcc

У меня есть следующая программа на C:

int main()
{
    int c[10] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 2};
    return c[0];
}

и при компиляции с использованием -S директивы с gcc я получаю следующую сборку:

    .file   "array.c"
    .text
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $0, -48(%rbp)
    movl    $0, -44(%rbp)
    movl    $0, -40(%rbp)
    movl    $0, -36(%rbp)
    movl    $0, -32(%rbp)
    movl    $0, -28(%rbp)
    movl    $0, -24(%rbp)
    movl    $0, -20(%rbp)
    movl    $1, -16(%rbp)
    movl    $2, -12(%rbp)
    movl    -48(%rbp), %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (GNU) 4.4.5 20110214 (Red Hat 4.4.5-6)"
    .section        .note.GNU-stack,"",@progbits

Я не понимаю, почему более ранние элементы массива дальше от bp? Похоже, что элементы массива размещаются в противоположном порядке.

Также почему gcc не использует push вместо movl, чтобы вставить элементы массива в стек?


РАЗЛИЧНЫЙ ВЗГЛЯД

Перемещение массива в глобальное пространство имен в качестве статической переменной в модуль, который я получаю:

    .file   "array.c"
    .data
    .align 32
    .type   c, @object
    .size   c, 40
c:
    .long   0
    .long   0
    .long   0
    .long   0
    .long   0
    .long   0
    .long   0
    .long   0
    .long   1
    .long   2
    .text
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    c(%rip), %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (GNU) 4.4.5 20110214 (Red Hat 4.4.5-6)"
    .section    .note.GNU-stack,"",@progbits

Используя следующую программу C:

static int c[10] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 2};

int main() 
{
    return c[0];
}

Это не дает более глубокого понимания стеку. Но это интересно видеть разностный вывод сборки, используя немного другую семантику.

Ответ 1

Прежде всего, стек x86 растет вниз. По соглашению rbp сохраняет исходное значение rsp. Следовательно, аргументы функции находятся в положительных смещениях относительно rbp, а его автоматические переменные находятся в отрицательных смещениях. Первый элемент автоматического массива имеет более низкий адрес, чем все остальные элементы, и, следовательно, он наиболее удален от rbp.

Вот удобная диаграмма, которая появляется на этой странице:

stack layout

Я не вижу причин, по которым компилятор не мог использовать серию инструкций push для инициализации вашего массива. Будет ли это хорошей идеей, я не уверен.

Ответ 2

Также почему gcc не использует push вместо movl, чтобы вставить элементы массива в стек?

Довольно редко есть большой инициализированный массив в нужном месте в стеке, в котором вы можете использовать последовательность нажатий, поэтому gcc не научился это делать. (Более подробно: инициализация массива обрабатывается как копия памяти блока, которая испускается как последовательность команд перемещения или вызов memcpy, в зависимости от того, насколько она будет большой. Код, который решает, что испускать, t знать, где в памяти идет блок, поэтому он не знает, может ли он использовать push.)

Кроме того, movl работает быстрее. В частности, push выполняет неявное чтение-изменение-запись %esp, и поэтому последовательность push es должна выполняться по порядку. movl к независимым адресам, напротив, может выполняться параллельно. Поэтому, используя последовательность movl, а не push es, gcc предлагает процессору более уровень parallelism на уровне инструкций.

Обратите внимание, что если я скомпилирую ваш код с активированным уровнем оптимизации, массив исчезнет совсем! Здесь -O1 (это результат запуска objdump -dr в объектном файле, а не -S вывода, так что вы можете увидеть фактический машинный код)

0000000000000000 <main>:
   0:   b8 00 00 00 00          mov    $0x0,%eax
   5:   c3                      retq   

и -Os:

0000000000000000 <main>:
   0:   31 c0                   xor    %eax,%eax
   2:   c3                      retq   

Ничего не делать всегда быстрее, чем что-то делать. Очистка регистра с помощью xor составляет два байта вместо пяти, но имеет формальную зависимость от старого содержимого регистра и изменяет коды условий, поэтому может быть медленнее и поэтому выбирается только при оптимизации размера.

Ответ 3

Имейте в виду, что на x86 стек растет вниз. Нажатие на стек вычитается из указателя стека.

%rbp <-- Highest memory address
-12
-16
-20
-24
-28
-32
-36
-40
-44
-48  <-- Address of array