Введение в язык ассемблера - Жуан Ифэн

задняя часть

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

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

Что компьютер действительно может понять, так это низкоуровневый язык, предназначенный для управления оборудованием. Язык ассемблера — это язык низкого уровня, который непосредственно описывает/управляет работой процессора. Если вы хотите понять, что делает ЦП и как работает код, вы должны изучить язык ассемблера.

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

1. Что такое ассемблер?

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

Все эти инструкции являются двоичными и называются кодами операций.Например, инструкция сложения00000011.переводчикФункция состоит в том, чтобы перевести программу, написанную на языке высокого уровня, в строку кодов операций.

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

Язык ассемблера — это текстовая форма двоичных инструкций., который имеет взаимно однозначное соответствие с инструкциями. Например, инструкция добавления00000011На языке ассемблера написано ADD. Пока он восстановлен в двоичном виде, язык ассемблера может напрямую выполняться процессором, поэтому это язык низкого уровня самого низкого уровня.

2. История

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

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

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

Машинные инструкции каждого процессора разные, поэтому и соответствующий язык ассемблера тоже разный. В этой статье представлен наиболее распространенный язык ассемблера x86, используемый процессорами Intel.

3. Зарегистрируйтесь

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

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

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

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

4. Типы регистров

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

  • EAX
  • EBX
  • ECX
  • EDX
  • EDI
  • ESI
  • EBP
  • ESP

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

Мы часто видим такие названия, как 32-битный ЦП и 64-битный ЦП, которые на самом деле относятся к размеру регистра. Размер регистра 32-битного процессора составляет 4 байта.

5. Модель памяти: куча

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

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

Во время работы программы для запросов на использование динамической памяти (таких как создание новых объектов или использованиеmallocкоманда) система выделит часть заранее выделенной памяти пользователю.Конкретное правило - начинать деление со стартового адреса (на самом деле в стартовом адресе будет кусок статических данных, который здесь игнорируется) . Например, пользователь запрашивает получить 10 байт памяти, затем со начального адреса0x1000Начать выделять ему, вплоть до адреса0x100A, если требуется еще 22 байта, то он выделяется0x1020.

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

6. Модель памяти: стек

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

См. пример ниже.


int main() {
   int a = 2;
   int b = 3;
}

В приведенном выше коде система начинает выполнятьmainфункция, она создаст для нее кадр в памяти, всеmainвнутренние переменные (например,aиb) хранятся в этом кадре.mainПосле завершения выполнения функции кадр будет переработан, освобождая все внутренние переменные и больше не занимая место.

Что произойдет, если внутри функции будут вызваны другие функции?


int main() {
   int a = 2;
   int b = 3;
   return add_a_and_b(a, b);
}

В приведенном выше кодеmainВызывается внутри функцииadd_a_and_bфункция. При выполнении этой строки система такжеadd_a_and_bСоздайте новый фрейм для хранения его внутренних переменных. То есть есть два кадра одновременно:mainиadd_a_and_b. Вообще говоря, фреймов столько, сколько слоев в стеке вызовов.

Подожди покаadd_a_and_bПо окончании прогона его кадр будет переработан, и система вернется к функцииmainТам, где выполнение было прервано только что, продолжайте выполнение. Благодаря этому механизму функция вызывается слой за слоем, и каждый слой может использовать свои собственные локальные переменные.

Все кадры хранятся в стеке.Поскольку кадры складываются слой за слоем, стек называется стеком. Генерация нового фрейма называется «in the stack», что по-английски «вталкивает», а повторное использование стека называется «out of the stack», что по-английски «поп». Особенность стека заключается в том, что кадр, который помещается в стек самое позднее, первым выходит из стека (поскольку самый внутренний вызов функции заканчивает операцию первым), что называется «последний вошел, первый вышел». " структура данных. Каждый раз, когда выполнение функции заканчивается, кадр автоматически освобождается, и весь стек освобождается после завершения выполнения всех функций.

Стек начинается с конечного адреса области памяти и распределяется от старшего (адрес) к младшему (адрес). Например, конечный адрес области памяти0x8000, предполагается, что первый кадр имеет размер 16 байт, тогда следующий выделенный адрес будет из0x7FF0start; предполагается, что второй кадр требует 64 байта, тогда адрес перемещается в0x7FB0.

Семь, инструкции процессора

7.1 Пример

Разобравшись с регистром и моделью памяти, вы сможете понять, что такое язык ассемблера. Ниже представлена ​​простая программаexample.c.


int add_a_and_b(int a, int b) {
   return a + b;
}

int main() {
   return add_a_and_b(2, 3);
}

gcc преобразует эту программу в язык ассемблера.


$ gcc -S example.c

После выполнения вышеуказанной команды будет сгенерирован текстовый файлexample.s, который представляет собой язык ассемблера, содержащий десятки строк инструкций. Скажем так, для простой операции на языке высокого уровня нижний слой может состоять из нескольких, а то и десятков инструкций процессора. ЦП последовательно выполняет эти инструкции, чтобы завершить этот шаг.

example.sПосле упрощения это может выглядеть следующим образом.


_add_a_and_b:
   push   %ebx
   mov    %eax, [%esp+8] 
   mov    %ebx, [%esp+12]
   add    %eax, %ebx 
   pop    %ebx 
   ret  

_main:
   push   3
   push   2
   call   _add_a_and_b 
   add    %esp, 8
   ret

Видно, что две функции исходной программыadd_a_and_bиmain, соответствующие двум меткам_add_a_and_bи_main. Внутри каждой метки находится запущенный процесс ЦП, в который преобразуется функция.

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


push   %ebx

В этой строкеpushэто инструкция ЦП,%ebxоператор, используемый этой инструкцией. Инструкция ЦП может иметь ноль или более операторов.

Далее я объясню эту программу сборки построчно, читателям рекомендуется скопировать эту программу в другое окно, чтобы не прокручивать страницу при чтении.

7.2 push-инструкция

По соглашению процедура начинается с_mainМетка начнет выполняться и отобразится в стеке какmainСоздайте кадр и запишите адрес, на который указывает Stack, в регистр ESP. Если есть данные, которые будут записаны позжеmainЭтот кадр будет записан по адресу, хранящемуся в регистре ESP.

Затем начните выполнять первую строку кода.


push   3

pushИнструкция используется для помещения оператора в стек, вот3написатьmainэтот кадр.

Хотя это выглядит просто,pushДирективы на самом деле имеют предварительную операцию. Сначала он возьмет адрес в регистре ESP, вычтет из него 4 байта, а затем запишет новый адрес в регистр ESP. Вычитание используется, потому что стек развивается от старшего к младшему, а 4 байта — потому что3Типint, занимает 4 байта. После получения нового адреса в первые четыре байта этого адреса записывается 3.


push   2

То же самое касается второй строки,pushинструкция будет2написатьmainЭтот кадр, положение близко к ранее написанному3. В этот момент регистр ESP будет вычтен еще на 4 байта (всего 8).

7.3 инструкция вызова

третий рядcallИнструкции используются для вызова функций.


call   _add_a_and_b

Вышеприведенный код говорит, чтобы позвонитьadd_a_and_bфункция. В это время программа найдет_add_a_and_bпометить и создать новый кадр для функции.

Начать выполнение ниже_add_a_and_bкод.


push   %ebx

Эта строка означает запись значения в регистр EBX в_add_a_and_bэтот кадр. Это связано с тем, что когда этот регистр используется позже, значение в нем сначала вынимается, а затем записывается обратно после использования.

В настоящее время,pushЗатем инструкция вычитает 4 байта из адреса в регистре ESP (суммарно минус 12).

7.4 инструкция по перемещению

movИнструкции используются для записи значения в регистр.


mov    %eax, [%esp+8] 

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

Следующая строка кода делает то же самое.


mov    %ebx, [%esp+12] 

Приведенный выше код добавляет 12 байт к значению регистра ESP, а затем извлекает данные из стека по этому адресу.3, запишите его в регистр EBX.

7.5 Инструкция добавления

addИнструкция используется для добавления двух операторов и записи результата в первый оператор.


add    %eax, %ebx

Приведенный выше код складывает значение регистра EAX (т.е. 2) со значением регистра EBX (т.е. 3), чтобы получить результат 5, а затем записывает этот результат в регистр EAX первого оператора.

7.6 поп-команда

popИнструкция используется для извлечения последнего записанного значения стека (то есть значения самого младшего адреса) и записи этого значения в место, указанное оператором.


pop    %ebx

Приведенный выше код означает извлечение самого последнего записанного значения из стека (т. е. исходного значения регистра EBX), а затем запись этого значения обратно в регистр EBX (поскольку добавление завершено, регистр EBX не используется).

Уведомление,popИнструкция также увеличивает адрес в регистре ESP на 4, что освобождает 4 байта.

7.7 инструкция возврата

retИнструкция используется для прекращения выполнения текущей функции и возврата права выполнения вышестоящей функции. То есть кадр текущей функции будет переработан.


ret

Как видите, в этой инструкции нет операторов.

вместе сadd_a_and_bФункция завершает выполнение, и система возвращается к предыдущемуmainТам, где функция прервана, продолжить выполнение.


add    %esp, 8 

Приведенный выше код указывает на то, что вручную добавить 8 байт к адресу в регистре ESP, а затем записать его обратно в регистр ESP. Это связано с тем, что регистр ESP является начальным адресом записи стека.popОперация восстановила 4 байта, а здесь восстановлено 8 байтов, что равно всем восстановленным.


ret

наконец-то,mainФункция завершила работу,retИнструкция завершает выполнение программы.

8. Справочная ссылка

(Заканчивать)