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...
Пока на собственное сообщение не было ответов, его можно удалить.