Какой регистр отвечает за работу указателя инструкции

Указатель команд (инструкций)

(IP
Instruction
Pointer)

Хранит
относительный адрес, по которому в RAM
находится инструкция, следующая за
исполняемой. Фактически этот регистр
«следит» за ходом выполнения программы.
Наращивание адреса выполняет CPU,
в зависимости от длины текущей команды.
Значение, хранящееся в IP,
может изменяться в зависимости от
структуры программы. Команды условных
и безусловных переходов, циклов, вызова
подпрограмм и т.д. изменяют содержимое
IP,
осуществляя переходы к требуемой точке
команды. Разрядность регистра IP
— 16 бит.

Регистры указатели

К
ним относятся индексные
регистры
:

SI
– Source (
Источник)

DI
– Destination (
Приемник)

Используются
в основном при перемещении цепочек
данных (многобайтных последовательностей
произвольной длины), но могут использоваться
и произвольным образом. Основное
их назначение – хранить индексы
(смещения) относительно некоторой базы,

(т.е., начала массива) при выборке операндов
из памяти.

Регистр
SI
– регистр индекса источника (source
index
register).
Он содержит относительный адрес начала
цепочки, которую следует переместить.

Регистр
DI
– регистр индекса приемника (destination
index
register).
Содержит относительный адрес, по которому
нужно переместить цепочку. Число
перемещаемых байт обычно хранится в
регистре CX
(счетчике). Кроме
операций по перемещению цепочек данных,
индексные регистры используют и для
адресации внутри массивов числовых
данных. Адрес базы при этом может
находиться в базовых регистрах BX
или BP
(
base
pointer).
Т.о., в этих регистрах хранится сегментная
часть адреса.

Указатель стека

(SP
Stack
Pointer)

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

Сегментные регистры

Эти
регистры используются только!
при работе с адресами. Это важнейшие
элементы в архитектуре CPU,
т.к. обеспечивают 20-ти разрядную адресацию
адресного пространства с помощью 16-ти
разрядных операндов.

CS
(
Code
Segment
Register)

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

DS
(
Data
Segment
Register)

Регистр
сегмента данных. Обычно указывает начало
область памяти, отведенной под данные.
В сочетании с регистрами DI,
SI
или
BX
может использоваться для доступа к
определенным байтам или словам внутри
области данных.

SS
(Stack Segment Register)

Регистр
сегмента
стека.
В сочетании с
регистром SP
указывает на текущее хранимое в стеке
число. Может также использоваться и в
паре с регистром BP
при выполнении некоторых инструкций.

ES
(
Extra
Segment
Register)

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

Регистры
данных регистры-указатели
сегментные регист
ры

AH

AL

аккумулятор

SI

источник

CS

команд

BH

BL

базовый

DI

приемник

DS

данных

CH

CL

счетчик

BP

указ.базы

ES

дополн.дан.

DH

DL

данных

SP

указ.стека

SS

стека

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]

  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #
  • #

Регистры процессора

Последнее обновление: 01.07.2023

Ключевую роль в обработке данных в процессоре играют специальные ячейки, известные как регистры.
Регистры в процессоре x86-64 можно разделить на четыре категории: регистры общего назначения, специальные регистры для приложений, сегментные регистры и специальные регистры режима
ядра. Здесь нас будут интересовать прежде всего регистры общего назначения (general-purpose registers), которые в основном и используются в приложениях на ассемблере.

Начнем с того, что процессор архитектуры x86 имел восемь 32-битных регистров общего назначения, регистр флагов и указатель инструкций. Регистры общего назначения:

  • EAX (Accumulator): для арифметических операций

  • ECX (Counter): для хранения счетчика цикла

  • EDX (Data): для арифметических операций и операций ввода-вывода

  • EBX (Base): указатель на данные

  • ESP (Stack pointer): указатель на верхушку стека

  • EBP (Base pointer): указатель на базу стека внутри функции

  • ESI (Source index): указатель на источник при операциях с массивом

  • EDI (Destination index): указатель на место назначения в операциях с массивами

  • EIP: указатель адреса следующей инструкции для выполнения

  • EFLAGS: регистр флагов, содержит биты состояния процессора

Можно получить доступ к частям 32-битных регистров с меньшей разрядностью. Например, младшие 16 бит 32-битного регистра EAX обозначаются как AX. К регистру AX можно обращаться как к отдельным байтам, используя имена AH (старший байт) и AL (младший байт).

Регистры процессора Intel x86

В архитектуре х64 эти регистры были расширены до 64 бит, а новые расширенные регистры получили имена, которые начинаются с буквы R, например,
RAX, RBX и т.д. Кроме того, были добавлены 8 новых 64-битных регистров R8 — R15

Регистры в архитектуре х64

Для обращения к 32-, 16- и 8-битной части 64-разрядных регистров используются стандартные для архитектуры x86 имена регистров. Для доступа к подрегистрам новых 64-битных
регистров R8 — R15 применяется соответствующий суффикс:

  • D: для получения младших 32 бит регистра, например, R11D

  • W: для получения младших 16 бит регистра, например, R11W

  • B: для получения младших 8 бит регистра, например, R11B

Таким образом, в архитектуре x86-64 мы можем использовать следующие регистры общего пользования:

  • Шестнадцать 64-разрядных регистров RAX, RBX, RCX,
    RDX, RSI, RDI, RBP, RSP, R8, R9, R10, R11, R12, R13, R14 и R15

  • Шестнадцать 32-разрядных регистров EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP,
    R8D, R9D, R10D, R11D, R12D, R13D, R14D и R15D

  • Шестнадцать 16-разрядных регистров AX, BX, CX, DX, SI, DI, BP, SP, R8W, R9W,
    R10W, R11W, R12W, R13W, R14W и R15W

  • Шестнадцать 8-разрядных регистров AL, AH, BL, BH, CL, CH, DL, DH, DIL, SIL,
    BPL, SPL, R8B, R9B, R10B, R11B, R12B, R13B, R14B и R15B

Например, запись в часть 64-битного регистра, например в регистр AL, влияет только на биты этой части. В случае AL загрузка 8-битного значения изменяет младшие 8 битов RAX,
оставляя остальные 48 бит без изменений.

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

Регистр флагов RFLAGS, содержит биты состояния процессора:

Бит

Имя

назначение

0

CF

Флаг переноса (Carry flag):казывает, был ли при сложении перенос или заимствование при вычитании. Используется в качестве входных данных для инструкций сложения и вычитания.

2

PF

Флаг четности: устанавливается, если младшие 8 битов результата содержат четное число единиц.

4

AF

Флаг настройки: указывает, был ли при сложении перенос или заимствование при вычитании младших 4 битов.

6

ZF

Флаг нуля (Zero flag): устанавливается, если результат операции равен нулю

7

SF

Флаг знака (Sign flag): устанавливается, если результат операции отрицательный.

8

TF

Флаг прерывания выполнения (Trap flag): используется при одношаговой отладке.

9

IF

Флаг разрешения прерывания: установка этого бита разрешает аппаратные прерывания.

10

DF

Флаг направления: контролирует направление обработки. Если не установлен, то порядок от самого младшего до самого старшего адреса.
Если установлен, то порядок обратный — от самого старшего до самого младшего адреса.

11

OF

Флаг переполнения (Overflow flag): если устанавлен, то операция привела к переполнению со знаком.

12-13

IOPL

Уровень привилегий ввода-вывода (I/O privilege level): уровень привилегий текущего выполняемого потока. IOPL 0 — это режим ядра, а 3 — пользовательский режим.

14

NT

Флаг вложенной задачи (Nested task flag): управляет цепочкой прерываний.

16

RF

Флаг возобновления (Resume flag): используется для обработки исключений во время отладки.

17

VM

Флаг режима виртуальной машины 8086: если установлен, режим совместимости с 8086 активен. Этот режим позволяет запускать некоторые приложения MS-DOS в контексте операционной системы в защищенном режиме.

18

AC

Флаг проверки выравнивания (Alignment check flag): если установлен, проверка выравнивания памяти активна.
Например, если установлен флаг AC, сохранение 16-битного значения по нечетному адресу вызывает исключение проверки выравнивания.
Процессоры x86 могут выполнять невыровненный доступ к памяти, когда этот флаг не установлен, но количество требуемых командных циклов может увеличиться.

19

VIF

Флаг виртуального прерывания (Virtual interrupt flag): виртуальная версия флага IF в виртуальном режиме 8086..

20

VIP

Флаг ожидания виртуального прерывания: Устанавливается, когда прерывание находится в состоянии ожидания в виртуальном режиме 8086.

21

ID

Флаг ID: если этот бит установлен, то поддерживается инструкция cpuid. Эта инструкция возвращает идентификатор процессора и информацию о его функциях.

Основными являются флаги переноса, нуля, знака и переполнения, которые называют флагами состояния. Как видно, не все биты из 64-х разрядного регистра флагов имеют назначение.
Неуказанные биты не используются.

В дополнение к регистрам общего назначения x86-64 предоставляет регистры специального назначения, в том числе восемь регистров для работы с числами с плавающей точкой,
реализованных в модуле x87 с плавающей точкой (floating-point unit или FPU). Intel назвала эти регистры ST0 — ST7. Каждый регистр с плавающей точкой имеет ширину 80 бит.
В отличие от регистров общего назначения обычная пользовательская программа не может получить к ним прямой доступ.

Также есть шестнадцать 128-битных регистров XMM (XMM0 — XMM15) и набор инструкций SSE/SSE2. Каждый регистр можно настроить как четыре 32-битных регистра с плавающей точкой;
два 64-битных регистра двойной точности с плавающей точкой; или шестнадцать 8-битных, восемь 16-битных, четыре 32-битных, два 64-битных или один 128-битный целочисленный регистр.
В более поздних вариантах семейства процессоров x86-64 AMD/Intel удвоили размер регистров до 256 бит каждый (переименовав их в YMM0-YMM15), добавив поддержки восьми 32-битных
значений с плавающей точкой или четырех 64-битных значений с плавающей точкой двойной точности. (целочисленные операции по-прежнему ограничивались 128 битами).

Разбираемся в С, изучая ассемблер

Время на прочтение
11 мин

Количество просмотров 86K

Перевод статьи Дэвида Альберта — Understanding C by learning assembly.

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

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

Примечание автора: Весь код из этой статьи был скомпилирован на процессоре x86_64 под Mac OS X 10.8.1 с использованием Clang 4.0 с отключенной оптимизацией (-O0).

Изучаем ассемблер с помощью GDB

Давайте начнем с дизассемблирования программы с помощью GDB и научимся читать выходные данные. Наберите следующий текст программы и сохраните его в файле simple.c:

int main(void)
{
    int a = 5;
    int b = a + 6;
    return 0;
}

Теперь скомпилируйте его в отладочном режиме и с отключенной оптимизацией и запустите GDB.

$ CFLAGS="-g -O0" make simple
cc -g -O0 simple.c -o simple
$ gdb simple

Поставьте точку останова на функции main и продолжайте выполнение до тех пор, пока не дойдете до оператора return. Введите число 2 после оператора next, чтобы указать, что мы хотим выполнить его дважды:

(gdb) break main
(gdb) run
(gdb) next 2

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

(gdb) disassemble
Dump of assembler code for function main:
0x0000000100000f50 <main+0>:    push   %rbp
0x0000000100000f51 <main+1>:    mov    %rsp,%rbp
0x0000000100000f54 <main+4>:    mov    $0x0,%eax
0x0000000100000f59 <main+9>:    movl   $0x0,-0x4(%rbp)
0x0000000100000f60 <main+16>:   movl   $0x5,-0x8(%rbp)
0x0000000100000f67 <main+23>:   mov    -0x8(%rbp),%ecx
0x0000000100000f6a <main+26>:   add    $0x6,%ecx
0x0000000100000f70 <main+32>:   mov    %ecx,-0xc(%rbp)
0x0000000100000f73 <main+35>:   pop    %rbp
0x0000000100000f74 <main+36>:   retq   
End of assembler dump.

По умолчанию команда disassemble выводит инструкции в синтаксисе AT&T, который совпадает с синтаксисом, используемым ассемблером GNU. Синтаксис AT&T имеет формат: mnemonic source, destination. Где mnemonic — это понятные человеку имена инструкций. А source и destination являются операндами, которые могут быть непосредственными значениями, регистрами, адресами памяти или метками. В свою очередь, непосредственные значения — это константы, они имеют префикс $. Например, $0x5 соответствует числу 5 в шестнадцатеричном представлении. Имена регистров записываются с префиксом %.

Регистры

На изучение регистров стоит потратить некоторое время. Регистры — это места хранения данных, которые находятся непосредственно на центральном процессоре. С некоторыми исключениями, размер или ширина регистров процессора определяет его архитектуру. Поэтому, если у вас есть 64-битный CPU, то его регистры будут иметь ширину в 64 бита. То же самое касается и 32-битных и 16-битных процессоров и т. д. Скорость доступа к регистрам очень высокая и именно из-за этого в них часто хранятся операнды арифметических и логических операций.

Семейство процессоров с архитектурой x86 имеет ряд специальных регистров и регистров общего назначения. Регистры общего назначения могут быть использованы для любых операций, и данные, хранящиеся в них, не имеют особого значения для процессора. С другой стороны, процессор в своей работе опирается на специальные регистры, и данные, которые хранятся в них, имеют определенное значение в зависимости от конкретного регистра. В нашем примере %eax и %ecx — регистры общего назначения, в то время как %rbp и %rsp — специальные регистры. Регистр %rbp — это указатель базы, который указывает на базу текущего стекового фрейма, а %rsp — указатель стека, который указывает на вершину текущего стекового фрейма. Регистр %rbp всегда имеет большее значение нежели %rsp, потому что стек всегда начинается со старшего адреса памяти и растет в сторону младших адресов. Если Вы не знакомы с понятием “стек вызовов”, то можете найти хорошее объяснение на Википедии.

Особенность процессоров семейства x86 в том, что они сохраняют полную совместимость с 16-битными процессорами 8086. В процессе перехода x86 архитектуры от 16-битной к 32-битной и в конце-концов к 64-битной, регистры были расширены и получили новые имена, чтобы сохранить совместимость с кодом, который был написан для более ранних процессоров.

Возьмем регистр общего назначения AX, который имеет ширину в 16 бит. Доступ к его старшему байту осуществляется по имени AH, а к младшему — по имени AL. Когда появился 32-битный 80386, расширенный (Extended) AX или EAX стал 32-битным регистром, в то время как AX остался 16-битным и стал младшей половиной регистра EAX. Аналогичным образом, когда появилась x86_64, то был использован префикс “R” и EAX стал младшей половиной 64-битного регистра RAX. Ниже приведена диаграмма, основанная на статье из Википедии, чтобы проиллюстрировать вышеописанные связи:

|__64__|__56__|__48__|__40__|__32__|__24__|__16__|__8___|
|__________________________RAX__________________________|
|xxxxxxxxxxxxxxxxxxxxxxxxxxx|____________EAX____________|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|_____AX______|
|xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx|__AH__|__AL__|
Назад к коду

Этого уже должно быть достаточно, чтобы перейти к разбору нашей дизассемблированой программы:

0x0000000100000f50 <main+0>:    push   %rbp
0x0000000100000f51 <main+1>:    mov    %rsp,%rbp

Первые две инструкции называются прологом функции или преамбулой. Первым делом записываем старый указатель базы в стек, чтобы сохранить его на будущее. Потом копируем значение указателя стека в указатель базы. После этого %rbp указывает на базовый сегмент стекового фрейма функции main.

0x0000000100000f54 <main+4>:    mov    $0x0,%eax

Эта инструкция копирует 0 в %eax. Соглашение о вызовах архитектуры x86 гласит, что возвращаемые функцией значения хранятся в регистре %eax, поэтому вышеуказанная инструкция предписывает нам вернуть 0 в конце нашей функции.

0x0000000100000f59 <main+9>:    movl   $0x0,-0x4(%rbp)

Здесь у нас то, с чем мы раньше не встречались: -0x4(%rbp). Круглые скобки дают нам понять, что это адрес памяти. В этом фрагменте %rbp, так называемый регистр базы, и -0x4, являющееся смещением. Это эквивалентно записи %rbp + -0x4. Поскольку стек растет вниз, то вычитание 4 из базового стекового фрейма перемещает нас к собственно текущему фрейму, где хранится локальная переменная. Это значит, что эта инструкция сохраняет 0 по адресу %rbp — 4. Мне потребовалось некоторое время, чтобы выяснить, для чего служит эта строчка, и как мне кажется Clang выделяет скрытую локальную переменную для неявно возвращаемого значения из функции main.

Вы также можете заметить, что mnemonic имеет суффикс l. Это означает, что операнд будет иметь тип long (32 бита для целых чисел). Другие возможные суффиксы — byte, short, word, quad, и ten. Если Вам попадется инструкция, не имеющая суффикса, то размер такой инструкции будет подразумеваться из размера регистра источника или регистра назначения. Например, в предыдущей строчке %eax имеет ширину 32 бита, поэтому инструкция mov на самом деле является movl.

0x0000000100000f60 <main+16>:   movl   $0x5,-0x8(%rbp)

Теперь мы переходим в самую сердцевину нашей тестовой программы. Приведенная строка ассемблера — это первая строка на С в функции main, и она помещает число 5 в следующий доступный слот локальной переменной (%rbp — 0x8), на 4 байта ниже от нашей предыдущей локальной переменной. Это местоположение переменной a. Мы можем использовать GDB, чтобы проверить это:

(gdb) x &a
0x7fff5fbff768: 0x00000005
(gdb) x $rbp - 8
0x7fff5fbff768: 0x00000005

Заметьте, что адрес памяти один и тот же. Также Вы можете обратить внимание, что GDB устанавливает переменные для наших регистров, поэтому, как и перед всеми переменными в GDB, перед их именем стоит префикс $, в то время как префикс % используется в ассемблере от AT&T.

0x0000000100000f67 <main+23>:   mov    -0x8(%rbp),%ecx
0x0000000100000f6a <main+26>:   add    $0x6,%ecx
0x0000000100000f70 <main+32>:   mov    %ecx,-0xc(%rbp)

Далее мы помещаем переменную a в %ecx, один из наших регистров общего назначения, добавляем к ней число 6 и сохраняем результат в %rbp — 0xc. Это вторая строчка функции main. Вы могли уже догадаться, что адрес %rbp — 0xc соответствует переменной b, что мы тоже можем проверить с помощью GDB:

(gdb) x &b
0x7fff5fbff764: 0x0000000b
(gdb) x $rbp - 0xc
0x7fff5fbff764: 0x0000000b

Остальное в функции main — это просто процесс уборки, который еще называют эпилогом.

0x0000000100000f73 <main+35>:   pop    %rbp
0x0000000100000f74 <main+36>:   retq

Мы достаем старый указатель базы и помещаем его обратно в %rbp, а затем инструкция retq перебрасывает нас к адресу возвращения, который тоже хранится в стековом фрейме.

До этого момента мы использовали GDB для дизассемблирования небольшой программы на С, прошли через чтение синтаксиса ассемблера от AT&T и раскрыли тему регистров и операндов адресов памяти. Также мы использовали GDB для проверки места хранения локальных переменных по отношению к %rbp. Теперь используем приобретенные знания для объяснения принципов работы статических локальных переменных.

Разбираемся в статических локальных переменных

Статические локальные переменные — это очень классная особенность С. В двух словах, это локальные переменные, которые инициализируются один раз и сохраняют свое значение между вызовами функции, в которой были объявлены. Простой пример использования статических локальных переменных — это генератор в стиле Python. Вот один такой, который генерирует все натуральные числа вплоть до INT_MAX.

/* static.c */
#include <stdio.h>
int natural_generator()
{
        int a = 1;
        static int b = -1;
        b += 1;
        return a + b;
}

int main()
{
        printf("%d\n", natural_generator());
        printf("%d\n", natural_generator());
        printf("%d\n", natural_generator());

        return 0;
}

Когда вы скомпилируете и запустите эту программу, то она выведет три первых натуральных числа:

$ CFLAGS="-g -O0" make static
cc -g -O0    static.c   -o static
$ ./static
1
2
3

Но как это работает? Чтобы это выяснить, перейдем в GDB и посмотрим на ассемблерный код. Я удалил адресную информацию, которую GDB добавляет в дизассемблерный вывод и теперь все помещается на экране:

$ gdb static
(gdb) break natural_generator
(gdb) run
(gdb) disassemble
Dump of assembler code for function natural_generator:
push   %rbp
mov    %rsp,%rbp
movl   $0x1,-0x4(%rbp)
mov    0x177(%rip),%eax     # 0x100001018 <natural_generator.b>
add    $0x1,%eax
mov    %eax,0x16c(%rip)     # 0x100001018 <natural_generator.b>
mov    -0x4(%rbp),%eax
add    0x163(%rip),%eax     # 0x100001018 <natural_generator.b>
pop    %rbp
retq   
End of assembler dump.

Первое, что нам нужно сделать, это выяснить, на какой инструкции мы сейчас находимся. Сделать это мы можем путем изучения указателя инструкции или счетчика команды. Указатель инструкции — это регистр, который хранит адрес следующей инструкции. В архитектуре x86_64 этот регистр называется %rip. Мы можем получить доступ к указателю инструкции с помощью переменной $rip, или, как альтернативу, можем использовать архитектурно независимую переменную $pc:

(gdb) x/i $pc
0x100000e94 <natural_generator+4>:  movl   $0x1,-0x4(%rbp)

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

Поскольку знать следующую инструкцию — это очень полезно, то мы заставим GDB показывать нам следующую инструкцию каждый раз, когда программа останавливается. В GDB 7.0 и выше, вы можете просто выполнить команду set disassemble-next-line on, которая показывает все инструкции, которые будут исполнены в следующей строке программного кода. Но я использую Mac OS X, который поставляется с версией GDB 6.3, так что мне придется пользоваться командой display. Эта команда аналогична x, за исключением того, что она показывает значение выражения после каждой остановки программы:

(gdb) display/i $pc
1: x/i $pc  0x100000e94 <natural_generator+4>:  movl   $0x1,-0x4(%rbp)

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

Мы уже прошли пролог функции, который рассматривали ранее, поэтому начнем сразу с третьей инструкции. Она соответствует первой строке кода, которая присваивает 1 переменной a. Вместо команды next, которая переходит к следующей строчке кода, мы будем использовать nexti, которая переходит к следующей ассемблерной инструкции. Теперь исследуем адрес %rbp — 0x4, чтобы проверить гипотезу о том, что переменная a хранится именно здесь:

(gdb) nexti
7           b += 1;
1: x/i $pc  mov   0x177(%rip),%eax # 0x100001018 <natural_generator.b>
(gdb) x $rbp - 0x4
0x7fff5fbff78c: 0x00000001
(gdb) x &a
0x7fff5fbff78c: 0x00000001

И мы видим, что адреса одинаковые, как мы и ожидали. Следующая инструкция более интересная:

mov    0x177(%rip),%eax     # 0x100001018 <natural_generator.b>

Здесь мы ожидали увидеть выполнение инструкций строки static int b = -1;, но это выглядит существенно иначе, нежели то, с чем мы встречались раньше. С одной стороны, нет никаких ссылок на стековый фрейм, где мы ожидали увидеть локальные переменные. Нет даже -0x1! В место этого, у нас есть инструкция, которая загружает что-то из адреса 0x100001018, находящегося где-то после указателя инструкции, в регистр %eax. GDB дает нам полезный комментарий с результатом вычисления операнда памяти, чем подсказывает, что по этому адресу размещается natural_generator.b. Давайте выполним инструкцию и разберемся, что происходит:

(gdb) nexti
(gdb) p $rax
$3 = 4294967295
(gdb) p/x $rax
$5 = 0xffffffff

Несмотря на то, что дизассемблер показывает как получателя регистр %eax, мы выводим $rax, поскольку GDB задает переменные для полной ширины регистра.

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

(gdb) p (int)$rax
$11 = -1

Похоже на то, что мы нашли b. Можем повторно убедиться в этом, используя команду x:

(gdb) x/d 0x100001018
0x100001018 <natural_generator.b>:  -1
(gdb) x/d &b
0x100001018 <natural_generator.b>:  -1

Так что переменная b не только хранится в другом участке памяти, вне стека, но и к тому же инициализируется значением -1 еще до того, как вызывается функция natural_generator. На самом деле, даже если вы дизассемблируете всю программу, то не найдете никакого кода, устанавливающего b в -1. Все это потому, что значение переменной b зашито в другой секции исполняемого файла нашей программы, и оно загружается в память вместе со всем машинным кодом загрузчиком операционной системы, когда процесс запущен.

С таким подходом, вещи начинают обретать смысл. После сохранения b в %eax, мы переходим к следующей строке кода, где мы увеличиваем b. Это соответствует следующим инструкциям:

add    $0x1,%eax
mov    %eax,0x16c(%rip)     # 0x100001018 <natural_generator.b>

Здесь мы добавляем 1 к %eax и записываем результат обратно в память. Давайте выполним эти инструкции и посмотрим на результат:

(gdb) nexti 2
(gdb) x/d &b
0x100001018 <natural_generator.b>:  0
(gdb) p (int)$rax
$15 = 0

Следующие две инструкции отвечают за возвращение результата a + b:

mov    -0x4(%rbp),%eax
add    0x163(%rip),%eax     # 0x100001018 <natural_generator.b>

Здесь мы загружаем переменную a в %eax, а затем добавляем b. На данном этапе мы ожидаем, что в %eax хранится значение 1. Давайте проверим:

(gdb) nexti 2
(gdb) p $rax
$16 = 1

Регистр %eax используется для хранения значения, возвращаемого функцией natural_generator, и мы ожидаем на эпилог, который очистит стек и приведет к возвращению:

pop    %rbp
retq

Мы разобрались, как переменная b инициализируется. Теперь давайте посмотрим, что происходит, когда функция natural_generator вызывается повторно:

(gdb) continue
Continuing.
1

Breakpoint 1, natural_generator () at static.c:5
5           int a = 1;
1: x/i $pc  0x100000e94 <natural_generator+4>:  movl   $0x1,-0x4(%rbp)
(gdb) x &b
0x100001018 <natural_generator.b>:  0

Поскольку переменная b не хранится на стеке с остальными переменными, она все еще 0 при повторном вызове natural_generator. Не важно сколько раз будет вызываться наш генератор, переменная b всегда будет сохранять свое предыдущее значение. Все это потому, что она хранится вне стека и инициализируется, когда загрузчик помещает программу в память, а не по какому-то из наших машинных кодов.

Заключение

Мы начали с разбора ассемблерных команд и научились дизассемблировать программу с помощью GDB. В последствии, мы разобрали, как работают статические локальные переменные, чего мы не смогли бы сделать без дизассемблирования исполняемого файла.
Мы провели много времени, чередуя чтение ассемблерных инструкций и проверки наших гипотез с помощью GBD. Это может показаться скучным, но есть веская причина для следующего подхода: лучший способ изучить что-то абстрактное, это сделать его более конкретным, а один из лучших способов сделать что-то более конкретным — это использовать инструменты, которые помогут заглянуть за слои абстракции. Лучший способ изучить эти инструменты — это заставлять себя использовать их, пока это не станет для вас обыденностью.

От переводчика: Низкоуровневое программирование — не мой профиль, поэтому если допустил какие-то неточности, буду рад узнать о них в ЛС.

Указатель инструкции

Указатель инструкции содержит смещение в сегменте кода текущей исполняемой инструкции процессора. 16-разрядный регистр-указатель инструкции носит название IP, 32-разрядный — EIP, 64-разрядный — RIP.

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

Материалы сообщества доступны в соответствии с условиями лицензии CC-BY-SA, если не указано иное.

A register serves as a quick memory for accepting, storing, and sending data and instructions that the CPU will need right away. A register is a collection of flip-flops, Single bit digital data is stored using flip-flops. By combining many flip-flops, the storage capacity can be extended to accommodate a huge number of bits. We must utilize an n-bit register with n flip flops if we wish to store an n-bit word.

The gates govern the flow of information, i.e., when and how the information is sent into a register, whereas the flip-flops store the binary information.

General Purpose Registers

Working of Registers:

When we provide the system with input, that input is stored in registers, and when the system returns results after processing, those results are also drawn from the registers. so that the CPU can use them to process the data that the user provides.

Registers are performed based on three operations:

  • Fetch: The Fetch Operation is used to retrieve user-provided instructions that have been stored in the main memory. Registers are used to fetch these instructions.
  • Decode: The Decode Operation is used to interpret the Instructions, which means that the CPU will determine which Operation has to be carried out on the Instructions after the Instructions have been decoded.
  • Execute: The CPU manages the Execute Operation. The results that the CPU generates are then stored in the memory before being presented on the user screen.

Types of Registers:

  • Status and control registers.
  • General-purpose data registers.
  • Special purpose register.

Status and Control Register:

Status and Control registers report and allow modification of the state of the processor and of the program being executed.

General Purpose Registers and Special Purpose Registers

General-Purpose Data Registers: 

General purpose registers are extra registers that are present in the CPU and are utilized anytime data or a memory location is required. These registers are used for storing operands and pointers. These are mainly used for holding the following:

  • Operands for logical and arithmetic operations
  • Operands for address calculation
  • Memory pointers

There are 3 types of General-purpose data registers they are:

Data registers: Data registers consists of four 32-bit data registers, which are used for arithmetic, logical and other operations. Data registers are again classified into 4 types they are:

  • AX: This is known as the accumulator register. Its 16 bits are split into two 8-bit registers, AH and AL, allowing it to execute 8-bit instructions as well. In 8086 microprocessors, it is used in the arithmetic, logic, and data transfer instructions. One of the numbers involved in manipulation and division must be in AX or AL.
  • BX: This is called a Base register. It has 16 bits and is split into two registers with 8 bits each, BH and BL. An address register is the BX  register. It typically includes a data pointer for indirect addressing that is based, based indexed, or register-based.
  • CX: This is known as the Count register. Its 16 bits are split into two 8-bit registers, CH and CL, allowing it to execute 8-bit instructions as well. This acts as a counter for loops. It facilitates the development of program loops. Shift/rotate instructions and string manipulation both allow the use of the count register as a counter.
  • DX: This is known as the Data register. Its 16 bits are split into two 8-bit registers, DH and DL so that it can execute 8-bit instructions as well. In I/O operations, the data register can be used as a port number. It is also applied to division and multiplication.

Pointer registers: The pointer registers consist of 16-bit left sections (SP, and BP) and 32-bit ESP and EBP registers.

  • SP: This is known as a Stack pointer used to point the program stack. For accessing the stack segment, it works with SS. It has a 16-bit size. It designates the item at the top of the stack. The stack pointer will be (FFFE)H if the stack is empty. The stack segment is relative to its offset address.
  • BP: This is known as the Base pointer used to point data in the stack segments. We can utilize BP to access data in the other segments, unlike SP. It has a 16-bit size. It mostly serves as a way to access parameters given via the stack. The stack segment is relative to its offset address.

Index registers: The 16-bit rightmost bits of the 32-bit ESI and EDI index registers. SI and DI are sometimes employed in addition and sometimes in subtraction as well as for indexed addressing.

  •  SI: This source index register is used to identify memory addresses in the data segment that DS is addressing. Therefore, it is simple to access successive memory locations when we increment the contents of SI. It has a 16-bit size. Relative to the data segment, it has an offset.
  • DI: The function of this destination index register is identical to that of SI. String operations are a subclass of instructions that employ DI to access the memory addresses specified by ES. It is generally used as a Destination index for string operations.

Special Purpose Registers:

To store machine state data and change state configuration, special purpose registers are employed. In other words, it is also defined as the CPU has a number of registers that are used to carry out instruction execution these registers are called special purpose registers. Special purpose registers are of 8 types they are cs, ds, ss, es, fs, and gs registers come under segment registers. These registers hold up to six segment selectors. 

  • CS (Code Segment register): A 16-bit register called a code segment (CS) holds the address of a 64 KB section together with CPU instructions. All accesses to instructions referred to by an instruction pointer (IP) register are made by the CPU using the CS segment. Direct changes to CS registration are not possible. When using the far jump, far call, and far return instructions, the CS register is automatically updated.
  •  DS (Data Segment register): A 64KB segment of program data is addressed using a 16-bit register called the data segment. The processor by default believes that the data segment contains all information referred to by the general registers (AX, BX, CX, and DX) and index registers (SI, DI). POP and LDS commands can be used to directly alter the DS register.
  •  SS (Stack Segment register): A 16-bit register called a stack segment holds the address of a 64KB segment with a software stack. The CPU by default believes that the stack segment contains all information referred to by the stack pointer (SP) and base pointer (BP) registers. POP instruction allows for direct modification of the SS register.
  •  ES (Extra Segment register): A 16-bit register called extra segment holds the address of a 64KB segment, typically holding program data. In string manipulation instructions, the CPU defaults to assuming that the DI register refers to the ES segment. POP and LES commands can be used to directly update the ES register.
  •  FS (File Segment register): FS registers don’t have a purpose that is predetermined by the CPU; instead, the OS that runs them gives them a purpose. On Windows processes, FS is used to point to the thread information block (TIB).
  •  GS (Graphics Segment register): The GS register is used in Windows 64-bit to point to operating system-defined structures. OS kernels frequently use GS to access thread-specific memory. The GS register is employed by Windows to control thread-specific memory. In order to access CPU-specific memory, the Linux kernel employs GS. A pointer to a thread local storage, or TLS, is frequently used as GS.
  •  IP (Instruction Pointer register): The registers CS and IP are used by the 8086 to access instructions. The segment number of the following instruction is stored in the CS register, while the offset is stored in the IP register. Every time an instruction is executed, IP is modified to point to the upcoming instruction. The IP cannot be directly modified by an instruction, unlike other registers; an instruction may not have the IP as its operand.
  •  Flag register: The status register for an x86 CPU houses its current state, and it is called the FLAGS register. The flag bits’ size and significance vary depending on the architecture. It often includes information about current CPU operation limitations as well as the outcome of mathematical operations. Some of these limitations might forbid the execution of a particular class of “privileged” instructions and stop some interrupts from triggering. Other status flags may override memory mapping and specify the response the CPU should have in the event of an arithmetic overrun.

Last Updated :
30 Nov, 2022

Like Article

Save Article

Понравилась статья? Поделить с друзьями:
  • Микроволновая печь витек инструкция по эксплуатации
  • Эдо лайт в честном знаке инструкция по применению
  • Весы miniland baby инструкция по применению на русском
  • General satellite gs b531m инструкция по применению
  • Стиральная машина electrolux ews 126510 w инструкция