Что такое код int86x

Содержание

Что такое код int86x

Описание.
Функция int86x выполняет прерывание 8086 программного обеспечения, которое определено номером прерывания intno . В отличие от функции int86 , функция int86x принимает значения регистра сегмента в segregs , позволяя программам, использующим сегменты данных большой модели или far -указатели, определять, какой сегмент или указатель должен быть использован в процессе системного вызова. Перед выполнением определенного прерывания int86x копирует содержимое inregs и segregs в соответствующие регистры. В segregs используются только значения регистров DS и ES. После возврата прерываний функция копирует значения текущего регистра в outregs и перезаписывает DS . Она также копирует состояние (статус) переносимого флага системы в поле cflag , содержащееся в outregs . Аргументы inregs и outregs являются объединениями типа REGS . Аргумент segregs является структурой типа SREGS . Эти типы объявлены в include -файле . Использование функции int86x предназначено для прямого вызова прерываний DOS , которые берут аргумент в регистре ES или значение регистра DS, которое отличается от умалчиваемого сегмента данных.

Возвращаемое значение.
Возвращаемым значением является значение в регистре AX после возвращения прерываний. Если поле flag в outregs является ненулевым, возникает ошибка и переменная _doserrno также устанавливает соответствующий код ошибки.

Замечание!
Значения сегмента для аргумента segreg могут быть получены либо при использовании функции segread , либо макро FP_SEG.

Доступ к регистрам через функции и структуры

Помогите построчно описать программу:

29.05.2012, 23:32

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

Доступ к элементам структуры через указатель на структуру
Как делается доступ к элементам структуры через указатель на структуру. Вроде же через ->но тогда.

Разный доступ к элементам структуры через указатель
Всем добрый вечер! Есть структура и два указателя на структуру. Первый указатель выделяет память.

Доступ к элементам структуры через указатель на структуру
Столкнулся с такой проблемой , никак не могу достучаться к элементу(char type) структуры через.

Виртуальные функции, как получить доступ к закрытой части класса.Через set-get?
Здравствуйте! Мне очень нужна ваша помощь. Есть вот такой код. Но мне нужно получить доступ к.

Что означает «int 0x80» в ассемблерном коде?

может кто-нибудь объяснить, что делает следующий ассемблерный код?

8 ответов:

он передает управление для прерывания вектора 0x80

на Linux, посмотрите на этой: он был использован для обработки system_call . Конечно, на другой ОС это может означать что-то совершенно другое.

int означает прерывание, а количество 0x80 — это номер прерывания. Прерывание передает поток программы тому, кто обрабатывает это прерывание, которое является прерыванием 0x80 в этом случае. В Linux 0x80 обработчик прерываний является ядром и используется для выполнения системных вызовов ядра другими программами.

ядро получает уведомление о том, какой системный вызов программа хочет сделать, изучив значение в регистре %eax (синтаксис газа и EAX в Intel синтаксис.) Каждый системный вызов имеет различные требования к использованию других регистров. Например, значение 1 на %eax означает системный вызов из exit() и значение %ebx содержит значение кода состояния для exit() .

имейте в виду, что 0x80 = 80h = 128

вы можете ознакомиться здесь это INT — это всего лишь одна из многих инструкций (на самом деле представление языка ассемблера (или я должен сказать «мнемонический»)), которая существует в наборе инструкций x86. Вы также можете найти дополнительную информацию об этой инструкции в Intel’s own manual found здесь.

чтобы подвести итог из PDF:

INT n / INTO / INT 3-вызов для прерывания Процедура

инструкция INT n генерирует вызов прерывания или исключения обработчик, указанный с помощью операнда назначения. Назначение операнд задает вектор от 0 до 255, закодированный как 8-разрядный без знака промежуточное значение. Инструкция INT n является общей мнемоникой для выполнение программного вызова обработчика прерываний.

Как видите, 0x80 — это назначение операнда в вашем вопросе. На этом этапе процессор знает, что он должен выполнить некоторый код, который находится в ядре, но какой код? Это определяется вектором прерывания в Linux.

одним из самых полезных программных прерываний DOS было прерывание 0x21. Вызывая его с различными параметрами в регистрах (в основном ah и al), вы можете получить доступ к различным операциям ввода-вывода, строковому выходу и т. д.

большинство систем и производных Unix не используют программные прерывания, за исключением прерывания 0x80, используется для выполнения системных вызовов. Это достигается путем ввода 32-разрядное значение, соответствующее функции ядра в регистр EAX процессора и затем выполнение INT 0x80.

взгляните на это, пожалуйста, где показаны другие доступные значения в таблицах обработчиков прерываний:

Как вы можете видеть в таблице указывает ЦП для выполнения системного вызова. Вы можете найти таблицу системных вызовов Linux здесь.

таким образом, перемещая значение 0x1 в регистр EAX и вызывая INT 0x80 в своей программе, вы можете заставить процесс выполнить код в ядре, который остановит (выйдет) текущий запущенный процесс (на Linux, x86 Intel CPU).

аппаратное прерывание не следует путать с программным прерыванием. здесь это очень хороший ответ на этот счет.

этой также является хорошим источником.

вы можете увидеть int 80h в действиях здесь.

int 0x80-это язык ассемблера инструкция, которая используется для вызова системные вызовы в Linux на x86 (т. е., Процессоры Intel-совместимого).

минимальный запускаемый пример системного вызова Linux

Linux устанавливает обработчик прерываний для 0x80 таким образом, что он реализует системные вызовы, способ для программ userland для связи с ядром.

скомпилировать и запустить с:

результат: программа печатает в stdout:

и выходит аккуратно.

вы не можете установить свои собственные обработчики прерываний непосредственно из пользовательского потому что у вас есть только кольцо 3 и Linux мешает вам сделать это.

лучшие альтернативы

int 0x80 был заменен лучшими альтернативами для выполнения системных вызовов: first sysenter , затем ВДСО.

минимальный 16-битный пример

сначала узнайте, как создать минимальную загрузочную ОС и запустить ее на QEMU и реальном оборудовании, как я объяснил здесь:https://stackoverflow.com/a/32483545/895245

вы можете работать в 16-битном реальном режиме:

это будет делать по порядку:

  • Do 0.
  • Do 1.
  • hlt : остановка выполнение

обратите внимание, как процессор ищет первый обработчик по адресу 0 , а второй в 4 : это таблица обработчиков называется IVT, и каждая запись имеет 4 байта.

пример минимального защищенного режима

современные операционные системы работают в так называемом защищенном режиме режим.

обработка имеет больше возможностей в этом режиме, так что это сложнее, но дух тот же.

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

инструкция «int» вызывает прерывание.

что такое прерывание?

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

Подробный Ответ!—15:

ЦП имеет таблицу процедур обслуживания прерываний (или ISR), хранящихся в памяти. В реальном (16-битном) режиме это сохраняется как IVT или яnterrupt VЭктор Tсостоянии. IVT обычно находится по адресу 0x0000:0x0000 (адрес 0x00000 ), и это-ряд адресов смещения сегмента, которые указывают на ISR. ОС может заменить уже существующие записи IVT своими собственными ISR.

(Примечание: размер IVT фиксируется на 1024 (0x400) байт.)

в защищенном (32-разрядном) режиме процессор использует IDT. IDT-это структура переменной длины, которая состоит из дескрипторов (в противном случае известный как ворота), которые сообщают процессору о обработчиках прерываний. Структура этих дескрипторов намного сложнее, чем простые записи смещения сегмента IVT; вот она:

*IDT может иметь переменный размер, но он должен быть последовательным, т. е. если вы объявляете свой IDT от 0x00 до 0x50, у вас должно быть каждое прерывание от 0x00 до 0x50. ОС не обязательно использует все из них, поэтому настоящий бит позволяет процессору правильно обрабатывать прерывания, которые ОС не делает намерены справиться.

когда происходит прерывание (либо внешним триггером (например, аппаратным устройством) в IRQ, либо int инструкция от программы), процессор толкает EFLAGS, затем CS, а затем EIP. (Они автоматически восстанавливаются с помощью iret прерывание инструкции return.) ОС обычно хранит дополнительную информацию о состоянии машины, обрабатывает прерывание, восстанавливает состояние машины и продолжает работу.

во многих * Nix ОС (в том числе Linux), системные вызовы основаны на прерываниях. Программа помещает аргументы системного вызова в регистры (EAX, EBX, ECX, EDX и др.).), и вызовы прерывают 0x80. Ядро уже установило IDT, чтобы содержать обработчик прерываний на 0x80, который вызывается, когда он получает прерывание 0x80. Затем ядро считывает Аргументы и соответственно вызывает функцию ядра. Он может хранить возврат в EAX / EBX. Системные вызовы в значительной степени были заменены на sysenter и sysexit (или syscall и sysret на AMD) инструкции, которые позволяют быстрее войти в кольцо 0.

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

Как уже упоминалось, это заставляет управление прыгать, чтобы прервать вектор 0x80. На практике это означает (по крайней мере, в Linux), что вызывается системный вызов; точный системный вызов и аргументы определяются содержимым регистров. Например, exit () можно вызвать, установив %eax в 1, а затем «int 0x80».

он сообщает процессору активировать вектор прерывания 0x80, который в ОС Linux является прерыванием системного вызова, используемым для вызова системных функций, таких как open() для файлов, и так далее.

СТРУКТУРА КОМАНД INTEL 80×86

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

Формат инструкции архитектуры Intel приведен на рис.1. Это схематичное представление. Пока без размерностей и пояснений.

префикс код
операции
modR/M SIB смещение непосредственный
операнд
mod reg Scale Index Base

Рис.1. Формат команд процессора х86 фирмы Intel

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

Префикс

Префиксы делятся на четыре группы:

    блокировки и повторения:

    На этом же основан один очень любопытный прием противодействия отладчикам, в том числе и знаменитому отладчику-эмулятору Cup386. Рассмотрим, как работает конструкция 0x66:RETN. Казалось бы, раз команда RETN не имеет операндов, то префикс 0x66 можно просто игнорировать. Но, на самом деле, все не так просто. RETN работает с неявным операндом-регистром ip/eip. Именно его и изменяет префикс. Разумеется, в реальном и 16-разрядном режиме указатель команд всегда обрезается до 16 бит, и поэтому, на первый взгляд, возврат сработает корректно. Но стек-то окажется несбалансированным! Из него вместе одного слова взяли целых два! Так нетрудно получить и исключение 0Ch - исчерпание стека.

    Любопытно, какой простой, но какой надежный прием. Впрочем, следует признать, что перехват INT 0Ch под операционной системой Windows бесполезен, и, не смотря на все ухищрения, приложение, породившие такое исключение, будет безжалостно закрыто. Однако, в реальном режиме это работает превосходно.

    Еще интереснее получится, если попытаться исполнить в 16-разрядном сегменте команду CALL. Если адрес перехода лежит в пределах сегмента, то ничего необычно ожидать не приходится. Инструкция работает нормально. Все чудеса начинаются, когда адрес выходит за эти границы. В защищенном 16-разрядном режиме при уровне привилегий CL0 с большой вероятностью регистр EIP "обрежется" до шестнадцати бит, и инструкция сработает (но, похоже, что не на всех процессорах). Если уровень не CL0, то генерируется исключение защиты 0Dh. В реальном же режиме эта инструкция может вести себя непредсказуемо. Хотя в общем случае должно генерироваться прерывание INT 0Dh. В реальном режиме его нетрудно перехватить и совершить дальний 'far'-переход в требуемый сегмент. Так поступает, например, операционная система Касперского OS\7R, дающая в реальном режиме плоскую (flat) модель памяти. Разумеется, такой поворот событий не может пережить ни один отладчик. Ни трассировщики реального режима, ни v86, ни protect-mode debugger, ни даже эмуляторы с этим справиться не в состоянии.

    Одно плохо - все эти приемы не работают под Windows и другими операционными системами. Это вызвано тем, что обработка исключения типа "Общее нарушение защиты" всецело лежит на ядре операционной системы, что не позволяет приложениям распоряжаться им по своему усмотрению. Забавно, но в режиме эмуляции MS-DOS некоторые EMS-драйверы ведут себя в этом случае совершенно непредсказуемо. Часто при этом они не генерируют ни исключения 0Сh, ни 0Dh. Это следует учитывать при разработке защит, основанных на приведенных выше приемах.

    Обратим внимание так же и на последовательности типа 0x66 0x66 [ххх]. Хотя фирма Intel не гарантирует корректную работу своих процессоров в такой ситуации, но фактически все они правильно интерпретируют такую ситуацию. Иное дело некоторые отладчики и дизассемблеры, которые спотыкаются и начинают некорректно вести себя.

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

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

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

    Префиксы переопределения сегмента могут встречаться перед любой командой, в том числе и не обращающейся к памяти, например, CS:NOP вполне успешно выполнится. А вот некоторые дизассемблеры сбиться могут. К счастью, IDA Pro к ним не относится. Самое интересное, что комбинация
    DS: FS: FG: CS: MOV AХ,[100]
    работает вполне нормально (хотя это и не гарантируется фирмой Intel). При этом последний префикс в цепочке перекрывает все остальные. Некоторые отладчики, наоборот, ориентируются на первый префикс в цепочке, что дает неверный результат. Этот пример хорош тем, что великолепно выполняется под Windows и другими операционными системами. К сожалению, на декодирование каждого префикса тратится один такт, и все это может медленно работать.

    Код операции

    Само поле кода операции занимает восемь бит и чаще всего имеет следующий формат (рис. 2):

    код операции направ-
    ление
    размер
    регистр
    условие инверсия
    7 6 5 4 3 2 1 0

    Рис. 2. Формат поля "Код операции"

    Поле размера указывает на размер операндов. Это поле равно 0, если операнды имеют размер в один байт. Это поле равно 1, если операнды имеют размер в слово (двойное слово в 32-битном режиме или в 16-битном режиме с префиксом 0x66).

    Поле направления обозначает, какой из операндов будет приемником. Если направление равно 0, то приемник - правый операнд, если же направление равно 1, то приемник - левый операнд. На примере инструкции mov bx,dx видно, как можно поменять местами операнды, изменив всего один бит:

    Однако, давайте задумаемся, как поле направления будет вести себя, когда один из операндов непосредственное значение? Разумеется, что оно не может быть приемником и независимо от содержимого этого бита будет только источником. Инженеры Intel учли такую ситуацию и нашли оригинальное применение, часто экономящее в лучшем случае целых три байта. Рассмотрим ситуацию, когда операнду размером слово или двойное слово присваивается непосредственное значение по модулю меньшее 0100h. Ясно, что значащим является только младший байт, а стоящие слева нули по правилам математики можно отбросить. Но попробуйте объяснить это процессору! Потребуется пожертвовать хотя бы одним битом, чтобы указать ему на такую ситуацию. Вот для этого и используется бит направления. Рассмотрим следующую команду:

    Если теперь флаг направления установить в единицу, то произойдет следующие:

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

    После выполнения инструкция в строке 0100h приобретет следующий вид:

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

    Совсем другое дело отладчики, и особенно отладчики-эмуляторы, которые часто вычисляют значение ip после выполнения команды (это легче запрограммировать). В результате чего наступает крах. Маленькая тонкость - до или после оказалась роковой, и вот вам в подтверждение дамп экрана:

    Он изменит 6-ю строку на XOR SP,SP. Это "завесит" многие отладчики, и, кроме того, не позволит дизассемблерам отслеживать локальные переменные адресуемые через SP. Хотя IDA Pro и позволяет скорректировать стек вручную, для этого надо сначала понять, что SP обнулился. В приведенном примере это очевидно (но в глаза, кстати, не бросается), а если это произойдет в многопоточной системе? Тогда изменение кода очень трудно будет отследить, особенно в листинге дизассемблера. Однако, нужно помнить, что самомодифицирующийся код все же уходит в историю. Сегодня он встречается все реже и реже.

    Кстати, IDA Pro вообще отказывается анализировать весь последующий код. Как это можно использовать? Да очень просто - если эмулировать еще два сегментных регистра в обработчике INT 06h, то очень трудно это будет как отлаживать, так и дизассемблировать программу. Однако, это опять-таки не работает под Win32!

    Управляющие/отладочные регистры кодируются нижеследующим образом:

    2-битная кодировка 3-битная кодировка
    Управляющие регистры Отладочные регистры
    000 CR0 DR0
    001 Зарезервировано DR1
    010 CR2 DR2
    011 CR3 DR3
    100 CR4 Зарезервировано
    101 Зарезервировано Зарезервировано
    110 Зарезервировано DR6
    111 Зарезервировано DR7

    Заметим, что коды операций mov, манипулирующих этими регистрами, различны, поэтому-то и возникает кажущееся совпадение имен. С управляющими регистрами связана одна любопытная мелочь. Регистр CR1, как известно большинству, в настоящее время зарезервирован и не используется. Во всяком случае, так написано в русскоязычной документации. На самом деле регистр CR1 просто не существует! И любая попытка обращения к нему вызывает генерацию исключение INT 06h. Например, сир386 в режиме эмуляции процессора этого не учитывает и неверно исполняет программу. А все дизассемблеры, за исключением IDA Pro, неправильно дизассемблируют этот несуществующий регистр:

    Все эти команды на самом деле не существуют и приводят к вызову прерывания INT 06h. He так очевидно, правда? И еще менее очевидно обращение к регистрам DR4-DR5. При обращении к ним исключения не генерируется.

    Между прочим, IDA Pro 3.84 дезассемблирует не все регистры. Зато великолепно их ассемблирует (кстати, ассемблер этот был добавлен другим разработчиком).

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

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

    Первым ключевым элементом является байт modR/M.

    mod reg R/M
    7 6 5 4 3 2 1 0

    Рис. 3. Формат байта modR/M

    Если mod содержит 11b, то два следующих поля будут представлять собой регистры. (Это так называемая регистровая адресация). Например:

    Как отмечалось выше, по байту modR/M нельзя точно установить регистры. В зависимости от кода операции и префиксов размера операндов, результат может коренным образом меняться.

    Биты 3-5 могут вместо определения регистра уточнять код операции (в случаи, если один из операндов представлен непосредственным значением). Младшие три бита всегда либо регистр, либо способ адресации, что зависит от значения mod. A вот биты 3-5 никак не зависят от выбранного режима адресации и задают всегда либо регистр, либо непосредственный операнд.

    Формат поля R/M, строго говоря, не документирован, однако достаточно очевиден, что позволяет избежать утомительного запоминания совершенно нелогичной на первый взгляд таблицы адресаций (таблица 3).

    X X X
    0 - нет базирования
    1 - есть базирование
    если 2-й бит=0, то X: '0' - SI, '1' - DI
    если 2-й бит=1, то X: '0' - BP, '1' - BX
    если 3-й бит=0, то X: '0' - база BX, '1' - BP
    если 3-й бит=1, то X: '0' - индексный регистр, '1' - базовый

    Рис. 4. Формат поля R/M.

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

    Действительно, в поле R/M все три бита тесно взаимосвязаны, в отличии от поля mod, которое задает длину следующего элемента в байтах.

    [Reg+Reg]
    Код
    операции
    00 reg Mem
    7 0 7 0
    [Reg+Reg+Offset8]
    Код
    операции
    01 reg Mem Offset8
    7 0 7 0
    [Reg+Reg+Offset16]
    Код
    операции
    10 reg Mem Offset16
    7 0 7 0

    Рис. 5. Формат команды в зависимости от поля mod.

    Разумеется, не может быть смещения Offset 12, (т.к. процессор не оперирует с полуторными словами), а комбинация 11 указывает на регистровую адресацию.

    Может возникнуть вопрос, как складывать с 16-битным регистром 8-битное смещение? Конечно, непосредственному сложению мешает несовместимость типов, поэтому процессор сначала расширяет 8 бит до слова с учетом знака. Поэтому, диапазон возможных значений составляет от -127 до 127 (или от -0x7F до 0x7FF).

    Все вышесказанное проиллюстрировано в приведенной ниже таблице 3. Обратим внимание на любопытный момент - адресация типа [ВР] отсутствует. Ее ближайшим эквивалентом является [ВР+0]. Отсюда следует, что для экономии следует избегать непосредственного использования ВР в качестве индексного регистра. ВР может быть только базой. И mov ax,[bр] хотя и воспринимается любым ассемблером, но ассемблируется в mov ах,[bр+0], что на байт длиннее.

    Исследовав приведенную ниже таблицу 3, можно прийти к выводу, что адресация в процессоре 8086 была достаточно неудобной. Сильнее всего сказывалось то ограничение, что в качестве индекса могли выступать только три регистра (ВХ, SI, DI), когда гораздо чаще требовалось использовать для этого СХ (например, в цикле) или (как возвращаемое функцией значение).

    Поэтому, начиная с процессора 80386 (для 32-разрядного режима), концепция адресаций была пересмотрена. Поле R/M стало всегда выражать регистр независимо от способа его использования, чем стало управлять поле mod, задающие, кроме регистровой, три вида адресации:

    mod адрес

    Видно, что поле mod по-прежнему выражает длину следующего поля - смещения, разве что с учетом 32-битного режима, где все слова расширяются до 32 бит.

    Напомним, что с помощью префикса 0x67 можно и в 16-битном режиме использовать 32-битный режимы адресации, и наоборот. Однако, при этом мы сталкиваемся с интересным моментом - разрядность индексных регистров остается 32-битной и в 16-битном режиме!

    В реальном режиме, где нет понятия границ сегментов, это действительно будет работать так, как выглядит, и мы сможем адресовать первые 4 мегабайта памяти (32 бита), что позволит преодолеть печально известное ограничение размера сегмента 8086 процессоров в 64К. Но такие приложения окажутся нежизнеспособными в защищенном или V86 режиме. Попытка вылезти за границу 64К сегмента вызовет исключение 0Dh, что приведет к автоматическому закрытию приложения, скажем, под управлением Windows. Аналогично поступают и отладчики (в том числе и многие эмуляторы, включая Cup386).

    Сегодня актуальность этого приема, конечно, значительно снизилась, поскольку "голый DOS" практически уже не встречается, а режим его эмуляции Windows крайне неудобен для пользователей.

    Таблица 3

    Итак, для начала поищем, кто выводит текст 'Crack me. Type password:'. В самом файле начало текста расположено со смещением 77h. Следовательно, учитывая, что com-файлы загружаются, начиная со смещения 100h, эффективное смещение равняется 100h+77h=177h. Учитывая обратное расположение старших и младших байт, ищем в файле последовательность 77h 01h.

    Вот она! Но что представляет собой код 0BAh? Попробуем определить это по трем младшим битам. Они принадлежат регистру DL(DX). А 0B4h 09h - это * AH,9. Теперь нетрудно догадаться, что оригинальный код выглядел как:

    И это при том, что не требуется помнить код команды MOV! (Хотя это очень распространенная команда и запомнить ее код все же не помешает).

    Вызов 21-го прерывания 0CDh 21h легко отыскать, если запомнить его символьное представление '=!' в правом окне дампа. Как нетрудно видеть, следующий вызов INT 21h лежит чуть правее по адресу 0Ch. При этом DX указывает на 0156h. Это соответствует смещению 056h в файле. Наверняка эта функция читает пароль. Что ж, уже теплее. Остается выяснить, кто и как к нему обращается. Ждать придется недолго.

    При разборе байта 0Eh не забудьте, что адресации [ВР] не существует в природе. Вместо этого мы получим [offset16]. На размер регистра и приемник результата указывают два младших бита байта 08Ah. Они равны 10b. Следовательно, мы имеем дело с регистром CL, в который записывается содержимое ячейки [0156h].

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

    Вряд ли мы скажем, о чем говорит код 087h. (Впрочем, обращая внимание на его близость к операции NOP, являющейся псевдонимом XCHG AХ,AХ, можно догадаться, что 087h - это код операции XCHG). Обратим внимание на связанный с ним байт 0F2h:

    Эта команда заносит в SI смещение пароля, содержащееся в DX. Такой вывод следует исключительно из смыслового значения регистров (код команды игнорируется). К сожалению, этого нельзя сказать о следующем байте - 0ACh. Это код операции LODSB, и его надо просто запомнить.

    0x02 - код операции ADD, а следующий за ним байт - код AH,AL.

    0хЕ2 - код операции LOOP, а следующий за ним байт - знаковое относительное смещение перехода.

    Чтобы превратить его в знаковое целое, необходимо дополнить его до нуля, (операция NEG, которую большинство калькуляторов не поддерживают). Тот же самый результат мы получим, если отнимем от 0100h указанное значение (в том случае, если разговор идет о байте). В нашем примере это равно пяти. Отсчитаем пять байт влево от начала следующей команды. Если все сделать правильно, то вычисленный переход должен указывать на байт 0ACh (команда LODSB), впрочем, последнее было ясно и без вычислений, ибо других вариантов, по-видимому, не существует.

    Почему? Да просто данная процедура подсчета контрольной суммы (или точнее хеш-суммы) очень типична. Впрочем, не стоит всегда полагаться на свою интуицию и "угадывать" код, хотя это все же сильно ускоряет анализ.

    С другой стороны, хакер без интуиции - это не хакер. Давайте применим нашу интуицию, чтобы "вычислить", что представляет собой код следующей команды. Вспомним, что 0B4h (10110100b) - это MOV AН,imm8.

    0BEh очень близко к этому значению, следовательно, это операция MOV. Осталось определить регистр-приемник. Рассмотрим обе команды в двоичном виде:

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

    Обратим внимание, что это, строго говоря, частный случай, и все могло оказаться иначе. Так, например, четвертый справа бит по аналогии должен быть флагом направления или знакового расширения, но увы - таковым в данном случае не является. Четыре левые бита - это код операции mov reg, imm. Запомнить его легко - это "13" в восьмеричном представлении.

    Итак, 0BEh 0ЗВh 001h - это MOV SI, 013Bh. Скорее всего, 01ЗВh - это смещение, и за этой командой последует расшифровщик очередного фрагмента кода. А может быть и нет - это действительно смелое предположение. Однако, байты 0З0h 024h это подтверждают. Хакеры обычно так часто сталкиваются с функций хоr, что чисто механически запоминают значение ее кода.

    Не трудно будет установить, что эта последовательность дизассемблируется как XOR [SI],AН. Следующий байт 046h уже нетрудно "угадать" - INC SI. Кстати, посмотрим, что же интересного в этом коде:

    Третий бит равен нулю! Выходит команда должна выглядеть как INC AН! (Что кстати, выглядит непротиворечиво смысле дешифровщика). Однако, все же это INC SI. Почему мы решили, что третий бит - флаг размера? Ведь Intel этого никак не гарантировала! А команда 'INC byte' вообще выражается через дополнительный код, что на байт длиннее.

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

    Записки программиста

    Шпаргалка по основным инструкциям ассемблера x86/x64

    12 октября 2020

    Простите, у вас не найдется минутки поговорить о спасителе нашем, ассемблере? В прошлой статье мы написали наше первое hello world приложение на асме, научились его компилировать и отлаживать, а также узнали, как делать системные вызовы в Linux. Сегодня же мы познакомимся непосредственно с ассемблерными инструкциями, понятием регистров, стека и вот этого всего. Ассемблеры для архитектур x86 (a.k.a i386) и x64 (a.k.a amd64) очень похожи, в связи с чем нет смысла рассматривать их в отдельных статьях. Притом акцент я постараюсь делать на x64, попутно отмечая отличия от x86, если они есть. Далее предполагается, что вы уже знаете, например, чем стек отличается от кучи, и объяснять такие вещи не требуется.

    Регистры общего назначения

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

    На x86 доступно восемь 32-х битных регистров общего назначения — eax, ebx, ecx, edx, esp, ebp, esi и edi. Регистры не имеют заданного наперед типа, то есть, они могут трактоваться как знаковые или беззнаковые целые числа, указатели, булевы значения, ASCII-коды символов, и так далее. Несмотря на то, что в теории эти регистры можно использовать как угодно, на практике обычно каждый регистр используется определенным образом. Так, esp указывает на вершину стека, ecx играет роль счетчика, а в eax записывается результат выполнения операции или процедуры. Существуют 16-и битные регистры ax, bx, cx, dx, sp, bp, si и di, представляющие собой 16 младших бит соответствующих 32-х битных регистров. Также доступны и 8-и битовые регистры ah, al, bh, bl, ch, cl, dh и dl, которые представляют собой старшие и младшие байты регистров ax, bx, cx и dx соответственно.

    Рассмотрим пример. Допустим, выполняются следующие три инструкции:

    Значения регистров после записи в eax значения 0 x AABBCCDD:

    Значения после записи в регистр al значения 0 x EE:

    Значения регистров после записи в ax числа 0 x 1234:

    Как видите, ничего сложного.

    Примечание: Синтаксис GAS позволяет явно указывать размеры операндов путем использования суффиксов b (байт), w (слово, 2 байта), l (длинное слово, 4 байта), q (четверное слово, 8 байт) и некоторых других. Например, вместо команды mov $ 0xEE , % al можно написать movb $ 0xEE , % al , вместо mov $ 0x1234 , % ax — movw $ 0x1234 , % ax , и так далее. В современном GAS эти суффиксы являются опциональными и я лично их не использую. Но не пугайтесь, если увидите их в чужом коде.

    На x64 размер регистров был увеличен до 64-х бит. Соответствующие регистры получили название rax, rbx, и так далее. Кроме того, регистров общего назначения стало шестнадцать вместо восьми. Дополнительные регистры получили названия r8, r9, …, r15. Соответствующие им регистры, которые представляют младшие 32, 16 и 8 бит, получили название r8d, r8w, r8b, и по аналогии для регистров r9-r15. Кроме того, появились регистры, представляющие собой младшие 8 бит регистров rsi, rdi, rbp и rsp — sil, dil, bpl и spl соответственно.

    Про адресацию

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

    Эта запись означает «прочитай 8 байт по адресу, записанному в регистре rsp, и сохрани их в регистр rax». При запуске программы rsp указывает на вершину стека, где хранится число аргументов, переданных программе (argc), указатели на эти аргументы, а также переменные окружения и кое-какая другая информация. Таким образом, в результате выполнения приведенной выше инструкции (разумеется, при условии, что перед ней не выполнялось каких-либо других инструкций) в rax будет записано количество аргументов, с которыми была запущена программа.

    В одной команде можно указывать адрес и смешение (как положительное, так и отрицательное) относительно него:

    Эта запись означает «возьми rsp, прибавь к нему 8, прочитай 8 байт по получившемуся адресу и положи их в rax». Таким образом, в rax будет записан адрес строки, представляющей собой первый аргумент программы, то есть, имя исполняемого файла.

    При работе с массивами бывает удобно обращаться к элементу с определенным индексом. Соответствующий синтаксис:

    Читается так: «посчитай rcx*8 + rsp + 16, и поменяй местами 8 байт (размер регистра) по получившемуся адресу и значение регистра rax». Другими словами, rsp и 16 все так же играют роль смещения, rcx играет роль индекса в массиве, а 8 — это размер элемента массива. При использовании данного синтаксиса допустимыми размерами элемента являются только 1, 2, 4 и 8. Если требуется какой-то другой размер, можно использовать инструкции умножения, бинарного сдвига и прочие, которые мы рассмотрим далее.

    Наконец, следующий код тоже валиден:

    .data
    msg :
    . ascii "Hello, world!\n"
    . text

    . globl _start
    _start :
    # обнуление rcx
    xor % rcx , % rcx
    mov msg ( ,% rcx , 8 ) , % al
    mov msg , % ah

    В смысле, что можно не указывать регистр со смещением или вообще какие-либо регистры. В результате выполнения этого кода в регистры al и ah будет записан ASCII-код буквы H, или 0 x 48.

    В этом контексте хотелось бы упомянуть еще одну полезную ассемблерную инструкцию:

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

    Fun fact! На x64 в байткоде инструкций никогда не используются 64-х битовые смещения. В отличие от x86, инструкции часто оперируют не абсолютными адресами, а адресами относительно адреса самой инструкции, что позволяет обращаться к ближайшим +/- 2 Гб оперативной памяти. Соответствующий синтаксис:

    Сравним длины опкодов «обычного» и «относительного» mov ( objdump -d ):

    Как видите, «относительный» mov еще и на один байт короче! Что это за регистр такой rip мы узнаем чуть ниже.

    Для записи же полного 64-х битового значения в регистр предусмотрена специальная инструкция:

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

    Арифметические операции

    Рассмотрим основные арифметические операции:

    # инциализируем значения регистров
    mov $ 123 , % rax
    mov $ 456 , % rcx

    # инкремент: rax = rax + 1 = 124
    inc % rax

    # декремент: rax = rax - 1 = 123
    dec % rax

    # сложение: rax = rax + rcx = 579
    add % rcx , % rax

    # вычитание: rax = rax - rcx = 123
    sub % rcx , % rax

    # изменение знака: rcx = - rcx = -456
    neg % rcx

    Здесь и далее операндами могут быть не только регистры, но и участки памяти или константы. Но оба операнда не могут быть участками памяти. Это правило применимо ко всем инструкциям ассемблера x86/x64, по крайней мере, из рассмотренных в данной статье.

    В данном примере инструкция mul умножает al на cl, и сохраняет результат умножения в пару регистров al и ah. Таким образом, ax примет значение 0 x 12C или 300 в десятичной нотации. В худшем случае для сохранения результата перемножения двух N-байтовых значений может потребоваться до 2*N байт. В зависимости от размера операнда результат сохраняется в al:ah, ax:dx, eax:edx или rax:rdx. Притом в качестве множителей всегда используется первый из этих регистров и переданный инструкции аргумент.

    Знаковое умножение производится точно так же при помощи инструкции imul. Кроме того, существуют варианты imul с двумя и тремя аргументами:

    mov $ 123 , % rax
    mov $ 456 , % rcx

    # rax = rax * rcx = 56088
    imul % rcx , % rax

    # rcx = rax * 10 = 560880
    imul $ 10 , % rax , % rcx

    Инструкции div и idiv производят действия, обратные mul и imul. Например:

    mov $ 0 , % rdx
    mov $ 456 , % rax
    mov $ 123 , % rcx

    # rax = rdx:rax / rcx = 3
    # rdx = rdx:rax % rcx = 87
    div % rcx

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

    Это далеко не все арифметические инструкции. Например, есть еще adc (сложение с учетом флага переноса), sbb (вычитание с учетом займа), а также соответствующие им инструкции, выставляющие и очищающие соответствующие флаги (ctc, clc), и многие другие. Но они распространены намного меньше, и потому в рамках данной статьи не рассматриваются.

    Логические и битовые операции

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

    Так, например, выглядит вычисление простейшего логического выражения:

    mov $ 0 , % rax # a = false
    mov $ 1 , % rbx # b = true
    mov $ 0 , % rcx # c = false

    # rdx := a || !(b && c)
    mov % rcx , % rdx # rdx = c
    and % rbx , % rdx # rdx &= b
    not % rdx # rdx =

    rdx
    or % rax , % rdx # rdx |= a
    and $ 1 , % rdx # rdx &= 1

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

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

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

    В данном контексте также следует вспомнить инструкции побитового сдвига, тестирования битов (bit test) и сканирования битов (bit scan):

    # положим что-нибудь в регистр
    movabs $ 0xc0de1c0ffee2beef , % rax

    # сдвиг влево на 3 бита
    # rax = 0x0de1c0ffee2beef0
    shl $ 4 , % rax

    # сдвиг вправо на 7 бит
    # rax = 0x001bc381ffdc57dd
    shr $ 7 , % rax

    # циклический сдвиг вправо на 5 бит
    # rax = 0xe800de1c0ffee2be
    ror $ 5 , % rax

    # циклический сдвиг влево на 5 бит
    # rax = 0x001bc381ffdc57dd
    rol $ 5 , % rax

    # положить в CF (см далее) значение 13-го бита
    # CF = !!(0x1bc381ffdc57dd & (1
    bt $ 13 , % rax

    # то же самое + установить бит (bit test and set)
    # rax = 0x001bc381ffdc77dd, CF = 0
    bts $ 13 , % rax

    # то же самое + сбросить бит (bit test and reset)
    # rax = 0x001bc381ffdc57dd, CF = 1
    btr $ 13 , % rax

    # то же самое + инвертировать бит (bit test and complement)
    # rax = 0x001bc381ffdc77dd, CF = 0
    btc $ 13 , % rax

    # найти самый младший ненулевой байт (bit scan forward)
    # rcx = 0, ZF = 0
    bsf % rax , % rcx

    # найти самый старший ненулевой байт (bit scan reverse)
    # rdx = 52, ZF = 0
    bsr % rax , % rdx

    # если все биты нулевые, ZF = 1, значение rdx неопределено
    xor % rax , % rax
    bsf % rax , % rdx

    Еще есть битовые сдвиги со знаком (sal, sar), циклические сдвиги с флагом переноса (rcl, rcr), а также сдвиги двойной точности (shld, shrd). Но используются они не так уж часто, да и утомишься перечислять вообще все инструкции. Поэтому их изучение я оставляю вам в качестве домашнего задания.

    Условные выражения и циклы

    Выше несколько раз упоминались какие-то там флаги, например, флаг переноса. Под флагами понимаются биты специального регистра eflags / rflags (название на x86 и x64 соответственно). Напрямую обращаться к этому регистру при помощи инструкций mov, add и подобных нельзя, но он изменяется и используется различными инструкциями косвенно. Например, уже упомянутый флаг переноса (carry flag, CF) хранится в нулевом бите eflags / rflags и используется, например, в той же инструкции bt. Еще из часто используемых флагов можно назвать zero flag (ZF, 6-ой бит), sign flag (SF, 7-ой бит), direction flag (DF, 10-ый бит) и overflow flag (OF, 11-ый бит).

    Еще из таких неявных регистров следует назвать eip / rip, хранящий адрес текущей инструкции. К нему также нельзя обращаться напрямую, но он виден в GDB вместе с eflags / rflags, если сказать info registers , и косвенно изменяется всеми инструкциям. Большинство инструкций просто увеличивают eip / rip на длину этой инструкции, но есть и исключения из этого правила. Например, инструкция jmp просто осуществляет переход по заданному адресу:

    В результате значение rax будет равно единице, так как первая инструкция inс будет пропущена. Заметьте, что адрес перехода также может быть записан в регистре:

    Впрочем, на практике такого кода лучше избегать, так как он ломает предсказание переходов и потому менее эффективен.

    Примечание: GAS позволяет давать меткам цифирные имена типа 1: , 2: , и так далее, и переходить к ближайшей предыдущей или следующей метке с заданным номером инструкциями вроде jmp 1b и jmp 1f . Это довольно удобно, так как иногда бывает трудно придумать меткам осмысленные имена. Подробности можно найти здесь.

    Условные переходы обычно осуществляются при помощи инструкции cmp, которая сравнивает два своих операнда и выставляет соответствующие флаги, за которой следует инструкция из семейства je, jg и подобных:

    je 1f # перейти, если равны (equal)
    jl 1f # перейти, если знаково меньше (less)
    jb 1f # перейти, если беззнаково меньше (below)
    jg 1f # перейти, если знаково больше (greater)
    ja 1f # перейти, если беззнаково больше (above)

    Существует также инструкции jne (перейти, если не равны), jle (перейти, если знаково меньше или равны), jna (перейти, если беззнаково не больше) и подобные. Принцип их именования, надеюсь, очевиден. Вместо je / jne часто пишут jz / jnz, так как инструкции je / jne просто проверяют значение ZF. Также есть инструкции, проверяющие другие флаги — js, jo и jp, но на практике они используются редко. Все эти инструкции вместе взятые обычно называют jcc. То есть, вместо конкретных условий пишутся две буквы «c», от «condition». Здесь можно найти хорошую сводную таблицу по всем инструкциям jcc и тому, какие флаги они проверяют.

    Помимо cmp также часто используют инструкцию test:

    Fun fact! Интересно, что cmp и test в душе являются теми же sub и and, только не изменяют своих операндов. Это знание можно использовать для одновременного выполнения sub или and и условного перехода, без дополнительных инструкций cmp или test.

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

    Инструкция jrcxz осуществляет переход только в том случае, если значение регистра rcx равно нулю.

    Инструкции семейства cmovcc (conditional move) работают как mov, но только при выполнении заданного условия, по аналогии с jcc.

    Инструкции setcc присваивают однобайтовому регистру или байту в памяти значение 1, если заданное условие выполняется, и 0 иначе.

    Сравнить rax с заданным куском памяти. Если равны, выставить ZF и сохранить по указанному адресу значение указанного регистра, в данном примере rcx. Иначе очистить ZF и загрузить значение из памяти в rax. Также оба операнда могут быть регистрами.

    Инструкция cmpxchg8b главным образом нужна в x86. Она работает аналогично cmpxchg, только производит compare and swap сразу 8-и байт. Регистры edx:eax используются для сравнения, а регистры ecx:ebx хранят то, что мы хотим записать. Инструкция cmpxchg16b по тому же принципу производит compare and swap сразу 16-и байт на x64.

    Важно! Примите во внимание, что без префикса lock все эти compare and swap инструкции не атомарны.

    Инструкция loop уменьшает значение регистра rcx на единицу, и если после этого rcx != 0 , осуществляет переход на заданную метку. Инструкции loopz и loopnz работают аналогично, только условия более сложные — (rcx != 0) && (ZF == 1) и (rcx != 0) && (ZF == 0) соответственно.

    Не нужно быть семи пядей во лбу, чтобы изобразить при помощи этих инструкций конструкцию if-then-else или циклы for / while, поэтому двигаемся дальше.

    «Строковые» операции

    Рассмотрим следующий кусок кода:

    В регистры rsi и rdi кладутся адреса двух строк. Командой cld очищается флаг направления (DF). Инструкция, выполняющая обратное действие, называется std. Затем в дело вступает инструкция cmpsb. Она сравнивает байты (%rsi) и (%rdi) и выставляет флаги в соответствии с результатом сравнения. Затем, если DF = 0, rsi и rdi увеличиваются на единицу (количество байт в том, что мы сравнивали), иначе — уменьшаются. Аналогичные инструкции cmpsw, cmpsl и cmpsq сравнивают слова, длинные слова и четверные слова соответственно.

    Инструкции cmps интересны тем, что могут использоваться с префиксом rep, repe (repz) и repne (repnz). Например:

    Префикс rep повторяет инструкцию заданное в регистре rcx количество раз. Префиксы repz и repnz делают то же самое, но только после каждого выполнения инструкции дополнительно проверяется ZF. Цикл прерывается, если ZF = 0 в случае c repz и если ZF = 1 в случае с repnz. Таким образом, приведенный выше код проверяет равенство двух буферов одинакового размера.

    Аналогичные инструкции movs перекладывает данные из буфера, адрес которого указан в rsi, в буфер, адрес которого указан в rdi (легко запомнить — rsi значит source, rdi значит destination). Инструкции stos заполняет буфер по адресу из регистра rdi байтами из регистра rax (или eax, или ax, или al, в зависимости от конкретной инструкции). Инструкции lods делают обратное действие — копируют байты по указанному в rsi адресу в регистр rax. Наконец, инструкции scas ищут байты из регистра rax (или соответствующих регистров меньшего размера) в буфере, адрес которого указан в rdi. Как и cmps, все эти инструкции работают с префиксами rep, repz и repnz.

    На базе этих инструкций легко реализуются процедуры memcmp, memcpy, strcmp и подобные. Интересно, что, например, для обнуления памяти инженеры Intel рекомендуют использовать на современных процессорах rep stosb , то есть, обнулять побайтово, а не, скажем, четверными словами.

    Работа со стеком и процедуры

    Со стеком все очень просто. Инструкция push кладет свой аргумент на стек, а инструкция pop извлекает значение со стека. Например, если временно забыть про инструкцию xchg, то поменять местами значение двух регистров можно так:

    Существуют инструкции, помещающие на стек и извлекающие с него регистр rflags / eflags:

    А так, к примеру, можно получить значение флага CF:

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

    Процедуры, как правило, «создаются» при помощи инструкций call и ret. Инструкция call кладет на стек адрес следующей инструкции и передает управление по указанному в аргументе адресу. Инструкция ret читает со стека адрес возврата и передает по нему управление. Например:

    someproc :
    # типичный пролог процедуры
    # для примера выделяем 0x10 байт на стеке под локальные переменные
    # rbp - указатель на фрейм стека
    push % rbp
    mov % rsp , % rbp
    sub $ 0x10 , % rsp

    # тут типа какие-то вычисления .
    mov $ 1 , % rax

    # типичный эпилог процедуры
    add $ 0x10 , % rsp
    pop % rbp

    # выход из процедуры
    ret

    _start :
    # как и в случае с jmp, адрес перехода может быть в регистре
    call someproc
    test % rax , % rax
    jnz error

    Примечание: Аналогичный пролог и эпилог можно написать при помощи инструкций enter $ 0x10 , $ 0 и leave . Но в наше время эти инструкции используются редко, так как они выполняются медленнее из-за дополнительной поддержки вложенных процедур.

    Как правило, возвращаемое значение передается в регистре rax или, если его размера не достаточно, записывается в структуру, адрес которой передается в качестве аргумента. К вопросу о передаче аргументов. Соглашений о вызовах существует великое множество. В одних все аргументы всегда передаются через стек (отдельный вопрос — в каком порядке) и за очистку стека от аргументов отвечает сама процедура, в других часть аргументов передается через регистры, а часть через стек, и за очистку стека от аргументов отвечает вызывающая сторона, плюс множество вариантов посередине, с отдельными правилами касательно выравнивания аргументов на стеке, передачи this, если это ООП язык, и так далее. В общем случае для произвольно взятой архитектуры, компилятора и языка программирования соглашение о вызовах может быть вообще каким угодно.

    Для примера рассмотрим ассемблерный код, сгенерированный CLang 3.8 для простой программки на языке C под x64. Так выглядит одна из процедур:

    Дизассемблерный листинг (при компиляции с -O0 , комментарии мои):

    # типичный пролог процедуры
    # регистр rsp не изменяется, так как процедура не вызывает никаких
    # других процедур
    400950: 55 push %rbp
    400951: 48 89 e5 mov %rsp,%rbp

    # инициализация локальных переменных:
    # -0x08(%rbp) - const unsigned char *data (8 байт)
    # -0x10(%rbp) - const size_t data_len (8 байт)
    # -0x14(%rbp) - unsigned int hash (4 байта)
    # -0x18(%rbp) - int i (4 байта)
    400954: 48 89 7d f8 mov %rdi,-0x8(%rbp)
    400958: 48 89 75 f0 mov %rsi,-0x10(%rbp)
    40095c: c7 45 ec 4b 43 41 48 movl $0x4841434b,-0x14(%rbp)
    400963: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp)

    # rax := i. если достигли data_len, выходим из цикла
    40096a: 48 63 45 e8 movslq -0x18(%rbp),%rax
    40096e: 48 3b 45 f0 cmp -0x10(%rbp),%rax
    400972: 0f 83 28 00 00 00 jae 4009a0

    # возвращаемое значение (hash) кладется в регистр eax
    4009a0: 8b 45 ec mov -0x14(%rbp),%eax

    # типичный эпилог
    4009a3: 5d pop %rbp
    4009a4: c3 retq

    Здесь мы встретили две новые инструкции — movs и movz. Они работают точно так же, как mov, только расширяют один операнд до размера второго, знаково и беззнаково соответственно. Например, инструкция movzbl (%rdx,%rcx,1),%esi читайт байт (b) по адресу (%rdx,%rcx,1) , расширяет его в длинное слово (l) путем добавления в начало нулей (z) и кладет результат в регистр esi.

    Как видите, два аргумента были переданы процедуре через регистры rdi и rsi. По всей видимости, используется конвенция под названием System V AMD64 ABI. Утверждается, что это стандарт де-факто под x64 на *nix системах. Я не вижу смысла пересказывать описание этой конвенции здесь, заинтересованные читатели могут ознакомиться с полным описанием по приведенной ссылке.

    Заключение

    Само собой разумеется, в рамках одной статьи, описать весь ассемблер x86/x64 не представляется возможным (более того, я не уверен, что сам знаю его прямо таки весь). Как минимум, за кадром остались такие темы, как операции над числами с плавающей точкой, MMX-, SSE- и AVX-инструкции, а также всякие экзотические инструкции вроде l > objdump -d для большинства реальных программ вы очень редко увидите что-то помимо описанного выше.

    Еще интересный топик, оставшийся за кадром — это атомарные операции, барьеры памяти, спинлоки и вот это все. Например, compare and swap часто реализуется просто как инструкция cmpxchg с префиксом lock. По аналогии реализуется атомарный инкремент, декремент, и прочее. Увы, все это тянет на тему для отдельной статьи.

    В качестве источников дополнительной информации можно рекомендовать книгу Modern X86 Assembly Language Programming, и, конечно же, мануалы от Intel. Также довольно неплоха книга x86 Assembly на wikibooks.org.

    Из онлайн-справочников по ассемблерным инструкциям стоит обратить внимание на следующие:

    А знаете ли вы ассемблер, и если да, то находите ли это знание полезным?

    Что такое IPX и как его использовать в своих целях

    В данной статье приводится
    краткое описание программы симметричного обмена
    информацией - IPX, входящей в состав сетевых
    операционных систем семейства NetWare фирмы Novell.
    Приводятся простейшие примеры использования
    программного интерфейса IPX в пользовательских
    программах.

    1. Подробнее об известном

    Если Вы когда-нибудь
    устанавливали или хотя бы достаточно осмысленно
    использовали сеть NetWare (известную у наших
    программистов в основном как Novell - по имени
    потерпевшего), то знаете, что на каждой рабочей
    станции для использования сети надо запустить
    так называемую оболочку (Shell), состоящую из двух
    резидентных (TSR) программ, называемых IPX и NETx (где x
    - номер версии MS-DOS). Кроме того, можно запустить
    еще эмулятор всем известного сетевого протокола
    сеансового уровня - NetBIOS. Впрочем последнее нужно
    сейчас уже только для отечественных разработок,
    почти все фирменные уже давно в NetBIOS не нуждаются.

    Для чего же нужны IPX и NETx? В
    самом грубом приближении можно сказать, что NETx
    передает серверу все обращения программ к
    удаленному диску, а IPX служит драйвером сетевого
    адаптера. Но это не все. Для того чтобы понять все,
    что делает IPX, вспомним, как структурируются
    функции технического (Hardware) и программного (Software)
    сетевого обеспечения. В большинстве случаев при
    разработке сетей используется стандартная
    модель OSI. OSI - это международный стандарт, который
    отражает представления Международного
    Института Стандартов о том, как должна
    осуществляться связь между компьютерными
    системами. В этой модели функции сетевого
    обеспечения разбиваются на семь групп - уровней.
    При этом чем выше уровень, тем более далекие от
    техники и близкие к пользователю функции собраны
    в нем. Для того чтобы описать функции каждого
    уровня, а заодно и объяснить, что делает IPX,
    позволю себе воспользоваться длинной цитатой из
    профессиональной документации фирмы Novell.

    Nota bene

    Уровень 1 - физический уровень.
    Этот слой имеет дело с типами аппаратуры (hardware),
    используемой для передачи сообщений и данных
    между станциями. (Световоды, витые пары,
    коаксиальные кабели, мультиплексоры и т.д.).

    Уровень 2 - передачи данных.
    Этот уровень имеет дело с такими вещами, как
    битовые шаблоны, маркеры, обнаружение ошибок при
    передаче по кабелю между станциями. Например, token
    ring и CSMA/CD (Ethernet) - это два стандартных метода,
    применяемых для реализации уровня передачи
    данных.

    Уровень 3 - сетевой уровень.
    Этот уровень решает задачи адресации и доставки
    коммуникационных пакетов по сети. Примерами
    протоколов этого уровня являются XNS IDP, IPX и TCP/IP.

    Уровень 4 - транспортный
    уровень. Задачей транспортного уровня является
    обеспечение доставки информации с сетевого
    уровня в правильном порядке без повторений.
    Например, приложения NetWare имеют доступ к SPX для
    выполнения функций данного уровня OSI.

    Уровень 5 - сеансовый уровень.
    Этот уровень предназначается для создания связи
    между компьютерами, установки именования и
    адресации. В системе NetWare задачи этого уровня
    выполняет ответчик (NETx) или эмулятор NETBIOS.

    Уровень 6 - представительский.
    Этот уровень транслирует данные, передаваемые по
    сети во внутренние данные компьютера и обратно.
    Например, этот уровень переводит закодированные
    данные или данные с различным порядком байтов в
    родной цифровой формат машины. В NetWare этот
    уровень реализуется совместно ответчиком и DOS.

    Уровень 7 - прикладной. Этот
    уровень является интерфейсом между сетью и
    прикладным матобеспечением,
    запускаемым на компьютере. Он в NetWare так же
    осуществляется при взаимодействии ответчика и
    DOS.

    Так вот, программа IPX реализует
    протоколы сетевого уровня (IPX) и транспортного
    уровня (SPX) (о чем честно и сообщает при старте).

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

    Точно так же и Вы можете
    использовать IPX для написания своих сетевых
    программ, не зависящих от того, какие сетевые
    адаптеры будут использоваться. Более того, Ваша
    программа будет пригодна практически для всех
    сетей, так как очень редко можно встретить
    сетевую плату, для которой не было бы драйвера
    NetWare (по крайней мере 2.1x).

    2. Почему IPX, а не NETBIOS?

    Некоторые программисты в нашей
    стране и сейчас пишут свои приложения для сетей
    NetWare, но используют для этого более известный
    сетевой протокол - NETBIOS. Лет 5 назад это было
    правильно, так как стандарт, принятый фирмой Novell -
    протокол IPX - не был общепринятым, а стандарт NETBIOS
    уже тогда поддерживался всеми серьезными
    разработчиками сетей. Но сейчас использование
    NETBIOS ведет только к снижению быстродействия и к
    потере свободной памяти при работе в NetWare (как
    говорилось выше, NETBIOS загружается на рабочей
    станции NetWare дополнительно к сетевой оболочке).
    Кроме того, при использовании эмулятора NETBIOS NetWare
    (по крайней мере версий 2.1x) возникают
    неприятности при одновременном обращении к нему
    нескольких параллельных процессов (TSR, процессов
    Модулы-2). И наконец, еще один аргумент в пользу IPX.
    В отличие от NETBIOS, IPX используется не только на
    рабочих станциях сети. Вызовы IPX и SPX один к одному
    проходят также и на сервере и на мосте, и вообще
    на любом узле сети NetWare. Значит, если Вы
    допускаете возможность перевода впоследствии
    Вашего приложения в задачу ( VAP или загружаемый
    модуль), выполняемую на сервере, то всегда
    целесообразно использовать вызовы IPX и SPX.

    В большинстве случаев
    использование NETBIOS обусловлено тем, что его
    протокол более известен. Далее я собираюсь
    изложить протокол IPX, а в следующей статье и SPX с
    достаточной подробностью, чтобы уважаемые
    читатели могли пользоваться этими протоколами
    при программировании.

    3. IPX. Принципы обмена
    информацией

    В этом пункте я собираюсь в
    общих чертах описать протокол IPX, который
    подробно будет описан в дальнейшем. Как Вы
    увидите, протокол IPX и используемые в нем
    структуры имеют определенную избыточность, в
    некоторых случаях неудобную. Это объясняется
    тем, что IPX (протокол, а не программа)
    первоначально предназначался для несколько
    других сетей. Он был разработан фирмой Xerox, и
    затем стал применяться фирмой Novell как основной
    протокол системы NetWare. При этом для соблюдения
    преемственности было сохранено и то, что в сетях
    NetWare не нужно.

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

    Пакет имеет длину не более 576
    байтов. Величина, прямо скажем, несколько
    странная. Во всяком случае, она не соответствует,
    насколько я знаю, длине физически пересылаемого
    пакета ни для одного сетевого адаптера. Для сетей
    ArcNet, например, максимальная длина передаваемого
    за один раз физического пакета равна 512 байтам.
    Поэтому максимальный "пакет" IPX на самом
    деле передается по сети как два физических
    пакета. Но пользователю до этого нет дела (если,
    конечно, не учитывать потери скорости).

    Каждый пакет IPX начинается со
    служебной информации - заголовка (header),
    состоящего из 30 байтов при использовании
    протокола IPX, или 42 байтов при использовании
    протокола SPX. Структура заголовка IPX приведена в
    справочном приложении в конце статьи полными
    объяснениями всех полей.

    Пакет, пересылаемый программой
    IPX, может располагаться в памяти передающей
    машины не обязательно непрерывно. Можно
    разделить его на несколько кусков (общая длина,
    конечно, не должна превосходить знаменитых 576
    байтов). Так же и буфер приема может быть разбит
    на несколько частей. На практике удобно
    разбивать пакет на две части: в одной хранить
    заголовок пакета, а в другой - полезную (для
    Вашего
    приложения) информацию. Более сложные приложения
    могут иметь собственную служебную информацию
    для каждого пакета. В этом случае ее тоже может
    оказаться полезным хранить отдельно.

    Как уже говорилось, передача в
    IPX происходит не от машины к машине, а от процесса
    к процессу. При этом на одном компьютере могут
    быть несколько процессов, которые одновременно
    ждут пакета. Для приема сообщений используются
    хранимые внутри IPX и недоступные программисту
    структуры - сокеты (гнезда). Каждому сокету
    присваивается уникальный номер. Для того чтобы
    принять сообщение, задача должна сначала открыть
    сокет с помощью специального запроса IPX, а затем
    послать IPX другой запрос - на ввод через открытый
    сокет. Передающая задача, в свою очередь, обязана
    указать в заголовке номер сокета, в который она
    направляет сообщение. Если принимающая сторона
    не успевает принимать сообщения, образуется
    очередь сообщений к сокету. Можно сделать и
    наоборот: заставить несколько процессов
    принимать информацию через один и тот же сокет. В
    этом случае мы получим очередь процессов. Правда,
    так делать плохо. Обычно организация очереди
    процессов на однопроцессорной машине приводит
    только к очень сильной путанице.

    4. Как передать сообщение

    Начнем подробное изучение IPX с
    самой простой задачи - передать строку с одного
    компьютера на другой и распечатать на дисплее.
    Более содержательные программы будут помещены в
    следующей главе, в продолжение этой статьи.

    В настоящее время Novell
    поставляет для разработчиков полный программный
    интерфейс (API) своей системы для всех основных
    вариантов языка C. Однако эти библиотеки не
    продаются за рубли, и следовательно вряд ли
    доступны читателю. Поэтому будем писать
    программу, не пользуясь фирменными библиотеками,
    а используя интерфейс с IPX через программные
    прерывания.

    Все функции IPX и SPX на рабочей
    станции доступны через прерывание 7Ah. Номер
    функции передается на регистре Bx. На других
    регистрах - параметры, одним из которых в ряде
    случаев является адрес управляющего блока IPX,
    который называется ECB (Event Control Block).

    Предполагаем, что нам известен
    сетевой адрес того узла, которому адресуем
    посылку. Сокет принимающего сокета - 800
    (по-нормальному 00 08 - припомним, что в сети сначала
    идет старший байт). Также будем считать, что нам
    известны сетевые адреса передающей и
    принимающей станций (для определенности - 0x02 и 0x05
    соответственно).

    Надо написать две программы -
    для передачи сообщения и для приема сообщения.
    Начнем с более простой - передающей. Для передачи
    служит функция с номером 3. На паре регистров Es:Si
    передается адрес ECB.

    Программа должна ввести
    строку, сформировать ECB и пакет, а затем передать
    его. Буфер, из которого формируется пакет, не
    обязан располагаться в памяти в виде
    непрерывного массива, а может быть разделен на
    некоторое количество кусков (см. справочное
    приложение). Я использовал это свойство и
    разделил буфер вывода на две части: системный
    заголовок IPX и массив с передаваемой информацией.
    Остальное ясно из текста программы.

    Текст программы приема
    приведен в листинге 1. Она должна открыть сокет и
    затем принять пакет. Для ожидания приема пакета
    используется динамическое ожидание: программа
    проверяет флаг использования ECB. Прием, как и
    передача, осуществляется в буфер, разделенный на
    две части: заголовок и передаваемую строку. По
    окончании программа закрывает сокет.

    Листинг 1. Программа приема
    сообщения

    #include
    #include
    #include
    #include
    #define TargetSocket 0x08

    /*
    Программа приема и распечатки строки символов
    */

    void main () <
    unsigned char RecieveString [80]; // Принимаемая строка
    IPXHeader Header;
    ECB_2 My_ECB;
    ECB_2 _far *My_ECB_ptr;

    union REGS inregs, outregs;
    struct SREGS segreg;

    My_ECB.ESRAddress = NULL;
    My_ECB.SocketNumber = TargetSocket;
    My_ECB.FragmentCount = 2;
    My_ECB.FragmentAddress_1 = &Header;
    My_ECB.FragmentSize_1 = header_length;
    My_ECB.FragmentAddress_2 = &RecieveString;
    My_ECB.FragmentSize_2 = sizeof RecieveString;

    inregs.x.bx = IPX_open;
    inregs.h.al = 0xFF;
    inregs.x.dx = TargetSocket;
    int86 (IPX_interrupt, &inregs, &outregs);

    /* Выдача кода завершения */

    if (outregs.h.al != 0x00)
    printf ("Ошибка открытия сокета. Код завершения %x
    \n",
    outregs.h.al);

    inregs.x.bx = IPX_listen;
    My_ECB_ptr = &My_ECB;
    inregs.x.si = FP_OFF ( My_ECB_ptr );
    segreg.es = FP_SEG ( My_ECB_ptr );
    int86x (IPX_interrupt, &inregs, &outregs, &segreg);

    while (My_ECB.InUseFlag) // Ожидание завершения приема
    <>;

    /* Распечатка строки или кода завершения */

    if (My_ECB.CompletionCode != 0)

    printf ("Ошибка приема сообщения. Код завершения %x
    \n",
    My_ECB.CompletionCode);
    else
    printf ("%s \n", RecieveString);

    inregs.x.bx = IPX_close;
    inregs.x.dx = TargetSocket;
    int86 (IPX_interrupt, &inregs, &outregs);
    >

    Листинг 2. Программа
    передачи сообщения

    #include
    #include
    #include
    #include
    #define TargetSocket 0x08
    #define TargetNode 0x02

    /*
    Программа передачи введенной оператором строки
    посредством IPX
    */

    void main () <
    unsigned char DispString [80]; // Строка, которую вводим с
    дисплея
    int Length; // Длина введенной строки
    IPXHeader Header;
    ECB_2 My_ECB;
    ECB_2 _far *My_ECB_ptr;

    union REGS inregs, outregs;
    struct SREGS segreg;

    printf ("Введите, пожалуйста, передаваемое
    сообщение \n");
    scanf ("%s", DispString);
    Length = strlen (DispString);

    My_ECB.ESRAddress = NULL;
    My_ECB.SocketNumber = TargetSocket;

    strnset (My_ECB.ImmediateAddress, 0x00, sizeof My_ECB.ImmediateAddress);

    My_ECB.ImmediateAddress[5] = TargetNode;
    My_ECB.FragmentCount = 2;
    My_ECB.FragmentAddress_1 = &Header;
    My_ECB.FragmentSize_1 = header_length;
    My_ECB.FragmentAddress_2 = &DispString;
    My_ECB.FragmentSize_2 = Length+1;

    /* Заполнение заголовка сообщения */

    Header.PacketType = 0x00;
    strnset (Header.DestantionNetwork, 0x00, sizeof Header.DestantionNetwork);
    strnset (Header.DestantionNode, 0x00, sizeof Header.DestantionNode);
    Header.DestantionNode [5] = TargetNode;
    Header.DestantionSocket = TargetSocket;

    inregs.x.bx = IPX_send;
    My_ECB_ptr = &My_ECB;
    inregs.x.si = FP_OFF ( My_ECB_ptr );
    segreg.es = FP_SEG ( My_ECB_ptr );
    int86x (IPX_interrupt, &inregs, &outregs, &segreg);

    while (My_ECB.InUseFlag) // Ожидание завершения посылки
    <>;

    /* и выдача кода завершения */

    printf ("Сообщение передано, код завершения %x
    \n",
    My_ECB.CompletionCode);

    Листинг 3. Константы и
    структуры IPX

    /*
    Константы и структуры IPX
    */

    #define IPX_interrupt 0x7A // Программное прерывание IPX
    #define header_length 30 // Длина заголовка пакета

    #define IPX_open 0x00
    #define IPX_close 0x01
    #define IPX_localTarget 0x02
    #define IPX_send 0x03
    #define IPX_listen 0x04
    #define IPX_event 0x05
    #define IPX_cancel 0x06
    #define IPX_specialEvent 0x07
    #define IPX_interval 0x08
    #define IPX_GetInternetwork 0x09
    #define IPX_control 0x0A
    #define IPX_disconnect 0x0B

    typedef struct <
    unsigned int Checksum;
    unsigned int Length;
    unsigned char TransportControl;
    unsigned char PacketType;
    unsigned char DestantionNetwork [4];
    unsigned char DestantionNode [6];
    unsigned int DestantionSocket;
    unsigned char SourceNetwork [4];
    unsigned char SourceNode [6];
    unsigned int SourceSocket;
    > IPXHeader;

    /* Управляющий блок с одним фрагментом данных */

    typedef struct <
    void _far *LinkAddress;
    void _far *ESRAddress;
    unsigned char InUseFlag;
    unsigned char CompletionCode;
    unsigned int SocketNumber;
    unsigned char IPXWorkspace [4];
    unsigned char DriverWorkspace [12];
    unsigned char ImmediateAddress [6];
    unsigned int FragmentCount;
    void _far *FragmentAddress;
    unsigned int FragmentSize;
    > ECB_1;

    /* Управляющий блок с двумя фрагментами данных */

    typedef struct <
    void _far *LinkAddress;
    void _far *ESRAddress;
    unsigned char InUseFlag;
    unsigned char CompletionCode;
    unsigned int SocketNumber;
    unsigned char IPXWorkspace [4];
    unsigned char DriverWorkspace [12];
    unsigned char ImmediateAddress [6];
    unsigned int FragmentCount;
    void _far *FragmentAddress_1;
    unsigned int FragmentSize_1;
    void _far *FragmentAddress_2;
    unsigned int FragmentSize_2;
    > ECB_2;

    /* Управляющий блок службы времени */

    typedef struct <
    void _far *LinkAddress;
    void _far *ESRAddress;
    unsigned char InUseFlag;
    unsigned char WorkSpace [5];
    > SpecialECB;

    /* Сетевой адрес плюс непосредственный адрес узла
    */

    typedef struct <
    unsigned char NetworkNumber [4]; // Номер локального участка
    unsigned char NodeAddress [6]; // Сетевой адрес
    unsigned int Socket; // Номер сокета
    unsigned char ImmediateAddress [6]; // Непосредственный адрес
    > IntAddr;

    Для открытия сокета
    используется функция IPX с номером 0. На регистре Dx
    передается номер сокета (0 - если сокет
    назначается динамически). На регистре Al - флаг
    "живучести". Если он равен 0, то сокет
    автоматически закрывается при завершении
    программы, если же - 0xFF - то сокет надо закрывать
    самому. Однако мои наблюдения показали, что лучше
    всегда самому закрывать сокет (автоматически он
    не очень закрывается). Поэтому используем честно
    флаг 0xFF. Код завершения возвращается на регистре
    Al. Для приема сообщения используется функция 4. На
    паре Es:Si передается адрес ECB. Наконец, для
    закрытия сокета используется функция 1. На
    регистре Dx передается номер сокета.

    Обе программы (листинги 1 и 2)
    используют описания структур и некоторых
    констант из файла ipx.h (листинг 3). В этом файле
    описаны некоторые функции, которые не
    объясняются (и не используются) в программах,
    рассмотренных в данной части статьи.

    При компиляции приведенных
    примеров надо учесть, что Ваш компилятор C может
    втихомолку вставить между полями структуры
    дополнительные байты. Это хорошо для ускорения
    работы Вашей программы, но плохо для связи с
    чужими программами. Используя IPX, надо
    блокировать такие действия. В компиляторе Microsoft C
    надо в командной строке компилятора указать
    опцию /Zp - отмена выравнивания.

    Теперь можете пытаться
    передавать сообщение. Предупреждаю только, что я
    для краткости не делал проверку того, что IPX
    действительно загружен. Если это не так,
    последствия могут быть неприятные (в лучшем
    случае - "глухое" зависание). Поэтому я
    рекомендую Вам либо самостоятельно дописать
    соответствующую часть программы, либо включить
    загрузку IPX в autoexec.bat.

    Справочное приложение

    Каждый пакет IPX состоит из заголовка и
    информационной части. Заголовок (header)
    представляет собой структуру следующего вида:

    Структура заголовка IPX

    Смещение Содержимое Тип
    0 Checksum BYTE [2]
    2 Length BYTE [2]
    4 Transport Control BYTE
    5 Packet Type BYTE

    6 Destination Network BYTE [4]
    10 Destination Node BYTE [6]
    16 Destination Socket BYTE [2]

    18 Source Network BYTE [4]
    22 Source Node BYTE [6]
    28 Source Socket BYTE [2]

    Все поля high-low, то есть наиболее
    значимый байт поля - первый. Это не соответствует
    формату процессоров Intel, для которых наиболее
    значимый последний (low-high).

    Поля имеют следующий смысл:

    Checksum (контрольная сумма)

    Это поле включено для соответствия
    оригинальному определению заголовка пакета
    Ксерокса. IPX всегда устанавливает это поле в 0xFFFF.
    Поскольку сетевые карты сами подсчитывают
    контрольную сумму пакета, оно никому не нужно.

    Это поле содержит полную длину сетевого пакета,
    которая равна длине заголовка плюс длина данных.
    Минимальная длина пакета - 30 байтов (заголовок),
    максимальная - 576 байтов (заголовок плюс данные).
    Поле заполняет IPX.

    Transport Control (управляющее слово моста)

    Это поле используется межсетевыми мостами. IPX
    всегда устанавливает его в 0 перед посылкой.

    Packet Type (тип пакета)

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

    0 - Unknown Packet Type (неизвестный тип)
    1 - Routing Information Packet (информация маршрутизации)
    2 - Echo Packet (эхо)
    3 - Error Packet (пакет обработчика ошибок)
    4 - Packet Exchange Packet (пакет пакетного обменника)
    5 - Sequenced Packet Protocol Packet (пакет протокола SPX)
    16-31 - Экспериментальные протоколы
    17 - NetWare Core Protocol (протокол ядра NetWare)

    Пользователи IPX должны устанавливать Packet Type 0 или
    4. SPX устанавливает 5 для пакетов его протокола.

    Так написано в документации фирмы Novell. Мне однако
    не удалось получить каких-либо особенностей и
    при использовании других типов. Кажется, имеют
    смысл только два типа: 5 (необходим при
    использовании SPX) и 17.

    Derstantion Network (сеть адресата)

    Это поле содержит номер сети,
    которой принадлежит адресуемый узел. В NetWare
    каждый локальный участок (Network) глобальной сети
    (Internetwork) получает от администратора сети
    уникальный 4-х байтовый номер.

    Если это поле равно 0, то NetWare подразумевает, что
    адресуемый узел находится на том же локальном
    участке сети, что и передающий, и информация не
    передается через мост.

    Destination Node (узел адресата)

    Это поле содержит физический адрес узла,
    которому передается пакет. Не все типы локальных
    сетей имеют одинаковую длину адресного поля.
    Ethernet использует для адресации шесть байтов, а
    Omninet - один. Если
    адрес занимает меньше, чем 6 байтов, то он
    размещается в последних байтах, а поле
    дополняется нулями.

    Адрес узла FF FF FF FF FF FFh (6 байтов, содержащих FF)
    служит для широковещательной (broadcast) передачи
    пакета всем узлам адресуемой локальной сети.

    Destination Socket (сокет адресата)

    Это поле содержит сокет процесса, которому
    адресовано сообщение.

    Фирма Novell поддерживает список сокетов, хорошо
    известных (well-known) во всем окружении NetWare.
    Разработчики программного обеспечения, которые
    пишут дополнительные (value-added) модули NetWare, могут
    получить назначение своих сокетов у фирмы Novell.
    Номера динамически распределяемых сокетов
    начинаются с 4000h (это отличие от стандарта Xerox).
    Номера well-known сокетов назначаются, начиная с 8000h.
    Сокеты с номерами, превышающими 8000h, не должны
    использоваться, если они не зарегистрированы для
    приложений в фирме Novell.

    Source Wetwork (исходная сеть)

    Поле содержит номер локальной сети, которой
    принадлежит исходный узел.

    Поле может содержать ноль. Это обозначает, что
    физическая локальная сеть, к которой присоединен
    источник, неизвестна.

    Source Node (узел источника)

    IPX записывает в это поле физический адрес узла
    источника.

    Source Socket (сокет источника)

    IPX записывает в это поле номер сокета процесса,
    передавшего пакет. Процессы не должны
    использовать для приема и передачи один и тот же
    сокет.

    К сожалению, я не нашел нигде определения сокета,
    используемого для передачи. Мои наблюдения
    показали, что, по-видимому, это - сокет, в котором
    последний раз ждал приема передающий процесс.
    Такая неопределенность делает довольно сложным
    выполнение требования несовпадения сокета
    приема и передачи. Я, разрабатывая свой
    транспортный протокол, обошел это требование
    тем, что для посылки всегда использую
    фиксированный номер сокета, а для приема
    подтверждения - динамически выделяемый. При
    использовании SPX проблем с "наложением"
    сокетов, по-видимому, не возникает.

    Б. Управляющий блок IPX

    Для того чтобы послать или принять пакет, Ваша
    программа должна сформировать управляющую
    структуру, называемую ECB (Event Control Block). ECB состоит
    из двух частей: 36-байтовая порция фиксированной
    длины и порция данных, длина которой зависит от
    количества буферов, ассоциированных с ECB. Ниже
    приведено изображение ECB и описание его полей.

    Структура ECB
    (ECB Structure)

    Смещение Содержимое Тип Поpядок
    0 Link Address BYTE [4] offset-segment
    4 ESR Address BYTE [4] offset-segment
    8 In Use Flag BYTE
    9 Completion Code BYTE

    10 Socket Number WORD hi-lo
    12 IPX Workspace BYTE [4]
    16 Driver Workspace BYTE [12]

    28 Immediate Address BYTE [6]
    34 Fragment Count WORD lo-hi
    38 Fragment Address 1 BYTE [4] offset-segment
    40 Fragment Size 1 WORD

    42 Fragment Address 2 BYTE [4] offset-segment
    46 Fragment Size 2 WORD

    Link Address (поле связи)

    IPX использует это поле во время работы с ECB. В
    общем случае это поле используется приложениями
    как поле связки в списке свободных ECB.

    ESR Address (адрес ESR)

    Это поле содержит адрес входной точки,
    определенной в приложении программы обработки
    события (Event Service Routine - ESR), которая вызывается IPX
    при завершении посылки или приема сообщения.
    Поскольку IPX устанавливает поля In Use Flag и Completion Code,
    приложение может просто периодически опрашивать
    эти поля для того, чтобы уловить конец операции.
    Если ESR не используется, поле ESR Address равно нулю.

    In Use Flag (флаг использования)

    Поле содержит ненулевое значение, когда IPX
    использует данный ECB. При получении ответа IPX
    пишет сюда ноль. Возможные ненулевые значения:

    FBh Событие произошло, но ECB стоит в очереди на
    обслуживание.
    FDh Событие обслуживается, а IPX ждет истечения
    временного
    интервала.
    FEh IPX ждет, пока в сокет придет пакет.
    FFh ECB используется для посылки пакета.
    FAh ECB обрабатывается IPX.
    F8h Произошла попытка передачи в то время, когда IPX
    был занят.
    Пакет и ECB направлены в очередь для последующей
    обработки.

    По приходе ответа IPX сбрасывает это поле в 0.

    Completion Code (код завершения)

    IPX использует это поле для индикации результата
    посылки или приема. Его не надо использовать,
    пока In Use Flag не установлен в ноль. Возможные
    значения будут указаны при описании функций IPX.

    Socket Number (номер сокета)

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

    IPX Workspace (рабочая область IPX)

    Это 4-байтовое поле резервируется для IPX. Оно не
    должно инициализироваться и изменяться во время
    использования ECB IPX. В остальное время может
    использоваться приложениями.

    Driver Workspace (рабочая область драйвера)

    Это 12-байтовое поле резервируется для сетевого
    драйвера. Оно не должно инициализироваться и
    изменяться во время использования ECB IPX. В
    остальное время может использоваться
    приложениями.

    Immediate Address (непосредственный адрес)

    Это поле содержит физический адрес узла адресата
    или пославшего пакет узла. В случае когда пакет
    посылается в другую локальную сеть - адрес моста.

    Перед посылкой пакета при помощи IPX приложение
    обязано заполнить это поле. SPX само заполняет его
    при установлении соединения.

    Fragment Count (количество фрагментов - буферов)

    Это поле содержит количество фрагментов, из
    которых состоит пакет (при посылке) или в которые
    он будет помещен (при приеме). Его значение должно
    быть больше нуля. Значение 1 указывает, что прием
    или передача производятся с использованием
    одного буфера.

    Fragment Descriptor (описатель фрагмента)

    Fragment Descriptor идентифицирует буфер, из которого
    берется или в который будет помещен кусок пакета.
    ECB IPX обязано иметь по крайней мере один описатель
    фрагмента (соответствующий заголовку пакета).
    Описатели фрагментов образуют список описателей
    фрагментов (Fragment Descriptor List).

    Fragment Descriptor содержит два поля:

    Address содержит адрес буфера приема/передачи.

    Size содержит величину буфера.

    Первый буфер, описанный в Fragment Descriptor List, должен
    быть не меньше 30 байтов для IPX и 42 байтов для SPX. Он
    должен вмещать полный заголовок пакета. Полная
    длина пакета (сумма величин фрагментов) не должна
    превосходить 576.

    Int (инструкция x86)

    Int — инструкция на языке ассемблера для процессора архитектуры x86, генерирующая программное прерывание.
    Синтаксис инструкции:

    где n — номер прерывания, которое будет сгенерировано. Как правило, номер прерывания записывается в виде шестнадцатеричного числа с суффиксом h (от англ. hexadecimal).

    Часть прерываний зарезервирована для команд процессора, часть — для команд операционной системы MS-DOS (команды с номерами 20h−5Fh). Например, прерывание int 21h отвечает за доступ к большинству команд MS-DOS; перед тем, как вызвать данное прерывание, в регистр процессора ah должен быть помещен номер требуемой функции.

    Int 3 [ править ]

    INT 3 — команда процессоров семейства x86, которая несёт функцию т. н. программного breakpoint, или точки останова. Исполнение команды приводит к вызову обработчика прерывания номер 3, зарезервированного для отладочных целей. В отличие от остальных команд вида INT N, которые кодируются двумя байтами, команда INT 3 кодируется только одним байтом с кодом 0xCC, хотя, конечно, двухбайтная инструкция 0xCD 0x03 тоже будет работать.

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

    Int (инструкция x86)

    Материал из Википедии — свободной энциклопедии

    Int (interrupt) — инструкция на языке ассемблера для процессора архитектуры x86, генерирующая программное прерывание.
    Синтаксис инструкции:

    где n — номер прерывания, которое будет сгенерировано. Как правило, номер прерывания записывается в виде шестнадцатеричного числа с суффиксом h (от англ. hexadecimal).

    Часть прерываний зарезервирована для команд процессора, часть — для команд операционной системы MS-DOS (команды с номерами 20h−5Fh). Например, прерывание int 21h отвечает за доступ к большинству команд MS-DOS; перед тем, как вызвать данное прерывание, в регистр процессора ah должен быть помещен номер требуемой функции.

    Int 3

    INT 3 — команда процессоров семейства x86, которая несёт функцию т. н. программного breakpoint, или точки останова. Исполнение команды приводит к вызову обработчика прерывания номер 3, зарезервированного для отладочных целей. В отличие от остальных команд вида INT N, которые кодируются двумя байтами, команда INT 3 кодируется только одним байтом с кодом 0xCC, хотя, конечно, двухбайтная инструкция 0xCD 0x03 тоже будет работать.

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

    Что такое код int86x

    Вход:
    AL:
    1 - Включить линию A20
    2 - Выключить линию A20
    3 - узнать, можно ли переключить линию A20 через порт 92h

    Выход:
    При ошибке установлен флаг переноса и AH=86
    Если в AL было переданно значение 3, то в BL==2 означает, что можно переключить линию A20 через порт 92h

    Примечание:
    Эта функция мало где описана, и, вероятно, поддерживается не всеми БИОСами

    16-разрядный режим 32-разрядный режим
    адрес mod R/M адрес mod R/M
    4Fh Внутренняя функция
    Эта функция вызывается обработчиком прерывания 9h (прерывание от клавиатуры). Точное назначение функции неизвестно. Реализация зависит от разработчиков БИОСа.
    80h Открыть устройство.
    По умолчанию, ничего не делает (зарезервированна для многозадачных ОС реального режима)

    Вход:
    BX = ID устройства
    CX = ID процесса

    81h Закрыть устройство.
    По умолчанию, ничего не делает (зарезервированна для многозадачных ОС реального режима)

    Вход:
    BX = ID устройства
    CX = ID процесса

    82h завершить программу устройства.
    По умолчанию, ничего не делает (зарезервированна для многозадачных ОС реального режима)

    Вход:
    BX = ID устройства

    83h Ждать события. Запустить таймер и установить флаг по истечении интервала.
    Только AT-совместимые компьютеры

    AL Функция
    Задать интервал и запустить таймер

    Вход:
    ES:BX - адрес байта, в котором будет установлен седьмой бит по истечении заданного времени
    CX - время ожидания в микросекундах (старшая часть)
    DX - время ожидания в микросекундах (младшая часть)

    Выход:
    AH = 83h (может так же равняться 80h, что означает неверный код функции или 86h, что означает, что функция не поддерживается)
    AL - статус (если 0, то установить таймер не удалось, т.к. уже установлен другой таймер)

    Примечание:
    На большинстве систем время ожидания округляедся до кратного 977 милисекундам
    На IBM PC/AT с биосом, датированным 1984/1/10 значение AL игнорируется, и всегда вызывается эта подфункция

    1 Отменить ожидание события

    Выход:
    AH = 83h (может так же равняться 80h, что означает неверный код функции или 86h, что означает, что функция не поддерживается)
    AL = 0

    Примечание:
    На IBM PC/AT с биосом, датированным 1984/1/10 значение AL игнорируется, и всегда вызывается подфункция 0

    84h Обеспечение джойстика (вероятно, подключённого через стандартный (и безнадёжно устаревший) игровой порт).

    Вход:
    DX = код подфункции:

    DX
    Читать текущие переключатели

    Выход:
    AL = значения переключателей (если равно нулю, то джойстик не найден)

    1 Читать входы X,Y джойстика

    Выход:
    AX = джойстик 'A', значение X (горизонталь)
    BX = джойстик 'A', значение Y (вертикаль)
    CX = джойстик 'B', значение X (горизонталь)
    DX = джойстик 'B', значение Y (вертикаль)
    Если джойстик не найден, то AX=BX=CX=DX=0

    85h клавиша SysReq.вызывается прерыванием INT 09H при нажатии и отпускании клавиши SysReq.
    Только AT-совместимые компьютеры
    По умолчанию, ничего не делает (зарезервированна для многозадачных ОС реального режима)

    Вход:
    AL = 0: SysReq нажата ('make')
    AL = 1: SysReq отпущена ('break')

    86h Ждать.
    Только AT-совместимые компьютеры
    Циклит с разрешенными прерываниями до истечения интервала времени.

    Вход:
    CX - время ожидания в микросекундах (старшая часть)
    DX - время ожидания в микросекундах (младшая часть)

    Примечание:
    На большинстве систем время ожидания округляедся до кратного 977 милисекундам
    Не реккомендуется указывать в качестве задержки значения меньше 1000 микросекунд (3e8h)

    87h Переслать блок extended-памяти.
    Только AT-совместимые компьютеры с процессором 286 и выше

    Вход:
    CX = размер блока в словах (максимум 8000h т.е. 64 кб.)
    ES:SI = GDT
    db 16 dup (0) 16 пустых байтов
    dw ? размер источника в байтах (2*[сх]+1)
    dw ? младшая часть адреса источника
    db ? старшая часть адреса источника
    db 93h разрешены чтение и запись
    dw 0 пустое (резервное) слово
    dw ? размер приемника в байтах (2*[сх]+1)
    dw ? младшая часть адреса приемника
    db ? старшая часть адреса приемника
    db 93h разрешены чтение и запись
    dw 0 пустое (резервное) слово
    db 16 dup (0) 16 пустых байтов

    Выход:
    AH - статус:
    0 - успешно
    1 - ошибка памяти
    2 - во время переключения возникло исключение.
    3 - переключение линии A20 не удалось
    80h - неиизвестная команда
    86h - неизвестная функция

    Примечание:
    Прерывания могут быть запрещены в процессе перемещения данных.
    Спецификация Embedded BIOS 4.1 утверждает, что байты 8-10h в GDT должны корректно указывать на GDT, т.е. должны быть заполнены аналогично частям источника (10h-18h) и приёмника(18h-20h)

    88h Размер extended-памяти.(не более 64Мб)
    Только AT-совместимые компьютеры

    Выход:
    AX = число непрерывных 1Kб блоков сверх 1Mб

    89h Войти в режим Protected (286 Virtual memory). это полезная штука, если вы не знаете, что это за зверь - Protected Mode i286.

    Вход:
    ES:SI => 8-элементная таблица глобальных дексрипторов (GDT) (ниже)
    BH = смещение в IDT начала первых 8 аппаратных прерываний (IRQ)
    BL = смещение в IDT начала вторых 8 аппаратных прерываний (IRQ)

    Выход:
    AH - статус:
    0 - успешно
    1 - ошибка памяти
    2 - во время переключения возникло исключение.
    3 - переключение линии A20 не удалось
    80h - неиизвестная команда
    86h - неизвестная функция
    Если не было ошибок, то все сегментные регистры изменяются. AX и BP теряются. 80286 входит в режим Protected без прямого выхода в режим Real.

    Примечание:
    Cтруктура GDT:
    ES:[SI]+00h фиктивный дескриптор (все нули)
    ES:[SI]+08h дескриптор этой GDT (инициализирует пользователь)
    ES:[SI]+10h дескриптор для IDT (инициализирует пользователь)
    ES:[SI]+18h дескриптор для caller DS (инициализирует пользователь)
    ES:[SI]+20h дескриптор для caller ES (инициализирует пользователь)
    ES:[SI]+28h дескриптор для caller SS (инициализирует пользователь)
    ES:[SI]+30h дескриптор для caller CS (инициализирует пользователь)
    ES:[SI]+38h временный BIOS CS (все нули)

    Структура Дескриптора:
    dw ? - размер в байтах
    dw ? - младшая часть адреса начала
    db ? - старшая часть адреса начала
    db ? - права доступа:
    Биты 0-3 : Тип сегмента:
    Для системных десткрипторов:
    0 - резерв
    1 - свободный 16-битный TSS
    2 - LDT
    3 - занятый 16-битный TSS
    4 - 16-битный шлюз вызова
    5 - Шлюз задачи
    6 - 16-битный шлюз прерывания
    7 - 16-битный шлюз ловушки
    8 - резерв
    9 - свободный 32-битный TSS (i386+)
    10 - резерв
    11 - занятый 32-битный TSS (i386+)
    12 - 32-битный шлюз вызова (i386+)
    13 - резерв
    14 - 32-битный шлюз прерывания (i386+)
    15 - 32-битный шлюз ловушки (i386+)
    Для десткрипторов приложений:
    бит 0 - было обращение к сегменту
    бит 1 - для дескриптора данных: доступ на запись, для дескриптора кода: доступ на чтение
    бит 2 - для дескриптора данных: запись с конца, для дескриптора кода: conforming
    бит 3 = 0 : данные; = 1 : код
    Биты 4 : Тип дескриптора (установлен -> для приложений; сброшен -> системный)
    Биты 5-6 : Уровень привилегий
    Биты 7 : Установлен -> сегмент находится в оперативной памяти
    db ? - на i286 резерв, на i386+ расширенные права доступа: Биты 0-3 : старшие 4 бита лимита сегмента
    Биты 4 : доступность
    Биты 5 : резерв
    Биты 6 : 16-битный, если сброшен, 32-битный если установлен
    Биты 7 : размерность: блоками по 4Кб, если установлен; байтами, если сброшен
    db ? - на i286 резерв, на i386+ старшие 8 бит начала сегмента

    90h Цикл занятости устройства.
    Только AT-совместимые компьютеры
    По умолчанию, ничего не делает (зарезервированна для многозадачных ОС реального режима)

    Вход:
    AL = код типа устройства
    00h = твердый диск
    01h = дискета
    02h = клавиатура
    03h = PS/2 мыш
    21h = ожидание ввода с клавиатуры Работает не везде
    80h = сеть
    FBh = звук Работает не везде
    FCh = перезапуск диска Работает не везде
    FDh = запуск мотора дискеты
    FEh = принтер
    ES:BX => управляющий блок для кодов 80h - BFh (по иным сведеньям 80h - FFh)

    Примечание:
    Здесь описанны вход и выход, которые должен ожидать обработчик этой функции.
    Установленный флаг CF должен означать, что истёкло время ожидания

    91h завершить прерывание.
    Только AT-совместимые компьютеры
    По умолчанию, ничего не делает (зарезервированна для многозадачных ОС реального режима)

    Вход:
    AL = код типа устройства (см. подфункцию 90H)
    = 00h - 7Fh: последовательно используемые устройства
    = 80h - BFh: реентерабельные устройства (ES:BX => пакет данных)
    = C0h - FFh: не доступны через эту функцию

    Примечание:
    Здесь описанны вход и выход, которые должен ожидать обработчик этой функции.

    C0h Адрес таблицы конфигурации (SCT)

    Выход:
    ES:BX = адрес таблицы конфигурации, таблица находится в ПЗУ BIOS;
    CF = 0 и AH = 0 при успешном вызове прерывания;
    CF = 1 и AH=86h или AH=80h если данная версия BIOS не поддерживает функцию C0h.

    Примечание:
    формат таблицы:

    Смещение Размер Описание
    00h 2 байта размер таблицы за исключением этого поля
    02h 1 байт код модели компьютера
    03h 1 байт суб-код модели компьютера
    04h 1 байт номер ревизии BIOS минус 1
    05h 1 байт
    бит Описание
    В системе установлена двойная шина (ISA + Micro Chanel)
    1 В системе установлена шина Micro Chanel, если установлен, иначе - ISA
    2 Биос имеет дополнительную область данных (EBDA)
    3 Функция 41h прерывания 15h поддерживается
    4 Прерывание 09h вызывает функцию 4Fh прерывания 15h
    5 Присутствуют часы реального времени (RTC)
    6 Установлен второй контроллер прерывания 8259 (в режиме каскадирования через IRQ 2, т.е. стандартная конфигурация)
    7 Третий канал DMA занят BIOS
    06h 1 байт
    бит Описание
    Резерв
    1 Поддерживается потоковая передача данных
    2 Используется НЕ 8042 контроллер клавиатуры
    3 Функция C8h прерывания 15h поддерживается
    4 Функция C7h прерывания 15h поддерживается
    5 Функция C6h прерывания 15h поддерживается
    6 Функция 09 прерывания 16h поддерживается
    Некорректно устанавливается некоторыми БИОС
    7 Присутствует 32-х битный контроллер DMA
    07h 1 байт
    бит Описание
    SCSI поддерживается в IML
    1 IML система
    2 Подключена информационная панель
    3 Присутствует SCSI контроллер
    4 POST поддерживает включение\выключение ROM-TO-RAM
    5 Резерв
    6 Резерв
    7 Резерв
    08h 1 байт
    бит Описание
    POST поддерживает POSTEXT
    1 система поддерживает разделение памяти на отметке 16Mб
    2 Резерв
    3-5 001 - ABIOS НЕ поддерживается
    010 - ABIOS поддерживается из ROM
    011 - ABIOS поддерживается, но необходима загрузка в RAM.
    111 - ABIOS присутствует
    6 В системе присутствует EEPROM
    7 (занято IBM)
    09h 1 байт
    бит Описание
    flash EPROM BIOS
    1 Система имеет расширеный режим мыши.
    4-2 Резерв
    5-7 (занято IBM)
    0Ah - далее область данных, смысл которых зависит от производителя.

    Некоторые биосы используют не все байты возможностей. К примеру, в Phoenix BIOS 4 rev. 6 User manual байты со смещёнием 7 и 8 отмечены как зарезервированные.

    C1h Получить сегмент дополнительной области данных BIOS (EBDA)

    Выход:
    Если CF установлен: ошибка (AH=80h или AH=86h - данная функция не поддерживается)
    Если CF сброшен: ES - сегмент EBDA

    C2h BIOS - интерфейс указывающего устройства (мышь)

    AL Функция
    Включить/Выключить

    Вход:
    BH = 00 - выключить
    = 01 - включить

    Выход:
    Если CF установлен: ошибка
    Значения AH в этом случае:
    01h - функция не поддерживается
    02h - неверный параметр
    03h - ошибка интерфейса PS/2
    04h - необходимо повторить операцию
    05h - отсутствует драйвер мыши
    80h или 86h - данная функция не поддерживается
    Если CF сброшен: успех, AH установлен в ноль.

    1 Сбросить мышь

    Выход:
    Если CF установлен: ошибка
    Значения AH в этом случае:
    01h - функция не поддерживается
    03h - ошибка интерфейса PS/2
    04h - необходимо повторить операцию
    05h - отсутствует драйвер мыши
    80h или 86h - данная функция не поддерживается
    Если CF сброшен: успех, AH установлен в ноль.
    BH = ID устройства(обычно нуль)
    BL = код, возврашаемый устройством при перенастройке. (AAh - большинство PS/2 мышей)

    Примечания:
    Параметры по умолчанию:
    Выключено
    Частота - 100 Гц
    Чуствительность - 4 отсечки на миллиметр
    Масштаб 1:1
    Рекомендуется вызывать при завершении программы если использовались какие-либо из подфункций функции C2h
    На некоторых BIOS возврат BL не предусмотрен

    2 Установить частоту

    Вход:
    BH - чатота:
    0 - 10 Гц
    1 - 20 Гц
    2 - 40 Гц
    3 - 60 Гц
    4 - 80 Гц
    5 - 100 Гц
    6 - 200 Гц

    Выход:
    Если CF установлен: ошибка
    Значения AH в этом случае:
    01h - функция не поддерживается
    03h - ошибка интерфейса PS/2
    04h - необходимо повторить операцию
    05h - отсутствует драйвер мыши
    80h или 86h - данная функция не поддерживается
    Если CF сброшен: успех, AH установлен в ноль.

    3 Установить чуствительность

    Вход:
    BH - чуствительность:
    00 - одна отсечка на милиметр
    01 - две отсечки на милиметр
    02 - 4 отсечки на милиметр
    03 - 8 отсечек на милиметр

    Выход:
    Если CF установлен: ошибка
    Значения AH в этом случае:
    01h - функция не поддерживается
    03h - ошибка интерфейса PS/2
    04h - необходимо повторить операцию
    05h - отсутствует драйвер мыши
    80h или 86h - данная функция не поддерживается
    Если CF сброшен: успех, AH установлен в ноль.

    04 Получить ID

    Выход:
    Если CF установлен: ошибка
    Значения AH в этом случае:
    01h - функция не поддерживается
    03h - ошибка интерфейса PS/2
    04h - необходимо повторить операцию
    05h - отсутствует драйвер мыши
    80h или 86h - данная функция не поддерживается
    Если CF сброшен: успех, AH установлен в ноль BH = ID устройства

    05 Инициализация

    Вход:
    BH - размер пакета данных (от 1 до 8, в байтах)

    Выход:
    Если CF установлен: ошибка
    Значения AH в этом случае:
    01h - функция не поддерживается
    03h - ошибка интерфейса PS/2
    04h - необходимо повторить операцию
    05h - отсутствует драйвер мыши
    80h или 86h - данная функция не поддерживается
    Если CF сброшен: успех, AH установлен в ноль.

    Примечания:
    Параметры устанавливаются следующим образом:
    Выключено
    Частота - 100 Гц
    Чуствительность - 4 отсечки на миллиметр
    Масштаб 1:1

    06 Дополнительные возможности

    BH Подфункция
    Получить параметры устройства

    Выход:
    BL - статус:
    бит 0 - нажата правая клавиша
    бит 1 - нажата средняя клавиша //не факт, что это действительно так, сам не проверял. В RBIL утверждается, что это поле зарезервированно
    бит 2 - нажата левая клавиша
    бит 3 - резерв
    бит 4 - масштаб 0 - 1:1, 1 - 2:1 (в некоторых источниках указано строго наоборот, что верно - не знаю)
    бит 5 - состояние 0 - выключено, 1 - включено
    бит 6 - 0 - потоковый режим, 1 - удалённый (remote) режим
    бит 7 - резерв
    CL - чуствительность:
    00 - одна отсечка на милиметр
    01 - две отсечки на милиметр
    02 - 4 отсечки на милиметр
    03 - 8 отсечек на милиметр
    DL - частота:
    0Ah - 10 Гц
    14h - 20 Гц
    28h - 40 Гц
    3Ch - 60 Гц
    50h - 80 Гц
    64h - 100 Гц
    C8h - 200 Гц

    02 Установить масштаб 1:1
    03 Установить масштаб 2:1

    Выход:
    Если CF установлен: ошибка
    Значения AH в этом случае:
    01h - функция не поддерживается
    03h - ошибка интерфейса PS/2
    04h - необходимо повторить операцию
    05h - отсутствует драйвер мыши
    80h или 86h - данная функция не поддерживается
    Если CF сброшен: успех, AH установлен в ноль.

    07 Установить свой обработчик событий

    Вход:
    ES:BX - адрес обработчика (если адрес установлн в ноль, то испольуется стандартный обработчик)

    Выход:
    Если CF установлен: ошибка
    Значения AH в этом случае:
    01h - функция не поддерживается
    03h - ошибка интерфейса PS/2
    04h - необходимо повторить операцию
    05h - отсутствует драйвер мыши
    80h или 86h - данная функция не поддерживается
    Если CF сброшен: успех, AH установлен в ноль.

    Примечания:
    Параметры передаются через стек:
    Слово 1 - статус:
    бит 0 - нажата левая клавиша
    бит 1 - нажата правая клавиша
    бит 2 - нажата средняя клавиша //не факт, что это действительно так, сам не проверял. В RBIL утверждается, что это поле зарезервированно
    бит 3 - резерв
    бит 4 - знак по оси X
    бит 5 - знак по оси Y
    бит 6 - переполнение по оси X
    бит 7 - переполнение по оси Y
    бит 8-16 - резерв // Возможно, знак и переполнение по оси Z при её наличии?
    Слово 2 - старший байт - знак, младший - координата X
    Слово 3 - старший байт - знак, младший - координата Y
    Слово 4 - обычно ноль, для но в случае если подключена трехкоординатная мышь используется для передачи координаты Z по аналогии со словами 2 и 3

    08 Отправить байт на указывающее устройство
    Вход:
    BL - отправляемый байт

    Примечания:
    Поддерживается не всеми БИОС.

    09 Получить данные с указывающего устройства
    Выход:
    BL - Первый считанный байт CL - Второй считанный байт DL - Третий считанный байт

    Примечания:
    Поддерживается не всеми БИОС.

    Примечания:
    Для проверки наличия этой функции пользуйтесь функцией C0h.

    Сервис памяти

    AH Функция
    88h Размер "расширенной" памяти

    Выход:
    AX - количество килобайт, выше 1 мб.
    CF установлен, если произошла ошибка
    Примечание:
    Часто эта функция исскуственно ограничена границей в 16мб
    Многие БИОСы неправильно устанавливают флаг CF, в связи с чем обычно реккомендуется следующий способ проверить наличие этой функции:
    безусловно, при таком подходе мы теряем часть случаев, когда функция исправно работает, зато дойдя до моента обработки результата мы уверены, что функция отработала верно.

8Ah Размер памяти свыше 64Мб

Выход:
AX - младшая часть размера памяти
DX - старшая часть размера памяти
Флаг CF установлен, если функция не поддерживается

Примечание:
Эта функция возвращает количество блоков по 1Кб свыше 64Мб
В некоторых источника эта функция описывается как аналог функции DA88h. Вероятно, разные производители БИОСов реализуют её по-разному, в связи с чем не рекомендуется её использовать.

C7h Карта памяти свыше 1Мб

Вход:
CS:DI - адрес, куда будет помещана карта памяти
Флаг CF установлен при ошибке

Примечание:
Не все БИОСы поддерживают эту функцию
Рекомендуется проверять наличие этой функции с помощью функции C0h
Карта имеет следующий формат:

Смещение Размер Описание
WORD Размер таблицы, за исключением этого слова
2 DWORD Количество килобайт логической памяти между 1Мб и 16Мб
6 DWORD Количество килобайт логической памяти между 16Мб и 4Гб
Ah DWORD Количество килобайт системной памяти между 1Мб и 16Мб
Eh DWORD Количество килобайт системной памяти между 16Мб и 4Гб
12h DWORD Количество килобайт кэшируемой памяти между 1Мб и 16Мб
16h DWORD Количество килобайт кэшируемой памяти между 16Мб и 4Гб
1Ah DWORD Количество килобайт до начала не системной памяти между 1Мб и 16Мб
1Eh DWORD Количество килобайт до начала не системной памяти между 16Мб и 4Гб
22h WORD Начальный сегмент наибольшего блока памяти в промежутке C0000h-DFFFFh
24h WORD Размер наибольшего блока памяти в промежутке C0000h-DFFFFh
26h DWORD Резерв
DAh Размер памяти от 1мб до 16мб

Выход:
AX=0
CL, BX - количество килобайт доступной памяти в промежутке от 1мб до 16мб (старшая часть в CL)

Примечание:
Если произошла ошибка - установлен флаг переноса (CF)
В большинстве случаев эта функция вернёт размер примерно 14 мб, т.к. в промежутке от 1 мб до 16 мб есть облать, используемая БИОСом.

E8h Память

AL функция
01h размер extended-памяти.

Выход:
ax = cx - размер сконфигурированный расширенной памяти в килобайтах в промежутке от 1Мб до 16Мб
bx = dx - размер сконфигурированной расширенной памяти в блоках по 64Кб свыше 16Мб
Если произошла ошибка - установлен флаг переноса (CF)

20h Карта памяти разбитая на блоки разных типов. допускает дыры в памяти.

Вход:
edx = 534d4150h ('SMAP');
ebx - Смещение от начала карты памяти(0 - начать с начала)
eсx - Размер буфера
es:di - Адрес буфера для размещения карты памяти

Выход:
eax - 534d4150h ('SMAP')
ebx - Следующее смещение от начала карты памяти, если = 0, то вся карта передана
ecx - Количество возвращенных байт

Примечание:
Эта функция не включает информацию о области памяти, адресуемой в графеческую карту

81h Размер extended-памяти.

Выход:
eax = ecx - размер сконфигурированный расширенной памяти в килобайтах в промежутке от 1Мб до 16Мб
ebx = edx - размер сконфигурированной расширенной памяти в блоках по 64Кб свыше 16Мб
Если произошла ошибка - установлен флаг переноса (CF)

APM API

Advanced Power Managment (APM) - это старый механизм управления электропитанием компьютера
Этот апи реализуется через функцию 53h, т.е. в AH необходимо передавать значение 53h

AL Функция
00h Проверить наличие APM

Выход:
Если установлен флаг переноса, то APM не поддерживается и AH - статус.
Иначе:
AH - старший номер версии (в коде BCD)
AL - младший номер версии (в коде BCD)
BX = 504Dh
CX :
бит 0 : 16-ти битный интерфейс поддерживается
бит 1 : 32-х битный интерфейс поддерживается
бит 2 : Вызов CPU IDLE снижает скорость процессора
бит 3 : APM выключено (disabled)
бит 3 : APM отключено (disengaged)

Примечание:
Некоторые старые версии Award BIOS не изменяют значение BX
Установленный бит 2 регистра CX означает, что если был вызов CPU IDLE, а сейчас системе требуется полная производительность, то необходимо сделать вызов CPU BUSY.

01h Подключиться к интерфейсу реального режима

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.

Примечание:
По умолчанию после установки соединения APM работает в режиме совместимости с версией 1.0. Что бы переключится на более новую версию используйте функцию 0Eh.

04h Отключиться от интерфейса

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.

05h CPU IDLE

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.

06h CPU BUSY

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.

Примечание:
Эта функция гарантирует, что после её выполнения процессор будет работать на полной мощности. Её можно вызывать, даже если процессор уже работает на полной мощности

07h Задать состояние

Вход:
BX - ID устройства
CX - состояние

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.

Примечание:
Не вызывайте эту функцию из обработчика аппаратного прерывания

Определены следующие > 0001h - все управляемые устройства
01XXh - видеокарты/дисплеи
02XXh - устройства хранения данных
03XXh - порты LPT
04XXh - порты COM
05XXh - сетевый адаптеры (APM версии 1.1 или выше)
06XXh - разъёмы PCMCIA (APM версии 1.1 или выше)
80XXh - аккамуляторы (поддерживается не везде) (счёт устройств начинается с единицы) (APM версии 1.2 или выше)
EXXXh - определяется производителем системы
на некоторых системах вместо ID 0001h используется ID 0000h. Это применимо не только к этой функции, но и ко всем функциям, использующим ID устройства.
Определены следующие состояния:
0000h - готовность (не работает с ID устройства 0001h). Устройство работает на полной мощности
0001h - ожидание. Устрпойство находится в режиме пониженного энергопотребления
0002h - пауза. Устройство выключено, но его состояния сохранено.
0003h - выключено.
0004h - обработка запроса на выход из состояний паузы или ожидания (только для устройства с ID 0001h) (APM версии 1.1 или выше)
0005h - отмена запроса на выход из состояний паузы или ожидания (только для устройства с ID 0001h) (APM версии 1.1 или выше) (имеется ввиду, что этот вызов отменяет запрос)
0020h-007Fh - определяется производителем системы

по стандарту версии 1.0 состояние 0003h не применимо к устройству с ID 0001h, но большинство систем обрабатывали такую ситуацию как выключение компьютера. Такое поведение было объявлено стандартным в версии 1.2, хотя упоминание о не пременимости состояния 0003h к устройству с ID 0001h было убрано с версии 1.1

08h Включить/выключить (enable/disable) управление питанием

Вход:
BX = 0001h (APM версии 1.1 или больше) FFFFh (APM версии 1.0)
CX = 0 - выключить, = 1 - включить

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.

Примечание:
APM не может быть одновременно выключено (disabled) и отключено (disengaged)
Не вызывайте эту функцию из обработчика аппаратного прерывания

09h Востановить значения, используемые при включении компьютера

Вход:
BX = 0001h (APM версии 1.1 или больше) FFFFh (APM версии 1.0)

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.

Примечание:
В результате APM для устройств будет отключено (disengaged), однако для системы в целом будет включено (engaged)
Не вызывайте эту функцию из обработчика аппаратного прерывания

0Ah Получить статус питания

Вход:
BX - ID устройства

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.
иначе:
BH - статус разёма питания:
00h - питание отключено
01h - питание подключено
02h - подключено резервное питание
FFh - неизвестно
BL - статус заряда аккамулятора:
00h - высокий
01h - низкий
02h - критический
03h - заряжается
FFh - неизвестно
CH - флаги аккамулятора (APM версии 1.1 или выше):
бит 0 : высокий уровень заряда
бит 1 : низкий уровень заряда
бит 2 : критический уровень заряда
бит 3 : аккамулятор заряжается
бит 4 : такого аккамулятора не существует
биты 5,6 зарезервированны (сброшены)
бит 7 : батарей нет
CL - уровень заряда аккамулятора от 0 до 100, FFh = неизвестно
DX - оставшееся время работы от аккамулятора (APM версии 1.1 или выше):
бит 15 : если установлен, то в минутах; если сброшен, то в секундах
биты 14-0 : оставшееся время в указанных единицах
если на входе ID устройства соответствовал аккамулятору, то:
SI - количество установленных в системе аккамуляторов (APM версии 1.2 или выше)

Примечание:
В качестве ID устройства может передаваться ID всей системы (0001h) или же ID аккамулятора
В результате DX равный FFFFh означает "неизвестно"
В результате CH равный FFFFh означает "неизвестно"
Не вызывайте эту функцию из обработчика аппаратного прерывания

0Bh Получить событие питания

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.
иначе:
BX - код события
CX - информация о событии (APM версии 1.2 или выше) (для событий 0003h и 0004h):
бит 0 : разъёмы PCMCIA были выключены в состоянии паузы
остальные биты зарезервированны

Примечание:
Коды событий:
0001h - запрос на переход в режим ожидания
0002h - запрос на переход в режим паузы
0003h - нормальное возобновление системы
0004h - критическое возобновление системы
0005h - низкий уровень заряда батареи
0006h - изменилось состояние питания (APM версии 1.1 или выше)
0007h - изменилось время (APM версии 1.1 или выше)
0008h - критический переход в режим паузы (APM версии 1.1 или выше)
0009h - запрос на переход в режим ожидания от пользователя (APM версии 1.1 или выше)
000Ah - запрос на переход в режим паузы от пользователя (APM версии 1.1 или выше)
000Bh - выход из режима ожидания (APM версии 1.1 или выше)
000Ch - изменение возможностей (APM версии 1.2 или выше)
01XXh - события оборудования
02XXh - события, определяемые производителем

Не вызывайте эту функцию из обработчика аппаратного прерывания

0Сh Получить состояние питания (APM версии 1.1 или выше)

Вход:
BX - ID устройства

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.
иначе:
CX - состояние:
0000h - готовность (не работает с ID устройства 0001h). Устройство работает на полной мощности
0001h - ожидание. Устрпойство находится в режиме пониженного энергопотребления
0002h - пауза. Устройство выключено, но его состояния сохранено.
0003h - выключено.
0020h-007Fh - определяется производителем системы

0Dh Включить/выключить автоматическое управление электропитанием устройства (APM версии 1.1 или выше)

Вход:
BX - ID устройства
CX = 0 - выключить, = 1 включить

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.

0Eh Задать версию APM (APM версии 1.1 или выше)

Вход:
BX = 0
CH - старший номер версии APM, поддерживаемой драйвером (в коде BCD)
CL - младший номер версии APM, поддерживаемой драйвером (в коде BCD)

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.
иначе:
CH - старший номер версии APM, используемый для текущего соединения (в коде BCD)
CL - младший номер версии APM, используемый для текущего соединения (в коде BCD)

Примечание:
Эту функцию необходимо вызывать после установки соединения функцией 01 для того, что бы использовать возможности новых версий APM. (По умолчанию APM эмулирует поведение версии 1.0)

0Fh Включить/отключить (engage/disengage) управление питанием (APM версии 1.1 или выше)

Вход:
BX - ID устройства
CX = 0 - выключить, = 1 включить

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.

Примечание:
Когда управление электропитанием отключено (disengaged), то APM автоматически управляет электропитанием устройств. Когда оно включено, то в управлении электропитанием участвует драйвер.

10h Узнать возможности (APM версии 1.2 или выше)

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.
иначе:
BL - количество установленных в системе аккамуляторов
CX - флаги возможностей:
бит 0 : может войти в глобальный режим ожидания
бит 1 : может войти в глобальный режим паузы
бит 2 : таймер пробуждения выведет систему из состояния ожидания
бит 3 : таймер пробуждения выведет систему из состояния паузы
бит 4 : сигнал на линии ring com-порта или модема выведет систему из состояния ожидания
бит 5 : сигнал на линии ring com-порта или модема выведет систему из состояния паузы
бит 6 : сигнал на линии ring PCMCIA выведет систему из состояния ожидания
бит 7 : сигнал на линии ring PCMCIA выведет систему из состояния паузы
биты 8-15 : зарезервированно

Примечание:
Эта функция не требует наличия установленного соединения

11h Таймер пробуждения (APM версии 1.2 или выше)

Вход:
BX = 0
CL :
00 - Выключить таймер
01 - Узнать настройку таймера
02 - Задать настройку таймера
Если CL = 02:
CH - секунда (в коде BCD)
DH - час (в коде BCD)
DL - минута (в коде BCD)
SI - в старшем байте месяц (в коде BCD), в младшем байте день (в коде BCD)
DI - год (в коде BCD)

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.
иначе, если на входе CL = 01:
CH - секунда (в коде BCD)
DH - час (в коде BCD)
DL - минута (в коде BCD)
SI - в старшем байте месяц (в коде BCD), в младшем байте день (в коде BCD)
DI - год (в коде BCD)

Примечание:
Значение таймера задаётся в абсолютном времени, а не относительном

12h Включить/выключить пробуждение системы по сигналу на линии ring (APM версии 1.2 или выше)

Вход:
BX = 0
CL :
00 - Выключить пробуждение по сигналу на линии ring
01 - Включить пробуждение по сигналу на линии ring
02 - Узнать статус пробуждения по сигналу на линии ring

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.
иначе:
CX - статус пробуждения по сигналу на линии ring:
00 - Выключено
01 - Включено

Примечание:
Эта функция задаёт реакцию на все линии ring, способные пробудить систему

13h Включить/выключить запросы по таймеру (APM версии 1.2 или выше)

Вход:
BX = 0
CL :
00 - Выключить запросы по таймеру
01 - Включить запросы по таймеру
02 - Узнать статус запросов по таймеру

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.
иначе:
CX - статус запросов по таймеру:
00 - Выключено
01 - Включено

Примечание:
По умолчанию APM может предложить перейти врежим ожидания или паузы, основываясь на времени бездействия системы. Данная функция позволяет управлять этим поведением

80h Определить наличия расширения от изготовителя системы

Выход:
Если установлен флаг переноса, то произошла ошибка и AH - статус.
иначе:
BX - ID изготовителя

Примечание:
Нередко также изменяются значения регистров AX и CX
Вызов функций расширения осуществляется через эту же функцию, а номер необходимой функции передаётся в BH

Коды статуса APM

Код Значение
01 Управление электропитанием выключено (disabled)
02 Соединение уже установлено
03 Соединение не установлено
04 Соединение с интерфейсом реального режима не установлено
05 Соединение с интерфейсом 16-ти битного (i286) защищённого режима уже установлено
06 Интерфейс 16-ти битного (i286) защищённого режима не поддерживается
07 Соединение с интерфейсом 32-х битного защищённого режима уже установлено
08 Интерфейс 32-х битного защищённого режима не поддерживается
09 Неизвестный ID устройства
0Ah Неверное значение в CX
0Bh (APM версии 1.1 или выше) Интерфейс не включён (engaged)
0Ch (APM версии 1.2 или выше) Функция не поддерживается
0Dh (APM версии 1.2 или выше) Таймер пробуждения выключен
60h Не могу перейти в запрошенный режим
80h Нет событий управления электропитанием для обработки
86h APM отсутствует

Прочие функции

AH Функция
04h Создать таблицу системных параметров ABIOS

Выход:
CF установлен - ошибка, иначе - успешно. 05h Создать таблицу инициализации ABIOS

Выход:
CF установлен - ошибка, иначе - успешно. 22h Получить расположение ROM BASIC

Выход:
CF установлен - ошибка (если AH=86h, то функция не поддерживается)
Если AX=0000, то ES:BX - расположение ROM BASIC в памяти. 52h Извлечь носитель информации

Вход:
DL - номер диска(80h . 0FFh)

Выход:
AH - состояние дисковой подсистемы
Если флаг переноса установлен - произошла ошибка

Программирование процессоров Intel x86 в защищённом режиме
Защита: передача управления

Автор: sergh
The RSDN Group


Версия текста: 1.0

Время пришло! Это замк о вая глава, она объединяет все предыдущие, сводит в цельную работающую систему и закрывает большую часть «хвостов».

Тема сложная и неоднородная – не существует одной единственной ниточки, потянув за которою можно было бы последовательно и логично описать процесс передачи управления в защищённом режиме. Приходится подходить с разных сторон, потом как-то соединять вместе полученные результаты… Причём эта последовательность выполняется два раза подряд.

Не огорчайтесь, если вам придётся прочитать эту главу пару раз. Представьте себе, сколько времени я её писал и сколько раз за это время перечитывал соответствующие места [Intel 2004] :)

[Первая сторона] Что мы знаем о защищённой ОС

Немного подумав над главой «Теоретическое введение в защиту», а заодно и над всеми предыдущими, можно построить упрощённую модель гипотетической защищённой ОС.

Структура

Приблизительная структура ОС изображёна на Рисунке 1 (красным обозначены дескрипторы GDT и IDT, к которым приложение не имеет доступа). Правда, пока неясно, зачем нужен Синий Сегмент Состояния Задачи, но интуиция уже подсказывает, что без него тут не обойтись…

Рисунок 1. Структура защищённой ОС.

Более-менее то же самое, с небольшими поясняющими комментариями, описывает Таблица 1.

Дескриптор… DPL Результат
… сегмента, содержащего код ядра ОС. Ядро ОС имеет все необходимые ему привилегии.
… сегмента, содержащего стек ядра ОС. К стеку имеет доступ только ядро.
… сегмента, содержащего данные ядра ОС. К данным имеет доступ только ядро.
… сегмента, содержащего код пользовательского приложения. 3 Приложение не имеет никаких лишних возможностей.
… сегмента, содержащего стек пользовательского приложения. 3 В качестве стека такой сегмент может использоваться только пользовательским приложением. Но поскольку любой стек это, прежде всего, сегмент данных, ядро тоже имеет к нему доступ.
… сегмента, содержащего данные пользовательского приложения. 3 Приложение имеет доступ к данным. И ядро, при необходимости, тоже.
… шлюза вызова, запрещённого для использования из пользовательских приложений (на картинке нет). При попытке приложения выполнить инструкцию call с соответствующим селектором, будет сгенерировано исключение #GP.
… шлюза вызова, разрешённого для использования из пользовательских приложений (на картинке нет). 3 Приложения могут использовать соответствующие системные функции.
… шлюза ловушки/прерывания, запрещённого для «явного» вызова из пользовательских приложений. При попытке приложения выполнить инструкцию int n с соответствующим номером, будет сгенерировано исключение #GP. Относится так же к инструкциям int 3 и into . На обработку аппаратных прерываний и исключений этот механизм не влияет, они будут обработаны независимо от уровня DPL.
… шлюза ловушки/прерывания, разрешённого для «явного» вызова из пользовательских приложений. 3 Приложения могут использовать соответствующие прерывания.
… сегмента состояния задачи. Любой Об этом ниже.
Таблица 1. Структура защищённой ОС.

Схема работы

Схема работы такой ОС достаточно очевидна (многозадачности нет ):

  • Инициализация
  • Запуск приложения
  • Завершение

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

Рисунок 2. Схема работы защищённой ОС

Поправки к модели

Существенные упрощения, внесённые в модель сознательно:

  • Ровно один сегмент состояния задачи, не используется никаких дополнительных «задачных» дескрипторов. Это тема главы «Многозадачность».
  • Даже имея только одну задачу, можно реализовать «самодельную» многозадачность. Хранить список процессов, по таймеру вручную переключать контексты… В этом случае возникает не только вопрос защиты ОС от приложения, но и вопрос защиты различных приложений друг от друга. Вариант решения предложен в главе «Страничная адресация», а пока будем считать, что у нас одно приложение, пусть и большое.
  • Обращение к портам ввода-вывода запрещёно для всех, кроме ядра ОС (нужен нулевой уровень привилегий). Будет исправлено в главе «Защита: ввод-вывод».

Несущественные упрощения, по разным причинам проникшие в модель или в её изображение:

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

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

Что нужно такой ОС от механизма передачи управления

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

  • При запуске ОС из кода ядра можно передать управление коду пользовательского режима.
  • При вызове системной функции, возникновении исключения, программного или аппаратного прерывания управление передаётся обработчику, находящемуся в ядре.
  • При возврате из системной функции, обработчика исключения, программного или аппаратного прерывания управление передаётся в точку вызова или на прерванную инструкцию.
  • А завершение реализуем через какое-нибудь программное прерывание, как в DOS, чтобы не плодить сущностей.

Теперь подумаем о программистах, писавших ядро и приложение:

  • Внутри себя приложение может передавать управление без ограничений.
  • Внутри себя ядро может передавать управление без ограничений.

И, наконец, вспомним о безопасности:

  • Код пользовательского режима может вызывать только явно предназначенные для этого функции кода ядра, но не может передать управление в произвольно выбранное место кода ядра.
  • Проверки на доступ к сегменту происходят при загрузке сегментного регистра. Код пользовательского режима не должен иметь возможности получить доступ к данным режима ядра через сегментные регистры, значения которых были загружены в режиме ядра.
  • Код ядра и код пользовательского режима должны пользоваться различными стеками (причина появления этого требования описана в главе «Теоретическое введение в защиту»), при передаче управления должно происходить переключение стека (stack switching).

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

[Вторая сторона] Классификация способов передачи управления

  • jmp , jxx (условные переходы: je , jc и т.п.)
  • call
  • int
  • ret , retf
  • iret , iretd
  • Генерация исключения (любая инструкция, вызывающая генерацию исключения: деление на 0, неизвестный код команды, что угодно ещё).
  • Внешнее прерывание

ПРИМЕЧАНИЕ

Обычно инструкции ret / retf / iret / iretd используются для возврата из функции или обработчика прерывания. Во-первых, естественно, это тоже «передача управления». Во-вторых, предварительно подготовив стек, эти инструкции можно использовать гораздо шире.

Существует ещё один, глубоко противоестественный, способ передачи управления, использующий страничную адресацию. При этом стандартная защита отдыхает, но в данном случае это не проблема, так как без нулевого уровня привилегий всё равно работать не будет. Идея способа поражает воображение, но практическая применимость сильно ограничена, подробнее – в главе «Страничная адресация».

По целевому сегменту кода:

  • Ближняя передача управления, происходит внутри сегмента кода, целевой сегмент явно не указан ( jxx , ret , ближние call и jmp ).
  • DPL целевого сегмента кода совпадает с CPL.
  • DPL целевого сегмента кода отличается от CPL.

ПРИМЕЧАНИЕ

Если рассматривать подчиненные сегменты кода, добавится ещё один пункт.

По участвующим дескрипторам:

  • Ни одного. Ближняя передача управления.
  • Напрямую, участвует дескриптор целевого сегмента кода. Стандартные jmp или call с адресом вида : , инструкции retf , iret , iretd .
  • Через шлюз ловушки/прерывания/вызова, участвует дескриптор шлюза и дескриптор целевого сегмента кода. Исключение, внешнее прерывание, инструкции int , call , jmp .

Объединяем

Попытаемся наложить требования защищённой ОС на рассмотренные выше варианты передачи управления. В таблице 2 перечислены все основные требования ОС, а в соседнем столбце дана расшифровка, приближенная к реалиям процессоров Intel x86.

ПРИМЕЧАНИЕ

Напоминаю: чем больше DPL, тем меньше привилегий. Соответственно, сегмент кода с большим DPL менее привилегирован, чем сегмент кода с меньшим DPL. Сегменты кода ядра имеют наименьший DPL, сегменты кода пользовательского приложения – наибольший.

# Требования Что это означает на практике
1 При запуске ОС из кода ядра можно передать управление коду пользовательского режима. Существует способ передачи управления от сегмента кода с меньшим DPL к сегменту кода с большим DPL. Не важно какой :)
2 При вызове системной функции, возникновении исключения, программного или аппаратного прерывания управление передаётся обработчику, находящемуся в ядре. Через шлюз можно передать управление из сегмента кода с большим DPL в сегмент кода с меньшим DPL (из приложения в ядро) и между сегментами кода с одинаковым DPL (из ядра в ядро).
3 При возврате из системной функции, обработчика исключения, программного или аппаратного прерывания управление передаётся в точку вызова или на прерванную инструкцию. Из сегмента кода с меньшим DPL, можно передать управление в сегмент кода с большим DPL инструкциями iretd и retf . И между сегментами кода с одинаковым DPL – тоже.
4 Внутри себя приложение может передавать управление без ограничений. Внутри себя ядро может передавать управление без ограничений. Ближняя передача управления (внутри сегмента кода) разрешена всегда, любыми способами. Передача управления напрямую (без дескрипторов шлюза) между сегментами кода с одинаковым уровнем привилегий разрешена всегда, любыми способами.
5 Код пользовательского режима может вызывать только явно предназначенные для этого функции кода ядра, но не может передать управление в произвольно выбранное место кода ядра. Из сегмента кода с большим DPL, невозможно напрямую передать управление в сегмент кода с меньшим DPL.
6 Проверки на доступ к сегменту происходят при загрузке сегментного регистра. Код пользовательского режима не должен иметь возможности получить доступ к данным режима ядра через сегментные регистры, значения которых были загружены в режиме ядра При изменении уровня привилегий в сторону уменьшения (в сторону увеличения DPL), сбрасываются значения сегментных регистров, которые не могли быть получены на новом уровне привилегий.
7 Код ядра и код пользовательского режима должны пользоваться различными стеками, при передаче управления должно происходить переключение стека. Код с различным уровнем привилегий должен использовать разные сегменты стека, при передаче управления с изменением уровня привилегий стек должен переключаться. Как это реализуется – разберёмся чуть позже.
Таблица 2. Требования и их практическое значение

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

Требование (4)

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

Таким образом, всегда разрешены:

  • Ближняя передача управления, происходит внутри сегмента кода. jxx , ret , ближние call и jmp .
  • Прямая передача управления между сегментами кода, если DPL целевого сегмента кода совпадает с CPL. Стандартные jmp или call с адресом вида : , инструкции retf , iret , iretd .

Требование (2)

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

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

Из главы «Теоретическое введение в защиту» уже известно, что:

  • Установив дескриптору шлюза высокий DPL можно сделать его доступным из пользовательских приложений.
  • При этом шлюз будет доступен и для кода с DPL меньше DPL шлюза (для ядра).
  • А при обработке исключений и аппаратных прерываний доступность шлюза для текущего уровня привилегий не проверяется.

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

Требование (3)

После обработки прерывания/исключения необходимо вернуться назад. Для x86 это означает возможность прямой передачи управления от более привилегированного сегмента кода к менее привилегированному инструкциями iretd и retf . Причём передачи в любую точку, не обязательно к месту вызова, достаточно изменить в стеке адрес возврата.

Это, кстати, и есть способ, который был нужен в Требовании (1).

Требование (5)

Соображения безопасности запрещают прямую передачу из менее привилегированного сегмента в более привилегированный – только через шлюз. Помимо прочего, запрещена прямая передача управления инструкциями iret , iretd и retf . То есть, если вызвать менее привилегированный код через шлюз (например, если поместить в менее привилегированный сегмент кода обработчик прерывания), обработчик не сможет стандартным способом вернуть управление.

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

Требование (6)

Изменение уровня привилегий в сторону уменьшения привилегий – возврат из обработчика исключения/прерывания инструкцией iretd и из системной функции инструкцией retf . При этом система проверяет регистры DS, ES, FS, GS, те из них, которые содержат селекторы слишком привилегированных сегментов (то есть сегментов, DPL которых меньше нового CPL), сбрасываются.

Итого

  • С – текущий уровень привилегий (current, текущий). Это не совсем CPL, читайте дальше :)
  • T – уровень привилегий сегмента, куда происходит передаче (target, целевой)
  • G – уровень привилегий шлюза, через который происходит передача (gate)
  • Соотношения вида «C > T» или «С > G» сравнивают не значения DPL, а именно уровни привилегий. То есть, к примеру, условие «C > T» выполняется, если текущий уровень привилегий выше, чем уровень привилегий целевого сегмента.
  • DPL(x) – значение поля DPL в дескрипторе сегмента кода или шлюза
  • CPL – текущий уровень привилегий в числовой форме. Если C > T, то CPL ПРИМЕЧАНИЕ

На мой взгляд, гораздо удобнее разбираться с таблицей, ориентируясь на непосредственное сравнение уровней привилегий, а не на DPL, но, на всякий случай, столбец с DPL в таблицу тоже включён.

Способ передачи управления Соотношения уровней привилегий Вердикт Соотношения DPL
Любой В одном сегменте, шлюз не используется можно --
Любой C == T, шлюз не используется можно CPL == DPL(T)
call, jmp C != T, шлюз не используется #GP CPL != DPL(T)
retf, iretd, iret C > T, шлюз не используется можно CPL DPL(T)
jmp C == T, C >= G можно CPL == DPL(T), CPL DPL(G)
jmp C != T, независимо от G #GP CPL != DPL(T)
call, int n C = G можно CPL >= DPL(T), CPL = DPL(T), CPL > DPL(G)
call, int n C > T, независимо от G #GP CPL = DPL(T)
Исключение C > T, независимо от G #GP CPL = DPL(T)
Внешнее прерывание C > T, независимо от G #GP CPL Таблица 3. «Можно/нельзя».

Что не попало в таблицу:

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

Несмотря на несколько «умозрительный» способ построения таблицы, она удивительным образом совпала с правильным вариантом :) Единственное, в ней не упомянуты RPL, которые иногда всё-таки оказываются важны.

На этом завершим слегка затянувшееся введение, и начнём уже двигаться вперёд.

[Первая сторона] Передача управления через шлюз

Для начала рассмотрим шлюз вызова. С точки зрения защиты, в процессе участвуют следующие параметры:

  1. Текущий уровень привилегий (CPL)
  2. RPL селектора в инструкции call
  3. DPL шлюза
  4. RPL селектора из дескриптора шлюза
  5. DPL целевого сегмента кода

Все они обозначены и пронумерованы на Рисунке 3.

Рисунок 3. Параметры защиты, участвующие в передаче управления через шлюз вызова

Условия успешной передачи управления (все условия должны выполняться одновременно):

  • CPL = DPL целевого сегмента

То есть, текущий уровень привилегий (с учётом RPL) должен оказаться достаточным для того, чтобы воспользоваться шлюзом, но при этом он (без учёта RPL) не должен превосходить уровень привилегий кода вызываемой функции. Если что-то не так – исключение #GP.

А как же параметр (4) – RPL селектора из дескриптора шлюза? А он просто игнорируется :)

ПРИМЕЧАНИЕ

При условии, что дескрипторы создаются системой один раз при инициализации, это довольно разумно. Так как, если этот RPL запрещает вызов, через шлюз передать управление не сможет никто – зачем такой шлюз? Значит, если шлюз есть, передача разрешена, а значит RPL можно не проверять :) Другое дело, если бы дескрипторы генерировались динамически… Но такой подход к проектированию ОС разработчики процессора, видимо, не рассматривали.

Со шлюзами ловушки/прерывания всё точно так же, только проще, так как параметров меньше.

  1. Текущий уровень привилегий (CPL)
  2. DPL шлюза
  3. RPL селектора целевого сегмента из дескриптора шлюза (он опять не понадобится)
  4. DPL целевого сегмента кода

Смотрите рисунок 4.

Рисунок 4. Параметры защиты, участвующие в передаче управления через шлюз ловушки/прерывания

Ну и условия успешной передачи управления (все условия должны выполняться одновременно):

  • CPL = DPL целевого сегмента

То есть, то же самое, что у шлюза вызова, но без RPL.

[Вторая сторона] Переключение стека

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

  • Проверки допустимости передачи управления.
  • Обнаруживается, что нужно изменить уровень привилегий.
  • Откуда-то получается селектор сегмента стека и смещение стека для нового уровня привилегий. Эти значения устанавливаются регистрам SS и ESP.
  • Если новый стек должен что-то содержать (адрес возврата, параметры), он заполняется.
  • Собственно передача управления, изменение уровня привилегий.

Основной вопрос, конечно, где же то «откуда-то», которое содержит данные о подходящем стеке. Тут есть два варианта.

Туда (call, int, исключение, внешнее прерывание)

При передаче управления с повышением уровня привилегий, источником данных о стеке служит сегмент состояния задачи (TSS). В главе «Введение в многозадачность: одна задача» он был описан несколько неподробно, в таблице 4 и на рисунке 5 приведено более детальное описание.

Смещение Содержимое
4 4 байта – значение смещения стека нулевого уровня привилегий.
8 2 байта – значение селектора стека нулевого уровня привилегий.
12 4 байта – значение смещения стека первого уровня привилегий.
16 2 байта – значение селектора стека первого уровня привилегий.
20 4 байта – значение смещения стека второго уровня привилегий.
24 2 байта – значение селектора стека второго уровня привилегий.
28…104
Таблица 4. Структура сегмента состояния задачи, второе приближение.

Рисунок 5. Структура сегмента состояния задачи, второе приближение

Описание соответствующей структуры на ассемблере:

Что ещё нужно сказать:

  • Бросается в глаза отсутствие данных для стека третьего уровня привилегий. Так и должно быть, коду с третьим уровнем привилегий невозможно передать управление «с повышением уровня привилегий». А для передачи управления «с понижением» предназначен другой механизм.
  • Проверка корректности селектора и смещения происходит только при их использовании. Значит, если какой-то уровень привилегий в системе не используется, в соответствующие места сегмента состояния задачи можно записать нули.
  • RPL селекторов важен! Он должен точно совпадать с уровнем привилегий, для которого предназначен соответствующий стек.

Наконец, более-менее точный сценарий:

  • Проверки допустимости передачи управления.
  • Обнаруживается, что нужно изменить уровень привилегий.
  • Процессор считывает из регистра TR селектор TSS. В TSS находит нужный селектор сегмента стека и смещение, проверяет на корректность. Текущие значения SS и ESP на время сохраняются во внутренних регистрах, регистрам SS и ESP присваиваются значения из TSS. В случае каких-либо ошибок на этом этапе, генерируется исключение #TS (Invalid TSS).
  • [call] В новый стек последовательно копируются: старый SS, старый ESP, параметры из старого стека, CS, EIP. Количество копируемых параметров берётся из дескриптора шлюза вызова. На каждый регистр и на каждый параметр уходит по 4 байта.
  • [int, исключение, внешнее прерывание] В новый стек последовательно копируются: старый SS, старый ESP, EFLAGS, CS, EIP. На каждый регистр уходит по 4 байта.
  • Собственно передача управления, изменение уровня привилегий.

На рисунке 6 показано состояние старого и нового стеков сразу же после вызова функции с двумя параметрами.

Рисунок 6. Два стека (на картинке младшие адреса сверху, т.е. стеки растут вверх).

Стек обработчика исключения/прерывания выглядит точно так же, только вместо параметров он будет содержать EFLAGS.

Обратно (retf, iret, iretd)

Передача управления с понижением уровня привилегий – фактически возврат из функции или обработчика, данные о старом стеке, так же как и данные об адресе, извлекаются из стека. Подразумевается, что, либо перед этим была выполнена корректная передача «с повышением», либо стек был подготовлен вручную.

  • Проверки допустимости передачи управления.
  • Обнаруживается, что нужно изменить уровень привилегий (DPL сегмента, селектором которого является CS из стека, не совпадает с CPL).
  • Из этого делается вывод, что в стеке должны содержаться дополнительные данные – SS и ESP старого стека.
  • [retf] Эти данные расположены в стеке глубже параметров. Поэтому при вызове через шлюз стек должен всегда освобождаться вызываемой функцией.
  • Данные считываются, выполняются различные проверки их корректности.
  • Собственно передача управления, изменение уровня привилегий, переключение стеков, сбрасывание значений сегментных регистров, содержащих селекторы слишком привилегированных сегментов.
  • [retf] При переключении, параметры автоматически удаляются и из старого стека тоже.

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

ПРЕДУПРЕЖДЕНИЕ

RPL селектора стека, RPL селектора сегмента кода, DPL сегмента стека и DPL сегмента кода должны совпадать между собой, иначе всё это закончится генерацией исключения #GP.

All together now

Наконец, объединим вместе всё, что нам известно про передачу управления, защиту и стеки.

Туда-и-обратно

Рассмотрим законченный сценарий, пусть это будет программное прерывание.

  1. Пользовательский код вызывает int 21h
  2. Проверка попадания номер прерывания в IDT, проверка корректности дескриптора, проверка допустимости вызова с точки зрения безопасности – DPL дескриптора шлюза не меньше CPL. В случае каких-то проблем – исключение #GP.
  3. Из дескриптора извлекается селектор и смещение. Проверяется, что селектор попадает в GDT, что это селектор сегмента кода, что смещение в него попадает. Проверка допустимости вызова с точки зрения безопасности – DPL дескриптора сегмента кода не больше CPL. В случае каких-то проблем – исключение #GP.
  4. Пусть DPL целевого сегмента кода не равен CPL
  5. Процессор считывает из регистра TR селектор TSS. В TSS находит нужный селектор сегмента стека и смещение, проверяет на корректность. Текущие значения SS и ESP на время сохраняются во внутренних регистрах, регистрам SS и ESP присваиваются значения из TSS. В случае каких-либо ошибок на этом этапе, генерируется исключение #TS (Invalid TSS).
  6. В новый стек последовательно копируются: старый SS, старый ESP, EFLAGS, CS, EIP. На каждый регистр уходит по 4 байта.
  7. CS и EIP загружаются новыми значениями, изменяется CPL.
  8. Начинается выполнение обработчика …
  9. Завершается. Обработчик возвращает управление инструкцией iretd .
  10. Проверка корректности CS и EIP из стека: попадает в GDT, соответствует сегменту кода, RPL селектора из стека равен DPL сегмента. В случае каких-то проблем – исключение #GP.
  11. Пусть DPL сохранённого CS по прежнему не равен CPL (при желании, обработчик мог всё там поменять).
  12. Из стека считывается значение ESP и SS, проверяются на корректность (попадание в GDT, соответствие доступному для записи сегменту данных) и на правильность уровней привилегий (должен совпадать с RPL и с DPL сегмента кода). В случае каких-то проблем – исключение #GP.
  13. В регистры CS, EIP, EFLAGS, SS, ESP записываются новые значения, проверяются регистры DS, ES, FS, GS, те из них, которые содержат селекторы сегментов, недоступные на новом уровне привилегий, сбрасываются.

К сожалению, мне не удалось изобразить всю последовательность на картинке, поместилась только работа со стеками. Передача управления при обработке программного прерывания была более-менее рассмотрена в главе «Программные прерывания» и не слишком сильно изменилась с тех пор.

Рисунок 7. Переключение стеков при обработке программного прерывания

Сценарии для исключения, внешнего прерывания и вызова функции через call почти ничем не отличаются, поэтому не приводятся полностью. Отличия, о которых всё же стоит упомянуть:

  • В случае исключения и внешнего прерывания не происходит проверки CPL call и шлюза вызова, добавляется проверка RPL, копирование параметров между стеками в начале и очистка стеков в конце.

Переключение режимов: инициализация TSS

Последний раз алгоритмы переключения режимов упоминались в главе «Программные прерывания», с тех пор мы узнали много нового, пора их обновить. Из реального режима в защищённый:

  1. Запретить маскируемые и немаскируемые прерывания.
  2. Инициализировать GDT и загрузить её адрес в GDTR.
  3. Инициализировать IDT и загрузить её адрес в IDTR.
  4. Установить флаг PE (младший бит регистра CR0).
  5. Выполнить дальний переход ( jmp или call ) для перезагрузки регистра CS.
  6. Перезагрузить все сегментные регистры.
  7. Установить значение регистру задачи. TSS, на который ссылается это регистр должен содержать данные о стеках для используемых уровней привилегий.
  8. Разрешить прерывания.

  1. Сделать текущим сегментом кода какой-либо доступный для чтения сегмент с пределом FFFFh байт.
  2. Загрузить во все сегментные регистры селекторы дескрипторов доступных для записи сегментов данных с пределом FFFFh.
  3. Сбросить флаг PE
  4. Выполнить дальний переход ( jmp или call ).
  5. Перезагрузить сегментные регистры.
  6. Восстановить в IDTR значение для реального режима.
  7. Разрешить прерывания.

В общем, если не считать всяких необязательных деталей (страничная адресация и LDT, но они действительно необязательны – если не используются, их можно не инициализировать), это полные, правильные алгоритмы, соответствующие написанному в [Intel 2004]. Не прошло и девяти глав :)

Использование RPL

В главе «Теоретическое введение в защиту» был рассмотрен единственный разумный способ использования RPL – сознательное понижение уровня привилегий системной функцией при обращении по переданному пользователем указателю. Там же было обещано, что в недалёком будущем будет описан способ, как это проделать технически. Будущее наступило :)

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

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

Уровень привилегий вызывающего кода можно получить из сохранённого в стеке адреса возврата (конкретно – из RPL регистра CS). А для модификации RPL селектора по приведённым выше правилам предназначена инструкция arpl (аббревиатура для Adjust RPL, то есть «подгонка» или «настройка» RPL):

Здесь source это селектор с «образцовым» RPL, а dest – селектор, RPL которого будет модифицирован. Если в результате исполнения arpl операнд dest был изменён, устанавливается флаг ZF, если нет – сбрасывается. dest может быть 16-и разрядным регистром или областью памяти, source – всегда 16-и разрядный регистр.

ПРИМЕЧАНИЕ

Инструкция arpl не относится к привилегированным и может вызываться из пользовательского режима. При попытке использовать arpl из реального режима, процессор генерирует исключение #UD – неизвестный код операции.

ПРЕДУПРЕЖДЕНИЕ

С этой инструкцией дело нечисто.

Во-первых, в Microsoft Visual C++ 6.0, 2003, 2005 есть уникальная ошибка: если в исходном коде встречается инструкция arpl x, y , то в бинарном файле она превратится в arpl y, x (незаметно для вас поменяется порядок операндов).

Во-вторых, в VMWare 5.0.0.build-13124 arpl ведёт себя ещё более странно – она не просто меняет второй операнд (видимо, писали на VC), но меняет его неправильно (а это уже самостоятельно привнесённое): вместо изменения младших битов просто копируется значение первого операнда.

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

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

  • lar (Load Address Rights)
  • verr (Verify Right Read)
  • verw (Verify Right Write)
  • lsl (Load Segment Limit)

Они подробно описаны в [Intel 2004].

ПРИМЕЧАНИЕ

Скорее всего, эти инструкции применяются примерно так же часто, как arpl , со всеми вытекающими :) Так что если они вам вдруг потребуются, готовьтесь к сюрпризам.

Пример


Изменение уровня привилегий

Что делает программа:

  • Переключается в защищённый режим
  • Передаёт управление коду с третьим уровнем привилегий
  • Этот код два раза вызывает системную функцию, реализованную на нулевом уровне привилегий, передаёт ей параметр. Первый раз функция возвращает управление, на второй раз она переключает процессор в реальный режим и завершает программу.

Схема работы изображена на рисунке 8.

Рисунок 8. Схема работы примера.

Как она это делает:

  • В GDT находятся следующие дескрипторы: шлюза вызова (та самая системная функция), сегмента состояния задачи, сегмента кода с нулевым уровнем привилегий, сегмента стека для него, сегмента кода с третьим уровнем привилегий, сегмента стека для него, сегмента данных, сегмента видеопамяти, сегмента стека для переключения в реальный режим (см. про него ниже).
  • При старте всё это инициализируется.
  • После чего переключается режим.
  • Текущее значение SS и ESP сохраняется до момента переключения обратно в реальный режим, в качестве новых значений прописываются селектор стека для нулевого уровня привилегий и размер выделенной под него памяти.
  • Стек «подготавливается» для инструкции retf , в качестве адреса возврата указывается начало «пользовательского» кода.
  • Исполняется инструкция retf , управление передаётся пользовательскому коду. При этом происходит изменение уровня привилегий с нулевого на третий, со всеми сопутствующими эффектами: переключением стеков и сбросом значений сегментных регистров.
  • Пользовательский код кладёт в стек параметр и вызывает системную функцию инструкцией call .
  • При этом происходит изменение уровня привилегий с третьего на нулевой и, естественно, переключение стеков.
  • Системная функция имеет четыре варианта поведения и выбирает один из них, в зависимости от значения параметра. В двух случаях она что-то (не важно, что) делает с экраном, в «остальных» случаях сразу возвращает управление, а в последнем, самом интересном случае, переключает процессор в реальный режим и завершает работу программы.
  • Последний случай реализован так – функция ничего не делает сама, а просто передаёт управление коду деинициализации защищённого режима.
  • За исключением одной особенности, код деинициализации полностью стандартен. Особенность связана с сегментами стека и описана ниже.

На что ещё стоит обратить внимание (при написании примера мне было сложнее всего найти ошибки именно в этих деталях):

  • На RPL селекторов.
  • На сегментный регистр ES, значение которого каждый раз заново устанавливается в системной функции, вместо того, чтобы один раз установить при инициализации. DPL сегмента видеопамяти – 0, поэтому при изменении уровня привилегий значение ES сбрасывается.
  • Код деинициализации начинается с установки сегмента стека, корректного с точки зрения реального режима – размером 64 Кб. Это связано с той же особенностью x86, которая позволяет организовать Unreal mode – процессор сохраняет значения флагов и предела сегмента при переключении режимов. В данном случае это могло привести к тому, что в реальном режиме размер сегмента стека оказался бы 128 байт, после чего DOS отказался бы работать. Давным-давно, в главе «Управление памятью: сегменты и дескрипторы» было замечание на эту тему, с упором именно на сегменты стека, но, скорее всего, вы про него уже забыли.

Надеюсь, после таких пояснений, проблем с пониманием примера не возникнет.

Задания

Разные способы нарушения работы программы:

  • Поменяйте DPL стека нулевого уровня
  • Поменяйте DPL шлюза вызова.
  • При переходе к пользовательскому коду, не кладите в стек информацию о стеке с третьим уровнем привилегий. Таким образом вы убедитесь, что она нужна. Верните всё обратно, но поменяйте RPL селекторов.
  • Не вызывайте инструкцию ltr .
  • Поменяйте RPL селектора стека нулевого уровня в TSS
  • Загружайте регистр ES селектором сегмента видеопамяти в начале программы, уберите повторную инициализацию из call_handler, убедитесь, что значение регистра сбрасывается. Исправьте программу так, чтобы этот вариант работал (смену уровней привилегий в программе сохраните!).
  • Уберите инициализацию DS после переключения в защищённый режим. Убедитесь, что программа падает. Измените программу так, чтобы не падала (опять же, смену привилегий сохраните).
  • Попробуйте завершать выполнение пользовательского кода не вызовом системной функции, а передачей управления в точку back_to_rm.
  • Попробуйте возвращать управление из call_handler обычной инструкцией retf 4 (без префикса), инструкцией retf (32-х разрядной и 16-и разрядной).

  • Поменяйте DPL в дескрипторе TSS. Убедитесь, что он никому не интересен.
  • Поменяйте RPL селектора сегмента кода в дескрипторе шлюза вызова. Убедитесь, что он тоже никому не интересен.
  • Выведите из пользовательского кода значения регистров CS и SS (для этого проще всего разрешить пользователям доступ к видеопамяти).
  • Выведите из call_handler дамп стека и значения регистров CS и SS.
  • Разбейте call_handler на две функции: draw_handler (с одним параметром) и exit_handler (без параметров), причём draw_handler должна работать не на нулевом, а на первом уровне привилегий. Поменяйте процесс инициализации и права доступа, чтобы всё это работало.
  • Переделайте первоначальную передачу управления пользовательскому коду так, чтобы вместо retf использовалась инструкция iretd . Перед выполнением iretd сбросьте 14-й бит (нумеруем от 0) в регистре флагов, об этом флаге читайте в главе «Многозадачность».
  • Перепишите программу так, чтобы вместо шлюза вызова и инструкции call использовался шлюз ловушки и int 21h . Параметр передавайте в регистре.
  • А теперь – не прерывание, а исключение. Не забудьте изменять адрес возврата в стеке, заодно убедитесь, что это всё ещё возможно. Убедитесь, что при обработке исключения DPL шлюза не играет роли.
  • Попытайтесь через шлюз передать управление с понижением уровня привилегий. Попробуйте разные варианты – вызов, программное прерывание, исключение, внешнее прерывание.

Илон Маск рекомендует:  Что такое код free
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL