Что такое базовый указатель и указатель стека? На что они указывают?

225

Используя этот пример из Википедии, в которой DrawSquare () вызывает DrawLine (),

альтернативный текст

(Обратите внимание, что эта диаграмма имеет высокие адреса внизу и низкие адреса вверху.)

Может ли кто-нибудь объяснить мне, что 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 (обычно) - это «указатель кадра», особенно «указатель стека». Это позволяет получать доступ к локальным объектам через [ebp-x] и к параметрам стека через [ebp + x] последовательно, независимо от указателя стека (который часто изменяется внутри функции). Адресация может осуществляться через ESP, освобождая EBP для других операций - но в этом случае отладчики не могут определить стек вызовов или значения локальных переменных.
peterchen
4
@Бен. Не обязательно. Некоторые компиляторы помещают стековые фреймы в кучу. Концепция растущего стека - это просто концепция, которая облегчает понимание. Реализация стека может быть любой (использование случайных кусков кучи делает хаки, которые перезаписывают части стека намного сложнее, поскольку они не такие детерминированные).
Мартин Йорк,
1
в двух словах: указатель стека позволяет работать операциям push / pop (так что push и pop знают, куда помещать / получать данные). Базовый указатель позволяет коду независимо ссылаться на данные, ранее помещенные в стек.
Tigrou

Ответы:

229

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
@ Роберт: Когда вы говорите «предыдущая» вершина стека до вызова функции, вы игнорируете оба параметра, которые помещаются в стек непосредственно перед вызовом функции и вызывающего EIP. Это может запутать читателей. Скажем так: в стандартном стековом фрейме EBP указывает на то же место, куда указывал ESP сразу после входа в функцию.
парик
7

РЕДАКТИРОВАТЬ: Более подробное описание см. В разделе Разборка / Функции x86 и стековые рамки в WikiBook о сборке x86. Я пытаюсь добавить информацию, которая может быть вам интересна при использовании Visual Studio.

Хранение вызывающего EBP в качестве первой локальной переменной называется стандартным стековым фреймом, и это может использоваться почти для всех соглашений о вызовах в Windows. Существуют различия, независимо от того, освобождает ли вызывающий или вызываемый объект переданные параметры и какие параметры передаются в регистрах, но они ортогональны стандартной задаче стека.

Говоря о программах Windows, вы, вероятно, могли бы использовать Visual Studio для компиляции кода C ++. Имейте в виду, что Microsoft использует оптимизацию под названием Frame Pointer Omission, что делает практически невозможным обход стека без использования библиотеки dbghlp и файла PDB для исполняемого файла.

Это опущение указателя кадра означает, что компилятор не хранит старый EBP в стандартном месте и использует регистр EBP для чего-то другого, поэтому вам трудно найти EIP вызывающего, не зная, сколько места нужно локальным переменным для данной функции. Конечно, Microsoft предоставляет API, который позволяет вам выполнять обход стека даже в этом случае, но поиск базы данных таблицы символов в файлах PDB занимает слишком много времени для некоторых случаев использования.

Чтобы избежать FPO в ваших модулях компиляции, вы должны избегать использования / O2 или явно добавлять / Oy- к флагам компиляции C ++ в ваших проектах. Вы, вероятно, ссылаетесь на среду выполнения C или C ++, которая использует FPO в конфигурации Release, поэтому вам будет сложно выполнять обход стека без dbghlp.dll.

wigy
источник
Я не понимаю, как EIP хранится в стеке. Разве это не должен быть регистр? Как регистр может быть в стеке? Спасибо!
пожрал Элизиум
Вызывающий EIP помещается в стек самой инструкцией CALL. Инструкция RET просто выбирает вершину стека и помещает ее в EIP. Если у вас переполнение буфера, этот факт может быть использован для перехода к пользовательскому коду из привилегированного потока.
парик
@devouredelysium Содержимое (или значение ) регистра EIP помещается (или копируется) в стек, а не в сам регистр.
BarbaraKwarc
@BarbaraKwarc Спасибо за значение -able ввода. Я не мог видеть, что ОП не было в моем ответе. Действительно, регистры остаются там, где они есть, только их значение отправляется в ОЗУ из ЦП. В режиме amd64 это становится немного сложнее, но оставим это другому вопросу.
парик
Что насчет этого amd64? Мне любопытно.
BarbaraKwarc
6

Прежде всего, указатель стека указывает на дно стека, так как стеки x86 строят от высоких значений адресов до более низких значений адресов. Указатель стека - это точка, в которой следующий вызов push (или call) установит следующее значение. Эта операция эквивалентна выражению 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 указывает на первый параметр вашей функции (или значение this метода класса). ebp-4 указывает на первую локальную переменную вашей функции, обычно это старое значение ebp, поэтому вы можете восстановить предыдущий указатель кадра.

jmucchiello
источник
2
Нет, ESP не указывает на дно стека. Схема адресации памяти не имеет к этому никакого отношения. Неважно, будет ли стек расти к более низким или более высоким адресам. «Вершина» стека всегда находится там, где будет помещено следующее значение (помещено на вершину стека), или, на других архитектурах, где было помещено последнее переданное значение и где оно находится в данный момент. Поэтому ESP всегда указывает на вершину стека.
BarbaraKwarc
1
С другой стороны, дно или основание стека - это то место, куда было помещено первое (или самое старое ) значение, а затем - более поздние значения. Отсюда и произошло название «указатель базы» для EBP: он должен был указывать на основание (или низ) текущего локального стека подпрограммы.
BarbaraKwarc
Барбара, в Intel x86, стек ВНЕШНИЙ ВНИЗ. Вершина стека содержит первый элемент, помещенный в стек, а каждый элемент после помещается НИЖЕ верхнего элемента. В нижней части стопки находятся новые предметы. Программы помещаются в память, начиная с 1 КБ, и растут до бесконечности. Стек начинается с бесконечности, реально максимум памяти минус ПЗУ и увеличивается до 0. ESP указывает на адрес, значение которого меньше, чем первый выдаваемый адрес.
jmucchiello
1

Давно я занимался программированием на ассемблере, но эта ссылка может быть полезной ...

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

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

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

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

Вим тен Бринк
источник
Сегментные регистры разделены на x86 - они gs, cs, ss, и если вы не пишете программное обеспечение для управления памятью, вы никогда не коснетесь их.
Майкл
ds также является регистром сегментов, и во времена MS-DOS и 16-битного кода вам определенно приходилось время от времени изменять эти регистры сегментов, поскольку они никогда не могли указывать на более чем 64 КБ ОЗУ. Тем не менее, DOS может получать доступ к памяти до 1 МБ, поскольку использует 20-битные адресные указатели. Позже мы получили 32-битные системы, некоторые с 36-битными адресными регистрами и теперь 64-битные регистры. Поэтому в настоящее время вам больше не нужно менять эти регистры сегментов.
Вим тен Бринк
Ни одна современная ОС не использует 386 сегментов
Ана Беттс
@ Пол: НЕПРАВИЛЬНО! НЕПРАВИЛЬНО! НЕПРАВИЛЬНО! 16-битные сегменты заменяются 32-битными сегментами. В защищенном режиме это позволяет виртуализировать память, в основном позволяя процессору отображать физические адреса в логические. Тем не менее, в вашем приложении все по-прежнему остается плоским, поскольку ОС виртуализировала память для вас. Ядро работает в защищенном режиме, позволяя приложениям работать в плоской модели памяти. См. Также en.wikipedia.org/wiki/Protected_mode
Wim ten Brink
@Workshop ALex: это техническая составляющая. Все современные операционные системы устанавливают все сегменты на [0, FFFFFFFF]. Это на самом деле не считается. И если вы прочитаете связанную страницу, вы увидите, что все причудливые вещи сделаны со страницами, которые гораздо более детализированы, чем сегменты.
MSalters
-4

редактировать Да, это в основном неправильно. Это описывает что-то совершенно другое на случай, если кому-то интересно :)

Да, указатель стека указывает на вершину стека (будь то первая пустая ячейка или последняя полная, в которой я не уверен). Базовый указатель указывает на область памяти выполняемой инструкции. Это на уровне кодов операций - самая основная инструкция, которую вы можете получить на компьютере. Каждый код операции и его параметры хранятся в ячейке памяти. Одна строка C или C ++ или C # может быть переведена в один код операции или последовательность из двух или более в зависимости от сложности. Они записываются в память программы последовательно и выполняются. При нормальных обстоятельствах базовый указатель увеличивается на одну инструкцию. Для управления программой (GOTO, IF и т. Д.) Его можно увеличить несколько раз или просто заменить следующим адресом памяти.

В этом контексте функции хранятся в памяти программ по определенному адресу. Когда функция вызывается, в стек помещается определенная информация, которая позволяет программе найти, откуда она была вызвана, а также параметры функции, а затем адрес функции в памяти программы помещается в базовый указатель. На следующем тактовом цикле компьютер начинает выполнять инструкции с этого адреса памяти. Затем в какой-то момент он вернется в ячейку памяти ПОСЛЕ инструкции, вызвавшей функцию, и продолжит оттуда.

Стивен Фридрихс
источник
У меня возникли проблемы с пониманием, что такое ЭБП. Если у нас есть 10 строк кода MASM, это означает, что при запуске этих строк ebp будет постоянно увеличиваться?
пожрал Элизиум
1
@Devoured - Нет. Это не правда. EIP будет увеличиваться.
Майкл,
Вы имеете в виду, что то, что я сказал, правильно, но не для EBP, а для IEP, так?
пожрал Элизиум
2
Да. EIP является указателем команды и неявно изменяется после выполнения каждой инструкции.
Майкл
2
Оооо мой плохой Я думаю о другом указателе. Я думаю, что пойду вымыть мой мозг.
Стивен Фридрихс
-8

esp означает «Расширенный указатель стека» ..... ebp для «Something Base Pointer» .... и eip для «Something Instruction Pointer» ...... Указатель стека указывает на адрес смещения сегмента стека , Базовый указатель указывает на адрес смещения дополнительного сегмента. Указатель инструкций указывает на адрес смещения сегмента кода. Теперь о сегментах ... это небольшие 64КБ деления области памяти процессоров ..... Этот процесс известен как сегментация памяти. Я надеюсь, что этот пост был полезным.

Адарша харел
источник
3
Это старый вопрос, однако sp означает указатель стека, bp означает базовый указатель, а ip - указатель инструкции. E в начале каждого просто говорит, что это 32-битный указатель.
Hyden
1
Сегментация здесь не имеет значения.
BarbaraKwarc