Как C99 справляется с невозможностью создания массива переменной длины во время выполнения?

Программирую встроенные системы. Одно золотое правило заключается в том, что мы никогда не вызываем malloc(); все данные должны быть статически размещены во время компиляции.

Следовательно, я не совсем знаком с массивами переменной длины, которые были представлены в C99.

Концепция кажется достаточно ясной, и мне не нужно ее объяснять. Мой вопрос: что происходит во время выполнения, если для такого массива недостаточно свободной памяти?

Я бы предположил, что это зависит от операционной системы, может быть, от компилятора, от того, что будет делать GCC/Linux и MS Visual Studio C в Windows? Любые определения X99 или Posix?


person Mawg says reinstate Monica    schedule 03.01.2017    source источник
comment
Мауг, я думаю, ты сам ответил на свой вопрос. Это приведет к неопределенному поведению, поэтому вам нужно посмотреть, как данная платформа, для которой вы разрабатываете, справляется с этим. Я предполагаю, что вам придется попытаться зафиксировать это, прежде чем вызывать функцию, использующую VLA. Я предполагаю, что если malloc следует избегать, то и VLA следует избегать.   -  person cdcdcd    schedule 15.01.2017
comment
Применяются те же правила, что и для любого переполнения стека, что может привести к неконтролируемому сбою на небольших встроенных микроконтроллерах и захвату с помощью защиты памяти на больших устройствах (включая Linux / Windows). Более того, выделение стека точки также является динамическим распределением, и поэтому его нельзя избежать в C. Лучшее, что вы можете сделать, это использовать встроенные компиляторы с поддержкой вычисления верхней границы стека для графа вызовов вашей программы, если вы избегаете указателей функций/ recursion/VLAs/alloca (или, возможно, предоставить подходящие ручные подсказки, скажем, списки возможных целей указателя функции).   -  person doynax    schedule 15.01.2017
comment
doynax, здесь я могу ошибаться, но на многих платформах/средах стек фиксируется для каждого процесса во время компиляции. Например, очень легко исчерпать стек в окнах (но это не означает, что он не является динамическим, просто верхний предел является статическим), но говоря, что вы можете обойти проблему, запустив новый поток с произвольное пространство стека.   -  person cdcdcd    schedule 15.01.2017
comment
@cdcdcd: Да, но только до доступной памяти. Вопрос касается встроенного устройства, поэтому, по-видимому, это пространство довольно ограничено, и, что еще хуже, может быть полностью непроверено в зависимости от конкретной платформы. Обычно требуется, чтобы такие системы (ваш тостер или что-то еще) были спроектированы таким образом, чтобы никогда не заканчивалась память, и в этом случае с распределением стека нужно обращаться осторожно. Номинально те же правила применимы и к Windows/Linux, но, поскольку в самой ОС отсутствуют такие гарантии распределения, вы вынуждены соглашаться на решения с максимальной эффективностью.   -  person doynax    schedule 15.01.2017
comment
дойнакс - точка взята. Просто добавил точку в качестве предостережения.   -  person cdcdcd    schedule 15.01.2017
comment
VLA обычно (зависит от компилятора) выделяет память из стека, аналогично alloca() или _alloca(), и в этом случае отсутствие кучи или malloc() не является проблемой. Во встроенной системе пространство стека (на поток) может быть очень ограничено.   -  person rcgldr    schedule 15.01.2017
comment
Я этого не знал (+1). Итак, если я создам такой массив при включении питания, он будет постоянно потреблять часть моего стека. Как вы говорите, наверное, лучше избегать.   -  person Mawg says reinstate Monica    schedule 15.01.2017


Ответы (2)


С точки зрения Стандарта, попытка выделить VLA с размером, который реализация не может вместить, вызывает Undefined Behavior. Поскольку стандарт не предоставляет средств для определения того, какой размер массива может безопасно создать реализация, и не требует, чтобы реализации допускали какой-либо конкретный размер, любая попытка создать объект VLA с размером больше 1 должна рассматриваться как вызов неопределенного поведения, за исключением случаях, когда кто-то знает достаточно о внутренней работе реализации, чтобы определить размер VLA, с которым он сможет справиться.

Если malloc() недоступен, лучше всего определить большой массив любого типа с самым грубым требованием выравнивания, сохранить его адрес в указателе с квалификацией volatile [хранилище, в котором находится сам указатель, должно быть квалифицировано таким образом] читать обратно и интерпретировать это как начало пула памяти. Никакое другое использование исходного объекта массива не допускается. Хотя стандарт не гарантирует, что компилятор не решит, что он должен генерировать код, который проверяет, идентифицирует ли указатель исходный объект, и, если это так, пропускает любой код, который будет использовать этот указатель для доступа к чему-либо, кроме исходного типа объекта, использование volatile в указателе должно сделать это маловероятным.

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

person supercat    schedule 09.01.2017
comment
все данные должны размещаться статически во время компиляции означает, что все данные должны размещаться статически во время компиляции. Вы не можете выделять свои собственные данные во время выполнения, если вы используете для этого свои собственные функции. - person n. 1.8e9-where's-my-share m.; 15.01.2017
comment
@nm: массив будет выделен во время компиляции; пользовательские функции будут просто возвращать указатели на части уже выделенного хранилища. - person supercat; 15.01.2017
comment
Распределение не означает, что нужно просить у ОС, компоновщика или Бога какой-либо ресурс. Это не обязательно одноуровневое дело. Компилятор выделяет кусок памяти для вашей программы (статически), затем ваш менеджер памяти использует этот кусок для выделения меньших кусков (динамически). - person n. 1.8e9-where's-my-share m.; 15.01.2017
comment
supercat, вы в основном говорите, что если вам нужен тип динамического распределения, не используйте VLA или malloc (они небезопасны). Вместо этого статически выделяйте ресурсы, которые, как вы знаете, безопасны во время компиляции (вы знаете ограничения платформы), затем напишите свою собственную версию TLB для этого ресурса и управляйте им. - person cdcdcd; 15.01.2017
comment
Кто обещал, что размер 1 обязательно будет успешным? Я бы не стал ему доверять. - person n. 1.8e9-where's-my-share m.; 15.01.2017
comment
@cdcdcd: В основном. Правила псевдонимов в Стандарте делают абсурдно трудным написание таких менеджеров таким образом, чтобы реализациям было запрещено ошибаться - вероятно, потому, что авторы компиляторов имели здравый смысл, чтобы не нарушать такие вещи, даже когда Стандарт разрешал это. Сегодняшний менталитет таков, что если Стандарт позволяет компилятору навязывать дурацкую оптимизацию с нарушением кода, это означает, что код был взломан. - person supercat; 15.01.2017
comment
@cdcdcd: если можно убедить компилятор гарантировать, что он не будет пытаться применять оптимизацию псевдонимов в определенных местах кода, разделение частей статического распределения может позволить программам воспользоваться многими преимуществами динамического распределение при сохранении достаточных ограничений на то, как вещи могут быть распределены, чтобы гарантировать, что программа будет работать со всеми возможными последовательностями входных данных. - person supercat; 15.01.2017
comment
@nm: Ничто в стандарте не обещает, что любая конкретная операция будет успешной без переполнения стека, за исключением того факта, что для каждой реализации должна быть хотя бы одна программа (возможно, надуманная), которая номинально использует ограничения реализации. дается в Стандарте без смерти. С другой стороны, реализация должна иметь возможность создавать массив переменной длины, равный 1, в любом случае, когда она вообще может создать любой такой массив, и неяркая интерпретация стандарта предполагает, что авторам не потребовалось бы реализации для принятия кода... - person supercat; 15.01.2017
comment
... который использует VLA, если они предполагают, что компиляторы должны иметь возможность обрабатывать весь такой код как вызывающий Undefined Behavior. Хотя часто бывает полезно разрешить программам содержать код, который невозможно выполнить без вызова UB, я не вижу особого смысла требовать, чтобы компилятор принимал синтаксические формы, не имеющие требуемого значения. - person supercat; 15.01.2017
comment
@nm: После дальнейших размышлений я соглашусь с тем, что предположение о том, что создание даже одного одноэлементного VLA в main() должно быть надежным, является чрезмерно оптимистичным, поскольку я знаю по крайней мере один встроенный компилятор, в котором он может дать сбой (Keil Компилятор /ARM использует malloc/free для VLA, но если код запуска не был сконфигурирован для настройки кучи — чего стандарт не требует для автономных реализаций — вызов malloc() завершится ошибкой во время выполнения). - person supercat; 16.01.2017
comment
Это не UB объявлять большой VLA. Стандарт не допускает сбоя автоматического распределения. Если реализация демонстрирует переполнение стека, то она не соответствует требованиям. - person M.M; 30.09.2017
comment
@MM: Если бы для соответствия требовалось, чтобы распределения VLA всегда были успешными, это сделало бы Стандарт практически невозможным для реализации соответствующим образом на реальном оборудовании. Какой смысл в таком стандарте? - person supercat; 30.09.2017
comment
Я знаю, каким будет ваш ответ на последний вопрос :) - person M.M; 01.10.2017

Массивы переменной длины обычно располагаются в стеке. Как и любая другая переменная, размещенная в стеке, это обычно делается путем вычитания из указателя стека (или добавления к нему для растущего вверх стека). Указатель кадра, вероятно, будет использоваться, чтобы функция могла отслеживать свой кадр стека перед лицом динамически определяемых изменений указателя стека. Как и в случае других распределений стека, в этом процессе обычно не выполняется проверка ошибок.

Это таит в себе несколько опасностей.

  1. Пространство, выделенное для стека, может быть переполнено. В зависимости от платформы это может привести к некоторой ошибке памяти от ядра, это может привести к тому, что платформа динамически выделяет больше пространства стека или может привести к перезаписи другой памяти.
  2. На платформах, которые используют страницы защиты за пределами стека для автоматического увеличения стека и/или обнаружения переполнения стека, выделение достаточно большого стека может «пропустить» эти страницы. Это может привести к ошибкам защиты памяти или более серьезному повреждению памяти в тех случаях, когда переполнение стека обычно обнаруживается.

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

person plugwash    schedule 30.09.2017