Программирование на Ассемблере под DOS

Дата публикации 16 авг 2002

Программирование на Ассемблере под DOS — Архив WASM.RU

Содержание

2.1. Проба молотка

  #1. Теоретически гвозди можно забивать и голыми руками. Но намного быстрее и безболезненнее делать это с помощью молотка. Пользоваться им, как известно, каждый дурак умеет. Чего там сложного? Взял оный в руки - и молоти: раз по гвоздю, два раза по пальцам (понимание приходит с опытом).   Молотки бывают разные: большие и маленькие, с длинной ручкой и с короткой ручкой, железные и деревянные, приспособленные для забивания гвоздей и приспособленные для пробивания черепов... Да и гвоздей разнообразие еще то! И разной длины, и разной толщины, и со шляпками разной формы, из разного материала сделанные... есть даже специализированные для прибивания рук к кресту! :(.
  Так вот: виды молотков и особенности их использования мы пока что оставим в покое. Для начала просто возьмем гвоздь, который уже не раз забивали голыми руками (больно было?) и забьем его при помощи молотка! Помните, как мы программу, выводящую окошки, без компилятора одними мнемониками писали?
  А теперь ее же, дуру, "под компилятор" перелопатим... В общем, настало время, братья, тайну нервную и страшную узнать. Сегодня мы познакомимся с компилятором.

  #2. Нижеследующий текст набираем в текстовом редакторе:

Код (Text):
  1.  
  2.  
  3. ;-[блок 2]--------------------------  
  4. CODESG segment
  5. assume CS:CODESG
  6. org 100
  7.  
  8. ;-[блок 3]--------------------------  
  9. MAIN proc
  10.   xor AL,AL
  11.   mov BH,10h
  12.   mov CH,5
  13.   mov CL,10h
  14.   mov DH,10h
  15.   mov DL,3Eh
  16.   mov AH,6
  17.   int 10h
  18.   call WINDOW
  19.   call WINDOW
  20.   call WINDOW
  21.   call WINDOW
  22.   int 20h
  23. MAIN endp
  24.  
  25. WINDOW proc
  26.   ADD BH,10h
  27.   ADD CH,1
  28.   ADD CL,1
  29.   SUB DH,1
  30.   SUB DL,1
  31.   INT 10h
  32.   RET
  33. WINDOW endp
  34.  
  35. ;-[блок 4]--------------------------  
  36. CODESG ends
  37.  
  38. end MAIN
  39.  

  Сохраняем получившуюся дуру под именем proga_1.asm.
  Далее запускаем файл tasm.exe и в качестве параметра передаем ему имя файла с исходным текстом программы...То есть командная строка у вас (в том же Windows Commander'е) вот какая должна быть:

Код (Text):
  1.  
  2. tasm proga_1.asm
  3.  

  Если вы правильно набрали текст программы, то TASM должен выплюнуть вам вот что:

Код (Text):
  1.  
  2. Turbo Assembler Version 4.1 Copyright (c) 1988, 1996 Borland International
  3. Assembling file: proga_1.asm
  4. Error messages: None
  5. Warning messages: None
  6. Passes: 1
  7. Remaining memory: 406k
  8.  

  Самое тут главное - это чтоб "Error messages" был "None". Это значит, что ошибок в программе нет.
  Поехали дальше... Если ошибок у вас действительно никаких не было, то в том же каталоге, что и proga_1.asm ищите файл proga_1.OBJ. Можете даже по F3 попробовать просмотреть его содержимое... Что-нибудь поняли? Если нет - отсылаю вас к "Введению в машинный код".
  А теперь запускаем хорошую программу TLINK следующим образом:

Код (Text):
  1.  
  2. tlink /t proga_1.obj
  3.  

  Обратите внимание: линкуем мы именно файл с типом OBJ, а не ASM.
  Что получилось? А получился файл proga_1.COM!! И этот .COM работает!
  Посмотрите на его содержимое в DZEBUG'е :smile3:. Отличется ли оно чем-то от той проги, что мы делали ранее?
  Нет, не отличается!
  Медитируем...

  #3. А теперь несколько слов о том, почему не стоит писать программы так, как мы это делали раньше:
  1. Эти проклятые адреса! Пойди рассчитай на какой адрес прыгнуть нужно, с какого смещения подпрограмма начинается, с какого блок данных... В принципе, как мы уже убедились, в этом нет ничего сложного... просто занятие это очень уж муторное и неинтересное...
  Как абсолютно верно заметил некто Евгений Олейников в RTFM_Helpers: "Когда я пишу программу, я не знаю точного адреса начала будущей подпрограммы"...
  2. Очень сложно было в том случае, когда возникала необходимость ВСТАВИТЬ ту или иную команду в середину кода. Приходилось перебивать код по новой... по новой пересчитывать адреса... ужас, в общем!
  Так вот: основная и самая главная функция "ассемблерного компилятора" - это как раз и есть "просчитывание адресов"!
  Смотрите, как хорошо и приятно: мы готовим исходный текст в обыкновенном текстовом редакторе :smile3:. Просто набиваем строчка за строчкой - нам не важны адреса (мы их вообще не видим!), не важны "точки входа" в процедуру... Спокойненько вставляем какую надо команду, спокойненько удаляем... Без лишнего напряга!
  Да вы посмотрите на блок 3 программы :smile3:. Там все те же хорошо знакомые команды :smile3:.
  Особое внимание обратите на команду CALL, которая у нас, как известно, вызывает процедуру. После нее идет не привычный адрес начала процедуры, а всего лишь ее "имя собственное"! А сама процедура находится между строчками WINDOW proс (начало процедуры) и WINDOW endp (конец процедуры).
  "WINDOW" - понятно, это "имя собственное". "Proc" - потому что процедура. "Endp" - потому что конец процедуры...
  Тут еще один момент... подобные "словеса" КОМАНДАМИ АССЕМБЛЕРА НЕ ЯВЛЯЮТСЯ. Они называются иначе - "директивами". Это не ПРИКАЗ делать то-то и то-то, а ЦЕННОЕ УКАЗАНИЕ компилятору (не процессору!), что и как ему делать с данным куском кода...
  Процедуры - вещь хорошая! Все процедуры хороши, и в большой программе их чертовски много! Это вам не языки высокого уровня, где можно длиннющие простыни кода лабать и все будет работать! ("Дельфи-компилятор не даст вам выстрелить себе в ногу, но будет так удивлен попыткой сделать это, что через некоторое время сделает это сам." (С) DZ WhiteUnicorn).
  Это ASM! Тут запутаться легче простого! Исходники неимоверно длинны и сложны! Все делается ручками (хоть и с помощью компилятора), а ошибки довольно трудно отслеживаются (для не-дZенствующих это вообще занятие безнадежное)! Вот и выкручиваются низкоуровневые программеры таким вот образом: всю программу (даже в тех случаях, когда это не обоснованно!) делят на меленькие кусочки-процедурки... Напишут процедурку, протестируют ее так и сяк... если работает - за следующую берутся...
  Сии "методы проектирования" мы с вами еще не раз рассмотрим :smile3:. Пока что знайте вот что: любую прогу можно/нужно рассматривать как КУЧУ ПРОЦЕДУР, которые все между собой повязаны...
  Но есть среди этих процедур САМАЯ ГЛАВНАЯ! Это та, С КОТОРОЙ НАЧИНАЕТСЯ ВЫПОЛНЕНИЕ ПРОГРАММЫ! Ее никто не вызывает. Она - босс! Она всеми командует, все гребет под себя... Описывают ее те же самые директивы, что и прочие "подчиненные процедуры"... Но есть у нас еще одна директивка, которая указывает, КАКАЯ ИМЕННО процедура ИЗ ВСЕХ вроде бы "равноправных" является ГЛАВНОЙ.
  Видите строчку end MAIN в конце исходного текста программы? Именно она и указывает ГЛАВНУЮ ПРОЦЕДУРУ ("MAIN" - ее имя собственное). Если бы мы написали end WINDOW, то выполнение программы у нас началось бы с первой строчки процедуры WINDOW, и ни одна строчка из MAIN выполнена не была бы...
  Уф... в общем, долго и упорно медитируйте...

  #4. Наличие многочисленных директив - это своего рода плата за то, что компилятор избавляет нас от необходимости просчитывать адреса. Как говорит дZенская программерская мудрость "любишь кататься, люби и саночки возить"...
  Как и в DZEBUG'е, "в TASM'e" мы также должны четко инструктировать компилятор (дабы он в свою очередь также четко проинструктировал процессор), что у нас является кодом, а что - данными...
  Посмотрите на исходник. Весь текст программы у нас хранится между директивами CODESG segment и CODESG ends. Где CODESG - это "имя собственное", "segment" - потому что "CODESG" он, собственно, и есть сегмент :smile3:, и "ends" - потому что конец сегмента... (сравните с процедурными директивами).
  Но тут такой вопрос:
  Директива ASSUME производит связывание сегментного решистра CS с "именем собственным".
  Далее у нас следует директива org 100h. Нужна она нам для того, чтобы компилятор понял, что мы хотим получить именно COM-файл, который, как известно, помещается в сегмент памяти начиная со смещения 100 (в общем-то, это необходимое, но вовсе не достаточное условие для получения COM-файл). Директивка очень интересная, о ней мы, надеюсь, еще поговорим подробнее, когда коснемся вирмейкерства.

  #5. Ладно... с директивами более-менее разобрались, исходник приготовили, пора разбираться че там дальше происходит...
  Дальше происходит так называемое "ассемблирование", т.е. перевод команд в соответствующие машинные коды. При этом просчитываются адреса меток, адреса начала подпрограмм, адреса начала/конца сегментов... и многое другое...
  Причем ассемблирование происходит как минимум в два приема. Посудите сами: откуда компилятору знать, с какого адреса начнется процедура WINDOW, если не известно, какая еще простыня команд ПОСЛЕ этого CALL'а будет? В DZEBUG'е мы это "в уме на листике" считали...
  TASM это тоже аналогичным образом делает :smile3:.
  При первом проходе он подсчитывает, сколько какая команда занимает места, с каких адресов начинаются процедуры и т. д., и только при втором проходе подставляет в call'ы КОНКРЕТНЫЕ АДРЕСА начала этих процедур... всего лишь... ну еще и ваши "d" и "b" в машинные "h" (которые на самом деле "b") переводит... (во завернул!)...
  А вообще, TASM много еще чего делает... программеры - народ ленивый...
  В результате ассемблирования мы получаем так называемый "объектный файл".
  "И что это за дрянь?" - спросите вы, и правильно спросите...
  А вы сравните шестнадцатеричное содержимое OBJ и COM файлов. В OBJ присутствует та же последовательность байтов, что и в OBJ. Но помимо этого и еще какая-то шестнадцатеричная ерунда присутствует: имя сассемблированного файла, версия ассемблера, "имя собственное" сегмента и т. д.
  Это своего рода "служебная" информация, предназначенная для тех случаев, когда ваш исполнимый файл вы хотите собрать из нескольких. При разработке больших приложений исходный текст, как правило, хранится в нескольких файлах; в каждом из них прописаны свои сегменты кода/данных/стека. А исполнимый получить нам нужно только один - с единым сегментом кода/данных/стека. Именно это TLINK и делает: завершает определение адресных ссылок и объединяет, если это требуется, несколько программных модулей в один...
  И этот один у нас и является исполнимым... УРА!

  #6. Итак, первую прогу с использованием компилятора мы написали. Теперь напишем вторую. Набиваем ее исходный текст и компилим:

Код (Text):
  1.  
  2. assume CS:SUXXX, ES:SUXXX
  3.  
  4. SUXXX segment
  5. org 100h
  6.  
  7. MAIN proc
  8.   lea bp,ABC
  9.   mov AH,13h
  10.   mov AL,3
  11.   xor bh,bh
  12.   mov bl,07h
  13.   mov cx,16d
  14.   xor DX,DX
  15.   int 10h
  16.   int 20h
  17. MAIN endp
  18.  
  19. ABC db 'H',0Ah,'e',0Bh,'l',0Dh,'l',0Ch
  20.     db 'o',0Bh,',',0Ah,' ',0Ah,'W',09h
  21.     db 'o',08h,'r',07h,'l',06h,'d',05h
  22.     db '!',02h,'!',02h,'!',02h
  23.  
  24. SUXXX ends
  25.  
  26. end MAIN
  27.  

  Итак, в этой программе мы использовали функцию 13h прерывания 10h (INT 10h, AH=13h).
  Вот ее описание:

  [INT 10h, ФУНКЦИЯ 13h] - записывает на экран символьную строку, начиная от указанной позиции.
  ВХОДНЫЕ ПАРАМЕТРЫ:
  AH = 13h;
  AL - код формата(0-3):
  AL=0, формат строки{симв., симв.,..., симв.} и курсор не перемещается,
  AL=1, формат строки{симв., симв.,..., симв.} и курсор перемещается,
  AL=2, формат строки{симв., атр.,...,симв., атр.} и курсор не перемещается,
  AL=3, формат строки{симв., атр.,...,симв., атр.} и курсор перемещается;
  BH - страница дисплея;
  BL - атрибут (для режимов AL=0, AL=1);
  CX - длина строки;
  DX - позиция курсора для записи строки;
  ES:BP - указатель строки.
  ВЫХОДНЫЕ ПАРАМЕТРЫ: отсутствуют.

  А еще мы использовали команду LEA, которая загружает в регистр адрес (смещение), с которого у нас начинается блок данных.

2.2. Несколько "тупых" процедурок

  #1. Дабы сходу "обломать" нездоровую критику, сразу предупреждаем, что нами сознательно допущена некоторая излишняя "процедуризация" исходного кода. А по сему, прежде чем взяться за изучение нижеследующего материала, твердо уясните: в данном случае (как есть сейчас) дробление кода на процедуры - дурь полная. А обосновываем мы эту дурь только тем, что впоследствии будем совершенствовать эти процедуры до уровня, когда, собственно, сие "дробление" и будет обосновано.
  И еще. Подобный модульный подход (это понятие немножко шире, чем просто "использование процедур") в некоторых случаях снижает быстродействие и увеличивает размер исполнимого файла, однако он же и значительно увеличивает "читабельность" исходника, что вполне обоснованно с точки зрения обучения. И с точки зрения "командной" разработки программ - тоже. (В общем, критика по этому поводу не принимается!).
  Итак, для начала набиваем вот какой текст (исходник это!):

Код (Text):
  1.  
  2. assume CS:PROGA
  3.  
  4. PROGA segment
  5. org 100h
  6.  
  7. ;-[TESTING]---------------------------
  8. ;Здесь мы будем тестировать процедуры
  9. ;---------------------------------------
  10. TESTING proc
  11.   call EXIT_COM
  12. TESTING endp
  13.  
  14. ;-[EXIT_COM, V1]--------------------
  15. ;Завершение работы программы
  16. ;На входе: пофиг
  17. ;На выходе: нихрена
  18. ;Прерывания: INT 20h
  19. ;Процедуры: ан нэту
  20. ;-----------------------------------
  21. EXIT_COM proc
  22.   int 20h
  23. EXIT_COM endp
  24.  
  25. PROGA ends
  26.  
  27. end TESTING
  28.  

  Здесь вам все должно быть понятно. INT 20h вынесен в отдельную процедуру, и только. Плюс еще какие-то нездоровые заголовки добавлены, которые и весят-то больше, чем сам код. Это нормально. После точки с запятой в исходнике вы можете писать все, что угодно. Все равно при компиляции это будет проигнорированно и, следовательно, на размер исполнимого файла не повлияет. (Точно так, как и длина "имен собственных" процедур и меток).
  Мы предлагаем использовать именно такую "шапку" комментария к каждой из ваших процедур. Все очень просто. Первая строчка - это "что делает процедура". Вторая - какие ей необходимо передать параметры. Третья - какие она возвращает параметры. Четвертая и пятая, соответственно, - какие в процедуре использовались прерывания и хм... ранее написанные процедуры :smile3:. Это намного облегчит понимание вашего исходника как вами самими, так и теми, кому вы его предоставите на поругание...
  Для тех, кто в танке: последующие процедуры вставляйте между процедурами TESTING и EXIT_COM - не ошибетесь :-p.

  #2. Следующая процедура также основана на одном-единственном прерывании. Все, что она делает - это возвращает текущие координаты курсора.

Код (Text):
  1.  
  2. ;-[CURSOR_READ, V1]-------------------
  3. ;Возвращает координаты курсора
  4. ;На входе: пофиг
  5. ;На выходе: DH - строка, DL - столбец
  6. ;Прерывания: INT 10h, AH=03h
  7. ;Процедуры: ан нэту
  8. ;------------------------------------------
  9. CURSOR_READ proc
  10.   push AX
  11.   push BX
  12.   push CX
  13.   mov AH,3
  14.   xor BH,BH
  15.   int 10h
  16.   pop CX
  17.   pop BX
  18.   pop AX
  19.   ret
  20. CURSOR_READ endp
  21.  

  Прежде всего обратите внимание на цепочку push'ей и pop'ов (далее - просто "поп"), на очередность записи в стек (AX, BX, CX) и извлечения (CX, BX, AX - обратное то есть). Все регистры, которые мы собираемся изменять внутри процедуры, должны обязательно сохраняться в ее начале и восстанавливаться в ее конце. В важности соблюдения этого правила вы еще не раз убедитесь на своем горьком опыте. Те, кто медитировал над заданием из главы 1.10 #4, уже знают, о чем тут идет речь (а кто не пытался - самое время!).
  Давайте посмотрим на нашу процедуру с точки зрения пушей и поп. AH мы изменяли для указания функции прерывания, которую мы хотим использовать. BH обнуляли для указания видеостраницы (пока будем только одну-единственную, нулевую, юзать). "А CX зачем" - спросите. - "Вроде мы его не трогали...". И точно, мы - не трогали. А посмотрите в описании, что в этот регистр нам засунуло прерывание в "результате" своего выполнения... Посмотрели? Оно вам надо?? То, что нам надо - координаты - засунуты в DX (DH, DL), поэтому их значения мы не сохраняем. Если бы нам нужно было получить информацию о типе курсора - мы бы запушили DX, а CX бы оставили в покое...
  Что? Без пива не разобраться?
  Так в чем, черт подери, дело? Разбирайтесь под пиво!!
  Вот вам еще одна аналогичная процедура, которая не определяет, а УСТАНАВЛИВАЕТ курсор в заданные координаты...

Код (Text):
  1.  
  2. ;-[CURSOR_SET, V1]---------------------------------------
  3. ;Устанавливает курсор в заданные координаты
  4. ;На входе: DH - строка, DL - столбец
  5. ;На выходе: нихрена
  6. ;Прерывания: INT 10h, AH=02h
  7. ;Процедуры: ан нэту
  8. ;--------------------------------------------------------
  9. CURSOR_SET proc
  10.   push AX
  11.   push BX
  12.   push CX
  13.   mov AH,2
  14.   xor BH,BH
  15.   int 10h
  16.   pop CX
  17.   pop BX
  18.   pop AX
  19.   ret
  20. CURSOR_SET endp
  21.  

  Если кто чего не понимает - смотрите комментарии к предыдущей процедуре (+ описание прерываний и команд! это обязательно!). Если кто не понял и предыдущую - снова отсылаю к главе 1.10

  #3. На основе двух предыдущих процедур мы напишем третью "курсорную" :smile3:, которая будет сдвигать курсор на одну позицию вправо.

Код (Text):
  1.  
  2. ;-[CURSOR_RIGHT, V1]-------------------------------------
  3. ;Перемещает курсор на одну позицию вправо
  4. ;На входе: пофиг
  5. ;На выходе: нихрена
  6. ;Прерывания: ан нэту
  7. ;Процедуры: CURSOR_READ, CURSOR_SET
  8. ;--------------------------------------------------------
  9. CURSOR_RIGHT proc
  10.   push DX
  11.   call CURSOR_READ
  12.   inc DL
  13.   call CURSOR_SET
  14.   pop DX
  15.   ret
  16. CURSOR_RIGHT endp
  17.  

  А здесь очень простая идеология :smile3:). Из двух процедур мы собрали третью :smile3:). Вызвали CURSOR_READ, получили в DX текущие координаты курсора. Ту координату, что столбец (DL, младшая часть DX), увеличили на единицу. А потом вызвали процедуру CURSOR_SET, которая у нас устанавливает координаты курсора. Новые координаты в нее передаются опять таки через тот же DX. Улавливаете?
  Естественно, мы запросто можем отказаться от процедур CURSOR_SET и CURSOR_READ и решить данную задачу внутри одной процедуры... В общем, свой выбор вы сделаете сами. Страшна Сцилла оптимизации по быстродействию, еще страшнее - Харибда оптимизации по размеру, но тварь самая страшная - это Программер, который оптимизирует свой код по собственной "удобноваримости"... (Хм... интересно, что бы сказали по этому поводу программеры Мелкософта...)
  Обратите также внимание на push/pop DX внутри процедуры. Мы просто сдвигаем курсор вправо. ПРОСТО СДВИГАЕМ на одну позицию. То что в DX нам надо? Сто лет оно нам не надо... херим... А остальные регистры - еще процедурами CURSOR_SET и CURSOR_READ неоднократно "похерены". В каком смысле "похерены"?? А в таком, что состояние регистров ПОСЛЕ вызова CURSOR_RIGHT в точности такое же, как и было ДО. Хотя, сами помните, что всю четверку регистров мы еще как юзали...
  Вот теперь можно сделать паузу и (это американцы пускай свой пластмассовый твикс кушают) ПОМЕДИТИРОВАТЬ...

  #4. Следующая процедура ну вааще элементарна:

Код (Text):
  1.  
  2. ;-[WRITE_CHAR, V1]---------------------------------------
  3. ;Печатает символ и переводит курсор на позицию вправо
  4. ;На входе: DL - код символа.
  5. ;На выходе: нихрена
  6. ;Прерывания: INT 10h, AH=09h
  7. ;Процедуры: CURSOR_RIGHT
  8. ;--------------------------------------------------------
  9. WRITE_CHAR proc
  10.   push AX
  11.   push BX
  12.   push CX
  13.   mov AH,9
  14.   xor BH,BH
  15.   mov BL,00000111b
  16.   mov CX,1
  17.   mov AL,DL
  18.   int 10h
  19.   call CURSOR_RIGHT
  20.   pop CX
  21.   pop BX
  22.   pop AX
  23.   ret
  24. WRITE_CHAR endp
  25.  

  Она символ на монитор выводит. Через 9-ю функцию 10-го прерывания. А потом (после вывода) курсор на позицию вправо перемещает. Догадайтесь сами, "путем вызова" какой процедуры... Ага, правильно :smile3:), CURSOR_RIGHT.
  Посмотрите на описание этого прерывания. Код символа должен быть в AL. А у нас в комментариях он в DL прописан. А перед INT 10h mov AL,DL нездоровый стоит. Нахрена он тут?? И правильно!! Этот mov можно удалить, и передавать значение через AL. Но я тварь вредная. Привык я, понимаете-ли, через DX передавать... Привычка - сила страшная!! Лень с ней бороться... Лень, а поэтому и не буду... Кто-то в подобном мове может и более глубокий смысл найдет - наверняка найдет!! В общем - ищите сами. На блюдечке с голубой каемочкой вам это не преподнесу :smile3:). Вредный.
  mov BL,00000111b (не 07h) написано специально. Это чтоб вы посмотрели, как кодируется атрибут (фон, цвет) энтого символа. В одном из предыдущих номеров даже табличка есть, из справочника содранная...

  #5. В языке "командного интерпретатора DOS" есть хорошая команда - CLS (то бишь очистка дисплея). Хорошая команда! Кто не поленится заглянуть внутрь command.com'а, увидят приблизительно следующее (на самом деле все чуть-чуть навороченнее, но прерывание то же):

Код (Text):
  1.  
  2. ;-[CLS, V1]-----------------------------------------
  3. ;Oчистка дисплея
  4. ;На входе: пофиг
  5. ;На выходе: нихрена
  6. ;Прерывания: INT 10h, AH=06h
  7. ;Процедуры: ан нэту
  8. ;--------------------------------------------------------
  9. CLS proc
  10.   push AX
  11.   push BX
  12.   push CX
  13.   push DX
  14.   mov AH,6
  15.   xor AL,AL
  16.   mov BH,00000111b
  17.   xor CX,CX
  18.   mov DH,24d
  19.   mov DL,79d
  20.   int 10h
  21.   pop DX
  22.   pop CX
  23.   pop BX
  24.   pop AX
  25.   ret
  26. CLS endp
  27.  

  Короче, элементарный скроллинг, только заданы максимально возможные координаты скроллируемого окошка и CX=0... в общем, окошки рисовали, помните...

  #6. Тестируем, штоль?? Дописываем процедуру TESTING:

Код (Text):
  1.  
  2. TESTING proc
  3.   mov DL,'*'
  4.   call WRITE_CHAR
  5.   call WRITE_CHAR
  6.   call WRITE_CHAR
  7.   call EXIT_COM
  8. TESTING endp
  9.  

  Компилим... CLS тож тестируем... Все работает??

  А теперь самое интересное :smile3:) и благоприятно влияющее на нервную систему :smile3:. Прелесть модульного подхода вот в чем: написали процедуру, протестировали успешно - и МОЖЕТЕ ЗАБЫТЬ нафиг, как она у вас работает, какие функции там используются, какие хитроПОПые алгоритмы там применены...
  Просто смотрите на заголовок, че она делает, чего ей надобно на входе, чего возвращает... а ее "внутренности" вам глубоко фиолетовы. Работает - и ладно. Ааааaaa?? Круто?
  Уф... медитируйте!!

2.3. Печать "шестнадцатеричных циферек"

  #1. Мы уже неоднократно юзали хорошую мнемоническую (aka ассемблерную) команду ADD :smile3:. Напомню, что в результате выполнения команд

Код (Text):
  1.  
  2. mov AX,2
  3. mov BX,3
  4. add AX,BX
  5.  

 в регистр AX у нас помещалась сумма (AX=AX+BX).

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

  Как мы это сделаем? Вы уже неоднократно слышали, что "в ассемблере" все делается "ручками" :smile3:. Сейчас вы лишний раз убедитесь в том (некоторые замрут в ужасе), что это утверждение истинно. Для вывода значения регистра мы вовсе не "познакомимся с новым прерыванием". Даже такая простейшая операция, как "вывод на дисплей значения регистра (переменной)" - это целая процедура. И не одна, как вы скоро в этом убедитесь. Страшно?

 Поехали!!

  #2.Задача: вывести на монитор значение регистра DL.
  Народ! Давайте сразу расставим границы между КОДОМ СИМВОЛА и его НАЧЕРТАНИЕМ.
  Например, у нас F3h в DL. Как мы хотим это вывести? Как символы 'F3' или же как ASCII символ, соответствующий коду F3h? Определяемся. Если в DL у нас F3h - то надо чтоб именно 'F3' у нас на монитор и выводилась. Не 'э перевернутое', не '46 33', а именно 'F3'. Помедитируйте. Уловите разницу между 'э', '46 33' и 'F3'.

  Эту задачу мы немножко упростим :smile3:. Для начала напишем процедуру, которая выводит только младшую тетраду регистра DL (цифру "3" в нашем примере). Для этого мы обратимся к процедуре WRITE_CHAR из прошлого номера. Именно она печатает нам на монитор символ, ASCII-код которого находится в DL.
  Но тут загвоздка: в DL-код, а печатается-то символ :smile3:. А нам, собственно, именно две циферки шестнадцатеричного кода, как два символа, и нужно напечатать. Ну, или хотя бы младшую циферку этого кода...
  Решается эта задача элементарно :smile3:. Главное - это правильно ее сформулировать!
  Вот что я тут "нарисовал":

Код (Text):
  1.  
  2.    ЕСТЬ         НУЖНО
  3. код символ    символ код
  4. -----------  ------------
  5. 00h  '?'       '0'   30h
  6. 01h  '?'       '1'   31h
  7. 02h  '?'       '2'   32h
  8. 03h  '?'       '3'   33h
  9. 04h  '?'       '4'   34h
  10. 05h  '?'       '5'   35h
  11. 06h  '?'       '6'   36h
  12. 07h  '?'       '7'   37h
  13. 08h  '?'       '8'   38h
  14. 09h  '?'       '9'   39h
  15. 0Ah  '?'       'A'   41h
  16. 0Bh  '?'       'B'   42h
  17. OCh  '?'       'C'   43h
  18. ODh  '?'       'D'   44h
  19. OEh  '?'       'E'   45h
  20. 0Fh  '?'       'F'   46h
  21.  

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

  Процедурка наша вот что должна делать - Всего-навсего перевести (конвертировать) тетраду в код соответствующего ей символа... Завернуто? Если разобраться, то не очень-то и завернуто.
  Смотрите: в DL у нас 03h. Хотим мы эту '3' на монитор вывести. Если вызовем WRITE_CHAR, то у нас символ "сердечко" выплюнется. А надо, чтоб символ '3' вывелся, код которого 33h.
  Соответственно и для остальных смотри по табличке.

 А теперь обратите внимание, насколько "шестнадцатеричная циферка" (тетрада) отличается от ASCII-кода, этой "циферке" соответствующего. Сам скажу: на 30h для цифр от '1' до '9', и на 37h для цифр от 'A' до 'F'. То есть "переконвертацию" мы запросто можем сделать командами add DL,30h (если тетрада в диапазоне 0...9) и add DL,37h (если тетрада в диапазоне A...F).

 Короче, вот код (пропиваю!):

Код (Text):
  1.  
  2. ;-[WRITE_HEX_DIGIT, V1]----------------------------------
  3. ;Печатает одну шестнадцатеричную цифру (младшую тетраду DL)
  4. ;(старшая тетрада должна быть равна 0)
  5. ;На входе: DL - цифра
  6. ;На выходе: нихрена
  7. ;Прерывания: ан нэту
  8. ;Процедуры: WRITE_CHAR
  9. ;--------------------------------------------------------
  10. WRITE_HEX_DIGIT proc
  11.   push DX  
  12.   cmp  DL,0Ah      
  13.   jae  HEX_LETTER
  14.   add  DL,30h
  15.   JMP  WRITE_DIGIT
  16.  HEX_LETTER:
  17.   add  DL,37h
  18.  WRITE_DIGIT:
  19.   call WRITE_CHAR
  20.   pop  DX  
  21.   ret
  22. WRITE_HEX_DIGIT endp
  23.  

 Сначала, ессно, изменяемые регистры сохраняем (мы ж их изменяем!). (Ну, и восстанавливаем в конце процедуры (PUSH и POP соответственно)).
 Потом у нас логическое ветвление организовано. Сравниваем значение DL с "общей границей" наших двух диапазонов (команда - CMP, "граница" - 0Ah). Если это значение больше или равно 0Ah, то прыжок на метку HEX_LETTER, прибавление к DL 37h и печать цифры (WRITE_CHAR). Иначе добавляем 30h и безо всяких условий перепрыгиваем на вызов WRITE_CHAR (минуя add DL,37h то бишь).
 Все. Тестируем.

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

Код (Text):
  1.  
  2. TESTING proc
  3.   mov DL,00h
  4.   call WRITE_HEX_DIGIT
  5.   mov DL,01h
  6.   call WRITE_HEX_DIGIT
  7.   mov DL,09h
  8.   call WRITE_HEX_DIGIT
  9.   mov DL,0Ah
  10.   call WRITE_HEX_DIGIT
  11.   mov DL,0fh
  12.   call WRITE_HEX_DIGIT
  13.   call EXIT_COM
  14. TESTING endp
  15.  

  Если сия "тестовая" (она же - главная) процедура выведет на монитор

Код (Text):
  1.  
  2. 019AF
  3.  

 , значит мы с высокой долей вероятности можем быть уверенными, что процедура WRITE_HEX_DIGIT работает правильно на всех значениях младшей тетрады DL.
  Кто не просто скопировал процедуру из буфера обмена, а действительно разобрался с тем, как она работает - сами знают, что значение старшей тетрады нашей процедуре НЕбезразлично. Оно должно быть равным 0.

  #4. Что мы имеем? Процедуру для вывода на дисплей одной шестнадцатеричной циферки - младшей тетрады (в DL). Но нам-то нужно две вывести! Сначала старшую циферку-тетраду, и только потом - младшую!
  Таким образом очередная задача разбивается на две части: печать старшей тетрады DL и печать младшей тетрады DL.
  Первая "подзадача" решается легко: нужно просто старшую тетраду переместить на место младшей и вызывать процедуру (основательно протестированную и 99,9%-но работающую) WRITE_HEX_DIGIT.
  А вторая подзадача - хм... заключается в восстановлении предыдущего (мы ж тетраду перенесли) значения DL и снова - вызове WRITE_HEX_DIGIT.
  (Хе! Вот теперь-то вы уж точно почувствуете всю прелесть "дробления кода на процедуры"!)

  Перенос тетрады мы осуществим при помощи команды SHR, которую в умных книжках обзывают как "логический сдвига разрядов операнда вправо". Объясню.
  Представьте себе деревянную доску, длинной в 8 бутылок пива и шириной в одну. (В принципе, доску эту можно и в два раза длиннее представить, но тогда на ней надо "DX" написать, мы же пока только "DL" напишем). А еще дурня, у которого на лбу SHR написано. Так вот, если этому придурку стукнуть по хребту, то он слева от доски поставит ПУСТУЮ бутылку, а остальные сдвинет на одну позицию вправо, в результате чего самая правая бутылка, ессно, с доски упадет.
  Бутылки, которые сразу стояли, могут быть пустыми или полными, а вот дурень SHR - только пустые ставит. И только слева.

Код (Text):
  1.  
  2. "исходное" 11110011
  3.    SHR     01111001
  4.    SHR     00111100
  5.    SHR     00011110
  6.    SHR     00001111
  7.  

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

Код (Text):
  1.  
  2. mov DL,11110011b
  3. mov CL,4
  4. shr DL,CL
  5.  

  В DL - наша цепочка битов. (11110011b = F3h, естественно).
  В CL заносим "на сколько позиций" нам нашу цепочку сдвинуть.
  Ну и SHR - это дурень, который сдвигает вправо, а слева нули дописывает.

  #5. Думаете, это все?? Разогнались!! Не все так просто :smile3:.
  WRITE_HEX_DIGIT у нас требует, чтобы первой тетрадой были только одни нули. Я заострял на этом ваше внимание.
  При печати первой тетрады это условие соблюдается. SHR слева нули дописывает.
  А вот при печати второй цифры нужно вот что: ничего никуда не сдвигая, обнулить старшую тетраду, а младшую (которая, собственно, и есть "цифра") оставить в покое.
  Решим мы эту команду при помощи логической операции "и" (and по-аглицкому).

Код (Text):
  1.  
  2. 0 0 1 1
  3. 0 1 0 1
  4. -------
  5. 0 0 0 1
  6.  

  А теперь и для особо одаренных:

Код (Text):
  1.  
  2. 0 and 0 = 0
  3. 0 and 1 = 0
  4. 1 and 0 = 0
  5. 1 and 1 = 1
  6.  

 Смотрите, интересно как получается:
 Если мы AND чего-либо (нуля или единички) с 0 делаем, то у нас в результате 0, и только 0 получается.
 А если AND с единичкой - то ЧТО БЫЛО, ТО И ОСТАЕТСЯ.
 (Это и есть потаенный дZенский смысл команды AND)

 Решение нашей проблемы (обнулить старшую тетраду, а младшую оставить без изменений) таким образом сводится к тому, что старшую тетраду нужно "AND 0", а младшую - "AND 1".
 То есть значению DL с 00001111b (оно же - 0Fh) "AND" сделать.

 На ассемблере это вот как выглядеть будет:

Код (Text):
  1.  
  2. and  DL,00001111b
  3.  

 Естественно, 00001111b = 0Fh

 Аминь!!

 #6. Уфф... Вот что получиться в итоге должно:

Код (Text):
  1.  
  2. ;-[WRITE_HEX, V1]----------------------------------------
  3. ;Печатает две шестнадцатеричные цифры
  4. ;На входе: DL - типа цифры две :))
  5. ;На выходе: нихрена
  6. ;Прерывания: ан нэту
  7. ;Процедуры: WRITE_HEX_DIGIT
  8. ;--------------------------------------------------------
  9. WRITE_HEX proc
  10.   push CX
  11.   push DX
  12.   mov  DH,DL                
  13.   mov  CL,4
  14.   shr  DL,CL
  15.   call WRITE_HEX_DIGIT
  16.   mov  DL,DH
  17.   and  DL,0Fh
  18.   call WRITE_HEX_DIGIT
  19.   pop  DX
  20.   pop  CX
  21.   ret
  22. WRITE_HEX endp
  23.  

  Ну че тут объяснять?? Я уже объяснил все!! Единственное, что могу добавить - это про mov DH,DL. Этой командой мы значение регистра копируем перед тем как биты "ему" сдвинуть. А потом статус-кво mov DL,DH восстанавливаем, чтоб и младшую цифру напечатать.
  Все. Тестируем. Вроде должно работать.

  #7. Так сказать "к вопросу о шаблонах мышления"...
  Мы тут доооолго трахались с тетрадами. Вроде успешно.
  Когда при тестировании понимаемости материала мы предложили пяти "подопытным" самостоятельно написать процедуру для вывода на монитор "большого" регистра (DX), они все как один начали сдвигать байты... :(
  Народ!! Это не есть правильно!!

Код (Text):
  1.  
  2. ;-[WRITE_HEX_WORD, V1]-----------------------------------
  3. ;Печатает шестнадцатеричное слово
  4. ;На входе: DX - слово
  5. ;На выходе: нихрена
  6. ;Прерывания: ан нэту
  7. ;Процедуры: WRITE_HEX
  8. ;--------------------------------------------------------
  9. WRITE_HEX_WORD proc
  10.   push  DX
  11.   xchg  DL,DH
  12.   call  WRITE_HEX
  13.   xchg  DL,DH
  14.   call  WRITE_HEX
  15.   pop   DX
  16.   ret
  17. WRITE_HEX_WORD endp
  18.  

  Команды xchg DL,DH и xchg DH,DL, кстати, работают абсолютно одинаково. Операнды просто меняются между собой значениями. В качестве одного из операндов может выступать память.

  #8. Ну, и напоследок, - информация к размышлению:

  Команду shr можно использовать для деления целочисленных операндов без знака на степени 2 :smile3:).

Код (Text):
  1.  
  2. mov     cl,4
  3. shr     ax,cl
  4.  

  Думаете эти "фокусники" который в уме офигенны уравнения считать умеют, шибко умные?? Нифига!! Они просто люди ЗНАЮЩИЕ. А делить на десять и вы умеете...
  Над этим я настоятельно рекомендую дооооолго помедитировать. А еще над тем, что девушки весьма и весьма любят, когда им фокусы показывают. Впрочем, это (вычисления в уме) - тема отдельная. Мы ее тоже когда-нить коснемся :smile3:. MATRIX MUST DIE!!

2.4. Печать десятичных циферек

  #1. Для тех, кто медитировал над главой 1.1, алгоритм должен быть понятен как 2+2=(вписать желаемое). Кто не въедет в алгоритм WRITE_DECIMAL - научитесь сначала переводить числа между радиксами на листике в клеточку.
  Итак, ставим новую задачу. В DX у нас шестнадцатеричное число. Нам нужно напечатать его на монитор в "десятичном формате". Еще раз обращаю ваше внимание на то, что существует множество способов ее решения. Мы же выбрали способ, который:
  а). Вам легче всего будет понять.
  б). Требует минимального количества "новых" команд.
  Кто скажет, что мы не правы - пусть первый кинет камень в эхо-конференцию RTFM_Helpers.

  #2. Прежде всего посмотрите на процедуру WRITE_HEX_DIGIT и вспомните, какой алгоритм положен в основу ее работы. Вспомнили? Рад за вас!!
  А теперь мы познакомимся к командой деления. Что будем делить?? Ну естественно, "регистры" :smile3:.

  Новая команда называется "деление беззнаковое" (DIVide unsigned).
  Что такое делимое/делитель/частное, я вас грузить не буду, это в учебнике арифметики для младших классов более чем понятно расжевано. Ну во всяком случае детишки это понимают.
  (Кто скажет, что 10/3=3 а остаток 333 в периоде - тот дурак. Кто скажет, что остаток будет равен 1, скажет правильно.)

  Следующий кусок кода демонстрирует деление значения регистра AX на значение регистра BL.

Код (Text):
  1.  
  2. mov ax,10d
  3. mov bl,3d
  4. div bl
  5.  

  Обратите внимание: ДЕЛИМОЕ у нас может располагаться только в регистре AX (другими словами, делимое задается неявно), а ДЕЛИТЕЛЬ - в любом регистре.
  Кто не верит - посмотрите под отладчиком...

  1. Если делитель размером в байт, то после операции частное помещается в AL, а остаток - в AH.
  2. Если делитель размером в слово, то делимое должно быть расположено в паре регистров DX:AX (младшая часть делимого в AX). После операции частное помещается в AX, а остаток - в DX.
  2. Если делитель размером в двойное слово, то делимое должно быть расположено в паре регистров EDX:EAX (младшая часть делимого находится в EAX.) После операции частное помещается в EAX, а остаток - в EDX.

  Внимание, подводный камень! В следующем примере:

Код (Text):
  1.  
  2. mov ax,10d
  3. mov bx,3d
  4. div bx
  5.  

  у вас вовсе не AX на BX делиться будет, а парочка DX:AX на BX.
  Помедитируйте над этим :smile3:)

  #3. А теперь, собственно, пропиваю саму процедуру вместе с традиционным расжевыванием оной:

Код (Text):
  1.  
  2. ;-[write_decimal, v1]------------------------------------
  3. ;печатает десятичное беззнаковое число
  4. ;на входе: dx - типа число
  5. ;на выходе: нихрена
  6. ;прерывания: ан нэту
  7. ;процедуры: write_hex_digit
  8. ;--------------------------------------------------------
  9. write_decimal proc
  10.   push ax
  11.   push cx
  12.   push dx
  13.   push bx
  14.   mov  ax,dx  ;(1)
  15.   mov  bx,10d ;(2)
  16.   xor  cx,cx  ;(3)
  17.  non_zero:
  18.   xor  dx,dx  ;(4)
  19.   div  bx     ;(5)
  20.   push dx     ;(6)
  21.   inc  cx     ;(7)
  22.   cmp  ax,0   ;(8)
  23.   jne  non_zero
  24.  write_digit_loop:
  25.   pop  dx     ;(9)
  26.   call write_hex_digit ;(10)
  27.   loop write_digit_loop
  28.   pop bx
  29.   pop dx
  30.   pop cx
  31.   pop ax
  32.   ret
  33. write_decimal endp
  34.  

  Алгоритм простой: пока частное не равно 0, делим его, делим и еще раз делим на 10d, запихивая остатки в стек. Потом - извлекаем из стека. Вот и вся "конвертация" из HEX в BIN :smile3:. Это если в двух словах.
  А если подробно, то вот что получается:
  Бряк 1 - подготавливаем делимое. Как уже говорилось, оно у нас задается неявно - обязательно через AX. А параметр у нас - через DX процедуре передается. Вот и перемещаем.
  Бряк 2 - это, собственно, делитель.
  Бряк 3 - очищаем CX. Он у нас будет в качестве счетчика. О нем мы еще поговорим.
  Бряк 4 - очищаем DX. Если не очистим, то мы не 1234h какое-нить на 10 делить будем, а 12341234h. Первое 1234 нам надо? Вот и я говорю - очищаем!
  Бряк 5 - делим! Частное - в AX, остаток - в DX.
  Бряк 6 - заносим остаток (DX) в стек ;).
  Бряк 7 - CX=CX+1. Это мы считаем сколько раз "щемили остаток", который "щемится", по кругу (прыжок на метку non_zero), пока AX не равно 0 (бряк 8). То есть делим, делим AX, пока он не окажется таким, что делить, собственно, нечего.
  Так-с... Деление закончено, число раз, которое мы поделили AX до его полного обнуления, хранится в CX.
  Дальше все просто. Нам нужно такое же количество раз (CX) извлечь значение (DX) из стека. И это будет "HEX", переведенный в DEC. (Оно же: число в двоичном коде, разобранное на последовательность десятичных цифр).
  Помните, как у нас организуется цикл? Через loop и CX?
  Если бы в качестве счетчика мы использовали какой-нибудь другой регистр, то пришлось бы извращаться со всякими метками и прыжками... а так все просто, все продуманно :smile3:. Цикл, в теле которого ИЗВЛЕЧЬ ЦИФЕРКУ (бряк 9) и НАПЕЧАТАТЬ ЦИФЕРКУ (бряк 10). Столько же раз, сколько мы и делили наше исходное шестнадцатеричное число.

  Для тех, кто не понял: бряк - это брекпоинт. Для тех, кто еще не знает, что такое брекпоинт - ищите объяснение в 'DZebug. Руководство юZверя'

  #4.Тестируем!!

Код (Text):
  1.  
  2. testing proc
  3.   mov  dx,12345d
  4.   call write_decimal
  5.   call exit_com
  6. testing endp
  7.  

  Двое из десяти "подопытных" чайников (есть такие) возмутились:
  - В DX же только четыре циферки влазят!
  - Ага! Аж два раза четыре!

Код (Text):
  1.  
  2. mov  dx,'DZ'
  3.  

  Как видите, туда еще и две буковки "влазят" ;)
  А то и все четыре, если кавычки считать...

  Медитируйте!!

2.5. Hello, world! или Изврат-2

  #1. Мы уже писали программу "Hello, World!" с использованием 13-й функции 10 прерывания. Посмотрите на ее исходник...
  Сегодня мы слабаем еще одно "Hello, World!", но уже несолько другим способом. Какой из этих способов более дZенский - решайте сами ;).
  Для начала мы создадим блок данных после всех-всех-всех процедур но между директивами начала и конца сегмента.
  Блок данных будет выглядеть следующим образом:

Код (Text):
  1.  
  2. abc db 'Hello, World-2$'
  3.  

  Вы должны спросить "А почему мы хотим напечатать Hello, World-2, а в конце строки у нас 2$? $ - это что? World за баксы продавать, штоль?? Да ну вас...
  Эту строчку мы будем выводить на монитор особым дZенским способом - посимвольно. То есть: возьмем первый символ из блока данных, выведем, потом второй и т. д. аналогично пока не встретим символ '$'.

  Опять-таки, это если в двух словах. Но ведь наверняка вам этого покажется мало ;).

  "Щемить" символы мы будем двумя способами. (Тут я хотел было написать "неправильным" и "правильным", но потом передумал и решил обозвать их "первым" и "вторым").
  В алгоритм первого способа вы и сами без труда въедете (я только "рабочую часть" приведу, пуши с попами сами проставляйте):

Код (Text):
  1.  
  2. ...
  3. next:
  4.  mov dl,[BX]
  5.  cmp dl,'$'
  6.  je finish
  7.  call write_char
  8.  inc BX
  9.  jmp next
  10. finish:
  11. ...
  12.  

  #2. А вот второй способ немножко навороченнее :smile3:. Чтобы в нем разобраться, мы сначала познакомимся со следующей группой команд: LODSB, LODSW, LODSD
  Их назначение - это загрузка элемента из последовательности (строки, цепочки) в регистр-аккумулятор al/ax/eax.
  Для тех, кто не понял - наша строчка "Hello, World-2$" как раз и является "последовательностью/строкой/цепочкой" из элементов размером в байт.
  Адрес цепочки передается через ds:esi/si, сами "элементы" (фиксированной ширины) возвращаются в al (байт, команда LODSB), ax (слово, команда LODSW) или eax (двойное слово, команда LODSD). В общем, последние буковки команд как раз и указывают на размерность элемента: [B]yte, [W]ord, [D]ouble word. Т. е. размер мы определяем неявно.
  После выполнения одной из этих команд значение регистра si изменяется на величину, равную длине элемента, но... хм...

  Пришло время еще одну большую тайну познать, братья. В справочнике Юрова написано, "знак этой величины зависит от состояния флага df:
  df=0 — значение положительное, то есть просмотр от начала цепочки к ее концу;
  df=1 — значение отрицательное, то есть просмотр от конца цепочки к ее началу."
  Обидно, да? Флаги-то мы с вами еще не расколупали... Чего-ж дальше-то делать, а?

  Как что? "Пропивать" стандартную процедуру и колупать ее, колупать, колупать, ногами ее, ногами, и по морде, по морде, по морде...

Код (Text):
  1.  
  2. ;-[write_string, v1]-------------------------------------
  3. ;печать строки символов на мониторе.
  4. ;(Строчка оканчивается символом '$'
  5. ;на входе: ds:dx - адрес строки
  6. ;на выходе: нихрена
  7. ;прерывания: ан нату
  8. ;процедуры: write_char
  9. ;--------------------------------------------------------
  10. write_string proc
  11.   push ax
  12.   push dx
  13.   push si
  14.   pushf              ;(1)
  15.   cld                ;(2)
  16.   mov  si,dx         ;(3)
  17.  string_loop:
  18.   lodsb              ;(4)
  19.   cmp  al,'$'        ;(5)
  20.   jz   end_of_string ;(6)
  21.   mov  dl,al         ;(7)
  22.   call write_char    ;(8)
  23.   jmp  string_loop   ;(9)
  24.  end_of_string:
  25.   popf               ;(10)
  26.   pop  si
  27.   pop  dx
  28.   pop  ax
  29.   ret
  30. write_string endp
  31.  

  Итак, что делает команда lodsb, вы уже поняли. А вот с df=0/df=1 пока что непонятки.
  Будем разбираться.

  Нам нужно, чтобы "просмотр цепочки" осуществлялся командой lodsb слева направа, для этого нужно установить значение df=0. Делаем мы это при помощи команды cld (бряк 2).
  Если нам нужно, чтобы "просмотр цепочки" осуществлялся справа налево, мы используем команду std. (Можете попробовать. Ерунда получится.)

  А теперь вспомним "золотое правило". Все регистры, которые мы изменяли ВНУТРИ процедуры, "на выходе" должны восстанавливать свои ПРЕДЫДУЩИЕ значения (кроме тех регистров, через которые мы возвращаем РЕЗУЛЬТАТ).
  Так вот: то же самое касается и флагов, которые мы изменяем.
  Для регистров мы использовали команды PUSH и POP. Для регистра флагов (изменять у которого мы можем только БИТЫ) используются команды pushf (записать в стек значения флагов, бряк 1) и popf (извлечь из стека, бряк 10).

  ПРИМЕЧАНИЕ: Это несколько вольное положение (хотя, вобщем-то, верное). Видимо, здесь следует хотя бы добавить, что сохранять/восстанавливать флаги нужно при изменении специальных флагов (if, df). (С) Хемуль

  Теперь колупаем дальше... (С) Serrgio

  Бряк 3. Мне удобнее передавать данные "в процедуру" через регистр DX, о чем и написано в заголовке. А lodsb (бряк 4) хотит, чтоб адрес ему в SI подавали. Удовлетворим его желание :smile3:
  Бряк 4. После выполнения этой команды ASCII-код первого символа "цепочки" "Hello, World-2$" помещается в AL, а значение регистра SI увеличивается на 1 и указывает теперь на второй элемент цепочки (символ 'e').
  Бряк 7. Удовлетворяем "пожелания" процедуры write_char. Из AL переносим в DL и печатаем (бряк 9) write_char'ом.

  Тэкс... Мы пропустили бряки 5 и 6-й...
  Смотрите: на бряке 9 мы "безусловно" зацикливаем "извлечение" и печать элементов цепочки. Безусловно - это значит до потери пульса. Чтоб этого не произошло, каждый из элементов цепочки мы сравниваем с символом-конца-цепочки (в нашем случае это '$', но можно использовать и любой другой). Если текущий символ равен символу-конца-цепочки, то выпрыгиваем (бряк 6) из этого безусловного цикла, восстанавливаем статус-кво (бряк 10) и все на этом...

  #3. Тестируем!!

Код (Text):
  1.  
  2. testing proc
  3.   lea dx,abc
  4.   call write_string
  5.   call exit_com
  6. testing endp
  7.  
  8.  

  Напечаталось то, что надо?
  Если true - читайте дальше.
  Если false - расколупывайте и медитируйте до полного просветления...

Диагноз

  Когда-то автор этого текста хотел осветить все вопросы низкоуровневого программирования, начиная от процессора 8086 до p3, от DOS'а до W2000 и линуха. Однако, практика показала, что при таких черепашьих темпах написания... к тому времени когда дойдет очередь хотя бы до w3.11 - windows-2000 уже станет вчерашним днем :(. Поэтому этот туториал, мягко говоря, не совсем завершен ;). © Serrgio / HI-TECH


0 3.309
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532