Как GCC реализует массивы переменной длины?

Как GCC реализует массивы переменной длины (VLA)? Являются ли такие массивы по существу указателями на динамически распределенное хранилище, например, возвращенными alloca?

Другим вариантом, о котором я мог думать, является то, что такой массив выделяется как последняя переменная в функции, так что смещение переменных известно во время компиляции. Тем не менее, смещение второй VLA снова не будет известно во время компиляции.

Ответы

Ответ 1

Здесь код распределения (x86 - код x64 похож) для следующей примерной строки, взятой из некоторых документов GCC для поддержки VLA:

char str[strlen (s1) + strlen (s2) + 1];

где расчет для strlen (s1) + strlen (s2) + 1 находится в eax (GCC MinGW 4.8.1 - без оптимизации):

mov edx, eax
sub edx, 1
mov DWORD PTR [ebp-12], edx
mov edx, 16
sub edx, 1
add eax, edx
mov ecx, 16
mov edx, 0
div ecx
imul    eax, eax, 16
call    ___chkstk_ms
sub esp, eax
lea eax, [esp+8]
add eax, 0
mov DWORD PTR [ebp-16], eax

Итак, это выглядит по существу alloca().

Ответ 2

Ну, это всего лишь несколько диких ударов в темноте, основанных на ограничениях вокруг VLA, но в любом случае:

VLA не может быть:

  • ехЬегп
  • элементы структуры
  • статичным
  • объявлен с неопределенными границами (за исключением прототипа функции)

Все это указывает на то, что VLA выделяется в стеке, а не в кучу. Так что да, VLA, вероятно, являются последними кусками памяти стека, выделенными всякий раз, когда выделяется новый блок (как в блочной области, это петли, функции, ветки или что-то еще). Это также объясняет, почему VLA увеличивает риск, в некоторых случаях значительно (слово предупреждения: даже не думайте об использовании VLA в сочетании с рекурсивными вызовами функций, например!). Это также приводит к тому, что доступ к ограниченному доступу может вызвать проблемы: после завершения блока все, что указывает на то, что было памятью VLA, указывает на недопустимую память.
Но на стороне плюса: это также почему эти массивы являются потокобезопасными, хотя (из-за потоков, имеющих собственный стек) и почему они быстрее по сравнению с кучей памяти.

Размер VLA не может быть:

  • Значение extern
  • ноль или отрицательный

внешнее ограничение довольно очевидно, как и ненулевое, неотрицательное... однако: если переменная, которая задает размер VLA, является подписанным int, например, компилятор не будет выдает ошибку: оценка и, следовательно, распределение VLA выполняется во время выполнения, а не во время компиляции. Следовательно, размер VLA не может и не обязательно должен быть задан во время компиляции.
Как справедливо указал MichaelBurr, VLA очень похожи на память alloca, с одним, IMHO, решающим различием: память, выделенная alloca, действительна с точки выделения и во всей остальной функции. VLA являются блочными областями, поэтому память освобождается после выхода из блока, в котором используется VLA:

void alloca_diff( void )
{
    char *alloca_c, *vla_c;
    for (int i=1;i<10;++i)
    {
        char *alloca_mem = alloca(i*sizeof(*alloca_mem));
        alloca_c = alloca_mem;//valid
        char vla_arr[i];
        vla_c = vla_arr;//invalid
    }//end of scope, VLA memory is freed
    printf("alloca: %c\n", *alloca_c);//fine
    printf("vla: %c\n\", *vla_c);//undefined behaviour... avoid!
}//end of function alloca memory is freed, irrespective of block scope