Можно ли получить доступ к памяти локальной переменной вне ее области? - PullRequest
964 голосов
/ 22 июня 2011

У меня есть следующий код.

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

И код просто выполняется без исключений времени выполнения!

Вывод был 58

Как этобыть?Разве память локальной переменной недоступна вне ее функции?

Ответы [ 20 ]

4711 голосов
/ 23 июня 2011

Как это может быть? Разве память локальной переменной недоступна вне ее функции?

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

Неделю спустя вы возвращаетесь в отель, не регистрируетесь, пробираетесь в свою старую комнату с украденным ключом и смотрите в ящик. Ваша книга все еще там. Поразительно!

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

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

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

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

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

C ++ не является безопасным языком . Это позволит вам весело нарушить правила системы. Если вы попытаетесь сделать что-то незаконное и глупое, например, вернуться в комнату, в которой вам не разрешено находиться, и порыться в столе, которого там больше нет, C ++ вас не остановит. Более безопасные языки, чем C ++, решают эту проблему, ограничивая ваши возможности - например, благодаря более строгому контролю над клавишами.

UPDATE

Боже мой, этот ответ привлекает много внимания. (Я не уверен почему - я посчитал это просто «забавной» небольшой аналогией, но неважно.)

Я подумал, что было бы уместно обновить это, добавив несколько технических мыслей.

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

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

Второй метод состоит в том, чтобы иметь «недолговечную» область хранения, в которой время жизни каждого байта хорошо известно. Здесь время жизни соответствует шаблону «вложения». Самая долгоживущая из этих кратковременных переменных будет размещена перед любыми другими недолговечными переменными и будет освобождена последней. Короткоживущие переменные будут размещены после самых долгоживущих и будут освобождены перед ними. Время жизни этих короткоживущих переменных «вложено» в время жизни долгоживущих.

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

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

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

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

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

Реализация C ++ не обязана оставлять нетронутым мусор, оставленный в стеке, чтобы потом можно было незаконно вернуться к нему позже; для компилятора совершенно законно генерировать код, который обнуляет все в «комнате», которую вы только что освободили. Это не потому, что опять-таки это будет дорого.

Реализация C ++ не требуется для обеспечения того, чтобы при логическом сжатии стека адреса, которые раньше были действительными, по-прежнему отображались в памяти. Реализация позволяет сообщить операционной системе: «Мы закончили с использованием этой страницы стека. Пока я не скажу иначе, выдайте исключение, которое уничтожит процесс, если кто-нибудь коснется ранее действующей страницы стека». Опять же, реализации на самом деле этого не делают, потому что это медленно и не нужно.

Вместо этого реализации позволяют вам совершать ошибки и сойти с рук. Большую часть времени. Пока однажды что-то действительно ужасное не пойдет не так и процесс не взорвется.

Это проблематично. Есть много правил, и их легко нарушить случайно. Я, конечно, много раз. И что еще хуже, проблема часто появляется только тогда, когда обнаруживается, что память повреждена на миллиарды наносекунд после того, как произошло повреждение, когда очень трудно определить, кто его испортил.

больше мБезопасные для emory языки решают эту проблему, ограничивая вашу власть.В «нормальном» C # просто нет возможности взять адрес локального и вернуть его или сохранить на потом.Вы можете взять адрес локального, но язык продуманно разработан так, что его невозможно использовать после окончания срока действия локального.Чтобы взять локальный адрес и передать его обратно, вам нужно перевести компилятор в специальный «небезопасный» режим, и вставить слово «небезопасный» в вашу программу, чтобы привлечь внимание ктот факт, что вы, вероятно, делаете что-то опасное, что может нарушать правила.

Для дальнейшего чтения:

  • Что если в C # разрешено возвращать ссылки?По совпадению это тема сегодняшнего сообщения в блоге:

    http://blogs.msdn.com/b/ericlippert/archive/2011/06/23/ref-returns-and-ref-locals.aspx

  • Почему мы используем стеки для управления памятью?Типы значений в C # всегда хранятся в стеке?Как работает виртуальная память?И еще много тем о том, как работает менеджер памяти C #.Многие из этих статей также актуальны для программистов на C ++:

    https://blogs.msdn.microsoft.com/ericlippert/tag/memory-management/

271 голосов
/ 23 июня 2011

То, что вы здесь делаете, - это просто чтение и запись в память, которая раньше была адресом a. Теперь, когда вы находитесь за пределами foo, это просто указатель на некоторую область произвольной памяти. Просто так получилось, что в вашем примере эта область памяти существует, и ничто другое не использует ее в данный момент. Вы ничего не нарушаете, продолжая использовать его, и ничто другое еще не перезаписало это. Таким образом, 5 все еще там. В реальной программе эта память использовалась бы почти сразу, и вы могли бы что-то сломать, делая это (хотя симптомы могут появиться не намного позже!)

Когда вы возвращаетесь с foo, вы сообщаете ОС, что больше не используете эту память, и ее можно переназначить на что-то другое. Если вам повезет, и он никогда не будет переназначен, а ОС не поймает вас на том, что вы снова его используете, тогда вам сойдет с рук ложь. Скорее всего, вы в конечном итоге переписываете все, что заканчивается этим адресом.

Теперь, если вам интересно, почему компилятор не жалуется, возможно, это связано с тем, что foo был устранен оптимизацией. Обычно он предупредит вас о подобных вещах. C предполагает, что вы знаете, что делаете, и технически вы здесь не нарушили область действия (здесь нет ссылки на саму a за пределами foo), только правила доступа к памяти, которые вызывают только предупреждение, а не ошибку .

Короче говоря: обычно это не сработает, но иногда будет случайно.

148 голосов
/ 19 мая 2010

Потому что место для хранения еще не растоптано.Не рассчитывай на такое поведение.

79 голосов
/ 25 июня 2011

Небольшое дополнение ко всем ответам:

, если вы сделаете что-то подобное:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

результат, вероятно, будет: 7

Это потому, что послепо возвращении из foo () стек освобождается, а затем повторно используется boo ().Если вы разберете исполняемый файл, вы увидите его ясно.

68 голосов
/ 22 июня 2011

В C ++ вы можете получить доступ к любому адресу, но это не значит, что вы должны . Адрес, к которому вы обращаетесь, больше не действителен. Это работает , потому что после возврата foo больше ничего не зашифровывало память, но при многих обстоятельствах могло произойти сбой. Попробуйте проанализировать вашу программу с помощью Valgrind или даже просто оптимизировать ее, и посмотрите ...

66 голосов
/ 22 июня 2011

Вы никогда не вызываете исключение C ++, получая доступ к недопустимой памяти. Вы просто даете пример общей идеи ссылки на произвольную ячейку памяти. Я мог бы сделать то же самое, как это:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Здесь я просто трактую 123456 как адрес двойного числа и пишу в него. Может произойти любое количество вещей:

  1. q действительно может быть действительным двойным адресом, например, double p; q = &p;.
  2. q может указывать где-то внутри выделенной памяти, и я просто перезаписываю туда 8 байтов.
  3. q указывает за пределы выделенной памяти, и диспетчер памяти операционной системы отправляет сигнал сбоя сегментации в мою программу, в результате чего среда выполнения завершает его.
  4. Вы выиграли в лотерею.

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

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

28 голосов
/ 22 июня 2011

Скомпилировали ли вы свою программу с включенным оптимизатором? Функция foo() довольно проста и может быть встроена или заменена в полученном коде.

Но я согласен с Марком Б, что итоговое поведение не определено.

22 голосов
/ 23 июня 2011

Ваша проблема не имеет ничего общего с scope .В показанном вами коде функция main не видит имена в функции foo, поэтому вы не можете получить доступ к a в foo напрямую с этим именем вне foo.

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

17 голосов
/ 19 мая 2010

Вы просто возвращаете адрес памяти, это разрешено, но, вероятно, ошибка.

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

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}
16 голосов
/ 25 июня 2011

Это поведение не определено, как указал Алекс - на самом деле, большинство компиляторов будут предупреждать против этого, потому что это простой способ получить сбои.

В качестве примера того, как вы, вероятно, можете получить привидение, попробуйте этот пример:

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

Это печатает "y = 123", но ваши результаты могут отличаться (действительно!). Ваш указатель забивает другие, не связанные локальные переменные.

...