Насколько корректно использовать адрес переменной в стеке
От: Nick-77  
Дата: 19.07.17 09:51
Оценка: -1
Вот в таком случае:


есть какой-то структурный тип MType

и функция:

static MType *f(MType *val, int size){
    MType tmp; 
    if (val)
        tmp = *val;
        else 
        // do smth to create new 
    ;
    // .....
    return &tmp;
}



а используется функция только с помощью макрообёртки
#define f_construct(val, size)     *f( (val), (size) )



т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.


Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.
Re: Насколько корректно использовать адрес переменной в стек
От: Лазар Бешкенадзе СССР  
Дата: 20.07.17 03:50
Оценка: +4
Здравствуйте, Nick-77, Вы писали:

N7>
N7>static MType *f(MType *val, int size){
N7>    MType tmp; 
N7>    if (val)
N7>        tmp = *val;
N7>        else 
N7>        // do smth to create new 
N7>    ;
N7>    // .....
N7>    return &tmp;
N7>}
N7>


N7>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.


Адрес то существует.

Я не знаю как сегодня но в 1998 это был отстой. Было понятие signal. Между выходом из функции и попыткой использовать содержимое по этому адресу может произойти туча всего и этот стек будет перезаписан 10 раз.
Отредактировано 20.07.2017 3:53 Лазар Бешкенадзе . Предыдущая версия .
Re: Насколько корректно использовать адрес переменной в стеке
От: icWasya  
Дата: 20.07.17 06:55
Оценка:
Здравствуйте, Nick-77, Вы писали:

...

N7>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.



N7>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.


Если после выхода из функции произойдёт прерывание , то его обработчик всё затрёт .
Re[2]: Насколько корректно использовать адрес переменной в стеке
От: kov_serg Россия  
Дата: 20.07.17 07:22
Оценка:
Здравствуйте, icWasya, Вы писали:

W>Здравствуйте, Nick-77, Вы писали:


W>...


N7>>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.



N7>>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.

Еще бывает что стек растёт в другую сторону на некоторых архитектурах.
  Скрытый текст
// ptsk.h : popup multitasking
#ifndef __PTSK_H__
#define __PTSK_H__

#include <setjmp.h>
#include "ticks.h"                   // any real time clock

#ifdef __cplusplus
extern "C" {
#endif

typedef struct ptsk_tcb_tag {
    void (*task)(void*);
    void *args;
    int   stack_size;
    jmp_buf ctx;
    struct ptsk_tcb_tag* next;
    struct ptsk_tcb_tag* prev;
} ptsk_tcb_t;

void ptsk_init(void);
void ptsk_done(void);
void ptsk_idle(void);                // give control to next task
void ptsk_addtask(ptsk_tcb_t *task); // create new task
void ptsk_deltask(ptsk_tcb_t *task); // if current it'll die
void ptsk_die(void);                 // kill current task
ptsk_tcb_t* ptsk_getcurrent();       // get current task descriptor
extern int ptsk_active;

void ptsk_sleep(ticks_t ticks);
void ptsk_sleep_till(ticks_t time);

/*

usage:

#include <stdio.h>
#include "ptsk.h"

void task1(void* arg) {
    for(int i=0;i<=9;++i) {
        printf("task1 \ti=%d\n",i);
        ptsk_idle();
    }
}
void task2(void* arg) {
    for(int i=0;i<=7;++i) {
        printf("task2 \t\ti=%d\n",i);
        ptsk_sleep(500);
    }
}
void task3(void* arg) {
    for(int i=0;i<=5;++i) {
        printf("task3 \t\t\ti=%d\n",i);
        ptsk_sleep(1000);
    }
}

ptsk_tcb_t t1={task1,(void*)1,1024};
ptsk_tcb_t t2={task2,(void*)2,1024};
ptsk_tcb_t t3={task3,(void*)3,1024};

void main() {
    ptsk_init();
    ptsk_addtask(&t1);
    ptsk_addtask(&t2);
    ptsk_addtask(&t3);
    ptsk_idle();
    ptsk_done();
}

*/

#ifdef __cplusplus
}
#endif

#endif // __PTSK_H__

// ptsk.c
#include "ptsk.h"

#ifdef __cplusplus
extern "C" {
#endif

enum {
    PTSK_1ST =0,
    PTSK_WAKE=1,
    PTSK_NEW =2,
    PTSK_RET =3
};

int         ptsk_active=0;
ptsk_tcb_t* ptsk_head;
ptsk_tcb_t* ptsk_tail;
ptsk_tcb_t* ptsk_curr;
jmp_buf     ptsk_last;
jmp_buf     ptsk_main;

void ptsk_init(void) {
    ptsk_active=0;
    ptsk_head=ptsk_tail=ptsk_curr=0;
}
void ptsk_done(void) {
    if (ptsk_active) longjmp(ptsk_main,PTSK_RET);
}
void ptsk_panic(void) {
    // exit(0);
    for(;;) {}
}
void ptsk_switch(void) {
    ptsk_curr=ptsk_curr->next;
    if (!ptsk_curr) {
        ptsk_curr=ptsk_head;
        if (!ptsk_curr) ptsk_done();
    }
    longjmp(ptsk_curr->ctx,PTSK_WAKE);
}
void ptsk_die(void) {
    if (!ptsk_curr) { ptsk_done(); ptsk_panic(); return; } // panic
    if (ptsk_curr->prev) ptsk_curr->prev->next=ptsk_curr->next; else ptsk_head=ptsk_curr->next;
    if (ptsk_curr->next) ptsk_curr->next->prev=ptsk_curr->prev; else ptsk_tail=ptsk_curr->prev;
    ptsk_switch();
}
void ptsk_deltask(ptsk_tcb_t *task) {
    if (ptsk_curr==task) ptsk_die();
    if (task->prev) task->prev->next=task->next; else ptsk_head=task->next;
    if (task->next) task->next->prev=task->prev; else ptsk_tail=task->prev;
}
int ptsk_stackalloc(int arg) {
    volatile int dummy; ptsk_tcb_t* tsk; int sz;
    static int* stk;
    if (arg==1) stk=(int*)&dummy;
    sz=(int*)&dummy-stk; if (sz<0) sz=-sz;
    if (sz<ptsk_tail->stack_size) ptsk_stackalloc(0);
    tsk=ptsk_tail;
    if (setjmp(ptsk_last)==PTSK_NEW) ptsk_stackalloc(1);
    switch(setjmp(tsk->ctx)) {
        case PTSK_1ST:  longjmp(ptsk_main,PTSK_RET);
        case PTSK_WAKE: tsk->task(tsk->args);
    }
    ptsk_die(); return dummy;
}
void ptsk_addtask(ptsk_tcb_t *task) {
    ptsk_tcb_t* last;
    last=ptsk_tail;
    task->prev=ptsk_tail;
    task->next=0;
    if (ptsk_tail) ptsk_tail->next=task; else ptsk_head=task;
    ptsk_tail=task;
    switch(setjmp(ptsk_main)) {
        case PTSK_1ST:  if (last) longjmp(ptsk_last,PTSK_NEW); else ptsk_stackalloc(1);
        //case PTSK_RET: break;
    }
}
void ptsk_idle(void) {
    if (!ptsk_curr) {
        ptsk_curr=ptsk_head; if (!ptsk_curr) return;
        ptsk_active=1;
        switch(setjmp(ptsk_main)) {
            case PTSK_1ST: ptsk_curr->task(ptsk_curr->args); ptsk_die();
            //case PTSK_RET: break;
        }
        ptsk_active=0;
        return;
    }
    switch(setjmp(ptsk_curr->ctx)) {
        case PTSK_1ST:  ptsk_switch();
        //case PTSK_WAKE: break;
    }
}
ptsk_tcb_t* ptsk_getcurrent() {
    return ptsk_curr;
}
void ptsk_sleep_till(ticks_t time) {
    do {
        ptsk_idle();
    } while(time-getticks()>0);
}
void ptsk_sleep(ticks_t ticks) {
    ptsk_sleep_till(getticks()+ticks);
}

#ifdef __cplusplus
}
#endif

W>Если после выхода из функции произойдёт прерывание , то его обработчик всё затрёт .
Это только не в защищённых режимах такая фигня. Бывает
Re: Насколько корректно использовать адрес переменной в стеке
От: Mr.Delphist  
Дата: 20.07.17 10:07
Оценка: +3
Здравствуйте, Nick-77, Вы писали:

N7>есть какой-то структурный тип MType


N7>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.


N7>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.


Зачем так делать? Asking for trouble?
Re: Насколько корректно использовать адрес переменной в стеке
От: N. I.  
Дата: 20.07.17 13:25
Оценка:
Nick-77:

N7>и функция:


N7>
N7>static MType *f(MType *val, int size){
N7>    MType tmp; 
N7>    if (val)
N7>        tmp = *val;
N7>        else 
N7>        // do smth to create new 
N7>    ;
N7>    // .....
N7>    return &tmp;
N7>}
N7>


N7>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.


Теоретически реализации никто не мешает вернуть из этой функции нулевой указатель вместо адреса умершего объекта, задокументировав, что invalid pointer value иногда может вести себя, как null pointer value. И, кстати, это был бы вполне резонный подход, дабы сразу бить по рукам тех, кто попытается что-либо прочитать или записать по "неправильному" адресу.
Re[3]: Насколько корректно использовать адрес переменной в стеке
От: cures Россия cures.narod.ru
Дата: 20.07.17 13:38
Оценка: +1 :)
Здравствуйте, kov_serg, Вы писали:

_>Это только не в защищённых режимах такая фигня. Бывает


Что мешает прерыванию случиться при работе программы в защищённом режиме? Вот если (асинхронные) прерывания запретили, то, наверное, шансов меньше. Но, во-первых, не уверен насчёт NMI, во-вторых, гипервизор вроде бы может прерывать даже выполнение в нулевом кольце, скажем, по сигналу с карты управления (отдельный Ethernet-интерфейс), и не факт, что он стек не затрёт. Ну и плюс крайне редкий вариант, что перед возвратом вдруг почему-то переполнилось TLB, соответственно, после возврата может произойти (синхронное) прерывание по неотмапленной странице (кода, или данных).
Так что такие возвращения — однозначный поиск неприятностей, для себя или для будущей поддержки. Если не платят зарплату — самое то
Re[4]: Насколько корректно использовать адрес переменной в стеке
От: Лазар Бешкенадзе СССР  
Дата: 20.07.17 13:47
Оценка:
Здравствуйте, cures, Вы писали:

_>>Это только не в защищённых режимах такая фигня. Бывает


C>Что мешает прерыванию случиться при работе программы в защищённом режиме?


Мне кажется товарищ имеет ввиду что в защищенных режимах переключается стек. Но насколько помню я (а изучал я I32 в 1993 году) возможны прерывания и с переключением стека и без оного.
Re: Насколько корректно использовать адрес переменной в стеке
От: uzhas Ниоткуда  
Дата: 20.07.17 15:02
Оценка:
Здравствуйте, Nick-77, Вы писали:

N7>насколько корректно


не надо так делать
Re[2]: Насколько корректно использовать адрес переменной в стеке
От: Nick-77  
Дата: 21.07.17 08:22
Оценка:
Здравствуйте, Mr.Delphist, Вы писали:

MD>Здравствуйте, Nick-77, Вы писали:


N7>>есть какой-то структурный тип MType


N7>>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.


N7>>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.


MD>Зачем так делать? Asking for trouble?




Что-то типа проверки границ допустимого
Re[2]: Насколько корректно использовать адрес переменной в стеке
От: Nick-77  
Дата: 21.07.17 08:23
Оценка:
NI>Теоретически реализации никто не мешает вернуть из этой функции нулевой указатель вместо адреса умершего объекта, задокументировав, что invalid pointer value иногда может вести себя, как null pointer value. И, кстати, это был бы вполне резонный подход, дабы сразу бить по рукам тех, кто попытается что-либо прочитать или записать по "неправильному" адресу.


Никто, кроме стандарта, вроде.
Re[2]: Насколько корректно использовать адрес переменной в стеке
От: Nick-77  
Дата: 21.07.17 08:44
Оценка:
Здравствуйте, icWasya, Вы писали:

N7>>т.е. вроде как несмотря на то, что формально tmp и её адреса после выхода из f не существует, реально стек ещё никто не успел (?) испортить и содержимое верное.


W>Если после выхода из функции произойдёт прерывание , то его обработчик всё затрёт .


Исчерпывающе, спасибо!
Re[3]: Насколько корректно использовать адрес переменной в стеке
От: N. I.  
Дата: 21.07.17 11:27
Оценка:
Nick-77:

NI>>Теоретически реализации никто не мешает вернуть из этой функции нулевой указатель вместо адреса умершего объекта, задокументировав, что invalid pointer value иногда может вести себя, как null pointer value. И, кстати, это был бы вполне резонный подход, дабы сразу бить по рукам тех, кто попытается что-либо прочитать или записать по "неправильному" адресу.


N7>Никто, кроме стандарта, вроде.


C11 — 6.2.4/2:

If an object is referred to outside of its lifetime, the behavior is undefined. The value of a pointer becomes indeterminate when the object it points to (or just past) reaches the end of its lifetime.


C11 — 3.19.2:

indeterminate value
either an unspecified value or a trap representation


C11 — 3.19.3:

unspecified value
valid value of the relevant type where this International Standard imposes no requirements on which value is chosen in any instance


Т.е. в C указатель на умерший объект вполне может стать нулевым.

C++14 плохо описывает способы инвалидации указателей, но при подготовке C++17 над этой проблемой уже успели поработать.

C++ N4660 [basic.stc]/4:

When the end of the duration of a region of storage is reached, the values of all pointers representing the address of any part of that region of storage become invalid pointer values (6.9.2). Indirection through an invalid pointer value and passing an invalid pointer value to a deallocation function have undefined behavior. Any other use of an invalid pointer value has implementation-defined behavior. [Footnote: Some implementations might define that copying an invalid pointer value causes a system-generated runtime fault.]


C++ N4660 [defns.impl.defined]

implementation-defined behavior
behavior, for a well-formed program construct and correct data, that depends on the implementation and that each implementation documents


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

Но это только одна из потенциально возможных причин, по которым данный хак может оказаться нерабочим. Когда компилятор захочет соптимизировать такой код, там может получиться что угодно. И если с текущей версией компилятора видимых проблем не наблюдается, то при обновлении на новую версию можно получить сюрприз.
Re[2]: Насколько корректно использовать адрес переменной в стеке
От: N. I.  
Дата: 21.07.17 12:27
Оценка: 4 (1)
N. I.:

NI>Теоретически реализации никто не мешает вернуть из этой функции нулевой указатель вместо адреса умершего объекта


Оказывается, G++ версий 5 и выше именно так и делает:

https://wandbox.org/permlink/zlYxL85EE1LVJBay
Re[4]: Насколько корректно использовать адрес переменной в стеке
От: Лазар Бешкенадзе СССР  
Дата: 21.07.17 13:23
Оценка:
Здравствуйте, N. I., Вы писали:

NI>C11 — 6.2.4/2:

NI>C++ N4660 [basic.stc]/4:

Какая гадость! Какая гадость эта ваша заливная рыба!
Re: Насколько корректно использовать адрес переменной в стеке
От: Masterspline  
Дата: 21.07.17 13:58
Оценка: +1
Вот тут
Автор: plastictown
Дата: 17.07.17
обсудили. Ты с того же собеседования?
Re: Насколько корректно использовать адрес переменной в стеке
От: Кодт Россия  
Дата: 24.07.17 08:48
Оценка:
Здравствуйте, Nick-77, Вы писали:

N7>Вот в таком случае:

<>
N7>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.

Если цель не в том, чтобы (контролируемо) выстрелить себе в ногу, а в том, чтобы вернуть значение по указателю, то
1) понадеяться на оптимизации NRVO
2) поклясться на времени жизни
3) сделать вот так
MType f_construct(MType* val, int size) {  // может быть и не инлайновым
  MType tmp;
  // пост-инициализация переменной tmp
  .....
  // NRVO, прииди!
  return tmp;
}

#define f_by_pointer(val, size)  (&(f_construct((val), (size))))

// ну и пример использования
MType v = f_construct(f_by_pointer(nullptr, 123), 456);
// хотя можно и без макроса обойтись
MType w = f_construct(&f_construct(nullptr, 123), 456);

Обрати внимание, что f_construct с самого начала (ещё будучи макросом) возвращал значение. Так пускай же именно это делает функция, без лишних танцев с бубном.
Перекуём баги на фичи!
Re[2]: Насколько корректно использовать адрес переменной в стеке
От: Nick-77  
Дата: 26.07.17 08:40
Оценка:
Здравствуйте, Кодт, Вы писали:

К>Здравствуйте, Nick-77, Вы писали:


N7>>Вот в таком случае:

К><>
N7>>Хотелось бы узнать у господ знатоков, насколько корректно подобное допущение.

К>Если цель не в том, чтобы (контролируемо) выстрелить себе в ногу, а в том, чтобы вернуть значение по указателю, то

К>1) понадеяться на оптимизации NRVO
К>2) поклясться на времени жизни
К>3) сделать вот так
К>
К>MType f_construct(MType* val, int size) {  // может быть и не инлайновым
К>  MType tmp;
К>  // пост-инициализация переменной tmp
К>  .....
К>  // NRVO, прииди!
К>  return tmp;
К>}

К>#define f_by_pointer(val, size)  (&(f_construct((val), (size))))

К>// ну и пример использования
К>MType v = f_construct(f_by_pointer(nullptr, 123), 456);
К>// хотя можно и без макроса обойтись
К>MType w = f_construct(&f_construct(nullptr, 123), 456);
К>

К>Обрати внимание, что f_construct с самого начала (ещё будучи макросом) возвращал значение. Так пускай же именно это делает функция, без лишних танцев с бубном.

Спасибо, но как обойти-то понятно, обидно просто, что нет механизма, позволяющего "немного" (!) подержать стек после возврата из функции
Re[3]: Насколько корректно использовать адрес переменной в стеке
От: watchmaker  
Дата: 26.07.17 10:36
Оценка:
Здравствуйте, Nick-77, Вы писали:

N7> обидно просто, что нет механизма, позволяющего "немного" (!) подержать стек после возврата из функции


Конечно же такой механизм существует: например на архитектуре x86-64 он называется RedZone. И популярные компиляторы clang и gcc вполне успешно им пользуются.

Только философия тут другая: программисту не нужно задумываться об особенностях реализации RedZone или аналогов на конкретной архитектуре, вместо этого от него требуется лишь написать корректный код на C++.
А уже потом компилятор (точно зная всякие неочевидные детали о том как работает стек на конкретной платформе: как он взаимодействует с прерываниями, когда инвалидируется и.т.п.) выкинет ненужные действия из машинного кода.
Re[3]: Насколько корректно использовать адрес переменной в стеке
От: Кодт Россия  
Дата: 26.07.17 10:57
Оценка: +2
Здравствуйте, Nick-77, Вы писали:

N7>Спасибо, но как обойти-то понятно, обидно просто, что нет механизма, позволяющего "немного" (!) подержать стек после возврата из функции


Возврат значения — это и есть немного подержать стек

Альтернативное решение — передать буфер в функцию, как параметр. В том числе, как дефолтный параметр, но там придётся повыкручиваться.
http://ideone.com/MTulkq
Я не сторонник такого трюкачества, но вдруг пригодится.

Почему нельзя подержать стек целиком (ну или как-то отдать это на откуп вызываемой функции — сколько там стека придержать)?
Потому что это изрядно меняет протокол работы со стеком. И ради одной, к тому же, очень опасной и ошибкоопасной, фичи, переделывать как бинарную совместимость, так и корректировать стандарт про время жизни объектов, мало кто захочет.

Вот смотри.
Стек состоит из кадров и управляется парой указателей: на начало кадра (для x86 это EBP) и на вершину стека (это ESP).

Функция, создающая свой кадр, делает следующее:
— сохраняет старый указатель кадра на вершине стека, — push ebp
— объявляет вершину стека началом своего кадра, — mov ebp, esp
— заодно, резервирует сколько-то места под локальные переменные — sub esp, N
Или, одной инструкцией, enter N. На других архитектурах и на множественных стеках (например, если стек плавающей арифметики отдельный) делается нечто подобное.
(Перед выходом, достаточно будет прыгнуть на начало кадра — mov esp, ebp — и сделать pop ebp; сокращённо, инструкция leave).

После чего функция обращается к локальным и временным объектам по смещению от ebp. (Отрицательное смещение — локальные переменные, положительное — аргументы).
Однако, если функция не создаёт свой кадр (это один из приёмов оптимизации), либо если она внутри резервирует переменное место на стеке (alloca или сишные переменные массивы), то ей приходится обращаться к некоторым или всем локальным объектам по смещению от esp.

В случае, если все вызываемые из неё функции придерживаются инварианта "esp сразу перед вызовом равен esp сразу после вызова", то компилятору не составит труда следить — насколько esp убежал от точки входа в нашу функцию, и какое смещение нужно подставлять.

Если в языке есть атрибут "вот эта функция шалит и нарушает инвариант", тогда компилятор перед вызовами её будет принудительно создавать кадр на вызывающей стороне, а как работать с кадрами и даже с цепочками кадров, — очевидно. Бегать по ebp, [ebp+1], [ebp+1]+1].
Если же такого атрибута нет, то нарушение инварианта будет огромным сюрпризом.

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

Конечно, было бы круто, если бы прямо из коробки и задёшево можно было обеспечивать инкапсуляцию и полиморфизм
Base f() {
  if (rand()) return Derived1();  // сейчас мы отхватим срезку до Base, а хотелось бы
  else        return Derived2();  // вернуть разные типы, да ещё и разных размеров
}
int(g())[] {
 int t[rand()]; // создали и заполнили массив произвольной (в рамках разумного) длины
 return t; // сейчас мы отхватим деградацию до указателя и убъём локальную переменную
}

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