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: Насколько корректно использовать адрес переменной в стеке
От: Mr.Delphist  
Дата: 20.07.17 10:07
Оценка: +3
Здравствуйте, Nick-77, Вы писали:

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


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


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


Зачем так делать? Asking for trouble?
Re[3]: Насколько корректно использовать адрес переменной в стеке
От: cures Россия cures.narod.ru
Дата: 20.07.17 13:38
Оценка: +1 :)
Здравствуйте, kov_serg, Вы писали:

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


Что мешает прерыванию случиться при работе программы в защищённом режиме? Вот если (асинхронные) прерывания запретили, то, наверное, шансов меньше. Но, во-первых, не уверен насчёт NMI, во-вторых, гипервизор вроде бы может прерывать даже выполнение в нулевом кольце, скажем, по сигналу с карты управления (отдельный Ethernet-интерфейс), и не факт, что он стек не затрёт. Ну и плюс крайне редкий вариант, что перед возвратом вдруг почему-то переполнилось TLB, соответственно, после возврата может произойти (синхронное) прерывание по неотмапленной странице (кода, или данных).
Так что такие возвращения — однозначный поиск неприятностей, для себя или для будущей поддержки. Если не платят зарплату — самое то
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; // сейчас мы отхватим деградацию до указателя и убъём локальную переменную
}

Но обычно такие вещи делаются через кучу. Даже в языках, где работа с кучей неявная, — например, в функциональных.
Перекуём баги на фичи!
Re[7]: Насколько корректно использовать адрес переменной в с
От: Кодт Россия  
Дата: 26.07.17 14:59
Оценка: +2
Здравствуйте, watchmaker, Вы писали:

W>То есть там просто вместо готового объекта передаётся указатель на alignas(T) char buffer[sizeof(T)], в который потом можно сделать placement_new и вернуть указатель на новый объект.


Кстати про aligned storage. (И, в частности, std::aligned_storage, чтобы не переизобретать).
Деструктор-то ведь тоже придётся вызывать вручную.

Более правильный подход — использовать в качестве буфера std::optional или его аналоги.
Перекуём баги на фичи!
Re[8]: Насколько корректно использовать адрес переменной в с
От: watchmaker  
Дата: 26.07.17 14:44
Оценка: 34 (1)
Здравствуйте, Nick-77, Вы писали:


N7>тык у меня ж речь о голом С, а он разве подправляет заголовки функций, ведь получается, что допустим struct Mtype f(int a); должна быть преобразована НЕЯВНО в struct * Mtype f(Struct *val, int a);


N7>что, мягко говоря, вызывает вопросы как это чудо будет линоваться, особенно с не-С кодом, с каким-нить пайтоном...


N7>?


Это правильный вопрос. Конкретный ответ на него даёт ABI. И обычно ответ: все должны делать эту замену (неявно, при совпадении некоторых условий).

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

Просто что касается того же Python, то внутри у него (у CPython) возврат структур сделан через собственную виртуальную машину и там вопрос про стек немного не имеет смысла. Но если нужно вызвать из Python какой-нибудь бинарный модуль, написанный на C, который при этом возвращает структуру, то такая замена произойдёт.
Но, конечно, в самом интерпретаторе Python об таких деталях не особо упоминают просто из-за того, что это немного offtopic. А Python делегирует эту низкоуровневую работу либо соответствующему компилятору (если собираем модуль на C, например), либо libffi (если подключаем модуль в run-time).
Отредактировано 26.07.2017 14:44 watchmaker . Предыдущая версия .
Re[2]: Насколько корректно использовать адрес переменной в стеке
От: swingus  
Дата: 08.09.17 01:35
Оценка: 5 (1)
То ли в 17м, то ли в 20 стандарте NRVO обязали делать. Теперь можно возвращать moveonly типы.

Здравствуйте, Кодт, Вы писали:


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

К>1) понадеяться на оптимизации NRVO
Re[2]: Насколько корректно использовать адрес переменной в стеке
От: N. I.  
Дата: 21.07.17 12:27
Оценка: 4 (1)
N. I.:

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


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

https://wandbox.org/permlink/zlYxL85EE1LVJBay
Насколько корректно использовать адрес переменной в стеке
От: 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: Насколько корректно использовать адрес переменной в стеке
От: Masterspline  
Дата: 21.07.17 13:58
Оценка: +1
Вот тут
Автор: plastictown
Дата: 17.07.17
обсудили. Ты с того же собеседования?
Re[5]: Насколько корректно использовать адрес переменной в стеке
От: Кодт Россия  
Дата: 26.07.17 12:52
Оценка: +1
Здравствуйте, watchmaker, Вы писали:

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

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

W>Честно говоря, если прочитать например SystemV ABI, то можно увидеть, что это автоматически происходит для всех структур (размер которых больше некоторого порогового значения).

W>То есть всё это извращение с передачей дефолтного параметра не нужно.

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

W>Поэтому такой трюк с дефолтным параметром сугубо вредный: он явно выражает то, что и так записано в ABI как необходимое действие.


Насчёт сугубо-вредности — тут можно пообсуждать.

Во-первых, мы не надеемся на NRVO, а вручную гарантируем это. И заодно, снимаем требование о копируемости-перемещаемости с типа. (Правда, добавляя некоторую дефолтную инициализацию и вторую фазу).
Во-вторых, с этим же трюком мы можем отдавать буфер произвольного вида. Ну самое простое,
#include <iostream>
#include <cstring>
using namespace std;

template<int N> struct charholder { char buf[N]; };

int main() {
  cout << strcat(strcat(strcpy(charholder<100>().buf,
                               "alfa"),
                        " + "),
                 "beta")
       << endl;
}

Здесь параметр не дефолтный, а очень даже явный, но суть та же.
Перекуём баги на фичи!
Re[7]: Насколько корректно использовать адрес переменной в стеке
От: Кодт Россия  
Дата: 26.07.17 14:52
Оценка: +1
Здравствуйте, watchmaker, Вы писали:

К>>Во-первых, мы не надеемся на NRVO

W>Тут как бы дело в том, что такого поведения требует ABI, а не стандарт C++. То есть NRVO тут вообще не при чём. Так делать компилятор обязан всегда, ибо программа должна удовлетворять требованиям из обоих этих документов.

ABI требует, чтобы вызывающая сторона передала буфер в функцию и получила туда ответ.
А будет ли это сделано задёшево или задорого, об этом ничего не сказано. И более того, — уже стандарт требует, чтобы компилятору разрешили делать это задорого (тип должен быть Copyable/Moveable), одновременно сам же разрешая компилятору делать это задёшево (NRVO).

К>> И заодно, снимаем требование о копируемости-перемещаемости с типа. (Правда, добавляя некоторую дефолтную инициализацию и вторую фазу).

W>Ага. Правда компилятор без этого трюка ещё и сам автоматически разрешит ситуацию, когда, например, у класса нет дефолтного инициализатора.

Ну уж тут "Направо пойдёшь — коня потеряешь, налево пойдёшь — сама тебя прибью. Твоя Василиса"

Конечно, рукосипедить нужно только тогда, когда исчерпаны обычные решения. То есть, если у нас жёсткие ограничения на свойства типа, или если профайлер нам здесь показал, что NRVO не выполняется, и что нам от этого плохо.
Перекуём баги на фичи!
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: Насколько корректно использовать адрес переменной в стеке
От: 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[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[4]: Насколько корректно использовать адрес переменной в стеке
От: Лазар Бешкенадзе СССР  
Дата: 21.07.17 13:23
Оценка:
Здравствуйте, N. I., Вы писали:

NI>C11 — 6.2.4/2:

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

Какая гадость! Какая гадость эта ваша заливная рыба!
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[4]: Насколько корректно использовать адрес переменной в стеке
От: watchmaker  
Дата: 26.07.17 12:20
Оценка:
Здравствуйте, Кодт, Вы писали:

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


К>Альтернативное решение — передать буфер в функцию, как параметр. В том числе, как дефолтный параметр, но там придётся повыкручиваться.

К>http://ideone.com/MTulkq
К>Я не сторонник такого трюкачества, но вдруг пригодится.

Честно говоря, если прочитать например SystemV ABI, то можно увидеть, что это автоматически происходит для всех структур (размер которых больше некоторого порогового значения).
То есть всё это извращение с передачей дефолтного параметра не нужно. Программисту достаточно написать просто
BigStruct foo() {
    BigStruct r;
    ...
    return r;
}

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



Единственный нюанс тут в том, что есть несколько разных ABI — формально нужно смотреть на версию своей для платформы. Но, к счастью, в вопросе об возврате структур по значению они практически все единогласны.
Re[6]: Насколько корректно использовать адрес переменной в с
От: watchmaker  
Дата: 26.07.17 13:17
Оценка:
Здравствуйте, Кодт, Вы писали:


W>>Поэтому такой трюк с дефолтным параметром сугубо вредный: он явно выражает то, что и так записано в ABI как необходимое действие.


К>Насчёт сугубо-вредности — тут можно пообсуждать.


К>Во-первых, мы не надеемся на NRVO


Тут как бы дело в том, что такого поведения требует ABI, а не стандарт C++. Так делать компилятор обязан всегда, ибо программа должна удовлетворять требованиям из обоих этих документов.

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

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

К> И заодно, снимаем требование о копируемости-перемещаемости с типа. (Правда, добавляя некоторую дефолтную инициализацию и вторую фазу).


Ага. Правда компилятор без этого трюка ещё и сам автоматически разрешит ситуацию, когда, например, у класса нет дефолтного инициализатора.
То есть там просто вместо готового объекта передаётся указатель на alignas(T) char buffer[sizeof(T)], в который потом можно сделать placement_new и вернуть указатель на новый объект.
Отредактировано 26.07.2017 14:51 watchmaker . Предыдущая версия .
Re[5]: Насколько корректно использовать адрес переменной в стеке
От: Nick-77  
Дата: 26.07.17 13:20
Оценка:
Здравствуйте, watchmaker, Вы писали:

W>Здравствуйте, Кодт, Вы писали:


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


К>>Альтернативное решение — передать буфер в функцию, как параметр. В том числе, как дефолтный параметр, но там придётся повыкручиваться.

К>>http://ideone.com/MTulkq
К>>Я не сторонник такого трюкачества, но вдруг пригодится.

W>Честно говоря, если прочитать например SystemV ABI, то можно увидеть, что это автоматически происходит для всех структур (размер которых больше некоторого порогового значения).

W>То есть всё это извращение с передачей дефолтного параметра не нужно. Программисту достаточно написать просто
BigStruct foo() {
W>    BigStruct r;
W>    ...
W>    return r;
W>}

W>И компилятор сам сделает замену: зарезервирует память в вызывающей функции и возвратит из функции указатель на r.
W>И это требование! То есть так поступать компилятор обязан даже при полностью выключенной оптимизации. И, конечное же, все компиляторы так и поступают: Пруф.
W>Поэтому такой трюк с дефолтным параметром сугубо вредный: он явно выражает то, что и так записано в ABI как необходимое действие.


вот тут я перестал понимать, ведь это самое r и будет в стеке функции, который помрёт, т.е. от возврата указателя толку мало, или там будет какая-то неявная обёртка в memcpy?
Re[6]: Насколько корректно использовать адрес переменной в стеке
От: watchmaker  
Дата: 26.07.17 13:58
Оценка:
Здравствуйте, Nick-77, Вы писали:

N7>вот тут я перестал понимать, ведь это самое r и будет в стеке функции, который помрёт, т.е. от возврата указателя толку мало, или там будет какая-то неявная обёртка в memcpy?


Память под BigStruct будет выделена в стеке вызывающей функции. Поэтому эта память будет жить и после завершения вызываемой функции. И поэтому функция может вернуть указатель на эту память, так как этот указатель останется валидным даже после завершения foo.
И по этому указателю компилятор будет хранить r (на момент выхода из функции).
Уточнение в скобках важно.

То есть у функции foo есть участок в стеке, который заведомо переживёт время жизни самой foo, и в которой должна размещаться переменная r.

При этом, если переменная r размещается в этом месте всё время своей жизни, то это называется NRVO. Иначе же допускается, что переменная может быть сконструирована на собственном стеке foo, а потом перед выходом скопирована в указанную память, в том числе и через memcpy (эта ситуация называется "NRVO не сработал").

В общем-то Кодт очень хорошо написал выше вручную код, который компилятор сам делает. Можно сверится с ним.
Re[4]: Насколько корректно использовать адрес переменной в стеке
От: Pzz Россия https://github.com/alexpevzner
Дата: 26.07.17 14:12
Оценка:
Здравствуйте, cures, Вы писали:

C>Что мешает прерыванию случиться при работе программы в защищённом режиме? Вот если (асинхронные) прерывания запретили, то, наверное, шансов меньше. Но, во-первых, не уверен насчёт NMI, во-вторых, гипервизор вроде бы может прерывать даже выполнение в нулевом кольце, скажем, по сигналу с карты управления (отдельный Ethernet-интерфейс), и не факт, что он стек не затрёт. Ну и плюс крайне редкий вариант, что перед возвратом вдруг почему-то переполнилось TLB, соответственно, после возврата может произойти (синхронное) прерывание по неотмапленной странице (кода, или данных).


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

C>Так что такие возвращения — однозначный поиск неприятностей, для себя или для будущей поддержки. Если не платят зарплату — самое то


Это да.
Re: Насколько корректно использовать адрес переменной в стеке
От: Pzz Россия https://github.com/alexpevzner
Дата: 26.07.17 14:13
Оценка:
Здравствуйте, Nick-77, Вы писали:

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


Совсем никак не корректно.
Re[7]: Насколько корректно использовать адрес переменной в стеке
От: Nick-77  
Дата: 26.07.17 14:16
Оценка:
Здравствуйте, watchmaker, Вы писали:

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


N7>>вот тут я перестал понимать, ведь это самое r и будет в стеке функции, который помрёт, т.е. от возврата указателя толку мало, или там будет какая-то неявная обёртка в memcpy?


W>Память под BigStruct будет выделена в стеке вызывающей функции. Поэтому эта память будет жить и после завершения вызываемой функции. И поэтому функция может вернуть указатель на эту память, так как этот указатель останется валидным даже после завершения foo.

W>И по этому указателю компилятор будет хранить r (на момент выхода из функции).
W>Уточнение в скобках важно.

W>То есть у функции foo есть участок в стеке, который заведомо переживёт время жизни самой foo, и в которой должна размещаться переменная r.


W>При этом, если переменная r размещается в этом месте всё время своей жизни, то это называется NRVO. Иначе же допускается, что переменная может быть сконструирована на собственном стеке foo, а потом перед выходом скопирована в указанную память, в том числе и через memcpy (эта ситуация называется "NRVO не сработал").


W>В общем-то Кодт очень хорошо написал выше вручную код, который компилятор сам делает. Можно сверится с ним.



тык у меня ж речь о голом С, а он разве подправляет заголовки функций, ведь получается, что допустим struct Mtype f(int a); должна быть преобразована НЕЯВНО в struct * Mtype f(Struct *val, int a);

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

?
Re[5]: Насколько корректно использовать адрес переменной в с
От: Лазар Бешкенадзе СССР  
Дата: 26.07.17 18:20
Оценка:
Здравствуйте, Pzz, Вы писали:

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


В оригинальном сообщении нигде не сказано что речь идет о пользовательской программе. Как известно операционные системы тоже пишутся на C. И если мы уже в ядре то фраза "переключает стек на стек ядра" становится непонятной.

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

https://software.intel.com/sites/default/files/managed/a4/60/325384-sdm-vol-3abcd.pdf

6.12.1

• If the handler procedure is going to be executed at a numerically lower privilege level, a stack switch occurs.
When the stack switch occurs:
a. The segment selector and stack pointer for the stack to be used by the handler are obtained from the TSS for the currently executing task. On this new stack, the processor pushes the stack segment selector and stack pointer of the interrupted procedure.
b. The processor then saves the current state of the EFLAGS, CS, and EIP registers on the new stack (see Figures 6-4).
c. If an exception causes an error code to be saved, it is pushed on the new stack after the EIP value.
• If the handler procedure is going to be executed at the same privilege level as the interrupted procedure:
a. The processor saves the current state of the EFLAGS, CS, and EIP registers on the current stack (see Figures 6-4).
b. If an exception causes an error code to be saved, it is pushed on the current stack after the EIP value.

...

To return from an exception- or interrupt-handler procedure, the handler must use the IRET (or IRETD) instruction.
The IRET instruction is similar to the RET instruction except that it restores the saved flags into the EFLAGS register. The IOPL field of the EFLAGS register is restored only if the CPL is 0. The IF flag is changed only if the CPL is less than or equal to the IOPL. See Chapter 3, "Instruction Set Reference, A-L," of the Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 2A, for a description of the complete operation performed by the IRET instruction.
If a stack switch occurred when calling the handler procedure, the IRET instruction switches back to the interrupted procedure’s stack on the return.

Отредактировано 26.07.2017 19:07 Лазар Бешкенадзе . Предыдущая версия .
Re[2]: Насколько корректно использовать адрес переменной в стек
От: sts  
Дата: 02.09.17 21:32
Оценка:
Здравствуйте, Лазар Бешкенадзе, Вы писали:

ЛБ>Здравствуйте, 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 раз.
бред
Re[3]: Насколько корректно использовать адрес переменной в стек
От: Лазар Бешкенадзе СССР  
Дата: 03.09.17 15:27
Оценка:
Здравствуйте, sts, Вы писали:

sts>бред


Да уж. Дельфинарий.
Re: Насколько корректно использовать адрес переменной в стеке
От: Molchalnik  
Дата: 07.09.17 20:51
Оценка:
Здравствуйте, Nick-77, Вы писали:

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



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



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

с вероятностью (90+x)% всё будет впорядке. Но с вероятностью (10 — x) % произойдут невероятные баги, которые невозможно будет отследить. И возникнут они не сразу, а через год существования проекта. И получится такой хтонический звиздец, что выть будет вся команда, сроки будут провалены, фирма получит убыток, а виновный кодер... В лучшем случае получит во все дырки, и ещё дополнительную просверлят в боку. А в худшем пойдёт доучивать материал для собеседований. А ещё компилятор может соптимизировать эту конструкцию неожиданным образом. Например, кланговский компилятор не стесняется даже волэйтил игнорировать в некоторых случаях.

Поэтому так никто не делает.
Re[3]: Насколько корректно использовать адрес переменной в стеке
От: N. I.  
Дата: 08.09.17 20:37
Оценка:
swingus:

S>То ли в 17м, то ли в 20 стандарте NRVO обязали делать.


Откуда инфа? В C++17 такого точно нет.

S>Теперь можно возвращать moveonly типы.


Уже в C++11 можно возвращать объекты классового типа, где нет вообще ни одного non-deleted конструктора; с введением в C++17 новых правил для prvalues появилось больше возможных вариантов сформировать валидное return statement. Но NRVO работает с lvalues, и к ней такие новшества никакого отношения не имеют.
Re[4]: Насколько корректно использовать адрес переменной в стеке
От: swingus  
Дата: 09.09.17 03:20
Оценка:
Здесь я вижу лёгкое безумие с вашей стороны. Если нет ни одного non-deleted конструктора (и оператора =), то это вероятно moveable type, а скорее всего copyable, всё зависит от членов класса. А взял я отсюда, например (на самом деле чуть ли не первая ссылка в выдаче):

The new C++17 standard brings many exciting new features. A smaller, more subtle improvement it brings is guaranteed copy elision. The keyword is guaranteed, as copy elision itself has always been part of the standard.

Здравствуйте, N. I., Вы писали:

NI>swingus:


S>>То ли в 17м, то ли в 20 стандарте NRVO обязали делать.


NI>Откуда инфа? В C++17 такого точно нет.


S>>Теперь можно возвращать moveonly типы.


NI>Уже в C++11 можно возвращать объекты классового типа, где нет вообще ни одного non-deleted конструктора; с введением в C++17 новых правил для prvalues появилось больше возможных вариантов сформировать валидное return statement. Но NRVO работает с lvalues, и к ней такие новшества никакого отношения не имеют.
Re[5]: Насколько корректно использовать адрес переменной в стеке
От: N. I.  
Дата: 09.09.17 06:24
Оценка:
swingus:

S>Здесь я вижу лёгкое безумие с вашей стороны. Если нет ни одного non-deleted конструктора (и оператора =), то это вероятно moveable type, а скорее всего copyable, всё зависит от членов класса.


Если нет ни одного non-deleted конструктора, то перемещать или копировать объект в контексте инициализации другого объекта такого же типа попросту нечем.

S>А взял я отсюда, например (на самом деле чуть ли не первая ссылка в выдаче):


S>The new C++17 standard brings many exciting new features. A smaller, more subtle improvement it brings is guaranteed copy elision. The keyword is guaranteed, as copy elision itself has always been part of the standard.


Про NRVO там сказано следующее:

A second consequence is that nothing changes for NVRO in C++17 with guaranteed copy elision. This is because, as mentioned before, the change only involves prvalues. With NVRO, the named value is a glvalue. The authors of the paper acknowledge this but chose to leave it out of scope.

 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.