Как может разыменование указателя NULL в C не привести к сбою программы?

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

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

Начнем с соответствующего кода. Это очень маленький код:

// ... code above skipped, not relevant ...

if (data == NULL) return -1;

information = parseData(data);

if (information == NULL) return -1;

/* Check if name has been correctly \0 terminated */
if (information->kind.name->data[information->kind.name->length] != '\0') {
    freeParsedData(information);
    return -1;
}

/* Copy the name */
realLength = information->kind.name->length + 1;
*result = malloc(realLength);
if (*result == NULL) {
    freeParsedData(information);
    return -1;
}
strlcpy(*result, (char *)information->kind.name->data, realLength);

// ... code below skipped, not relevant ...

Это уже все. Вылетает в strlcpy. Я даже могу рассказать вам, как strlcpy на самом деле вызывается во время выполнения. strlcpy на самом деле вызывается со следующими параметрами:

strlcpy ( 0x341000, 0x0, 0x1 );

Зная это, довольно очевидно, почему происходит сбой strlcpy. Он пытается прочитать один символ из указателя NULL, и это, конечно, приведет к сбою. И поскольку последний параметр имеет значение 1, исходная длина должна была быть 0. В моем коде явно есть ошибка, он не может проверить, что данные имени имеют значение NULL. Я могу это исправить, без проблем.

У меня такой вопрос:
Как этот код вообще может добраться до strlcpy?
Почему этот код не дает сбой в операторе if?

Я попробовал это локально на своей машине:

int main (
    int argc,
    char ** argv
) {
    char * nullString = malloc(10);
    free(nullString);
    nullString = NULL;

    if (nullString[0] != '\0') {
        printf("Not terminated\n");
        exit(1);
    }
    printf("Can get past the if-clause\n");

    char xxx[10];
    strlcpy(xxx, nullString, 1);
    return 0;   
}

Этот код никогда не передается оператору if. Он падает в операторе if, и это определенно ожидается.

Итак, может ли кто-нибудь придумать какую-либо причину, по которой первый код может передать этот оператор if без сбоя, если имя-> данные действительно NULL? Это вообще для меня загадочно. Это не кажется детерминированным.

Важная дополнительная информация:
Код между двумя комментариями действительно полный, ничего не упущено. Кроме того, приложение является однопоточным, поэтому нет другого потока, который мог бы неожиданно изменить какую-либо память в фоновом режиме. Платформой, на которой это происходит, является ЦП PPC (G4, если он может играть какую-либо роль). И в случае, если кто-то задается вопросом о «виде», это потому, что «информация» содержит «объединение» с именем «вид», а имя снова является структурой (вид — это объединение, каждое возможное значение объединения — это другой тип структуры); но это все не должно иметь большого значения здесь.

Я благодарен за любую идею здесь. Я еще больше благодарен, если это не просто теория, а если есть способ проверить, действительно ли эта теория верна для клиента.

Решение

Я уже принял правильный ответ, но на случай, если кто-нибудь найдет этот вопрос в Google, вот что произошло на самом деле:

Стрелки указывали на уже освобожденную память. Освобождение памяти не обнулит всю память и не заставит процесс сразу вернуть ее системе. Таким образом, несмотря на то, что память была освобождена ошибочно, она содержала правильные значения. Рассматриваемый указатель не равен NULL во время выполнения "if check".

После этой проверки я выделяю новую память, вызывая malloc. Не знаю, что именно здесь делает malloc, но каждый вызов malloc или free может иметь далеко идущие последствия для всей динамической памяти виртуального адресного пространства процесса. После вызова malloc указатель фактически равен NULL. Каким-то образом malloc (или какой-то системный вызов malloc использует) обнуляет уже освобожденную память, где находится сам указатель (не данные, на которые он указывает, сам указатель находится в динамической памяти). Обнулив эту память, указатель теперь имеет значение 0x0, что равно NULL в моей системе, и при вызове strlcpy он, конечно, рухнет.

Таким образом, настоящая ошибка, вызывающая такое странное поведение, находилась в совершенно другом месте моего кода. Никогда не забывайте: Освобожденная память сохраняет свои значения, но как долго это не зависит от вас. Чтобы проверить, есть ли в вашем приложении ошибка доступа к уже освобожденной памяти, просто убедитесь, что освобожденная память всегда обнуляется перед ее освобождением. В OS X вы можете сделать это, установив переменную среды во время выполнения (не нужно ничего перекомпилировать). Конечно, это немного замедляет работу программы, но вы обнаружите эти ошибки гораздо раньше.


person Mecki    schedule 26.08.2009    source источник
comment
Вы можете запросить у клиента дамп ядра и исследовать его в отладчике.   -  person qrdl    schedule 26.08.2009
comment
@qrdl: у меня есть журнал сбоев процесса. Это Mac OS X, и процесс сбоя всегда создает такой журнал сбоев. У меня есть трассировка стека, поэтому я знаю, где происходит сбой, и у меня есть значения во всех регистрах на момент сбоя; а также зная, что этот сбой вызван доступом к памяти в ячейке памяти 0x0 (указатель NULL). Никакой другой полезной информации в таком логе нет.   -  person Mecki    schedule 26.08.2009
comment
Пожалуйста, покажите, где вы объявляете «результат» и как вы выделяете память, на которую он указывает? Вы показали, где устанавливается «* результат», но не где назначается «результат».   -  person NVRAM    schedule 26.08.2009
comment
Ой, извините, результат — это выходной параметр функции. Он имеет тип char ** и в вызывающей программе находится в стеке char * name; и передается в функцию как ..., &name, .... Так что, если что-то не испортит стек, с этим не должно быть проблем.   -  person Mecki    schedule 26.08.2009
comment
Анекдот разработчиков: при разыменовании PS1 возвращается NULL 3.   -  person Goz    schedule 26.08.2009
comment
Если бы вы разместили журнал сбоев здесь, все еще может помочь.   -  person sigjuice    schedule 26.08.2009


Ответы (17)


Возможно, структура находится в памяти, которая была free() создана, или куча повреждена. В этом случае malloc() мог модифицировать память, думая, что она свободна.

Вы можете попробовать запустить свою программу под проверкой памяти. Одной из программ проверки памяти, которая поддерживает Mac OS X, является valgrind, хотя она поддерживает Mac OS X только на Intel, а не на PowerPC.

person mark4o    schedule 26.08.2009
comment
Вау, блестящий ответ! Даже не думал об этом. Да, действительно, указатель данных может указывать на что-то полезное в операторе if, но сам может располагаться в уже освобожденной (еще не повторно используемой) памяти. Вызов malloc может вызвать изменения в этой свободной памяти, так что теперь указатель данных внезапно указывает на NULL. Это определенно возможно! - person Mecki; 26.08.2009
comment
Пока этот ответ выглядит наиболее многообещающе; что-то настолько простое и все же я никогда не думал об этой возможности. Конечно, malloc может изменить любую память, используемую в моем коде, если я ее случайно освободил. Я могу проверить это с клиентом. Вы можете заставить OS X загрузить альтернативную реализацию malloc (установив переменную среды), которая гарантирует немедленный сбой доступа к свободной памяти (в целях отладки). Посмотрим, что нам это скажет :-) - person Mecki; 26.08.2009
comment
Вы, сэр, гений :-) Я не учел, что вызов malloc может делать такие вещи, как изменение сопоставления VM процесса, обнуление страниц и так далее. Теперь я могу воспроизвести проблему локально, и действительно, когда я пропускаю этот вызов malloc, он не падает. Итак, принятый ответ, поздравляю! - person Mecki; 27.08.2009
comment
Если вы можете запустить свою программу на Intel Mac, я бы по-прежнему рекомендовал запускать ее с помощью инструмента проверки памяти valgrind, чтобы искать другие возможные повреждения кучи, переполнения буфера, использование неинициализированных значений, утечки памяти и т. д. Если у вас есть набор тестов, который можно периодически запускать под valgrind, тогда так даже лучше. - person mark4o; 27.08.2009

Во-первых, разыменование нулевого указателя является неопределенным поведением. Это может привести к сбою, а не к сбою, или установить обои на изображение Губки Боба Квадратные Штаны.

Тем не менее, разыменование нулевого указателя обычно приводит к сбою. Таким образом, ваша проблема, вероятно, связана с повреждением памяти, например. от записи после конца одной из ваших строк. Это может вызвать сбой с отложенным эффектом. Я особенно подозрительн, потому что очень маловероятно, что malloc(1) завершится ошибкой, если ваша программа не упирается в конец доступной виртуальной памяти, и вы, вероятно, заметили бы, если бы это было так.

Изменить: ОП указал, что это не результат, который равен нулю, а information->kind.name->data. Вот потенциальная проблема:

Нет проверки, является ли information->kind.name->data нулевым. Единственная проверка на это

if (information->kind.name->data[information->kind.name->length] != '\0') {

Предположим, что information->kind.name->data равно нулю, но информация->вид.имя->длина равна, скажем, 100. Тогда это утверждение эквивалентно:

if (*(information->kind.name->data + 100) != '\0') {

Что не разыменовывает NULL, а разыменовывает адрес 100. Если это не приведет к сбою, а адрес 100 содержит 0, то этот тест будет пройден.

person Tyler McHenry    schedule 26.08.2009
comment
Когда моя ОС устанавливает мои обои на изображение Губки Боба, если я уважаю указатель NULL, я верну его обратно :-P Кстати, malloc здесь не терпит неудачу, * результат не равен NULL, это 0x341000 в адресном пространстве процесса. Это имя->данные, которые имеют значение NULL, и это нормально, так как данные, которые я проанализировал, могут не содержать имени... что они должны иметь имя в соответствии с клиентом, и в функции разбора также может быть ошибка. другая проблема, я думаю. - person Mecki; 26.08.2009
comment
Но, как указывает ОП, информация-›kind.name-›length должна быть равна нулю, потому что он знает, что realLength равна 1. - person Martin B; 26.08.2009

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

Согласно стандарту C 6.5.3.2/4:

Если указателю было присвоено недопустимое значение, поведение унарного оператора * не определено.

Так что сбой может быть, а может и нет.

person Kirill V. Lyadvinsky    schedule 26.08.2009
comment
Справедливо ли это только для C как языка или это справедливо и для UNIX в соответствии со стандартом POSIX? Поскольку наиболее распространенное поведение, которое вы видите в UNIX при разыменовании указателя NULL, заключается в том, что приложение получает сигнал, который обычно завершает его. По крайней мере, это то, что я ожидал как разработчик. - person Mecki; 26.08.2009
comment
Да, это верно для C как языка. Undefined означает, что может случиться что угодно. Зависеть от каких-либо конкретных событий или ожидать их появления — плохая идея, потому что это может не сработать так, как вы ожидаете, даже при выполнении одной и той же программы на одной и той же платформе. - person Nick Meyer; 26.08.2009
comment
Наиболее распространенное поведение в случае вашего клиента ничего не значит. Вот почему вы должны придерживаться стандарта, а не особенностей реализации. - person Michael Foukarakis; 26.08.2009

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

person Community    schedule 26.08.2009
comment
Хм... возможно. Трудно проверить, не имея возможности выполнить код. Немного усложняет ситуацию то, что вся функция, содержащая код, встраивается в другую не потому, что я использую встроенный атрибут, а потому, что GCC считает, что это хорошая идея, когда используются флаги оптимизации. - person Mecki; 26.08.2009

Моя теория состоит в том, что information->kind.name->length — очень большое значение, так что information->kind.name->data[information->kind.name->length] на самом деле относится к допустимому адресу памяти.

person Bombe    schedule 26.08.2009
comment
Еще одна идея, которая пришла мне в голову. Если длина > 4095, я получаю доступ к памяти после первой страницы памяти (страница защиты NULL без разрешения на чтение/запись, которая обычно вызывает сигнал при доступе), и это действительно не приведет к сбою. Что говорит против этого, так это то, что realLength равна 1 после добавления 1 к значению длины, поэтому значение длины должно было быть нулем раньше. - person Mecki; 26.08.2009

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

person JaredPar    schedule 26.08.2009
comment
Я тоже думал об этом, но, во-первых, strlcpy не будет пытаться писать в указатель NULL (он просто читает из него, у меня есть правильный источник), а во-вторых, это в OS X, где указатель NULL - это просто указатель на первую страницу памяти процесса, и эта страница не имеет прав ни на чтение, ни на запись, поэтому при попытке чтения из нее программа получает сигнал SIGBUS. - person Mecki; 26.08.2009

К вашему сведению, когда я вижу эту строку:

if (information->kind.name->data[information->kind.name->length] != '\0') {

Я вижу до трех различных разыменований указателя:

  1. Информация
  2. название
  3. данные (если это указатель, а не фиксированный массив)

Вы проверяете информацию на ненулевую, но не имя и не данные. Почему вы так уверены, что они верны?

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

Увидел, что вы на Mac - игнорируйте комментарий gflags - это может помочь кому-то еще, кто это читает. Если вы используете что-то более раннее, чем OS X, есть ряд удобных инструментов Macsbugs для нагрузки на кучу (например, команда скремблирования кучи, 'hs').

person plinth    schedule 26.08.2009
comment
Да, вы совершенно правы, ни один из них не проверен. И чтобы ответить на ваш вопрос: я понятия не имею, верны ли они. Все они могут указывать в никуда. Но что я знаю, так это то, что второй параметр strlcpy равен NULL, поэтому данные указывают на NULL, и ничто в программе не могло бы это изменить, поэтому он должен был быть NULL и в операторе if... но там я разыменовал его и ничего не пошло не так (data[0] совпадает с (*data), так что это означает разыменование, не так ли?). - person Mecki; 26.08.2009

Меня интересует приведение char* в вызове strlcpy.

Может ли тип data* отличаться по размеру от char* в вашей системе? Если указатели char меньше, вы можете получить подмножество указателя данных, которое может быть NULL.

Пример:

int a = 0xffff0000;
short b = (short) a; //b could be 0 if lower bits are used

Изменить: исправлены орфографические ошибки.

person Key    schedule 26.08.2009
comment
Я тоже думал об этом приведении ... теоретически это могло вызвать проблему, но на практике я обнаружил проблему (см. Обновление вопроса), и это было не так - в моей системе все указатели имеют одинаковый размер. Тем не менее, вы получаете голосование, потому что вы первый, кто подумал о том, что актерский состав является возможной причиной всего этого; если система имеет действительно разные размеры указателя для разных типов данных (что, я думаю, совершенно правильно), это может вызвать такую ​​​​проблему. - person Mecki; 27.08.2009

Вот один конкретный способ, которым вы можете обойти указатель «данные», являющийся NULL в

if (information->kind.name->data[information->kind.name->length] != '\0') {

Скажем, информация-> вид.имя-> длина большая. По крайней мере, больше 4096, на определенной платформе с определенным компилятором (скажем, на большинстве * nix со стандартным компилятором gcc) код приведет к чтению памяти «адрес вида. имя-> данные + информация-> вид. имя ->длина].

На более низком уровне это чтение означает «чтение памяти по адресу (0 + 8653)» (или любой другой длины). На *nix принято помечать первую страницу в адресном пространстве как «недоступную», что означает, что разыменование указателя NULL, который считывает адрес памяти от 0 до 4096, приведет к распространению аппаратной ловушки на приложение и его сбою.

Прочитав эту первую страницу, вы можете случайно попасть в действующую сопоставленную память, например. разделяемая библиотека или что-то еще, что случайно попало туда — и доступ к памяти не будет неудачным. И это нормально. Разыменование указателя NULL является неопределенным поведением, ничто не требует его сбоя.

person leeeroy    schedule 26.08.2009
comment
Да, это совершенно верно (вы знаете свою виртуальную машину UNIX, не так ли? ;-)); если длина 4096 или больше, это действительно не сбой. Единственная проблема в том, что мы можем точно сказать, что длина равна 0, иначе realLength не была бы 1, когда происходит сбой (и реальная длина равна 1, когда происходит сбой). Тем не менее, хорошая идея. Вы получаете голос за указание на это. - person Mecki; 27.08.2009

Отсутствие символа '{' после последнего оператора if означает, что что-то в разделе "//... код выше пропущен, не актуален..." контролирует доступ ко всему этому фрагменту кода. Из всего вставленного кода выполняется только strlcpy. Решение: никогда не используйте операторы if без фигурных скобок, чтобы прояснить управление.

Учти это...

if(false)
{
    if(something == stuff)
    {
        doStuff();

    .. snip ..

    if(monkey == blah)
        some->garbage= nothing;
        return -1;
    }
}
crash();

Только "сбой();" казнят.

person Community    schedule 26.08.2009
comment
Конечно, компилятор поймает несоответствие {}, но кто знает... ;) - person Pod; 26.08.2009
comment
Да, но это было только потому, что я не скопировал код напрямую, вместо этого я набрал его и забыл добавить. Он есть в реальном исходном файле :-P Я только что сделал это, поскольку имена в реальном исходном файле очень загадочны (если не сказать уродливы), и использование более очевидных имен облегчает чтение. - person Mecki; 27.08.2009

Я бы запустил вашу программу под valgrind. Вы уже знаете, что существует проблема с указателями NULL, поэтому профилируйте этот код.

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

Как уже упоминалось, ссылка на ячейку памяти 0 - это своего рода вещь "que sera, sera".

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

if (information->kind.name->data[information->kind.name->length] != '\0') {

линия как

    if (information == NULL) {
      return -1; 
    }
    if (information->kind == NULL) {
      return -1; 
    }

и так далее.

person Shawn Leslie    schedule 26.08.2009
comment
Ты прав; проверка каждого отдельного указателя на NULL перед его использованием - правильный способ сделать это. Хорошо, в данном случае это ничего не решило (см. принятое решение и мое обновление к вопросу), но, несмотря на то, что я нашел причину всего этого, я меняю код именно на то, что выглядит в вашем примере :-) - person Mecki; 27.08.2009

Вау, это странно. Одна вещь кажется мне немного подозрительной, хотя она может и не способствовать:

Что произойдет, если информация и данные будут хорошими указателями (не нулевыми), но information.kind.name будет нулевым. Вы не разыменовываете этот указатель до строки strlcpy, поэтому, если он был нулевым, до этого он может не сработать. Конечно, перед этим вы разыменовываете data[1], чтобы установить его в \0, что также должно привести к сбою, но из-за какой-то случайности ваша программа может иметь доступ для записи к 0x01, но не к 0x00.

Кроме того, я вижу, что вы используете information->name.length в одном месте, но information->kind.name.length в другом, не уверен, что это опечатка или это желательно.

person bdk    schedule 26.08.2009
comment
Извините, это раз опечатка. Исправление. Я несколько изменил названия на более очевидные (иначе этот небольшой фрагмент кода нечитаем) :-) - person Mecki; 26.08.2009
comment
Что касается вашего ответа: я разыменовываю его перед strlcpy. x[0] совпадает с (*x), а x[1] совпадает с *(x + 1) и так далее. Каждый доступ к массиву, доступ для чтения или записи разыменовывает указатель. Однако я не пишу в него, я только пытаюсь читать из него (но strlcpy также пытается только читать из него; у меня есть настоящий код strlcpy правильного libC здесь, и я проверил это). - person Mecki; 26.08.2009

Несмотря на то, что разыменование нулевого указателя приводит к неопределенному поведению и не обязательно к сбою, следует проверять значение information->kind.name->data, а не содержимое information->kind.name->data[1].

person Michael Foukarakis    schedule 26.08.2009

char * p = NULL;

р [я] как

p += i;

что является допустимой операцией даже для нулевого указателя. затем он указывает на ячейку памяти 0x0000[...]i

person StampedeXV    schedule 26.08.2009
comment
Да, но p[i] == 0 похоже на *(p + i) == 0, а это невозможно, если p равно NULL. Хорошо, не совсем правильно, это возможно, если i больше, чем размер страницы в большинстве систем (поскольку тогда вы не получаете доступ к защите NULL в большинстве ОС и, следовательно, не вызываете сбой), но в моем случае realLength равно 1 после добавления 1 к длине и, следовательно, длина должна быть равна 0 раньше. И в случае, если я выше нуля, у нас есть * (p + 0) == 0, и это приведет к сбою, если p равно NULL, попробуйте. Также, если (p[0] == 0) произойдет сбой, если p равно NULL, попробуйте. - person Mecki; 26.08.2009
comment
Вы проверили, что такое NULL? NULL обычно является #define в C. Это также может быть 0xFFFFFFFFF или что-то произвольное. :) - person StampedeXV; 26.08.2009
comment
В Mac OS X NULL действительно определяется как (void *)0, но вы правы, C не говорит, что NULL должен быть равен 0, это может быть любое значение. NULL — это просто NULL, и вам, как разработчику, все равно, что это значит. - person Mecki; 27.08.2009

Вы всегда должны проверять, является ли информация-> вид.имя-> данные нулевым, но в этом случае

in

if (*result == NULL) 
    freeParsedData(information);
    return -1;
}

вы пропустили {

должен быть

if (*result == NULL)
{ 
     freeParsedData(information);
     return -1;
}

Это хорошая причина для такого стиля кодирования, а не

if (*result == NULL) { 
    freeParsedData(information);
    return -1;
}

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

person Captain Lepton    schedule 26.08.2009
comment
Извините, я пропустил это, оно есть в реальном исходном коде. - person Mecki; 27.08.2009
comment
Отлично, но в любом случае я настоятельно рекомендую этот стиль кодирования, поскольку он выравнивает начальную и конечную фигурные скобки по горизонтали, что облегчает поиск соответствующих фигурных скобок и в целом упрощает просмотр структуры кода. Вы можете видеть, что одна из самых фундаментальных задач при программировании с динамической памятью на C состоит в том, чтобы отслеживать выделение и освобождение памяти и следить за тем, чтобы вы рассмотрели каждый случай. Стандартно и разумно всегда устанавливать указатель в NULL после освобождения, чтобы использовать сам указатель в качестве флага, указывающего на нераспределенную память. - person Captain Lepton; 28.08.2009

* результат = malloc (реальная длина); // ???

Адрес вновь выделенного сегмента памяти сохраняется в месте, на которое ссылается адрес, содержащийся в переменной «результат».

Это намерение? Если это так, может потребоваться модификация strlcpy.

person Community    schedule 26.08.2009
comment
Да, по умыслу. Результат — это возвращаемое значение функции. Настоящим возвращаемым значением является int, указывающее только на успех или неудачу (и вид ошибки), но в случае успеха также должен быть возвращен результат. Это происходит из-за того, что вызывающая сторона имеет char * xxx; переменная и вызов функции с помощью (..., &xxx,...), поэтому при успешном возврате xxx теперь указывает на выделенную память. - person Mecki; 27.08.2009

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

person Siva Sankar    schedule 16.07.2012