Jah love, Jah love - protect us Vincent Ford (performed by Bob Marley) Ты должен быть сильным, Ты должен уметь сказать «Руки прочь, прочь от меня». Ты должен быть сильным, Иначе зачем тебе быть. Виктор Цой Нет защиты, кроме любви. Борис Гребенщиков
Если вы успешно дочитали курс до этой главы, то, во-первых, вас можно и нужно поздравить! Поздравляю! Во-вторых, скорее всего, вы уже понимаете, зачем нужна подсистема защиты – в каждой из предыдущих глав были ссылки приблизительно следующего вида: «чтобы всё было хорошо, пользователь не должен иметь возможность …, подробнее этот вопрос рассмотрен в главах, посвящённых защите». Что ж, пришло время выполнять обещания. Но сначала, как обычно, ещё немного о «зачем».
- Ты хочешь поговорить об этом? Собирательный образ психолога
Как вы могли заметить, все описанные до сих пор возможности более-менее повторяют реальный режим: память есть и там, прерывания и исключения – тоже. Защита – первое и решающее отличие. Да, я хочу поговорить об этом :)
Не случайно режим называется «Protected» – защищённый. Не Advanced, не Extended, не Improved, и даже не ++ или #, а именно Protected. Защита – его соль и суть. Ради неё всё и затевалось, именно она склеивает воедино всё остальное, без неё не было бы ни современных ОС, ни современных приложений. И это притом, что защита не даёт каких-то невероятных новых возможностей, наоборот, она отнимает многие старые. Нелогично? Ну, например, можно провести аналогию с переходом от «макаронного» кода, изобилующего goto, к структурному программированию. Или с ассемблера на язык высокого уровня. Только наличие правильно заданных ограничений позволяет системе оставаться управляемой и стабильной, и, как следствие, развиваться и расти дальше.
СОВЕТ А теперь подумайте о своей жизни, как о разрабатываемой вами сложной системе. А о морали, нравственности, порядочности – как о тех самых правильных ограничениях, без которых невозможно развитие. Подумали? Действуйте! Извиняюсь за отступление от темы, мысль слишком важна. |
«Старый» подход упёрся в свой потолок: при появлении многозадачности, графических интерфейсов и т.п., система «компьютер + ОС + запущенное ПО» становится слишком сложной, утверждение «программисты умные, всё будет в порядке» перестаёт работать, и, если других гарантий надёжности нет, всё начинает падать с удручающим постоянством.
ПРИМЕЧАНИЕ Хотя бы потому, что в понятие «программисты» теперь входит слишком много людей: несколько сотен на ОС, по двое-трое-десятеро на каждое приложение. Вы правда думаете, что они все умные? Ну так я вас огорчу :) |
Для примера, допустим, что вы всё-таки написали многозадачную ОС реального режима:
ПРИМЕЧАНИЕ Те, кто писал резиденты под DOS, поняли, что я имею в виду. Остальным объясняю: 1. Есть стандартный обработчик прерывания 2. Запускается Программа1, которой это прерывание нужно. Она залезает в таблицу векторов прерываний и меняет стандартный обработчик на свой. Как правильная, вежливая программа, она запоминает адрес старого обработчика. Во-первых, этот адрес надо будет восстановить, когда Программа1 будет выгружаться, во-вторых, можно вызывать этот обработчик из своего, для того чтобы не мешать работе стандартных механизмов. 3. Запускается Программа2, которой тоже нужно это же прерывание. Она проделывает ту же операцию, но поскольку стандартный обработчик уже подменён, она запоминает адрес обработчика Программы1. 4. Если обе программы вызывают «старые» обработчики, вся цепочка может корректно работать. Если нет, будет вызываться только наиболее новый. 5. При выгрузке, программы попытаются восстановить в таблице векторов прерываний запомненные адреса обработчиков. И вот тут всё зависит от очерёдности. Если первой будет выгружаться Программа1, она запишет в таблицу адрес стандартного обработчика. А Программа2 – адрес обработчика из уже выгруженной Программы1. 6. И в этот момент ваша ОС накроется медным тазом. Конечно, такая ситуация могла возникнуть и в DOS, существует и классическое решение – если программа видит, что в таблице адрес не её обработчика, то есть после неё кто-то уже перехватил это же прерывание, она ничего не восстанавливает и не выгружается. Но вы же понимаете, что так будут поступать далеко не все… |
Согласитесь, так жить нельзя. Очевидны две проблемы:
Классическим решением проблемы сложности является абстракция, в данном случае это отделение ОС от пользовательских приложений, и самих пользовательских приложений друг от друга. Кроме того, введение уровня абстракции над системными механизмами позволяет реализовать принудительный контроль ресурсов. В общем-то, этим и занимается защита – отделяет. В нашем случае это понижает сложность и одновременно повышает устойчивость (в грамотно написанной ОС приложение можно почти безболезненно выгрузить).
Так что защита в защищённом режиме играет одну из ключевых ролей. Пытаться абстрагироваться можно и без неё… Но только работать это не будет.
Описанный в предыдущих главах механизм управления памятью тоже относится к «необходимым ограничениям». Проверки существования дескриптора, типа дескриптора, размеров, разрешения чтения/записи, дают большую структурированность, в частности становится невозможно писать по случайному адресу. И в общем-то всё это тоже относится к защите, в том числе и с точки зрения Intel. Согласно терминологии Intel, система защиты в целом называется «protection» и все перечисленные выше проверки относятся к «просто protection» (это не термин). Но есть еще один раздел системы защиты, называемый privilege level protection (а это термин, он довольно неуклюже переводится на русский как «защита по привилегиям»), и именно ему посвящена эта и следующие главы.
Если проводить аналогии, то «просто protection» это стены, крыша и дверь дома, а privilege level protection – возможность запереть дверь на ключ. Если дверь не запирать, дом прекрасно защищён от ветра, снега и дождя, но практически не защищён от человека. С другой стороны, «возможность запереть дверь» сама по себе не существует и появляется только тогда, когда дом и дверь уже есть. Это некоторая дополнительная надстройка над существующими проверками, отделяющая тех, у кого есть ключ, от тех, у кого ключа нет. И, конечно, если у дома разваливаются стены, даже запертая дверь плохо поможет (в качестве примера: по непроверенным слухам, в ОС семейства Windows 9x IDT находится в области памяти, доступной на запись для обычных приложений).
Но спустимся с небес к нашим баранам. Начнём со списка потребностей в защите, которые «всплыли» в предыдущих главах.
Обобщая, получаем следующее:
ПРИМЕЧАНИЕ Вопросу защиты портов ввода-вывода посвящена отдельная глава «Защита: ввод-вывод», здесь эта тема только несколько раз упоминается. |
Как вы помните, дескрипторы сегментов и дескрипторы шлюзов довольно сильно отличаются по формату. Единственное, что объединяет форматы абсолютно всех дескрипторов, это формат пятого байта (напоминаю, нумеруем байты от нуля). Рассмотрим его подробнее.
Положение | Название | Описание |
---|---|---|
Биты 0 – 3 | Type | Уточняет тип дескриптора (флаги в дескрипторах сегмента кода/данных, номер в системных дескрипторах). |
Бит 4 | S | Определяет тип дескриптора – системный или сегмента кода/данных. |
Биты 5 – 6 | DPL | Descriptor Privilege Level. Это ОНО! Именно на это поле обратите своё внимание, во многом ему посвящена глава. Описано ниже. |
Бит 7 | P | Для завершённости – флаг Segment Present. В рамках курса всегда установлен в 1, больше нигде не упоминается, подробно рассмотрен в приложении. |
На картинке:
С точки зрения защиты, ключевое поле – DPL, Descriptor Privilege Level. Название переводится как «уровень привилегий дескриптора», и очень точно отражает предназначение поля.
Царь-царевич, Король-королевич, Сапожник, портной. Кто ты будешь такой? Считалка
Процессоры Intel x86 поддерживают четыре уровня привилегий.
ПРЕДУПРЕЖДЕНИЕ Обратите внимание: меньшее численное значение соответствует большему уровню привилегий. Во избежание двусмысленности, в скобках будет уточняться, что имеется в виду. Например: «… минимальный (численно максимальный) уровень ..». |
Под разными названиями, поле, содержащее уровень привилегий, встречается в нескольких системных структурах. Смыслы тоже разные, хотя и близкие.
Читатель ждёт уж рифму «розы»; На вот, возьми её скорей! Александр Сергеевич Пушкин
Самое известное изображение уровней привилегий.
Все просмотренные мною источники по защищённому режиму содержат эту картинку (кроме [Зубков 1999], но у него вообще ни одной картинки нет). Я не знаю зачем, видимо так сложилось по каким-то историческим причинам. Скорее всего, где-нибудь в 1982 году, при создании 80286 неизвестный автор рекламной брошюры Intel попытался таким образом донести свою мысль до покупателей. Видимо, брошюра пользовалась успехом, и с тех пор это изображение так и кочует из документа в документ.
Помимо картинки обычно вводится соответствующая терминология:
Конечно, ни картинка, ни терминология ничего не объясняют, только вводят новые сущности. Тем не менее, эта терминология очень широко распространена, знать её нужно, собственно ради неё и сделана эта врезка. Ну и чтобы не нарушать традицию :)
DPL это пятый и шестой биты пятого байта дескриптора. Для разных дескрипторов DPL имеет разный смысл, по этому признаку все дескрипторы можно разбить на три класса и одно исключение из правил.
Значение DPL это уровень привилегий, которыми обладает находящийся в сегменте код. Именно этим отличается ОС и пользовательские приложения: код ОС находится в сегментах с нулевым DPL и обладает максимальным (нулевым) уровнем привилегий, код приложений – минимальным (третьим).
ПРИМЕЧАНИЕ Передаче управления между сегментами кода и куче связанных с этим проблем посвящена глава «Защита: передача управления». |
Кроме дескрипторов «обычного» сегмента кода, существуют дескрипторы подчинённого (conforming) сегмента кода. Их описание вынесено в приложение.
В поле DPL хранится минимальный (численно максимальный) уровень привилегий, необходимый для доступа к ресурсу. Если к ресурсу обращается код с недостаточными привилегиями, процессор генерирует исключение #GP.
Из описанных в предыдущих главах, к этому классу относятся дескрипторы сегментов данных и обработчиков прерываний (оба варианта). Сюда же можно отнести и дескрипторы сегментов кода (только «обычные», подчинённые будут работать иначе), при использовании с сегментными регистрами DS, ES, FS, GS, то есть не для передачи управления, а как «данные-только-для-чтения». Все неизученные типы дескрипторов также относятся к этому классу.
Например, такое прерывание может вызвать только код с нулевым уровнем привилегий:
gate_descriptor <int26_handler, 8, 0, 10001111b, 0>
|
А такое – любой:
gate_descriptor <int26_handler, 8, 0, 11101111b, 0>
|
Аналогично с любыми другими дескрипторами ресурсов.
ПРИМЕЧАНИЕ В данном случае проверка производится только при явном вызове обработчика инструкцией int 26. При возникновении внешнего прерывания или исключения проверки нет, обработчик будет вызван независимо от уровня привилегий текущего кода (в [Григорьев 1993] утверждается обратное, это ошибка автора). Поскольку никакие другие ресурсы (на данный момент из всех «других» описаны только сегменты кода/данных) не могут быть использованы подобным «самопроизвольным» образом, к ним это замечание не относится. |
Сначала рассмотрим ситуацию (ситуация предполагает периодическое изменение привилегий, сейчас нам не важно как это происходит). Пусть имеется:
Два варианта развития событий:
Для предотвращения таких ситуаций системный код должен использовать свой сегмент стека. А, видимо, для повышения мотивации использования разных стеков, разработчики Intel просто запретили использовать один и тот же: при загрузке селектора в регистр SS, текущий уровень привилегий должен точно совпадать с DPL дескриптора сегмента.
Подробнее о переключении стеков при смене уровней привилегий написано в главе «Защита: передача управления».
Текущий уровень привилегий (Current Privilege Level, CPL) – уровень привилегий исполняемого в данный момент кода, или, другими словами, уровень привилегий, на котором в данный момент находится процессор. Обычно CPL совпадает со значением двух младших битов регистра CS.
Именно CPL определяет, что разрешено, а что запрещено, именно он проверяется при попытке выполнения привилегированных команд (см. ниже), сравнивается с DPL ресурса (см. выше) и с IOPL (см. ниже). В общем, несколько упрощая, при нулевом CPL можно сделать с системой вообще всё, в любом другом случае придётся спрашивать разрешения.
ПРИМЕЧАНИЕ Защита многослойна, нулевой уровень привилегий разрешает «вообще всё» только с точки зрения доступа к любым ресурсам независимо от их DPL (а со стеками и это не получится, см. выше), к любым портам ввода-вывода и к любым инструкциям. А, например, обращаться за пределы сегмента, менять read-only сегменты и передавать исполнение сегментам данных запрещёно независимо от текущего уровня привилегий. Но, если очень нужно, имея нулевой CPL, можно определить свою собственную GDT и описать там те дескрипторы, которые хочется, обходя таким образом практически любые механизмы защиты. |
Значение CPL определяется в соответствии с двумя простыми правилами (подчинённые сегменты кода добавят третье, но пока – два):
Изменение CPL тоже рассмотрено в главе «Защита: передача управления».
Два младших бита «нормального» селектора (не загруженного в CS) называются уровень привилегий запроса (Request Privilege Level, RPL), и примерно в 99% ситуаций, единственно разумным значением этого поля будет 0. Тем не менее, поле придумано разработчиками не совсем напрасно, и ситуация, когда ненулевой RPL имеет смысл, всё-таки существует.
Рассмотрим следующий сценарий:
Теперь добавим уровни привилегий:
Поскольку «F» выполняется с нулевым уровнем привилегий, она может читать/записывать данные независимо от значения DPL сегмента SEG. Может быть, это как раз то поведение, которое требуется, но может быть, нужно ограничить возможности работы только теми сегментами, которые доступны «A». И вот тут-то на помощь приходит RPL! С ним сценарий выглядит так:
ПРИМЕЧАНИЕ Это близко к понятию имперсонализации (impersonation), отличие в том, что с помощью RPL невозможно увеличить свои права, только уменьшить. |
Описанное выше применение RPL сводится к следующему правилу: при загрузке селектора сегмента в сегментный дескриптор, система защиты сравнивает c DPL сегмента не только CPL, но и RPL селектора.
Это единственный разумный случай использования RPL. К сожалению, есть ещё несколько не очень разумных, в которых значение RPL всё-таки играет роль:
Уровень привилегий ввода-вывода (Input-Output Privilege Level, IOPL), находится в 13-м и 12-м битах регистра EFLAGS. IOPL подробно описано в главе «Защита: ввод-вывод», а здесь упомянуто просто для комплекта.
Следующие инструкции в защищённом режиме можно выполнять только при нулевом CPL (в реальном режиме таких проблем нет, можно всем, всё и всегда):
Эти инструкции называются привилегированными (privileged instructions), попытка выполнить их при ненулевом CPL приведёт к генерации исключения #GP.
ПРИМЕЧАНИЕ Обратите внимание: cli, sti, in, out – не привилегированные инструкции, контроль за их исполнением осуществляется иначе, более гибко. Это тема главы «Защита: ввод-вывод». |
Если вернуться к началу главы, к разделу «Насущные проблемы», и вспомнить, чего требовалось достичь, то окажется, что практически всё уже успешно достигнуто. Список результатов приведён в Таблице 2.
Проблема | Решение |
---|---|
Отличать пользовательское приложение от ОС | Динамически – CPL, статически – DPL дескриптора сегмента кода. |
Запретить для пользовательских приложений потенциально опасные инструкции. | Привилегированные инструкции |
Разделять ресурсы, к которым пользователю обращаться можно, и ресурсы, к которым пользователю обращаться нельзя | DPL дескриптора ресурса, CPL |
Порты ввода-вывода. | CPL, IOPL, подробнее – в главе «Защита: ввод вывод». |
Для сегментов кода предложенные решения порождают проблему следующего уровня: отделять мы уже умеем, теперь надо как-то наладить связь между кодом с разными уровнями привилегий. Этому вопросу посвящена глава «Защита: передача управления».
А с сегментами данных новых проблем нет, получена логичная законченная система проверок, даже две – для обычного сегментного регистра и для SS. В качестве некоторого подведения итогов, ниже они приведены полностью.
При загрузке селектора в регистр:
При выполнении операции:
При загрузке селектора в регистр:
При выполнении операции – то же, что и в случае обычного сегментного регистра, отличие только в том, что две первые проверки никогда не сработают: в SS не может находиться нулевой селектор или селектор read-only сегмента.
Программа выполняет следующие действия:
В итоге на экране будет три восклицательных знака.
ПРЕДУПРЕЖДЕНИЕ Как выяснилось, VMWare Workstation 4.0.5 build-6030 ведёт себя не совсем корректно. Сценарий: * Тот же пример, но значение RPL селектора установлено в 1 * После последней перезагрузки виртуальной машины не запускались версии примера с RPL равным 2 или 3 Результат: * На экране нет ни одного восклицательного знака, т.е. установка fs отработала без ошибок Версии с RPL равным 2 и 3 работают нормально, после них версия с RPL равным 1 тоже работает нормально. В VMWare Workstation 5.0.0 build-12124 ошибка исправлена. |
; rpl.asm ; Программа, проверяющая действие поля RPL .model tiny .code .386p org 100h ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Структуры ; ;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Сегментный дескриптор segment_descriptor struct limit_low dw 0 ; Младшие два байта поля Segment limit base_low dw 0 ; Младшие два байта поля Base Address base_high0 db 0 ; Второй байт поля Base Address type_and_permit db 0 ; Флаги flags db 0 ; Ещё одни флаги base_high1 db 0 ; Старший байт поля Base Address segment_descriptor ends ; Дескриптор шлюза gate_descriptor struct offset_low dw 0 ; Два младших байта поля Offset selector dw 0 ; Поле Segment Selector zero db 0 type_and_permit db 0 ; Флаги offset_high dw 0 ; Старшие байты поля Offset gate_descriptor ends ; Регистр, описывающий таблицу table_register struct limit dw 0 ; Table Limit base dd 0 ; Linear Base Address table_register ends ;;;;;;;;;;;;;;;;;;;;;;;;;; ; ; Код ; ;;;;;;;;;;;;;;;;;;;;;;;;;; start: ; Подготавливаем DS push cs pop ds ; В es - начало видеобуфера. Можно было сделать то же ; самое средствами защищённого режима, но так проще push 0b800h pop es ; Устанавливаем правильный сегмент в long-jmp-to-RM mov ax, cs mov cs:rm_cs, ax ; Прописываем адрес начала cs в качестве базового адреса сегментов call cs_to_eax mov cs_dsc.base_low, ax shr eax, 16 mov cs_dsc.base_high1, al ; Сохраняем IDTR реального режима sidt fword ptr old_idtr ; Запрешаем прерывания call disable_interrupts ; Инициализируем IDT call initialize_idt ; Инициализируем GDT call initialize_gdt ; Переключаем режим call set_PE ; 16-разрядный дальний переход. Перключает содержимое cs из нормального ; для реального режима (адрес) в нормальное для защищённого (селектор). ; Базовый адрес целевого сегмента совпадает с cs, ; поэтому смещение можно прописать сразу db 0EAh ; код команды дальнего перехода dw $ + 4 ; смещение dw 8 ; селектор ; В данный момент сегмент кода - 64 Кб, базовый адрес равен ; адресу сегмента кода до переключения в защищённый режим, ; потому можно без проблем переключаться обратно ; Селектор второго дескриптора GDT, RPL равен 3 mov ax, 10011b ; Попытка поместить селектор в сегментный регистр mov fs, ax call clear_PE ; Мы в реальном режиме, осталось разобраться с ; значением регистра cs ; 16-разрядный дальний переход. Перключает содержимое cs из нормального ; для защищённог режима (селектор) в нормальное для реальног (адрес). ; Адрес сегмента вычисляется и прописывается во время выполнения. db 0EAh ; код команды дальнего перехода dw $ + 4 ; смещение rm_cs dw 0 ; сегмент ; восстанавливаем IDTR реального режима lidt fword ptr old_idtr call enable_interrupts ret ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Обработчики ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; intGpf_handler: push eax push ebx mov bh, 07h ; белый текст на чёрном фоне mov bl, '!' mov eax, 80 * 24 * 2 mov word ptr es:[eax], bx call upscroll_screen ; прокручивает экран на строчку вверх ; Запоминаем пятый байт дескриптора в al и ah mov al, res_dsc.type_and_permit mov ah, al ; Оставляем в al только DPL (биты 5-6), инкрементируем это поле and al, 01100000b add al, 00100000b and al, 01100000b ; Обнуляем в ah поле DPL and ah, 10011111b ; Совмещаем or ah, al ; И кладём в дескриптор mov res_dsc.type_and_permit, ah pop ebx pop eax add esp, 4 iretd ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Данные ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Глобальная таблица дескрипторов GDT label byte ; Нулевой дескриптор segment_descriptor <> ; Дескриптор сегмента кода, размер 64 Kb cs_dsc segment_descriptor <0ffffh, 0, 0, 10011010b, 0, 0> ; 10011010b - 1001, C/D - 1, 0, R/W - 1, 0 ; 0 - G - 0, 000, Limit - 0 ; Дескриптор ресурса, в данном случае – сегмента данных res_dsc segment_descriptor <0ffffh, 0, 0, 10010010b, 10001111b, 0> ; 10011010b - 1001, C/D - 0, 0, R/W - 1, 0 ; 10001111b - G - 1, 000, Limit - 1111 ; Данные для загрузки в GDTR gdtr table_register <$ - GDT - 1, 0> ; Таблица дескрипторов прерываний IDT label byte db 13 dup ( 8 dup (0)) ; 0 – 12 ; Дескриптор шлюза ловушки, обработчик #GP gate_descriptor <intGpf_handler, 8, 0, 8Fh, 0> ; Данные для загрузки в IDTR idtr table_register <$ - IDT - 1, 0> ; Место для хранения IDTR реального режима old_idtr table_register <> ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;; Служебные функции ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; Инициализирует GDT initialize_gdt: ; Вычисляем линейный адрес начала массива дескрипторов call cs_to_eax add eax, offset GDT ; Записываем его в структуру mov dword ptr gdtr.base, eax ; Загружаем GDTR lgdt fword ptr gdtr ret ; Инициализирует IDT initialize_idt: ; Вычисляем линейный адрес начала массива дескрипторов call cs_to_eax add eax, offset IDT ; Записываем его в структуру mov dword ptr idtr.base, eax ; Загружаем GDTR lidt fword ptr idtr ret ; Запрещает маскируемые и немаскируемые прерывания disable_interrupts: cli ; запретить прерывания in al, 70h ; индексный порт CMOS or al, 80h ; установка бита 7 в нем запрещает NMI out 70h, al ret ; Разрешает маскируемые и немаскируемые прерывания enable_interrupts: in al, 70h ; индексный порт CMOS and al, 7Fh ; сброс бита 7 отменяет блокирование NMI out 70h, al sti ; разрешить прерывания ret ; Устанавливает флаг PE set_PE: mov eax, cr0 ; прочитать регистр CR0 or al, 1 ; установить бит PE, mov cr0, eax ; с этого момента мы в защищенном режиме ret ; Сбрасывает флаг PE clear_PE: mov eax, cr0 ; прочитать CR0 and al, 0FEh ; сбросить бит PE mov cr0, eax ; с этого момента мы в реальном режиме ret ; Вычисляет линейный адрес начала сегмента кода cs_to_eax: mov eax, 0 mov ax, cs shl eax, 4 ret ; Прокручивает экран на строчку вверх upscroll_screen: push eax push ecx push edx mov eax, 0 ; Текущий символ mov ecx, 80 * 24 ; Колическтво символов на экране ; (без последней строки, она отдельно) screen_loop: mov dx, word ptr es:[eax * 2 + 80*2] mov word ptr es:[eax * 2], dx ; Меняем символ inc eax loop screen_loop mov ecx, 80 ; Длинна последней строки mov dx, 0720h ; символ, которым заполняется строка last_line_loop: mov word ptr es:[eax * 2], dx ; Меняем символ inc eax loop last_line_loop pop edx pop ecx pop eax ret end start |