Що таке базовий покажчик та покажчик стека? На що вони вказують?


225

Використовуючи цей приклад із вікіпедії, в якій DrawSquare () викликає DrawLine (),

alt текст

(Зверніть увагу, що ця діаграма має високі адреси внизу та низькі адреси вгорі.)

Хтось міг би мені пояснити, що ebpі що espв цьому контексті?

З того, що я бачу, я б сказав, що вказівник стека вказує завжди на верхню частину стека, а базовий вказівник на початок поточної функції? Або що?


редагувати: я маю на увазі це в контексті програм Windows

edit2: І як eipтеж працює?

edit3: У мене є такий код із MSVC ++:

var_C= dword ptr -0Ch
var_8= dword ptr -8
var_4= dword ptr -4
hInstance= dword ptr  8
hPrevInstance= dword ptr  0Ch
lpCmdLine= dword ptr  10h
nShowCmd= dword ptr  14h

Усі вони, здається, є дублями, тому беручи по 4 байти кожен. Тож я можу побачити, що є розрив від hInstance до var_4 у 4 байти. Хто вони? Я припускаю, що це зворотна адреса, як видно на малюнку вікіпедії?


(Примітка редактора: вилучено довгу цитату з відповіді Майкла, яка не належить до питання, але подальше запитання було відредаговано):

Це тому, що потік виклику функції:

* Push parameters (hInstance, etc.)
* Call function, which pushes return address
* Push ebp
* Allocate space for locals

Моє запитання (останнє, я сподіваюся!) Тепер таке: що саме відбувається з того моменту, коли я спливу аргументи функції, яку я хочу викликати до кінця прологу? Я хочу знати, як розвиваються ebp, esp за ці моменти (я вже зрозумів, як працює пролог, просто хочу знати, що відбувається після того, як я натиснув аргументи на стек і перед прологом).


23
Важливо відзначити, що стек росте «вниз» в пам’яті. Це означає, що для переміщення вказівника стека вгору ви зменшуєте його значення.
BS

4
Один натяк на розмежування того, що роблять EBP / ESP та EIP: EBP та ESP мають справу з даними, тоді як EIP має справу з кодом.
ммммммммм

2
У вашому графіку ebp (як правило) - це "покажчик кадру", esp - "покажчик стека". Це дозволяє отримувати доступ до локальних жителів через [ebp-x] та параметри стеку через [ebp + x] послідовно, незалежно від покажчика стека (який часто змінюється в межах функції). Адресацію можна зробити через ESP, звільнивши EBP для інших операцій - але таким чином, налагоджувачі не можуть визначити стек викликів або значення місцевих жителів.
peterchen

4
@Ben. Не неприємно. Деякі компілятори кладуть кадри стека в купу. Концепція зростаючого стека - це саме те, що дозволяє легко зрозуміти. Реалізація стека може бути будь-якою (використання випадкових фрагментів купи робить хаки, які переписують частини стека набагато складніше, оскільки вони не є детермінованими).
Мартін Йорк

1
двома словами: покажчик стека дозволяє операціям push / pop працювати (тому push і pop знає, куди потрібно поставити / отримати дані). базовий покажчик дозволяє коду незалежно посилатися на дані, які були попередньо висунуті на стек.
tigrou

Відповіді:


228

esp це, як ви кажете, це верхня стопка.

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

Більшість функціональних прологів виглядають приблизно так:

push ebp      ; Preserve current frame pointer
mov ebp, esp  ; Create new frame pointer pointing to current stack top
sub esp, 20   ; allocate 20 bytes worth of locals on stack.

Тоді пізніше у функції у вас може з'явитися код (наприклад, обидві локальні змінні 4 байти)

mov [ebp-4], eax    ; Store eax in first local
mov ebx, [ebp - 8]  ; Load ebx from second local

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

Редагувати:

У вашому оновленому запитанні відсутні два записи в стеці:

var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
*savedFramePointer = dword ptr 0*
*return address = dword ptr 4*
hInstance = dword ptr  8h
PrevInstance = dword ptr  0C
hlpCmdLine = dword ptr  10h
nShowCmd = dword ptr  14h

Це тому, що потік виклику функції:

  • Параметри Push ( hInstanceтощо)
  • Функція виклику, яка висуває зворотну адресу
  • Штовхати ebp
  • Виділіть місце для місцевих жителів

1
Дякую за пояснення! Але я зараз свого роду розгублений. Припустимо, я викликаю функцію, і я перебуваю в першому рядку її прологу, все ще не виконавши жодного рядка з неї. У цей момент, яке значення має ebp? Чи є у стека щось, окрім висунутих аргументів? Дякую!
пожирав елізіум

3
EBP не магічно змінюється, тому, поки ви не встановите новий EBP для своєї функції, ви все одно матимете значення для абонентів. І крім аргументів, стек буде містити і старий EIP (зворотна адреса)
MSalters

3
Гарна відповідь. Хоча це не може бути повним, не зазначивши, що в епілозі: інструкції "залишити" та "повернути".
Кальмарій

2
Я думаю, що цей образ допоможе з’ясувати деякі речі щодо того, що таке потік. Також пам’ятайте, що стек росте вниз. ocw.cs.pub.ro/courses/_media/so/laboratoare/call_stack.png
Andrei-Niculae Petre

Це я, або всі знаки мінусу відсутні у фрагменті коду вище?
BarbaraKwarc

96

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

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

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

Після виходу з усієї функції, яку потрібно зробити, встановити ESP на значення EBP (яке розставляє локальні змінні зі стека та виставляє вхідний EBP у верхній частині стека), а потім виводить зі стека старе значення EBP, а потім функція повертається (з'являється повернення адреси в EIP).

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


15

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

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

Робити розподіл пам'яті таким чином дуже , дуже швидко та ефективно.


14
@Robert: Коли ви кажете "попередній" верх стека перед тим, як функція була викликана, ви ігноруєте обидва параметри, які висуваються на стек безпосередньо перед викликом функції та EIP абонента. Це може збентежити читачів. Скажімо, що в стандартному кадрі стека EBP вказує на те саме місце, куди ESP вказував відразу після введення функції.
wigy

7

РЕДАКТУВАННЯ: Для кращого опису дивіться x86 Розбирання / Функції та Рамки Стек у WikiBook про збірку x86. Я намагаюся додати інформацію, яка може вам зацікавити використання Visual Studio.

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

Якщо говорити про програми Windows, можливо, ви можете скористатися Visual Studio для компіляції коду С ++. Майте на увазі, що Microsoft використовує оптимізацію під назвою Frame Pointer Omission, що робить її майже неможливою для прогулянки стеком без використання бібліотеки dbghlp та файлу PDB для виконуваного файлу.

Цей опущення покажчика кадру означає, що компілятор не зберігає старий EBP у стандартному місці та використовує регістр EBP для чогось іншого, тому вам важко знайти EIP абонента, не знаючи, скільки місця потрібно місцевим змінним для даної функції. Звичайно, Microsoft пропонує API, який дозволяє робити прогулянки стеками навіть у цьому випадку, але пошук бази даних символьних таблиць у файлах PDB займає занадто багато часу для деяких випадків використання.

Щоб уникнути FPO у підрозділах компіляції, вам слід уникати використання / O2 або явно додавати / Oy- до прапорів компіляції C ++ у своїх проектах. Ви, ймовірно, посилаєтеся на час виконання C або C ++, який використовує FPO у конфігурації випуску, тож вам буде важко робити прогулянки стеками без dbghlp.dll.


Я не розумію, як EIP зберігається у стеку. Чи не повинен це бути реєстр? Як реєстр може бути на стеці? Дякую!
пожирав елізіум

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

@devouredelysium Вміст (або значення ) реєстру EIP ставиться (або копіюється на) стек, а не сам реєстр.
BarbaraKwarc

@BarbaraKwarc Дякуємо за введений значенням . Я не міг побачити, чого пропав ОП у моїй відповіді. Дійсно, регістри залишаються там, де вони є, лише їх значення надсилається в ОЗП від центрального процесора. У режимі amd64 це стає трохи складніше, але залиште це іншим питанням.
перуки

Що з цим amd64? Мені цікаво.
BarbaraKwarc

6

Перш за все, покажчик стека вказує на нижню частину стека, оскільки стеки x86 будуються з високих значень адреси до нижчих значень адреси. Покажчик стека - це точка, в якій наступний виклик для натискання (або виклику) розмістить наступне значення. Його операція еквівалентна оператору C / C ++:

 // push eax
 --*esp = eax
 // pop eax
 eax = *esp++;

 // a function call, in this case, the caller must clean up the function parameters
 move eax,some value
 push eax
 call some address  // this pushes the next value of the instruction pointer onto the
                    // stack and changes the instruction pointer to "some address"
 add esp,4 // remove eax from the stack

 // a function
 push ebp // save the old stack frame
 move ebp, esp
 ... // do stuff
 pop ebp  // restore the old stack frame
 ret

Базовий вказівник знаходиться вгорі поточного кадру. ebp загалом вказує на вашу зворотну адресу. ebp + 4 вказує на перший параметр вашої функції (або це значення методу класу). ebp-4 вказує на першу локальну змінну вашої функції, як правило, на старе значення ebp, щоб ви могли відновити попередній покажчик кадру.


2
Ні, ESP не вказує на нижню частину стека. Схема адреси пам'яті не має нічого спільного. Не має значення, чи зростає стек до нижчих чи вищих адрес. "Вершина" стека - це завжди там, де наступне значення буде висунуте (покладене на верхню частину стека), або в інших архітектурах, де було поставлено останнє висунуте значення і де воно лежить в даний час. Тому ESP завжди вказує на верхню частину стека.
BarbaraKwarc

1
З іншого боку, нижня або основа стека - це місце, де було поставлено перше (або найдавніше ) значення, а потім охоплене більш новими значеннями. Ось звідки походить назва "базовий покажчик" для EBP: воно повинно було вказувати на базу (або низ) поточного локального стека підпрограми.
BarbaraKwarc

Барбара, в Intel x86, стек знаходиться НЕЗАДАЧНО. У верхній частині стека міститься перший елемент, натиснутий на стек, а кожен елемент після цього висувається Внизу верхнього. У нижній частині стека розміщуються нові елементи. Програми розміщуються в пам'яті починаючи з 1 к і ростуть до нескінченності. Стек починається з нескінченності, реально максимум пам'яті мінус ПЗУ, і зростає до 0. ESP вказує на адресу, значення якої менше, ніж перша адреса.
jmucchiello

1

З давніх пір я робив програму складання, але це посилання може бути корисним ...

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

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

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

Майте на увазі, що складання дуже специфічне для процесора. Сторінка, на яку я пов’язана, містить інформацію про різні типи процесорів.


Регістри сегментів є окремими для x86 - це gs, cs, ss, і якщо ви не пишете програмне забезпечення для управління пам'яттю, ви їх ніколи не торкаєтесь.
Майкл

DS також є сегментним регістром, і за часів MS-DOS та 16-бітового коду вам неодмінно потрібно періодично змінювати ці сегментні регістри, оскільки вони ніколи не могли вказувати більше 64 КБ оперативної пам’яті. Однак DOS міг отримати доступ до пам'яті до 1 Мб, оскільки він використовував 20-бітові вказівники адреси. Пізніше ми отримали 32-бітові системи, деякі з 36-бітовими регістрами адрес, а тепер 64-бітні регістри. Тому в наш час вам більше не потрібно буде змінювати ці сегментні регістри.
Вім десять Бринк

Жодна сучасна ОС не використовує 386 сегментів
Ана Беттс

@Paul: НЕМАЄМО! НЕ ПРАВО! НЕ ПРАВО! 16-бітові сегменти замінюються 32-бітовими сегментами. У захищеному режимі це дозволяє віртуалізувати пам'ять, в основному дозволяє процесору відображати фізичні адреси на логічні. Однак у вашій програмі все ще здається рівним, оскільки ОС віртуалізувала пам'ять для вас. Ядро працює в захищеному режимі, що дозволяє програмам працювати в плоскій моделі пам'яті. Дивіться також en.wikipedia.org/wiki/Protected_mode
Wim ten Brink

@Workshop ALex: Це технічність. Усі сучасні ОС встановлюють для всіх сегментів [0, FFFFFFFF]. Це насправді не рахується. І якщо ви прочитаєте пов’язану сторінку, ви побачите, що всі химерні речі виконуються зі сторінок, які є значно більш дрібними, ніж сегменти.
MSalters

-4

Редагувати Так, це в основному неправильно. Він описує щось зовсім інше, якщо когось цікавить :)

Так, вказівник стека вказує на верхню частину стека (будь то перше порожнє розташування стека чи останнє повне, про яке я не впевнений). Базовий вказівник вказує на місце пам'яті виконуваної інструкції. Це на рівні опкодів - найпростіша інструкція, яку можна отримати на комп’ютері. Кожен опкод та його параметри зберігаються у пам'яті. Один рядок C або C ++ або C # може бути переведений на один опкод або послідовність двох або більше, залежно від того, наскільки він складний. Вони записуються в пам'ять програми послідовно і виконуються. За звичайних обставин базовий покажчик збільшується на одну інструкцію. Для управління програмою (GOTO, IF тощо) його можна збільшити кілька разів або просто замінити на наступну адресу пам'яті.

У цьому контексті функції зберігаються в пам'яті програми за певною адресою. Коли функція викликається, певна інформація висувається на стек, що дозволяє програмі знайти її повернувся туди, куди була викликана функція, а також параметри функції, тоді адресу функції в пам'яті програми висувається в базовий покажчик. На наступному тактовому циклі комп'ютер починає виконувати інструкції з цієї адреси пам'яті. Тоді в якийсь момент він повернеться до місця пам'яті ПІСЛЯ інструкції, яка викликала функцію, і продовжить звідти.


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

1
@Devoured - Ні. Це неправда. eip буде збільшуватися.
Майкл

Ви маєте на увазі, що те, що я сказав, є правильним, але не для EBP, а для IEP, це так?
пожирав elysium

2
Так. EIP - покажчик інструкцій і неявно змінюється після виконання кожної інструкції.
Майкл

2
Ой, моя погана. Я думаю про інший покажчик. Я думаю, я піду помити мозок.
Стівен Фрідеріхс

-8

esp означає "Розширений покажчик стека" ..... ebp для "Щось базовий покажчик" .... і eip для "Щось вказівник на інструкцію" ...... Вказівник стека вказує на адресу зміщення сегмента стека . Базовий покажчик вказує на зміщену адресу додаткового сегмента. Покажчик інструкції вказує на зміщену адресу сегмента коду. Тепер про сегменти ... це невеликі підрозділи 64 КБ області пам'яті процесорів ..... Цей процес відомий як сегментація пам'яті. Я сподіваюся, що цей пост був корисним.


3
Це старе питання, однак, sp означає для покажчика стека, bp - базовий покажчик, а ip - для вказівника інструкції. На початку всі просто кажуть, що це 32-бітний покажчик.
Хайден

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