
- •Аппаратно-ориентированное программирование
- •Ббк 32.973.73
- •Удк 681.3 ббк 32.973.73ф 73
- •1. Основы программирования на ассемблере
- •1.1. Принципы построения ассемблерных программ
- •1.2. Понятие архитектуры компьютера
- •1.3. Регистры программиста в ia32
- •1.4. Описание сегментной структуры программы
- •2. Простейшие средства ассемблера
- •2.1. Средства описания данных
- •2.2. Обращения к функциям ос посредством прерываний
- •2.3. Средства преобразования в исполняемый файл
- •2.4. Управление строками при выводе и ввод данных
- •2.5. Простейшие способы адресации
- •3. Архитектурные элементы для построения программ
- •3.1. Организация условных переходов
- •3.2. Средства организации циклов
- •3.3. Особенности команд умножения и деления
- •3.4. Организация процедур
- •3.5. Неарифметические операции над кодами
- •4. Использование неэлементарных способов адресации
- •4.1. Косвенно-регистровая адресация
- •4.2. Использование индексной адресации данных
- •4.3. Базовая и индексно базовая адресации
- •4.4. Адресация с масштабированием
- •5. Взаимосвязи программных единиц
- •5.1. Многомодульная разработка программ
- •5.2. Использование библиотек объектных модулей
- •5.3. Организация стекового кадра подпрограммы
- •5.4. Программный доступ к системным функциям Win32
- •5.5. Особенности использования объектных файлов формата coff
- •5.6. Стандартный доступ к системным функциям Unix
- •6. Вспомогательные средства базовой архитектуры
- •6.1. Использование строковых команд пересылки
- •6.2. Применение строковых команд сравнения
- •7. Использование ассемблерных отладчиков
- •7.1. Особенности отладчика gdb для программ в Linux
- •7.2. Отладчики текстового режима для Windows
- •Библиографический список
- •Оглавление
3.4. Организация процедур
Одним из важнейших понятий в программировании являются подпрограммы. Подпрограммой называется группа команд, которая может выполняться специальным запуском из другого места программы, причем после такого запуска далее продолжается выполнение программы за местом запуска. Запуск подпрограммы называется вызовом подпрограммы (английское слово CALL). В языках высоко уровня приказ на вызов подпрограммы обычно (согласно синтаксису конкретного языка программирования) записывается просто через имя подпрограммы и перечисление аргументов для этого выполнения. На нижнем уровне программирования, задаваемого ассемблером, команда вызова подпрограммы записывается с помощью мнемокода CALL.
В общем случае к подпрограммам обращаются другие подпрограммы, поэтому подпрограмма, которая обратилась к другой подпрограмме, называется вызывающей, а та подпрограмма, которую вызвали, называется вызванной. С учетом того, что основную подпрограмму можно обычно рассматривать как подпрограмму относительно операционной системы, этими обозначениями достигается единая терминология.
К подпрограмме могут обращаться другие подпрограммы (или основная программа) из разных мест этих вызывающих программ. Каждый раз после завершения действий вызванной программы необходимо вернуться в место, следовавшее за местом вызова (в вызванной программе), т.е. каждый раз, возможно, в разные места. В некоторых устаревших архитектурах для сохранения такой информации о месте возврата использовались регистры. В более современной архитектуры для этих целей используется стек. Тот самый аппаратно-программный стек, который встроен в архитектуру процессора.
Адрес следующей команды за командой CALL (за командой обращения к подпрограмме) называют адресом возврата. Именно этот адрес возврата следует использовать, чтобы вернуться к продолжению выполнения вызывающей программ из подпрограммы. Для проникновения в существо проблем, возникающих при организации и использовании подпрограмм, нам потребуются дополнительные детали из архитектуры процессора.
Важнейшим регистром процессора является регистр, в котором автоматически поддерживается адрес следующей для выполнения команды. Его обычно называют счетчиком команд. В архитектуре Intel этот регистр назван указателем инструкций (instruction pointer) и обозначается EIP. Он имеет разрядность 32 бита. В 16-битной архитектуре используется младшая половина этого регистра, обозначаемая IP. Следует заметить, что в 16-битной архитектуре этого регистра в большинстве случаев недостаточно для указания места следующей команды, так как с помощью 16-битного кода можно указать только смещение в 64 килобайтном сегменте, а это мало даже для компьютеров 80-х годов XX века. Поэтому для указания начала самого сегмента в архитектуре служит еще регистр CS (сокращение от code segment), который имеет 16 битов. В 32-битной архитектуре этот регистр также используется, но довольно сложным образом, и, главное, манипуляции над ним практически всегда поручаются операционной системе. Поэтому понимание роли этого специального регистра в 32-битных ОС не существенно для начинающих программистов. Мы для начального знакомства ограничимся рассмотрением использования регистра EIP.
Может быть, начинающим интересно узнать, каким образом в регистре EIP автоматически поддерживается адрес следующей для выполнения команды. Делает это аппаратура и, в большинстве случаев, достаточно просто. В самом начале выполнения любой команды к содержимому регистра EIP прибавляется длина этой команды (которая взята из сегмента команд для выполнения; это делается еще до анализа, что и как должна делать текущая команда). При выполнении команд управления - условных и безусловных переходов, циклов и некоторых других - в регистр EIP заносится адрес команды, на которую нужно перейти, если этот переход действительно реализуется. В результате таких организационных решений для выборки следующей команды из памяти (из сегмента команд) аппаратуре необходимо только прочитать машинный код, начиная с адреса, задаваемого регистром EIP!
Объяснений, приведенных в начале раздела для описания существа запоминаемой информации при вызове подпрограмм, должно быть достаточно, чтобы понять: - запоминать при вызове следует как раз содержимое регистра EIP. Поэтому аппаратура как бы выполняет команду PUSH EIP - в действительности не нужно записывать в программе что-нибудь подобное, это действие выполняется в ходе реализации команды CALL. Команда CALL в простейшей форме используется в виде
CALL имя_подпрограммы
Заметим, что в архитектуре Intel для обозначения подпрограмм принята более частная терминология, а именно они обозначаются термином процедура. Напомним, что в терминологии языка Си все подпрограммы называются функциями, но в языке Паскаль используются оба термина: процедурами в нем называются подпрограммы, не возвращающие собственного значения, а функциями называются подпрограммы, обязательно возвращающие собственное значение. Из-за несогласованного многообразия использования этих терминов мы и применяем более общие понятие подпрограммы. Далее через некоторое время мы перейдем на использование термина процедура в архитектуре Intel, пока же мы используем термин подпрограмма для более точного изложения существа вопросов.
Работа аппаратно-программного стека в архитектуре Intel также использует специальные регистры. Наиболее важным для начального знакомства построения этого стека является регистр ESP - расширенный регистр указателя стека (Stack Pointer). Он также 32-битный, а его младшая половина с обозначением SP используется в 16-битной архитектуре с этими же целями. Регистр ESP своим содержимым - относительным адресом в сегменте стека - указывает на верхнее запомненное поле в стеке. При выполнении команды PUSH для двойного слова регистр EIP автоматически уменьшается на 4. Стек растет от старших адресов к младшим, заполняясь со дна, которым является самый старший адрес в сегменте стека. При выполнении команды POP, которая снимает из стека 4-байтовое значение двойного слова, регистр EIP автоматически увеличивается на 4. При выполнении команд PUSH и POP для слов одинарной длины (16-битных операндов) указатель стека в EIP уменьшается или увеличивается, соответственно, на 2. (Заметим, что адрес начала сегмента стека задается с помощью специального регистра SS - Stack Segment, но нам особенности использования этого регистра сейчас не нужны.)
Наиболее важной командой в составе подпрограммы является команда, задаваемая мнемокодом RET. Ее действие соответствует действию оператора return в языке программирования Си. Детальные действия команды RET заключаются в том, что она снимает из стека верхнее значение и помещает его в регистр EIP (в 16-битной архитектуре снимает слово и помещает его в IP). Как следствие, следующей автоматически будет выполняться команда, расположенная по адресу возврата, т.е. расположенная сразу за той командой CALL вызова подпрограммы, которая и обеспечила перед этим обращение к подпрограмме. В отличие от языков высокого уровня, в частности языка Си, присутствие команды RET совершенно необходимо в подпрограмме. При отсутствии этой команды в конце подпрограммы будет автоматически выполняться двоичный код команды, записанный в памяти за последней командой, предусмотренной программистом в исходном коде. За последней командой подпрограмм в машинном коде не может быть ничего, какие-то коды, оставшиеся от других программ или коды команд других подпрограмм, там будут обязательно (редким, но возможным явлением может оказаться нарушение защиты памяти, но для программиста это также не то, что ему бы хотелось).
В ассемблерах MASM и TASM подражающим языкам высокого уровня для выделения команд подпрограммы из остальной части программного текста служат специализированные директивы, задаваемые ключевыми словами PROC и ENDP. Их используют согласно следующей схеме:
имя_подпрограммы PROC
команды подпрограммы
ret
имя_подпрограммы ENDP
Таким образом, директива с ключевым словом PROC служит для именования подпрограммы, а вспомогательная директива с ключевым словом ENDP - для обозначения конца подпрограммы, причем в обоих директивах должно быть использовано одно и то же имя, иначе диагностируется ошибка.
В ассемблере NASM синтаксис гораздо проще, но, в то же время, значительно дальше от привычного для многих стиля алголоподобных языков высокого уровня. Подпрограммой здесь может быть любая последовательная группа команд, начинающаяся с метки и завершающаяся командой RET. Таким образом, подпрограмма здесь имеет общий вид
имя_подпрограммы:
команды подпрограммы
ret
Иными словами, абсолютно ничего лишнего, все элементы минимально необходимы. С учетом того, что для начинающих может казаться неудобным невыделение служебными конструкциями начала и конца подпрограммы, мы будем в начале и конце подпрограмм вставлять поясняющие комментарии. Целесообразно делать так же и в более серьезном программировании на ассемблере.
При построении и использовании подпрограмм возникает серьезная проблема, которую на техническом жаргоне называют косвенным эффектом. Косвенный эффект подпрограммы может заключаться в том, что кроме желаемых целей будут изменяться данные, используемые где-то вне подпрограммы. Этими данными оказывается содержимое регистров, которые, по существу, являются глобальными переменными.
При вызове подпрограммы естественно полагать, что текущие значения в регистрах останутся неизменными и после возврата из подпрограммы, если только какие-то регистры не предназначены для выдачи результатов из подпрограммы. Но внутри достаточно сложной подпрограммы эти самые регистры могут быть использованы для каких-то потребностей этой подпрограммы. Особенно характерна такая ситуация для использования подпрограмм, написанных другим программистом или достаточно давно.
Чтобы гарантировано избежать косвенного эффекта изменения регистров, достаточно в начале подпрограммы сохранить значения регистров, используемых подпрограммой, а в конце подпрограммы - восстановить значения этих регистров, используя запомненные значения. Наиболее удобное место для временного хранения содержимого регистров - это стек. (Такое решение обеспечивает в перспективе, как мы уведим позже, рекурсивное использование подпрограмм.). Таким образом, правильно построенная подпрограмма имеет, как минимум, вид
имя_подпрограммы:
PUSH регистр1
PUSH регистр2
. . .
PUSH регистрN
команды подпрограммы
POP регистрN
. . .
POP регистр2
POP регистр1
ret
где регистр1, регистр2, . . ., регистрN обозначают все регистры, используемые в этой подрограмме. Следует обратить особое внимание на то, что восстановление регистров производится в порядке, обратном их запоминанию в стеке (иначе содержимое регистров поменяется).
Когда используемых в подпрограмме регистров много, целесообразно для их запоминания использовать специальную команду PUSHA, а для их восстановления - команду POPA. Эти команды запоминают и восстанавливают, соответственно, регистры EAX, EBX, ECX, EDX, ESP, EBP, ESI, EDI. (Кроме уже изложенных выше, здесь используется еще специальный регистр EBP, роль и применение которого будут рассмотрены позже.)