Почему код работает?

182
18 апреля 2019, 17:30
#include "pch.h"
#include <iostream>
using namespace std;
void innermostFun(int* ptrValue)
{
    *ptrValue = 10;
}
int* middleFun(int* ptrValue)
{
    innermostFun(ptrValue);
    int someValue = 15;
    return &someValue;
}
int main()
{
    int value = 0;
    int* invalidPtr = middleFun(&value);
    cout << *invalidPtr;
    return 0;
}

Собственно то почему этот код должен выбросить ошибку написано в статье: http://scrutator.me/post/2015/12/30/pointers_demystified_p2.aspx. Почему у меня cout << *invalidPtr; не бросает ошибку?

Answer 1

Вы возвращаете указатель на локальную переменную someValue, которая после выхода из функции не существует:

int* middleFun(int* ptrValue)
{
    innermostFun(ptrValue);
    int someValue = 15;
    return &someValue;
}

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

Answer 2

Небольшой пример:

class task {
public:
    explicit task(int _priority)
        : m_priority(_priority)
    {}
public:
    int priority_a() const { return m_priority; }
    int priority_b() const { return 0x00; }
protected:
    int m_priority;
};
task* create_task() {
    task t { 0xFF };
    return &t;
}
int main() {
    task *t = create_task();
    t->priority_b();
    return t->priority_a();
}

Скомпилировал с флагом -fomit-frame-pointer(спасибо @vladnimof), оптимизация выключена.

Метод task::priority_a:

mov QWORD PTR [rsp-8], rdi       // извлекаем указатель на объект
mov rax, QWORD PTR [rsp-8]       // ... и как-то его используем
mov eax, DWORD PTR [rax]
ret

Метод task::priority_b:

mov QWORD PTR [rsp-8], rdi       // извлекаем указатель на объект
mov eax, 0                       // ... но никак его не используем
ret

Функция main:

sub rsp, 24
call create_task()
mov QWORD PTR [rsp+8], rax       // поместили указатель на объект в стек
mov rax, QWORD PTR [rsp+8]       // прочитали из стека :)
mov rdi, rax                     // передали как первый аргумент
call task::priority_b() const
mov rax, QWORD PTR [rsp+8]       // аналогично
mov rdi, rax                     // аналогично
call task::priority_a() const
nop
add rsp, 24
ret

Функция create_task:

sub rsp, 24
lea rax, [rsp+12]
mov esi, 255
mov rdi, rax
call task::task(int)
mov eax, 0                       // А в eax то уже ноль!
add rsp, 24
ret
Answer 3

Так как внутренние переменные хранятся в стеке и программы ничто не зачищают (оптимизаторы ещё те) то можно хакнуть какую нибудь функцию на пароли / номера карточек и т.д. Ваш код будет работать до тех пока оптимизация компилятора всё не испортит. Чтобы ваш код работал нужно переменную делать volatile (Выполнить несмотря ни на что - есть!). Вот работающий пример использовать память не по назначению:

// > g++ -Wall -Wextra -Wpedantic -Os stacksecret.cpp
// > g++ -Wall -Wextra -Wpedantic -Os -S stacksecret.cpp
// warning: address of local variable ‘s’ returned [-Wreturn-local-addr]
# include <iostream>
typedef
struct ssecret {
int x ;
int y ;
}
secret ;
secret * f  ( ) {
  secret s ;
  s . x = 666 ;
  s . y = 999 ; 
  return & s ; }
/*  leaq    -8(%rsp), %rax
    ret */
secret volatile * v  ( ) {
  secret volatile s ;
  s . x = 666 ;
  s . y = 999 ; 
  return & s ; }    
  /*movl    $666, -8(%rsp)
    leaq    -8(%rsp), %rax
    movl    $999, -4(%rsp)
    ret  */
int main  ( ) {
  secret * ns = f ( ) ;
  std::cout<<"x = "<< ns->x <<" , y = "<< ns->y << std::endl;
      ns -> x = 0 ;
      ns -> y = 0 ;
  secret volatile * s = v ( ) ;
  /*movl    $666, 8(%rsp)
    movl    $999, 12(%rsp)*/
      std::cout<<"x = "<< s->x <<" , y = "<< s->y << std::endl;
      s -> x = 0 ;
      s -> y = 0 ;
  /*movl    $0, 8(%rsp)
    movl    $0, 12(%rsp)
    addq    $24, %rsp*/
 }

---Вывод---

>./a.out 
x = 0 , y = 0
x = 666 , y = 999

В ассемблерном коде видно, что несмотря на оптимизацию в стек всё-таки ставят значения. Функция v возвращает StackPointer-8 как адрес локальной переменной структуры. И можно пользоваться этими данными как угодно. (Раз ничьё - значит моё!) Правильно использовать volatile надо чтобы зачищать память. Вот функция main чистит , знает порядок.

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

Answer 4

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

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

Практически следующий код будет без проблем выполняться правильно:

int *Func()
 {
    int a = 10;
    return &a;
 }
void main()
 {
   int vvv = *Func(); // vvv будет = 10
 }

Пользоваться такой возможностью безусловно не рекомендуется.

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

READ ALSO
Динамическое изменение типа std::vector

Динамическое изменение типа std::vector

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

166
PSQLException ошибка

PSQLException ошибка

При тестировании кода выдаёт exception

131
Выбор одной записи из RethinkDB на Java

Выбор одной записи из RethinkDB на Java

Как можно извлечь одну запись из таблицы RethinkDB на Java? Я попробовал сделать так, как написано в документации:

146
Понятие абстрактного типа данных

Понятие абстрактного типа данных

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

177