Распределяет ли malloc () непрерывный блок памяти? - PullRequest
34 голосов
/ 09 марта 2009

У меня есть кусок кода, написанный очень старым школьным программистом :-). это выглядит примерно так

typedef struct ts_request
{ 
  ts_request_buffer_header_def header; 
  char                         package[1]; 
} ts_request_def; 

ts_request_def* request_buffer = 
malloc(sizeof(ts_request_def) + (2 * 1024 * 1024));

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

  1. Всегда ли malloc выделяет непрерывный блок памяти? потому что в этом коде, если блоки не являются смежными, код будет большой ошибкой

  2. Делая free (request_buffer), освободит ли он все байты, выделенные malloc, т.е. sizeof (ts_request_def) + (2 *1024* 1024), или только байты размера структуры sizeof (ts_request_def)

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

Ответы [ 14 ]

48 голосов
/ 09 марта 2009

Чтобы ответить на ваши пронумерованные баллы.

  1. Да.
  2. Все байты. Malloc / free не знает и не заботится о типе объекта, только о размере.
  3. Строго говоря, неопределенное поведение, но распространенный прием, поддерживаемый многими реализациями. Ниже приведены другие альтернативы.

Последний стандарт C, ISO / IEC 9899: 1999 (неофициально C99), позволяет гибкие элементы массива .

Примером этого может быть:

int main(void)
{       
    struct { size_t x; char a[]; } *p;
    p = malloc(sizeof *p + 100);
    if (p)
    {
        /* You can now access up to p->a[99] safely */
    }
}

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

Кроме того, gcc допускает массивы нулевой длины в качестве расширения. Массивы нулевой длины недопустимы в стандартном C, но gcc ввел эту функцию до того, как C99 предоставил нам гибкие члены массива.

В ответ на комментарий я объясню, почему приведенный ниже фрагмент является технически неопределенным поведением. Номера секций, которые я цитирую, относятся к C99 (ISO / IEC 9899: 1999)

struct {
    char arr[1];
} *x;
x = malloc(sizeof *x + 1024);
x->arr[23] = 42;

Во-первых, 6.5.2.1 # 2 показывает, что [i] идентично (* ((a) + (i))), поэтому x-> arr [23] эквивалентно (* ((x-> arr) ) + (23))). Теперь 6.5.6 # 8 (с добавлением указателя и целого числа) говорит:

"Если и операнд-указатель, и результат указывают на элементы одного и того же объекта массива или один после последнего элемента объекта массива, при оценке не должно быть переполнения; в противном случае поведение не определено".

По этой причине, поскольку x-> arr [23] не находится в массиве, поведение не определено. Вы все еще можете подумать, что все в порядке, потому что malloc () подразумевает, что массив теперь расширен, но это не совсем так. В информативном приложении J.2 (в котором перечислены примеры неопределенного поведения) приводятся дополнительные пояснения на примере:

Индекс массива вне диапазона, даже если объект, очевидно, доступен с заданный индекс (как в выражении lvalue a [1] [7] с учетом объявления int а [4] [5]) (6,5,6).

10 голосов
/ 09 марта 2009

3 - Это довольно распространенная уловка C для выделения динамического массива в конце структуры. Альтернативой было бы поместить указатель в структуру и затем выделить массив отдельно, не забывая также освободить его. То, что размер установлен в 2 МБ, кажется немного необычным.

7 голосов
/ 09 марта 2009

Это стандартный трюк C, и он не более опасен, чем любой другой буфер.

Если вы пытаетесь показать своему боссу, что вы умнее «очень старого школьного программиста», этот код вам не подходит. Старая школа не обязательно плохая. Похоже, "старой школы" парень знает достаточно об управлении памятью;)

7 голосов
/ 09 марта 2009

1) Да, это так, иначе malloc потерпит неудачу, если не будет достаточно большого непрерывного блока. (Ошибка с malloc вернет нулевой указатель)

2) Да, будет. Выделение внутренней памяти будет отслеживать объем памяти, выделенный этим значением указателя, и освободить все это.

3) Это что-то вроде языкового взлома и немного сомнительно в его использовании. Это все еще подвержено переполнению буфера, просто может занять немного больше времени у злоумышленников, чтобы найти полезную нагрузку, которая вызовет это. Стоимость «защиты» также довольно высока (вам действительно нужно> 2 МБ на буфер запросов?). Это также очень уродливо, хотя ваш начальник может не оценить этот аргумент:)

5 голосов
/ 11 марта 2009

Я не думаю, что существующие ответы вполне понимают суть этого вопроса. Вы говорите, что программист старой школы делает что-то вроде этого;

typedef struct ts_request
{ 
  ts_request_buffer_header_def header; 
  char                         package[1]; 
} ts_request_def;

ts_request_buffer_def* request_buffer = 
malloc(sizeof(ts_request_def) + (2 * 1024 * 1024));

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

typedef struct ts_request
{ 
  ts_request_buffer_header_def header; 
  char                         package[2*1024*1024 + 1]; 
} ts_request_def;

ts_request_buffer_def* request_buffer = 
malloc(sizeof(ts_request_def));

Держу пари, что он действительно делает что-то вроде этого;

typedef struct ts_request
{ 
  ts_request_buffer_header_def header; 
  char                         package[1]; // effectively package[x]
} ts_request_def;

ts_request_buffer_def* request_buffer = 
malloc( sizeof(ts_request_def) + x );

Чего он хочет добиться, так это выделения запроса с переменным размером пакета x. Конечно, нельзя объявлять размер массива с помощью переменной, поэтому он обходит это хитростью. Похоже, что он знает, что делает со мной, уловка хорошо подходит к респектабельному и практическому концу шкалы трюка с буквой C.

3 голосов
/ 11 марта 2009

Да. malloc возвращает только один указатель - как он мог сообщить запрашивающей стороне, что он выделил несколько блоков для удовлетворения запроса?

3 голосов
/ 09 марта 2009

Я часто видел и использовал этот шаблон.

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

Доступ к элементам массива также немного более эффективен, но это все менее и менее важно для современных компьютеров.

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

Единственная потенциальная проблема, которую я вижу, заключается в том, что компилятор переставляет порядок хранения переменных-членов, потому что этот прием требует, чтобы поле пакета оставалось последним в хранилище. Я не знаю, запрещает ли стандарт С перестановку.

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

3 голосов
/ 09 марта 2009

Эксплойт (вопрос 3) действительно соответствует интерфейсу с вашей структурой. В контексте это распределение может иметь смысл, и без дополнительной информации невозможно сказать, безопасно это или нет.
Но если вы имеете в виду проблемы с выделением памяти больше, чем структура, это ни в коем случае не плохой дизайн C (я бы даже не сказал, что это ОДНА школа ...;))
Здесь просто последнее замечание - смысл наличия char [1] заключается в том, что завершающий NULL всегда будет в объявленной структуре, то есть в буфере может быть 2 *1024* 1024 символа, и вам не нужно учитывать для NULL на «+1». Может показаться маленьким подвигом, но я просто хотел отметить.

3 голосов
/ 09 марта 2009

Что касается # 3, без кода трудно ответить. Я не вижу в этом ничего плохого, если это часто не происходит. Я имею в виду, что вы не хотите выделять 2 МБ памяти постоянно. Вы также не хотите делать это без необходимости, например, если вы только когда-либо используете 2k.

Тот факт, что вам по каким-то причинам это не нравится, недостаточно для того, чтобы возражать против этого или оправдывать его полное переписывание. Я бы внимательно посмотрел на использование, попытался понять, о чем думал оригинальный программист, внимательно посмотреть на переполнение буфера (как указал workmad3) в коде, который использует эту память.

Есть много распространенных ошибок, которые вы можете найти. Например, проверяет ли код, чтобы убедиться, что malloc () успешно?

2 голосов
/ 13 марта 2009
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...