Я розумію, що таке покажчик стека - але для чого він використовується?


11

Покажчик стека вказує на верхню частину стека, де зберігаються дані про те, що ми називаємо основою "LIFO". Вкрасти чужу аналогію - це як стопку посуду, в яку ви кладете і приймаєте посуд вгорі. Покажчик стека, OTOH, вказує на верхню "тарілку" стека. Принаймні, це правда для x86.

Але чому комп’ютер / програма "дбає" про те, на що вказує вказівник стека? Іншими словами, яку мету має вказівник стека та знати, куди він вказує?

Пояснення, зрозуміле програмістам C, буде вдячне.


Тому що ви не бачите верхньої частини стопки в барані, як ви бачите верхню частину посуду.
tkausl


8
Ви не берете блюдо з нижньої стопки. Ви додаєте один зверху, а хтось інший бере його зверху . Ви тут думаєте про чергу.
Кіліан Фот

@Snowman Ваша редакція, схоже, змінює значення питання. moonman239, чи можете ви перевірити, чи зміна Сніговика точна, зокрема додавання "Якій цілі справді служить цей стек на відміну від пояснення його структури?"
8bittree

1
@ 8bittree Будь ласка, дивіться опис редагування: Я скопіював питання, як зазначено в темі теми, в основу питання. Звичайно, я завжди відкритий для того, щоб я щось змінив, і оригінальний автор завжди вільний відкатати або іншим чином редагувати публікацію.

Відповіді:


23

Якій цілі справді служить цей стек на відміну від пояснення його структури?

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

Призначення, яке служить стеку, полягає в тому, що стек - це частина переробки продовження мовою без супроводу .

Давайте розпакуємо це.

Продовження просто кажучи, відповідь на питання "що буде далі в моїй програмі?" У кожній точці кожної програми щось відбуватиметься далі. Будуть обчислені два операнди, потім програма продовжується обчисленням їх суми, а потім програма продовжується шляхом присвоєння суми змінній, а потім ... і так далі.

Реіфікація - лише високе слово для конкретної реалізації абстрактної концепції. "Що буде далі?" - абстрактне поняття; спосіб складання стека є частиною того, як ця абстрактна концепція перетворюється на справжню машину, яка справді обчислює речі.

Коротині - це функції, які можуть запам'ятати, де вони були, на деякий час контролюють, а потім відновляють місце, де вони пізніше припиняються, але не обов'язково одразу після того, як щойно називаються програми програми. Подумайте про "повернення прибутковості" або "чекайте" в C #, який повинен пам'ятати, де вони були, коли запитується наступний елемент або завершується асинхронна операція. Мови з підпрограмми або подібними особливостями мови вимагають більш досконалої структури даних, ніж стек, щоб реалізувати продовження.

Як стек реалізує продовження? Інші відповіді говорять як. Стек зберігає (1) значення змінних та тимчасовостей, час життя яких, як відомо, не перевищує активацію поточного методу, та (2) адресу коду продовження, пов'язаного з останньою активацією методу. У мовах за винятком обробки стека також може зберігатися інформація про "продовження помилки" - тобто, що програма буде робити далі, коли виникла виняткова ситуація.

Дозвольте скористатися цією можливістю, щоб зазначити, що стек не говорить вам "звідки я взявся?" - хоча він часто так використовується при налагодженні. Стек повідомляє вам, куди ви збираєтесь далі , і які значення змінних активації будуть, коли ви туди потрапите . Той факт, що мовою без супротивників, куди ви йдете далі, майже завжди, звідки ви приїхали, полегшує цей вид налагодження. Але немає вимоги, щоб компілятор зберігав інформацію про те, звідки взявся контроль, якщо він може піти, не роблячи цього. Оптимізація зворотного дзвінка, наприклад, знищує інформацію про те, звідки походить управління програмою.

Чому ми використовуємо стек для реалізації продовження в мовах без супроводу? Оскільки характеристикою синхронної активації методів є те, що схема "призупинення поточного методу, активація іншого методу, відновлення поточного методу, знаючи результат активованого методу", коли складений з собою логічно утворює стек активацій. Створити структуру даних, яка реалізує таку поведінку, схожу на стек, дуже дешево і просто. Чому це так дешево і просто? Оскільки набори мікросхем були багато десятиліть спеціально розроблені, щоб полегшити подібне програмування для авторів-компіляторів.


Зауважте, що цитата, на яку ви посилаєтесь, була помилково додана в редакції іншим користувачем і відтоді була виправлена, завдяки чому ця відповідь не зовсім вирішує питання.
8bittree

2
Я впевнений, що пояснення повинно збільшити чіткість. Я не зовсім переконаний, що "стек є частиною виправлення продовження мови без супротивів", навіть близький до цього :-)

4

Основне використання стека - це збереження зворотної адреси для функцій:

void a(){
    sub();
}
void b(){
    sub();
}
void sub() {
    //should i got back to a() or to b()?
}

і з точки зору це все. З точки зору компілятора:

  • всі аргументи функцій передаються регістрами процесора - якщо регістрів недостатньо, аргументи будуть поставлені в стек
  • після закінчення функції (більшість) регістри повинні мати ті ж значення, що й до введення в неї, тому використані регістри створюються резервними копіями в стеку

І з точки зору ОС: програму можна перервати будь-коли, тому після того, як ми виконаємо системне завдання, нам доведеться відновити стан процесора, тому дозволяє зберігати все в стеці

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


1
Я вважаю, що точніше сказати, що аргументи висуваються на стек, хоча часто як оптимізаційні регістри використовуються замість цього на процесорах, які мають достатньо вільних регістрів для виконання завдання. Це ніт, але я думаю, що це краще відповідає тому, як мови розвивалися історично. Найдавніші компілятори C / C ++ взагалі не використовували регістри для цього.
Gort the Robot

4

LIFO - FIFO

LIFO означає Last In, First Out. Як і в, Останній елемент, поміщений у стек, - це перший елемент, вийнятий із стека.

Те, що ви описали з аналогією страв (у першій редакції ), - це черга чи FIFO, First In, First Out.

Основна різниця між ними полягає в тому, що LIFO / стек штовхає (вставляє) і спливає (видаляє) з того самого кінця, а FIFO / черга робить це з протилежних кінців.

// Both:

Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]

// Stack            // Queue
Pop()               Pop()
-> [a, b]           -> [b, c]

Вказівник стека

Давайте подивимось, що відбувається під кришкою стека. Ось трохи пам’яті, кожне поле - це адреса:

...[ ][ ][ ][ ]...                       char* sp;
    ^- Stack Pointer (SP)

І є вказівник стека, що вказує на нижню частину порожнього стека (чи стек росте чи зростає внизу, тут не особливо актуально, тому ми будемо ігнорувати це, але, звичайно, у реальному світі, це визначає, яка операція додає , і які віднімають з SP).

Тож давайте натиснемо a, b, and cще раз. Графіка зліва, операція "високого рівня" посередині, псевдо-код C-ish праворуч:

...[a][ ][ ][ ]...        Push('a')      *sp = 'a';
    ^- SP
...[a][ ][ ][ ]...                       ++sp;
       ^- SP

...[a][b][ ][ ]...        Push('b')      *sp = 'b';
       ^- SP
...[a][b][ ][ ]...                       ++sp;
          ^- SP

...[a][b][c][ ]...        Push('c')      *sp = 'c';
          ^- SP
...[a][b][c][ ]...                       ++sp;
             ^- SP

Як ви бачите, кожен раз, коли ми pushвставляємо аргумент у місцеположення, на яке вказує поточний покажчик стека, і коригуємо вказівник стека, щоб він вказував на наступне місце.

Тепер попустимо:

...[a][b][c][ ]...        Pop()          --sp;
          ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'c'
          ^- SP
...[a][b][c][ ]...        Pop()          --sp;
       ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'b'
       ^- SP

Popпротилежне до цього push, він налаштовує вказівник стека, щоб він вказував на попереднє місце та видаляв елемент, який там був (зазвичай, щоб повернути його тому, хто зателефонував pop).

Ви, мабуть, це помітили bі cдосі залишаєтесь у пам’яті. Я просто хочу запевнити, що це не друкарські помилки. До цього ми повернемося незабаром.

Життя без вказівника стека

Подивимося, що станеться, якщо у нас немає покажчика стека. Починаючи з повторного натискання:

...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]...        Push(a)        ? = 'a';

Е, хм ... якщо у нас немає вказівника стека, ми не можемо щось перенести на адресу, на яку вказує. Можливо, ми можемо використовувати вказівник, який вказує на базу замість верху.

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[a][ ][ ][ ]...        Push(a)        *bp = 'a';
    ^- bp
// No stack pointer, so no need to update it.
...[b][ ][ ][ ]...        Push(b)        *bp = 'b';
    ^- bp

Ой-ой. Оскільки ми не можемо змінити фіксовану величину основи стека, ми просто перекреслили a, натиснувши bна те саме місце.

Ну чому б ми не відслідковували, скільки разів нас натискали. І нам також потрібно буде слідкувати за часом, який ми вискакували.

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);
                                         int count = 0;

...[a][ ][ ][ ]...        Push(a)        bp[count] = 'a';
    ^- bp
...[a][ ][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Push(a)        bp[count] = 'b';
    ^- bp
...[a][b][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Pop()          --count;
    ^- bp
...[a][b][ ][ ]...                       return bp[count]; //returns b
    ^- bp

Добре це працює, але насправді він дуже схожий на попередній, за винятком того, що *pointerвін дешевший pointer[offset](без зайвої арифметики), не кажучи вже про менший набір. Мені це здається втратою.

Спробуємо ще раз. Замість того, щоб використовувати стиль рядка Pascal для пошуку кінця колекції на основі масиву (відстеження кількості елементів у колекції), спробуємо стиль рядка C (сканувати від початку до кінця):

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[ ][ ][ ][ ]...        Push(a)        char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       *top = 'a';
    ^- bp    ^- top

...[ ][ ][ ][ ]...        Pop()          char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       --top;
    ^- bp       ^- top                   return *top; // returns '('

Ви, можливо, вже здогадалися тут про проблему. Неініціалізована пам'ять не гарантовано дорівнює 0. Отже, коли ми шукаємо верхню aчастину місця , ми в кінцевому підсумку переходимо через купу невикористаного місця пам’яті, в якому є випадкове сміття. Точно так само, коли ми скануємо на вершину, ми в кінцевому підсумку пропускаємо далеко за те, що aми тільки штовхнули, поки нарешті не знайдемо інше місце пам’яті, яке просто трапляється 0, і повертаємось назад і повертаємо випадкове сміття безпосередньо перед цим.

Це досить просто виправити, нам просто потрібно додати операції до Pushта, Popщоб переконатися, що верхня частина стека завжди оновлюється, щоб бути позначеною символом a 0, і ми повинні ініціалізувати стек таким термінатором. Звичайно, це також означає, що ми не можемо мати 0(або будь-яке значення, яке ми обираємо як термінатор) як фактичне значення в стеку.

Крім того, ми також змінили, що були операціями O (1), на O (n).

TL; DR

Вказівник стека відстежує верхню частину стека, де відбувається вся дія. Є способи їх позбутися ( bp[count]і topпо суті все ще є вказівником стека), але вони в кінцевому підсумку є складнішими і повільнішими, ніж просто указкою стека. І не знаючи, де знаходиться вершина стека, це означає, що ви не можете використовувати стек.

Примітка: Вказівник стека, що вказує на "дно" стеку виконання в x86, може бути помилковим уявленням, пов'язаним з тим, що весь стек виконання знаходиться внизу. Іншими словами, основа стека розміщується на високій адресі пам'яті, а кінчик стека переростає в нижчі адреси пам'яті. Покажчик стека робить точку на кінчик стека , де відбувається вся дія, тільки що наконечник знаходиться на більш низькому , ніж адреса пам'яті підстави стека.


2

Покажчик стека використовується (з покажчиком кадру) для стека виклику (перейдіть за посиланням на wikipedia, де є хороша картинка).

Стек викликів містить кадри викликів, які містять зворотну адресу, локальні змінні та інші локальні дані (зокрема, розлитий вміст регістрів; формали).

Читайте також про хвостові дзвінки (деяким хвостовим рекурсивним дзвінкам не потрібен кадр виклику), обробці винятків (наприклад, setjmp & longjmp , вони можуть включати вискакування відразу багатьох кадрів стеків), сигнали та переривання та продовження . Див. Також виклики конвенцій та двійкових інтерфейсів додатків (ABI), зокрема AB86 x86-64 (який визначає, що деякі формальні аргументи передаються регістрами).

Крім того, введіть у C кілька простих функцій (функцій), потім gcc -Wall -O -S -fverbose-asm скопіюйте їх і загляньте в створений .s файл асемблера.

Аппель написав старий документ 1986 року, в якому стверджував, що збирання сміття може бути швидшим, ніж розподіл стека (використовуючи стиль компіляції продовження в компіляторі), але це, мабуть, помилково для сучасних процесорів x86 (особливо через ефекти кешу).

Зауважте, що виклики конвенцій, ABI та компонування стека відрізняються на 32 бітах i686 та на 64 бітах x86-64. Також умови викликів (і хто несе відповідальність за розподіл або виклик кадру виклику) можуть бути різними для різних мов (наприклад, C, Pascal, Ocaml, SBCL Common Lisp мають різні умови виклику ....)

BTW, останні розширення x86, такі як AVX , накладають дедалі більші обмеження вирівнювання на покажчик стека (IIRC, кадр виклику на x86-64 хоче вирівняти до 16 байт, тобто двох слів або покажчиків).


1
Ви можете згадати, що вирівнювання до 16 байт на x86-64 означає вдвічі більший розмір / вирівнювання вказівника, що насправді цікавіше, ніж кількість байтів.
Дедуплікатор

1

Простіше кажучи, програма піклується про те, що вона використовує ці дані і потрібно відслідковувати, де їх знайти.

Якщо ви оголошуєте локальні змінні у функції, стек - це те, де вони зберігаються. Крім того, якщо ви викликаєте іншу функцію, стек - це місце, де вона зберігає зворотну адресу, щоб вона могла повернутися до функції, в якій ви були, коли закінчена та, яку ви дзвонили, і забрати там, де вона зупинилась.

Без SP, структуроване програмування, як ми знаємо, було б по суті неможливим. (Ви можете обійти його, не маючи цього, але це дуже потребує впровадження власної версії, тому це не має великої різниці.)


1
Твоє твердження, що структуроване програмування без стека було б неможливим, є помилковим. Програми, складені у стилі продовження, що проходять, не використовують жодного стека, але вони є ідеально розумними програмами.
Ерік Ліпперт

@EricLippert: Для значень "ідеально розумних" досить безглуздих, щоб вони включали, як стояти на голові і перевертати себе назовні, можливо. ;-)
Мейсон Уілер

1
З продовженням проходження , стек виклику взагалі не потрібен. Ефективно, кожен виклик - це хвостовий дзвінок і перехід, а не повернення. "Оскільки CPS і TCO усувають концепцію повернення неявних функцій, їх комбіноване використання може усунути потребу у стеку виконання."

@MichaelT: Я сказав "по суті" неможливо з причини. CPS теоретично може цього досягти, але на практиці стає смішно важко дуже швидко написати код реального світу будь-якої складності в CPS, на що Ерік зазначив у серії публікацій блогу на цю тему .
Мейсон Уілер

1
@MasonWheeler Ерік розповідає про програми, зібрані в CPS. Наприклад, цитуючи блог Джона Харропа : In fact, some compilers don’t even use stack frames [...], and other compilers like SML/NJ convert every call into continuation style and put stack frames on the heap, splitting every segment of code between a pair of function calls in the source into its own separate function in the compiled form.Це відрізняється від "реалізації власної версії [стека]".
Doval

1

Для стека процесорів у процесорі x86 аналогія стосу посуду дійсно неточна.
З різних причин (здебільшого історичних) стек процесорів зростає зверху пам’яті до нижньої частини пам’яті, тому кращою аналогією буде ланцюг ланцюгових ланок, що звисають зі стелі. Натискаючи щось на стек, ланцюжок додається до найнижчої ланки.

Покажчик стека відноситься до найнижчої ланки ланцюга і використовується процесором, щоб "побачити", де знаходиться найнижча ланка, щоб посилання можна було додавати або видаляти без необхідності переміщення всієї ланцюга від стелі вниз.

У певному сенсі, усередині процесора x86 стек перевернутий, але звичайний підвіконня термінології стека звикає, так що найнижча ланка називається вершиною стека.


Посилання на ланцюги, про які я згадував вище, насправді є осередками пам'яті в комп'ютері, і вони звикають зберігати локальні змінні та деякі проміжні результати обчислень. Комп'ютерні програми дбають там, де знаходиться верхня частина стека (тобто там, де висить ця найнижча ланка), оскільки велика більшість змінних, до яких потрібна функція, існують близько до місця, на який вказує покажчик стека, і бажаний швидкий доступ до них.


1
The stack pointer refers to the lowest link of the chain and is used by the processor to "see" where that lowest link is, so that links can be added or removed without having to travel the entire chain from the ceiling down.Я не впевнений, що це хороша аналогія. Насправді посилання ніколи не додаються та не видаляються. Покажчик стека більше схожий на трохи стрічки, яку ви використовуєте для позначення одного з посилань. Якщо ви втратите цю стрічку, у вас не буде способу дізнатися, яка була найнижча посилання, яку ви використовували взагалі ; подорож ланцюгом від стелі вниз не допоможе вам.
Doval

Отже, покажчик стека надає орієнтир, який програма / комп'ютер може використовувати для пошуку локальних змінних функції?
moonman239

Якщо це так, то як комп'ютер знаходить локальні змінні? Це просто пошук кожної адреси пам'яті знизу вгору?
moonman239

@ moonman239: Ні, під час компіляції компілятор відстежує, де зберігається кожна змінна відносно вказівника стека. Процесор розуміє таку відносну адресацію, щоб надати прямий доступ до змінних.
Барт ван Інген Шенау

1
@BartvanIngenSchenau Ага, гаразд. Начебто, коли ти знаходишся посеред нікуди і тобі потрібна допомога, тож даєш 911 уявлення про те, де ти маєш відношення до орієнтиру. Покажчик стека, в цьому випадку, як правило, є найближчим «орієнтиром» і тому, мабуть, найкращою точкою відліку.
moonman239

1

Ця відповідь відноситься конкретно до в покажчик стека поточного потоку (виконання) .

У процедурних мовах програмування потік, як правило, має доступ до стека 1 для наступних цілей:

  • Контрольний потік, а саме "стек викликів".
    • Коли одна функція викликає іншу функцію, стек викликів запам'ятовує, куди слід повернутися.
    • Стек викликів необхідний, тому що саме так ми хочемо, щоб "виклик функції" поводився - "щоб вибрати, де ми зупинилися" .
    • Існують й інші стилі програмування, які не мають викликів функцій в середині виконання (наприклад, дозволено лише вказати наступну функцію, коли буде досягнуто завершення поточної функції) або взагалі не мають викликів функцій (лише з використанням переходів goto та умовних умов). ). Цим стилям програмування може не знадобитися стек викликів.
  • Параметри виклику функції.
    • Коли функція викликає іншу функцію, параметри можуть бути висунуті на стек.
    • Необхідно, щоб абонент і абонент дотримувались тієї ж конвенції щодо того, хто відповідає за очищення параметрів зі стека, коли виклик закінчується.
  • Локальні змінні, що живуть у межах виклику функції.
    • Зауважте, що локальна змінна, що належить абоненту, може бути доступною для виклику, передавши покажчик на цю локальну змінну на виклик.

Примітка 1 : присвячена використанню потоку, хоча його вміст повністю читабельний - і зручний - іншими потоками.

У програмуванні складання, C і C ++ всі три цілі можуть бути виконані одним і тим же стеком. В деяких інших мовах деякі цілі можуть виконуватися окремими стеками або динамічно розподіленою пам'яттю.


1

Ось свідомо спрощена версія того, для чого використовується стек.

Уявіть стек як купу індексних карток. Вказівник стека вказує на верхню карту.

Коли ви викликаєте функцію:

  • Адресу коду ви пишете відразу після рядка, який викликав функцію на картці, і кладете її на купу. (Тобто ви збільшуєте вказівник стека на один і записуєте адресу, куди він вказує)
  • Потім ви записуєте значення, які містяться в регістрах, на деякі картки і кладете їх на купу. (тобто ви збільшуєте покажчик стека на кількість регістрів і копіюєте вміст регістра на місце, на яке він вказує)
  • Потім кладете карту маркера на купу. (тобто ви заощаджуєте поточний покажчик стека.)
  • Потім ви записуєте значення кожного параметра, з яким викликається функція, один на карту, і кладете його на купу. (тобто ви збільшуєте покажчик стека на кількість параметрів і записуєте параметри до місця, на яке вказує вказівник стека.)
  • Потім ви додаєте карту для кожної локальної змінної, потенційно записуючи на неї початкове значення. (тобто ви збільшуєте покажчик стека на кількість локальних змінних.)

У цей момент запускається код у функції. Код складається, щоб знати, де кожна картка відносно верхньої частини. Тож відомо, що змінна xє третьою картою зверху (тобто вказівник стека - 3) і що параметр y- шоста карта зверху (тобто вказівник стека - 6.)

Цей метод означає, що адресу кожної локальної змінної чи параметра не потрібно вводити в код. Натомість всі ці елементи даних адресовані відносно вказівника стека.

Коли функція повертається, операція зворотного просто:

  • Шукайте маркерну картку і відкиньте всі картки над нею. (тобто встановити покажчик стека на збережену адресу.)
  • Відновіть регістри з раніше збережених карток та викиньте їх. (тобто віднімаємо фіксоване значення з покажчика стека)
  • Почніть виконувати код з адреси на картці зверху, а потім киньте його. (тобто відніміть 1 від вказівника стека.)

Стек тепер повернувся в стан, який був до виклику функції.

Враховуючи це, зверніть увагу на дві речі: розподіл та розселення місцевих жителів - надзвичайно швидка операція, оскільки це просто додавання числа або віднімання числа з покажчика стека. Також зауважте, як це природно працює при рекурсії.

Це пояснюється для пояснювальних цілей. На практиці параметри та локальні дані можуть бути внесені до регістрів як оптимізація, і вказівник стека, як правило, збільшується та зменшується на розмір слова машини, а не на один. (Щоб назвати пару речей.)


1

Сучасні мови програмування, як ви добре знаєте, підтримують концепцію викликів підпрограми (найчастіше їх називають "виклики функцій"). Це означає що:

  1. У середині деякого коду ви можете викликати якусь іншу функцію у вашій програмі;
  2. Ця функція прямо не знає, звідки її викликали;
  3. Тим не менше, коли робота виконується, і returnконтроль повертається в точну точку, де був ініційований виклик, при цьому всі локальні змінні значення діють, як і коли був ініційований виклик.

Як комп’ютер відстежує це? Він підтримує постійний запис про те, які функції чекають, на які дзвінки потрібно повернутися. Цей запис є стек і так як це такий важливий, ми зазвичай називаємо його стек.

А оскільки ця модель виклику / повернення настільки важлива, процесори давно розроблені для надання спеціальної апаратної підтримки для неї. Покажчик стека - це апаратна функція в процесорах - реєстр, призначений виключно для відстеження верхньої частини стека і використовується інструкціями ЦП для розгалуження на підпрограму та повернення з неї.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.