Можно ли получить доступ к памяти локальной переменной вне ее области?


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

#include <iostream>

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

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

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

вывод 58

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

19 886

19 ответов:

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

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

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

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

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

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

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

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

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

обновление

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

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

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

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

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

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

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

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

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

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

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

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

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

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

более безопасные для памяти языки решают эту проблему, ограничивая вашу мощность. В "нормальном" 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/

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

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

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

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

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

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

Если вы делаете что-то вроде этого:

#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(). Если вы деассемблируете исполняемый файл, вы увидите его ясно.

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

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

unsigned int q = 123456;

*(double*)(q) = 1.2;

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

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

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

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

вы скомпилировали свою программу с включенным оптимизатором ?

функция foo () довольно проста и, возможно, была встроена/заменена в результирующем коде.

но я Агре с маркой B, что результирующее поведение не определено.

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

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

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

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

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
}

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

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

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

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", но ваши результаты могут отличаться (действительно!). Ваш указатель сбивает другие, несвязанные локальные переменные.

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

вы фактически вызвали неопределенное поведение.

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

Так вы не изменяли a но скорее место памяти, где a когда-то был. Эта разница очень похожа на разницу между сбоем и не сбоем.

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

, это неопределено поведение, и вы не должны полагаться на него работать!

обратите внимание на все предупреждения . Не только решать ошибки.
GCC показывает это предупреждение

внимание: адрес локальной переменной' a ' возвращается

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

может, потому что a - Это переменная, временно выделенная на время существования ее области видимости (

вещи с правильной (?) выход консоли может резко измениться, если вы используете ::printf, но не cout. Вы можете поиграть с отладчиком в приведенном ниже коде (проверено на x86, 32-бит, MSVisual Studio):

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}

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

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

позвольте мне возьмем реальный пример:

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

Это "грязный" способ использования адресов памяти. Когда вы возвращаете адрес (указатель), вы не знаете, принадлежит ли он локальной области функции. Это просто адрес. Теперь, когда вы вызвали функцию "foo", этот адрес (местоположение памяти) " a " уже был выделен там в (безопасно, по крайней мере сейчас) адресуемой памяти вашего приложения (процесса). После того, как функция " foo " вернулась, адрес "a" можно считать "грязным", но он есть, не очищен и не очищен нарушенный/изменены выражениями в другой части программы (в данном конкретном случае по крайней мере). Компилятор C/C++ не останавливает вас от такого "грязного" доступа (может предупредить вас, если вам все равно). Вы можете безопасно использовать (обновлять) любую ячейку памяти, которая находится в сегменте данных вашего экземпляра программы (процесса), если вы не защищаете адрес каким-либо образом.