Что делает компилятор с [i], который является массивом? А что, если a - указатель?

Мне сказали c-faq, что компилятор делает разные вещи для работы с [i], а a - это массив или указатель, Вот пример из c-faq:

char a[] = "hello";
char *p = "world";

Учитывая вышеприведенные объявления, когда компилятор видит выражение a [3], он испускает код для начала в местоположении `` a '', перемещает три мимо него и выбирает там символ. Когда он видит выражение p [3], он испускает код для начала в месте "p", извлекает значение указателя там, добавляет три к указателю и, наконец, извлекает символ, на который указывает.

Но мне сказали, что при работе с [i] компилятор стремится преобразовать (который является массивом) в указатель на массив. Поэтому я хочу видеть коды сборки, чтобы узнать, что правильно.

EDIT:

Здесь источник этого утверждения. c-faq И обратите внимание на это предложение:

выражение формы a [i] заставляет массив распадаться на указатель, следуя приведенному выше правилу, а затем индексироваться так же, как и указательная переменная в выражении p [i] (хотя возможная память доступ будет иным, "

Я довольно смущен этим: поскольку a заглох на указатель, то почему он имеет в виду, что "доступ к памяти будет другим?"

Здесь мой код:

// array.cpp
#include <cstdio>
using namespace std;

int main()
{
    char a[6] = "hello";
    char *p = "world";
    printf("%c\n", a[3]);
    printf("%c\n", p[3]);
}

А вот часть кода сборки, которую я получил, использовал g++ -S array.cpp

    .file   "array.cpp" 
    .section    .rodata
.LC0:
    .string "world"
.LC1:
    .string "%c\n"
    .text
.globl main
    .type   main, @function
main:
.LFB2:
    leal    4(%esp), %ecx
.LCFI0:
    andl    $-16, %esp
    pushl   -4(%ecx)
.LCFI1:
    pushl   %ebp
.LCFI2:
    movl    %esp, %ebp
.LCFI3:
    pushl   %ecx
.LCFI4:
    subl    $36, %esp
.LCFI5:
    movl    $1819043176, -14(%ebp)
    movw    $111, -10(%ebp)
    movl    $.LC0, -8(%ebp)
    movzbl  -11(%ebp), %eax
    movsbl  %al,%eax
    movl    %eax, 4(%esp)
    movl    $.LC1, (%esp)
    call    printf
    movl    -8(%ebp), %eax
    addl    $3, %eax
    movzbl  (%eax), %eax
    movsbl  %al,%eax
    movl    %eax, 4(%esp)
    movl    $.LC1, (%esp)
    call    printf
    movl    $0, %eax
    addl    $36, %esp
    popl    %ecx
    popl    %ebp
    leal    -4(%ecx), %esp
    ret 

Я не могу понять механизм [3] и p [3] из приведенных выше кодов. Например:

  • где был "привет" инициализирован?
  • Что означает $1819043176? возможно, это адрес памяти "привет" (адрес a)?
  • Я уверен, что "-11 (% ebp)" означает [3], но почему?
  • В "movl -8 (% ebp),% eax", содержимое poniter p сохраняется в EAX, правильно? Таким образом, $.LC0 означает содержимое указателя p?
  • Что означает "movsbl% al,% eax"?
  • И, обратите внимание на эти 3 строки кодов:
      movl $1819043176, -14 (% ebp)
      movw $111, -10 (% ebp)
      movl $.LC0, -8 (% ebp)

    Последний использует "movl", но почему он не перезаписал контент -10 (% ebp)? (Теперь я знаю anser:), адрес является инкрементным, и "movl $.LC0 -8 (% ebp) будет только перезаписывать {-8, -7, -6, -5} (% ebp))

Извините, но я полностью смущен этим механизмом, а также код сборки...

Большое спасибо за вашу помощь.

Ответ 1

a - указатель на массив символов. p - это указатель на char, который, в данном случае, указывает на строковый литерал.

movl    $1819043176, -14(%ebp)
movw    $111, -10(%ebp)

Инициализирует локальный "привет" в стеке (поэтому на него ссылается через ebp). Поскольку в приветствии содержится более 4 байтов, требуется две инструкции.

movzbl  -11(%ebp), %eax
movsbl  %al,%eax

Ссылки a[3]: двухэтапный процесс связан с ограничением в отношении доступа к памяти, на которую ссылается хотя бы ebp (мой x86-fu немного ржавый).

movl -8(%ebp), %eax действительно ссылается на указатель p.

LC0 ссылается на местоположение "относительной памяти": фиксированная ячейка памяти будет выделена после загрузки программы в память.

movsbl %al,%eax означает: "перемещайте один байт, ниже" (дайте или возьмите... Мне нужно будет посмотреть его... Я немного ржавый на этом фронте). al представляют собой байты из регистра eax.

Ответ 2

Поступая на стороне языка, поскольку сторона ассемблера уже обработана:

Обратите внимание на это предложение: "выражение формы a [i] приводит к распаду массива в указатель, следуя приведенному выше правилу, а затем индексируется так же, как и указательная переменная в выражении p [i] (хотя возможные обращения к памяти будут разными:" Я довольно смущен этим: поскольку a заглох на указатель, то почему он имеет в виду, что "обращения к памяти будут разными?"

Это происходит потому, что после разложения доступ равен для (теперь значения указателя) и указателя. Но разница в том, как именно это значение указателя получает в первую очередь. Рассмотрим пример:

char c[1];

char cc;
char *pc = &cc;

Теперь у вас есть массив. Этот массив не принимает никакого хранилища, кроме одного char! Для него нет указателя. И у вас есть указатель, указывающий на char. Указатель принимает размер одного адреса, и у вас есть один char, на который указывает указатель. Теперь посмотрим, что произойдет для случая массива, чтобы получить значение указателя:

c[0] = 'A';
// #1: equivalent: *(c + 0) = 'A';
// #2: => 'c' appears not in address-of or sizeof 
// #3: => get address of "c": This is the pointer value P1

Случай с указателем отличается:

pc[0] = 'A';
// #1: equivalent: *(pc + 0) = 'A';
// #2: => pointer value is stored in 'pc'
// #3: => thus: read address stored in 'pc': This is the pointer value P1

Как вы видите, для случая массива для получения значения указателя, в котором мы добавляем значение индекса (в данном случае скучное 0), нам не нужно читать из памяти, потому что адрес array уже является значением указателя. Но для случая указателя нужный нам указатель хранится в указателе: нам нужен один из памяти, чтобы получить этот адрес.

После этого путь равен для обоих:

// #4: add "0 * sizeof(char)" to P1. This is the address P2
// #5: store 'A' to address P2

Вот код ассемблера, сгенерированный для массива и case-указателя:

        add     $2, $0, 65  ; write 65 into r2
        stb     $2, $0, c   ; store r2 into address of c
# pointer case follows
        ldw     $3, $0, pc  ; load value stored in pc into r3
        add     $2, $0, 65  ; write 65 into r2
        stb     $2, $3, 0   ; store r2 into address loaded to r3

Мы можем просто сохранить 65 (ASCII для 'A') по адресу c (который будет известен уже во время компиляции или ссылки, когда он является глобальным). Для случая с указателем сначала нужно загрузить адрес, хранящийся в нем, в регистр 3, а затем записать 65 на этот адрес.

Ответ 3

Хотя верно, что массивы не являются указателями, они ведут себя очень похоже. В обоих случаях компилятор внутренне сохраняет адрес в типизированном элементе, и в обоих случаях может быть один или несколько элементов.

В обоих массивах и указателях при разыменовании оператором [] компилятор оценивает адрес элемента, который вы индексируете, умножая индекс на размер типа данных и добавляя его к адресу указателя или массив.

Фундаментальное различие между указателем и массивами состоит в том, что массив по существу является ссылкой. Если законным является инициализация указателя на нуль или изменение значения, которое хранит указатель, массивы не могут быть нулевыми, и они не могут быть установлены в другие массивы; они по сути являются постоянными указателями, которые не могут быть установлены в нуль.

Кроме того, можно выделять массивы в стеке, и это невозможно для указателей (хотя указатели могут быть установлены на адреса в стеке, но это может стать уродливым).

Ответ 4

Эти определения выглядят одинаково, но на самом деле совершенно разные.

Предположим, что ваши массивы объявлены внутри функции:
void f()
{
    char a[] = "hello";
    char *p = "world";
}

В первом случае 'a' распадается на указатель const, который указывает на 6 символов в STACK. Во втором случае "p" является неконстантным указателем, который указывает на 6 символов в области CONST (сегмент данных).

Его вполне законно писать:

a[3] = 'L';

но

p[3] = 'L';

выглядит корректно, но вызывает нарушение памяти, потому что массив символов находится не в стеке, а в разделе только для чтения.

Кроме того,

a++

является незаконным ('a' распадается на указатель const, который является r-значением), но

p++

является законным (p является l-значением).