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

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

#include  int * foo() { int a = 5; return &a; } int main() { int* p = foo(); std::cout << *p; *p = 8; std::cout << *p; } 

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

Выходной сигнал составил 58

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

20 Solutions collect form web for “Можно ли получить доступ к памяти локальной переменной за пределами ее объема?”

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

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

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

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

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

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

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

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

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

ОБНОВИТЬ

Святая доброта, этот ответ получает много внимания. (Я не уверен, почему – я считал, что это просто «забавная» небольшая аналогия, но что бы там ни было).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

 #include #include  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 ++ вы можете получить доступ к любому адресу, но это не значит, что вам нужно . Адрес, к которому вы обращаетесь, больше недействителен. Это работает, потому что ничто больше не сместило память после возвращения foo, но при многих обстоятельствах может произойти сбой. Попробуйте проанализировать свою программу с помощью Valgrind или даже просто оптимизируйте ее и посмотрите …

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

 unsigned int q = 123456; *(double*)(q) = 1.2; 

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

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

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

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

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

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

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

Ваша проблема не имеет ничего общего с областью . В коде, который вы показываете, функция main не видит имена в функции foo , поэтому вы не можете получить доступ a f in напрямую с этим именем вне 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 } 

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

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

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

 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 снова, и вам, вероятно, не повезет больше … 😉

Вы действительно вызывают неопределенное поведение.

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

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

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

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

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

предупреждение: адрес локальной переменной ‘a’ возвращен

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

Он может, потому что a – переменная, временно назначаемая для срока действия своей области (функция foo ). После того, как вы вернетесь из foo память свободна и может быть перезаписана.

То, что вы делаете, описывается как неопределенное поведение . Результат нельзя предсказать.

Вещи с правильным (?) Выходом консоли могут сильно измениться, если вы используете :: 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» можно считать «грязным», но он там, не очищен и не нарушен / изменен выражениями в другой части программы (по крайней мере, в этом конкретном случае). Компилятор AC / C ++ не останавливает вас от такого «грязного» доступа (может вас предупредить, если вам все равно). Вы можете безопасно использовать (обновлять) любую ячейку памяти, которая находится в сегменте данных вашего экземпляра программы (процесса), если вы не защитите адрес каким-либо образом.

Это определенно проблема времени! Объект, на который указывает указатель p является «запланированным», если он выходит из области foo . Однако эта операция происходит не сразу, а несколько циклов процессора. Является ли это неопределенным поведением, или C ++ на самом деле делает некоторые вещи предварительной очистки в фоновом режиме, я не знаю.

Если вы вставляете вызов функции sleep вашей операционной системы между вызовом foo и инструкциями cout , заставляя программу ждать секунду или около того до разыменования указателя, вы заметите, что данные исчезли к тому моменту, когда вы хотите их прочитать ! Посмотрите на мой пример:

 #include  #include  using namespace std; class myClass { public: myClass() : i{5} { cout < < "myClass ctor" << endl; } ~myClass() { cout << "myClass dtor" << endl; } int i; }; myClass* foo() { myClass a; return &a; } int main() { bool doSleep{false}; auto p = foo(); if (doSleep) sleep(1); cout << p->i < < endl; p->i = 8; cout < < p->i < < endl; } 

(Обратите внимание, что я использовал функцию sleep из unistd.h , которая присутствует только в Unix-подобных системах, поэтому вам нужно будет заменить ее с помощью Sleep(1000) и Windows.h если вы находитесь в Windows.)

Я заменил ваш int classом, поэтому я точно вижу, когда вызывается деструктор.

Результат этого кода следующий:

 myClass ctor myClass dtor 5 8 

Однако, если вы измените doSleep на true :

 myClass ctor myClass dtor 0 8 

Как вы можете видеть, объект, который должен быть уничтожен, фактически уничтожен, но я полагаю, что есть некоторые инструкции по предварительной очистке, которые должны выполняться до того, как объект (или просто переменная) будет уничтожен, поэтому до тех пор, пока это не будет выполнено, данные is still accessible for a short period of time (however there's no guarantee for that of course, so please don't write code that relies on this).

This is very weird, since the destructor is called immediately upon exiting the scope, however, the actual destruction is slightly delayed.

I never really read the part of the official ISO C++ standard that specifies this behavior, but it might very well be, that the standard only promises that your data will be destroyed once it goes out of scope, but it doesn't say anything about this happening immediately, before any other instruction is executed. If this is the case, than this behavior is completely fine, and people are just misunderstanding the standard.

Or another cause could be cheeky compilers that don't follow the standard properly. Actually this wouldn't be the only case where compilers trade a little bit of standard conformance for extra performance!

Whatever the cause of this is, it's clear that the data IS destroyed, just not immediately.

Interesting Posts

Будут ли все 32-разрядные приложения работать в 64-разрядной операционной системе?

Аргументы по умолчанию: * args и ** kwargs

Что такое папка __MACOSX?

Как я могу уменьшить пропускную способность, потребляемую автоматическими обновлениями Windows?

Как справиться с категориальными особенностями с помощью spark-ml?

Angular 2 2.0.0-rc.1 Свойство ‘map’ не существует в типе ‘Observable ‘ не совпадает с сообщением о выпуске

Hibernate – @ElementCollection – странное поведение удаления / вставки

Android Endless List

Скрытие закрытий в Swift

Как разобрать динамический ключ JSON в результате результата Nested JSON?

Анализ формата даты ISO 8601 типа 2015-06-27T13: 16: 37.363Z в Java

MongoDB – аргумент $ size должен быть массивом, но имеет тип: EOO

Ошибка узла: SyntaxError: неожиданный импорт маркера

Компиляция java-программы в исполняемый файл

Определить 32-битную или 64-битную папку установки Windows 7?

Давайте будем гением компьютера.