Файл: 2005 Рудольф Марек. Ассемблер на примерах. Базовый курс. .pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 26.10.2023

Просмотров: 144

Скачиваний: 12

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
177
Ассемблер на примерах. Базовый курс Мы должны получить 95, но никак на 945. Что произошло Ответить на этот вопрос нам поможет отладчик. Запустим grdb: С : > g r d b
GRDB version 3.6 Copyright (c) LADsoft 1997-2002
-> Введем команду test.com 45 5 0' для загрузки программы с параметрами
->1 test.com 45 50
Size: 000000E1
-> Предположим, что наши подпрограммы работают правильно, поэтому будем использовать команду р для прохождения подпрограмм за один шаг. Посмотрим, что возвращает подпрограмма ASCIIToNum в регистр ЕАХ. Первый вызов ASCIIToNum следует сразу после инструкции MOV есх,10. Рассмотрим весь сеанс отладки до первого вызова ASCIIToNum.
- > 1 t e s t . c o m 4 5 5 0
Size: 000000E1 р еах:00000000 ebx:00000000 есх:000000Е1 edx:00000000 esi:00000081 edi:00000000 ebp:00000000 esp:0000FFEE eip:00000103 eflags:00000202
NV UP EI PL NZ NA PO NC ds: 10FB es:10FB fs:10FB gs:10FB SS:10FB cs:10FB
10FB:0103 E8 44 00 call 014A
->P eax:00000000 ebx:00000000 ecx:000000El edx:00000000 esi:00000081 edi:00000084 ebp:00000000 esp:0000FFEE eip:00000106 eflags:00000287
NV UP EI MI NZ NA PE CY ds: 10FB es:10FB fs:10FB gs:10FB ss:10FB cs:10FB
10FB:0106 C6 05 00 mov byte [di],0000 ds:[0084]=20
->p eax:00000000 ebx:00000000 ecx:000000El edx:00000000 e s i : 0 0 0 0 0 0 8 1 edi:00000084 ebp:00000000 esp:0000FFEE eip:00000109 eflags:00000287
NV UP EI MI NZ NA PE CY ds: 10FB es:10FB fs:10FB gs:10FB ss:10FB cs:10FB
10FB:0109 66 B9 0A 00 00 00 mov ecx,0000000A
->p eax:00000000 ebx:00000000 ecx:0000000A edx:00000000 esi:00000081 edi:00000084 ebp:00000000 esp:0000FFEE eip:0000010F eflags:00000287
NV UP E I MI NZ NA PE CY ds: 10FB es:10FB fs:10FB gs:10FB ss:10FB CS:10FB
10FB:010F E8 6D 00 call 017F
->P eax:000003Bl ebx:00000000 ecx:0000000A edx:00000000 esi:00000081 edi:00000084 ebp:00000000 esp:0000FFEE eip:00000112 eflags:00000297 178
Глава 10. Программирование в DOS
NV UP E I MI NZ AC PE CY ds: 10FB es:10FB fs:10FB gs:10FB SS:10FB cs:10FB
10FB:0112 66 89 C2 mov edx,eax
-> После первого преобразования получим результат 0хЗВ1, который никак не соответствует десятичному 45. 0хЗВ1 — это 945. Почему мы получили именно этот результат Если мы предположили, что подпрограмма корректна, то тогда нужно проверить параметры, которые мы передали подпрограмме. Посмотрим, что находится в памяти по адресу, который содержится в SI:
->d s i
10FB;0080 20 34 35-00 35 30 0D-01 01 01 01-01 01 01 01 45.50 10FB:0090 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00A0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00B0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:OOCO 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00D0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00E0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00F0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:0100 BE 81 00 E8-44 00 C6 05-00 66 B9 0A-00 00 00 E8 ....D....f
-> Наша функция преобразования не проверяет входящие символы, поэтому у нас и вышла ошибочка. Обратите внимание перед первым символом у нас образовался пробел (код 20). Лечится это очень просто перед вызовом подпрограммы преобразования нужно вызвать SkipSpace, чтобы удалить все символы вначале строки. Отредактируем нашу программу и снова откомпилируем ее. Запускаем с теми же самими параметрами
C : \ t e s t 3 4 5 5 0 5 Опять мы получили не то, что нужно. Снова запускаем отладчики переходим к первому вызову ASCIIToNum: Р еах:00000034 ebx:00000000 есх:0000000A edx:00000000 esi:00000083 edi:00000084 ebp:00000000 esp:0000FFEE eip:00000112 eflags:00000287
NV UP EI MI NZ NA PE CY ds: 10FB es:10FB fs:10FB gs:10FB ss:10FB cs:10FB
10FB:0112 E8 6D 00 call 0182
->d si
10FB:0080 35-00 35 30 0D-01 01 01 01-01 01 01 01 5.50 10FB:0090 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00A0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00B0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00C0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00D0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 179
Ассемблер на примерах. Базовый курс
10FB:00E0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:00F0 01 01 01 01-01 01 01 01-01 01 01 01-01 01 01 01 10FB:0100 BE 81 00 E8-41 00 E8 44-00 C6 05 00-66 B9 0A 00 . .. . A . . D . . . . f . . .
-> Получается, что виновата подпрограмма SkipSpace, которая пропускает один лишний символ, поэтому в качестве аргументов мы получаем 5 и 0 — отсюда и результат 5 + 0 = 5. Команда LODSB увеличивает указатель SI в любом случае, даже тогда, когда встреченный символ — не пробел. Значит, перед выходом из подпрограммы этот указатель нужно уменьшить и проблема исчезнет. Перепишем подпрограмму так
SkipSpace:
.again: lodsb загружаем в AL байт по адресу DS:SI, увеличиваем SI cmp al,' ' сравниваем символ с пробелом jz again если равно, продолжаем dec si ret Все, «баг» исправлен, сохраним полученную программу, откомпилируем и запустим ее
C:\test4 45 50 95 Полученное значение правильно. Поздравляю Процесс разработки программы завершен ю. Резидентные программы Обычно после завершения очередной программы DOS освобождает память, которую эта программа занимала, и загружает на ее место новую. Но некоторые программы, завершившись, продолжают оставаться в памяти. Такие программы называются резидентными. В большинстве случаев резидентные программы используются для обработки прерываний процессора. Типичный пример резидентной программы — драйвер мыши, который опрашивает последовательный порт компьютера, читая коды, передаваемые мышью. Этот драйвер использует программное прерывание 0x33, через которое он передает свои события (перемещение мышки, нажатие той или иной клавиши мышки) прикладным программам. На резидентные программы накладывается ряд ограничений. Таким нельзя самим вызывать программные прерывания. Дело в том, что DOS — принципиально однозадачная система, поэтому функции прерываний DOS не обладают свойством реентерабельности (повторной входимости). Если программа, об
Глава 10. Программирование в DOS рабатывающая системный вызов DOS, сама выполнит системный вызов, то ОС рухнет. Существуют, правда, способы обойти это ограничение, но они слишком сложны для того, чтобы рассматривать их в этой книге. Напишем небольшую резидентную программу, которая активизируется при нажатии клавиши Scroll Lock. Программа изменяет цвет всех символов, отображенных на экране, на красный. До сих пор мы старались избегать прямого взаимодействия с периферийными устройствами и использовали функции операционной системы. В случае с резидентной программой использовать DOS мы больше не можем, поэтому ничего другого нам не остается — мы должны взаимодействовать с устройствами напрямую. Примите пока как данность, что из порта 0x60 можно получить так называемый скан-код нажатой клавиши, то есть не код посылаемого ею символа, а просто число, указывающее ее положение на клавиатуре. Скан-код клавиши
Scroll Lock равен 0x46. Каждое нажатие клавиши генерирует аппаратное прерывание IRQ1, которое принимает скан-код от клавиатуры, преобразовывает его в код и ставит в очередь непрочитанных клавиш. Эта очередь доступна операционной системе и прикладным программам через вызовы BIOS. Чтобы активизировать программу, мы должны перехватить прерывание IRQ 1
(int 0x9), то есть сделать нашу программу его обработчиком. Затем мы будем в цикле читать скан-код из порта 0x60. Если нажатая клавиша — не Scroll
Lock, то мы вернем ее скан-код первоначальному обработчику прерывания. В противном случае мы выполним намеченное действие (сменим цвет символов) и вернем управление первоначальному обработчику прерывания. Выход из прерывания осуществляется по команде iret. Вовремя обработки прерывания мы должны сохранить контекст выполняемой программы, то есть сохранить все регистры внутри нашей подпрограммы. Если этого не сделать, последствия предугадать будет сложно, нов большинстве случаев крах выполняемой программы будет обеспечен. Первым делом напишем подпрограмму color, изменяющую цвет всех символов на красный. Организация видеопамяти была рассмотрена (хоть и очень кратко) вначале этой главы. Символы, выведенные на экран, хранятся начиная с адреса сегмента 0хВ800 в формате символщвет. Наша задача — модифицировать значения байтов, находящихся по нечетным адресам 0хВ800:0х0001,
ОхВ800:ОхОООЗ, 0хВ800:0х0005 и т.д. — это и есть цвет символов. Не вдаваясь в подробности, скажем, что красному цвету соответствует значение 0x04. Подпрограмма заполняет 80x25 байтов памяти, что соответствует стандартному разрешению текстового режима.
181
Ассемблер на примерах. Базовый курс color: push ax push ex push si push es xor si,si mov ax, 0xB800 mov es,ax mov ex,80*25
.repeat: inc si mov byte [es:si],0x4 inc si dec ex jnz .repeat pop es pop si pop ex pop ax ret Мы можем протестировать нашу подпрограмму. Допишем к ней заголовок секции кода и завершим обычную, нерезидентную, программу системным вызовом 0х4С:
SECTION . t e x t c a l l c o l o r mov ax, 0x4c00 i n t 0x21 c o l o r : сохраняем в стеке значения
; регистров, которые будем использовать сбрасываем SI загружаем адрес сегмента в АХ ив сегментный регистр количество повторений увеличиваем на 1 записываем цвет 0x04 — красный увеличиваем на 1 уменьшаем СХ на 1 переходим к . r e p e a t , пока СХ > 0 восстанавливаем все регистры Теперь напишем обработчик прерывания IRQ 1:
n e w _ h a n d l e r : p u s h ax i n a l , 0x60 emp a l , 0x46 j n z p a s s _ o n c a l l c o l o r p a s s _ o n : pop ax jmp f a r [ c s : o l d _ v e c t o r ] сохраняем значение АХ читаем скан-код клавиши сравниваем с 0x46 (Scroll-Lock) если нет, переходим к pass_on вызываем подпрограмму восстанавливаем значение АХ передаем управление первоначальному обработчику Переменная o l d _ v e c t o r должна содержать адрес первоначального обработчика прерывания (старое значение вектора прерывания вектора. В качестве
182
Глава 10. Программирование в DOS сегментного регистра мы используем CS, потому что другие сегментные регистры входе обработки прерывания могут получить произвольные значения. Сохраним значение старого вектора прерывания в переменную old_vector, а указатель на наш обработчик поместим вместо него в таблицу прерываний. Назовем эту процедуру setup. setup: cli xor ax,ax mov es,ax mov ax,new_handler xchg ax,[es:0x9*4] mov [ds :old__vector] ,ax mov ax,cs xchg ax,[es:0x9*4+2] mov [ds:old_vector+2],ax sti ret отключаем прерывания сбрасываем АХ таблица прерываний находится в сегменте Ос метки new_handler начинается новый обработчик прерывания вычисляем адрес старого вектора и меняем местами со значением АХ. Теперь в таблице смещение нового обработчика, в АХ — старого сохраняем старый адрес в переменной old_vector адрес текущего сегмента — CS пишем в таблицу сегмент нового обработчика, в АХ загружаем сегмент старого сохраняем сегмент на 2 байта дальше old_vector включаем прерывания выходим из подпрограммы Теперь мы попросим операционную систему не разрушать программу после ее завершения, а хранить ее в памяти. Для этого используется системный вызов 0x31. Ввод Сохраняем программу резидентной АН = 0x31
AL = выходной код
DX = число параграфов памяти, необходимых для хранения резидентной программы Вывод Ничего Код всей нашей резидентной программы r e s i d e n t . asm приведен в листинге
10.10. Листинг 10.10. Пример резидентной программы
SECTION .text org 0x100 jmp initialize
183
Ассемблер на примерах. Базовый курс new_handler: push ax in al, 0x60 cmp al, 0x46 jnz pass_on call color pass_on: pop ax jmp far [cs:old_vector] color: push ax push ex push si push es xor mov mov mov si, si ax, 0xB800 es ,ax ex,80*25
.repeat: inc mov inc dec jnz pop pop pop pop ret old_ si byte [es : si] ,! si ex
.repeat es si ex ax
.vector dd 0 initialize: call setup mov mov shr inc int ax,0x3100 dx,initialize dx,4 dx
0x21 setup: cli
,0x4 сохраняем значение АХ читаем скан-код клавиши сравниваем с 0x4 6 (Scroll-Lock) если нет, переходим к pass_on вызываем подпрограмму восстанавливаем значение АХ передаем управление первоначальному обработчику сохраняем в стеке значения регистров, которые будем использовать сбрасываем SI загружаем адрес сегмента в АХ ив сегментный регистр количество повторений увеличиваем SI на 1 записываем цвет 0x04 — красный увеличиваем на 1 уменьшаем СХ на 1 переходим к .repeat, пока СХ > 0 восстанавливаем все регистры вызываем регистрирацию своего обработчика функция DOS: делаем программу резидентной вычисляем число параграфов в памяти нужно разместить весь код вплоть до метки initialize делим на 16 добавляем 1 завершаем программу и остаемся резидентом отключаем прерывания
184
Глава 10. Программирование в DOS хог ахах mov es,ax mov ax,new_handler xchg ax,[es:0x9*4] mov [ds:old_vector],ax mov ax,cs xchg ax,[es:0x9*4+2] mov [ds:old_vector+2],ax sti ret сбрасываем АХ таблица прерываний находится в сегменте Ос метки new_handler начинается новый обработчик прерывания вычисляем адрес старого вектора и меняем местами со значением АХ. Теперь в таблице смещение нового обработчика, в АХ — старого сохраняем старый адрес в переменной old_vector адрес текущего сегмента — CS пишем в таблицу сегмент нового обработчика, в АХ загружаем сегмент старого сохраняем сегмент на 2 байта дальше old_vector включаем прерывания выходим из подпрограммы Теперь откомпилируем программу nasm -f bin -о resident.com resident.asm. После ее запуска мы ничего не увидим, но после нажатия клавиши Scroll Lock весь текст на экране покраснеет. Избавиться от этого резидента (выгрузить программу из памяти) можно только перезагрузкой компьютера или закрытием окна эмуляции DOS, если программа запущена из-под Windows.
10.13. Свободные источники информации Дополнительную информацию можно найти на сайтах
• www.ctyme.com/rbrown.htm — версия списка прерываний Ральфа
Брауна (Ralf Brown's Interrupt List);
• http://programmistu.narod.ru/asm/lib_l/index.htm — Ассемблер и программирование для IBM PC» Питер Абель.
185
Программирование в Windows Родные приложения Программная совместимость Запуск приложений од Windows Свободные источники информации Ассемблер на примерах. Базовый курс

11.1. Введение
Когда-то Microsoft Windows была всего лишь графической оболочкой для операционной системы DOS. Но со временем она доросла до самостоятельной полнофункциональной операционной системы, которая использует защищенный режим процессора. В отличие от подобных операционных систем
(Linux, BSD и др, в Windows функции графического интерфейса пользователя (GUI) встроены непосредственно в ядро операционной системы.
11.2. Родные приложения Родные приложения взаимодействуют с ядром операционной системы посредством так называемых вызовов. Через API (Application
Programming Interface) операционная система предоставляет все услуги, в том числе управление графическим интерфейсом пользователя. Графические элементы GUI имеют объектную структуру, и функции API часто требуют аргументов в виде довольно сложных структур со множеством параметров. Поэтому исходный текст простейшей ассемблерной программы, управляющей окном и парой кнопок, займет несколько страниц. В этой главе мы напишем простенькую программу, которая отображает окошко со знакомым текстом «Hello, World!» и кнопкой ОК. После нажатия
ОК окно будет закрыто.
11.2.1. Системные вызовы API В операционной системы DOS мы вызывали ядро с помощью прерывания
0x21. В Windows вместо этого нам нужно использовать одну из функций API. Функции API находятся в различных динамических библиотеках (DLL). Мы должны знать не только имя функции и ее параметры, но также и имя библиотеки, которая содержит эту функцию user32.dll, kernel32.dll и т.д. Описание
API можно найти, например, в справочной системе Borland Delphi (файл win32.hlp). Если у вас нет Delphi, вы можете скачать только файл win32.zip это архив, содержащий файл win32.hlp): ftp://ftp.borland.com/pub/delphi/1echpubs/delphi2/win32.zip
187
Ассемблер на примерах. Базовый курс
11.2.2. Программа «Hello, World!» с кнопкой под Windows Наша программа должна отобразить диалоговое окно и завершить работу. Для отображения диалогового окна используется функция MessageBoxA, а для завершения программы можно использовать функцию ExitProcess. В документации понаписано дескриптор окна владельца
LPCTSTR lpText, // адрес текста окна сообщения
LPCTSTR lpCaption, // адрес текста заголовка окна сообщения
UINT uType // стиль окна
); Первый аргумент — это дескриптор окна владельца, то есть родительского окна. Поскольку у нас нет никакого родительского окна, мы укажем значение
0. Второй аргумент — это указатель на текст, который должен быть отображен в диалоговом окне. Обычно это строка, заканчивающаяся нулевым байтом. Третий аргумент аналогичен второму, только он определяет не текст окна, а текст заголовка. Последний аргумент — это тип (стиль) диалогового окна, мы будем использовать символическую константу МВ_ОК. Второй вызов API — это ExitProcess, ему нужно передать всего один аргумент как в DOS), который определяет код завершения программы. Чтобы упростить разработку программы на языке ассемблера, подключим файл win32.inc, который определяет все типы аргументов функций (например, HWND и LPCTSTR соответствуют простому типу dword) и значения констант. Подключим этот файл
%include «win32n.inc»; Мы будем использовать функции, которые находятся в динамических библиотеках, поэтому нам нужно воспользоваться директивами EXTERN и
IMPORT:
EXTERN MessageBoxA /MessageBoxA определен вне программы
IMPORT MessageBoxA u s e r 3 2 . d l l а именно — в u s e r 3 2 . d l l
EXTERN E x i t P r o c e s s / E x i t P r o c e s s определен вне программы
IMPORT E x i t P r o c e s s k e r n e l 3 2 . d l l а именно - в k e r n e l 3 2 . d l l Точно также, как в DOS, нужно создать две секции кода и данных.
SECTION CODE USE32 CLASS=CODE ; наш код
SECTION DATA USE32 CLASS=DATA наши статические данные Осталось понять, как можно вызвать функции API. Подробнее мы обсудим это в главе 13, посвященной компоновке программ, написанных частью на ассемблере, а частью на языках высокого уровня, а сейчас рассмотрим только правила передачи аргументов функциям API.
188
Глава 11. Программирование в Windows Соглашение о передаче аргументов называется STDCALL. Аргументы передаются через стек в порядке справа налево (также, как в языке С, а очистка стека входит в обязанности вызванной функции (как в Паскале. Мы помещаем аргументы в стек по команде PUSH, а затем вызываем нужную функцию по команде CALL. He нужно беспокоиться об очистке стека. Код нашей программы приведен в листинге 11.1. Листинг 11.1. Программа «Hello, World!» с кнопкой под Windows
%include «win32n.inc» подключаем заголовочный файл
EXTERN MessageBoxA ;MessageBoxA определен вне программы
IMPORT MessageBoxA user32.dll а именно - в user32.dll
EXTERN ExitProcess ;ExitProcess определен вне программы
IMPORT ExitProcess kernel32.dll а именно — в kernel32.dll
SECTION CODE USE32 CLASS=CODE начало кода метка для компоновщика, указывающая точку входа помещаем в стек последний аргумент. тип окна с единственной кнопкой ОК теперь помещаем в стек адрес
;нуль-завершенной строки заголовка адрес нуль-завершенной строки, которая будет отображена в окне указатель на родительское окно — нулевой нет такого окна вызываем функцию API. Она выведет окно сообщения и вернет управление после нажатия ОК аргумент ExitProcess — код возврата завершаем процесс
..start: push UINT MB_OK push LPCTSTR title push LPCTSTR banner push HWND NULL call [MessageBoxA] push UINT NULL call [ExitProcess]
SECTION DATA USE32 CLASS=DATA banner db 'Hello world!',OxD,OxA,0 t i t l e d b ' H e l l o ' , 0 строка сообщения с символом EOL строка заголовка Для того, чтобы откомпилировать эту программу, нам нужна версия NASM для
Windows, которую можно скачать по адресу http://nasm.sourceforge.net. NASM создаст объектный файл, который нужно будет скомпоновать в исполняемый файл. Для компоновки мы используем свободно распространяемый компоновщик alink, который можно скачать по адресу http://alink.sourceforge.net. Назовем наш файл msgbox.asm. Теперь запустим nasmw с параметром -fobj: С -fobj msgbox.asm
189
Ассемблер на примерах. Базовый курс В результате получим файл msgbox.obj, который нужно передать компоновщику alink:
C:\WIN32>ALINK -oPE msgbox Параметры -о определяет тип исполняемого файла. Родным для Windows является тип РЕ. После компоновки появится файл msgbox.exe, который можно будет запустить.
1   ...   12   13   14   15   16   17   18   19   20

11.3. Программная совместимость Для процессора, работающего в защищенном режиме, доступен другой специальный режим — VM86, обеспечивающий эмуляцию реального режима. Все привилегированные команды (то есть cli, popf и др, а также все обращения к периферийным устройствам (команды in и out) перехватываются и эмулиру­
ются ядром операционной системы так, что прикладной программе кажется, что она действительно управляет компьютером. На самом же деле все вызовы функций DOS и BIOS обрабатываются ядром операционной системы.
11.4. Запуск приложений под Windows Для запуска приложения в среде Windows мы используем так называемый Сеанс MS DOS». Для запуска режима выполните команду cmd Пуск -> Выполнить -* cmd). Открывшееся окно работает в режиме VM86 и полностью эмулирует функции DOS. Если файлы компилятора NASM находятся в каталоге C:\NASM, мы можем использовать команду DOS, чтобы перейти в этот каталог cd С Помните, что в DOS существует ограничение на длину файлов — все имена файлов должны быть в формате 8+3 (8 символов — имя, 3 — расширение. Для редактирования исходных файлов используйте редактор, который показывает номера строк — так будет проще найти ошибку. Избегайте также слишком дружественных приложений, которые добавляют расширение
«.txt» после расширения «.asm».
11.5. Свободные источники информации Мы рекомендуем следующие источники, посвященные программированию на языке ассемблера в Windows: http://win32asm.cjb.net http://rsl.szif.hu/

tomcat/win32 http://asm.shadrinsk.net/toolbar.html
190
Программирование в Linux Структура памяти процесса Передача параметров командной строки и переменных окружения Вызов операционной системы Коды ошибок Облегчим себе работу утилиты
Asmutils. Макросы Asmutils Отладка. Отладчик ALD Ассемблер GAS Ключи командной строки компилятора Ассемблер на примерах, Базовый курс

12.1. Введение
Linux — современная многозадачная операционная система. Большая часть ядра Linux написана на С, но небольшая его часть (аппаратно-зависимая) написана на языке ассемблера. Благодаря портируемости языка С Linux быстро распространилась за пределы х86-процессоров. Ведь для переноса ядра на другую аппаратную платформу разработчикам пришлось переписать только ту самую маленькую часть, которая написана на ассемблере. Как любая другая современная многозадачная система, Linux строго разделяет индивидуальные процессы. Это означает, что ни один процесс не может изменить ни другой процесс, ни тем более ядро, вследствие чего сбой одного приложения не отразится ни на других приложениях, ни на операционной системе. В х86-совместимых компьютерах процессы в памяти защищены так называемым защищенным режимом процессора. Этот режим позволяет контролировать действия программы доступ программы к памяти и периферийным устройствам ограничен правами доступа. Механизмы защиты разделены между ядром операционной системы (которому разрешается делать абсолютно все) и процессами (им можно выполнять только непривилегированные команды и записывать данные только в свою область памяти. Защищенный режим также поддерживает виртуальную память. Ядро операционной системы предоставляет все операции для работы с виртуальной памятью. Кроме, конечно, трансляции логических адресов в физические — эта функция выполняется железом (см. главу 8). Благодаря виртуальной памяти (и, конечно же, 32-битным регистрам, любой процесс в Linux может адресовать 4 Гб адресного пространства. Именно 4 Гб и выделены каждому процессу. Что имеется ввиду Если сделать дамп памяти от 0 до самого конца (4 Гб), вы не обнаружите ни кода другого процесса, ни данных другого процесса, ни кода ядра — 4 Гб полностью предоставлены в ваше распоряжение. Ясно, что реальный объем виртуальной памяти будет зависеть от физической памяти и от объема раздела подкачки, но суть в том, что процессы скрыты друг от друга и друг другу не мешают.
192

Глава 12. Программирование в Linux
12.2. Структура памяти процесса Мы уже знаем, что нам доступны все 4 Гб и ни один процесс не может вторгнуться в наше адресное пространство. Как же распределена память нашего процесса Как было сказано в предыдущих главах, программа состоит из четырех секций секция кода, секция статических данных, секция динамических данных (куча) истек. Порядок, в котором загружаются эти секции в память, определяется форматом исполняемого файла. Linux поддерживает несколько форматов, но самым популярным является формат ELF (Executable and
Linkable Format). Рассмотрим структуру адресного пространства файла. Предположим для простоты, что программа не использует динамических библиотек. Адрес
0x08048000
.text
.data
.bss
.stack Исполняемый код Статические данные (известны вовремя компиляции) Динамические данные (так называемая куча) Свободная память Стек
OxBFFFFFFF (3 Гб) Обычно программа загружается с адреса 0x08048000 (примерно 128 Мб). При загрузке программы загружается только одна ее страница. Остальные страницы загружаются по мере необходимости (неиспользуемые части программы никогда физически не хранятся в памяти. На диске программа хранится без секций .bss и .stack — эти секции появляются только тогда, когда программа загружается в память. Если программа подключает какие-нибудь динамические библиотеки, их модули загружаются в ее адресное пространство, но начинаются с другого адреса (обычно с 1 Гб и выше. Секции этих модулей аналогичны секциям обычной программы (то есть .text, .data, .bss). А что хранится в трехгигабайтном зазоре между .bss и .stack, то есть в свободной памяти Эта память принадлежит процессу, но она не распределена по страницам. Запись в эту область вызовет сбой страницы (page fault) — после этого ядро уничтожит вашу программу. Для создания динамических переменных вам нужно попросить ядро распределить соответствующую память, но об это мы поговорим чуть позже.
193
Ассемблер на примерах. Базовый курс
12.3. Передача параметров командной строки и переменных окружения Если процессы полностью изолированы друг от друга, то как нам получить параметры командной строки и переменные окружения Ведь они — собственность другого процесса — оболочки, запустившей нашу программу. Решили эту проблему очень просто при запуске программы параметры командной строки и переменные окружения помещаются в стек программы. А поскольку стек нашей программы — это наша собственность, мы можем без проблем получить к нему доступ. Рассмотрим структуру стека
ESP после запуска программы
argc argv[0] argv[1] argv[argc-1]
NULL env[0] env[1] erw[n]
NULL Свободная память Количество параметров (dword) Указатель на имя программы Указатели на аргументы программы Конец аргументов командной строки Указатели на переменные окружения Конец переменных окружения Нужный аргумент легко вытолкнуть из стека по команде POP, а потом записать в какую-нибудь переменную. Первое значение, которое мы получим из стека — это количество аргументов командной строки (argc), второй — указатель на имя нашей программы. Если argc > 1, значит, программа была запущена с аргументами и дальше в стеке находятся они. Примеры обработки аргументов командной строки будут рассмотрены после того, как вы научитесь выводить данные на экран.
12.4. Вызов операционной системы Вызвать функции операционной системы DOS можно было с помощью прерывания 0x21. В Linux используется похожий метод прерывание с номером
0x80. Но как мы можем с помощью прерывания добраться до ядра, если оно находится за пределами нашего адресного пространства Благодаря защищенному режиму, при вызове прерывания 0x80 изменяется контекст программы значение сегментного регистра) и процессор начинает выполнять код ядра. После завершения обработки прерывания будет продолжено выполнение нашей программы.
194

Глава 12. Программирование в Linux Подобно DOS, аргументы отдельных системных вызовов (syscalls) передаются через регистры процессора, что обеспечивает небольшой выигрыш вскорости. Номер системного вызова помещается в регистр ЕАХ. Если системный вызов принимает аргументы, то он ожидает их в регистрах ЕВХ, ЕСХ и т.д. По соглашению для передачи аргументов служит набор регистров в фиксированном порядке ЕВХ, ЕСХ, EDX, ESI и EDI. Ядро версии 2.4.x и выше допускает также передачу аргумента через регистр ЕВР.
12.5. КОДЫ ОШИбок Возвращаемое системным вызовом значение заносится в ЕАХ. Это код ошибки, определяющий состояние завершения системного вызова, то есть уточняющий причину ошибки. Код ошибки всегда отрицательный. В программе на языке ассемблера, как правило, достаточно отличать успешное завершение от ошибочного, нов ходе отладки значение кода ошибки может помочь выяснить ее причину. Справочная система Linux содержит страницы, описывающие каждый системный вызов вместе с кодами, значениями и причинами всех ошибок, которые он может вернуть.
12.6. M a n - страницы В отличие от DOS и Windows ОС Linux полностью документирована. В ее справочной системе (которая называется Manual Pages — Страницы Руководства) вы найдете исчерпывающие сведения не только обо всех установленных программах, но и обо всех системных вызовах Linux. Конечно, для эффективного использования страниц (как и остальной документации) вам придется выучить английский, поскольку далеко не все страницы переведены на русский язык. Достаточно уметь читать руководства хотя бы со словарем. Предположим, что мы хотим написать простейшую программку, которая завершает свою работу сразу после запуска. В DOS нам нужно было использовать системный вызов АН=0х4С. Помните Теперь давайте напишем подобную программку под Linux. Для этого найдите файл unistd.h, который обычно находится в каталоге /usr/snVlinux/include/asm:
#ifndef _ASM_I3 8 6_UNISTD_H_
#define _ASM_I3 86_UNISTD_H_
/*
* This f i l e contains the system c a l l numbers.
*/
#define NR_exit 1
#define NR_fork 2 195
Ассемблер на примерах. Базовый курс
#define NR_read 3
#define NR_write 4
#define NR_open 5
#define NR_close 6
#define _syscal11(type,name,typel,argl) \ type name(typel a r g l ) \
{ \
long r e s ; \ asm v o l a t i l e («int $0x80» \
: «=a» ( res) \
: «0» ( NR_##name), «b» ( ( l o n g ) ( a r g l ) ) ) ; \ s y s c a l l _ r e t u r n ( t y p e , r e s ) ; \
} В этом файле вы найдете номера системных вызовов Linux. Нас интересует вызов NR_exit:
#define NR_exit 1 Это значит, что функция ядра, завершающая процесс, имеет номер 1. Описания функций ядра (системных вызовов) собраны в секции 2 справочного руководства. Посмотрим, что там сказано о функции exit(). Для этого введите команду
man 2 e x i t Вы увидите содержимое страницы
_EXIT(2) Linux Programmer's Manual , _EXIT(2)
NAME
_exit, _Exit — terminate the current process
SYNOPSIS
#include void _exit(int status);
#include void _Exit(int status);
DESCRIPTION
The function _exit terminates the calling process «immedi­
ately». Any open file descriptors belonging to the process are closed; any children of the process are inherited by process 1, init, and the process's parent is sent a SIGCHLD signal. The value status is returned to the parent process as the process's exit status, and can be col­
lected using one of the wait family of calls. The function
_Exit is equivalent to _exit.
RETURN VALUE
These functions do not return.
196

Глава 12. Программирование в Linux Системному вызову 'exit' нужно передать всего один параметр (как в DOS) — код возврата (ошибки) программы. Код 0 соответствует нормальному завершению программы. Код нашего терминатора будет выглядеть так mov еах,1 номер системного вызова — exit mov ebx,0 код возврата 0 int 0x80 вызов ядра и завершение текущей программы
12.7. Программа «Hello, World!» под Linux Давайте немного усложним нашу программу — пусть она выведет заветную фразу и завершит работу. Вспомните, что в главе 8 говорилось о файловых дескрипторах стандартных потоков — ввода (STDIN, обычно клавиатура, вывода (STDOUT, обычно экран) и ошибок (STDERR, обычно экран. Наша программа «Hello,
World!» должна вывести текст на устройство стандартного вывода STDOUT и завершиться. Сточки зрения операционной системы STDOUT — это файл, следовательно, мы должны записать нашу строку в файл. Снова заглянув в unistd.h, находим системный вызов write(). Смотрим его описание man 2 w r i t e
WRITE(2) Linux Programmer's Manual WRITE(2)
NAME write — write to a file descriptor
SYNOPSIS
#include ssize_t write(int fd, const void *buf, size_t count); Если вы не знаете Сто данная страница, скорее всего, для вас будет непонятна. Как передать аргументы системному вызову В языке ассемблера вы можете загрузить в регистр либо непосредственное значение, либо значение какого-то адреса памяти. Для простоты будем считать, что для аргументов, не отмеченных звездочкой, нужно указывать непосредственные значения, а аргументы со звездочкой требуют указателя. Функции write
нужно передать три аргумента дескриптор файла, указатель на строку, данные из которой будут записаны в файл (это buf), и количество байтов этой строки, которые нужно записать. Функция возвратит количество успешно записанных байтов или отрицательное число, которое будет кодом ошибки. Для компилирования нашей программы в объектный файл будем использовать
nasm, а для компоновки — компоновщик Id, который есть в любой версии
197
Ассемблер на примерах. Базовый курс
Linux. Для получения объектного файла в формате ELF нужно использовать опцию компилятора -f elf. Компоновщик Id обычно вызывается с ключами, перечисленными в табл. 12.1. Наиболее часто используемые ключи компоновщика Id Таблица 12.1 Ключ
-o
-s Назначение Установить имя выходного (исполняемого) файла Удалить символьную информацию из файла Чтобы компоновщик Id определил точку входа программы, мы должны использовать глобальную метку _start. Код нашей программы «Hello, World!» приведен в листинге 12.1. Листинг 12.1. Программа «Hello, World!» под Linux
SECTION .text global _start
_start: mov eax,4 mov ebx,1 mov ecx,hello mov edx,len указываем компоновщику точку входа. это сделать необходимо первый аргумент — номер системного вызова — write константа STDOUT определена как 1 адрес выводимой строки длина «Hello, World!» вместе с символом конца строки вызываем ядро для вывода строки системный вызов номер 1 — exit код завершения программы О вызываем ядро для завершения программы наша строка плюс символ конца строки вычисляем длину строки int 0x80 mov eax,1 mov ebx,0 int 0x80
SECTION .data hello db «Hello, world!»,Oxa len equ $ - h e l l o Теперь откомпилируем программу nasm -f elf hello.asm А теперь запустим компоновщик
Id
-S -о hello о Ключ -о определяет имя результирующего исполняемого файла. Ключ -s удаляет символьную информацию из исполняемого файла, чтобы уменьшить его размер.
198