Что означает в ассемблере

Что означает в ассемблере

Когда вы пишете программу на ассемблере, вы просто пишете команды процессору. Команды процессору — это просто коды или коды операций или опкоды. Опкоды — фактически "читаемый текст"- версии шестнадцатеричных кодов. Из-за этого, ассемблер считается самым низкоуровневым языком программирования, все в ассемблере непосредственно преобразовывается в шестнадцатеричные коды. Другими словами, у вас нет компилятора, который преобразовывает язык высокого уровня в язык низкого уровня, ассемблер только преобразовывает коды ассемблера в данные.

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

Комментарии в ваших программах оставляются после точки с запятой. Точно также как в дельфи или си через //.

Числа в ассемблере могут представляться в двоичной, десятеричной или шестнадцатеричной системе. Для того, чтобы показать в какой системе использовано число надо поставить после числа букву. Для бинарной системы пишется буква b (пример: 0000010b, 001011010b), для десятеричной системы можно ничего не указывать после числа или указать букву d (примеры: 4589, 2356d), для шестнадцатеричной системы надо указывать букву h, шестнадцатеричное число надо обязательно писать с нулём в начале (примеры: 00889h, 0AC45h, 056Fh, неправильно F145Ch, С123h).

Самая первая команда будет хорошо всем известная MOV. Эта команда используется для копирования (не обращайте внимания на имя команды) значения из одного места в другое. Это ‘место’ может быть регистр, ячейка памяти или непосредственное значение (только как исходное значение). Синтаксис команды:

Вы можете копировать значение из одного регистра в другой.

Вышеприведенная команда копирует содержание ecx в edx. Размер источника и приемника должны быть одинаковыми,

например: эта команда — НЕ допустима:

Этот опкод пытается поместить DWORD (32-битное) значение в байт (8 битов). Это не может быть сделано mov командой (для этого есть другие команды).

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

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

смещение 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F 40 41 42
данные 0D 0A 50 32 44 57 25 7A 5E 72 EF 7D FF AD C7

(Каждый блок представляет байт)

Значение смещения обозначено здесь как байт, но на самом деле это это — 32-разрядное значение. Возьмем для примера 3A, это также — 32-разрядное значение: 0000003Ah. Только, чтобы с экономить пространство, некоторые используют маленькие смещения.

Посмотрите на смещение 3A в таблице выше. Данные на этом смещении — 25, 7A, 5E, 72, EF, и т.д. Чтобы поместить значение со смещения 3A, например, в регистр, вы также используете команду mov:

Означает: поместить значение с размером DWORD (32-бита) из памяти со смещением 3Ah в регистр eax. После выполнения этой команды, eax будет содержать значение 725E7A25h. Возможно вы заметили, что это — инверсия того что находится в памяти: 25 7A 5E 72. Это потому, что значения сохраняются в памяти, используя формат little endian . Это означает, что самый младший байт сохраняется в наиболее значимом байте: порядок байтов задом на перед. Я думаю, что эти примеры покажут это:

  • dword (32-бит) значение 10203040 шестнадцатиричное сохраняется в памяти как: 40, 30, 20, 10
  • word (16-бит) значение 4050 шестнадцатиричное сохраняется в памяти как: 50, 40

Вернемся к примеру выше. Вы также можете это делать и с другими размерами:

Вы, наверное, уже поняли, что префикс ptr обозначает, что надо брать из памяти некоторый размер. А префикс перед ptr обозначает размер данных:

Иногда размер можно не указывать:

Так как eax — 32-разрядный регистр, ассемблер понимает, что ему также требуется 32-разрядное значение, в данном случае из памяти со смещением 403045h.

Можно также непосредственные значения:

Эта команда просто запишет в регистр edx, значение 5006. Скобки, [ и ], используются, для получения значения из памяти (в скобках находится смещение), без скобок, это просто непосредственное значение.

Можно также использовать регистр как ячейку памяти (он должен быть 32-разрядным в 32-разрядных программах):

В mov cx, [eax], процессор сначала смотрит, какое значение (= ячейке памяти) содержит eax, затем какое значение находится в той ячейке памяти, и помещает это значение (word, 16 бит, потому что приемник, cx, является 16-разрядным регистром) в CX.

Стековые операции — PUSH, POP. Перед тем, как рассказать вам о стековых операциях, я уже объяснял вам, что такое стек. Стек это область в памяти, на которую указывает регистр стека ESP. Стек это место для хранения адресов возврата и временных значений. Есть две команды, для размещения значения в стеке и извлечения его из стека: PUSH и POP. Команда PUSH размещает значение в стеке, т.е. помещает значение в ячейку памяти, на которую указывает регистр ESP, после этого значение регистра ESP увеличивается на 4. Команда Pop извлекает значение из стека, т.е. извлекает значение из ячейки памяти, на которую указывает регистр ESP, после этого уменьшает значение регистра ESP на 4. Значение, помещенное в стек последним, извлекается первым. При помещении значения в стек, указатель стека уменьшается, а при извлечении — увеличивается. Рассмотрим пример:

  • 1: поместить 100 в ecx
  • 2: поместить 200 в eax
  • 3: разместить значение из ecx (=100) в стеке (размещается первым)
  • 4: разместить значение из eax (=200) в стеке (размещается последним)
  • 5/6/7: выполнение операций над ecx, значение в ecx изменяется
  • 8: извлечение значения из стека в ebx: ebx станет 200 (последнее размещение, первое извлечение)
  • 9: извлечение значения из стека в ecx: ecx снова станет 100 (первое размещение, последнее извлечение)

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

(стек здесь заполнен нулями, но в действительности это не так, как здесь). ESP стоит в том месте, на которое он указывает)

Вызов подпрограмм возврат из них — CALL, RET. Команда call передает управление ближней или дальней процедуре с запоминанием в стеке адреса точки возврата. Команда ret возвращает управление из процедуры вызывающей программе, адрес возврата получает из стека. Пример:

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

Подробнее: команда call помещает регистр EIP (указатель на следующюю команду, которая должна быть выполнена) в стек, а команда ret извлекает его и передаёт управление этому адресу. Вы также можете определить аргументы, для вызываемой программы (процедуры). Это можно сделать через стек:

Внутри процедуры, аргументы могут быть прочитаны из стека и использованы. Локальные переменные, т.е. данные, которые необходимы только внутри процедуры, также могут быть сохранены в стеке. Я не буду подробно рассказывать об этом, потому, что это может быть легко сделано в ассемблерах MASM и TASM. Просто запомните, что вы можете делать процедуры и что они могут использовать параметры.

Одно важное замечание: регистр eax почти всегда используется для хранения результата процедуры.

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

Вот и кончился очередной урок. На следующем уроке мы напишем первую программу на ассемблере.

Операнд – объект, над которым выполняется машинная команда.

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

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

Читайте также:  Как перепрошить самсунг галакси джи 2
Способы адресации операндов

Под способами адресации понимаются существующие способы задания адреса хранения операндов:

Операнд задается на микропрограммном уровне (операнд по умолчанию): в этом случае команда явно не содержит операнда, алгоритм выполнения команды использует некоторые объекты по умолчанию (регистры, признаки и т.д.).

Операнд задается в самой команде (непосредственный операнд): операнд является частью кода команды. Для хранения такого операнда в команде выделяется поле длиной до 32 бит. Непосредственный операнд может быть только вторым операндом (источником). Операнд-получатель может находиться либо в памяти, либо в регистре.

Операнд находится в одном из регистров (регистровый операнд): в коде команды указываются именами регистров. В качестве регистров могут использоваться:

  • 32-разрядные регистры ЕАХ, ЕВХ, ЕСХ, EDX, ESI, EDI, ESP, EBP;
  • 16-разрядные регистры АХ, ВХ, СХ, DX, SI, DI, SP, ВР;
  • 8-разрядные регистры АН, AL, BH, BL, CH, CL, DH, DL;
  • сегментные регистры CS, DS, ,SS, ES, FS, GS.
  • прямую адресацию;
  • косвенную адресацию.

Прямая адресация : эффективный адрес определяется непосредственно полем смещения машинной команды, которое может иметь размер 8, 16 или 32 бита.

Ассемблер заменяет sum на соответствующий адрес, хранящийся в сегменте данных (по умолчанию адресуется регистром ds ) и значение, хранящееся по адресу sum , помещает в регистр eax .

Косвенная адресация в свою очередь имеет следующие виды:

  • косвенная базовая (регистровая) адресация;
  • косвенная базовая (регистровая) адресация со смещением;
  • косвенная индексная адресация;
  • косвенная базовая индексная адресация.

Косвенная базовая (регистровая) адресация. При такой адресации эффективный адрес операнда может находиться в любом из регистров общего назначения, кроме sp/esp и bp/ebp (это специфические регистры для работы с сегментом стека). Синтаксически в команде этот режим адресации выражается заключением имени регистра в квадратные скобки [].

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

Косвенная базовая (регистровая) адресация со смещением предназначена для доступа к данным с известным смещением относительно некоторого базового адреса, используется для доступа к элементам структур, когда смещение элементов известно заранее, на стадии разработки программы, а базовый (начальный) адрес структуры должен вычисляться динамически, на стадии выполнения программы. Модификация содержимого базового регистра позволяет обратиться к одноименным элементам различных экземпляров однотипных структур данных.

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

Значение эффективного адреса второго операнда вычисляется выражением mas+( esi *4) и представляет собой смещение относительно начала сегмента данных.

Наличие возможности масштабирования существенно помогает в решении проблемы индексации при условии, что размер элементов массива постоянен и составляет 1, 2, 4 или 8 байт.

Данный вид адресации также может использоваться со смещением.

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

Эффективный адрес второго операнда формируется как esi+edx . Значение по этому адресу помещается в регистр eax.

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

Операндом является порт ввода-вывода.
Помимо адресного пространства оперативной памяти микропроцессор поддерживает адресное пространство ввода-вывода, которое используется для доступа к устройствам ввода-вывода. Объем адресного пространства ввода-вывода составляет 64 Кбайт. Для любого устройства компьютера в этом пространстве выделяются адреса. Конкретное значение адреса в пределах этого пространства называется портом ввода-вывода. Физически порту ввода-вывода соответствует аппаратный регистр (не путать с регистром микропроцессора), доступ к которому осуществляется с помощью специальных команд ассемблера in и out .

Регистры, адресуемые с помощью порта ввода-вывода, могут иметь разрядность 8, 16 или 32 бит, но для конкретного порта разрядность регистра фиксирована. В качестве источника информации или получателя применяются регистры-аккумуляторы eax , ax , al . Выбор регистра определяется разрядностью порта. Номер порта может задаваться непосредственным операндом в командах in и out или значением в регистре dx . Последний способ позволяет динамически определить номер порта в программе.

Счетчик адреса – специфический вид операнда. Он обозначается знаком $. Специфика этого операнда в том, что когда транслятор ассемблера встречает в исходной программе этот символ, он подставляет вместо него текущее значение счетчика адреса (регистр EIP ). Значение счетчика адреса представляет собой смещение текущей машин­ной команды относительно начала сегмента кода, адресуемого сегментным регистром CS . При обработке транслятором очередной команды ассемблера счетчик адреса увеличивается на длину сформированной машинной команды. Обработка директив ассемблера не вле­чет за собой изменения счетчика. В качестве примера использования в команде значения счетчика адреса можно привести следующий фрагмент:

При использовании подобного выражения для перехода нельзя забывать о длине самой команды, в которой это выражение используется, так как значение счетчика адреса соответствует смещению в сегменте кода данной, а не следующей за ней команды. В приведенном выше примере команда jmp занимает 2 байта. Длина этой и некоторых других команд может зависит от того, какие в ней используются операнды. Команда с регистровыми операндами будет короче команды, один из операндов которой расположен в памяти. В большинстве случаев эту информацию можно получить, зная формат машинной команды.

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

Записи (аналогично структурному типу) используются для доступа к битовому полю некоторой записи. Для доступа к битовому полю записи используется директива RECORD .

Операторы в языке ассемблера

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

Приоритет Оператор
1 length, size, width, mask, ( ), [ ],
2 .
3 :
4 ptr, offset, seg, this
5 high, low
6 +, — (унарные)
7 *, /, mod, shl, shr
8 +, -, (бинарные)
9 eq, ne, lt, le, gt, ge
10 not
11 and
12 or, xor
13 short, type

Характеристика основных операторов.

Арифметические операторы . К ним относятся унарные операторы + и , бинарные + и , операторы умножения *, целочисленного деления /, получения остатка от деления mod. Например,

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

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

Если значение size больше или равно 50, то результат в аl равен 1, в противном случае — 0. Команда cmp сравнивает значение аl с нулем и устанавливает соответствующие флаги в EFLAGS . Команда je на основе анализа этих флагов передает или не передает управление на метку m1 .

Назначение операторов сравнения приведено в таблице

Оператор Условие
eq ==
ne !=
lt
ge >=

Логические операторы выполняют над выражениями побитовые операции. Выражения должны быть константными. Например,

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

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

Оператор переопределения типа ptr применяется для переопределения или уточнения типа метки или переменной, определяемых выражением. Тип может принимать одно из следующих значений.

Читайте также:  Умножение строки матрицы на число
Тип Пояснение Назначение
byte 1 байт переменная
word 2 байта переменная
dword 4 байта переменная
qword 8 байт переменная
tword 10 байт переменная
near ближний указатель функция
far дальний указатель функция

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

Оператор переопределения сегмента : (двоеточие) вычисляет физический адрес относительно конкретно задаваемой сегментной составляющей, в качестве которой могут выступать:

  • имя сегментного регистра,
  • имя сегмента из соответствующей директивы SEGMENT
  • имя группы.

Для выборки на выполнение очередной команды микропроцессор анализирует содержимое сегментного регистра CS , в котором содержится физический адрес начала сегмента кода. Для получения адреса конкретной команды микропроцессор складывает промасштабированное (умноженное на 16) значение сегментного регистра CS с содержимым регистра EIP . Запись CS:EIP содержит адрес текущей выполняемой команды. Аналогично обрабатываются операнды в машинных командах.

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

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

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

Оператор определения длины массива length возвращает число элементов, определенных операндом dup . Если операнд dup отсутствует, то оператор length возвращает значение 1.Например,

Оператор type возвращает число байтов, соответствующее определению указанной переменной:

Оператор size возвращает произведение длины length и типа type и используется при ссылках на переменную с операндом dup .
Для предыдущего примера

Оператор short –модификация атрибута near в команде jmp, если переход не превышает границы +127 и -128 байт. Например,

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

Оператор width возвращает размер в битах объекта типа RECORD или его поля.

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

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

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

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

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

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

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

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

По умолчанию команда 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. Ниже приведена диаграмма, основанная на статье из Википедии, чтобы проиллюстрировать вышеописанные связи:

Назад к коду

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

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

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

Здесь у нас то, с чем мы раньше не встречались: -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.

Читайте также:  Как присвоить букву диску в windows 10

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Заключение

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

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

Ссылка на основную публикацию
Чем чистить датчик абсолютного давления
ВСЁ СВОИМИ РУКАМИ 12.06.2018 . . После покупки Шевроле Лачетти оказалось, что эта первая моя машина, на которой был установлен...
Фото авы удаленного вк
Рабочий способ который на 100 процентов поможет вам вернуть и восстановить вашу удаленную фотографию в социальной сети вконтакте. Мы постарались...
Фото внутренностей айфон 6
Шаг 1 Время обзора iPhone 6! Давайте посмотрим на некоторые технические спецификации: Процессор Apple A8 с 64-битной архитектурой Копроцессор движения...
Чем хорош увлажнитель воздуха отзывы
у нас на работе стоял, попеременно двигали каждый к себе поближе, ибо да, с ним как-то лучше, мне лично глазам...
Adblock detector