Вопрос

я бы хотел знать Архитектуры, которые нарушают предположения Я перечислил ниже. Кроме того, я хотел бы знать, является ли какое -либо из предположений ложными для всех архитектур (то есть, если какие -либо из них просто совершенно неверны).

  1. sizeof (int *) == sizeof (char *) == sizeof (void *) == sizeof (func_ptr *)

  2. Представление в памяти всех указателей для данной архитектуры одинаково независимо от указанного типа данных.

  3. Представление указателя в памяти совпадает с целым числом той же длины бита, что и архитектура.

  4. Умножение и разделение типов данных указателя запрещено только компилятором. Примечание: да, я знаю, что это бессмысленно. Я имею в виду - есть ли аппаратная поддержка, чтобы запретить это неправильное использование?

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

  6. Увеличение указателя эквивалентно добавлению sizeof(the pointed data type) к адресу памяти хранится указателем. Если p является int32* тогда p+1 равен адресу памяти 4 байта после p.

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

Это было полезно?

Решение

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

sizeof(int *) == sizeof(char *) == sizeof(void *) == sizeof(func_ptr *)

Я не знаю ни одного систем, где я знать Это должно быть ложным, но подумайте:

Мобильные устройства часто имеют некоторое количество памяти только для чтения, в которой хранится код программы и тому подобное. Значения только для чтения (переменные CONM) могут храниться в памяти только для чтения. А поскольку адресное пространство ПЗУ может быть меньше, чем нормальное адресное пространство ОЗУ, размер указателя также может быть другим. Аналогичным образом, указатели на функции могут иметь другой размер, так как они могут указывать на эту память только для чтения, в которую загружается программа, и которая в противном случае не может быть изменена (поэтому ваши данные не могут храниться в ней).

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

Представление в памяти всех указателей для данной архитектуры одинаково независимо от указанного типа данных.

Подумайте о указателях участников и обычных указателях. У них нет такого же представления (или размера). Указатель участника состоит из this указатель и смещение.

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

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

Зависит от того, как определяется эта длина бита. :) an int На многих 64-битных платформах все еще 32 бита. Но указатель - 64 бита. Как уже говорилось, процессоры с сегментированной моделью памяти будут иметь указатели, состоящие из пары чисел. Кроме того, указатели участников состоят из пары чисел.

Умножение и разделение типов данных указателя запрещено только компилятором.

В конечном счете только типы данных указателей существует в компиляторе. То, с чем работает процессор, не является указателями, а целыми и адресами памяти. Таким образом, больше нигде, где эти операции на типах указателей мог быть запрещенным. С таким же успехом вы можете попросить ЦП запретить конкатенацию объектов строк C ++. Он не может сделать это, потому что тип строки C ++ существует только на языке C ++, а не в сгенерированном машинном коде.

Однако, чтобы ответить на то, что вы иметь в виду, посмотрите на процессоры Motorola 68000. Я считаю, что у них есть отдельные регистры для целых чисел и адресов памяти. Это означает, что они могут легко запретить такие бессмысленные операции.

Все значения указателей могут быть подняты на одно целое число.

Ты там в безопасности. Стандарты C и C ++ гарантируют, что это всегда возможно, независимо от макета пространства памяти, архитектуры процессора и всего остального. В частности, они гарантируют Определенное реализацией картирование. Анкет Другими словами, вы всегда можете преобразовать указатель в целое число, а затем преобразовать это целое число, чтобы получить оригинальный указатель. Но языки C/C ++ ничего не говорят о том, каким должно быть промежуточное целочисленное значение. Это зависит от отдельного компилятора, а оборудование, которое он нацелен.

Увеличение указателя эквивалентно добавлению sizeof (заостренное тип данных) к адресу памяти, хранящимся по указателю.

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

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

Наконец, как я упоминал в комментарии к вашему вопросу, имейте в виду, что C ++ - просто язык. Ему все равно, к какой архитектуре она составлена. Многие из этих ограничений могут показаться неясными для современного процессора. Но что, если вы нацелены на процессор прошлых лет? Что если вы нацеливаетесь на процессор следующего десятилетия? Вы даже не знаете, как они будут работать, поэтому вы не можете многое предположить о них. Что если вы нацеливаете на виртуальную машину? Компиляторы уже существуют, которые генерируют байт -код для Flash, готовые к запуску с сайта. Что если вы хотите скомпилировать свой C ++ в исходный код Python?

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

Другие советы

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

  1. Не требуется стандартом (Смотрите этот вопрос) Например, sizeof(int*) может быть неравенным size(double*). void* гарантированно сможет хранить любую стоимость указателя.
  2. Не требуется стандартом. По определению, размер является частью представления. Если размер может быть другим, представление тоже может быть разным.
  3. Не обязательно. На самом деле, «длина бита архитектуры» является неопределенным утверждением. Что такое 64-битный процессор, на самом деле? Это адресный автобус? Размер регистров? Шина данных? Какая?
  4. Не имеет смысла «умножить» или «разделить» указатель. Это запрещено компилятором, но вы, конечно, можете умножить или разделить основное представление (что на самом деле не имеет смысла для меня), и это приводит к неопределенному поведению.
  5. Может я не понимаю твою точки зрения, но все В цифровом компьютере является лишь какой -то бинарный номер.
  6. Да; Что-то вроде. Это гарантированно укажет на место, которое sizeof(pointer_type) дальше. Это не обязательно эквивалентно арифметическому добавлению числа (т.е. дальше это логическая концепция здесь. Фактическое представление специфично для архитектуры)

Для 6.: Указатель не обязательно является адресом памяти. См., Например, "Заговор великого указателя"По пользователю переполнения стека Джалф:

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

А также:

Указатель - это не адрес памяти! Я упомянул это выше, но скажем снова. Указатели обычно реализуются компилятором просто как адреса памяти, да, но они не должны быть ».

Некоторая дополнительная информация о указателях из стандарта C99:

  • 6.2.5 §27 гарантирует, что void* а также char* Имейте идентичные представления, то есть их можно использовать взаимозаменяемо без преобразования, то есть тот же адрес обозначен тем же рисунком (который не должен быть правдой для других типов указателей)
  • 6.3.2. void* и снова и все еще быть действительным; Это не включает в себя указатели функций!
  • 6.3.2.3 §6 утверждает, что void* можно поднять (и из) целых чисел, а 7.18.1.4 §1 предоставляет апроприяченные типы intptr_t а также uintptr_t; Проблема: эти типы необязательны - стандарт явно упоминает, что не должен быть целого типа, достаточно большой, чтобы фактически удерживать значение указателя!

sizeof(char*) != sizeof(void(*)(void) ? - НЕ на x86 в 36 -битном режиме адресации (поддерживается практически на каждом процессоре Intel с Pentium 1)

«Представление указателя в памяти совпадает с целым числом той же длины бита»,-в любой современной архитектуре нет представления о любой современной архитектуре; Мягленная память никогда не завоевала популярность и уже устарела до того, как C был стандартизирован. Память на самом деле даже не содержит целых чисел, просто биты и, возможно, слова (не байты; большая часть физической памяти не позволяет вам читать всего 8 бит.)

«Умножение указателей невозможно» - 68000 Family; Регистры адресов (те, которые держали указатели), не поддерживали то, что IIRC.

«Все указатели могут быть подняты на целые числа», а не на фото.

«Увеличение T* эквивалентно добавлению sizeof (t) к адресу памяти» - TRUE по определению. Также эквивалентно &pointer[1].

Я не знаю о других, но для DOS предположение в #3 не соответствует действительности. DOS составляет 16 бит и использует различные уловки, чтобы отобразить память на сумму более 16 бит.

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

Я думаю, что это предположение является ложным, потому что на 80186, например, 32-разрядный указатель проводится в двух регистрах (реестр смещения A A Segment Register), и который полусловный, в котором регистр имеет значение во время доступа.

Умножение и разделение типов данных указателя запрещено только компилятором.

Вы не можете умножить или разделить типы. ;П

Я не уверен, почему вы хотели бы размножить или разделить указатель.

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

Стандарт C99 позволяет хранить указатели в intptr_t, который является целочисленным типом. Так да.

Увеличение указателя эквивалентно добавлению sizeof (заостренное тип данных) к адресу памяти, хранящимся по указателю. Если P - int32*, то P+1 равен адресу памяти 4 байта после p.

x + y куда x это T * а также y целое число равносильно (T *)((intptr_t)x + y * sizeof(T)) насколько я знаю. Выравнивание может быть проблемой, но накладка может быть предусмотрена в sizeof. Анкет Я не совсем уверен.

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

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

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

Конкретно:

  1. Представление в памяти всех указателей для данной архитектуры одинаково независимо от указанного типа данных. Правда, за исключением чрезвычайно дурацких прошлых дизайнов, которые пытались реализовать защиту не на сильности на языках, а в аппаратном обеспечении.
  2. Представление указателя в памяти совпадает с целым числом той же длины бита, что и архитектура. Может, конечно, какой -то интегральный тип одинаково, см. LP64 против LLP64.
  3. Умножение и разделение типов данных указателя запрещено только компилятором. Верно.
  4. Все значения указателей могут быть подняты на одно целое число. Другими словами, какие архитектуры все еще используют сегменты и смещения? Сегодня ничто не использует сегменты и смещения, но C int часто не достаточно велик, вам может понадобиться long или же long long Чтобы держать указатель.
  5. Увеличение указателя эквивалентно добавлению sizeof (заостренное тип данных) к адресу памяти, хранящимся по указателю. Если P - int32*, то P+1 равен адресу памяти 4 байта после p. Да.

Интересно отметить, что каждый процессор Intel Architecture, то есть каждый отдельный писатель, содержит сложную сегментацию подразделения эпической, легендарной, сложности. Тем не менее, это эффективно отключено. Всякий раз, когда накапливается ОС ПК, она устанавливает основания сегмента до 0 и длины сегмента ~ 0, выявляя сегменты и давая плоскую модель памяти.

В 1950 -х, 1960 -х и 1970 -х и 1970 -х годах было много архитектур «слов». Но я не могу вспомнить какие -либо основные примеры, в которых был компилятор C. Я помню ICL / Three Rivers Perq Machines В 1980 -х годах это было рассмотрено и имел хранилище управления для записи (микрокод). В одном из его экземпляров был C -компилятор C и аромат Unix, называемый PNX, но компилятор C требовал специального микрокода.

Основная проблема состоит в том, что типы char* на адресах Word, адресованные машинах, неловки, однако вы их реализуете. Вы часто поднимаетесь с sizeof(int *) != sizeof(char *) ...

Интересно, что до C был язык под названием Bcpl в котором основным типом указателя был адрес слова; то есть увеличение указателя дало вам адрес следующего слова, и ptr!1 дал вам слово в ptr + 1. Анкет Был другой оператор для решения байта: ptr%42 Если я помню.

РЕДАКТИРОВАТЬ: Не отвечайте на вопросы, когда уровень сахара в крови низкий. Ваш мозг (конечно, мой) не работает, как вы ожидаете. :-(

Незначительный придир:

P - INT32*, затем P+1

неправильно, это должно быть без знака Int32, иначе он будет завершен на 2 ГБ.

Интересная странность - я получил это от автора компилятора C для чипа транскутера - он сказал мне, что для этого компилятора NULL определяется как -2 ГБ. Почему? Потому что у транскутуры был подписанный диапазон адресов: от -2 ГБ до +2 ГБ. Вы можете поверить в это? Удивительно, не так ли?

С тех пор я встретил разных людей, которые сказали мне, что определение такого нуля сломано. Я согласен, но если вы не в конечном итоге оказали нулевые указатели, находящиеся в середине вашего диапазона адресов.

Я думаю, что большинство из нас могут быть рады, что мы не работаем над транспупутерами!

Я хотел бы знать архитектуры, которые нарушают предположения, которые я перечислил ниже.

Я вижу, что Стивен С упомянул машины Perq, а Msalters упомянул 68000 и фото.

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

sizeof (int *) == sizeof (char *) == sizeof (void *) == sizeof (func_ptr *)?

Не обязательно. Некоторые примеры:

Большинство компиляторов для 8-битных процессоров Гарвард-архитектуры-PIC и 8051 и M8C-Make Sizeof (int *) == sizeof (char *), но отличается от SizeOf (func_ptr *).

Некоторые из очень маленьких чипсов в этих семьях имеют 256 байт оперативной памяти (или меньше), но несколько килобит Progmem (Flash или ROM), поэтому компиляторы часто делают SizeOf (int *) == sizeof (char *) равны 1 (а один 8-битный байт), но размер (func_ptr *) равен 2 (два 8-битных байта).

Компиляторы для многих больших чипов в этих семьях с несколькими килобитами оперативной памяти и около 128 килобит ProgMem Make Sizeof (int *) == sizeof (char *) равный 2 (два 8-битных байта), но размер ( func_ptr *) равна 3 (три 8-битных байта).

Несколько чипов Гарвардской архитектуры могут хранить ровно полное 2^16 ("64Kbyte") Progmem (Flash или ROM) и еще 2^16 ("64Kbyte") из оперативной памяти ввода/вывода с памятью. Компиляторы для такого чипа делают размер (func_ptr *) всегда 2 (два байта); но часто есть способ сделать другие виды указателей Sizeof (int *) == sizeof (char *) == sizeof (void *) в aa "long ptr" 3-байтовый общий указатель У этого есть дополнительный волшебный бит, который указывает, указывает ли этот указатель на RAM или Progmem. (Это тот тип указателя, который вы должны перейти к функции "print_text_to_the_lcd ()", когда вы называете эту функцию из разных подпрограмм, иногда с адресом строки переменной в буфере, который может быть где -либо в оперативной памяти, а в других случаях с одним из многих постоянных строк, которые могут быть где -либо в Прогемме). Такие компиляторы часто имеют специальные ключевые слова («короткие» или «близкие», «длинные» или «далеко»), чтобы программисты специально указали три различных вида указателей ChAR в одной и той же программе - постоянные строки, которым нужно только 2 байта, чтобы указать, где В Progmem они расположены, непостоянные строки, которым нужно только 2 байта, чтобы указать, где в оперативной памяти они находятся, и тип 3-байтовых указателей, которые принимают «print_text_to_the_lcd ()».

Большинство компьютеров, построенных в 1950 -х и 1960 -х годах, используют 36-битная длина слова или 18-битная длина слова, с 18-битной (или меньшей) адресной автобусом. Я слышал, что компиляторы C для таких компьютеров часто используют 9-битные байты, с sizeof (int *) == sizeof (func_ptr *) = 2, который дает 18 бит, поскольку все целые числа и функции должны быть выдвинуты на слово; но sizeof (char *) == sizeof (void *) == 4, чтобы воспользоваться преимуществами Специальные инструкции PDP-10 Это хранит такие указатели в полном 36-битном словом. Это полное 36-битное слово включает в себя 18-битный адрес слова и еще несколько битов в остальных 18-битных, которые (среди прочего) указывают на битовое положение символа заостренного в этом слове.

Представление в памяти всех указателей для данной архитектуры одинаково независимо от указанного типа данных?

Не обязательно. Некоторые примеры:

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

Некоторые компиляторы в некоторых системах используют "Дескрипторы" для реализации указателей символов и других видов указателей. Такой дескриптор другой для указателя, указывающего на первого "Чар" в "char big_array[4000]«чем на указатель, указывающий на первого" Чар "в"char small_array[10]", которые, возможно, являются разными типами данных, даже когда небольшой массив начинается в точности в том же месте в памяти, ранее занятой большим массивом. Дескрипторы позволяют таким машинам ловить и улавливать переполнение буфера, которые вызывают такие проблемы на других машинах.

А "Даты с низким содержанием жиров" Используемые в SafeLite и аналогичных «мягких процессорах» имеют аналогичную «дополнительную информацию» о размере буфера, на который указывает указатель. Указатели с низким содержанием жиров имеют такое же преимущество в переполнениях буфера ловли и улавливания.

Представление указателя в памяти то же самое, что целое число той же длины бита, что и архитектура?

Не обязательно. Некоторые примеры:

В "Tagged Architecture" Машины, каждое слово памяти имеет некоторые биты, которые указывают, является ли это слово целым числом, указатель или что -то еще. С такими машинами, глядя на биты тега, скажет вам, было ли это слово целым числом или указателем.

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

Умножение и разделение типов данных указателя запрещено только компилятором. Примечание: да, я знаю, что это бессмысленно. Я имею в виду - есть ли аппаратная поддержка, чтобы запретить это неправильное использование?

Да, какое -то оборудование не поддерживает такие операции.

Как уже упоминали другие, инструкция «Умножение» в 68000 и 6809 работают только с (некоторые) «регистры данных»; Они не могут быть непосредственно применены к значениям в «Адрес -регистрах». (Компилятору было бы довольно легко обойти такие ограничения - перемещать эти значения из регистра адреса в соответствующий регистр данных, а затем использовать Mul).

Все значения указателей могут быть подчинены в один тип данных?

Да.

Для того чтобы memcpy () правильно работать, Стандарт C требует, чтобы каждое значение указателя любого вида можно поднять на void -указатель («void *»).

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

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

Я не уверен.

Я подозреваю, что все значения указателей могут быть подписаны на интегральные типы данных «size_t» и «ptrdiff_t», определенные в »<stddef.h>".

Увеличение указателя эквивалентно добавлению sizeof (заостренное тип данных) к адресу памяти, хранящимся по указателю. Если P - int32*, то P+1 равен адресу памяти 4 байта после p.

Неясно, о чем вы здесь спрашиваете.

В: Если у меня есть массив какой -либо структуры или примитивного типа данных (например, A "#include <stdint.h> ... int32_t example_array[1000]; ..."), и я увеличиваю указатель, который указывает на этот массив (например," int32_t p = & example_array [99]; ... p ++; ... "), указатель теперь указывает на самый следующий член подряд на этом Массив, который является Sizeof (заостренный тип данных) байты дальше в памяти?

A: Да, компилятор должен сделать указатель, после увеличения его один раз, укажите на следующем независимом последовательном int32_t в массиве, Sizeof (заостренный тип данных) дальше в памяти, чтобы соответствовать стандартам.

Q: Итак, если P - int32*, то P+1 равен адресу памяти 4 байта после P?

A: Когда Sizeof (int32_t) на самом деле равен 4, да. В противном случае, например, для определенных машин с добавлением слов, включая некоторые современные DSP, где sizeof (int32_t) может равняться 2 или даже 1, тогда P+1 равен адресу памяти 2 или даже 1 "C Bytes" после p.

Q: Итак, если я возьму указатель и брошу его в "int" ...

A: Один тип «Весь мир ереси Вакс».

Q: ... а затем бросьте это «int» обратно в указатель ...

A: Еще один тип «Весь мир ереси Вакс».

Q: Поэтому, если я возьму указатель P, который является указателем на int32_t, и разыграл его в какой -то интегральный тип, который достаточно большой, чтобы сдержать указатель, а затем добавьте sizeof( int32_t ) к этому интегральному типу, а затем затем отбросьте этот интегральный тип обратно в указатель - когда я делаю все это, полученный указатель равен P+1?

Не обязательно.

Многие DSP и несколько других современных чипов имеют ориентированную на слова адресацию, а не байто-ориентированную обработку, используемую 8-битными чипсами.

Некоторые из компиляторов C для таких чипсов Cram 2 символов в каждое слово, но требуется 2 таких слова, чтобы удерживать int32_t, поэтому они сообщают, что sizeof( int32_t ) 4. (я слышал слухи, что есть компилятор C для 24-битный Motorola 56000, который делает это).

Компилятор обязан распоряжаться тем, чтобы сделать «p ++» с указанием на int32_t увеличивает указатель к следующему значению int32_t. Есть несколько способов для компилятора сделать это.

Одним из стандартов, соответствующих способу сохранения каждого указателя на int32_t как «адрес народного слова». Поскольку для сохранения одного значения int32_t требуется 2 слова, компилятор C компилятор "int32_t * p; ... p++«В какой -то язык сборки, который увеличивает это значение указателя на 2. С другой стороны, если это сделает»int32_t * p; ... int x = (int)p; x += sizeof( int32_t ); p = (int32_t *)x;«Этот компилятор C для 56000, вероятно, будет компилировать его на язык сборки, который увеличивает значение указателя на 4.

Я больше всего привык к указателям, используемым в смежном, виртуальном пространстве памяти.

Несколько PIC и 8086 и других систем имеют непрерывную оперативную память-несколько блоков оперативной памяти по адресам, которые «сделали аппаратное обеспечение проще». С отображенным памятью ввода/вывода или вообще ничего прикрепленного к зазорам в адресном пространстве между этими блоками.

Это еще более неловко, чем кажется.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top