Файл листинга ассемблер что это
Компилятор VC++ может создать текстовый файл, показывающий ассемблерный код, сгенерированный для файла C/C++ file. Этот файл позволяет узнать, какой вид кода генерирует компилятор. Файл дает хорошее представление о ряде принципов, таких как обработка исключений, таблицы вызова и т.д. Элементарного знания языка ассемблера достаточно для понимания вывода файла листинга. Цель данной статьи (первой в серии из двух статей) – показать, как файл листинга помогает понять внутренние механизмы компилятора C++.
Сегмент Const
Компилятор поместил постоянную строку "Hello World" в сегмент CONST. Последствия этого показаны на следующем пробном приложении.
Сначала это пробное приложение печатает "Hello World", пытается преобразовать строку "Hello" в "Hola'" и в конце печатает измененную строку. Скомпонуем и запустим приложение. Оно аварийно завершит работу с исключением нарушения доступа и строкой szHelloWorld[2] = 'l';.
На этот раз приложение успешно запустится. Изучение листинга показывает причину.
1. В первом случае данные "Hello World" помещаются в сегмент CONST, являющийся неизменяемым сегментом
2. Во втором случае данные помещаются в сегмент _DATA, являющийся сегментом для чтения и записи
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Лабораторная работа №3. Работа с файлом листинга
Краткие теоретические сведения
Права доступа к файлам
Как многопользовательская операционная система, ОС Linux содержит механизм разграничения доступа к данным, позволяющий как защитить данные одного пользователя от нежелательного вмешательства других, так и разрешить другим доступ к этим данным для совместной работы. Любой ресурс компьютера под управлением ОС Linux представляется как файл.
По отношению к файлу пользователь может входить в одну из трех категорий: владелец, член группы владельца, все остальные. Для каждой из этих категорий есть свой набор прав доступа. Первым владельцем файла становится его создатель. Дальше файл можно передать другому владельцу или в другую группу командой
Набор прав доступа задается тройками битов и состоит из прав на чтение, запись и исполнение файла. В символьном представлении он выглядит как строка rwx , где вместо любого символа может стоять дефис. Буква означает наличие права (установлен в единицу второй бит триады r — чтение, первый бит w - запись, нулевой бит х - исполнение), а дефис означает отсутствие права (нулевое значение соответствующего бита). Очевидно, что эти три бита могут быть записаны еще и как восьмеричное число. Так, права доступа r-х (чтение и исполнение без записи) понимаются как три двоичные цифры 101 или как восьмеричная цифра 5. Численное представление прав доступа называется абсолютным, или двоичной маской.
Полная строка прав доступа в символьном представлении устроена так:
В абсолютном представлении права владельца являются старшей цифрой восьмеричного числа, права группы - средней
и права остальных - младшей. Так, права rwxr-x--x выглядят как двоичное число 111 101 001, или восьмеричное 751.
Команда изменения прав доступа chmod понимает как абсолютное, так и символьное указание прав.
Свойства (атрибуты) файлов и каталогов можно вывести на терминал с помощью команды ls с ключом -l :
Назначим файлу /home/debugger/README права rw-r , то есть разрешим себе чтение и запись, группе только чтение, остальным пользователям — ничего:
В символьном представлении можно явно указывать, кому какое право мы хотим добавить, отнять или присвоить. Добавим право на исполнение файла README группе и всем остальным:
Формат символьного режима:
Возможные значения аргументов команды представлены в таблице:
& Значение
Категория & u & Владелец
& g & Группа владельца
& о & Прочие
& а & Все пользователи, то есть «а» эквивалентно «ugo»
Действие & + & Добавить набор прав
& – & Отменить набор прав
& = & Назначить набор прав
Право & r & Право на чтение
& w & Право на запись
& x & Право на исполнение
& s & Право смены идентификатора пользователя или группы
& t & Бит прилипчивости (sticky-бит)
& u & Такие же права, как у владельца
& g & Такие же права, как у группы
& о & Такие же права, как у прочих
Название бита прилипчивости унаследовано от тех времен, когда объем оперативной памяти был маленьким, а процесс подкачки медленным. Этот бит позволял оставлять небольшие часто используемые программы в памяти для ускорения их запуска. Сейчас его значение переосмыслено: этот бит, установленный для каталога, приводит к тому, что удалять файлы из этого каталога могут только владелец файла и владелец каталога. Обычно это используется в каталогах, открытых для записи всем (например, /tmp ).
Права смены пользователя и группы ( SUID -бит и SGID -бит) означают следующее. Обычно исполняемый файл (программа или командный сценарий) получает те же права на доступ к файлам, что и пользователь, который запустил его на выполнение. Но у этого файла есть еще и владелец, полномочия которого могут быть совсем другими. Наличие одного из этих битов позволяет выполняющейся программе пользоваться полномочиями владельца программного файла или члена его группы.
Назначение файла листинга
Листинг — это один из выходных файлов, создаваемых транслятором. Он имеет текстовый вид и нужен при отладке программы, т. к. кроме строк самой программы содержит дополнительную информацию.
Обычно nasm создает в результате ассемблирования только объектный файл. Получить файл листинга можно, указав ключ -l и задав имя файла листинга в командной строке. Например:
nasm -l main.lst main.asm
Рассмотрим фрагмент файла листинга.
Строки в первой части листинга имеют следующую структуру (рис. [pic:l3]):
Все ошибки и предупреждения, обнаруженные при ассемблировании, транслятор выводит на экран и файл листинга не создается.
- «Номер строки» представляет собой номер строки файла листинга. Номера строк особенно полезны при работе с перекрестными ссылками. Учтите, что номера строк в поле «номер строки» — это не номера строк исходного модуля. Например, при расширении макрокоманды или включении файла отсчет строк продолжается, хотя текущая строка в исходном файле остается той же. Чтобы перевести номер строки (сгенерированный, например, при создании перекрестных ссылок), вы должны найти соответствующую строку в листинге, а затем (по номеру или на глаз) найти ее в исходном файле.
- «Адрес» — это смещение машинного кода от начала текущего сегмента.
- «Машинный код» представляет собой действительную последовательность шестнадцатеричного значения байт и слов, которые ассемблируются из соответствующей исходной строки программы. Например, инструкция int 80h начинается по смещению 00000014 в сегменте кода. Информация справа от данной инструкции - это машинный код, в который ассемблируется инструкция, то есть инструкция int 80h ассемблируется в CD80 (в шестнадцатеричном представлении). CD80 - это инструкция на машинном языке, вызывающая прерывание ядра.
- Наконец, поле «исходный текст программы» — это просто строка исходной программы вместе с комментариями. Некоторые строки на языке ассемблера (например, строки, содержащие только комментарии) не генерируют никакого машинного кода, и поля «смещение» и «исходный текст программы» в таких строках отсутствуют. Тем не менее номер строки им присваивается.
Описание инструкции вычитания
Схема команды целочисленного вычитания sub выглядит следующим образом:
sub операнд_1, операнд_2
работы команды включает два действия:
- выполнить вычитание: операнд_1 = операнд_1 - операнд_2 ;
- установить флаги.
Флаги, устанавливаемые командой, подробнее рассматриваются ниже.
Команды условного перехода
Все машинные команды условного перехода, кроме одной, вычисляют условие перехода, анализируя один, два или три флага из регистра флагов, и лишь одна команда условного перехода вычисляет условие перехода, анализируя значение регистра CX. Команда условного перехода в языке Ассемблер имеет вид
Мнемоника перехода (от одной до трёх букв) связана со значением анализируемых флагов (или регистра CX), либо со способом формирования этих флагов. Чаще всего программисты формируют флаги, проверяя отношение между двумя операндами op1 op2 , для чего выполняется команда вычитания или команда сравнения. Команда сравнения имеет мнемонический код операции cmp и такой же формат, как и команда вычитания:
Она и выполняется точно так же, как команда вычитания — за исключением того, что разность не записывается на место первого операнда. Таким образом, единственным результатом команды сравнения является формирование флагов, которые устанавливаются так же, как и при выполнении команды вычитания.
Программист по своему желанию может трактовать результат вычитания (сравнения) как производимый над знаковыми или же беззнаковыми числами. От этой трактовки может зависеть, будет ли один операнд считаться большим, чем другой, или же нет. Так, например, рассмотрим два коротких целых числа 0FFh и 01h — с учетом того, что отрицательные числа представляются процессором в дополнительном коде. Если числа знаковые, 0FFh = -1 < 01h = 1, а если беззнаковые, то 0FFh = 255 >01h = 1.
Инструкции условной передачи управления бывают следующими:
Мнемокод | условие перехода | |
Флаги | Смысл | |
ja/jnbe | CF or ZF=0 | выше /не ниже и не равно |
jae/jnb | CF=0 | выше или равно/не ниже |
jb/jnae | CF=1 | ниже/не выше и не равно |
jbe/jna | CF or ZF=1 | ниже или равно/не выше |
je/jz | ZF=1 | равно/нуль |
jne/jnz | ZF=0 | не равно/не нуль |
jg/jnle | (SF xor OF) or ZF=0 | больше/не меньше и не равно |
jge/jnl | SF xor OF=0 | больше или равно/не меньше |
jl/jnge | (SF xor OF)=1 | меньше/не больше и не равно |
jle/jng | ((SF xor OF) or ZF)=1 | меньше или равно/не больше |
jp/jpe | PF=1 | есть паритет/паритет четный |
jnp/jpo | PF=0 | нет паритета/паритет нечетный |
jc | CF=1 | перенос |
jnc | CF=0 | нет переноса |
jo | OF=1 | переполнение |
jno | OF=0 | нет переполнения |
jns | SF=0 | знак + |
js | SF=1 | знак - |
Мнемоники, идентичные по своему действию, написаны в таблице через дробь (например, ja и jnbe ). Программист выбирает, какую из них применить, чтобы получить более простой для понимания текст программы.
Примечание: термины «выше» («a» от англ. «above») и «ниже» («b» от англ. «below»)применимы для сравнения беззнаковых величин (адресов), а термины «больше» («g» от англ. «greater») и «меньше» («l» от англ. «lower») используются при учете знака числа. Таким образом, мнемонику инструкции ja/jnbe можно расшифровать как «jump (переход) if above (если выше) / if not below equal (если не меньше или равно)».
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
ЛАБОРАТОРНАЯ РАБОТА 3
Цель: Научиться работать с файлом листинга; изучить дополнительные приёмы компоновки и использования директив объявления данных; научиться программировать ветвления в ассемблерной программе и вести простой диалог через устройство ввода вывода.
Краткие теоретические сведения
Листинг – это один из выходных файлов, создаваемых транслятором. Он имеет текстовый вид и нужен при отладке программы, т.к. кроме строк самой программы содержит дополнительную информацию.
Обычно as создает в результате ассемблирования только объектный файл. Получить файл листинга можно, указав ключ -a и задав имя файла листинга в командной строке. Например:
Строки в первой части листинга имеют следующую структуру:
Все ошибки и предупреждения, обнаруженные при ассемблировании, транслятор выводит на экран, и файл листинга не создается.
Номер строки представляет собой номер строки файла листинга. Номера строк особенно полезны при работе с перекрестными ссылками.
Важно понимать, что номера строк в поле “номер строки” — это не номера строк исходного модуля. Например, при расширении макрокоманды или включении файла отсчет строк продолжается, хотя текущая строка в исходном файле остается той же. Чтобы перевести номер строки (сгенерированный, например, при создании перекрестных ссылок), вы должны найти соответствующую строку в листинге, а затем (по номеру или на глаз) найти ее в исходном файле.
Адрес — это смещение машинного кода от начала текущего сегмента.
Машинный код представляет собой действительную последовательность шестнадцатеричного значения байт и слов, которые ассемблируются из соответствующей исходной строки программы. Информация справа от данной инструкции — это машинный код, в который ассемблируется инструкция.
Исходный текст программы — это просто строка исходной программы вместе с комментариями. Некоторые строки на языке ассемблера (например, строки, содержащие только комментарии) не генерируют никакого машинного кода, и поля “смещение” и “исходный текст программы” в таких строках отсутствуют. Тем не менее номер строки им присваивается.
Подробнее о содержимом и об опциях создания файла листинга можно прочесть, выполнив команду .
Описание инструкций. Команды условного перехода
Команда перехода по адресу, имеющаяся в наборе инструкций процессоров ARM , выглядит следующим образом: b , где аргумент – метка, поставленная выше или ниже в коде программы – указывает, по какому именно адресу должен быть выполнен переход.
Как и многие другие команды ARM , команда перехода может быть снабжена кодом условия, и будет выполняться, только если это условие истинно. Добавив к команде b код условия, мы получаем целое семейство команд условного перехода.
Команда условного перехода выполняет или не выполняет переход по заданному адресу в зависимости от флагов состояния процессора, хранящихся в специальном регистре cpsr . Флаги – это биты специального регистра, отражающие состояние процессора в текущий момент времени. Наиболее часто используются следующие: флаг отрицательного результата N (установлен в единицу, если реузльтат последней арифметической операции был отрицательным), флаг нуля Z (единица, если в результатом последней операции был ноль), флаг переноса С (устанавливается, если в результате последней операции случился перенос бита из старшего разряда) и флаг переполнения V (в результате последней операции произошло переполнение разрядной сетки).
Чаще всего программисты формируют флаги, проверяя отношение между двумя операндами op1 op2 , для чего выполняется команда вычитания или команда сравнения. Команда сравнения имеет мнемонический код операции cmp и такой же формат, как и команда вычитания:
Она и выполняется точно так же, как команда вычитания за исключением того, что разность не записывается на место первого операнда. Таким образом, единственным результатом команды сравнения является формирование флагов, которые устанавливаются так же, как и при выполнении команды вычитания. Команды этой группы выполняют условный переход в зависимости от состояния флагов регистра cpsr . Синтаксис этих команд в ассемблере приведен в таблице.
В результате, ветвления в программе организуются в два этапа:
- проверка условия командой cmp или формирование флагов каким-то другим способом, например, арифметической инструкцией;
- использование инструкции условного перехода, т.е. одной из разновидностей команды b , приведенной в таблице.
Как упоминалось ранее, объединение и размещение секций выполняет компоновцик. С помощью специального скрипта компоновщика программист может управлять тем, как именно объединяются секции и в какой области памяти они размещаются. Ниже приведён пример очень простого скрипта.
- Команда SECTIONS определяет, как будут объединены секции и куда они должны быть помещены.
- В блоке, следующем после команды SECTIONS , приводится численное значение — счётчик размещений. Размещение всегда инициализируется значением 0x0 . Его можно проинициализировать каким-либо другим значением. В данном случае установка нами значения в ноль избыточное действие.
- и 4. Эта часть скрипта определяет, что секции .text из исходных файлов abc.o и def.o должны перейти в секцию .text выходного файла.
Скрипт компоновщика можно упростить указанием символа * вместо имён файлов:
Если программа содержит обе секции ( .text и .data ), то объединение и размещение секции .data можно выполнить следующим образом:
Здесь секция .text помещается по адресу 0x0 , а секция .data по адресу 0x400 . Если же счётчику размещений не были присвоены конкретные значения, то секции помещаются в соседних областях памяти.
Пример скрипта компоновщика
Используем последний пример скрипта для управления расположением программных секций .text и .data . Для этой цели воспользуемся слегка модифицированной версией программы для вычисления суммы элементов массива:
Единственное отличие — то, что массив теперь находится в секции .data . Также стоит отметить, что в этой программе не нужна инструкция для перепрыгивания через данные, т. к. скрипт корректно размещает секции .text и .data . В результате объявление данных может быть расположено в программе в любом удобном месте, а скрипт компоновщика позаботится о правильном размещении секций а памяти.
Когда программа компонуется, скрипт передаётся в качестве входных данных компоновщику, как показано в следующих командах:
Опция -T определяет, что файл sum-data.lds должен быть использован в качестве скрипта компоновщика. Сброс таблицы символов даст понимание того, как секции помещаются в памяти
Из таблицы символов становится очевидным, что секция .text размещается с адреса 0x0 , а секция .data с 0x400 .
Больше об ассемблерных директивах
Рассмотрим еще несколько часто используемых директив на примере двух программ:
- Программа суммы элементов массива
- Программа, считающая длину строки
Следующий код суммирует массив байт и сохраняет результат в r3 :
В коде представлены две новые ассемблерные директивы .byte и .align . Эти директивы описаны ниже.
Аргументы директивы .byte имеют размер 1 байт и собраны в последовательность байт в памяти. Существуют аналогичные директивы .2byte и .4byte для хранения 16- и 32-битных значений соответственно. Общий синтаксис приведён ниже
Аргумент может быть простым целым числом представленным в двоичной, восьмеричной, десятичной или шестнадцатеричной формах. Целые числа могут также быть представлены как символьные константы (обозначены одинарными кавычками), в этом случае будет использоваться ASCII значение символа.
Аргументом могут также служить выражения составленные их букв и других символов. Например:
Процессору ARM требуется, чтобы инструкции были представлены в 32-битных ячейках памяти. Адрес первого из четырёх байт должен быть кратным 4. Чтобы придерживаться этого, директива .align используется для заполнения недостающими битами до тех пор, пока адрес не будет кратным 4. Это требуется только когда данные байт или полуслов вставлены в код.
Следующий код считает длину строки и сохраняет результат в регистр r1:
В коде представлены две новые ассемблерные директивы .asciz и .equ .
Директива .asciz в качестве аргументов принимает строковые литералы. Строковые литералы представляют собой последовательность символов в двойных кавычках. Строковые литералы собраны в ячейки памяти последовательно. Ассемблер автоматически вставляет нули после каждой строки.
Директива .ascii аналогична .asciz , но ассемблер не вставляет нули после каждой строки.
Ассемблер поддерживает так называемые таблицы символов. В них содержатся имена меток с адресами. Всякий раз, когда ассемблер встречает определение метки, он делает запись в таблицу символов. Всякий раз, когда ассемблер встречает ссылку на метку, он заменяет метку соответствующим адресом из таблицы символов.
Использование директивы .equ также возможно для ручной вставки записей в таблицу символов для сопоставления имён и значений, которые не обязательно являются адресами. Эти имена и имена меток вместе называются символьными именами.
Общий синтаксис этой директивы представлен ниже.
Имя является символьным именем и имеет те же ограничения, что и имя метки. Выражение может быть простым литералом или выражением, описанным в директиве .byte .
Организация обмена информации
Для простейшего обмена информацией программы с окружающим миром мы воспользуемся последовательной консолью. Для использования ее в эмуляторе QEMU необходимо выполнить проброс порта UART . UART (универсальный асинхронный приемопередатчик) – устройство и одноимённый протокол, обеспечивающие передачу данных по последовательному интерфейсу. Наиболее широко распространенным примером является последовательный порт персонального компьютера, однако последовательный интерфейс широко используется и в микроконтроллерных устройствах. UART эмулируется системой qemu-system-arm, и мы соединим его c консолью хоста – то есть выполним проброс из хост-системы (снаружи эмулятора) в гостевую систему (внутрь).
Чтобы было удобно вести диалог с ассемблерной программой, мы соединим последовательную консоль эмулятора с одним из графических окон терминала, запущенных на хост-системе. Доступ к терминалу на хост-системе будет осуществляться через виртуальный файл устройства /dev/pts/? , где знак вопроса заменяется номером консоли. Узнать, какой именно файл устройства соответствует запущенному треминалу можно, выполнив в нём команду tty . Так, например, для использования первого терминала запуск эмулятора будет иметь вид:
Таким образом, информация будет отправляться и приниматься с двух сторон: из окружающей среды, в роли котороый выступает ОС GNU /Linux на хост-системе, и изнутри эмулятора QEMU . В эмулируемой qemu-system-arm модели connex в качестве последовательного порта с нулевым номером выступает порт по адресу 0x40100000 . Поэтому для того чтобы почесть или передать байт, нужно почесть или записать байт по адресу 0x101f1000 :
Чтобы иметь возможность читать через UART то, что отвечает пользователь, может понадобиться отключить перехват текстового ввода (по умолчанию ввод с клавиатуры, выполненный в окне терминала, получает запущенная в нём программа-облочка командной строки). Самый простой способ это сделать – поставить запущенную в терминале оболочку на большую паузу, например “усыпить” на целый день командой sleep 1d .
Также заметим, что для корректной работы этой программы необходимо использовать скрипт компоновщика, размещающий по отдельным адресам секции кода и данных, как было показано в примере выше.
Последнее замечание будет касаться точности эмуляции UART . Реализованная модель в QEMU не воспроизводит в точности передачу байтов с точки зрения синхронизации и задержек, существующих в реальном устройстве: данные в эмулируемом UART просто мгновенно “появляются” по соответствующему адресу. В реальном устройстве его использование несколько сложнее (например, требуется проверка флага “Transmit FIFO Full” по дополнительному порту, прежде чем выполнять вывод очередного байта, и т.д.).
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Лабораторная работа №1. Первая программа на языке ассемблера
Краткие теоретические сведения
Структура ассемблерной программы
В отличие от многих современных высокоуровневых языков программирования, в ассемблерной программе каждая команда располагается на отдельной строке. Нельзя разместить несколько команд на одной строке. Не принято также разбивать одну команду на несколько строк.
Синтаксис ассемблера NASM , которым мы будем пользоваться далее, является регистрочувствительным. Т.е. есть разница между большими и малыми буквами.
Команда может быть директивой — указанием транслятору, которое выполняется в процессе превращения программы в машинный код. Многие директивы начинаются с точки. Для удобства чтения программы они обычно пишутся БОЛЬШИМИ БУКВАМИ. Кроме директив еще бывают инструкции — команды процессору. Именно они и будут составлять машинный код программы.
Особенности создания ассемблерной программы
На платформе Linux язык ассемблера является самым низкоуровневым языком программирования. Т.е. он больше любых других приближен к архитектуре ЭВМ и ее аппаратным возможностям, позволяет получить к ним более полный доступ, нежели в языках выского уровня, наподобие C/C++, Perl, Python и пр. Заметим, что получить полный доступ к ресурсам компьютера в современных архитектурах нельзя, самым низким уровнем работы прикладной программы является обращение напрямую к ядру ОС. Именно на этом уровне и работают программы, написанные на ассемблере в Linux. Но, в отличие от языков высокого уровня (ЯВУ), ассемблерная программа содержит только тот код, который ввел программист, и конечно же вся ответственность за логичность кода полностью лежит на плечах программиста.
Простой пример. Обычно подпрограммы заканчиваются командой возврата. Если в ЯВУ ее не задать явно, транслятор все равно добавит ее в конец подпрограммы. Ассемблерная подпрограмма без команды возврата не вернется в точку вызова, а будет выполнять код, следующий за подпрограммой, как будто он является ее продолжением.
Эти особенности приводят к тому, что ассемблерные программы часто «подвешивают» компьютер, особенно у начинающих программистов. Выделим разновидности «зависания» по способу борьбы с ним.
- Простое — для выхода из него достаточно нажать Ctrl+C (сначала нажимается клавиша Ctrl, и дальше нужно, не отпуская ее, нажать вторую клавишу — C; затем клавиши отпускаются в любом порядке). Программа при этом аварийно завершается выходом в ОС.
- Мягкое — кажется, что машина никак не реагирует на клавиатуру и безнадежно зависла. В любом случае, ядро системы при этом продолжает работать и позволяет использовать базовые функции для сохранения целостности данных. Этими функциями можно управлять при помощи т. н. Magic Keys (см. описание SysRq Keys).
- Жесткое — если зависло ядро ОС. Это может случиться в случае использования тестового ядра, находящегося в разработке, или при неправильной ручной сборке ядра, или при попытке использовать недокументированные особенности аппаратного обеспечения. В этом случае поможет аппаратный сброс при помощи кнопки «Reset», расположенной на передней панели системного блока.
Важно помнить, что в 90% случаев зависание является простым. Чаще всего не хватает аппаратных возможностей компьютера для быстрой обработки данных и необходимо просто подождать или нажать Ctrl+C.
Процесс обработки программы на языке ассемблера
Из-за специфики программирования, а также по традиции, для создания программ на языке ассемблера обычно пользуются утилитами командной строки (хотя поддержка ассемблера и есть в некоторых универсальных интегрированных средах). Весь процесс технического создания ассемблерной программы можно разбить на 4 шага (исключены этапы создания алгоритма, выбора структур данных и т.д.).
- Набор программы в текстовом редакторе и сохранение ее в отдельном файле. Каждый файл имеет имя и тип, называемый иногда расширением. Тип в основном используется для определения назначения файла. Например, программа на C имеет тип c , на Pascal — pas , на языке ассемблера — asm .
- Обработка текста программы транслятором. На этом этапе текст превращается в машинный код, называемый объектным. Кроме того есть возможность получить листинг программы, содержащий кроме текста программы различную дополнительную информацию и таблицы, созданные транслятором. Тип объектного файла — o , файла листинга — lst . Этот этап называется трансляцией.
- Обработка полученного объектного кода компоновщиком. Тут программа «привязывается» к конкретным условиям выполнения на ЭВМ. Полученный машинный код называется выполняемым. Кроме того, обычно получается карта загрузки программы в ОЗУ. Выполняемый файл обычно не имеет расширения в отличие от программ ОС семейства DOS и Windows, карта загрузки — map . Этот этап называется компоновкой или линковкой.
- Запуск программы. Если программа работает не совсем корректно, перед этим может присутствовать этап отладки программы при помощи специальной программы — отладчика. При нахождении ошибки приходится проводить коррекцию программы, возвращаясь к шагу 1.
Таким образом, процесс создания ассемблерной программы можно изобразить в виде следующей схемы. Конечной целью, напомним, является работоспособный выполняемый файл hello (см. рис. [pic:l1]).
Основные возможности текстового редактора mcedit
mcedit — это текстовый редактор, встроенный в двухпанельный файловый менеджер Midnight Commander. Сама по себе среда Midnight Commander (или просто mc ) очень схожа с другими «командерами». Например, чтобы создать в текущем каталоге файл lab1.asm и начать его редактирование, можно набрать:
Общий вид командной строки для запуска:
mcedit [-bcCdfhstVx?] [+число] file
+число | переход к указанной числом строке (не ставьте пробел между знаком + и числом) |
-b | черно-белая цветовая гамма |
-c | цветовой режим ANSI для терминалов без поддержки цвета |
-d | отключить поддержку мыши |
-V | вывести версию программы |
mcedit — это полноценный полноэкранный редактор, позволяющий редактировать файлы размером до 64 Мб, с возможностью редактирования бинарных файлов. Основными возможностями являются: копирование блока, перемещение, удаление, вырезка, вставка; отмена; выпадающие меню; вставка файлов; макро-команды; поиск регулярных выражений и их замена; подсветка синтаксиса; перенос по словам; изменяемая длина табуляции; использование перенаправления потоков для применения, например, проверки орфографии при помощи ispell.
Редактор крайне прост в использовании и может быть использован без предварительного изучения. Выпадающее меню вызывается клавишей F9. Список наиболее часто используемых горячих клавиш приведен ниже (Ctrl и Shift обозначают соответствующие клавиши клавиатуры, Meta — условное обозначение для набора мета-клавиш, на современном компьютере это обычно Alt или Esc):
F3 | Начать выделение текста. Повторное нажатие F3 закончит выделение |
Shift+F3 | Начать выделение блока текста. Повторное нажатие F3 закончит выделение |
F5 | Скопировать выделенный текст |
F6 | Переместить выделенный текст |
F8 | Удалить выделенный текст |
Meta+l | Переход к строке по её номеру |
Meta+q | Вставка литерала (непечатного символа). См. ниже |
Meta+t | Сортировка строк выделенного текста |
Meta+u | Выполнить внешнюю команду и вставить в позицию под курсором её вывод |
Ctrl+f | Занести выделенный фрагмент во внутренний буфер обмена mc (записать во внешний файл) |
Ctrl+k | Удалить часть строки до конца строки |
Ctrl+n | Создать новый файл |
Ctrl+s | Включить или выключить подсветку синтаксиса |
Ctrl+t | Выбрать кодировку текста |
Ctrl+u | Отменить действия |
Ctrl+x | Перейти в конец следующего слова |
Ctrl+y | Удалить строку |
Ctrl+z | Перейти на начало предыдущего слова |
Shift+F5 | Вставка текста из внутреннего буфера обмена mc (прочитать внешний файл) |
Meta+Enter | Диалог перехода к определению функции |
Meta+- | Возврат после перехода к определению функции |
Meta++ | Переход вперед к определению функции |
Meta+n | Включение/отключение отображения номеров строк |
tab | Отодвигает вправо выделенный текст, если выключена опция «Постоянные блоки» |
Meta-tab | Отодвигает влево выделенный текст, если выключена опция «Постоянные блоки» |
Shift+Стрелки | Выделение текста |
Meta+Стрелки | Выделение вертикального блока |
Meta+Shift+- | Переключение режима отображения табуляций и пробелов |
Meta+Shift++ | Переключение режима «Автовыравнивание возвратом каретки» |
Также работают и привычные по Norton и Volkov Commander’ам клавиши:
Ctrl-Ins | копировать |
Shift-Ins | вставить |
Shift-Del | вырезать |
Ctrl-Del | удалить выделенный текст |
Выделение мышью также работает на некоторых терминалах.
Клавиши автозавершения (обычно Alt-Tab или Escape Tab) завершают слово, на котором находится курсор, используя ранее применявшиеся в файле слова.
Для задания макроса нажмите Ctrl-R и нажимайте клавиши, которые нужны для воспроизведения в будущем. Повторное нажатие Ctrl-R завершит запись макроса. Затем нажмите на клавишу, на которую хотите повесить этот макрос. Макрос сохранится, когда нажмете Ctrl-A и затем назначенную макросу клавишу. Макрос выполнится по нажатию Meta, Ctrl, или Esc назначенной клавиши, если клавиша не используется другими функциями.
Дополнительную информацию, как обычно в Linux, можно получить при помощи команды man mc .
Правила оформления ассемблерных программ
При наборе программ на языке ассемблера придерживайтесь следующих правил:
- директивы набирайте большими буквами, инструкции – малыми;
- пишите текст широко;
- не выходите за край экрана – его неудобно будет редактировать и печатать;
- для отступов пользуйтесь табуляцией (клавиша TAB );
- блоки комментариев задавайте с одинаковым отступом.
Оптимальной считается такая строка:
Количество табуляций перед комментарием определяется длиной аргументов команды и может быть от 1 до 3.
По мере знакомства с синтаксисом языка будут приводиться дополнительные правила.
NASM превращает текст программы в объектный код. Имя программы задается в командной строке. В простейшем случае это выглядит так: nasm hello.asm (расширение указывать обязательно). Текст программы из файла hello.asm преобразуется в объектный код, который запишется в файл hello.o . Т. о. имена всех файлов получаются из имени входного файла и расширения по умолчанию.
NASM всегда создает выходные файлы в текущем каталоге.
NASM не запускают без параметров, т. к. он — всего лишь транслятор, а не интегрированная среда разработки.
Рекомендуется в рабочем каталоге все файлы хранить в определенной иерархии. Например, для первой работы создайте каталог ~/labs/asm/01 .
Например, для компиляции приведенного выше текста программы «Hello World» необходимо писать:
nasm -f elf hello.asm
Ключ -f указывает транслятору создавать бинарные файлы в формате ELF (если используется 64-битная версия Linux, следует вместо elf указывать elf64 для генерации 64-битного кода).
Более подробно синтаксис командной строки рассмотрен в следующих работах.
Как видно из схемы на рис. [pic:l1], чтобы получить исполняемую программу, объектный файл необходимо передать на обработку компоновщику (или, как его еще называют, линковщику):
ld -o hello hello.o
Ключ -o с последующим значением задает в данном случае имя создаваемого исполняемого файла.
Формат командной строки LD подробно рассмотрен в следующих работах, также его можно увидеть, набрав ld --help . Для получения более подробной информации см. man ld . Запустить на выполнение созданный исполняемый файл можно, набрав в командной строке:
Примечание: в данном случае исполняемый файл hello выполняется из текущего каталога (что обеспечивают символы «./» перед его именем).
От теоретического материала я уже немного устал. Решил попрактиковаться, а заодно и поэкспериментировать с сегментами. Для экспериментов выбрал программку из книги Феногенова, и чуток изменил ее (текст можно взять тут):
Она ассемблируется и TASM и MASM.
Ассемблеры, в процессе трансляции, позволяют создавать, так называемые, файлы листингов. Файл листинга содержит не только исходный текст, но и транслированный машинный код в шестнадцатеричном формате. Файлы листингов TASM и MASM немного различаются. Листинги TASM содержат еще и номера строк в самое левой колонке, после которой уже идут шестнадцатеричные адреса смещения команд и данных (в MASM колонка адресов первая) относительно начала сегмента . Затем показан ассемблированный машинный код в шестнадцатеричном формате и затем соответствующие ему команды ассемблера.
Итак, к практике! Для каждой программы я создаю отдельную папку и в ней две подпапки MASM и TASM. Чтобы ассемблировать программу я захожу в каждую из них FAR-ом и запускаю соответственно m.bat или t.bat (расширение .bat можно не указывать, просто m или t). И из открывшегося окна уже ассемблирую программу. Ассемблируем hello.asm TASM-ом с ключом получения листинга ( /l ). Вводим последовательно команды: tasm /l hello и затем tlink hello . Обратите внимание что для TASM расширение .asm указывать не обязательно. Смотрим скриншот:
В директории, помимо объектного файла и исполняемого, образовался файл листинга - hello.lst, и файл карты данных - hello.map. Смотрим hello.lst.
Команды имеют различную длину и располагаются в памяти вплотную друг к другу. Так, первая команда mov ax,data, начинается со смещения 0000 сегмента кода и занимает 3 байта. Соответственно вторая команда начинается со смещения 0003. Вторая команда имеет длину 2 байта, поэтому третья команда начинается с байта 0005 и т.д.
Предложения программы с директивами segment , assume , end не транслируются в какие-либо машинные коды (как это уже говорилось) и не находят отражения в памяти . Они нужны лишь для передачи транслятору служебной информации, управляющей ходом трансляции.
Транслятор не смог полностью определить код команды mov ax,data . В этой команде в AX засылается адрес сегмента data. Однако этот адрес станет известен лишь в процессе загрузки исполняемого файла программы в память . Поэтому в листинге на месте этого адреса стоят нули. Символ s указывает на то, что в дальнейшем вместо нулей сюда будет подставлен сегментный адрес.
Еще одна помеченная команда с кодом ВА 0000 располагается в строке 6 листинга. В этой команде в регистр DX заносится смещение поля с именем msg, расположенное в сегменте данных (ключевое слово offset, указанное перед именем поля, говорит о том, что речь идет не о содержимом ячейки msg, а об ее смещении). Поле msg расположено в самом начале сегмента данных, и его смещение от начала сегмента равно 0, что и указано в коде команды. Почему же эта команда помечена буквой r , являющейся сокращением слова relocatable - перемещаемый?
Чтобы ответить на этот вопрос, нам придется рассмотреть, как сегменты программы размещаются в памяти. Как уже говорилось, любой сегмент может располагаться в памяти только с адреса, кратного 16, т.е. на границе 16-байтового блока памяти (параграфа). Конкретный адрес программы в памяти зависит от конфигурации компьютера, - какой размер занимает DOS, сколько загружено резидентных программ и драйверов, а также в каком режиме запускается программа - в отладчике или без него. Предположим, что сегментный адрес сегмента команд оказался равным 1306h (а). В нашей программе сегмент команд имеет размер 11h байт (что указано в строке 10 и в разделе Groups & Segments листинга), т.е. занимает целый параграф плюс один байт. Сегмент данных имеет размер 14h байт (строка 15 листинга и раздел Groups & Segments) и тоже требует для своего размещения немного больше одного параграфа. Из-за того, что сегмент данных должен начаться на границе параграфа, ему будет назначен сегментный адрес 1308h и между сегментами образуется пустой промежуток размером 15 байт . По той же причине между сегментом данных и стека образуется пустой промежуток в 12 байт .
Потеря 27 байт из многомегабайтовой памяти, разумеется, не имеет никакого значения. Однако в некоторых случаях, например, при компоновке единой программы из большого количества модулей с небольшими по размеру подпрограммами, суммарная потеря памяти может оказаться значительной.
Для того, чтобы устранить потери памяти, можно сегмент данных объявить с выравниванием на байт:
Такое объявление даст возможность системе загрузить сегмент данных так, как показано на рис. (б). Сегмент данных частично перекрывает сегмент команд, начинаясь на границе его последнего параграфа (в нашем случае по адресу 1307h). Для того, чтобы данные не наложились на последние команды сегмента команд, они смещаются вниз так, что начинаются сразу же за сегментом команд . В нашем примере, где сегмент команд "выступает" за сегментный адрес 1307h всего на 1 байт, данные и надо сместить на этот 1 байт. В результате поле msg, с которого начинается сегмент данных, и которое в листинге имело смещение 0, получит смещение 1 . Все остальные адреса в сегменте данных также сместятся на один байт вперед. В результате данные будут располагаться в физической памяти вплотную за командами, без всяких промежутков, однако все обращения в сегменте команд к данным должны быть скорректированы на величину перекрытия сегментов, в нашем случае - на 1 байт. Эта коррекция выполняется системой после загрузки программы в память, но еще до ее запуска. Адреса, которые могут потребовать описанной коррекции, и помечаются в листинге трансляции буквой " r ". Из сказанного следует очень важный и несколько неожиданный вывод: коды команд программы в памяти могут не совпадать с кодами, показанными в листинге трансляции. Это обстоятельство необходимо учитывать при отладке программ с помощью интерактивного отладчика, который, естественно, показывает в точности то, что находится в памяти, и что не всегда соответствует листингу трансляции.
Вернемся к рассмотрению листинга трансляции. Данные, введенные нами в программу, также оттранслировались: вместо символов текста в загрузочный файл попадут коды ASCII этих символов.
При выводе этих кодов на экран видеосистема компьютера преобразует их назад в изображения символов, записанных в исходном тексте программы.
Из раздела листинга Groups & Segments можно увидеть размер сегментов программы в байтах в шестнадцатеричном представлении. Например размер сегмента data равен 0014h байтам, что соответствует десятичным 20 байтам (количество символов между апострофами). Под стек мы выделяли 256 байт – 0100h. Размер сегмента команд равен 0011h байт, что в десятичном равно 17 байт.
Размер же всей программы окажется больше суммы длин сегментов , во-первых, из-за пустых промежутков между сегментами (у нас на них уйдет 15 + 12 = 27 байт), и, во-вторых, за счет подсоединения к EXE программе обязательного заголовка , имеющего минимальный размер 512 байт ( он может быть и больше, если программа содержит большое количество настраиваемых элементов ). Итого считаем:
17 байт – сегмент кода
20 байт – сегмент данных
256 байт – сегмент стека
27 байт - "пустые" байты между сегментами
512 байт - заголовок программы
Итого 832 байта
что и видим в выводе команды DIR:
Образ программы в памяти начинается с сегмента префикса программы (Program Segment Prefics, PSP), образуемого и заполняемого системой . PSP всегда имеет размер 256 байт; он содержит таблицы и поля данных, используемые системой в процессе выполнения программы. Вслед за PSP располагаются сегменты программы в том порядке, как они объявлены в программе . Сегментные регистры автоматически инициализируются следующим образом: ES и DS указывают на начало PSP (что дает возможность, сохранив их содержимое, обращаться затем в программе к PSP), CS - на начало сегмента команд, a SS - на начало сегмента стека. В указатель команд IP загружается относительный адрес точки входа в программу ( из операнда директивы end ), а в указатель стека SP - величина, равная объявленному размеру стека, в результате чего указатель стека указывает на конец стека (точнее, на первое слово за его пределами ).
Таким образом, после загрузки программы в память адресуемыми оказываются все сегменты, кроме сегмента данных. Инициализация регистра DS в первых двух строках программы позволяет сделать адресуемым и этот сегмент.
Итак, первой исполняемой командой по адресу 144B мы видим:
144B:0000 B84D14 MOV AX,144D
Посмотрим внимательней на адресок 144D , который заносится в регистр AX. По идее это должен быть адрес сегмента данных, так как в тексте нашей программы этой строке соответствует команда mov ax,data . Код нашей программы (с учетом выравнивания на границу параграфа, 17 байт кода + 15 пустых байт) занимает 32 байта ( 20h байт), то есть два параграфа. Сегмент данных мы определили в нашей программе сразу за сегментом кода, то есть сегмент данных начинается через 20h байт от начала программы. Адрес в CS у нас равен 144B0 (не забываем про нолик). Прибавим 20h к 144B0h и получаем 144D0h. 144B0h+20h=144D0h . В листинге трансляции эта же команда определена как B8 0000s . Вспоминаем, что говорилось про буковку s . Этот адрес подставляется ОС в процессе загрузки программы в память . Кстати стоит еще заметить как выглядит код этой команды уже в памяти при исполнении - B84D14 . Как видим B8 осталась, а адрес записан сюда ОС с изменением очередности байт . Выполняем эту команду и смотрим состояние регистров. Затем выполняем вторую команду и смотрим состояние регистров и дамп сегмента кодов.
Еще стоит обратить внимание на изменение в регистре AX, после команды mov ah,09 . Изменился только старший байт, который принял занчение 09, а младший байт сохранил свое занчение 4D. А так же на то, что в регистр DX было помещено смещение (0000) от начала сегмента данных (в нашем случае выводимой на экран фразы).
Полный дам выполнения программы приведен ниже:
AX=144D BX=0000 CX=0140 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=143B ES=143B SS=144F CS=144B IP=0003 NV UP EI PL NZ NA PO NC
144B:0003 8ED8 MOV DS,AX
-t
AX=144D BX=0000 CX=0140 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=144D ES=143B SS=144F CS=144B IP=0005 NV UP EI PL NZ NA PO NC
144B:0005 B409 MOV AH,09
-d ds:0
144D:0000 2D 3D 2A 20 48 65 6C 6C-6F 20 57 6F 72 6C 64 20 -=* Hello World
144D:0010 2A 3D 2D 24 00 00 00 00-00 00 00 00 00 00 00 00 *=-$.
144D:0020 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144D:0030 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144D:0040 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144D:0050 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144D:0060 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
144D:0070 2A 2A 2A 2A 2A 2A 2A 2A-2A 2A 2A 2A 2A 2A 2A 2A ****************
-t
AX=094D BX=0000 CX=0140 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=144D ES=143B SS=144F CS=144B IP=0007 NV UP EI PL NZ NA PO NC
144B:0007 BA0000 MOV DX,0000
-t
AX=094D BX=0000 CX=0140 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=144D ES=143B SS=144F CS=144B IP=000A NV UP EI PL NZ NA PO NC
144B:000A CD21 INT 21
-g 10d
-=* Hello World *=-
Программа завершилась нормально
-
Просмотр файла листинга
Изучим листинг, сгенерированный для следующего приложения.
1. Листинг только ассемблера (/FA)
Листинг ассемблирования помещается в файл с расширением .asm в промежуточном каталоге. Например, если имя файла - main.cpp, то в промежуточном каталоге появится файл main.asm. Ниже приведен фрагмент кода главной функции из файла листинга:
• Строки, начинающиеся с ; , являются комментариями
• PUBLIC _main означает, что функция _main используется совместно с другими файлами (в отличие от статических функций). У статических функций нет префикса.
• CONST SEGMENT указывает начало сегмента данных CONST. Компилятор VC++ помещает в эту секцию постоянные данные, такие как строки. Видно, что строка "Hello World" помещается в сегмент CONST. Изменение любых данных в сегменте вызывает генерацию исключения нарушения доступа. Подробнее об этом позже.
• _TEXT SEGMENT отмечает начало другого сегмента. Компилятор помещает весь код в этот сегмент.
• _argc$ = 8 и _argv$ = 12 указывают стековые положения аргументов argc и argv. В данном случае это значит, что если прибавить 8 к указателю стека (регистр ESP процессора), то получится адрес параметра argc. Для адреса возврата будет смещение 4.
• _main PROC NEAR указывает на начало функции _main. Заметьте, что у функций C (функций, объявленных с extern "C") в начале имени ставится _, у функции C++ имя декорируется.
• Видно, что компилятор проталкивает адрес строки "Hello World" в стек и вызывает функцию printf. После окончания вызова функции указатель стека увеличивается на 4 (так как printf имеет соглашение о вызовах C).
• EAX – регистр, хранящий возвращаемое значение функции. EAX подвергается операции "исключающее или" сам с собой. (Это быстрый способ привести регистр к нулю.) Причина состоит в том, что содержимое оригинального кода возвращает 0 из функции main.
• Наконец, ret 0 – команда возврата из функции. Числовой аргумент 0, идущий за командой ret, указывает число, на которое надо увеличить указатель стека.
Это был листинг только ассемблера. Посмотрим, как выглядят три остальных листинга.
2. Ассемблер с исходным кодом (/FAs)
Этот листинг дает более ясную картину, чем первый. Он показывает исходный текст вместе с ассемблерным кодом.
3. Ассемблер с машинным кодом (/FAc)
Листинг показывает коды команд вместе с мнемониками команд. Этот листинг обычно генерируется в файле .cod. В данном примере листинг окажется в файле main.cod.
4. Ассемблер, машинный и исходный код (/FAsc)
Этот листинг также генерируется в файле .cod. Как и ожидалось, он показывает исходный код вместе с машинным кодом и ассемблером.
Были рассмотрены все четыре типа листинга, генерируемые компилятором. Обычно нет необходимости смотреть на машинный код. Большей частью ассемблер с исходником (/FAs) – самый полезный листинг.
Посмотрев разные типы листингов и как генерировать листинги, узнаем, какую полезную информацию можно собрать из листинга.
Установка файла листинга
Можно установить параметры компилятора C/C++ для генерации файла листинга в диалоговом окне «Настройки проекта VC6», как показано ниже.
Компилятор генерирует следующие разные типы листинга:
1. Только ассемблерный код (.asm)
2. Ассемблерный код и машинный код. (.cod)
3. Ассемблерный код вместе с исходным кодом. (.asm)
4. Ассемблерный код вместе с машинным и исходным кодом. (.cod)
Читайте также: